Sprint 27 - Adam-Poppenheimer/Civ-Clone GitHub Wiki

Goals

  • A full implementation of the barbarian AI.
  • Some sensible way of displaying farms.
  • All wonders up to the Medieval era.
  • The Liberty policy tree.
  • The Honor policy tree.

Risks and Mitigation

  1. While the architecture for my barbarian AI seems generally fairly solid at encapsulating components of the problem, that encapsulation breaks down when it comes to determining goal utility. As it stands, the utility of each goal is implicitly connected to the utility of every other goal. That could make it difficult to balance the utilities of my goals to create reasonable behavior.
    • This might be an unavoidable complexity. I like the fact that BarbarianUnitBrain (and, in fact, the rest of the codebase) is agnostic to the number and types of goals right now. In theory I could centralize the utility calculations, creating a UnitGoalUtilityLogic class, but that would create a class that would need a direct coupling to all the goal brains to intelligently select between them, or else do something similarly unpleasant. It might be smart to defer any action on this front until it becomes a very clear problem. It's entirely possible I'm worrying about nothing. But if problems do arise, a single utility logic class seems like the next best way to go, despite its problems.
  2. Now that AI is beginning to seem like a more reasonable task, it seems much more feasible that I might add city states and computer-controlled civs to the game. But my barbarian-focused AI code isn't designed in a particularly decoupled fashion, which could make it hard to cleanly repurpose its logic in the future and maintain a larger collection of AI in the face of additions and bugfixes.
    • I've already decoupled unit commands and their execution from the barbarian AI. That's certainly a start. It's possible that I could establish a more generic UnitBrain in place of BarbarianUnitBrain, and a corresponding IUnitGoalBrain interface to replace the IBarianGoalBrain class I've already got. Most of the Barbarian goals are specific to the subsystem's needs, but I might be able to generalize the Wait Until Healed, Flee, and Attack Units goals, depending on how my hypothetical city states and AI opponents want to manage their units. Since I don't know what those hypothetical classes need, I can't know for sure where the generalization will come in. But I can probably facilitate future changes by continuing to separate my code and making sure everything's in small, decoupled chunks. That'll make it easier to change things in the future, since I'll be able to grab individual units of behavior and generalize them.
  3. 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.
    • I think the mitigation for this issue from Sprint 26 still holds. Switching out the terrain texture for farms and doing nothing else is a reasonable first pass. Good-looking farms can be a stretch goal if I get to it, or if the fading farms look particularly bad.
  4. 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.
    • The mitigation from Sprint 26 has some value here. There might be a more intensive re-architecting in store for triangulation. I need to put terrain mesh triangulation under some amount of unit tests at some point. Culture bumping up against rivers might be a good time to try some things to clean the HexMap namespace up and extend test coverage at least a little bit.

Review

I began by returning to the barbarian unit goals, beginning the sprint with an implementation of Pillage. I took this task as an opportunity to start generalizing my AI code a bit, which led to a barbarian-agnostic influence map system. Working through the Pillage goal also revealed a bunch of problems in my move unit goal, improvements, and my pillage handler that took a fair while to resolve. Particularly, I opted to remove the Animator from Improvement, as using it to select between states was enormous overkill.

After that, I decided to resolve an old problem that I hadn't elevated to importance until very recently: melee combat. It was always frustrating that you couldn't give a move and attack order at the same time, so I decided to provide that functionality to both the player and the AI. But this revealed a bunch of problems with how CombatExecuter was implemented and unit tested that I could not abide by. Thus I ended up spending an entire day refactoring CombatExecuter to make its unit tests simpler and more thorough. Mostly I separated a lot of its logic into more focused classes, which were must easier to test and understand. But it ended up making that section of the codebase much cleaner and easier to extend, so I think it was worth it.

I then returned to the barbarian AI, implementing the Capture Civilian goal. This required me to add an AttackUnitGoal, which revealed some small issues with combat that needed resolution. I needed to add success and fail actions to PerformMeleeAttack to properly handle success and failure declarations on AttackUnitGoal. I also added a class that determines the order in which units get attacked, which should help ensure that military units are attacked before civilian ones. After finishing that goal I added civilian capturing, a fairly simple task given the existence of the PostCombatResponder class.

Next was the Flee goal. I ended up using a logistic curve to prioritize the goal based on ally and enemy presence. It took a bit of time to configure the slope to make sure it acted reasonably, but overall the issue wasn't as challenging as other AI tasks and was resolved in a timely manner.

I continued the AI tasks by implementing the generalized Attack goal. This induced a bit of reorganization (particularly the breaking up of a budding God Object), but I managed a basic implementation without much difficulty. I do believe, however, that the current implementation needs tweaking before it can be considered. I would've attended to the tweaking this sprint, but I wasn't sure what, exactly, needed to be done, so I opted to defer it until I've got a better grasp on the problem at hand.

