clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Plan canonical solver state and fast search in OpenSpec

Dami committed 88dbdce
commit
88dbdce4051a3fb034d00df61ac77197b1e179fa
parent
54ada95c9fd9a7c279ba06720f7da126539e7d9b
author
Dami <dami.etoile@gmail.com>
5 files changed, +177 -0
Aopenspec/changes/scale-solver-state-search/.openspec.yaml2+
Aopenspec/changes/scale-solver-state-search/design.md67++++++++++++++++++++++++++++++
Aopenspec/changes/scale-solver-state-search/proposal.md25+++++++++++
Aopenspec/changes/scale-solver-state-search/specs/solver-state-search/spec.md53++++++++++++++++++++++++
Aopenspec/changes/scale-solver-state-search/tasks.md30+++++++++++++
A openspec/changes/scale-solver-state-search/.openspec.yaml +2 -0
A openspec/changes/scale-solver-state-search/design.md +67 -0
@@ -0,0 +1,67 @@
1+## Context2+3+`tools/solve.mjs` runs the authoritative engine, but each breadth-first node holds a cloned settled snapshot and a full path string. Its key manually concatenates object IDs, positions, nouns, and automaton phases. Current world drafts need 2 to 4 million expansions. W6 exposed a restore bug that advanced a noun transformation without a turn; that is fixed separately in `8ecc483` and is now a prerequisite for differential search checks. The world's older `/tmp/cw` compact simulator demonstrates a possible data layout, but its rule and contact semantics predate W6 and cannot be adopted as the engine implementation.4+5+## Goals / Non-Goals6+7+**Goals:**8+9+- Keep shortest-path, BYPASS, and `--replay` behavior while reducing frontier memory; make `--forbid` mean the rule was never active and report HANDS-OFF separately.10+- Give the solver one canonical, round-trippable identity for every state variable that affects future transitions.11+- Add an event-free transition path that shares engine semantics and is differential-tested against public gameplay transitions before activation.12+- Make future mechanics register their dynamic snapshot fields before a solver can explore them.13+14+**Non-Goals:**15+16+- Change puzzle rules, authored world content, Gift/Workshop presentation, or the public `getState()` snapshot format.17+- Treat a search-cap result as proof that a level is unsolvable.18+- Introduce gates, switches, crumble tiles, or their visual treatment in this change.19+20+## Decisions21+22+### The codec is authored-level specific23+24+Build stable object and automaton slot tables once from the normalized level. Encode each object's presence, cell, and current noun in authored-ID order, followed by automaton phases in stable system-ID order. Use a versioned byte representation with bounded integer encoding, and derive a stable map key from those bytes. A missing object gets an explicit sentinel. A noun dictionary covers authored object nouns and nouns available as word predicates; an unknown runtime noun fails loudly. The codec decodes to a settled snapshot accepted by `game.restore()` and keeps static level definitions outside each search node.25+26+Rules, sentence spans, active carriers, `won`, display fields, and move count are not part of current nonterminal state equivalence: rules and carriers derive from objects; won states terminate before enqueue; depth gives the shortest move count; automata phase is stored explicitly. A future mechanic must declare dynamic snapshot fields through a codec extension. Unknown dynamic fields cause an error instead of merging distinct states. Static authored fields such as terrain or word text stay in the level definition.27+28+Alternative considered: canonical JSON of every full snapshot. It is easy to inspect but retains repeated static data and leaves memory proportional to snapshot size. The byte codec is more work but can also serve as the branch payload.29+30+### Breadth-first search stores parent links31+32+Use one queue with a head index and one compact blob per visited state. Each node stores its parent index and action; reconstruct a path only for a solved node. Keep transition-expansion counting and the configured search cap stable. The existing real-engine transition remains the baseline and differential oracle.33+34+Alternative considered: retain one frontier array per depth and shorten only the key. That still stores full snapshots and copies path strings at every branch.35+36+### The fast path shares mechanics, not a second rule engine37+38+Introduce an engine search transition boundary that takes compact state plus a direction or wait and returns either a blocked result or the next compact state with minimal provenance facts: whether the player pushed a word, whether any actor pushed a word, and which rules were active at each evaluation phase. It omits animation events, presentation clones, and history. Extract or reuse the same parser, movement, transformation, automata, and contact functions as ordinary turns rather than copying the older compact simulator. Enable the solver fast path only after deterministic differential fixtures pass for stacked words, W6 precedence, transformations, removal, automata phases, and Undo/restore equivalence. Keep the public engine path as fallback during rollout.39+40+Alternative considered: copy `/tmp/cw/engine-fast.js` or `sim.mjs`. Those files encode older STOP/DEFEAT/stacked-reading behavior and would make CI disagree with play.41+42+### Solver constraints inspect every evaluated rule phase43+44+`--forbid` means the named rule must never be active, including the initial board and intermediate rule evaluations before automata or contact destruction. The fast transition's rule trace is the evidence. A future `mustForm` CI check can reuse that trace, but metadata naming and gate policy remain in the worlds team's authoring change. BYPASS means the player never pushes a word; HANDS-OFF means no word moves by any cause. The transition provenance is evaluated along the route rather than inferred from the final board.45+46+### Performance is measured on authored work47+48+Record expansions, elapsed time, and peak memory for core, Word Lab, Crossword, Crate Works, Tidewater, and Clockwork under the same cap before and after each stage. Use equal route and outcome comparisons; timing is reported, not hard-coded as a cross-machine test threshold. The 2 to 4 million expansion worlds are acceptance cases rather than an assumed 30× speed promise.49+50+## Risks / Trade-offs51+52+- [Risk] A codec omits a new dynamic field and merges futures. → Require a versioned dynamic-state registry and fail closed on unknown fields; add a two-states-same-board/different-mechanic fixture when each mechanic lands.53+- [Risk] The no-event path diverges from gameplay rules. → Share phase functions and keep differential tests that compare next state and victory across representative reachable states before switching the solver.54+- [Risk] Compact decoding and map-key conversion save memory but cost CPU. → Benchmark each stage separately and retain the authoritative fallback until measured.55+- [Risk] A transient rule can activate and disappear in one turn. → Capture each rule-evaluation phase for `--forbid` instead of checking only settled rules.56+- [Risk] Browser and Node environments have different byte/string behavior. → Use deterministic integer encoding with explicit round-trip tests; the solver remains a Node authoring tool.57+58+## Migration Plan59+60+1. Add codec and round-trip tests against current settled snapshots; keep the old solver search path.61+2. Switch BFS storage to compact nodes and parent links; compare solver routes and BYPASS flags with the baseline.62+3. Add and differential-test the event-free engine transition; enable it in the solver only after parity and performance checks.63+4. Coordinate the per-proof CI search-budget field with the worlds team in the world-authoring OpenSpec change. Rollback is a solver-side fallback to the public engine path; public gameplay state stays unchanged.64+65+## Open Questions66+67+- The worlds notes use both `solverBudget` and `solverMax`. Their author-facing field name is pending the worlds team's reply and belongs to the world-authoring workflow.
A openspec/changes/scale-solver-state-search/proposal.md +25 -0
@@ -0,0 +1,25 @@
1+## Why2+3+The real-engine solver retains a full cloned board and growing path string for every breadth-first node. Current authored worlds need millions of expansions, and the handwritten state key would omit future gate, switch, or crumble state. W6 also exposed how important exact snapshot replay is for noun transformations. The solver needs a compact, authoritative state contract before it becomes a bundled-world CI gate or a basis for stateful mod mechanics.4+5+## What Changes6+7+- Add a canonical solver-state codec that round-trips settled engine state and distinguishes every variable that can affect future turns, including object existence, position and noun, automata phase, and registered mechanics state.8+- Replace full-snapshot frontier nodes and repeated path strings with compact state blobs, a queue, and predecessor links while preserving shortest paths. Correct `--forbid` to exclude rules active at the start or transiently during a turn, and distinguish BYPASS from HANDS-OFF evidence.9+- Provide a no-presentation engine transition path for solver exploration, with differential checks against ordinary `createGame().step()` and `wait()` before the solver uses it.10+- Fail closed when an added stateful mechanic lacks codec coverage, so future gates, switches, or one-step tiles cannot silently merge distinct states.11+- Measure representative current worlds and report expansions, time, and memory before and after the change. The existing author-facing solver command stays compatible.12+13+## Capabilities14+15+### New Capabilities16+17+- `solver-state-search`: canonical compact solver states, equivalent fast transitions, and bounded shortest-path search.18+19+### Modified Capabilities20+21+None.22+23+## Impact24+25+`tools/solve.mjs`, new solver codec/search modules and tests, and an engine search transition boundary. Engine snapshots remain the authoritative state shape. World pack files and designer-owned presentation files are outside this change; future mechanic specs must register their state with the codec before solver acceptance.
A openspec/changes/scale-solver-state-search/specs/solver-state-search/spec.md +53 -0
@@ -0,0 +1,53 @@
1+## ADDED Requirements2+3+### Requirement: Solver state SHALL have a canonical compact identity4+For one authored level, the solver SHALL encode each settled nonterminal state in stable authored-ID order for entities and stable system-ID order for automata. The identity SHALL include every dynamic value that can change a future transition: existence, position, and noun of each entity; phase of each automaton; and registered stateful-mechanic fields. It SHALL exclude values derived from those fields and SHALL reject a dynamic field with no codec registration.5+6+#### Scenario: Object order does not change identity7+- **WHEN** two settled snapshots contain the same surviving entities, coordinates, nouns, and automata phases but list objects in different array orders8+- **THEN** their compact solver identities are equal9+10+#### Scenario: Removal and phase change remain distinct11+- **WHEN** otherwise identical states differ by a removed entity or by one automaton phase12+- **THEN** their compact solver identities differ13+14+#### Scenario: Future mechanic state cannot be silently merged15+- **WHEN** two otherwise identical states differ in a registered gate, switch, or crumble field16+- **THEN** their identities differ; if that dynamic field lacks a registration, encoding fails with an actionable error17+18+### Requirement: Compact states SHALL round-trip through authoritative restore19+Encoding and decoding a settled state SHALL preserve its behavior under the public engine's next accepted movement or wait. Decoding SHALL use the authored level for static data and SHALL not apply noun transformations or advance automata until the subsequent turn.20+21+#### Scenario: One-link transformation survives round-trip22+- **WHEN** a state contains ROCK after `BABA IS ROCK` with `ROCK IS FLAG` also active23+- **THEN** encoding and restoring it leaves ROCK in place; a later accepted turn may transform it to FLAG24+25+#### Scenario: Automaton phase survives round-trip26+- **WHEN** two states share entity positions but have different automaton phases27+- **THEN** decoding each state restores its own phase and the next wait produces the same result as ordinary engine replay28+29+### Requirement: Solver search SHALL retain shortest-path and analysis behavior30+The compact breadth-first search SHALL preserve the real engine's accepted-transition graph. It SHALL return shortest paths, use the configured expansion cap, and preserve `--replay` and already-won reporting. It SHALL report BYPASS when the player never pushes a word and HANDS-OFF when no word moves by any cause. Search nodes SHALL not retain full cloned snapshots or full path strings.31+32+#### Scenario: Shortest route remains stable33+- **WHEN** core and bundled teaching levels are searched with the compact frontier34+- **THEN** solved/limited status, shortest route length, and BYPASS classification match the authoritative baseline under the same cap35+36+#### Scenario: A forbidden rule is never active37+- **WHEN** an author runs `--forbid "GAP IS CRATE"`38+- **THEN** the search rejects an initially active rule and every transition that activates it at any rule-evaluation phase, including a rule that disappears before the turn settles39+40+#### Scenario: Automaton word motion is distinguished from player work41+- **WHEN** an automaton pushes a word on a winning path and the player never pushes a word42+- **THEN** the solver reports BYPASS but not HANDS-OFF43+44+### Requirement: Fast solver transitions SHALL match public gameplay transitions45+An event-free engine transition SHALL return the same next gameplay state or rejection as public `step()` and `wait()` for the same settled input. It SHALL expose word-motion provenance and every evaluated rule set needed by solver constraints without constructing presentation events or Undo history. The solver SHALL only enable this path after differential fixtures cover the current rule vocabulary and automata.46+47+#### Scenario: W6 contact and rule ordering match48+- **WHEN** a reachable state has stacked word alternatives, PUSH with STOP, noun transformation, or simultaneous WIN and DEFEAT contact49+- **THEN** the fast transition and public engine produce equivalent surviving entities, rules, automata phases, win state, and accepted status50+51+#### Scenario: Clockwork wait matches52+- **WHEN** a Clockwork state advances an automaton through a wait action53+- **THEN** the fast transition yields the same actor positions and next phase as public `wait()`
A openspec/changes/scale-solver-state-search/tasks.md +30 -0
@@ -0,0 +1,30 @@
1+## 1. Baseline and state inventory2+3+- [ ] 1.1 Record current solver route, expansion, time, and memory baselines for core, Word Lab, and representative Crossword, Crate Works, Tidewater, and Clockwork proofs.4+- [ ] 1.2 Inventory every field in settled engine snapshots and identify which fields determine future transitions, including automata phases and current transformation nouns.5+- [ ] 1.3 Add a regression that restoring a settled state and taking one step matches direct play for noun chains, automata, removal, and rule edits.6+7+## 2. Canonical compact codec8+9+- [ ] 2.1 Implement authored-ID slot tables and a versioned byte codec for object existence, cell, noun, and automata phase.10+- [ ] 2.2 Add encode/decode and key-stability fixtures for reordered object arrays, removed entities, changed nouns, and changed automata phases.11+- [ ] 2.3 Define the dynamic-mechanic codec registration boundary and reject unregistered state fields; prove gate and crumble example states do not collide.12+13+## 3. Memory-bounded search14+15+- [ ] 3.1 Replace full-snapshot frontier nodes and growing path strings with compact blobs, a queue head, and predecessor links.16+- [ ] 3.2 Compare shortest paths, LIMIT counts, BYPASS flags, `--replay`, and already-won behavior with the pre-change solver on representative packs.17+- [ ] 3.3 Separate player-pushed-word BYPASS from all-word-motion HANDS-OFF and test automaton-driven word movement.18+19+## 4. Equivalent fast transitions and constraints20+21+- [ ] 4.1 Extract a search transition boundary that reuses current parser, movement, transform, automata, and contact semantics without presentation events or Undo history.22+- [ ] 4.2 Differential-test public `step()` and `wait()` against the fast transition across W6 stacked rules, PUSH/STOP, DEFEAT/WIN, transformation chains, SINK removal, and Clockwork phases.23+- [ ] 4.3 Make `--forbid` reject rules active initially or in any intermediate rule-evaluation phase; verify a rule that disappears before the turn settles.24+- [ ] 4.4 Enable the fast transition in the solver only after route parity and performance checks; retain the authoritative fallback.25+26+## 5. Verification and worlds handoff27+28+- [ ] 5.1 Rerun representative worlds at their documented search caps and report route, expansion, elapsed time, and peak memory deltas to the worlds team.29+- [ ] 5.2 Run the full Node suite, production build, mod loading, browser controls, rule-change and Undo flows, and strict OpenSpec validation.30+- [ ] 5.3 Coordinate the per-proof search-budget field and bundled-world CI gate with the separate world-authoring change.