As a running tradition in the AuD class at OVGU, students are required to
participate in a bot writing competition for a game written by senior students.
For this year’s competition,
Terratician Expandoria

The game is a city-builder offering multiple modes of play. I will be focusing
on the ‘challenge’ mode, since that is what the main competition revolves
around. The goal is to survive the highest number of rounds, where the demand
of resources increases each round. Each tile, with the exception of grass and
stone tiles, has its own mathematical formula for its respective resource
production. This article does not only assume that the reader knows how to play
the game, but also that they are familier with such rules. More can be found
Design Decisions
The following are the design decisions I took when architecting the bot, they are numbered by the order of importance.
1. Mathematical Formulas Are More Accurate
The bot strictly assumes that the formulas of each tile is the most accurate description of the tile’s relation with other tiles. In order to fully describe what that means, consider the placement of a wheat tile as example. If we decide to have a bot where a set of ‘wants’ are to be fulfilled manually using a bunch of if-else statements, edge cases such as the tile’s proximity to Moai and therefore its propagating effect on the Money resource would be hard to correctly model. Moreover, it is close to impossible to describe the weights of the priorities of ‘wants’ in a conditional if-else manner. In order to avoid such complexities, my bot models the formulas provided in the docs for every tile manually, allowing for intricate placement decisions from the basic act of ‘maximising’ mathematical formulas.
Even though one would be fast to argue that considerations such as placing a grass tile beside a Maoi is better than beside a wheat is a very easy decision, more complex strategies are significantly easier to write when the base assumption is that we have a list of placement suggestions ordered by their greedy score maximisation. This fact is built upon by the next design decision.
2. Negative Rules, i.e. Unwants
Once we have established that ‘wants’ or in other words ‘positive rules’ are
hard to write code for, we can move on to a more systematic way of writing the
rules the bot will use. Consider the following code snippet for the rule
apply_wheat_groups_size():
| |
From its name, apply_wheat_groups_size() is a rule that enforces wheats to be
grouped with a maximum size of max. One can easily notice that the function
gives a judgement on a PlacementSuggestion, deciding whether that suggestion
breaks the rule or not. The name ’negative rules’ comes from the fact that the
function is rejecting a placement of the wheat tile (represented by
suggestion) if its group count is larger than max. Likewise, ‘positive
rules’ would mean that the suggestion is created by searching for wheat
placements on the map that make a group less than max. Notice, that the
reason this function works is that the asumption here is that we are judging
best PlacementSuggestions first. All rules in the bot are written in
’negative rules’ format. The next design decision will describe how the
suggestions are created.
3. Efficient creation of Placement Suggestions
The bot maintains a Map<CubeCoordinate, TileInfo> that represents the actual
world of the game. There is also an array for every tile type as duplicate to
the map to allow for quick access by type. Moreover, as there is no official
API that makes available the list of coordinates where one is allowed to place
tiles on, the bot also internally maintains a global (relative to the bot)
Set<CubeCoordinate> of placable coordinates. It is also important to keep in
mind that I have written classes for every tile type, each implementing a
function which calculates the tile’s score.
From the previously mentioned implementation details, the bot is able to generate a list of placement suggestions, calculate a score for each suggestion and sort them from highest delta resource growth to lowest.
Ofcourse, inorder for Design Decision 1 & 2 to work, we have to calculate the
scores of all tiles that are relevant to the current tile. Relevancy here is
whether the target tile affects another tile in the region. As an example here
are the functions affects() and in_range() of the WindmillRepr class:
| |
The previous functions describe that any Windmill tile production’s score is
always only affected by Wheat and Forest tiles that are in the area of 3 and 1
respectively. And that the Windmill tile itself only affects Marketplaces,
Moais and other Windmills. These functions are heavily used when calculating
any suggestion’s score, as the alternative would mean calculating a global
score (which is inefficient past round 4). The calculation is done in the
function get_relevant_score(TileInfo info, CubeCoordinate coord) of the
abstract Director class.
4. Game Directors
Since the implementation has a RuleSet class that holds all the apply_*
functions that represent all the different rules the bot can maintain, a
question arises: When, How, and Who controls the execution of the rules? Enter:
directors.
One may want to write ‘multiple’ bots each having a different strategy,
to ease that process, I have written an abstract class Director which
all ‘sub-bots’ extend from. The following is a simplified snippet of the
class:
| |
The methods are expected to be called directly from the executeTurn
function. The order is vaguely like so: get_preferred_tile() ->
ask_for_suggestions() -> pick() -> accept_suggestion().
The function suggest() is called by the classes that represent each tile to
submit a PlacementSuggestion to the Director.
Dictators and The Bot Development Platform
‘Dictators’ is the very technical term used internally to represent the classes
that extend the class Director.
Currently the available bots are:
GreedyDictator: Simply selects the best placementStrictDictator: Applies rules without disabling/parameter optimization.DynamicDictator: Rules are applied with paramenters that are tested extensively (method described later)
To show how easy it is to start developing bot strategies using this platform,
here is the entirety of the GreedyDictator bot code:
| |
If you are interested in the performance of GreedyDictator, here is a very
small analysis (n=35+2 known seeds):
| |
Even though the bot reaches round 11, it is very inconsistent with a standard
deviation of around 3. Because I believe a bot’s performance is not just about
its average round/resource count but also its standard deviation and highest
frequency count, I will not be making comparisons between GreedyDictator and
the performance of the average bot on the AuD scoreboards (Although I would be
very interested in the percentage of people that beat the greedy approach).
Beating the Greedy Approach & My Submission
In order to show how easy it is to describe ‘winning’ bots using my platform,
here is the entirety of my bot’s submission(DynamicDictator). Which has the
following performance (n=150+2):
| |
Admittedly, the results are very much dependendant on the random seeds. However,
if you run the DynamicDictator bot analysis multiple times, you would see a
consistent lower standard deviation (around 2, if lucky less). And a higher
round average of 8.86.
DynamicDictator code:
| |
Due to the limitation of time I can spend on this, the performance of the
previous DynamicDictator is not to its full potential. I believe with enough
time spent, one can theoretically achieve way better performance.
Discussing Strategy
The following are some of the observations I made when developing the bot, there is no guarantee they are correct. They are here just as a potential guidance for strategy developers:
- only one placement of a tile is allowed per turn
- memory is more abundant than time
- the longer the tile is placed the better
- more tiles placed earlier the better
- marketplaces convert food/materials to money, and they are very important for money production (develope good percentage calculators)
- after placing using
controller.placeTile(),world.getMap()does not update (same turn) - begining new wheat groups in expectation of windmills is a good strategy
Testing the Bot
Testing a bot’s performance requires the definition of what does it mean to have a ‘performant bot’. The official AuD competition (currently as of 06.06) measures a bot’s performance through the average of 100 seeds. Of course that is very arguable, as getting an unlucky batch of 100 seeds will disadvantage certain bots (due to the nature of the redraw of tiles). As such, I personally just used standard deviation and running the bot over a large number of seeds multiple times until I got a decent looking analysis before uploading the bot.
Speaking of analysis, I used the python script run-avgs (found in the git
repo) which runs the bot over a user-specified number of seeds, saving them to
a file, and calculates the different metrics (as shown previously in this
article).
Conclusion
The game itself has probably an implicit soft-limit on the average of rounds a bot can reasonably achieve without over-optimization. The way the tiles redraw is programmed means that sooner or later, you will get unlucky with your card draws and fail the run. There is no current garauntee from the game that one can endlessly reach the next round’s target resources.
So if the game is not designed to allow for endless runs and the success of a bot just means reaching round 5, you may ask: was it all worth it? And for that I answer: need it be?