I rounded out the barbarian AI by adding the Wait Until Healed goal, the last goal of significance to be added. While I'm not confident that my utility calculations are optimal (that'll require a more thorough analysis of the barbarian AI as a whole) I did manage to fairly easily create a basic implementation. Configuration will come later.

After finishing the AI, I moved onto graphics task. First and most important of those tasks was the Farmland issue. I ended up going with the simplest solution: replacing the texture of farmland cells with a farmland texture. Building that texture, then, became the bulk of the task. I spent a lot of time durdling around in Gimp before coming up with a mediocre but functional tessellation. I don't like the solution I came up with and very much want something better, but given the difficulties of my current terrain implementation I felt it was the best I could manage.

I then made some tweaks to my camera, delegating beginning-of-turn camera positioning to PlayerBrain so that I could suppress such movement during the barbarian's turn.

After that I leaned away from the UI a bit and spent some time improving my unit strength estimation logic. I ended up using CombatInfoLogic to compare the argued unit against all of the cutting-edge units in the game to get some estimation of what the argued unit's strength might be. I figured that'd help keep the code DRY, since all of my combat strength estimation would be routing through CombatInfoLogic alone. That required the addition of unit promotion lines, a new IUnit implementation to act as a facade for templates, and a UnitUpgradeLogic class (which figures out upgrade lines and cutting edge units). Thus I've built out some tools for the future task of unit upgrading, though upgrading itself remains unimplemented.

Next I returned to the UI and added unit icons to the game, both to disambiguate units on the map and to make it easier to interact with stacked units. This took considerably longer than expected, since I failed to anticipate how hard it'd be to determine when an icon should and should not be added. Particularly, I failed to consider how vision would play into the issue. Placing the icons and getting them to stack was easier than expected. But I did manage a reasonable implementation.

I ended the sprint tackling (though not completing) what's ended up a gargantuan problem: updating culture boundaries to respect rivers and standing water. Not only is the task very complex, but the architecture I inherited from the Hex Map Guide, being very case-heavy, is completely unwieldy. It is quite difficult to divine all of the cases, organize them in code, and address them in an efficient manner. While I've made pretty good progress in creating river- and ocean-sensitive borders, the whole process has left a sour taste in my mouth. Triangulation, especially culture triangulation, is monstrously difficult to extend or maintain.

Retrospective

What went well?

  • I managed a fully-functioning first pass of my barbarian AI. There's still a lot of tweaking and configuration to do, but its core is now finished.
  • Dealing with CombatExecuter and a number of other poorly-designed classes has helped me better understand how to build testable code, which should help me develop better architectures in the future.
  • Unit manipulation is now in a much better place. It's a lot easier to order units to perform melee attacks, and users can finally select units that are stacked on top of each-other with relative ease.
  • While I don't like my solution, I finally have a functional method for displaying farms, which was a major graphical feature missing from the game until now.
  • Though there is much more I can do to generalize my AI code, I think I've taken a few important steps to creating generalized tools. I suspect that, if I ever implement a full competitor AI, I'll be helped considerably by some of the architectural decisions I made during this sprint.
  • The way in which I did unit strength estimation is, I think, intelligent. It's both non-repetitive and thorough, which should help improve AI behavior.
  • I successfully suppressed the desire to separate small tasks into mostly-empty pomodoros and fixed two small bugs in the space of a single pomodoro, thereby preserving the length of my workday.

What could be improved?

  • It is now clear that my terrain triangulation code is very poorly designed. It's case-heavy, poorly-organized, highly sensitive to any change in the simulation, and is difficult to understand and extend. While I don't know if I can or should fix this glaring inadequacy in my codebase, I definitely need a better pattern for any future triangulation tasks. And if I ever come back to this project, the first thing I ought to do is rip out the map rendering code and start over from scratch.
  • I ignored both of the weekly builds of this project during this sprint, perpetuating a longstanding flaw in my design process. I need to keep pushing myself to build this project frequently so that I know what it'll actually look like to the end user. I might consider switching the weekly build to either the beginning or the middle of the week, so that I'm more likely to have the energy to force myself to build the project.
  • I definitely felt my energy flagging towards the end of the week, especially during the triangulation tasks. I think I need to take a day or two off to recuperate and refocus.
  • I feel like my long-term task prioritization has lost focus. I need to determine, on a larger scale, what is and is not important to the completion of the game and more carefully choose tasks.
  • My barbarian AI has now moved from an engineering task to a design task, which means I need to very carefully consider what to do with it. Even if I don't create a well-playtested barbarian, I should at least come up with a more specific list of requirements so I know what to design and when to stop.