Design Patterns and Video Games

2D Strategy Game (22): Armies

I add the support of armies in the game. Using this feature, we can gather several units in the same cell, and move them all at once. The plan is to disable combat in the world map (with cities etc.) and allow it in a tactical view (like Age of Wonders for instance).

This post is part of the 2D Strategy Game series

Game state

I add a new Army class to the game state (core.state.item package), as a child of Item:

Items in game state with armies

As for the other children of Item, it can be associated to a cell in a game state Layer. In practice, and as for Unit, I only use it in the units layer.

I add a couple of commodity methods to the Army class:

Game logic

I update the DistanceMap in core.logic to compute the distance map (cf this post) with armies support. I want to limit the number of units an army can have. As for other game rules, I add a new getArmyMaxSize() method in the Rules class, which returns the maximum number an army can have. Then, during the computation of the distance map, I forbid the use of cells where the limit is exceeded:

# Somewhere in the compute() method of DistanceMap:
items = world.units.getItemsArea(area)
for itemCells in items:
    item = itemCells.item
    block = False
    if item.playerId != playerId:
        block = True
    elif isinstance(item, Unit):
        if (armySize + 1) > maxArmySize:
            block = True
    elif isinstance(item, Army):
        size = len(cast(Army, item))
        if (armySize + size) > maxArmySize:
            block = True
    else:
        logging.warning(f"Unsupported unit type {type(item)}")
        block = True
    if block:
        for cell in itemCells.cells:
            p = cell[0] - area[0], cell[1] - area[1]
            nodes[p] = MoveCost.INFINITE

We first get all the items in the area covered by the distance map (line 2). Then, for each item found (line 3), we compute a block boolean which, if set as True, disables all the cells used by the item (lines 18-21). This occurs if the item does not belong to the current player (lines 6-7), if the item is a unit and the size of the army to move is already to the maximum (lines 8-10), or if the item is an army, and the two armies are too large (lines 11-14).

I update the MoveUnit class in core.logic.command.unit, which moves a unit or an army. For computing the move points an army can use, I use the getLowestIntProperty() in the Army class. Then, I need to compute the list of units that move (stored in toUnits) and the list of units that stay (stored in fromUnits). To distinguish the two unit cases, I have a selection list with all the units that move (provided by the caller of the MoveUnit class, for instance, the UI). First of all, as usual, I am paranoid: I don't trust the caller, and I want to be 100% sure that all the units to move are actually in the army (e.g., I don't do a toUnits = selection).

A naive way to compute the units in the selection is:

toUnits = [unit for unit in army.units if unit in selection]   # Not good in our case !

However, this does not work! In fact, it will collect all units with the same reference or with the same content (e.g. __eq__() returns True). In other words, it does something like:

# Equivalent to [unit for unit in army.units if unit in selection]: not what we want !
toUnits = [unit for unit in army.units if any(unit is selected or unit == selected for selected in selection)]

It means that, if there is a single pikeman in the selection, then all pikemen of the army are moved, instead of only the selected one.

A solution is to manually perform the search in the selection list and only use the is operator, which returns True if the two objects are the same:

toUnits = [unit for unit in army.units if any(unit is selected for selected in selection)]  

We can similarly compute the opposite:

fromUnits = [unit for unit in army.units if not any(unit is selected for selected in selection)]

Note that there is no none() operator in Python, so we must use not any().

User interface

I create a new frame for armies:

Army frame with armies

The frame presents units in the army, from the first to the last one. It uses the anchor system: the first one is anchored to the frame title, the second one to the first one, etc. Each unit is presented using a new control class Toggle (in ui.component.control) which is similar to Button, except that if switches between an "enabled" (the icon is colored) and a "disabled" state (the icon is grayed). Thanks to these controls, the player can (un)select units in the army. In the following example, the first and third units are selected:

Army frame with selected units

Finally, I update a bit the WorldGameMode class in ui.mode to handle armies.

Final program

Download code and assets