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

Product Backlog Tasks

  1. Cities generate resources every turn, either for their civilization or themselves.
  2. Cities can construct buildings that can affect people, cities, and civilizations.
  3. Cities have people that consume food and gather resources, which players can assign to various slots or leave unemployed.
  4. Cities will automatically assign their people to slots if left to their own devices.
  5. The game consists of some number of players who each control a civilization.
  6. Players can quickly and easily figure out the yield of a given tile from either the world map or the city selection display.
  7. All code, including UI code, is thoroughly unit-tested and documented.
  8. The codebase should be clean and well-organized enough for presentation to prospective employers.

Basic Design

Story

The story remains the overarching premise of the original game, and is still not a priority at this point in development.

Mechanics

There is a single civilization under the player's control. The player can create cities on any hex of a static hexagonal grid. Cities grow their population, expand their borders, produce resources for themselves and their cities, and construct buildings. Buildings can provide static yield, worker slots, or some modifications to the city or civilization that possesses them. Players have full control over the assignment of people to slots. They can also instruct the governor of any particular city to auto-assign workers to maximize various types of yield. Governors will not override slots the player has designated be filled manually.

Aesthetics

Still utilitarian. There should exist only visual indicators of important information.

Technology

The game is built with Unity 3D 5.6, with Visual Studio 2015 as an IDE. Version control and project management is facilitated by GitHub. The project includes Zenject, Moq, and UniRx as plugins.

Risks and Mitigations

  1. I don't have a clear way of mocking UI objects like Text, Button, Slider, or Dropdown, which might make it more difficult to unit test my UI properly.
    • I might not need to mock these objects. Instead, I can configure and inject them in the same way I've been injecting other dependencies up to this point. I can use Zenject's InjectOptional attribute on all of the serializing fields (things I expect to configure in the editor) to facilitate injection at test time without requiring it at runtime. I can keep instantiation to a minimum by using SetUpFixture instead of SetUp, and then test the properties of these objects more directly. It's not ideal, but I probably don't need as much complicated mocking behavior for things like Text, since most method calls to that class result in fairly simple state changes.
  2. My current namespace structure feels messy and doesn't facilitate codebase navigation. And if the codebase is already hard to navigate at this stage, it's likely to become unmanageable as the project progresses.
    • I'll spend some time at the outset of the Sprint reorganizing my namespaces to make it easier to navigate to specific classes. At the very least I'll need to split City up into more sub-namespaces, or else remove those namespaces from City altogether and place them directly under Assets.
  3. The codebase has a bunch of missing unit tests, which means that I'm not adequately ensuring the necessary behavior of my modules and am thus building on shaky scaffolds.
    • After reorganizing the namespaces I can go back in and fix any of the missing unit tests. That'll be a substantial task for the UI, given that it has almost no tests to speak of, but it's a reasonable price to pay for regression testing and a guarantee of functionality.
  4. My UI is currently split between Signals via Zenject/UniRx and my homebrewed state machine system, which means that I'm not organizing my code well under either paradigm. This threatens to turn my UI into spaghetti.
    • I'll need to figure out how to create display closing signals so that I can detach my UI behavior more thoroughly from my simple state machines. I might actually consider using the state machine logic to fire DisplayOpen and DisplayClose signals. Alternatively, I could look for more elaborate UI solutions online, maybe some plugins that handle these tasks more thoroughly.
  5. I intentionally set aside player assignment of slots because I didn't have a clear way of implementing it. Adding it in now might substantially increase the complexity of PersonDistributionLogic, or else take up a considerable chunk of development time.
    • There is a reason I separated City's logic out into pieces like this. PersonDistributionLogic can afford to get complex as long as it provides a fairly simple interface for City to use. The real thing I need to avoid is making other modules more complex in order to facilitate PersonDistributionLogic. If the complexity is self-contained then it won't affect the rest of the codebase much. Better still would be to figure out what I'm actually looking for and think clearly and carefully about how to get it. I suspect I'll come up with a good solution if I approach the problem sensibly.
  6. Some of my classes have very complex networks of dependencies, which might indicate an overly-coupled architecture that'll cause me problems down the line.
    • Perhaps after namespaces and before unit tests, I should take a look at my dependencies and see if there are any ways I can minimize them. I suspect there are modules that are too complex for their own good. If that's not possible, I should consider consolidating dependencies into classes so I can push them around the codebase easier. But that doesn't help the underlying problem of connectivity. I could try creating facades for some of my more coupled classes to use. That way, those classes are dependent on only a single class, and the large number of dependencies appears in a fairly simple context. We'll see if a good opportunity for facades presents itself.
  7. Direct input from the player (mouse and keyboard events and the like) is currently coupled to the Input module, which will make classes that rely on such input difficult to test.
    • I can address this by pushing all UI events into the Signal/Observable pattern Zenject and UniRx provide. That way, individual classes consume signal streams rather than coupling themselves to Input and axis names. In order to make that work, I'll need to figure out how to fire events in my unit tests. That might be as simple as calling the standard implementation (since the implementation for a signal is supposed to be empty anyhow). I'm sure I can come up with something more sophisticated if need be.
  8. I'm not sure how to make good use of branches, nor do I have a strong committing discipline. Both of these things might inhibit my ability to use version control in a collaborative context and will make the repository less clean.
    • I can start by using a few simple rules. For starters, every sprint should be its own branch that gets pulled off of Master at the beginning of the sprint and pushed back onto Master at its end. Every issue can have its own branch forked off of the sprint branch, possibly with multiple commits, that gets merged back into the sprint branch when it's resolved. We'll see how that goes.
  9. Percentage-based modifiers to yields and productivity will add a lot of complexity that might bloat or couple my code in undesirable ways.
    • I do believe that either Zenject or UniRx have an ObservableField class that does exactly what I was attempting to do in MerchantRepublics. Alternatively, I can offload this complexity to my various logic classes. For instance, let's say we implement a workshop building that increases production yield in a city by 10%. City doesn't need to care about this module if it's pulling its total yield from ResourceGenerationLogic. ResourceGenerationLogic can concern itself with modifiers by calling into something like CityModifierLogic.GetModifier("Production Yield"). CityModifierLogic, then, can figure out what the various production modifiers are, possibly caching the results and whatnot. That ought to suit my needs just fine.
  10. It is incredibly inconvenient to configure much of anything, given that configuration classes and their ScriptableObject implementations are scattered throughout the codebase.
    • I should probably consolidate my configuration code into a much smaller number of much larger objects. I might consider gathering all of the City config files into a single, larger file, and do the same with maps. That way, configuration is still organized but not as split into pieces. If I want to add more organization to this, I can use editor scripts to subsection the inspector by the specific module the configuration variable is used by. But that's muddy for fields used by multiple modules, so I'll probably avoid doing that.

