clone

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

Align FRAIL spec with noun-mapped leftovers

Dami committed 17f4f9b
commit
17f4f9b719dbb291ec89409f66be561c4045cb0d
parent
511ed5bfc01da7e1e05fa02d81b613e25b883214
author
Dami <dami.etoile@gmail.com>
4 files changed, +130 -101
Mopenspec/changes/add-frail-leftover-tiles/design.md97++++++++++++++++++------------
Mopenspec/changes/add-frail-leftover-tiles/proposal.md14++--
Mopenspec/changes/add-frail-leftover-tiles/specs/frail-leftover-tiles/spec.md95+++++++++++++++---------------
Mopenspec/changes/add-frail-leftover-tiles/tasks.md25+++++---
M openspec/changes/add-frail-leftover-tiles/design.md +59 -38
@@ -14,8 +14,9 @@ mechanic lifecycle rather than adding a second board representation.
1414 - Make `FRAIL` a normal rule-driven property.1515 - Make departure, handover, and leftover timing deterministic and visible.1616 - Count things and words consistently with existing movement and SINK rules.17-- Keep `leaves` attached to entity identity through transforms, contacts,18- Undo/reset, shared payloads, and solver replay.17+- Keep the level-scoped noun map attached to the FRAIL mechanic definition;18+ keep the transformed noun and stable entity ID in authoritative object state19+ through transforms, contacts, Undo/reset, shared payloads, and solver replay.1920 - Support chains without introducing a numeric counter or renderer-only state.2021 2122 ### Non-goals
@@ -27,13 +28,26 @@ mechanic lifecycle rather than adding a second board representation.
2728 2829 ## Decisions2930 30-### Frail is a parsed property31+### Frail is a parsed property with a level-scoped declaration3132 32-The host recognizes `FRAIL` as a supported property. A level may author33-`DRIFT IS FRAIL`, `SAND AND SHOAL IS FRAIL`, or another valid noun/property34-sentence. Active FRAIL membership is derived from the settled rules and current35-noun, just like other rule-driven properties. The property word can therefore36-be pushed or removed to change future behavior.33+The host recognizes `FRAIL` as a supported property. A level that uses the34+mechanic declares one level-scoped entry:35+36+```js37+mechanics: [{38+ id: 'frail',39+ type: 'frail',40+ version: 1,41+ state: 'derived',42+ leaves: { SAND: 'SHOAL', SHOAL: 'WATER', DRIFT: 'WATER' },43+}]44+```45+46+The declaration has no runtime counter or per-object `leaves` field. A level47+may author `DRIFT IS FRAIL`, `SAND AND SHOAL IS FRAIL`, or another valid48+noun/property sentence. Active FRAIL membership is derived from the settled49+rules and current noun, just like other rule-driven properties. The property50+word can therefore be pushed or removed to change future behavior.3751 3852 ### Departure is sampled at a turn boundary3953
@@ -43,7 +57,8 @@ resolves active automata using the ordinary atomic push rules. After the final
4357 transform phase and before contacts, it evaluates every object that is FRAIL at4458 that checkpoint:4559 46-1. The object was a FRAIL object in the starting settled cell.60+1. The object was a FRAIL object in the starting settled cell and is still a61+ FRAIL object at the crumble checkpoint.4762 2. A distinct occupant moved away from that cell during this turn. A pushed4863 word, pushed thing, player, or automaton qualifies, even if that distinct4964 mover is also FRAIL.
@@ -57,28 +72,32 @@ one plank while YOU enters it safe.
5772 The moving FRAIL entity itself never triggers crumbling for its old cell. If a5873 FRAIL entity is pushed or walks away, it remains the entity at its new cell;5974 its own movement is not a departure trigger. Transforming a frail object away60-does not count as departure, but the object's `leaves` metadata remains on its61-stable ID for later use if it becomes FRAIL again.75+does not count as departure; the level-scoped map is consulted only if its76+current noun later crumbles while FRAIL.6277 6378 ### Leftovers are nouns, not counters6479 65-An authored object may carry `leaves: '<NOUN>'`. The field is validated as a66-known noun token and is preserved by entity ID through noun transforms. When67-the object crumbles, `from` is its current noun and:80+The `frail.leaves` map is validated as a map of syntactically valid noun81+tokens. A leftover need not be present among the initially authored objects;82+the solver codec includes every map key and value in its level-bound token83+table. The map is static level data and is not copied onto objects. When an84+object crumbles, `from` is its current noun and:6885 69-- with `leaves`, it changes into that noun at the same coordinate and keeps its70- ID and metadata;71-- without `leaves`, it is removed from the object set.86+- if `frail.leaves[from]` exists, it changes into that noun at the same87+ coordinate and keeps its stable ID;88+- if the current noun is absent from the map, it is removed from the object89+ set.7290 7391 The resulting noun is part of the same settled turn. If it is also FRAIL and a7492 later turn has a distinct occupant depart its cell, it can crumble again. This7593 expresses multi-crossing designs as an authored chain, for example76-`SAND(leaves: SHOAL) -> SHOAL(leaves: WATER) -> WATER`, without hidden counts.94+`SAND -> SHOAL -> WATER`, using the current noun as the lookup key and without95+hidden counts.7796 7897 ### Contacts follow the crumble checkpoint7998 8099 Crumbled leftovers or removed objects participate in the existing contact81-phase. A `leaves: 'WATER'` result is therefore subject to the current100+phase. A `frail.leaves[from] = 'WATER'` result is therefore subject to the current82101 `WATER IS SINK` rule on that same turn, with the existing contact precedence.83102 Nothing can be authored as a frail object sitting on a water hazard to bypass84103 initial contacts: reset still resolves initial contacts before the first frame,
@@ -86,14 +105,15 @@ and there is no floor layer.
86105 87106 ### State and event contract88107 89-The mechanic's static definition holds the authored `leaves` mapping by stable90-object ID; the dynamic noun and existence remain in `state.objects`. The91-settled result adds `crumbled`, ordered deterministically by stable entity ID,92-with `{ id, from, to }`, where `to` is the leftover noun or `null` for removal.93-The event is cloned with the snapshot and is presentation-only. Undo and reset94-restore the prior object noun/existence and leaves metadata exactly; sharing95-and CommonJS export preserve the validated metadata; the solver key includes96-object ID, noun, position, existence, and the registered FRAIL mechanic state.108+The mechanic's static definition holds the authored noun map; the dynamic noun109+and existence remain in `state.objects`. The settled result adds `crumbled`,110+ordered deterministically by stable entity ID, with `{ id, from, to }`, where111+`to` is the mapped leftover noun or `null` for removal. The event is cloned112+with the snapshot and is presentation-only. Undo and reset restore the prior113+object noun/existence exactly; sharing and CommonJS export preserve the114+validated mechanic declaration; the solver key includes object ID, noun,115+position, existence, and any registered mechanic projection inputs, without a116+separate FRAIL counter or per-object leaves value.97117 98118 ### Automata and solver99119
@@ -109,16 +129,16 @@ is distinct.
109129 The Tidewater draft fixtures remain world-team owned; these scenarios define110130 the engine acceptance cases without editing the pack:111131 112-- **Draft A, One plank each:** `DRIFT IS FRAIL`, three `DRIFT` entities with113- `leaves: WATER`, and a word pushed across them. Each word departure is safe114- while YOU hands over the cell; after YOU leaves the final plank it becomes115- water. Breaking `DRIFT IS FRAIL` keeps the bridge.116-- **Draft B, Two tides:** `SAND(leaves: SHOAL)` and117- `SHOAL(leaves: WATER)` prove a chain and show a deliberately pushed ROCK118- consumes one crossing.132+- **Draft A, One plank each:** `DRIFT IS FRAIL`, three `DRIFT` entities, and a133+ `frail.leaves` entry of `DRIFT: WATER`, with a word pushed across them. Each134+ word departure is safe while YOU hands over the cell; after YOU leaves the135+ final plank it becomes water. The accepted fixture pins the FRAIL sentence.136+- **Draft B, Two tides:** `frail.leaves` entries `SAND: SHOAL` and137+ `SHOAL: WATER` prove a chain as YOU crosses out, then pushes `IS` back over138+ the bridge for the second crossing. The accepted fixture has no ROCK.119139 - **Word and automaton crossing:** a pushed word and an active automaton each120140 leave distinct FRAIL cells; both produce stable crumble events.121-- **Initial SINK:** a `leaves: WATER` result and a frail object authored on a141+- **Initial SINK:** a `frail.leaves` WATER result and a frail object authored on a122142 `WATER IS SINK` cell use existing reset/contact behavior, with no shielding123143 exception.124144
@@ -128,10 +148,11 @@ the engine acceptance cases without editing the pack:
128148 movement and transforms, then require the destination cell to be empty.129149 - **A leftover chain disappears from the solver.** Keep current noun and130150 existence in the authoritative object state and include them in the compact131- key; keep authored `leaves` level-bound and add encode/decode/restore parity151+ key; keep the noun map level-bound and add encode/decode/restore parity132152 fixtures without introducing a hidden crumble counter.133-- **Unknown metadata changes sharing.** Validate `leaves` against the host134- noun vocabulary and reject malformed values atomically.153+- **Unknown metadata changes sharing.** Validate the FRAIL declaration and154+ every map key/value as a noun token and reject malformed values atomically.155+ Reject per-object `leaves` as unsupported metadata.135156 - **Contact ordering becomes ambiguous.** Keep crumble before the existing136157 contact phase and test SINK, DEFEAT, and WIN with a same-turn leftover.137158
M openspec/changes/add-frail-leftover-tiles/proposal.md +8 -6
@@ -19,10 +19,11 @@ mechanic during Undo, reset, shared play, or search.
1919 keeps the frail entity in place. A distinct mover may itself be FRAIL.2020 - Count every mover, including pushed words and automata, while a moving FRAIL2121 entity itself never causes its own cell to crumble.22-- Preserve authored `leaves` metadata by stable entity ID. On crumble, the23- entity becomes the declared noun or is removed when no leftover is declared;24- a leftover may itself be frail, allowing authored chains such as25- `SAND -> SHOAL -> WATER`.22+- Preserve one level-scoped `leaves` map in the FRAIL mechanic declaration.23+ On each crumble, look up the entity's current noun. A mapped result becomes24+ that noun with the same stable entity ID; a noun with no mapping is removed.25+ A leftover may itself be frail, allowing authored chains such as26+ `SAND -> SHOAL -> WATER` without per-object metadata.2627 - Emit settled `crumbled: [{ id, from, to }]` records and include the resulting2728 noun/removal in normal snapshots, history, sharing, replay, and solver keys.2829 - Add Tidewater proof fixtures and differential scenarios for step-off,
@@ -37,5 +38,6 @@ counters remain separate changes.
3738 3839 The turn resolver, normalized level/mod metadata, event shape, snapshot and3940 restore paths, solver transition/key logic, and Tidewater fixtures change.40-Existing levels without a `FRAIL` rule or `leaves` metadata retain current41-behavior. Rendering consumes the settled event; it does not own crumble state.41+Existing levels without a `FRAIL` rule or a `frail` mechanic declaration retain42+current behavior. Rendering consumes the settled event; it does not own43+crumble state.
M openspec/changes/add-frail-leftover-tiles/specs/frail-leftover-tiles/spec.md +48 -47
@@ -1,29 +1,33 @@
11 ## ADDED Requirements22 3-### Requirement: Levels MAY declare rule-driven frail leftovers4-A normalized level SHALL permit the supported `FRAIL` property and MAY attach5-`leaves: '<NOUN>'` to an authored object with a stable ID. The host SHALL6-validate every leaves value as a known noun, preserve it by object ID, and7-reject unsupported fields or executable values. A missing leaves value SHALL8-mean removal on crumble.9-10-#### Scenario: A valid drift object is normalized11-- **WHEN** a level declares `DRIFT IS FRAIL` and object `drift-1` as12- `{ noun: 'DRIFT', leaves: 'WATER' }`13-- **THEN** the normalized level retains the rule and the ID-bound leaves14- metadata, and the initial object state is unchanged15-16-#### Scenario: Invalid leaves metadata is rejected17-- **WHEN** a pack declares `leaves: 'NOT_A_NOUN'` or a function-valued leaves18- field3+### Requirement: Levels SHALL support a derived FRAIL mechanic4+The normalized level SHALL support a level-scoped FRAIL mechanic when opted5+in; that mechanic SHALL have exactly one entry with `{ id: 'frail', type:6+'frail', version: 1, state: 'derived', leaves: { ... } }`.7+The host SHALL validate every map key and value as a valid noun token and8+include those tokens in the level-bound solver codec even when no authored9+object initially has that noun. The map is static level data; objects SHALL10+NOT carry a per-object `leaves` field.11+An unmapped current noun SHALL mean removal on crumble.12+13+#### 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 unchanged18+19+#### 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` field1922 - **THEN** registration fails with the pack and level path and installs none of2023 the invalid level's FRAIL metadata2124 2225 ### Requirement: Frail objects crumble after distinct departure2326 For an accepted turn, the host SHALL evaluate FRAIL once after player and2427 automata movement and all movement-triggered transforms, before SINK, DEFEAT,25-and WIN contacts. A frail object SHALL crumble only when a distinct occupant26-departed its starting cell and no other entity occupies that cell at the28+and WIN contacts. A frail object SHALL crumble only when it was FRAIL at the29+start of the turn, remains FRAIL at the checkpoint, a distinct occupant30+departed its starting cell, and no other entity occupies that cell at the2731 checkpoint. A handover SHALL prevent crumbling. A moving FRAIL object SHALL2832 NOT crumble itself; a different FRAIL mover MAY cause departure.2933
@@ -39,13 +43,13 @@ NOT crumble itself; a different FRAIL mover MAY cause departure.
3943 crumble checkpoint4044 4145 #### Scenario: A distinct departure crumbles the cell42-- **WHEN** a distinct word, thing, or player leaves a FRAIL cell and no other43- entity remains there after all movement and transforms46+- **WHEN** a distinct word, thing, player, or automaton leaves a FRAIL cell and47+ no other entity remains there after all movement and transforms4448 - **THEN** the FRAIL object crumbles exactly once before contacts4549 4650 #### Scenario: A different FRAIL mover can trigger crumble47-- **WHEN** one FRAIL entity leaves a cell containing a second, stationary48- FRAIL entity and no other entity remains there51+- **WHEN** one FRAIL entity leaves a cell containing a second, stationary FRAIL52+ entity and no other entity remains there4953 - **THEN** only the stationary FRAIL entity crumbles; the mover remains at its5054 destination5155
@@ -55,24 +59,24 @@ NOT crumble itself; a different FRAIL mover MAY cause departure.
5559 `crumbled` record merely because the FRAIL object moved5660 5761 ### Requirement: Leftovers preserve identity and chain through nouns58-On crumble, an object with `leaves` SHALL become that noun at the same cell59-while retaining its stable ID and authored leaves metadata. An object without60-leaves SHALL be removed. The resulting noun SHALL be eligible for later FRAIL61-rules, allowing repeated authored chains without a hidden counter.62+On crumble, the host SHALL look up the object's current noun in the63+level-scoped `frail.leaves` map. A mapped noun SHALL replace the current noun64+at the same cell while retaining the stable ID. An unmapped65+noun SHALL remove the object. The resulting noun SHALL be eligible for later66+FRAIL rules, allowing repeated authored chains without a hidden counter.6267 6368 #### Scenario: A drift plank becomes water64-- **WHEN** a DRIFT object with `leaves: 'WATER'` crumbles69+- **WHEN** a DRIFT object crumbles under `leaves: { DRIFT: 'WATER' }`6570 - **THEN** the same ID is present as WATER at the same coordinate and the6671 settled event reports `{ id, from: 'DRIFT', to: 'WATER' }`6772 6873 #### Scenario: A two-link chain advances over two departures69-- **WHEN** SAND with `leaves: 'SHOAL'` first crumbles and the resulting SHOAL70- later crumbles with `leaves: 'WATER'`71-- **THEN** the first and second accepted turns report DRIFT-like transitions72- for the same ID, with `from/to` of SAND/SHOAL and SHOAL/WATER respectively74+- **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 ID7377 7478 #### Scenario: No leftover removes the object75-- **WHEN** a FRAIL ROCK without leaves crumbles79+- **WHEN** a FRAIL ROCK has no ROCK entry in `frail.leaves` and crumbles7680 - **THEN** its stable ID is absent from the settled object state and the event7781 reports `{ id, from: 'ROCK', to: null }`7882
@@ -95,11 +99,11 @@ from contact, including initial reset contacts.
9599 Every accepted turn that crumbles one or more objects SHALL expose a cloned96100 `crumbled` array of `{ id, from, to }` records, deterministically ordered by97101 stable entity ID. Undo, reset, sharing, replay, and restore SHALL reproduce the98-authoritative object noun/existence and leaves metadata exactly.102+authoritative object noun/existence and level-scoped FRAIL declaration exactly.99103 100104 #### Scenario: Undo restores the plank101105 - **WHEN** a turn crumbles a DRIFT object and the player chooses Undo102-- **THEN** the prior DRIFT object, coordinate, leaves metadata, rules, and106+- **THEN** the prior DRIFT object, coordinate, mechanic declaration, rules, and103107 history are restored and no crumble event remains active104108 105109 #### Scenario: Shared replay preserves a chain stage
@@ -110,10 +114,10 @@ authoritative object noun/existence and leaves metadata exactly.
110114 ### Requirement: Solver transitions include frail state111115 The compact solver SHALL use the same movement, transform, FRAIL checkpoint,112116 and contact ordering as the public engine. Its key SHALL distinguish object113-identity, position, current noun, existence, and any registered mechanic state114-that affects a future crumble. Authored `leaves` metadata is level-bound115-static data, while existence and current noun are already authoritative object116-state; neither may be duplicated as a hidden FRAIL runtime bit. Solver replay117+identity, position, current noun, existence, and any registered mechanic118+projection input that affects a future crumble. The `frail.leaves` map is119+level-bound static data; current noun and existence are authoritative object120+state. Neither may be duplicated as a hidden FRAIL runtime bit. Solver replay117121 SHALL reproduce `crumbled` and chain transitions without JSON-order or118122 presentation-only state.119123
@@ -130,17 +134,14 @@ presentation-only state.
130134 `crumbled` records131135 132136 #### Scenario: Restore preserves derived FRAIL parity133-134-- **WHEN** a snapshot is decoded and restored at either link of a leftover135- chain137+- **WHEN** a snapshot is decoded and restored at either link of a leftover chain136138 - **THEN** public and fast transitions derive the same crumble result from137- object existence, noun, position, and authored `leaves` metadata without a138- hidden FRAIL state field139+ object existence, noun, position, and the level-scoped map without a hidden140+ FRAIL state field139141 140142 #### Scenario: Hidden FRAIL state fails closed141-142-- **WHEN** an implementation attempts to encode a separate crumble counter or143- existence bit for an object already represented by the authoritative object144- snapshot143+- **WHEN** an implementation attempts to encode a separate crumble counter,144+ per-object leaves value, or existence bit already represented by the145+ authoritative object snapshot145146 - **THEN** codec or mechanic validation rejects the redundant state before146147 search or play
M openspec/changes/add-frail-leftover-tiles/tasks.md +15 -10
@@ -1,11 +1,13 @@
11 ## 1. Foundation and validation22 3-- [ ] 1.1 Register the FRAIL mechanic type and validate supported property and4- `leaves` noun tokens in the normalized level/mod boundary.5-- [ ] 1.2 Preserve authored `leaves` metadata by stable object ID through6- CommonJS load/export, catalog storage, data-only sharing, and restore.7-- [ ] 1.3 Reject malformed, unknown, duplicate, or executable FRAIL metadata8- atomically with an actionable pack/level path.3+- [ ] 1.1 Register the FRAIL mechanic type and validate the single level-scoped4+ declaration `{ id: 'frail', type: 'frail', version: 1, state: 'derived',5+ leaves: { NOUN: NOUN } }` at the normalized level/mod boundary.6+- [ ] 1.2 Preserve the validated noun map through CommonJS load/export,7+ catalog storage, data-only sharing, and restore; keep current noun and8+ stable object ID in authoritative object state.9+- [ ] 1.3 Reject malformed, unknown, duplicate, executable, or per-object10+ FRAIL metadata atomically with an actionable pack/level path.911 1012 ## 2. Turn lifecycle1113
@@ -14,15 +16,18 @@
1416 - [ ] 2.2 Evaluate distinct occupant departures once at the end-of-movement1517 checkpoint, preserve handovers, and ensure a moving FRAIL entity cannot1618 crumble itself.17-- [ ] 2.3 Apply `leaves` as an ID-preserving noun transform or remove the18- entity, including repeated leftover chains and normal same-turn contacts.19+- [ ] 2.3 Look up the object's current noun in `frail.leaves`; apply a mapped20+ result as an ID-preserving noun transform or remove it when unmapped,21+ including repeated leftover chains and normal same-turn contacts.1922 - [ ] 2.4 Emit cloned settled `crumbled: [{ id, from, to }]` records in stable2023 deterministic order without changing legacy events.2124 2225 ## 3. State, sharing, and solver2326 24-- [ ] 3.1 Include FRAIL noun/existence and authored leaves metadata in accepted25- snapshots, Undo, reset, replay, and authoritative restore.27+- [ ] 3.1 Include FRAIL noun/existence in accepted snapshots, Undo, reset,28+ replay, and authoritative restore. Keep the mechanic definition bound to the29+ level and reapply it on reset/restore; do not copy it into dynamic snapshots30+ or add per-object leaves or a hidden crumble counter.2631 - [ ] 3.2 Register FRAIL's derived projection inputs with the compact solver2732 codec and distinguish same-board states with different current nouns,2833 existence, or chain stage without adding a hidden FRAIL state bit.