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 FRAILandmechanics: [{ 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
frailmechanic, or an object-levelleavesfield - 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 FRAILand pushes a word ahead without leaving the cell - THEN the DRIFT object remains and no
crumbledrecord 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
crumbledrecord 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' }
Scenario: A two-link chain advances over two departures
- WHEN the map contains
SAND: 'SHOAL'andSHOAL: '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.leavesand 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
crumbledrecords
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