DEV Community

Cover image for The board came back. The highlights lied.
Michael Truong
Michael Truong

Posted on

The board came back. The highlights lied.

I ship Codenames AI, a web game. Each game mode keeps its own save in localStorage. Reload the tab, switch to a different mode, come back later: the board, turn, clue history, and in-progress results all come back. That felt like a win until I added spymaster clue targeting.

While drafting a clue, I can click cards on my team. Those clicks sync the clue count and show which words I had in mind. They are visual intent only. They are not part of the clue submission payload.

I assumed that if I reloaded the same game, the UI could restore those highlights too. Same session, same cards, same mental model. On a single-mode refresh that was mostly harmless.

Switching game modes was not. Each mode loads its own 25-card board. Leftover targeting clicks stayed in memory. If a word on the new board matched a card I had highlighted in the previous mode, that new card lit up. I had never clicked it. The board was truthful. The UI was lying about what I was drafting.

Clearing those highlights when the board changed would have stopped the lie. That cheaper fix was not enough. I had already assumed a same-game reload should bring the clicks back. Keeping that assumption and stopping the leak meant persisting the clicks per mode, the same way we persist the board. That would have treated a thinking aid like a move. The collision forced the real question: should coming back restore those clicks at all?

What coming back restores

People now expect drafts to survive a reload. Google Docs made that the default: leave, come back, the paragraph is still there. Autosave is already on in this game. A lot of players never press Save. They just come back.

A saved snapshot carries the board, the turn, typed clue fields, history, and any in-flight result. It omits targeting clicks on purpose. The snapshot type documents that omission in gamePersistence.ts.

A clue you typed already works the Docs way. An unsubmitted AI-generated clue does not. Refresh asks the model again. The clue word and the highlighted targets can change (INSECT and one card can come back as BODYPART and two different cards). That generation has not crossed into accepted game state. A useful consequence is that a model experiment, upgrade, swap, or config change can take effect on the next reload instead of replaying the last output.

Human targeting clicks are the same shape: a thinking aid around a later submit, not a move on the board.

Restored from snapshot Cleared or regenerated on restore
Board, revealed cards, team, outcome, and pending guess result Human-intended target highlights
Human-typed clue word and count Unsubmitted AI clue and targets
Submitted clue history Manual clue-count override flag

That is why "persist domain state, discard UI state" is the wrong summary. Some UI state should persist (the typed clue). Some AI-generated state should not, even though it occupies the same fields. React state versus a domain object does not tell you enough. The snapshot has to encode what the product has accepted as true, not whatever happened to exist in the UI, and not because anyone pressed Save.

What restore actually does

Switching modes and reloading the tab both restore the saved game, then clear the drafting pose. Targets, the manual count override, and any cached AI overlay go with it. The same clear runs on new game and after a successful submit, so a leftover thinking aid cannot leak into the next turn.

So restore is not "rehydrate everything the component used to know." It is "restore the game, then clear the drafting pose."

The regression test I care about is behavioral: highlight a card, leave the game, come back to the saved board, and confirm the highlight is gone. Highlights only reflect choices made in the current drafting session.

Same problem, different surfaces

The leftover highlight was not a styling bug. Dropping the clicks from the snapshot fixed the category: a thinking aid is not current truth.

If you are building agent UIs with drafts, wizards, or "thinking aloud" interactions, coming back poses the same question. Autosave does not settle what on the screen is current truth. Keep proposals and thinking-aloud hints ephemeral, even when they sit in the same inputs as accepted work. Document that acceptance boundary in the snapshot type so the next contributor does not "helpfully" persist whatever the component last held.

Takeaway: Coming back should restore what the product treats as current truth, not whatever happened to be on screen. Accepted work stays. Proposals and thinking-aloud hints do not, even when they share the same fields. The persistence boundary is semantic, not architectural. Test what coming back looks like, not whether the save still loads.


If you'd like to see the project that inspired these lessons, you can try Codenames AI.

Top comments (3)

Collapse
 
xulingfeng profile image
xulingfeng

I have a small suggestion.
My English is not very good.
Would this game support multi‑language selection🤣

Collapse
 
michaeltruong profile image
Michael Truong

Codenames itself has official editions in multiple languages, so supporting additional language decks is definitely something I could explore.

The main work would be adding language-specific word lists and making sure the AI gives good clues and guesses in each language.

I’ll add it to the ideas list 🙂

Collapse
 
xulingfeng profile image
xulingfeng

Looking forward if you get round to it. Thanks for considering my suggestion😝