Udebs -- A discrete game analysis engine for python

Udebs is a game engine that reads in rules from an xml configuration enforces those rules in a single python class. The engine is useful for a number of purposes.

  1. Allowing a programmer to implement a game by focusing on it's rules and not how they should be enforced.
  2. Allows other programs to explore the state spaces of a game without worrying about entering illegal states.
  3. Allow easy modifications to the rules of a game without worrying about breaking the entire game.

So let's work through an example by building a game of tic tac toe and see what udebs can do.


In [1]:
import udebs

In [2]:
game_config = """
<udebs>
    <config>
        <logging>True</logging>
    </config>

    <entities>
        <xplayer />
        <oplayer />
    </entities>
</udebs>
"""

In [3]:
game = udebs.battleStart(game_config)


INITIALIZING Unknown
Env time is now 0

The above snippet of code is a minimal example of how to initiate a udebs game instance.

We have created a game that contains two objects. The xplayer and the yplayer. Unfortunatly, neither of these objects can do anything yet, but we will fix that soone enough.

As well, it's important to note that by default udebs logs every action the game engine takes. We can turn that off by setting logging to False in the configuration.

Now let's actually build out a playable game.

Game One: Actions


In [4]:
game_config = """
<udebs>
    <config>
        <name>tictactoe</name>
    </config>

    <map>
        <dim>
            <x>3</x>
            <y>3</y>
        </dim>
    </map>

    <definitions>
        <strings>
            <token />
        </strings>
    </definitions>

    <entities>
        <x />
        <o />

        <xplayer>
            <token>x</token>
        </xplayer>

        <oplayer>
            <token>o</token>
        </oplayer>

        <placement>
            <effect>($caster STAT token) RECRUIT $target</effect>
        </placement>

    </entities>
</udebs>
"""

In [5]:
game = udebs.battleStart(game_config)

game.castMove("xplayer", (1,1), "placement")
game.castMove("oplayer", (0,0), "placement")

game.printMap()


INITIALIZING tictactoe
Env time is now 0

xplayer uses placement on (1, 1, 'map')
x1 has been recruited
x1 has moved to (1, 1, 'map')

oplayer uses placement on (0, 0, 'map')
o1 has been recruited
o1 has moved to (0, 0, 'map')

o1 _  _  
_  x1 _  
_  _  _  

So now we have added a few important elements to our game.

The first is that we have defined a board that the game can be played on. The "map" attribute defines a 3 x 3 square grid that our game can be played on. Alternativly, we could define a hex grid by setting the type attribute on the map tag (\<map type="hex">).

Secondly we created an action that the players can perform: placement. An action is a udebs object that has an 'effect' attribute. Actions are usually initated by another udebs entity onto a third one. The castMove method is the primary way that actions are performed. This method takes three arguments, [ caster target action ]. The caster and target are stored in the caster and target variables respectivly and can be accessed in an actions effect.

(udebs has two other methods for initiating actions. castInit and castAction. CastInit is used when the action just activates and there is no caster or target. CastAction is useful when there is a caster but no target.)

Finally, we have also defined an attribute on the player objects. This attribute is a string that is simply a reference to another udebs object. In this case it is the token that each player places on the board.

Game Two: Time and requirements

Our game still has a bunch of problems. We currently do not enforce turn order, there is nothing stopping a player from playing in a non empty square, and we have no way of knowing when the game is finished and who won.


In [6]:
game_config = """
<udebs>
    <config>
        <name>tictactoe</name>
    </config>

    <map>
        <dim>
            <x>3</x>
            <y>3</y>
        </dim>
    </map>

    <definitions>
        <strings>
            <token />
        </strings>
        <stats>
            <act />
        </stats>
    </definitions>

    <entities>

        <!-- tokens -->
        <x />
        <o />

        <!-- players -->
        <players />

        <xplayer>
            <group>players</group>
            <token>x</token>
            <act>2</act>
        </xplayer>

        <oplayer>
            <group>players</group>
            <token>o</token>
            <act>1</act>
        </oplayer>

        <!-- actions -->
        <placement>
            <require>
                <i>($target NAME) == empty</i>
                <i>($caster STAT act) >= 2</i>
            </require>
            <effect>
                <i>($caster STAT token) RECRUIT $target</i>
                <i>$caster act -= 2</i>
            </effect>
        </placement>

        <tick>
            <effect>(ALL players) act += 1</effect>
        </tick>

    </entities>
</udebs>
"""

In [7]:
game = udebs.battleStart(game_config)

game.castMove("xplayer", (1,1), "placement")
game.castMove("xplayer", (0,0), "placement")
game.castMove("xplayer", (1,1), "placement")
game.printMap()


INITIALIZING tictactoe
Env time is now 0

xplayer uses placement on (1, 1, 'map')
x1 has been recruited
x1 has moved to (1, 1, 'map')
xplayer act changed by -2 is now 0

xplayer uses placement on (0, 0, 'map')
failed because ($caster STAT act) >= 2
xplayer uses placement on x1
failed because ($target NAME) == empty
_  _  _  
_  x1 _  
_  _  _  

