Step 4: Making a Game Class

Page content

You will need to make a Game class. Yours can look a lot like the one that comes with SampleGame called T2Game.py

In this entry we will dissect the example given game class, T2Game.py, in order to better understand what’s required in a SkeletonGame’s main class. Before we get too deep, it’s worth noting that a familiarity with Object Oriented concepts is very valuable here, as is some experience with Python. You can pick up everything you need from a few nights going through the Python tracks on Code Academy.

If you investigate the SampleGame folder you will see two files: config.yaml (discussed in a previous entry) and T2Game.py –the latter defines the “main” class of the Sample Game, and is the place to start in understanding how this game works and what is expected in your own game. Some good additional Python resources are this wikibook, and this overview of object orientation in Python.

Imports

The first few lines are imports; they include code written by others for use into your program. Some are included with python (e.g., logging) and others are part of the PyProcGame/PyProcGameHD/SkeletonGame framework (e.g., anything starting with procgame)

import logging
import procgame
import procgame.game
import procgame.dmd
from procgame.game import SkeletonGame
from procgame import *
import os
from procgame.modes import Attract
from procgame.game.skeletongame import run_proc_game

Nothing is wildly special in the above. In the next block, you’ll find the imports of python modules that define “Modes” –Modes are important, but let’s table that until we get through this file.

# these are modes that you define, and probably store in
# a my_modes folder under this one....
import my_modes
from my_modes import BaseGameMode, ExBlankMode 

from my_modes.SkillShotMode import SkillShotMode
from my_modes.leftToRight import bottomLampsLeftToRight

The above imports do a few things: the first imports my_modes which is a folder under SampleGame, and ultimately a package in Python lingo. In Python, the execution of this line causes Python to look a definition of a module of that name, and that search starts from the current folder, looking for a file of that name, or a package of that name. What python finds is a folder, so within that folder, Python is looking for a file called __init__.py which will be executed when that package is imported. That file can include seperate import lines to define “All” the files in that sub-folder, but importing all the files in that folder is not automatic.

The next line imports two specific classes from my_modes –those examples will be the ones we dissect in the next entry.

The next two lines give examples of how to import a new Mode that you create if you do not wish to modify the __init__.py; the general for there is:

from <folder>.<file> import <class_name>

Reading the Python docs about modules and packages should shed more light here, if you are so inclined.

Logging

The next line sets up the logging level and format.

# set up a few more things before we get started 
# the logger's configuration and format
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
curr_file_path = os.path.dirname(os.path.abspath( __file__ ))

Logging level INFO produces a considerable amount of output to the console, but actually not as verbose as is possible. The levels are listed here, and a good tutorial on using Logging (which is worlds better than just using print is availble here).

The curr_file_path line simply lets the SkeletonGame code know where in the system the current file is located for relative path loading.

The Game Class

Now we get to the meat of things. The Game class is the main game object. It “owns” all the attributes of the physical machine (.coils, .lamps, .switches) and can communicate with these components via the P-ROC interface. In addition the Game class owns the state of the active game, including (among other things) the “Player” (.current_player()), the list of players (.players), the ball number (.ball), and the Mode Queue (.modes). The Game class also is the home of the display controller (which we typically don’t access directly), and the sound controller (.sound),

class T2Game(SkeletonGame):

Some understanding of Object Orientation goes a long way. Our game is a sub-class of SkeletonGame, which means it includes all the fields (i.e., variables) and methods (i.e., functions) of the SkeletonGame class. SkeletonGame is a subclass of BasicGame, which is in turn a sub-class of GameController.

    # constructor for the game object; called once
    def __init__(self):

        # THESE MUST BE DEFINED for SkeletonGame
        self.curr_file_path = curr_file_path
        self.trough_count = 3

In the code above, the __init__(self): method is defined, which is the T2Game class constructor –this is the function that gets called when a new instance of the T2Game class is created. That means, typically, this function is run once, when your game is executed. If you “peek ahead” to the bottom of the file, you will see where your game is constructed.

