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
I add a new Army
class to the game state (core.state.item
package), as a child of Item
:
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:
findRepresentativeUnit()
: returns the unit with the most common class in the army. For instance, if there are four pikemen and three bowmen, then the most representative unit is one of the pikemen. I use it later to choose which icon to display for an army.getLowestIntProperty(ItemProperty)
: returns the lowest integer value of a property found in each unit of the army. For instance, if we choose ItemProperty.MOVE_POINTS, then it returns the lowest move points found in units: I use it later for getting the max points an army can use for moving.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()
.
I create a new frame for 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:
Finally, I update a bit the WorldGameMode
class in ui.mode
to handle armies.