To force turn order and to prevent playing in non empty squares we need to the concept of a requirement. A requirement is a condition that must be true for the action to trigger. If the requirements are not met udebs will treat it as an illegal action and refuse to trigger the action.

In this case we have defined a second attribute "act" that udebs will track. It is a numerical value or "stat". Then we added a requirement to our placement action saying that a player must have an act value of at least two in order to activate. Likewise we have also added a requirement that the placement be in an empty square. This will prevent a player from placeing in a spot that has already been played in.

Note: the \<i> tags are useful in effects and requirements when more than one action must be taken.

As shown, the xplayer tries to play twice in a row. Since the player does not have enough act to move twice udebs refuses to perform the second action. In the third action the player tried to play in a square that already had been played in. Udebs also refused to act on this action.

We must also create a method for increasing a players act after every play. To do this we will use udebs built in timer.

We defined a new action called tick which is a special action that is triggered every time the in game timer increments. This action will increment the act of every object in the group "players". To trigger the in game timer we must simply use the udebs method controlTime.


In [8]:
game = udebs.battleStart(game_config)

game.castMove("xplayer", (1,1), "placement")
game.controlTime()
game.castMove("oplayer", (0,0), "placement")
game.controlTime()
game.castMove("xplayer", (0,1), "placement")
game.printMap()


INITIALIZING tictactoe
Env time is now 0

xplayer uses placement on (1, 1, 'map')
x1 has been recruited
x1 has moved to (1, 1, 'map')
xplayer act changed by -2 is now 0

Env time is now 1
init tick
oplayer act changed by 1 is now 2
xplayer act changed by 1 is now 1

oplayer uses placement on (0, 0, 'map')
o1 has been recruited
o1 has moved to (0, 0, 'map')
oplayer act changed by -2 is now 0

Env time is now 2
init tick
oplayer act changed by 1 is now 1
xplayer act changed by 1 is now 2

xplayer uses placement on (0, 1, 'map')
x2 has been recruited
x2 has moved to (0, 1, 'map')
xplayer act changed by -2 is now 0

o1 _  _  
x2 x1 _  
_  _  _  

Game Three: Inheritance and Immutability

Before we talk about detecting the end of the game let's talk a little more about engine details.


In [9]:
game_config = """
<udebs>
    <config>
        <name>tictactoe</name>
        <immutable>True</immutable>
    </config>

    <map>
        <dim>
            <x>3</x>
            <y>3</y>
        </dim>
    </map>

    <definitions>
        <strings>
            <token />
        </strings>
        <stats>
            <act />
        </stats>
    </definitions>

    <entities>

        <!-- tokens -->
        <x />
        <o />

        <!-- players -->
        <players />

        <xplayer immutable="False">
            <group>players</group>
            <token>x</token>
            <act>2</act>
        </xplayer>

        <oplayer immutable="False">
            <group>players</group>
            <token>o</token>
            <act>1</act>
        </oplayer>

        <!-- actions -->
        <force_order>
            <require>($target NAME) == empty</require>
            <effect>($caster STAT token) RECRUIT $target</effect>
        </force_order>

        <placement>
            <group>force_order</group>
            <require>($target NAME) == empty</require>
            <effect>($caster STAT token) RECRUIT $target</effect>
        </placement>

        <tick>
            <effect>(ALL players) act += 1</effect>
        </tick>

    </entities>
</udebs>
"""

In [10]:
game = udebs.battleStart(game_config)

game.castMove("xplayer", (1,1), "placement")
game.controlTime()
game.castMove("oplayer", (0,0), "placement")
game.controlTime()
game.castMove("xplayer", (0,1), "placement")
game.printMap()


INITIALIZING tictactoe
Env time is now 0

xplayer uses placement on (1, 1, 'map')
x has moved to (1, 1, 'map')
x has moved to (1, 1, 'map')

Env time is now 1
init tick
oplayer act changed by 1 is now 2
xplayer act changed by 1 is now 3

oplayer uses placement on (0, 0, 'map')
o has moved to (0, 0, 'map')
o has moved to (0, 0, 'map')

Env time is now 2
init tick
oplayer act changed by 1 is now 3
xplayer act changed by 1 is now 4

xplayer uses placement on (0, 1, 'map')
x has moved to (0, 1, 'map')
x has moved to (0, 1, 'map')

o _ _ 
x x _ 
_ _ _ 

Some quick notes:

By default udebs assumes that any object could hold some information about the current game space. So when we used placement udebs created a copy of the x tile and placed it in the map. We can change this behaviour by explicitly telling udebs that the x and y tiles will never hold gamestate by creating them as immutable objects.

  1. The default assumption for immutablity can be set in the udebs configuration block.
  2. Individual entities can be set using a tag when defining the entity.

In our case, the only objects that hold state are the player objects. So we set all objects to immutable by default and explicitly set the player objects to mutable. This allows udebs to stop creating copies of the x and o tiles every time we place them.

In this example the only effect is that the printMap method stops showing numbers next to the tiles. However, for treesearch and other more intense processes the speedup can be considerable.

