Ruthless Criticism and Self-Criticism: A Postmortem

What Went Wrong

The design of Signals Intelligence is overly elaborate, and has too many moving parts. Every part needs to work, and the parts that don't work drag down the core game loop. If the player doesn't like or understand one of the systems, the game suffers, and if the player can skip one of the systems, you might as well not bother. The scope wasn't all that ambitious, but it was ambitious for a 48 hour jam game built without an engine for turn-based games or genre-specific base code.

Programming

At least it runs now.

Spaghetti Code, 1970s programming

I wrote the whole program with one big main-loop and no abstractions. It's all enums, flags, (named) tuples, state machines, and one big loop. There were barely any classes or functions in the code. When I do this in a game jam, it's usually for more than one reason:

  • I do not know yet which kinds of abstractions or data structures I actually need
  • I cannot waste time on bottom-up functionality I might not need
  • I do not have the time to refactor during the compo
  • Once I have something running, I know what kinds of data structures I need

I never had the breathing room during the first 48 hours to sit back and refactor everything into OOP style, or at least OOP style where it made sense, so my whole code was just tuples, enums, and tables for data, and that really slowed me town. Still, I kept going, because I didn't have time to refactor, until the code reached 2200 lines, and then I really didn't have time to refactor, because it was so much code!

Rolling my own pathfinding

This was more or less an unforced error. I thought I could get away with a simple kind of pathfinding for enemy moves, some sort of greedy heuristics-based system that just tries a couple of moves in the right direction and returns when the solution is good enough. I should have used A* right away.

Unfortunately, I had already simplified the unit movement code to move only in a straight line, to simplify both the mechanics and the enemy AI. Later I had relaxed that to one corner turn by move action. I really liked how this made moves actions into more "chunky" decisions. If you move over a mountain, then your action is limited by the mountain movement range. This means I don't need to run A* on the player's turn. The movement is pretty deterministic, there are only two paths (with one corner) to check.

For the enemy movement, this turned a problem that could have been simplified or sped up into a problem that could be solved with A* or Dijkstra, but not any further! If only I had made the system use floating-point costs under the hood, so that "You movement range is 5 on fields, 8 on roads, and 2 on mountains" would translate into floating-point costs of 0.2, 0,125, and 0.5. That would have been easy! I could cache a lot. But with my system, I couldn't just cache the cost of moving over the mountain, because it really matters where you start. If you start on a road, you can stay on the road! In terms of game design, this is great! In terms of performance and caching, it is horrible. It wouldn't have been a problem if I hadn't written this all in Python. Maybe somebody in the comments can tell me why and how I could do JPS (jump point search) anyway, but it wasn't obvious to me, and I boneheadedly tried to do something weird instead, and that didn't work at all.

I only fixed this in the post-jam bugfix version, with the drawback that the game now freezes during the enemy turn. I can't stress enough how bad of an idea it actually was to do greedy depth-first pathfinding, and how correct it felt at the time, because A* was slow, and I couldn't do JPS!

pathfind.png

To illustrate the problem some more, here's an example of a tank that can move two spaces over mountains, and four over open terrain. Although the top path and the bottom path contain the same amount of mountain tiles, the bottom path takes one less action.

pathfind_2.png

If you try to find the optimal path over multiple moves, this leads to quite complex pathfinding situations like this one.

Procedural Generation

This is also an unforced error, but not as bad. I thought I didn't have time to design a map, so I used Perlin Noise to generate the outline of the map, more Perlin Noise to place mountains, and some greedy iterative process (I never learn!) to route two roads through the map. And then I also needed to place the units! The maps are all samey, and there is no reason I couldn't just have drawn a static map and shipped it with the game. There was no upside to this, apart from saving myself the time to write something that loads the level from an ASCII art map, but that would still have been faster than the code that randomly places units. It's not like there isn't already a python module for loading TMX maps!

Broken State Machine

