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: th...
For further actions, you may consider blocking this person and/or reporting abuse
The regression test as written cannot fail on this incident. It uses one board - highlight, leave, come back to the saved board - while the collision needed two boards that share a word, since the card lit up only because a word you had highlighted in the previous mode also appeared on the new 25-card grid. A clear that ran on restore but not on board change would pass that assertion and still reproduce the original lie on the next mode switch, so the case worth pinning is switching into a mode whose grid shares a word and finding nothing lit. That is also why acceptance is not the line your table actually draws: the human-typed clue word is unsubmitted too and it is restored, so what separates it from the highlights is that a highlight is a reference into one particular grid while the clue word is a standalone string no board change can invalidate.
You’re right, thank you. The restore-only test used one board, so it could pass even if highlights were cleared on restore but not on board change. The collision needed two grids sharing a word.
I’ve tightened the regression to highlight on board A, switch to board B that still contains the same word, and assert nothing is lit. That case now fails if the board-change clear is removed. I’ve updated the post to reflect the stronger regression case.
Your point about the highlight being tied to a particular grid is useful too. It strengthens the persistence boundary: the typed clue is durable user work, while the highlight is transient intent whose meaning depends on the board it came from.
Appreciate the careful read.
The grid test you just adopted covers two of the three distinctions the table needs. Acceptance separates submitted history from everything unsubmitted. Grid reference separates a highlight, a reference into one particular board, from the typed clue, a standalone string no board change can invalidate. The unsubmitted AI clue has two parts, and only its targets are grid references. Its word is a standalone string, unsubmitted, in the same field as the typed one, and the table regenerates it while the typed word is restored. Neither line separates those two cells. What is left is who produced the value, and the post says why that matters: a model swap or config change can take effect on the next reload instead of replaying the last output.
That third line deserves the regression the collision got, in two cases, since the two words need not share the field at once. First, type a clue, reload, assert the word is still there. Second, not a same-config reload: under one config your own refresh can already turn INSECT into BODYPART, so asserting the old proposal is gone passes or fails on whatever the model said the second time, flaky rather than blind. Anchor it to the config sentence instead: let the model propose, switch to a config whose answer you control, a stub that always returns one word, reload, and assert the field holds that word and not the saved one. A replay from cache fails every run, an honest re-ask passes every run.
If the snapshot type is where the acceptance boundary gets documented, that second test keeps it honest about the boundary the type does not name: two values in one field, the same acceptance state, and only one of them typed by a person.
Good catch on provenance as the third distinction. The grid test pins acceptance and grid reference; it did not pin who produced the value.
We’ve added reload regressions for that case: a human-typed clue survives reload, while an unsubmitted model-generated clue is regenerated. The test controls the model’s second answer so replaying the saved output fails deterministically.
I’ve merged that coverage and updated the post to reflect the stronger regression tests. Thanks for pushing on it.
With provenance in, the last row of the table lands on the wrong side of its own line. The manual clue count override flag is unsubmitted, it is a standalone value rather than a reference into any particular grid, and a person sets it. Those are the same three answers the typed clue word gives, and the typed word is restored. The flag is cleared. All three distinctions predict restore for it, so either that row belongs on the other side of the table or there is a fourth line the thread has not named.
The cost is not only taxonomy. The post has targeting clicks sync the clue count, so the flag is what marks a count as set by hand instead of derived from the lit cards. Restore keeps the count and drops the flag, which leaves a hand-set count wearing a derived label on a board with nothing lit. The first targeting click of the next drafting session then re-derives the count and overwrites a value the table promises to restore. That is the original failure turned inward: a field holding a value whose origin the code has forgotten, except the forgetting now happens inside the restored state rather than across two grids.
The check has the same shape as the two just merged. Override the count by hand, restore, click one card on your own team, assert the restored count survives. If it does not, the restored row reads wider than it is, because the count comes back and then does not stay.
This sent me back through the implementation because the mismatch you pointed out is real at the state level: we persist
clueCountText, but not the manual-override flag.The important detail I’d missed in the post is what that flag actually means. It isn't durable provenance saying “this count was human-authored, so preserve it.” The count is normally derived from the current target selection, and manually typing a count temporarily overrides that derived value. We added this behavior deliberately during testing: the manual value remains until the target selection changes, at which point the new selection derives a new count and supersedes the override. We have a test pinning that behavior even without a reload.
That means
type 5 → change targets → derived countandtype 5 → reload → select a target → derived countare intentionally consistent. Persisting the override flag wouldn't preserve provenance that the product currently needs; it would change the interaction semantics.I think you've found a gap in my framing, though. The table makes the override flag look like provenance that we're arbitrarily discarding, when it's really transient state describing whether the current count is temporarily overriding the value derived from the current target selection. The count itself is durable user work; that relationship to the current target selection isn't.
That's another distinction I hadn't made explicit in the post: persisting a value doesn't necessarily mean persisting the derivation state that governs how it changes. I'd noticed this behavior before, but it sat in the grey area of product taste and I didn't have a useful way to classify it.
I have a small suggestion.
My English is not very good.
Would this game support multi‑language selection🤣
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 🙂
Looking forward if you get round to it. Thanks for considering my suggestion😝
A little update on this. I ended up building it 😄
Chinese is now available as the first additional language. You can select it when starting a game, and the word list and AI clues and guesses will use Chinese too.
I ended up doing a bit more than just adding a Chinese word list. The game now has proper support for different word packs, so Chinese is the first one using that structure rather than being a one-off special case. That should make it much easier to add more languages and other word packs in the future.
It turned out to be a useful improvement to the game architecture, so your suggestion gave me a good reason to build it properly 🙂
I can't judge the Chinese word choices and gameplay as well as a native speaker can, so if you get a chance to try it I'd be interested to hear how it feels.
Haha, thank you so much! You’re incredibly efficient. I’ll give it a try right after I publish Stratagems #29. I’ll point out any grammatical issues I come across.👏
That distinction between accepted state and thinking aloud state is something I have run into without ever naming it properly. Always treated persistence as one bucket, restore everything or restore nothing, and never stopped to ask whether two values sitting in the same field actually deserve the same treatment. The highlight tied to a specific grid versus the typed clue being a standalone string is a small detail but it explains a lot of weird bugs I have shipped in the past. Good writeup, saving this for the next time I touch anything with autosave.
I’ve tried a few rounds, but I still haven’t really gotten the core of the game. Maybe tabletop games just aren’t for me, hahaha 😂
Haha, fair enough 😄 Codenames can take a little while to click if you haven't played it before. The core idea is really just trying to find one clue that connects several of your team's words without leading the guessers toward the wrong words.
Thanks for giving the Chinese version a few rounds though. Your suggestion ended up pushing me to build the broader word-pack support, so it was still a really useful addition to the game 🙂
That’s awesome to hear! Happy my small suggestion helped the project. I’ll keep trying to get the hang of Codenames😂