clone

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

main
miau-is-true / openspec / changes / add-frail-leftover-tiles / specs / frail-leftover-tiles / spec.md
spec.md 7.6K rendered
1## ADDED Requirements23### Requirement: Levels SHALL support a derived FRAIL mechanic4The normalized level SHALL support a level-scoped FRAIL mechanic when opted5in; that mechanic SHALL have exactly one entry with `{ id: 'frail', type:6'frail', version: 1, state: 'derived', leaves: { ... } }`.7The host SHALL validate every map key and value as a valid noun token and8include those tokens in the level-bound solver codec even when no authored9object initially has that noun. The map is static level data; objects SHALL10NOT carry a per-object `leaves` field.11An unmapped current noun SHALL mean removal on crumble.1213#### Scenario: A valid derived declaration is normalized14- **WHEN** a level declares `DRIFT IS FRAIL` and15 `mechanics: [{ id: 'frail', type: 'frail', version: 1, state: 'derived', leaves: { DRIFT: 'WATER' } }]`16- **THEN** the normalized level retains the declaration and the initial object17 state is unchanged1819#### Scenario: Invalid FRAIL metadata is rejected20- **WHEN** a pack declares an unknown noun in a FRAIL map, a function-valued21 map entry, a duplicate `frail` mechanic, or an object-level `leaves` field22- **THEN** registration fails with the pack and level path and installs none of23 the invalid level's FRAIL metadata2425### Requirement: Frail objects crumble after distinct departure26For an accepted turn, the host SHALL evaluate FRAIL once after player and27automata movement and all movement-triggered transforms, before SINK, DEFEAT,28and WIN contacts. A frail object SHALL crumble only when it was FRAIL at the29start of the turn, remains FRAIL at the checkpoint, a distinct occupant30departed its starting cell, and no other entity occupies that cell at the31checkpoint. A handover SHALL prevent crumbling. A moving FRAIL object SHALL32NOT crumble itself; a different FRAIL mover MAY cause departure.3334#### Scenario: Standing on a plank is safe35- **WHEN** YOU stands on a DRIFT cell under `DRIFT IS FRAIL` and pushes a word36 ahead without leaving the cell37- **THEN** the DRIFT object remains and no `crumbled` record is emitted3839#### Scenario: A handover is safe40- **WHEN** a pushed word leaves a FRAIL cell and YOU enters that same cell in41 the accepted turn42- **THEN** the FRAIL object remains because the destination is occupied at the43 crumble checkpoint4445#### Scenario: A distinct departure crumbles the cell46- **WHEN** a distinct word, thing, player, or automaton leaves a FRAIL cell and47 no other entity remains there after all movement and transforms48- **THEN** the FRAIL object crumbles exactly once before contacts4950#### Scenario: A different FRAIL mover can trigger crumble51- **WHEN** one FRAIL entity leaves a cell containing a second, stationary FRAIL52 entity and no other entity remains there53- **THEN** only the stationary FRAIL entity crumbles; the mover remains at its54 destination5556#### Scenario: Moving FRAIL is not self-crumbling57- **WHEN** a FRAIL object is pushed or moved by YOU or an automaton58- **THEN** it remains at its destination and its old cell does not emit a59 `crumbled` record merely because the FRAIL object moved6061### Requirement: Leftovers preserve identity and chain through nouns62On crumble, the host SHALL look up the object's current noun in the63level-scoped `frail.leaves` map. A mapped noun SHALL replace the current noun64at the same cell while retaining the stable ID. An unmapped65noun SHALL remove the object. The resulting noun SHALL be eligible for later66FRAIL rules, allowing repeated authored chains without a hidden counter.6768#### Scenario: A drift plank becomes water69- **WHEN** a DRIFT object crumbles under `leaves: { DRIFT: 'WATER' }`70- **THEN** the same ID is present as WATER at the same coordinate and the71 settled event reports `{ id, from: 'DRIFT', to: 'WATER' }`7273#### Scenario: A two-link chain advances over two departures74- **WHEN** the map contains `SAND: 'SHOAL'` and `SHOAL: 'WATER'`, and the same75 object crumbles first as SAND and later as SHOAL76- **THEN** the events report SAND/SHOAL and SHOAL/WATER for the same stable ID7778#### Scenario: No leftover removes the object79- **WHEN** a FRAIL ROCK has no ROCK entry in `frail.leaves` and crumbles80- **THEN** its stable ID is absent from the settled object state and the event81 reports `{ id, from: 'ROCK', to: null }`8283### Requirement: Crumbling precedes existing contacts84The leftover state SHALL be settled before the existing SINK, DEFEAT, and WIN85contact phase. A leftover SHALL not create a floor layer or shield any object86from contact, including initial reset contacts.8788#### Scenario: Water leftover uses SINK89- **WHEN** a frail object becomes WATER under `WATER IS SINK`90- **THEN** the existing contact phase consumes the water according to normal91 SINK precedence and the final state/events match an authored WATER object9293#### Scenario: Initial water contact remains destructive94- **WHEN** a level places an object on a cell containing `WATER IS SINK`95- **THEN** reset resolves the existing contact before the first frame; FRAIL96 does not shield or defer it9798### Requirement: Settled events and snapshots expose crumble results99Every accepted turn that crumbles one or more objects SHALL expose a cloned100`crumbled` array of `{ id, from, to }` records, deterministically ordered by101stable entity ID. Undo, reset, sharing, replay, and restore SHALL reproduce the102authoritative object noun/existence and level-scoped FRAIL declaration exactly.103104#### Scenario: Undo restores the plank105- **WHEN** a turn crumbles a DRIFT object and the player chooses Undo106- **THEN** the prior DRIFT object, coordinate, mechanic declaration, rules, and107 history are restored and no crumble event remains active108109#### Scenario: Shared replay preserves a chain stage110- **WHEN** a shared level is imported after one leftover link has resolved111- **THEN** the decoded snapshot and next transition distinguish its current112 noun and stable ID from the pre-crumble board and produce the same event113114### Requirement: Solver transitions include frail state115The compact solver SHALL use the same movement, transform, FRAIL checkpoint,116and contact ordering as the public engine. Its key SHALL distinguish object117identity, position, current noun, existence, and any registered mechanic118projection input that affects a future crumble. The `frail.leaves` map is119level-bound static data; current noun and existence are authoritative object120state. Neither may be duplicated as a hidden FRAIL runtime bit. Solver replay121SHALL reproduce `crumbled` and chain transitions without JSON-order or122presentation-only state.123124#### Scenario: Same board different chain stages remain distinct125- **WHEN** two search states have identical positions but one object is SAND126 and the other is SHOAL127- **THEN** the solver retains them as distinct states and explores their128 different future leftovers129130#### Scenario: Public and compact transitions agree131- **WHEN** a differential fixture runs a handover, pushed-word departure,132 automaton departure, and two-link chain through both transition paths133- **THEN** each path has the same settled objects, rules, contacts, and134 `crumbled` records135136#### Scenario: Restore preserves derived FRAIL parity137- **WHEN** a snapshot is decoded and restored at either link of a leftover chain138- **THEN** public and fast transitions derive the same crumble result from139 object existence, noun, position, and the level-scoped map without a hidden140 FRAIL state field141142#### Scenario: Hidden FRAIL state fails closed143- **WHEN** an implementation attempts to encode a separate crumble counter,144 per-object leaves value, or existence bit already represented by the145 authoritative object snapshot146- **THEN** codec or mechanic validation rejects the redundant state before147 search or play