Secondly:

Udebs objects inherit properties from their group. So if we wanted to create several actions that would exhaust a players turn, they can all inherit from the force_turn object instead of writting the same effects and requires constantly in them all.

Game Three: Game Loop and Detecting Completion


In [11]:
def ENDSTATE(state):
    def rows(gameMap):
        """Iterate over possible win conditions in game map."""
        size = len(gameMap)

        for i in range(size):
            yield gameMap[i]
            yield [j[i] for j in gameMap]

        yield [gameMap[i][i] for i in range(size)]
        yield [gameMap[size - 1 - i][i] for i in range(size)]

    # Check for a win
    tie = True
    for i in rows(state.getMap().map):
        value = set(i)
        if "empty" in value:
            tie = False
        elif len(value) == 1:
            if i[0] == "x":
                return 1
            elif i[0] == "o":
                return -1

    if tie:
        return 0

# Setup Udebs
udebs.importFunction(ENDSTATE, {"args": ["self"]})

In [12]:
game_config = """
<udebs>
    <config>
        <name>tictactoe</name>
        <immutable>True</immutable>
    </config>

    <map>
        <dim>
            <x>3</x>
            <y>3</y>
        </dim>
    </map>

    <definitions>
        <strings>
            <token />
            <result />
        </strings>
        <stats>
            <act />
        </stats>
    </definitions>

    <entities>

        <!-- tokens -->
        <x />
        <o />

        <!-- players -->
        <players />

        <xplayer immutable="False">
            <group>players</group>
            <token>x</token>
            <act>2</act>
        </xplayer>

        <oplayer immutable="False">
            <group>players</group>
            <token>o</token>
            <act>1</act>
        </oplayer>

        <!-- actions -->
        <force_order>
            <require>($target NAME) == empty</require>
            <effect>($caster STAT token) RECRUIT $target</effect>
        </force_order>

        <placement>
            <group>force_order</group>
            <require>($target NAME) == empty</require>
            <effect>($caster STAT token) RECRUIT $target</effect>
        </placement>

        <tick>
            <effect>
                <i>(ALL players) act += 1</i>
                <i>INIT end</i>
            </effect>
        </tick>

        <end>
            <require>
                <i>score = (ENDSTATE)</i>
                <i>$score != None</i>
            </require>
            <effect>
                <i>(ALL players) result REPLACE $score</i>
                <i>EXIT</i>
            </effect>
        </end>

    </entities>
</udebs>
"""

In [13]:
game = udebs.battleStart(game_config)

game.castMove("xplayer", (1,1), "placement")
game.controlTime()
game.castMove("oplayer", (0,0), "placement")
game.controlTime()
game.castMove("xplayer", (0,1), "placement")
game.controlTime()
game.castMove("oplayer", (0,2), "placement")
game.controlTime()
game.castMove("xplayer", (2,1), "placement")
game.controlTime()
game.printMap()


INITIALIZING tictactoe
Env time is now 0

xplayer uses placement on (1, 1, 'map')
x has moved to (1, 1, 'map')
x has moved to (1, 1, 'map')

Env time is now 1
init tick
oplayer act changed by 1 is now 2
xplayer act changed by 1 is now 3
init end
failed because $score != None

oplayer uses placement on (0, 0, 'map')
o has moved to (0, 0, 'map')
o has moved to (0, 0, 'map')

Env time is now 2
init tick
oplayer act changed by 1 is now 3
xplayer act changed by 1 is now 4
init end
failed because $score != None

xplayer uses placement on (0, 1, 'map')
x has moved to (0, 1, 'map')
x has moved to (0, 1, 'map')

Env time is now 3
init tick
oplayer act changed by 1 is now 4
xplayer act changed by 1 is now 5
init end
failed because $score != None

oplayer uses placement on (0, 2, 'map')
o has moved to (0, 2, 'map')
o has moved to (0, 2, 'map')

Env time is now 4
init tick
oplayer act changed by 1 is now 5
xplayer act changed by 1 is now 6
init end
failed because $score != None

xplayer uses placement on (2, 1, 'map')
x has moved to (2, 1, 'map')
x has moved to (2, 1, 'map')

Env time is now 5
init tick
oplayer act changed by 1 is now 6
xplayer act changed by 1 is now 7
init end
oplayer result changed to 1
xplayer result changed to 1
Exit requested

o _ _ 
x x x 
o _ _ 

The game records that the game has ended by setting the 'cont' attribute on the main game object to false nothing else really changes about the game state. Actions will still work because we haven't told the system that it is illegal to place token after the game finishes. That is it for now however there are a ton of other things that you can do with the udebs system.

  • Create a "reset" object that will return the game to it's origional state using the resetState state method.
  • Easily revert actions or reset to a previous timestamp using the getRevert funtion.
  • Use the "treesearch" submodule to parse all possible legal gamestates.
  • Automatically increment the in game timer using the gameLoop method.

Take a look at some of the included examples for ideas. See documentation for complete list of methods callable using udebs configurations.