wordle-five: the engine, the reward, the solver and the probe that checks them

The Python is the source of truth; src/demos/wordle/engine.ts will be a port of
it, and CI gates the two against a SHA-256 over all 21.2M (guess, answer)
pattern pairs rather than a hand-picked vector file — a vector file only ever
catches the cases somebody thought of.

The reward is three weighted components, and the third one is the reason this
demo is worth building. `solved` and `economy` pull toward winning. `consistency`
pulls against them, because a player maximising information deliberately guesses
words that cannot win — a word that splits the remaining candidates evenly
teaches more than a word that might happen to be right. That is good play, and
it costs consistency.

The probe ladder proves the tension is real rather than asserted:

  inaction        0.0000   crude       0.0111   plausible  0.1224
  candidate_only  0.8925   exhaustive  0.9031   oracle     0.9458

The two good policies are 0.05 apart and neither dominates — the entropy oracle
takes 1.00 economy and 0.73 consistency, the candidate-only player takes 0.75
and 1.00. Which one wins is a decision about what you want, which is the whole
argument the site exists to make. probe.py fails CI if either starts dominating.

Two traps found by building it. `consistency` is scored over turns SPENT, not
guesses accepted: counting only legal guesses hands a free 1.0 to a policy that
plays one word and then jams the parser five times — one guess, no
contradictions, perfect score. And `economy`'s denominator is the depth the
SHIPPED solver reaches, not a depth-optimal search: entropy-greedy is not
depth-optimal, so grading it against an exact optimum would make the oracle
rung fail its own assertion on some seeds.

The word lists are built from Wordnik (MIT) intersected with SCOWL, never from
the original game's 2,315 answers. 4,603 answers makes this materially harder
than the original, so the published SALET/3.4212 results are cited as belonging
to that list and our own reference player's TARES/3.72 is measured here.

verifiers is an optional extra. The engine, reward, solver and probe all run —
and gate — without an RL stack resolvable.

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:39:03 -07:00
parent 5a9ff8dda9
commit a56f097f28
54 changed files with 8201 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
/**
* The join between the demo registry and the vertical lineup.
*
* This is the ONLY place the marketing pages touch the registry. Home, Gallery
* and Vertical all read demos through here, so when the registry's export name
* or path moves, it moves in one line instead of in four pages.
*/
import { demos } from '@/lib/demo-kit';
import type { DemoMeta, Vertical } from '@/lib/demo-kit/types';
import { VERTICALS, verticalByKey } from '@/content/verticals';
import type { VerticalEntry } from '@/content/verticals';
/**
* Route shapes, written down once. The router owns the actual `<Route>`
* elements; these are what every link on the marketing side builds, so if the
* two ever disagree, they disagree here and not in twenty JSX attributes.
*/
export const routes = {
home: '/',
gallery: '/gallery',
honesty: '/honesty',
demo: (slug: string) => `/demos/${slug}`,
vertical: (slug: string) => `/verticals/${slug}`,
/** Gallery pre-filtered to one vertical. Deep-linkable on purpose. */
galleryFiltered: (key: Vertical) => `/gallery?vertical=${key}`,
} as const;
/** The public repository. Every claim on the site is meant to end up here. */
export const REPO_URL = 'https://github.com/karti-ai/PIG-Demo';
/** Registry order is authorial; this is the order every list renders in. */
export const allDemos: readonly DemoMeta[] = [...demos].sort(
(a, b) => a.order - b.order || a.slug.localeCompare(b.slug),
);
export const liveDemos: readonly DemoMeta[] = allDemos.filter((d) => d.status === 'live');
/**
* The demo the site leads with. `reference` is the hello-world vertical, and
* the first live one in it is the front door; if there is none yet, any live
* demo will do, and only then do we fall back to whatever the registry has.
* Written as a function of the registry so adding a demo never edits Home.
*/
export const featuredDemo: DemoMeta | undefined =
liveDemos.find((d) => d.vertical === 'reference') ?? liveDemos[0] ?? allDemos[0];
/** Every vertical key that at least one demo is filed under. Filter source. */
export const verticalKeysInUse: readonly Vertical[] = Array.from(
new Set(allDemos.map((d) => d.vertical)),
);
export function demosForVertical(key: Vertical | null): readonly DemoMeta[] {
if (!key) return [];
return allDemos.filter((d) => d.vertical === key);
}
export function demoBySlug(slug: string | undefined): DemoMeta | undefined {
if (!slug) return undefined;
return allDemos.find((d) => d.slug === slug);
}
/**
* A demo's vertical, for a label on a card. `reference` deliberately resolves
* to nothing — the word game is not an industry, and labelling it as one would
* be the first small lie on a site whose whole argument is that it doesn't
* tell them.
*/
export function verticalForDemo(demo: DemoMeta): VerticalEntry | undefined {
return verticalByKey(demo.vertical);
}
/** The lineup, in the order we would build it. */
export const lineup: readonly VerticalEntry[] = [...VERTICALS].sort((a, b) => a.rank - b.rank);