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 source

ADDED Requirements

Requirement: Levels SHALL support a derived FRAIL mechanic

The normalized level SHALL support a level-scoped FRAIL mechanic when opted in; that mechanic SHALL have exactly one entry with { id: 'frail', type: 'frail', version: 1, state: 'derived', leaves: { ... } }. The host SHALL validate every map key and value as a valid noun token and include those tokens in the level-bound solver codec even when no authored object initially has that noun. The map is static level data; objects SHALL NOT carry a per-object leaves field. An unmapped current noun SHALL mean removal on crumble.

Scenario: A valid derived declaration is normalized

  • WHEN a level declares DRIFT IS FRAIL and mechanics: [{ id: 'frail', type: 'frail', version: 1, state: 'derived', leaves: { DRIFT: 'WATER' } }]
  • THEN the normalized level retains the declaration and the initial object state is unchanged

Scenario: Invalid FRAIL metadata is rejected

  • WHEN a pack declares an unknown noun in a FRAIL map, a function-valued map entry, a duplicate frail mechanic, or an object-level leaves field
  • THEN registration fails with the pack and level path and installs none of the invalid level's FRAIL metadata

Requirement: Frail objects crumble after distinct departure

For an accepted turn, the host SHALL evaluate FRAIL once after player and automata movement and all movement-triggered transforms, before SINK, DEFEAT, and WIN contacts. A frail object SHALL crumble only when it was FRAIL at the start of the turn, remains FRAIL at the checkpoint, a distinct occupant departed its starting cell, and no other entity occupies that cell at the checkpoint. A handover SHALL prevent crumbling. A moving FRAIL object SHALL NOT crumble itself; a different FRAIL mover MAY cause departure.

Scenario: Standing on a plank is safe

  • WHEN YOU stands on a DRIFT cell under DRIFT IS FRAIL and pushes a word ahead without leaving the cell
  • THEN the DRIFT object remains and no crumbled record is emitted

Scenario: A handover is safe

  • WHEN a pushed word leaves a FRAIL cell and YOU enters that same cell in the accepted turn
  • THEN the FRAIL object remains because the destination is occupied at the crumble checkpoint

Scenario: A distinct departure crumbles the cell

  • WHEN a distinct word, thing, player, or automaton leaves a FRAIL cell and no other entity remains there after all movement and transforms
  • THEN the FRAIL object crumbles exactly once before contacts

Scenario: A different FRAIL mover can trigger crumble

  • WHEN one FRAIL entity leaves a cell containing a second, stationary FRAIL entity and no other entity remains there
  • THEN only the stationary FRAIL entity crumbles; the mover remains at its destination

Scenario: Moving FRAIL is not self-crumbling

  • WHEN a FRAIL object is pushed or moved by YOU or an automaton
  • THEN it remains at its destination and its old cell does not emit a crumbled record merely because the FRAIL object moved

Requirement: Leftovers preserve identity and chain through nouns

On crumble, the host SHALL look up the object's current noun in the level-scoped frail.leaves map. A mapped noun SHALL replace the current noun at the same cell while retaining the stable ID. An unmapped noun SHALL remove the object. The resulting noun SHALL be eligible for later FRAIL rules, allowing repeated authored chains without a hidden counter.

Scenario: A drift plank becomes water

  • WHEN a DRIFT object crumbles under leaves: { DRIFT: 'WATER' }
  • THEN the same ID is present as WATER at the same coordinate and the settled event reports { id, from: 'DRIFT', to: 'WATER' }
  • WHEN the map contains SAND: 'SHOAL' and SHOAL: 'WATER', and the same object crumbles first as SAND and later as SHOAL
  • THEN the events report SAND/SHOAL and SHOAL/WATER for the same stable ID

Scenario: No leftover removes the object

  • WHEN a FRAIL ROCK has no ROCK entry in frail.leaves and crumbles
  • THEN its stable ID is absent from the settled object state and the event reports { id, from: 'ROCK', to: null }

Requirement: Crumbling precedes existing contacts

The leftover state SHALL be settled before the existing SINK, DEFEAT, and WIN contact phase. A leftover SHALL not create a floor layer or shield any object from contact, including initial reset contacts.

Scenario: Water leftover uses SINK

  • WHEN a frail object becomes WATER under WATER IS SINK
  • THEN the existing contact phase consumes the water according to normal SINK precedence and the final state/events match an authored WATER object

Scenario: Initial water contact remains destructive

  • WHEN a level places an object on a cell containing WATER IS SINK
  • THEN reset resolves the existing contact before the first frame; FRAIL does not shield or defer it

Requirement: Settled events and snapshots expose crumble results

Every accepted turn that crumbles one or more objects SHALL expose a cloned crumbled array of { id, from, to } records, deterministically ordered by stable entity ID. Undo, reset, sharing, replay, and restore SHALL reproduce the authoritative object noun/existence and level-scoped FRAIL declaration exactly.

Scenario: Undo restores the plank

  • WHEN a turn crumbles a DRIFT object and the player chooses Undo
  • THEN the prior DRIFT object, coordinate, mechanic declaration, rules, and history are restored and no crumble event remains active

Scenario: Shared replay preserves a chain stage

  • WHEN a shared level is imported after one leftover link has resolved
  • THEN the decoded snapshot and next transition distinguish its current noun and stable ID from the pre-crumble board and produce the same event

Requirement: Solver transitions include frail state

The compact solver SHALL use the same movement, transform, FRAIL checkpoint, and contact ordering as the public engine. Its key SHALL distinguish object identity, position, current noun, existence, and any registered mechanic projection input that affects a future crumble. The frail.leaves map is level-bound static data; current noun and existence are authoritative object state. Neither may be duplicated as a hidden FRAIL runtime bit. Solver replay SHALL reproduce crumbled and chain transitions without JSON-order or presentation-only state.

Scenario: Same board different chain stages remain distinct

  • WHEN two search states have identical positions but one object is SAND and the other is SHOAL
  • THEN the solver retains them as distinct states and explores their different future leftovers

Scenario: Public and compact transitions agree

  • WHEN a differential fixture runs a handover, pushed-word departure, automaton departure, and two-link chain through both transition paths
  • THEN each path has the same settled objects, rules, contacts, and crumbled records

Scenario: Restore preserves derived FRAIL parity

  • WHEN a snapshot is decoded and restored at either link of a leftover chain
  • THEN public and fast transitions derive the same crumble result from object existence, noun, position, and the level-scoped map without a hidden FRAIL state field

Scenario: Hidden FRAIL state fails closed

  • WHEN an implementation attempts to encode a separate crumble counter, per-object leaves value, or existence bit already represented by the authoritative object snapshot
  • THEN codec or mechanic validation rejects the redundant state before search or play