Saturday, 27 March 2021

20,000 Light Years Into Space 15th Anniversity Edition

"20,000 Light Years Into Space" is a single player strategy game, which I made fifteen years ago. The first version was written in a week in 2006, for the Pyweek competition.

This post is mostly technical stuff. If you'd like to know more about the game itself then click here. If you played before and want to know what changed, there's a new section in the manual.

In technical terms, over time, the game worked less and less well. It was made with Python 2 and couldn't be trivially upgraded to Python 3.

I occasionally got email about the game, and a longer email conversation with another developer around the beginning of this year led me to think about how it could be updated. I realised I was looking at a major technical challenge, and I like that sort of thing...

I knew I would need some thorough regression tests in order to do the update. By accident, the game involved a confusing mixture of floating-point and integer values, which would become a real problem with Python 3's alterations to the division operator.

In Python 3, / always returns a floating-point value. If you want an integer value, you have to use // instead. The impact of this change is potentially huge, because you have to think carefully about each division in the program, and in some programs, some divisions could operate on both integers and floating-point values, depending on the context. In these cases, it is incorrect to leave / as it is, and also incorrect to replace it with //.

The solution to this problem is a combination of regression testing and static type checking. Static type checking didn't exist in Python 2.4 back in 2006 but nowadays it is an optional language feature. Type errors are detected by running the mypy program to analyse all source code. Amongst many other things, this distinguishes between integers and floats.

The annotation feature sort-of exists in Python 2 as well, but I decided to go to Python 3 before introducing it. Here was my process:

  • Start at version 1.4 (from about 10 years ago).
  • Create regression tests for the existing Python 2 version of the game. These are "demo recordings" of actual games with lots of internal detail that can be checked against future versions.
  • Port to Python 3. The result was a modified copy of 1.4 which passes the regression tests with both Python 2 and 3.
  • Add type annotations, committing to Python 3. The result can be type-checked.
  • Add pytest as a proper test framework, with test coverage, and then achieve full statement and branch coverage by adding unit tests alongside the regression tests. The result is a fairly reliable automatic test system for the new version and any version beyond that.
  • Make other improvements as necessary! These include support for high screen resolutions, fixing various bugs, and better sound and graphics. Result: version 1.5.0.

This is a technical debt resolving process which I have used before, at work, on other programs and in other languages. The big step that's missing (no small thing) is formally establishing requirements, so that the purpose of each test case is clear and the correct behaviour of the software is specified. For 20KLY, this is something that I have not done. I have only high-level requirements, which are:

  • Game works well on modern systems.
  • Game supports high resolutions.
  • Game itself is the same as version 1.4.

I had the opportunity to make improvements, potentially making the game more fun. For example, altering the difficulty curve to make it smoother. But I decided that if I started down that path, I would not stop until it was completely different. So, while I know about some bugs, they have stayed in place: now, they are features! I have concentrated on the technical aspects of restoring the game rather than "remaking" or "rebooting" it.

I have an idea that a future update could add more game modes, and these could be quite different, with different bugs. But, if the experience of the last ten years is anything to go by, it can be hard to find the time. I find that my day job takes up a lot of the mental resources needed for working on other projects. The important thing is to set achievable goals: it was achievable to make this version 1.5.0 in spare time over a few months, but it probably wouldn't have been achievable to create a playable remake or a reboot.

Having read about all this technical stuff, why not get the game? I also updated the manual with non-technical notes about the new version, including a long list thanking all of the other people who contributed over the years.