Notes

Building a pass-and-play party game: lessons from fixing five bugs

Published 2026-10-06 · 6 min read

A pass-and-play party game sounds simple: one phone, a few people, no accounts, and no network once the page has loaded. But that simplicity leaves a surprising number of ways for things to break. This is a bug-by-bug account of fixing five of them in Mystery Agents, a hidden-spy game in the style of Spyfall for three to eight players, plus two rules that were missing entirely. The lessons apply to any turn-based app, and opinions are marked as opinions.

How the game works

Everyone at the table learns the secret location except one player: the spy. Players take turns questioning each other until someone's answers do not add up, then the table votes. In pass-and-play mode, a single device is handed around so each person can read their role privately before passing it on. The mode looked finished when it arrived, but it was not: every round eventually broke something.

Why these bugs hid

Most appeared at a boundary: the end of a round, or the start of the next round with leftover state still in place. A quick look at the app never reveals that. You have to seat three or four people, play a full round to the end, and see what happens next. Opinion: for any game or workflow with rounds, "I opened it and it loaded" is not a test.

Bug 1: the results screen crashed at the end of every round

The end-of-round summary put a row of stretched columns inside a scrolling list. Asking a row to stretch inside something scrollable means asking it to fill a height with no defined limit, which the layout engine cannot resolve. As a result, the screen meant to announce the winner crashed every single time. The fix was to wrap the row so its height comes from its own content rather than an undefined parent. The rule is narrow, because the same stretch behavior works fine in a non-scrolling layout, but you only find it by finishing a round.

Bug 2: the timer ran at double speed

Backing out of a round halfway did not cancel the countdown already running. Starting a new round then created a second timer alongside the orphaned first one, and both counted down the same clock. To a player, the round seemed half as long as it should be. The fix was to make every exit from a round, not just the normal time-is-up path, stop the timer explicitly, and never create a new timer while an old one might still be alive. The general lesson: anything that starts must have a stop on every path out.

Bug 3: a tied vote was decided by who was counted first

The original logic scanned the votes and kept whichever name first reached the highest count. So two players tied on votes were not actually tied in practice: whoever was accused earlier lost. This is easy to write because the code runs and returns a plausible winner every time. Nothing looks broken unless you know the rule it should enforce. The fix keeps every player currently in the lead and calls it a real tie only when more than one remains.

Bug 4: a button that looked ready and did nothing

While the location data was still loading, the main button said "Start game" but its handler was disabled. It looked ready, but tapping it did nothing and gave no explanation. The fix was small but important: until it is ready, the button now says "Loading location set…", so a disabled control looks disabled rather than stuck. Opinion: a control that cannot be used should say why, in words.

Bug 5: Thai role names came out as English-Thai mashups

The localization step built Thai role text by replacing English words one at a time inside a longer string. Occasionally, that left a Thai word fused to an untranslated English word with no space between them, which is readable to nobody. The fix was to stop translating piece by piece and build a direct one-to-one map covering all 163 role names, keeping the word-by-word approach only as a fallback for anything the map misses. If your app has more than one language, a plain lookup table is easier to review than string surgery.

Two rules that were missing, not buggy

First, the spy had no way to win except by talking through the whole round undetected, with no action of their own. The fix added a "spy calls the location" move available mid-round: the spy can guess where the round is set, and that guess decides the round outright. A correct guess means the spy wins, and a wrong one means the officers win. That turns the spy from a purely defensive role into one with a real decision to make.

Second, a tied vote still somehow produced an accused player because the counting logic assumed someone had to be picked. The corrected rule is that a tie means nobody is caught and the spy scores the round. That matches how the game is meant to be played and removes a way the outcome could feel arbitrary to the table.

Testing a game that depends on turn order

Unit tests of single functions would have caught neither the layout crash nor the double timer, because both exist only at the boundary between one round and the next. The test suite that came out of this repair plays the game: a small helper taps through the same buttons a real player would, in the same order, and the tests assert what the app shows after each step rather than mocking around the interaction. That is what exposed the stretch-in-a-scroll-view crash and the second timer. Opinion: for stateful, sequential software, tests that drive the interface from the outside are worth more than a larger pile of isolated function tests.

A related lesson: an ad slot nobody could reach

An earlier web build placed an advertising slot near the top of the page. It seemed reasonable until I checked what was actually at that position. The game draws itself across the whole browser window as one full-screen layer above everything else, including the slot. Clicking where the ad sat landed on the game layer, so the slot was present in the markup but completely unreachable. A slot nobody can see or click is a strong candidate for being flagged as an invalid placement, and either way it did nothing, so it was removed until there is a real gap in the layout that the game surface does not cover. You can check this on any page with the browser's element inspector or by asking the page which element sits at a given point.

Checklist for your own pass-and-play game

  • Play at least one full round with real people before calling a build done, and play a second round straight after it.
  • Make every exit path stop timers and clear round state, not just the normal ending.
  • Write down the tie-breaking rule in words before you code it, and test a tie on purpose.
  • Make disabled buttons say why they are disabled.
  • Prefer a complete lookup table over piecemeal translation of text.
  • Drive the interface in your tests the way a player would, including the boundary between rounds.

Mystery Agents is an independent game inspired by Spyfall-style hidden-role party games. It is not affiliated with or endorsed by the publishers of Spyfall.