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:
@@ -10,7 +10,6 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
@@ -105,15 +104,27 @@ def hard_mode_violation(guess: str, prev_guess: str, prev_pattern: str) -> str |
|
||||
return None
|
||||
|
||||
|
||||
def answer_for_seed(seed: int) -> str:
|
||||
"""The hidden word for a seed.
|
||||
def fnv1a32(text: str) -> int:
|
||||
"""FNV-1a, 32-bit. Chosen because it is trivial to reproduce exactly.
|
||||
|
||||
Seeded from a dedicated Random rather than the module-global one, because
|
||||
TextArena seeds the process-global RNG and anything sharing it becomes
|
||||
order-dependent under concurrency.
|
||||
A language's built-in RNG is not portable: `random.Random(7)` is a
|
||||
Mersenne Twister and there is no honest one-line JavaScript equivalent, so
|
||||
seed 7 would pick one word here and a different one in the browser. Every
|
||||
permalink on the site would then disagree with the recorded run it claims
|
||||
to show. A hash sidesteps the whole problem — both sides compute the same
|
||||
integer from the same string, and there is nothing to keep in step.
|
||||
"""
|
||||
h = 0x811C9DC5
|
||||
for byte in text.encode():
|
||||
h ^= byte
|
||||
h = (h * 0x01000193) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
def answer_for_seed(seed: int) -> str:
|
||||
"""The hidden word for a seed. Identical in engine.ts — see fnv1a32."""
|
||||
pool = answers()
|
||||
return pool[random.Random(seed).randrange(len(pool))]
|
||||
return pool[fnv1a32(str(seed)) % len(pool)]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
Reference in New Issue
Block a user