Sprint 26 - Adam-Poppenheimer/Civ-Clone GitHub Wiki
Goals
- The existence of barbarians, which wander around the map and attack things.
- Some sensible way of displaying farms.
- All wonders up to the Medieval era.
- The Liberty policy tree.
- The Honor policy tree.
Risks and mitigation
- The current implementation of civilizations makes them all player-controlled (effectively simulating a hotseat game). This won't work for a barbarian civilization, since they need to be controlled by some sort of AI. Separating this and figuring out how to switch between player-controlled and AI-controlled civilizations could be a fiddly and messy process if I'm not careful.
- I might need to add to the codebase the notion of a Player, which GameCore hands control to when it's that player's turn, and which GameCore receives control from when the player is done. How the player receives, handles, and gives back this control will depend on whether it's controlled by a player or an AI. In the case of an AI it can run its sub-processes in code and then call back to GameCore. For a player, it can enable the UI and permit player interaction, and then pressing the End Turn button can return control to GameCore.
- While barbarians are much simpler than a normal civilization, they'll still require some sort of AI. Given that I've never built an AI of any substance before, I'll be wandering into completely new territory that could present all sorts of unforeseen problems.
- I should spend some time beforehand studying game AI as it applies to 4X games in particular. I should also review that game AI book I've read but never really applied. I'd imagine flocking behavior might be useful to spread barbarians out across the map from various encampments and towards player cities.
- I've tried at least twice to build a solution for farms and neither attempt led to a good result. Given the importance of the issue, I can't simply de-prioritize it and defer it until later. I might struggle to come up with even a partial solution to the issue.
- Right now (and perhaps for the final product, too) I don't need anything near as fancy as what Civ 5 does. It might be that a repeating farm-like texture could work just fine, one which replaces the texture of the ground. That won't give distinct edges to farm plots, but I don't need that right now. I just need some unambiguous indication that a cell is covered in farmland. I might also consider creating a Farmlands mesh that just copies the terrain mesh point- for-point, excluding sections of the map that don't need farms. And then I can do...something with that. That'll at least let me define hard boundaries between farmland and wilderness.
- Culture and rivers both contains unusual triangulation strategies. Their combination (which is necessary to conform cultural boundaries to rivers) might prove messy, buggy, and unwieldy.
- I can always clean up culture triangulation as I'm adding in this new functionality. I suspect most of the complexity here will be, as usual, in the corners. So as long as I can properly identify all the cases and separate them into separate methods I should be fine.
- Building out barbarians means I need to figure out what barbarians are supposed to do. Studying Civ 5 might not provide meaningful answers, since the behavior of barbarians might not become obvious through anything but an intense inspection.
- Studying them in action might be a bit difficult, since that'll require playing a live game. There might be some analysis online I could make use of. I'm sure somebody's cracked open the game and analyzed the behavior of its barbarian wander algorithm. Alternatively, I could come up with my own policies and strategies. That seems like an acceptable fallback if I can't find requirements in the source material.
- Right now unit movement is built almost entirely around interaction with the UI. It's not designed to handle movement instructions from deeper in the codebase. While I'm sure unit movement will work for the AI, I've little confidence that it'll work well, particularly when it comes to combat.
- The combat issue, particularly the melee combat issue, requires some resolution. Units shouldn't have to be adjacent to enemy units in order to attack; they should be able to move and attack in the same action. I might need to add a fast or instant movement mode, one tied to cell visibility most likely. Other than that, I'll just need to see how the system breaks when I start controlling it more directly from the code.
- It might be difficult and tedious to test my barbarian implementation if I have to play a regular game in order to see how it pans out.
- I'll need to add faculties to the editor to make it easier to test barbarian spawning, movement, and assaults. That'll include adding barbarian encampments, adding units, and iterating a barbarian turn given a particular map configuration.
Review
I began with the creation of the Player, a sort of intermediary that stands between GameCore's turn progression and Civilizations. The main goal was to have something that could either call into some AI code or enable/listen to the UI, thereby allowing a distinction between computer-controlled and human-controlled players. That ended up forcing a large number of relatively small changes in the codebase, given how central turn exchange is to the game. I did that ultimately to create the incredibly simple Player class, which is little more than a wrapper for its brain. The architecture is functional but it's not clear how efficient it is.
Next I moved into barbarians, starting with an implementation of Barbarian civilizations, which the simulation treats differently than its traditional counterparts. That involved a considerable number of small changes to the simulation code, but this time for a clearly-valuable reason. Extending the simulation and the UI to handle the differences in city capturing, diplomacy availability, war, great people generation, and map editor options like adding cities or civ destruction. The architecture seemed fairly supportive of these changes, though in retrospect there are several things I forgot like resource requirements and happiness that I should probably incorporate at some point.
After building the barbarian civilization, I moved into completely new territory and started building out the barbarian's AI. I started by searching around for information on AI for 4X games and found a very useful master's thesis called Integrating AI for Turn-Based 4X Strategy Game that provided all sorts of techniques and analysis relevant to the task at hand. I read that thoroughly and used its lessons to start planning my own AI.
I realized early on that even the simple case of barbarian AI was going to be fairly complex, so I set about clarifying the problem in my head. I then did something that I had not yet done in this project, something so simple and useful that I can't believe I haven't been doing it up to this point: I documented my design goals and reasoned about the necessary architecture in a separate Wiki page. That's definitely something I'm going to have to do for all the complex processes in the codebase, I think, particularly triangulation, map generation, the high-level architecture of the program, and any future AI I end up creating.
Documenting the problem helped me discover two important things. First, the barbarian civ doesn't need to handle civilization-wide decision-making. I can resolve all barbarian behavior by considering each unit in isolation. And second, I reduced barbarian behavior to a short list of goals every unit should have (Guard encampment, wander, capture civilian unit, pillage improvement, attack units, flee, wait unit healed, be prisoners, and sack city). These discoveries helped me design an architecture and start subdividing the unmanageable task of barbarian AI into smaller, more precise and more manageable goals.
I decided on the following plan: BarbarianPlayerBrain is the main class that controls the whole AI. I use another class to generate influence maps that'll inform how various units behave. BarbarianPlayerBrain retrieves these influence maps and then uses them to retrieve a series of commands from BarbarianUnitBrain. BarbarianUnitBrain has access to a number of BarbarianGoalBrains that it chooses between based on each brain's utility function. BarbarianUnitBrain selects the highest-utility BarbarianGoalBrain and retrieves commands from it. BarbarianPlayerBrain then takes those retrieved commands and passes them to a UnitCommandExecuter class. Once all unit commands have been retrieved, BarbarianPlayerBrain then tells UnitCommandExecuter to perform all unit commands it's been given, at which point control passes from BarbarianUnitBrain.
This architecture allowed me to do several things. For starters, I managed to decouple decision-making from task execution, and in fact decoupled UnitCommandExecuter from barbarians entirely (which'll be useful if I make other types of AI). It also allowed me to create a single class that encapsulates all the behavior associated with a particular unit goal (namely its utility and the commands the unit should execute to follow that goal). So I have one and exactly one class for every goal. BarbarianUnitBrain doesn't care about the number of goals, and so I can add or remove goals without difficulty. The only problem is balancing the relative utility of each goal against the others, which does require considering all of the goals at once. But this seems like an unavoidable necessity rather than a fault of the architecture.
I established the architecture of my barbarian AI and implemented an acceptable first pass of the Wander goal before breaking up the monolithic and nondescript "Barbarian AI, First Pass" issue into its constituent goals, at which point I considered that issue resolved. I ended up creating a pretty effective Wander strategy, and plenty of room to add more goals in the future.
After working in the AI for a while, I then backed out and started building out barbarian encampments, since I needed encampments for many of the other goals to make sense. Encampments turned out to be a very complicated task, far more than I was expecting. I was tired and unfocused during the early stages of the work, which was not a great way to start things out. I discovered quickly that adding a new feature wasn't going to be enough; encampments needed to be their own entities, placed on the map like units or improvements. I forgot about serialization requirements when scoring the issue, which ultimately required a new Composer class. But by far the biggest problems came from the unit tests. For whatever reason I was having a very hard time cleanly and meaningfully testing the code associated with encampments. I spent a lot of time unsatisfied with how I was testing things and spent a lot of time trying to build an architecture that was good for testing. But I did eventually get barbarian encampments functional, from their periodic spawning across the map and the rate at which they produce units to their visual appearance and gold bounty when destroyed.
After the barbarian encampment problem I started working through the Guard Encampment goal. That goal revealed several important AI tasks I needed to attend to (most notably the utility function I hadn't built during the early stages of AI development). I also tried to use influence maps to implement the goal, but ultimately decided that influence maps weren't worth my time and opted for a more straightforward solution. This task, too, was a struggle to test, though most of the added difficulty came from a continued iteration of my architecture.
I wrapped up the sprint by implementing the Be Prisoners goal. This goal was much easier to address both because I had a better architecture to work with and also because the goal itself was simpler. I ended up resolving it about as quickly as I'd anticipated.
While performing the review, I also finally added a custom sidebar that organizes the ever-expanding Civ Clone Wiki a bit better.
Retrospective
What went well?
- I managed to get any amount of AI working, and learned a lot in the process.
- I successfully analyzed and documented the design goals, difficulties, and high-level architecture necessary to make the barbarian AI work. Through careful planning and study I took a complex problem I had no idea how to solve and turned it into a collection of manageable subtasks.
- I'm well on my way to an effective implementation of barbarians, and in fact already know exactly what I need to do to finish it.
- I figured out how to cleanly exchange turns between human-controlled and computer-controlled players. I even managed the execution of an AI's turn across multiple frames without it causing any problems at all for turn exchange (thought it might still cause issues with player input).
- My feature managing code, though still ambiguously named, seems robust to changes. I was able to add barbarian encampments to the game's visuals almost as an afterthought.
- Working through and documenting both barbarian spawning and barbarian AI was a very good decision. I should've been doing that from the beginning for pretty much all the complex requirements of my codebase.
- I finally created an interface for randomization that made unit testing randomly-driven classes much easier. Not only did that allow me to unit test barbarian AI, but that should help me unit test other randomly-driven things (like map generation) in the future.
What could be improved?
- While the idea of a Player as distinct from a Civilization seems important, I'm not sure my current implementation of Player provides much utility. I should think more carefully about what purpose Player serves in the codebase and whether its existence is worth it.
- I really need to start documenting the high-level of my implementation. Unit tests (for the most part) cover the individual scale, but I have very little record of how big things like map generation or terrain triangulation are supposed to work. I need to add documents for these subsystems so I know what they are and how to reason about them, and also to detect any problems they might have.
- While I think it's useful to overbudget sprints as a way of maintaining discipline, this sprint was laughably over-ambitious. I should never have scheduled this many tasks across so many sections of the codebase. While I don't think I could've anticipated how complex the AI task was going to be (since I've never built an AI before) I probably should've added tasks more conservatively in case the AI's complexity started to balloon.
- The more I think about it, the more I realize the lack of unit tests in the MapGeneration namespace is a serious flaw. I I should probably add that as a long-term goal and work to rectify that issue, perhaps once I've completed my AI.
- My struggles with unit tests this sprint make me think one of two things is true: either I've just recently discovered that my original unit testing strategies are bad, or that the barbarian AI architecture is particularly hard to test. The latter is a fine result, but if the former is true I might need to completely rethink the way I've been doing unit tests up to this point, or possibly rethink how I've been programming.