I really lost hours of time on my game because I didn't properly do the state machine the first time. Now using a state machine in the main loop was itself not the error. You can achieve the same goals with OOP, and make one subclass for every state, with judicious use of design patterns like "Template Method" and "Subclass Sandbox", but realistically, a state machine like I ended up with is a better fit. There is a lot of overlap between different states: In every game state, the map needs to be drawn, and in most states, the game needs to read inputs. If you wanted to mirror this with a class hierarchy, you would end up with multiple inheritance, mixins, or questionable is-a relationships. Instead of one big switch statement, there are all kinds of conditionals for different things that happens in different states. The problem here was that a) I didn't use enough states, and b) I switched states in the middle of my game loop, inside the game loop logic. ```py while gameloop: if state==State.ENEMYTURN: ... planmovements() ... state=State.PLAYERSTURN if state==State.UNITMOVING: ... if state==State.PLAYERSTURN: ... if clickedendturnbutton(): state=State.ENEMY_TURN

# THERE IS NO COMMON GAME UPDATE LOGIC HERE
# BECAUSE THE GAME DOES NOT UPDATE EVERY TIME
# IT IS TURN BASED

calculate fog_of_war()
draw_map_and_units()
draw_particle_systems()
if state==State.UNIT_MOVING:
    draw_unit_path()
    ...
if state==State.PLAYERS_TURN:
    draw_UI_buttons()
    ...
update_screen()

``` The correct way to do it is this:

```py while gameloop: state=checkswitchstate()

if state==State.ENEMY_TURN:
     next_move=planned_movements.pop(0)
     ...
     if empty(next_move):
         switch_state(State.PLAYERS_TURN)
if state==State.ENEMY_TURN_PLANNING:
     plan_movements()
     ...
if state==State.UNIT_MOVING:
     ...
if state==State.PLAYERS_TURN:
     ....
     if clicked_end_button():
         switch_state(State.ENEMY_TURN)

# NO COMMON GAME UPDATE LOGIC HERE AGAIN

calculate fog_of_war()
draw_map_and_units()
    if state==State.UNIT_MOVING:
    draw_unit_path()
    ...
if state==State.PLAYERS_TURN:
    draw_UI_buttons()
    ...
update_screen()

```

I had to add a system to transition states between frames, and I added more states to better mirror actual gameplay in the bugfix version. Now the "switching-during-game-logic" thing would never have been possible with the OOP approach. Every state has a draw() method, an update() method, and maybe additional methods for input handling and UI drawing, if you want to make it more fine-grained. But all I really needed to do was to switch in defined places, and not let the player press a button to switch from "enemy turn" to "player's turn". That led to a lot of crashes.

Project/Time Management

Originally, I wanted to participate in the compo. That's how far off I was.

Scope

The game idea sprung out of Zeus's head in full plate armour – some time around 2013. I have to admit, I wanted to do a game like this for a long time: A war-game where you are limited by the fog of war. The idea in my head was considerably more elaborate, and instead of grid-based and turn-based, it was active-time battle, or real-time with pause. I keep a notebook (paper!) with my old game ideas I want to get around to, and this idea was waiting for me.

The design I went with was much more Advance Wars and much less Company of Heroes. Still, it was too big of an idea: Supply lines, pathfinding, turn-based fighting, enemy units who autonomously counter-attack when attacked, spotting for artillery, fuel, ammunition, food, fog of war, all these feel non-negotiable.

Sprites First

I started with the game's artwork. Since I had a pretty good idea of what I wanted the final game to be, I started by drawing all the sprites for the game's map tiles and units. This was kind of a mistake, because it committed me onto a path where I would use all these tiles and sprites, or my work would be wasted.

This didn't just commit me to a fixed scope, it also committed me to a certain design. I hadn't started with grey boxes or programmer art. On the other hand, I also didn't really have time to make the pixel art bad first, and then good, and it didn't take that long to get it right the first time.

No design iteration

Since I was running out of time, and since I had already committed to all these units and mechanics, I had no time to iterate on the basic design. I could have started with the idea of decrypting intercepted messages, and made that an integral part of a different kind of game, maybe some sort of rogue-like where you need to interpret scrolls you find on the floor, or a first-person Myst-style puzzle game. The connection to the theme "Signals" was always set in stone though, so I kept the military theme.

There was also no time for play-testing iteration. I didn't show the working game to anybody before I submitted it. My normal philosophy is to have something that is playable (if not winnable) during the first day.

Non-Core Systems

