Sprint 11 - Adam-Poppenheimer/Civ-Clone GitHub Wiki
Goals
- A map editor that can set the game into almost any state the simulation could.
- Console tools to help test and debug the simulation.
- Resource trading, facilitated by roads.
- Bugfixing for all existing features.
- Properly-refreshing UI.
Risk and Mitigations
- While I'm sure integration tests would be a very useful way of testing important behavior and preventing regression, it's not clear to me how I'm supposed to build them, what aspects of my architecture are good candidates for such tests, or what tests might be useful. Thus it's possible I could spend many hours building tests that are of no use to me.
- I can focus integration tests on specific processes that I think might pose problems. Unit movement is a clear example, since it's both complex and hard to address with unit tests. I can also make my efforts forward-facing; there's not much reason for me to integration test code that's working. Instead, I can attend to code I haven't proven works or on code that's actively broken, and then create integration tests to handle specific cases that I encounter.
- I've struggled in the past with adequately testing the full scope of my project, since I usually don't have a complete grasp on everything that could go wrong. Proceeding without a clear plan for figuring out requirements and finding errors could end with me wasting hours doing inefficient ad-hoc testing.
- Playtesting would be a smart way of addressing this, though that remains a difficult thing to gain access to. I suppose I could figure out some use-cases and try to build acceptance tests (or some similar formal process). That might help me reason about what I need the game to do. And as I find errors I can add new acceptance tests to cover it. Formalizing the tests (even if only in writing) will help me keep track of what I need to be doing.
- It's not clear to me what console commands I might want, nor how existing developer console tools might work with my architecture. And spending dozens of hours building out an elaborate methods for manipulation could be an enormous waste of time if I never use any of it.
- As I'm testing, I should make note of everything that takes an inordinate amount of time or every time some aspect of the simulation gets in my way. The most common of these problems can then become console commands that I add to the game. For instance, if I consistently find gradual unit movement to be intrusive, I can build out the code necessary to perform instantaneous movement. And if I don't encounter a clear need for console commands, I can avoid implementing them entirely. This sprint will involve a lot of testing, so I should encounter areas of difficulty fairly quickly.
- My map composition system remains brittle in the face of certain changes to the codebase. I don't currently have a smart way of storing the states of my units, and every time I make any change to the simulation there's a good chance map serialization will break, as well.
- Since this hasn't given me a lot of problems yet, it might not be that big of a concern. It doesn't increase the burden of adding state by that much. If it does become a problem, I can try implementing the Private Data pattern and make sure that my private data is serializable. That'll allow me to consolidate serialization concerns in a single place, which should help things.
- Given the amount of UI or UI-adjacent code and its lack of test coverage, it could be difficult to ensure correct solutions of a lot of the errors I'm facing. It might also be difficult to ensure that the errors never come up again, given the lack of regression tests.
- If I encounter a bunch of UI or UI-adjacent code that seems like it might have problems, I can always refactor it and add tests to some or all of what remains to ensure correctness. I don't need to be dogmatic about avoiding UI tests. I could also reconsider my earlier stance forbidding UI tests, though I'm hesitant to do so given the problems I was encountering. Even if I reverse that decision, however, I don't think it's worth proactively adding test fixtures to every UI element. I can add them as they become necessary or useful, but retroactive tests won't be nearly as useful as proactive ones.
- While playmode tests and Zenject's IntegrationTestFixture should help things considerably, my architecture's reliance on mediators and logic separation could make setting up tests time-consuming, thus decreasing their usefulness.
- I can start by focusing on the most important sections of the codebase, or the ones most likely to break. That should help the cost-benefit analysis even if I can't figure out a fast way to set up tests. Choosing good pieces of the integration to test'll be useful. And I should avoid creating integration tests that overlap with existing unit tests, if such structures arise. At the moment, I've no idea how my architecture will fare in the face of integration testing. Only way to know is to try.
Review
I've resolved a very large number of bugs and improved the map editor considerably, though there's still much to be done in both arenas. I've also added a bunch of regression testing to the MapManagement namespace that's given me greater confidence in the correctness of my code and also revealed a few bugs with my implementation. Unfortunately, the changes I've made seem to have broken map saving and loading, which makes it hard to see what other problems may have arisen.
Retrospective
What went well?
- I resolved every issue in the sprint, including all of the desirable tasks.
- The codebase is now in a much more stable and robust condition. A lot of problems that I've been having for weeks have been resolved, and I'm much more confident in my code's correctness.
- My map editor is now a lot easier to work with, especially when it comes to improvements.
What could be improved?
- Though the majority of my risks and mitigations involved integration testing, I did very little of it. This is likely to make my solutions ephemeral and has most likely left the codebase with all sorts of invisible problems. I need to start driving bugfixing with the tests necessary to prove that the bug has been resolved, so that I know whether my solution has been undone.
- Given the difficulty of testing coroutines, I should consider finding another way of performing asynchronous operations like unit movement or runtime clearing. Otherwise it'll be difficult to confirm that these processes work.
- There are several UI classes whose scope, organization, and number of dependencies makes it very difficult to figure out what's going on. DefaultUnitState in particular feels like a pile of spaghetti for reasons I don't entirely understand. This poor organization is starting to cost me. I should spend some time studying DefaultUnitState so I can figure out what about it feels so wrong, what aspects of its design are causing me problems, and then use that information to create smarter UI in the future.
- I think the Responder-suffixed classes I've periodically used might be bad design. At the very least they don't have meaningful names that describe what they do. It's also possible that they're stealing responsibilities from the classes that should be managing them. I should review the Responder classes, making sure that either their existence is justified or that their responsibilities are given to the correct class. UnitResponder and CityPossessionResponder are particularly suspect cases.
- The Cities namespace is unwieldy. Its sub-namespaces have turned out to be of no utility. I should flatten that hierarchy so that all the classes in Cities.Growth, Cities.Production, and whatnot are directly under Cities. I might apply a similar logic to Units.
- My factories are only partially under unit tests, which makes them susceptible to bugs and problems. I should update my factory test fixtures so that they adequately test the behavior they're trying to implement.
- Now that I've abstracted away certain key classes (like Unit and Improvement) away from the GameObjects that underpin them, it might be possible to revoke their MonoBehaviour status and turn them into normal classes. City in particular doesn't need a visible representation in the world, since its visuals are handled by FeatureManager. And it doesn't need state from Animator. Doing so could make interacting with those classes a lot easier, though it would muddy the semantics of Destroy().