Mystery deduction engine
1. Foundational mental model
Section titled “1. Foundational mental model”Mystery (/challenges/mystery) hides one country and describes its flag in words. The player gets one hint on Start and one more after each wrong guess. Every guess also shows the same color, pattern and region feedback as the Daily, from calculateSimilarityScore. The game ends on a correct guess or after MAX_GUESSES (8).
The engine has two parts:
- Hint generation (
mystery-hints.ts).getHintsForCountry(code)collects every hint that applies to the flag, drops redundant ones, sorts them by tier, shuffles within each tier, and keeps the firstMAX_FLAG_HINTS(8). - Reveal loop (
MysteryChallenge.svelte). A counterhintsRevealedstarts at 1 and grows by one per wrong guess, capped at the number of hints the flag has.
Tiers go from broad to specific:
| Tier | Meaning | Hint types (sample) |
|---|---|---|
| 0 | Palette | colors, colorCount |
| 1 | Layout | horizontalTricolor, verticalTricolor, stripes, noSymbols, redDominant |
| 2 | Structural devices | canton, nordicCross, saltire, stars, crescent, triangle, sun |
| 3 | Specific marks and shape | emblem, text, map, eagles, lions, weapons, shields, squareFlag, ratio12 |
France, for example, gets colors, noSymbols and verticalTricolor: one tier-0 hint and two tier-1 hints.
2. Implementation status & known gaps
Section titled “2. Implementation status & known gaps”Mystery is live and fully client-side. There is no server check, no seed in the URL, and no scoring beyond the counts shown on the result screen (guesses used and hints seen).
Hint coverage varies a lot by country. Over the 205 countries in gameCountries:
| Hints available | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|
| Countries | 27 | 53 | 55 | 41 | 18 | 11 |
Only 11 countries reach the cap of eight. For the other 194 the hint panel stops growing before the guesses run out: a country with three hints has no new hint to offer from the fourth guess onward.
3. Concrete implementation
Section titled “3. Concrete implementation”Reveal loop
Section titled “Reveal loop” const isCorrect = guessCountry.code === targetCountry.code; const newGuesses = [...guesses, guessCountry]; const gameOver = isCorrect || newGuesses.length >= MAX_GUESSES;
guesses = newGuesses; lastSubmittedGuessCode = guessCountry.code;
if (isCorrect) { void playChallengeWinSound(); } else { frameShake.trigger(); void playChallengeErrorSound(); }
if (!isCorrect) { hintsRevealed = Math.min(hintsRevealed + 1, allHints.length); }handleStart sets hintsRevealed = 1. The start screen copy matches: “Press Start to reveal the first hint. You have 8 guesses. Each wrong guess reveals another hint.”
Ordering hints
Section titled “Ordering hints”function mixSeed(seed: number, tierIndex: number): number { return (seed ^ ((tierIndex + 1) * 0x9e37_79b9)) & RNG_M;}
/** Broad-tier-first order; Fisher–Yates inside each tier using `mixSeed(seed, tier)`. */function orderMysteryHintsByTier(hints: MysteryHint[], seed: number): MysteryHint[] { const buckets: MysteryHint[][] = Array.from({ length: HINT_TIER_COUNT }, () => []); for (const hint of hints) { const tier = Math.min(getMysteryHintTier(hint.type), HINT_TIER_COUNT - 1); buckets[tier]!.push(hint); }Because the cap is applied after ordering, a flag with many tier-3 marks loses its most specific hints first. The comment above HINT_TIER_BY_TYPE describes this order: broad hints that split the space come first.
The shuffle
Section titled “The shuffle”const RNG_A = 1103515245;const RNG_C = 12345;const RNG_M = 0x7f_ff_ff_ff;
/** Seeded shuffle (Fisher–Yates) for reproducible ordering when a seed is provided. */function seededShuffle<T>(array: T[], seed: number): T[] { const shuffled = [...array]; let state = seed; for (let index = shuffled.length - 1; index > 0; index--) { state = (state * RNG_A + RNG_C) & RNG_M; const randomIndex = state % (index + 1);Redundancy filter
Section titled “Redundancy filter”dedupeRedundantFlagHints removes colorCount when verticalTricolor, horizontalTricolor or bicolorVertical is present, since those layouts already state how many colors there are.
4. Internal mechanics & mathematics
Section titled “4. Internal mechanics & mathematics”Why the shuffle is biased
Section titled “Why the shuffle is biased”The LCG step is meant to compute with and . JavaScript evaluates state * RNG_A as a double. With close to the product is about
so the low bits are rounded away before & RNG_M runs. At that magnitude the spacing between doubles is or more, so adding is lost too and the result is almost always even. Exact arithmetic only survives when , which means , about 0.4% of seeds.
The first randomIndex is state % 2 for a two-item tier, which is 0 nearly every time, so the pair is swapped. Measured over 100,000 random seeds against an exact BigInt version of the same LCG:
| Tier size | JavaScript seededShuffle | Exact LCG |
|---|---|---|
| 2 | reversed 99.8%, original 0.2% | 50.1% / 49.9% |
| 3 | 3 of 6 orders, about 33% each | all 6, about 16.7% each |
| 4 | 3 orders near 33%, 16 seen in total | 12 of 24 orders, about 8.4% each |
Even the exact version reaches only 12 of 24 orders for four items. The modulus is a power of two, and the low bits of such an LCG have short periods, so state % (index + 1) for small index is poorly mixed.
For France, the tier-1 pair noSymbols and verticalTricolor is a two-item tier, so almost every game shows them in the same order after the tier-0 colors hint.
How fast hints narrow the pool
Section titled “How fast hints narrow the pool”Let be the number of hints for the target and the number of wrong guesses so far. Hints on screen:
With the distribution in section 2, the expected hint count is
so on average the hint panel is complete by the fifth guess and the last three guesses rely on similarity feedback alone.
5. Threat model, failure modes & edge cases
Section titled “5. Threat model, failure modes & edge cases”- Answer exposure. The target country and its hints live in component state. Anyone with devtools can read them. Mystery awards no verified reward, so this is accepted.
- Hints that fit many countries. A three-hint flag such as a plain tricolor may still match several countries after all hints are shown. The similarity feedback on each guess is what separates them.
- Unknown code.
getHintsForCountryreturns[]for a code missing fromCOUNTRIES_BY_CODE.hintsRevealedthen stays capped at 0 after the first wrong guess, and the game still runs on similarity feedback alone. - Same order every time. Because of the shuffle bias, repeat players see the same within-tier order for most small tiers. This makes the hint panel more predictable than the code comment (“so runs are not identical every time”) intends.
- Repeated targets. No history is kept, so the same country can be drawn in consecutive games with probability .
6. Architectural tradeoffs & non-goals
Section titled “6. Architectural tradeoffs & non-goals”| Decision | Benefit | Cost |
|---|---|---|
| Tiered reveal, broad first | Early hints split the pool. Later hints confirm. | Countries with few hints give nothing new late in the game. |
| One hint per wrong guess, no button | Simple loop. No hint economy to balance. | The player cannot trade a guess for information. |
| No score decay | Every win counts the same. Result screen still shows guesses and hints. | No reward for solving early beyond the share text. |
| Hand-rolled LCG shuffle | No dependency. Seedable in tests. | Precision loss makes small tiers nearly fixed. |
| Fully client-side | No backend, instant start | Answers are inspectable. No shared puzzle. |
Non-goals: daily or shared Mystery puzzles, free-text hints, and difficulty levels.