Pin seed->word across both languages with a shared hash

engine.ts used mulberry32 and engine.py used random.Random(seed). Same seed,
different word — so every ?seed= permalink on the site would have shown a
different puzzle than the recorded run it claimed to be replaying, and nobody
would have noticed until someone checked one by hand.

Both now derive the index from FNV-1a 32-bit over the decimal seed. A hash
rather than a PRNG because there is no honest one-line JavaScript equivalent of
Mersenne Twister, and this way there is nothing to keep in step: both sides
compute the same integer from the same string. Math.imul on the JS side is
load-bearing — a plain multiply overflows into a double and diverges after the
first few bytes.

Twelve seeds are pinned as a vector in both test suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
karti-ai
2026-08-28 15:42:17 -07:00
parent a56f097f28
commit 69607fbfe9
22 changed files with 3508 additions and 86 deletions
+15
View File
@@ -72,6 +72,21 @@ def test_seeded_answer_is_deterministic() -> None:
assert answer_for_seed(1) != answer_for_seed(2)
# Pinned so the browser cannot drift. `src/demos/wordle/__tests__/engine.test.ts`
# asserts the same twelve words for the same twelve seeds. If a language's
# built-in RNG were used instead of the shared hash, these two lists would
# differ and every ?seed= permalink would show a different word than the
# recorded run it claims to replay.
SEED_VECTORS = [
"wants", "amber", "spume", "toady", "divot", "filly",
"bobby", "clews", "hikes", "lawns", "wreak", "twist",
]
def test_seed_vectors_match_the_typescript_port() -> None:
assert [answer_for_seed(s) for s in range(12)] == SEED_VECTORS
def test_rejections_do_not_consume_a_row() -> None:
game = Game(seed=3, answer="tares")
assert game.play("zzzzz")[1] is not None # not a word