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.
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)
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.
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()
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.
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()
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()
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()
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.
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.
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()
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.
Take a look at some of the included examples for ideas. See documentation for complete list of methods callable using udebs configurations.