The rest of the constructor defines the switches that will start out as “automatically closed”, and invokes the __init__() method of the super-class (that is, calls the __init__ method defined in SkeletonGame); that code sets up a lot of stuff for us, and it’s nice that you don’t have to write that code. :)

        # optional definition for 'auto-closed' switches
        self.osc_closed_switches = ['trough2','trough3']

        # call the super class which makes the game 
        # the so-called 'machine yaml' file must meet the following requirements:
        #    name the shooter-feeding coil 'trough'
        #    name trough switches numbered left-to-right trough1, trough2, trough3
        #    name the shooter lane switch 'shooter'
        super(T2Game, self).__init__('config/T2.yaml', self.curr_file_path)

The next few lines instantiate the Modes that manage the behavior of the game. When you add a new mode, you’ll need to add a line like the following:

        self.base_game_mode = BaseGameMode(game=self)
        self.blank_mode = ExBlankMode(game=self)
        self.skill_shot_mode = SkillShotMode(game=self)
        self.left_right_mode = bottomLampsLeftToRight(game=self)

If you’d like to add any global helpers, notice that the leftTargetLamps is defined just to be helpful, creating an array of the lamps corresponding to the left five targets, accessible through the game instance.

        # this is also a reasonable place to setup lists of lamps, switches, drivers, etc.
        # that might be useful in more than one mode.
        self.leftTargetLamps = [ self.lamps.target1, 
                        self.lamps.target2, 
                        self.lamps.target3,
                        self.lamps.target4,
                        self.lamps.target5]

The last thing the game __init__() needs to do is call .reset(); the definition of reset immediately follows.

        # call reset (to reset the machine/modes/etc)
        self.reset()

    # called when you want to fully reset the game
    def reset(self):
        # EVERY SkeletonGame game should start its reset() with a call to super()
        super(T2Game,self).reset()

        # EVERY SkeletonGame game should end its reset() with a call to start_attract_mode()
        self.start_attract_mode() # plays the attract mode and kicks off the game

The .do_ball_search() method, shown below, is called by the framework when a ball search is required. Your methods should actually pulse coils in order to release stuck balls. This example is far from ideal since it doesn’t have any delay

    def do_ball_search(self, silent=False):
        """ If you don't want to use the full ball search mode
             --e.g., you can't figure out how to tag your yaml when you port 
                this to your own game, 
            you can use this much simpler ball_search implementation.
        SkeletonGame will default to calling this method if the other ball_search 
        is disabled in your config.yaml or if your machine yaml doesn't provide 
        enough info for ballsearch to work. """

        super(T2Game, self).do_ball_search(silent) # always start by calling this
        # this increases self.ball_search_tries; which you may want to check to
        # escalate the 'level' of your search.

        # this strategy is fire any coil that has the same name as a switch
        # that's active right now; better would be to use ballsearch tags, etc.
        delay_time=0.0
        for sw in self.switches:
            if(sw.name in self.coils and (not sw.name.startswith('trough'))):
                if(sw.is_active):
                    self.delay(time=delay_time, handler=self.coils[sw.name].pulse)
                    delay_time+=0.5

        # alternatively we could do something like the below, 
        # but we would need to stagger the pulse calls                    
        # if(self.switches.outhole.is_active()):
        #     self.coils.outhole.pulse()
        # if(self.switches.lockTop.is_active()):
        #     self.coils.lockTop.pulse()
        # if(self.switches.lockLeft.is_active()):
        #     self.coils.lockLeft.pulse()
        # if(self.switches.ballPopper.is_active()):
        #     self.coils.ballPopper.pulse()

Last, and certainly not least, at the bottom of the file:

## the following just set things up such that you can run Python ExampleGame.py
# and it will create an instance of the correct game objct and start running it!

if __name__ == '__main__':
    # change T2Game to be the class defined in this file!
    run_proc_game(T2Game)

That is what is actually run when you type python T2Game.py, and it passes the class into the run_proc_game() function (defined in SkeletonGame) to create an instance of your game and run it.