Review

I've pretty thoroughly reinforced the mechanics developed in Sprint 1. Players can now manually assign and lock slots, though they're incapable of manually assigning unemployed people. The UI mostly refreshes correctly, though there are still some issues with setting new production projects. But these tasks represented a relatively small amount of the work. The majority of my work came down to building out test suite. I now have most of the UI in the game under tests, though some of them are of dubious quality. I've done other organization and refactoring tasks that have put my codebase into a pretty solid state (to the best of my knowledge, at least).

Retrospective

What went well?

  1. I addressed every risk I identified at the beginning of the sprint, resolving most of the problems I'd identified.
  2. My unit tests are becoming more maintainable and more sophisticated. I've started creating more utility methods to make it easier to create tests. And I've begun using test cases to more rigorously check my work.
  3. I managed to resolve every issue of substantial consequence to the program. There were a few issues left at the end of the sprint, but none of them were very important.
  4. I've finally managed to establish a decent paradigm for UI. I'm using Unity's animation system as a purely logical state machine. That state machine sends requests to the actual displays, which are completely decoupled from the state machine itself. Individual states are implemented as classes, and transitions into a given state are handled by a separate class. I think this model is finally something clean and extensible, which means I might have largely resolved the UI woes I've been having since Merchant Republics.
  5. Even if the sprint was initiated poorly, once it was in progress I attended to it fairly well. I ran through tasks by priority, focused only on the goals of the sprint, and didn't attempt to change those goals mid-stream.
  6. This sprint had much better committing discipline than previous work cycles. Pretty much every commit addressed a single issue, and they all referenced the issues they were addressing so that GitHub could link the two to each-other.
  7. My logical decoupling is beginning to show its efficacy. I'm finding it a lot easier to add new behavior to classes when they're this separated, and I'm also finding that desired changes aren't reverberating throughout the codebase as much.
  8. I've added and begun experimenting with a new tool (UniRx) and a new programming paradigm (reactive/event-driven programming) that's proving quite useful. This type of programming will probably become an important tool in my arsenal, so it's good that I'm learning new and useful things.

What could be improved?

  1. This sprint didn't focus nearly as much on player activity as it should've. That is the point of this exercise, after all. I need to make sure that future sprints don't focus so much on building out tests and infrastructure. I should always be building this things in tandem with and to the needs of gameplay.
  2. My unit tests are still of mixed quality, and some of them are of dubious value. I need to make sure that every single test is running on test cases, ideally fed in from another file that can be added to without much difficulty. I also need to determine what behavior is worth testing and what behavior is too unimportant to spend time confirming (if such behavior even exists).
  3. I don't think this sprint prioritized very well at the macro level. A lot of the tasks I assigned to this sprint were probably not the most important issues. For instance, manual assignment of people to slots is not nearly as important as units existing in the game. I should consider putting more work into my Product Backlog, not only adding more tasks but prioritizing it so that I'm only ever focusing on the most important things.
  4. There was a lot of task difficulty mis-estimation, especially at the beginning of the project. When initiating the sprint, I didn't spend very much time thinking about how long things were going to take, which shows in the quality of my task estimations. I should spend more time reasoning about how long particular tasks might take.
  5. I spent a decent chunk of this sprint deconstructing the state-based UI I'd developed, only to reconstruct it later under a different paradigm. That sort of undoing and redoing work has been a considerable issue in the past. While it may have been warranted and was a useful experiment in UniRx and event-driven programming, it was not an efficient use of my time. I should do what I can to either avoid making these sorts of changes or to decouple my codebase so much that such changes are much easier and less arduous to make. Removing the signal logic from DisplayBase was a good start (as it made a lot of my UI agnostic to how it was being called) but I need to go further. That way I can change the interstitial bits without those changes affecting other parts of the codebase.
  6. I let my work discipline slip by insisting to myself that I didn't need to do certain pieces of work, or that it made more sense to wait to begin new categories of task. This is a logical fallacy and should be treated as such. There's no reason to hold off doing work for later. At the very least, I could've spent time populating the product backlog or working through less important bugs.
  7. My code is not documented. Though unit tests and good naming conventions alleviate this problem somewhat, I still need to describe what various classes and members are, why they exist, and how they fit into the larger architecture of the program. Some of those tasks will be overkill until the codebase is in a more stable configuration, but I can at least describe the purpose of individual classes, or explain the semantics behind the logic-suffixed classes.