Jam Technical Post-Mortem
The code was obviously a mess - we did everything in 3 days. And then we spent the next 48 hours bug fixing. This cost us in early jam exposure and cost us the opportunity to fix badly needed UI elements and such that we later received feedback for. I did most of the core game loop and CPU player decision logic, so I will focus on what went wrong there and how I can do better in future Jams.
Start with the Core Loop - brutally scope MVP
After we agreed on the theme and the general conceptual rules, the first bit of code that I jumped into was the AI decision script. In previous game jams, I had learned that I wanted to get the core game working as quickly as possible so it could be tested. This was also strategic since we had not decided who would work on the game scene code - I could code the AI decision script in C# and just make regular interfaces available if syndeos wanted to code the main game loop in gdscript. The AI was driving half the game loop (the other half would be like 3 buttons in the UI) so I thought this was a good place to begin.
Unfortunately, I had learned somewhat of the wrong lesson from the previous jams. I really should have started with the actual game loop :) and even if I did write the AI script, I should have scoped it down to MVP. My first draft was too complex, in that I thought through the entire decision making process, planned it out, and then wrote the code.
I could have just made the signal emit and connect points, and had a random action. That would have been enough to unblock the game loop and we could actually write the logic later. In my defense, it is hard to separate Game Rules (what actions are legal) from strategy - which action the AI should actually take.
Core Loop should drive game flow - not daisy-chained signals
I learned from previous projects:
- Don't go grabbing other scripts/nodes arbitrarily
- Don't traverse on your tree as the way to trigger other methods
- Keep your signals organized and flow it through a signal bus (keeps you from having to do the first two points)
- C#-Native Async and Timers are goofy in godot 3
So we did that, and honestly this game is so much better organized than the signal conspiracy board I still haven't gotten untangled from Rusty Goes to Space.
But we ended up in a situation where the main Game Scene loop would call out to the Dice Manager (which triggered the dice actions and reported results of rolls) and then Do Nothing until Dice Manager sent a signal. This was out of whack and shared the core game flow responsibility across multiple parts. Besides being a poor design, it led to the issues in the next session.
What I should have done is to keep the Game Scene in charge of driving the game - if it needs to wait (for an animation, physics, Ai decision, etc.) it should be in charge of waiting and picking back up - not ending and then asynchronously beginning a second function when a signal is arbitrarily received. And also bite the bullet and just figure out how to do async right in godot C#.
Beware State Machine race conditions
The design flaw above led to some poor outcomes:
- The Dice Manager can send a signal arbitrarily and impact core game loop (and it did)
- The state machine tried to outline the flow of the game, but it really just moved in response to stimuli - player clicking, dice manager, AI script
- It was very possible to enter softlock conditions - where the state machine required external stimuli to progress the game, but it never got it
I made a state machine class that defined the game states, triggered events when state was changed, and had code blocks to run once the state had transitioned. This was a turn based game, so everything should be driven by decisions of the Player and AI Player. Except there was one element that wasn't - the dice rolling was actual 3D physics that actually had to settle and report back a result.
This is where I spent most of my time troubleshooting and bugfixing - and the bugs were really bad to where the dice that were on the UI were not the actual dice rolled, the internal game logic would hold on to stale values, and there were even sequence breaks and softlocks that would randomly occur.
The core problem was that in order for either player to start, the 3 common dice had to have finished rolling AND their own 2 dice had to finish rolling. (and to complicate matters, really the pool dice had to report back before the handler code for the 2 dice should run, since hand calculations in that script needed all 5 dice). And throw in some other bugs, like all 7 dice sending an (unrequested) signal when only 2 of the dice were rolled.
The fix is kind of the same as above - things that require the main loop to wait should be tasks that don't block the main thread, but pause code execution of the logic until the task is completed. Also, state changes should happen only when they are supposed to happen (per core game flow - not by arbitrary signals)
Duplicated and conflicting calculation logic
This was sad. But pressed for time, I would just copy/paste some code and modify it slightly. I ended up "solving" the same problem 5 times in 5 slightly different ways. Take the hand ranking for example - it should be easy to say Runt -> Pair -> 2 Pair -> so on... But I never had a shared calculation for hand rank UNTIL THE LAST TIME I HAD TO SOLVE IT (lol so the "shared" calculation was used only once).
Also, I decided it would be easier to pass things like raw strings, integers, etc. rather than deal with potential gdscript / C# boundaries in the signal bus. So that was really gross.
The specific fix here is just use an Enum with the possible hand rank definitions in the right order. I can even embed that Enum in a Class and put custom methods in that class to keep other logic consistent and contained; The more general principle is to just design the components right, or at least when they are going to be used in more than one place. If code logic is reused, put it in a function. If a definition is to be reused, put it in a data structure, If a bit of information and logic need to be used in multiple scripts, put it in a class.
This was an embarrassingly simple solution. It didn't make any visible bugs (compared to the last 2 more complex issues) - but it did make writing the code stupid, and it contributed to sunk cost fallacy, and created tech debt that we have to unravel to fix the other things.
Making the Same Mistakes
I should count this as a win - I collaborated with another dev, I wrote more (and better) code in a shorter amount of time compared to the past game jams, and I finished a project where I originally thought the scope was too big (probably still was too big for me to do solo).
I really like Game Jams - they are actually one of the only times I get a project to completion - having the deadline and constraints helps me finish.
But I still have frustrations that the feedback seems to be similar - I keep "learning" the same lessons:
- Start with MVP and core game loop
- Get playtesting/ers early
- UX needs to be intuitive and clear
- Tutorial needs to be in the game - and interactive enough (especially for strategy games)
- And I never seem to actually get to the polish and juice stage
So that is why we will be pushing at least 1 update (maybe more) to "Honest Abe Cannot Be Trusted". We'll get good experience on what TO do.
Get Honest Abe Cannot Be Trusted
Honest Abe Cannot Be Trusted
Dice poker vs the most honest man in America. If you can't trust Honest Abe, who can you trust?
| Status | In development |
| Authors | checkmate101, rogueemberlight, syndeos |
| Genre | Card Game, Strategy |
| Tags | 3D, abraham-lincoln, bluffing, Dice, Funny, liars-dice, poker, Short, Singleplayer |
| Languages | English |
More posts
- v0.3.0 - Claim your Faces and Keep a Dice!3 days ago
- v0.2.4 - Bugfixes and Tweaks11 days ago
- v0.2.0 - First Post-Jam Update!17 days ago
- Jam Post-Mortem (Abe's "Honest" Assessment)30 days ago
- Jam Bugfix #2 (still legal to do)43 days ago
- Jam Bugfix #1 (legal to do)43 days ago

Leave a comment
Log in with itch.io to leave a comment.