Combinatorics and the Airsick Bag
Dead-ends in 1980s adventure games arise not from developer malice, but from the combinatorial impossibility of manually verifying massive state machines.
By Leo Marchetti
Sparked by Show HN: Automatically detect and patch walking-dead states in Sierra games · discussion

I was reading through a recent Hacker News thread discussing lucasartsifier—a tool designed to retroactively remove dead-ends from old Sierra adventure games. If you scroll down the comments, you immediately hit a very specific, deeply ingrained internet consensus: the people who built 1980s adventure games were either deeply incompetent or actively malicious toward their players.
The interactive fiction community even maintains a formal taxonomy for this called the <dfn>Cruelty Scale</dfn>. Under their definitions, the "Cruel" tier applies to games where it is possible to get completely stuck without the engine ever notifying you. You can play for another five hours, saving your game meticulously, completely unaware that you are already effectively dead. The standard narrative treats shipping a game with these "walking dead" states as a profound failure of game design.
But examining how variables and objects were tracked in the SCI0 engine reveals a completely different reality.[^1] Tracking global state across interconnected rooms isn't a problem of malice. It forces an immense combinatorics problem onto a fundamentally human design process.
[^1]: (Yes, early Sierra games used the older AGI engine. My focus here is on the memory model, and tracking global invariants gets exponentially harder once SCI0 introduced object-oriented heap allocation into the mix.)
To understand how easily these unwinnable states emerge without anyone noticing, let's look at a deliberately stripped-down TLA+ model of a classic famous TV Tropes example: the airsick bag puzzle. The player starts in an airport terminal, needs to board a plane, and has the option to interact with an empty bag or a trash can.
---- MODULE AirsickPuzzle ----
VARIABLES location, has_bag, bag_in_trash
Init ==
/\ location = "Terminal"
/\ has_bag = FALSE
/\ bag_in_trash = FALSE
TakeBag ==
/\ location = "Terminal"
/\ ~bag_in_trash
/\ has_bag' = TRUE
/\ UNCHANGED <<location, bag_in_trash>>
TrashBag ==
/\ location = "Terminal"
/\ has_bag
/\ bag_in_trash' = TRUE
/\ has_bag' = FALSE
/\ UNCHANGED location
BoardPlane ==
/\ location = "Terminal"
/\ location' = "Airplane"
/\ UNCHANGED <<has_bag, bag_in_trash>>
Next == TakeBag \/ TrashBag \/ BoardPlane
====
If we load this into the TLC model checker and run standard safety checks—asserting that the game doesn't crash, that variables don't hold illegal values, or that the player doesn't clone the bag—everything passes with flying colors. The system is structurally sound. But to verify that a game is actually playable, we have to evaluate liveness properties.
According to the TLC documentation, liveness asserts that "something good eventually happens". In our case, the good thing is that the player can actually win the game. Safety tells us the game won't crash; liveness tells us the game can be beaten. So we add a simple temporal property stating Eventually(GameWon).
When TLC evaluates this model against that temporal requirement, it violently fails.
Error: Temporal properties were violated.
Error: The following behavior constitutes a counter-example:
State 1: <Initial predicate>
/\ location = "Terminal"
/\ has_bag = FALSE
/\ bag_in_trash = FALSE
State 2: <TakeBag line 10>
/\ location = "Terminal"
/\ has_bag = TRUE
/\ bag_in_trash = FALSE
State 3: <TrashBag line 16>
/\ location = "Terminal"
/\ has_bag = FALSE
/\ bag_in_trash = TRUE
State 4: <BoardPlane line 23>
/\ location = "Airplane"
/\ has_bag = FALSE
/\ bag_in_trash = TRUE
Let's trace the exact steps that caused the model checker to yell at us. Look at State 4. The player grabbed the bag, dropped it in the trash can, and walked onto the airplane. The logic allowed this sequence because boarding the plane only required being in the terminal.
But once the player's location variable updates to "Airplane", they can never interact with the terminal's trash can again. If the airsick bag is required to solve a puzzle mid-flight to avoid an explosive decompression event, the player is now permanently soft-locked. The engine has absolutely no programmatic way to realize this has happened. It merrily continues processing player input.
If we drew a simple node-graph visualizing this branching structure, the "Win" node requires holding the bag inside the airplane. The path through State 3 and State 4 creates a completely isolated branch—a mathematically valid sequence of state transitions that permanently walls off the solution.
This is just a toy model with exactly three variables and three possible actions. A real SCI0 adventure game loads hundreds of interconnected objects, boolean flags, and actor states into memory simultaneously. To prevent every single dead-end, a human designer would have to map the exact permutation of every inventory item against every location transition, ensuring no sequence of valid interactions ever severs the path to the win condition.
Doing this for a tiny three-room game is incredibly tedious. Doing this for a sprawling open-world structure with fifty inventory items requires checking millions of potential pathways.
If we wanted to fix this in our code, we would have to add a <dfn>guard</dfn> to the BoardPlane action, explicitly checking that has_bag = TRUE before letting the player change locations. But that requires the developer to possess omniscient foresight about why the bag is needed later. It also creates a deeply unnatural game environment where the airport terminal physically blocks you from boarding your flight just because your pockets are empty. Multiply that requirement across every item and every room, and the cognitive load becomes impossible.
Modern formal methods running on multi-core processors catch these liveness violations in milliseconds by exhaustively mapping the entire state space. A 1988 QA team of teenagers with clipboards cannot manually traverse a graph of 2^50 permutations to find the exact edge case that quietly breaks the game.
Combinatorics always wins.
The original developers were simply building massive finite state machines without finite state tooling. They were heavily constrained by the era's mathematical limits. When you give a human an exponentially branching tree of variables and ask them to manually verify every temporal path, they will inevitably miss the specific route where throwing away a paper bag on screen three mathematically ruins a sequence on screen nine. Concurrency and state tracking are brutal, and shipping soft-locks at that massive scale is an entirely predictable tooling constraint, not a character flaw.
I should pause and admit my own limitations here. I don't spend my weekends disassembling 16-bit DOS executables, and reading 35-year-old architectural constraints through the lens of modern formal verification is deeply anachronistic. It is entirely possible that some teams did attempt to map these graphs manually and simply ran out of budget. My reconstruction of the memory limits relies heavily on external observation rather than primary source code, which means I might be fundamentally misinterpreting how the engine handled global state on the back end.
But analyzing historical software failures requires us to look at the actual tooling available to the people doing the work. We use markers of retroactive frustration as heuristics to judge developers, forgetting that the mathematical complexity of the systems they built often exceeded human verification.