clone

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

docs(openspec): define rule-driven automata and mechanic stressors

Dami committed a3be2e0
commit
a3be2e07cb37e166b760c6d9936e0c0071faef2a
parent
cecd54943661a5c0c25204edeaf5f977b4088c98
author
Dami <dami.etoile@gmail.com>
10 files changed, +279 -7
Mdocs/design-backbone.md2+-
Adocs/mod-mechanic-stressors.md22+++++
Aopenspec/changes/add-rule-driven-automata/.openspec.yaml2+
Aopenspec/changes/add-rule-driven-automata/design.md67+++++++++++++++++
Aopenspec/changes/add-rule-driven-automata/proposal.md28+++++++
Aopenspec/changes/add-rule-driven-automata/specs/rule-driven-automata/spec.md121++++++++++++++++++++++++++++++
Aopenspec/changes/add-rule-driven-automata/tasks.md32++++++++
Mopenspec/specs/commonjs-mods/spec.md4+-
Mopenspec/specs/game-presentation/spec.md4+-
Mopenspec/specs/rule-engine/spec.md4+-
M docs/design-backbone.md +1 -1
A docs/mod-mechanic-stressors.md +22 -0
@@ -0,0 +1,22 @@
1+# Mod mechanic stressors2+3+This is an engine planning note for level authors. The worlds team coordinates proof examples in [`worlds-inbox.md`](worlds-inbox.md); the user will provide further behavior details before these contracts are frozen.4+5+| Stressor | Current engine | Missing game state and events | First proof to request |6+| --- | --- | --- | --- |7+| Patterned emoji actor | Supported by `level.automata`: a word property activates a bounded move/wait cycle; phase, Undo, and reset are deterministic. The solver now includes phase in its search key. | A timing-dependent authored proof and final visual review are pending. | Transfer `TICK` from one noun to another by moving the property word, then use the new carrier to push a word. |8+| Gate and switch | `STOP` can represent a closed gate; the engine has no switch-driven gate transition. | Switch trigger state, gate open/closed state, same-turn collision order, `gateChanged` event, solver snapshot. | Place a switch before a gate, require an object to hold or toggle it, and cross while another actor moves. |9+| One-step tile | `SINK` and `DEFEAT` remove contact occupants but cannot count uses of a floor cell. | Visit counter or pending crumble state, trigger timing, `crumbled` event, result for a remaining occupant, solver snapshot. | Cross one floor cell once, return to find it gone, then Undo and cross again. |10+11+The smallest safe mod interface for the next two stressors is declarative data attached to stable authored object IDs. The host resolves occupancy and contacts; shared packs carry data only. A switch could reference gate IDs, and a crumble tile could declare a trigger and one use. We should define exact fields only after a playable proof fixes these turn-order choices:12+13+1. Does a switch trigger on entry, continuous occupancy, or exit? Which occupants count?14+2. Does it hold a gate open, toggle it, or latch it? When does the gate become passable?15+3. Does a tile crumble on entry, after standing one beat, or on exit? What happens to an occupant on it?16+4. Do automaton movements trigger switches and spend tile uses in the same turn?17+18+All mechanic state must be included in `getState()`, Undo, reset, `restore()`, replay, and the solver key. Blocked player input remains outside the turn clock unless a later design decision changes that rule.19+20+## Provisional world defaults21+22+Design assigned pressure plates and gates to `crate-works`, and one-step sandbars or ice floes to `tidewater`. Its starting model is occupancy-held plates activated by any occupant; linked gates re-evaluate at the end of the turn after contacts and stay open if closing would trap an occupant. Plates and gates share an authored colour channel. Crumbling is a rule property such as `SAND IS FRAIL`: a carrier disappears when its occupant leaves, any mover counts, and breaking the sentence disables future crumbling. These are draft constraints pending two complete scenarios from each world and the user's further direction.
A openspec/changes/add-rule-driven-automata/.openspec.yaml +2 -0
A openspec/changes/add-rule-driven-automata/design.md +67 -0
@@ -0,0 +1,67 @@
1+## Context2+3+The engine derives properties from movable word sentences and returns settled events after each turn. Clockwork needs a data-only automaton record `{ id, subject?, property, pattern, phase? }`. Public pattern steps are objects: `{ action: 'move', dx, dy, cue?, sound? }` or `{ action: 'wait', cue?, sound? }`. With no subject, the automaton is property-carried: every noun currently matched by `SUBJECT IS PROPERTY` follows the pattern, so pushing the property word can transfer the behavior to another noun.4+5+## Goals / Non-Goals6+7+**Goals:**8+9+- Normalize optional-subject automata and object pattern steps.10+- Advance one phase per accepted player turn only when at least one current entity carries the property; a blocked automaton action still consumes that beat.11+- Resolve each turn in the agreed order: player move, parse/transform, automaton actions, parse/transform, then SINK/DEFEAT/WIN.12+- Move automata with the same atomic push rules as `YOU`, using front-first ordering.13+- Expose the exact automata state and automated movement details needed to derive current/next beat feedback.14+- Include phase in Undo/reset, replay, and solver state, and validate local/shared data.15+16+**Non-Goals:**17+18+- Beat-driven property additions/removals or completion predicates.19+- JavaScript callbacks, arbitrary predicates, or executable behavior in shared payloads.20+- Pattern string shorthand, pattern versions, required handoff metadata, or explicit carrier fields.21+- Replacing board sentence parsing or adding new core vocabulary beyond declared property words.22+23+## Decisions24+25+### Use the implemented object-step schema26+27+The level record uses `id`, optional `subject`, `property`, `pattern`, and optional `phase`. A pattern step is either a move object with integer `dx`/`dy` and optional `cue`/`sound`, or a wait object with optional `cue`/`sound`. Only cardinal move deltas are valid. Object steps match the public mod contract and avoid a second string-only representation.28+29+### Let board sentences activate and transfer the property30+31+The automaton is active when the current level's parsed rules contain the matching `SUBJECT IS PROPERTY`, with optional subject restriction. The property word remains part of the visible board sentence, so pushing it can change which noun carries the behavior on the next parse. Active carrier IDs are derived from rules and objects rather than duplicated in state.32+33+### Resolve a turn in explicit phases34+35+For an accepted player turn, resolve the player move first, then re-parse rules and transformations. Resolve automaton actions for current carriers, moving front-first along each action direction and tying by entity ID. Re-parse rules and transformations again, followed by SINK, DEFEAT, and WIN. This prevents contacts from observing half-applied automaton movement.36+37+### Advance phase on active accepted turns38+39+If the automaton is active after the player phase, its shared numeric phase advances once after the automaton beat, even when the action is blocked and the entity stays in place. A blocked player input is not a turn and does not advance phase. Explicit `wait` is a valid action.40+41+### Keep cues and sound presentation-only42+43+Cue and sound fields travel in `automated` event entries and are consumed by presentation/audio. They do not alter coordinates, rule semantics, or solver state beyond the phase transition. The stage derives current and next beat display from the automata pattern and phase.44+45+### Share normalized data only46+47+Trusted local CommonJS modules may declare the automaton record after validation. Shared payloads carry normalized automata data and are validated before installation without evaluating code. Unsupported fields or malformed steps fail atomically.48+49+## Risks / Trade-offs50+51+- [Risk] An automaton action can be blocked by STOP or bounds. → Preserve the board position, report the block in `automated`, and still advance the active phase for that accepted player turn.52+- [Risk] Pushing the property word transfers behavior unexpectedly. → Re-parse after player movement and derive active carriers from rules plus objects.53+- [Risk] Multiple carriers collide during one beat. → Use front-first projection ordering and the existing atomic push chain with stable entity ID ties.54+- [Risk] Long patterns enlarge solver state. → Bound pattern length during validation and key only normalized automata state.55+56+## Migration Plan57+58+1. Add optional automaton normalization with absent data preserving current levels.59+2. Add player/parse/automaton/parse/contact resolution phases and automata state/events.60+3. Include phase in Undo/reset, replay, and solver state keys.61+4. Add local module and shared payload validation fixtures.62+5. Add visual/audio feedback derived from phase and pattern data.63+64+## Open Questions65+66+- Whether phase wraps, clamps at the final pattern step, or reports completion as a terminal phase.67+- The final visual treatment for cue/sound fields.
A openspec/changes/add-rule-driven-automata/proposal.md +28 -0
@@ -0,0 +1,28 @@
1+## Why2+3+The engine needs a small declarative contract for turn-based gameplay patterns without introducing executable custom rules. Authors need to describe a level-scoped automaton with a subject/property rule and repeatable movement pattern, while players and tools need deterministic phase state, visible beat feedback, and safe mod sharing.4+5+## What Changes6+7+- Add an optional level automaton with `{ id, subject?, property, pattern, phase? }` data.8+- Define `pattern` as object steps: `{ action: 'move', dx, dy, cue?, sound? }` or `{ action: 'wait', cue?, sound? }`.9+- Activate the automaton's property dynamically when the level's board contains `SUBJECT IS PROPERTY`.10+- Advance numeric phase once per accepted player turn, including explicit `wait`; blocked input does not advance it.11+- Include automata state in settled state, Undo/reset snapshots, replay, and solver state; report automated movement in `automated` event entries.12+- Validate local CommonJS and data-only shared automata and derive visible current/next beat feedback from normalized phase and pattern data.13+14+## Capabilities15+16+### New Capabilities17+18+- `rule-driven-automata`: declarative pattern steps, level-scoped activation, phase state, validation/sharing, deterministic tools, and visual feedback.19+20+### Modified Capabilities21+22+None. Existing board sentence rules and MVP vocabulary remain the source of activation; the automaton is additive level data.23+24+## Impact25+26+- Affected level state, settled events, mod validation, data-only sharing, Undo/reset, replay, solver state keys, and presentation feedback.27+- No executable callbacks, completion predicates, arbitrary shared code, or new pattern string shorthand are introduced.28+- Existing levels without an automaton remain unchanged.
A openspec/changes/add-rule-driven-automata/specs/rule-driven-automata/spec.md +121 -0
@@ -0,0 +1,121 @@
1+## ADDED Requirements2+3+### Requirement: Levels MAY declare a deterministic property automaton4+A level MAY author automata records with `{ id, subject?, property, pattern, phase? }`. `pattern` SHALL be an ordered list of `{ action: 'move', dx, dy, cue?, sound? }` or `{ action: 'wait', cue?, sound? }` objects. Move deltas SHALL be cardinal. A missing `subject` SHALL enable property-carried mode; a present subject SHALL restrict matching to that noun. Runtime state MAY add derived fields, but `carriers`, flattened `instanceIds`, `active`, and `next` SHALL NOT be required authored mod fields. An absent automata field SHALL preserve current level behavior.5+6+#### Scenario: A property-carried automaton starts at phase zero7+- **WHEN** a level declares `{ id: 'clock', property: 'TICK', pattern: [{ action: 'move', dx: -1, dy: 0 }, { action: 'wait' }] }`8+- **THEN** the initial state reports automata entry `clock`, phase zero, and the first normalized pattern step9+10+#### Scenario: A subject restricts the carrier11+- **WHEN** a level declares `{ id: 'mouse-clock', subject: 'MOUSE', property: 'TICK', pattern: [{ action: 'move', dx: -1, dy: 0 }] }`12+- **THEN** only the noun matched by `MOUSE IS TICK` follows the automaton and other nouns matched by `IS TICK` do not13+14+#### Scenario: A legacy level remains compatible15+- **WHEN** a level has no automata field16+- **THEN** the engine uses existing board-rule state and exposes no automata entry17+18+### Requirement: Board rules SHALL activate and transfer property-carried automata19+An automaton SHALL be active for each entity whose noun is currently matched by the parsed `SUBJECT IS PROPERTY` rule, subject to an optional automaton subject restriction. The property word SHALL remain part of the visible board sentence, and pushing it SHALL transfer or remove the active behavior after the next rule parse without mutating global rules.20+21+#### Scenario: A visible property rule activates a carrier22+- **WHEN** the board forms `MOUSE IS TICK` for an automaton with property `TICK`23+- **THEN** the current MOUSE entity is active for that automaton and follows its current pattern step24+25+#### Scenario: Pushing the property word transfers the carrier26+- **WHEN** a player pushes the `TICK` word so the board changes from `MOUSE IS TICK` to `BELL IS TICK`27+- **THEN** the next parsed state removes MOUSE as an active carrier and makes BELL active for the automaton28+29+### Requirement: Turns SHALL resolve player and automaton actions deterministically30+For an accepted player turn, the engine SHALL resolve the player move first, re-parse rules and transformations, resolve automaton actions for current active entities, re-parse rules and transformations again, then resolve `SINK`, `DEFEAT`, and `WIN`. Automaton actions SHALL use the existing atomic push rules, move front-first along their action direction, and tie by stable entity ID. A blocked player input SHALL not start a turn.31+32+#### Scenario: Player movement precedes automaton movement33+- **WHEN** a player move pushes a property word into a new sentence34+- **THEN** the engine reparses that sentence before selecting active entities and the new active entity acts in the same accepted turn35+36+#### Scenario: Automaton movement uses front-first pushing37+- **WHEN** two active entities act right with a PUSH chain ahead38+- **THEN** the engine resolves the front-most entity first, applies the same atomic push rules as YOU, and produces stable automated movement entries39+40+#### Scenario: Contacts settle after both movement phases41+- **WHEN** an automaton action moves an entity into SINK, DEFEAT, or WIN contact42+- **THEN** the engine resolves those contacts only after the second parse/transform phase and exposes the settled result43+44+### Requirement: Phase SHALL advance on active accepted turns45+If at least one entity is active for an automaton after the player phase, its shared phase SHALL advance exactly once after the automaton beat, including when the action is blocked or is `wait`. A blocked or invalid player input SHALL not advance phase. The automata state SHALL retain `id`, optional `subject`, `property`, `pattern`, `phase`, and `active`.46+47+#### Scenario: An active entity advances phase48+- **WHEN** a player makes an accepted turn while at least one entity is active for `TICK`49+- **THEN** the automata state increments phase once and the next pattern step becomes the step used by the following turn50+51+#### Scenario: A blocked automaton action still consumes the beat52+- **WHEN** an accepted player turn occurs but the active entity's move is blocked by STOP or bounds53+- **THEN** the entity stays in place, `automated` reports the block, and phase still advances once54+55+#### Scenario: Explicit wait is a beat56+- **WHEN** the active automaton step has `action: 'wait'` and the player makes an accepted turn57+- **THEN** no automaton entity moves, phase advances once, and the `automated` entry carries any cue or sound58+59+#### Scenario: No active entity pauses the automaton60+- **WHEN** an accepted player turn leaves no entity matched by the automaton property61+- **THEN** phase and the automata entry remain unchanged and no automated movement is emitted62+63+### Requirement: State and events SHALL expose automated movement and beat feedback64+The settled state and event automata entries SHALL expose the normalized authored fields `id`, optional `subject`, `property`, `pattern`, and `phase`, plus derived `active`, `carriers: [{ noun, instanceIds }]`, flattened `instanceIds`, and `next`, where `next` is the action object at `pattern[phase]`. Settled events SHALL additionally expose an `automated` array whose entries report action, entity ID, from/to coordinates when moved, blocked details when blocked, and optional cue/sound. The presentation MAY derive current/next beat labels from these fields and SHALL expose that feedback in visible and accessible text; reduced motion SHALL apply the same state immediately. Derived fields SHALL not be required authored mod fields.65+66+#### Scenario: An automated move is observable67+- **WHEN** an accepted turn advances an active automaton move68+- **THEN** the settled event automata entry includes the derived carrier groups, flattened `instanceIds`, and `next` action, and `automated` includes action `move`, entity ID, from/to coordinates, and cue/sound when supplied69+70+#### Scenario: A blocked automated action is observable71+- **WHEN** an active automaton move is blocked72+- **THEN** the `automated` entry includes the action, entity ID, and block details while the state retains the entity position and the automata entry still exposes its derived carriers, `instanceIds`, and `next`73+74+#### Scenario: Derived carrier fields follow active rules75+- **WHEN** a rule word is pushed and the active rule changes from `MOUSE IS TICK` to `BELL IS TICK`76+- **THEN** the next state and event automata entry reports `carriers` for BELL, flattened BELL `instanceIds`, and the action object at `pattern[phase]` without requiring those fields in the authored level data77+78+#### Scenario: Reduced motion keeps beat feedback readable79+- **WHEN** reduced motion is enabled during an automaton turn80+- **THEN** the stage derives and displays the current/next pattern steps immediately without changing the simulation result81+82+### Requirement: Automata SHALL be validated in local and shared pack paths83+The host SHALL validate automaton IDs, optional subject, property vocabulary, object action tokens, cardinal move deltas, pattern bounds, phase bounds, cue/sound values, and duplicate IDs before registering or installing a pack. Trusted local CommonJS modules MAY provide automata data; data-only shared payloads SHALL carry normalized records and SHALL never execute code.84+85+#### Scenario: A valid local automaton is registered86+- **WHEN** a trusted local module exports a level with valid object pattern steps87+- **THEN** the loader registers the normalized automata entry only after complete pack validation88+89+#### Scenario: Invalid automaton data is rejected atomically90+- **WHEN** a pack contains an unknown action, non-cardinal delta, invalid property, or duplicate automaton ID91+- **THEN** validation reports the pack and level path and registers none of its automata or levels92+93+#### Scenario: A shared automaton remains data-only94+- **WHEN** a shared payload contains normalized automata entries95+- **THEN** the decoder validates and previews the entries without evaluating module source or installing before explicit confirmation96+97+### Requirement: Automata state SHALL be part of Undo, reset, replay, and solver state98+Undo snapshots, reset state, deterministic replay records, and solver state keys SHALL include normalized automata `id`, optional subject, property, pattern, phase, and active state. Undo SHALL restore the preceding phase and active state; replay and solver SHALL reproduce the same automated movement and phase sequence for the same accepted intents.99+100+#### Scenario: Undo restores a phase transition101+- **WHEN** the player undoes a turn that advanced an automaton from phase 3 to phase 4102+- **THEN** the prior phase, active state, board, and resolved rules are restored exactly103+104+#### Scenario: Reset restores native automata state105+- **WHEN** the player resets after automaton turns106+- **THEN** every automata entry returns to its declared phase or zero and no prior automated movement remains107+108+#### Scenario: Solver distinguishes phase states109+- **WHEN** two search nodes have identical board coordinates but different automata phases or active states110+- **THEN** the solver treats them as distinct states and does not collapse one path into the other111+112+### Requirement: Automata SHALL provide a visual feedback handoff113+The presentation handoff SHALL use normalized pattern steps, phase, `active`, and `automated` cue/sound fields to show the current and next pattern actions. It SHALL keep this feedback separate from simulation geometry and preserve readable state under reduced motion.114+115+#### Scenario: A pattern step is ready for visual review116+- **WHEN** an author supplies normalized automata data and a settled automated event117+- **THEN** reviewers can map phase and action objects to the current/next visual feedback and movement result118+119+#### Scenario: Missing optional cue does not block play120+- **WHEN** a valid automaton step omits cue or sound121+- **THEN** the engine and stage retain the normalized action and phase and use the default visual/audio treatment
A openspec/changes/add-rule-driven-automata/tasks.md +32 -0
@@ -0,0 +1,32 @@
1+## 1. Automaton data and engine state2+3+- [x] 1.1 Define normalized level automaton schema `{ id, subject?, property, pattern, phase? }` with object `move`/`wait` steps and optional cue/sound.4+- [x] 1.2 Add backward-compatible `automata` state entries with `id`, optional subject, property, pattern, phase, and active fields.5+- [x] 1.3 Activate property-carried automata from parsed `SUBJECT IS PROPERTY`, with optional subject restriction and transfer when the property word moves.6+- [x] 1.4 Implement player → parse/transform → automaton front-first movement → parse/transform → contact resolution using atomic push rules.7+- [x] 1.5 Advance phase once only when a player turn is accepted and at least one entity is active, including blocked automaton actions and explicit `wait`.8+9+## 2. Events, Undo, replay, and solver10+11+- [x] 2.1 Extend settled events with `automated` entries reporting action, entity ID, from/to or blocked details, and optional cue/sound.12+- [x] 2.2 Include normalized automata state and phase in Undo/reset snapshots and verify exact restoration.13+- [x] 2.3 Extend replay and solver state keys to distinguish automata phase and active state and reproduce front-first movement.14+- [ ] 2.4 Add deterministic fixtures for property transfer, blocked actions, wait, contacts, reset, Undo, replay, and solver exploration.15+16+## 3. Mod validation and sharing17+18+- [x] 3.1 Validate optional subject, property vocabulary, object action tokens, cardinal deltas, pattern/phase bounds, cue/sound values, and unique IDs in local CommonJS mods.19+- [x] 3.2 Normalize automata into data-only shared payloads and reject unsupported or malformed fields without executing shared code.20+- [ ] 3.3 Add atomic failure fixtures for malformed local and shared automata while preserving earlier registered packs.21+22+## 4. Presentation and feedback23+24+- [x] 4.1 Derive current/next pattern feedback from automata phase and render it with accessible text and optional cue/sound.25+- [x] 4.2 Provide reduced-motion and mobile-safe beat feedback without changing simulation geometry or timing semantics.26+- [ ] 4.3 Add visual fixtures for property transfer, blocked actions, current/next steps, and automated movement details.27+28+## 5. Verification29+30+- [x] 5.1 Run browser controls, rule changes, bundled CommonJS mod loading, and Undo coverage with automata-enabled and legacy levels.31+- [ ] 5.2 Run deterministic solver/replay checks and the full Node suite.32+- [x] 5.3 Run production build and document visual review items before enabling an authored automaton in a default pack.
M openspec/specs/commonjs-mods/spec.md +2 -2
M openspec/specs/game-presentation/spec.md +2 -2
@@ -45,14 +45,14 @@ The app SHALL expose a polite live region and readable text equivalents for move
4545 - **THEN** the board, INDEX, active rules, current proof, pack status, and controls remain understandable from static text and tile labels4646 4747 ### Requirement: Supported character glyphs SHALL use scoped facing state48-The presentation SHALL maintain facing per entity ID as visual state separate from simulation coordinates. The built-in MIAU and KEKE glyphs SHALL be directional; a mod palette object SHALL opt into directional treatment explicitly. Only an accepted horizontal self-move SHALL update an entity's facing. Pushed entities, vertical self-moves, transformations, idle turns, and blocked intents SHALL preserve the entity's prior facing. Glyphs without directional treatment SHALL use their unchanged fallback. Undo SHALL restore the per-ID facing snapshot, while loading or resetting a level SHALL use its native declared or neutral facing. A facing change MAY use a 70ms glyph-only squash; reduced motion SHALL apply the facing change instantly.48+The presentation SHALL maintain facing per entity ID as visual state separate from simulation coordinates. The built-in MIAU and FENN glyphs SHALL be directional; a mod palette object SHALL opt into directional treatment explicitly. Only an accepted horizontal self-move SHALL update an entity's facing. Pushed entities, vertical self-moves, transformations, idle turns, and blocked intents SHALL preserve the entity's prior facing. Glyphs without directional treatment SHALL use their unchanged fallback. Undo SHALL restore the per-ID facing snapshot, while loading or resetting a level SHALL use its native declared or neutral facing. A facing change MAY use a 70ms glyph-only squash; reduced motion SHALL apply the facing change instantly.4949 5050 #### Scenario: A direct horizontal MIAU move updates facing5151 - **WHEN** a MIAU entity makes an accepted horizontal self-move5252 - **THEN** its supported glyph faces the travel direction after the settled turn, with a 70ms glyph-only squash when motion is enabled and unchanged engine coordinates5353 5454 #### Scenario: Pushes and vertical movement preserve facing55-- **WHEN** a KEKE entity is pushed or makes an accepted vertical self-move55+- **WHEN** a FENN entity is pushed or makes an accepted vertical self-move5656 - **THEN** its prior facing remains unchanged, regardless of the movement direction or whether it was directly controlled5757 5858 #### Scenario: A transformation preserves per-ID facing
M openspec/specs/rule-engine/spec.md +2 -2