The P-ROC software architecture is divided into 3 layers:
pyprocgame is a set of Python classes designed to provide a framework for implementing mode-based custom rulesets for pinball games. The classes make responding to abritrary and non-trivial switch events easy, as well as providing support for displaying graphics and text on the pinball display (DMD). pyprocgame is based on pypinproc, the Python wrapper for libpinproc.
This guide is written with the assumption that you are familiar with object-oriented programming and, to a lesser extent, the Python programming language. Terminology such as object, class, and subclass are used frequently within this guide and having a basic understanding of what those terms mean is important. There is a significant amount of demo code available for pyprocgame; you should not hesitate to examine it or post to the appropriate forums if you have questions.
pyprocgame provides a powerful set of classes to help you implement your game. The following are the base classes which are used in every game:
In addition, pyprocgame provides a number of classes to make controlling the dot matrix display (DMD) of your game much easier:
These classes will be described in greater depth in the sections that follow.
Fortunately you won’t need to understand all of those classes in order to build a pinball game with pyprocgame, but you will need is a basic understanding of how the GameController and ModeQueue work.
Let’s look at a ridiculously simple game implemented with pyprocgame:
import procgame game = procgame.game.GameController(machine_type='wpc') game.load_config('mygame.yaml') game.enable_flippers(enable=True) game.run_loop()
This particular game isn’t much fun, but it’s a good way to demonstrate what a pyprocgame program looks like from the very highest level. Let’s see what’s happening line-by-line:
Our first step is to import the pyprocgame module (called “procgame” in the context of Python). This particular program assumes that pyprocgame is in your sys.path. If it’s not, you will need to modify sys.path.
game = procgame.game.GameController(machine_type='wpc')
Next we create a new GameController object. This is the central object in your pinball game. It maintains collections for all of the switches, lamps and coils, as well as players in the current game. It also contains a ModeQueue, which we’ll cover later. (If this were an actual full-blown pyprocgame program we would create our own subclass of GameController.)
Note that the connection to the P-ROC hardware is established in the constructor for GameController and the hardware is reset to obtain a known state. We pass the machine_type value as 'wpc' in order to initialize P-ROC to the proper settings for controlling a WPC driver board.
Here we load a YAML file that describes the pinball hardware. The P-ROC software uses YAML files (a “human-friendly data serialization standard”) to describe the machine that the P-ROC hardware is connected to (see Machine Configuration Files for a complete description of these files). This statement loads the configuration and configures all of the switches, lamps and coils, as well as the flippers so that we can...
It wouldn’t be pinball without flippers; here’s where we turn them on. The pyprocgame code behind this statement uses the machine description (from the YAML file previously loaded with load_config()) to create the association between the flipper buttons (switches) and the flipper coils.
Internally, this takes advantage of P-ROC’s switch rules feature, which enables a hardware-triggered linkage between switch events and coil drivers to guarantee that when the player hits the flipper button the coil will be fired immediately. This keeps P-ROC-based games responsive, rather than suffering from any latency between the computer host processing of the switch event and activating the coil driver. The same principle can be applied to pop bumpers.
Finally we start the game’s run loop, which allows the game to actually run. The run loop checks for events from the P-ROC hardware and sends them to the ModeQueue so that they can be responded to by your game code. This method call is blocking and does not return until program execution is interrupted (usually by a Ctrl-C).
Most pinball games are a bit more sophisticated than just hitting the flippers. You usually have targets to hit, banks of drop targets to knock down, and so on. In the abstract those features seem pretty easy to implement: respond to the switch event and award points. But what about more complex rulesets? Multiball? Stacked multiballs? Things can get complicated quickly!
When we were designing pyprocgame our goal was to enable the developer (that’s you) to create rulesets that are as complicated as they can imagine while keeping the task of implementing (and debugging) those rulesets as sane as possible. Just like you, we want to design our own games, and we want to have fun doing it.
To reiterate the above, we designed pyprocgame to be flexible enough to allow you to create any game ruleset you can imagine, yet provide enough of a framework to help you get off the ground quickly. We’ve strived to keep the features modular and limit interdependence so that if, for example, you want to write your own routines to control the DMD you can do so, or if you want to create your own mode system you can replace ours and still take advantage of the Python interface to libpinproc and the DMD utilities.
We’ve been talking about pyprocgame at a very high level, but let’s get down to specifics for a moment:
Mode objects are the building blocks of pyprocgame games. In pyrpocgame a mode is a functional subset of a game that receives switch events. When active, modes are organized in a queue (ModeQueue), which determines the order in which they receive switch events. That is, when the GameController‘s run_loop() receives a switch event from the P-ROC hardware, only objects in the ModeQueue will be notified of the event. If you want your game to react to a switch event, one or more of your modes must be given that responsibility.
We subclass Mode to create our own useful modes. Let’s look at a simple mode:
class FirstMode(procgame.game.Mode): def __init__(self, game): super(FirstMode, self).__init__(game=game, priority=5) def sw_startButton_active(self, sw): print("Start!") return procgame.game.SwitchStop
Here we have defined a class, FirstMode, which subclasses the procgame Mode class. The Mode constructor takes 2 parameters. game is a reference to an instance of our own GameController subclass, and priority governs the order in which this mode will receive events, relative to the others – more on that later.
Next we define a method with a rather distinctive name: sw_startButton_active(). This is our switch event handler. When a Mode is instantiated its method list is scanned for methods that match a certain naming pattern: sw_(switch name)_active in this case. This tells pyprocgame that it should call this method when the button named startButton is active (closed in this case; this is configurable for each switch using the YAML file).
Similarly, a method named sw_trainWreck_inactive() would be called when the trainWreck switch had changed to an inactive state. The switch name in these method names must correspond to a switch name in the YAML configuration; otherwise a warning message will be printed when instantiating the class. More on switch even handlers (including responding to events after a delay) later.
Our switch handler in this case is very simple. It prints out a message and returns procgame.game.SwitchStop. Each switch event handler must return SwitchStop or SwitchContinue. A return value of stop instructs ModeQueue to stop processing this event; a return value of continue tells the ModeQueue to allow this switch event to be sent to other active modes. If you do not explicitly return a value from a switch handler method the behavior will be the same as if SwitchContinue had been returned.
Previously switch handlers returned True or False to indicate SwitchStop or SwitchContinue, respectively. This practice has been superseded by these constants for clarity. They are backward compatible.
This is where the priority of a mode becomes important. The ModeQueue is essentially a priority queue: the highest-priority modes receive switch events first. If the switch handler returns continue the switch event is then sent to lower priority modes. In this way you can use a high priority mode to give switches on the playfield to have special meaning during any number of modes, without having to handle that special case alongside the code for the more normal meaning of the switch. Or you can easily have a switch result in multiple mode triggers.
Now that we have a mode, how do we add it to the ModeQueue so that it will receive events? Let’s create a more mature example game by subclassing GameController, assuming our FirstMode class is defined elsewhere in the file:
class ExampleGame(procgame.game.GameController): def __init__(self, machine_type): super(ExampleGame, self).__init__(machine_type) self.load_config('mygame.yaml') def reset(self): super(ExampleGame, self).reset() first_mode = FirstMode(self) self.modes.add(first_mode) self.enable_flippers(enable=True) game = ExampleGame(machine_type='wpc') game.reset() game.run_loop()
We’ve reorganized the code a bit to reflect the recommended layout for pyprocgame games. First we moved the configuration loading to the constructor, and added an override for procgame.game.GameController.reset(), which is called to reset the state of the game and the hardware. Because the ModeQueue (self.modes in this context – every GameController has a ModeQueue at self.modes) is cleared by reset(), we can simply add an instance of our mode at this point.
In some cases you may wish to respond to a switch event only after the switch has been in that state for a certain time period. The Mode class provides a means for accomplishing this with incredible ease – just add a _for_(time period)_ suffix to the normal switch method convention:
You can schedule a method to be called after a specified delay using procgame.game.Mode.delay():
def sw_target1_active(self, sw): self.delay(delay=0.5, handler=self.delayed_event) return True def delayed_target(self): print("It's been 500 milliseconds!")
If you want to cancel a delay at a later time, store the return value from delay():
def sw_target1_active(self, sw): self.delayed_name = self.delay(delay=0.5, handler=self.delayed_event) def sw_target2_active(self, sw): # Cancel the previously-scheduled delay: self.cancel_delayed(self.delayed_name) def delayed_target(self): print("It's been 500 milliseconds!")
Mode subclasses can also implement the following methods to receive and respond to changes in state:
Modes can be very course-grained, such as a mode that controls all of multiball from start to finish (Multiball), or very fine-grained (MultiballActivate, MultiballRunning, MultiballJackpot, MultiballRestart). It’s up to you to determine how you want to lay out your modes.
Additionally, it’s important to note that modes do not need to correspond to modes on your playfield. You can create a Mode subclass and add it to the ModeQueue and use it for all sorts of things within your game: displays, timers, visual effects, service mode, initial entry, and so on.
We’ve spent a good amount of time talking about how to react to events within the game, but a huge part of pinball is affecting changes within the game: powering coils, turning lamps on and off, and pulsing flashers. Once you have a fleshed out YAML file for your machine, you can easily control individual elements of the game by accessing them within the GameController subclass. Since you’ll usually be making these changes from within switch handlers, we’ll show the examples in that context:
def sw_someButton_active(self, sw): self.game.lamps.startButton.schedule(schedule=0xff00ff00, cycle_seconds=0, now=True) self.game.coils.popper.pulse(50) self.game.lamps.shootAgain.pulse(0) # Turn on indefinitely.
pyprocgame uses configuration files in the YAML format. YAML is a human-readable structured text file format. Configuration files generally consist of a set of “keys” at the top level
Machine configuration files describe the physical components of a pinball machine: coils, lamps, switches, etc., and make it easier to refer to those components in code. The following is a subset a machine configuration file for Judge Dredd (JD.yaml):
PRGame: machineType: wpc numBalls: 6 PRFlippers: - flipperLwR - flipperLwL PRBumpers: - slingL PRSwitches: flipperLwR: number: SF2 flipperLwL: number: SF4 leftRampToLock: number: S63 type: 'NC' PRCoils: flipperLwRMain: number: FLRM PRLamps: perp1W: number: L11 perp1R: number: L12
System configuration files contain values common to all games, and values specific to the system being developed on, such as file paths. The configuration file is managed by the procgame.config module; you can retrieve values from the configuration using value_for_key_path().
The configuration file is located at ~/.pyprocgame/config.yaml. Note that the tilde (~) is a UNIX convention meaning the user’s home directory.
On Windows it can be tricky to determine your home directory. Luckily pyprocgame prints out the full path that it expects to find the config.yaml file at. Make sure that the path that pyprocgame prints matches where you placed your configuration file.
If you encounter difficulty creating a .pyprocgame directory in Windows, try using the command print: mkdir .pyprocgame. Yes, that’s “dot-pyprocgame”. Dot-files and dot-folders are common in UNIX-like systems. By default they are not shown in directory listings.
When creating your config.yaml file, be sure that its actual extension is .yaml, not .txt. Some components of Windows like to add a .txt extension when you are not expecting it.
An example config.yaml file follows:
font_path: - ~/Projects/PROC/shared/dmd - ~/Projects/PROC/my_fonts config_path: - ~/Projects/PROC/shared/config
font_path is used by font_named(), while config_path is used by config_named().
There are a lot of different ways one could run a DMD with pyprocgame, but here we’re going to talk about the recommended approach, which is well-integrated with the mode queue system. Let’s talk about how the P-ROC hardware works first. The P-ROC board provides a three hardware frame buffers, displaying them in order as new frames are provided by the software. This helps keep the display smooth to avoid hiccups caused by operating system scheduling variances. Much like a switch event, P-ROC sends a DMD event when it’s ready to display another frame. So if we send the next frame whenever we see this event, we can keep P-ROC’s frame buffers full and maintain smooth, skipless video.
The DisplayController class makes this pretty easy. Here’s how we incorporate it into our GameController subclass:
class DemoGame(game.GameController): def __init__(self, machine_type): super(DemoGame, self).__init__(machine_type) self.dmd = dmd.DisplayController(self, 128, 32) def dmd_event(self): self.dmd.update()
That’s great, but how do we tell the DisplayController what to display? Every time DisplayController.update() is called it traverses the mode queue and asks each mode if it has a DMD frame to display. If it does, it composites it upon the frames of lower priority modes. Once it has the final frame assembled it is uploaded to the P-ROC hardware.
Note the order in which the frames are composited: frames from lower priority modes are overwritten by higher priority frames. So imagine that you have laid out your modes like this:
- Priority 1 (low): General game play mode. Provides a frame showing the score.
- Priority 5 (medium): “Hurry-up” mode. Provides a frame showing the hurry-up countdown and jackpot value.
If you’ve been thinking about how you’d organize your modes already, this is the sort of pattern that you should follow for switch events. More specialized modes get first crack at the switch events due to their priority. This pattern also works well with DisplayController: the hurry-up information is shown to the player when that mode is active; otherwise the score is shown.
How does the mode supply the DMD frame to DisplayController, though? To explain that we first need to introduce the Layer class, which provides a sequence of frames via its method next_frame(). There are a number of useful Layer subclasses provided with pyprocgame:
- FrameLayer: Provides an endless sequence of one frame (dmd.Frame).
- AnimatedLayer: Provides an ordered sequence of dmd.Frame objects.
- TextLayer: Uses a dmd.Font to display a text string to the user.
- GroupedLayer: Composites the output of multiple Layer subclasses into one common output. This can be used to create complicated displays with numerous subcomponents.
- ScriptedLayer: Runs a simple “script” (dictionary) to display a sequence of layers, showing each layer for a specified amount of time.
DisplayController checks for an attribute on each Mode class called layer. If the mode has a layer, the next_frame() from that layer is used; otherwise it is ignored. Let’s add a layer to an example mode:
class HurryUpMode(game.Mode): def __init__(self): super(HurryUpMode, self).__init__(priority=5) self.layer = dmd.TextLayer(x=128/2, y=8, font=my_font, justify="center") def update_countdown_display(self, seconds): self.layer.set_text('%d seconds' % (seconds));
To be written.