Even worse for time management, I started with the systems that were core to the theme, not the systems that were part of the core loop, so I started with the map and units, message decrypting, coordinate scrambling, and fog of war. Then I wrote out all the unit stats, and then I actually implemented unit movement. In the middle of day two, I decided not to add a certain feature to the signal decoding (you won't know what is missing because I am not telling you). At the end of day two, I finally had player movement. I added a day, and in the evening of day three, I had the enemy player working, if only barely.

The deeper cause of the problem wasn't the design, but the fact that programming little systems and mechanics is fun, and there is little relationship between how much fun something is to code, and how much fun it is to play, or how vital it is to the game design. You can get lost in endless rabbit-holes with tangential game mechanics, or you get get carried away implementing engine features, just like you can get lost polishing your UI at the expense of game content.

Game Design

The design of Signals Intelligence was too rigid, too elaborate, and had too many moving parts. But more than that, I think I can determine a number of problems that aren't just described by "complexity". These are mistakes other people can learn from!

The Slow Death

The design of Signals Intelligence originally included some kind of "raw material" resources, but I quickly settled for a "money" resource, and then I didn't even do that, because fo time constraints. I thought it would be kind of funny if the GODLESS COMMIES didn't use money, and all their production was based on materials like "oil", "iron", "wood", "copper" and "potatoes", whereas the FREE WORLD uses the almighty dollar.

The version that exists now has some element of inevitability, because there is no way to build or buy new units. The main problem with this is that you can get to a state where you know you are going to lose, because you ran out of units, but the game doesn't know it, yet. I should really have added a "give up" or "try again" button, a menu, or a main menu to return to.

Anti-Pattern: Enemy AI, Level Design, Balance

Certain genres of games live and die not with their design or game mechanics, but with their content. In the case of both single-player strategy campaigns, and single-player stealth games, the content is the "enemy AI" and level design. AI is such an overhyped concept these days, but let me be clear: I don't mean LLMs, and I also don't mean alpha-beta search or GOAP. By AI, I basically mean "scripted behaviour". I think "AI" is a misnomer, but that's what it is called.

In both stealth games and single-player strategy campaigns, you don't want the computer to think through all the options. You want the computer to present a highly predictable challenge, almost a puzzle, so that the player can slowly overcome it. You don't want to play the campaign of StarCraft, Command & Conquer, or Age of Empires against a computer who is actively trying to in. You want to play against a computer who is beatable, again and again, in varied scenarios. In stealth games, you don't want to play against enemy lookouts who go out actively looking for you. You want them to say "It's probably just the wind" until it's too late.

This is really difficult! A good single-player strategy campaign is based on varied levels, with custom scripted behaviour, and that's a lot of work for a game jam. It's even more work to build a game AI that actively tries to win, because that's not only hard to program, it's also a complete nightmare to balance. It's easy to go from "Enemy AI that always glitches out" to "Enemy AI that always wins". Balancing an enemy that always tries to win is hard.

At this point, I usually start to hear the siren song of multiplayer, and I think "This could all be solved by making this a two-player game". Just imagine all the NPC AI/design/scripting/coding work Chris Hecker avoided by making Spy Party multiplayer!

It's not a good idea, of course. Sure, if instead of inventing a new chess AI for your new chess variant, you just let players play against each other, you only have to implement the game mechanics. Sure, if you get players to play against other humans, they will have fun figuring out the game together, even though they wouldn't have fun alone. But it's usually a bad idea during a game jam. Players won't have another jammer to play with at home, and they will be unlikely to rate Ludum Dare entries against their romantic partners, non-gamedev friends, or family. By making a multi-player game for Ludum Dare, I cut my player base down by a factor of 20 to 100, and I run the risk of getting comments that just say "looks interesting. I had nobody to play with so I played against myself".

The best case scenario is to implement online multi-player with matchmaking, so players have somebody else, but in the case of a game jam, that someone else will be rushing through 20 games to rate, and they won't all be online at the same time. That is, even if I could just snap my fingers and implement online multiplayer for Signals Intelligence, it wouldn't actually help the player experience.

If I make the game a two-player affair, I will have to make some concessions, and I would probably have to replace the "Signals Intelligence" gimmick, or at least tone it down. I can't expect the red player to actually have his units wait half a turn before attacking. Also, I would need to implement some sort of two minute turn timer.

Game Design Anti-Pattern: The Gimmick

If your game can be described as "$GENRE but with a twist", there is always a risk: What if your twist is bad? What if your game is better without the twist? What if your game would be better if you just made a generic $GENRE game?

The standard game design advice is probably to ignore the negative feedback and to double down on the twist. If your game is a puzzle platformer with a gimmicky gravity mechanic, you should consider toning down all the platforming, and to focus on the gravity puzzling. If your game is a rouguelike with chess elements, and the players don't like the chess elements, maybe you should re-frame it as a chess game with roguelike elements.

In the attention economy, the gimmick presents both a problem and a solution. Without the gimmick, YouTubers and games journalists wouldn't know what to say about your game, or how to describe it to audiences, and they might not even play or review your game, because "just another decent entry in the genre" doesn't cut it.

If you just look at it from a game design perspective, and not from a marketing perspective, the best advice is often "kill your darlings". If the chess elements make the roguelike that much worse, and you are trying desperately to shoehorn them in, maybe cut them. Maybe cut the whole gimmick and just make a good game!

Signals Intelligence could probably be a more fun game if it didn't have the signal gimmick, the decyphering, but then it would not have an identity. It would just be a turn-based tactics game that lacks complexity.

Anti-Pattern: Hidden Complex Systems

I have made this mistake before, and I have identified it. I even wrote a post about it. If your game has complex systems, you have to expose them to the player. If there is an enemy AI that makes interesting plans, it has to inform the player. If there is a trading system and all the enemy factions trade with each other, the player must know this. If your world is populated with little critters who eat and sleep and reproduce, then you better show little thought bubbles with food or hearts or ZzzzZZZzzz to explain why the critters are doing what they are doing. Otherwise, you might as well not bother. If the system is hidden from the player's view, the actions might as well be random. In Signals Intelligence, I have implemented fuel and ammunition as a game mechanic, so you can cut off enemy units from supply. You can starve enemy artillery, if you are good at decyphering, you can confidently attack enemy tanks and artillery with infantry, because they are out of fuel.

Most of the time though, the focus on Fog of War and signals means that the player won't interact with these systems directly, and all the resupplying and resource management is invisible to the player. It might as well not be there. As the developer though, you often struggle to see it. To you, this is all obvious and transparent. You know what moves the enemy is making under the fog of war, or just outside the edge of the screen, because you wrote it. You don't interact with it as "the enemy makes a random move at a random moment", because to you, it's all there.

Anti-Pattern: Vertical Slices/Minigames

I have already learned that making a game out of half a dozen minigames is a bad idea during a Ludum Dare. It's tempting, but usually you can't make something like WarioWare or Mario Party or GameSoup or even McPixel during a game jam. It's just too much work, and you waste time on the weakest link, or on games you cut from the collection. Adding more content to the signal part of Signals Intelligence would have meant adding more screens, more minigames.

It might be fun to imagine an interrogation mini-game or a mini-game where you develop microfilm, or a minigame about numbers stations, or one where you analyse enemy newspapers. But once you solve these, they would all be dead weight, in the same way the coordinate de-scrambling is dead is boring once you figure it out, and the way the whole signals intelligence stuff is redundant when you have enough vision of the map.

What Went Right

Symbol Drawing

screenshotemitch/emcodebook.png Isn't this fun? You decode the enemy symbols by drawing, MSPAINT style. This has to be worth something! It neatly sidesteps the question whether the game will tell you if "Artillery" is the correct answer. It obviously won't.

Mechanical Theme

I had considered something about Morse Code, smoke signals, or a dating sim where you have to spend as much time as possible speed dating without ever getting the people you're dating enough information to make a decision. Mixed signals, get it? Yeah, I'm glad I went with SIGINT as my theme, too. I thought it was kind of obvious, but I have seen multiple games about routing traces/signal wires on circuit boards, and not that many about intercepting messages. I really like the theme, and I like that my interpretation is mechanical, deeply integrated into the game and not just something I mention in the intro, not a stretch, but still straightforward.

Cold War Setting

At least nobody told me yet that it's offensive, so there's that.

Mouse-Only Controls

At some point I considered to add some things that require keyboard presses, but I decided to just use the mouse, and that made it possible to play the web version on an iPad. I hadn't even thought about it at the time!

PyGame-CE

I couldn't have made this game with an actual engine. I'm sure I could have made it with raylib or libGDX, and I'm sure there are turn-based game construction kits for Unity and Godot, just like there are RPG kits, and point-and-click adventure kits. PyGame-CE has all the stuff I needed, and some features that I used that the old pygame didn't have. I literally couldn't have made this like this game five years ago, because old pygame didn't have some features I rely on.

Pygbag and WASM

In the past, I have used certain game engines and libraries just because I wanted to let people play my game in the browser. I like making games by writing Python code. All I need is GNU Emacs and aseprite and I'm good. That doesn't mean I don't like Godot or Flixel or Unity, but a full-fat engine is not what I would choose in a game jam. Pygbag/pygame-wasm let me run my PyGame-CE code through WASM, and it just works. So next time, I won't have to choose between my old comfy shoes and having a version playable in the browser. I still might, if I want to make a 3D game.

Lessons Learned

I learned very little. To be honest, I already knew most of the lessons, and yet I persisted, against my better judgement. I ignored my own rules. I had a ton of fun, though.