Context
The current turn lifecycle moves player-controlled things, reparses and transforms rules, advances automata, reparses and transforms again, then resolves contacts. A frail tile is a normal object with an authored stable ID; it is not an underlying floor layer and does not shield a cell from contacts. The mechanic therefore uses the existing object noun and the foundation's mechanic lifecycle rather than adding a second board representation.
Goals and non-goals
Goals
- Make
FRAILa normal rule-driven property. - Make departure, handover, and leftover timing deterministic and visible.
- Count things and words consistently with existing movement and SINK rules.
- Keep the level-scoped noun map attached to the FRAIL mechanic definition; keep the transformed noun and stable entity ID in authoritative object state through transforms, contacts, Undo/reset, shared payloads, and solver replay.
- Support chains without introducing a numeric counter or renderer-only state.
Non-goals
- A floor layer beneath an object.
- A durability counter, crack decoration, or automatic multi-step timer.
- A new contact precedence rule for SINK, DEFEAT, or WIN.
- JavaScript callbacks in a CommonJS module.
Decisions
Frail is a parsed property with a level-scoped declaration
The host recognizes FRAIL as a supported property. A level that uses the
mechanic declares one level-scoped entry:
mechanics: [{
id: 'frail',
type: 'frail',
version: 1,
state: 'derived',
leaves: { SAND: 'SHOAL', SHOAL: 'WATER', DRIFT: 'WATER' },
}]
The declaration has no runtime counter or per-object leaves field. A level
may author DRIFT IS FRAIL, SAND AND SHOAL IS FRAIL, or another valid
noun/property sentence. Active FRAIL membership is derived from the settled
rules and current noun, just like other rule-driven properties. The property
word can therefore be pushed or removed to change future behavior.
Departure is sampled at a turn boundary
For an accepted turn, the host captures the set of objects occupying each cell before movement. It then resolves player movement, reparses/transforms, and resolves active automata using the ordinary atomic push rules. After the final transform phase and before contacts, it evaluates every object that is FRAIL at that checkpoint:
- The object was a FRAIL object in the starting settled cell and is still a FRAIL object at the crumble checkpoint.
- A distinct occupant moved away from that cell during this turn. A pushed word, pushed thing, player, or automaton qualifies, even if that distinct mover is also FRAIL.
- No other entity occupies the frail cell at the crumble checkpoint.
If all three conditions hold, the frail entity crumbles. A mover entering the cell in the same accepted turn is a handover and prevents crumbling. This avoids evaluating the same plank once per mover and makes a pushed word leaving one plank while YOU enters it safe.
The moving FRAIL entity itself never triggers crumbling for its old cell. If a FRAIL entity is pushed or walks away, it remains the entity at its new cell; its own movement is not a departure trigger. Transforming a frail object away does not count as departure; the level-scoped map is consulted only if its current noun later crumbles while FRAIL.
Leftovers are nouns, not counters
The frail.leaves map is validated as a map of syntactically valid noun
tokens. A leftover need not be present among the initially authored objects;
the solver codec includes every map key and value in its level-bound token
table. The map is static level data and is not copied onto objects. When an
object crumbles, from is its current noun and:
- if
frail.leaves[from]exists, it changes into that noun at the same coordinate and keeps its stable ID; - if the current noun is absent from the map, it is removed from the object set.
The resulting noun is part of the same settled turn. If it is also FRAIL and a
later turn has a distinct occupant depart its cell, it can crumble again. This
expresses multi-crossing designs as an authored chain, for example
SAND -> SHOAL -> WATER, using the current noun as the lookup key and without
hidden counts.
Contacts follow the crumble checkpoint
Crumbled leftovers or removed objects participate in the existing contact
phase. A frail.leaves[from] = 'WATER' result is therefore subject to the current
WATER IS SINK rule on that same turn, with the existing contact precedence.
Nothing can be authored as a frail object sitting on a water hazard to bypass
initial contacts: reset still resolves initial contacts before the first frame,
and there is no floor layer.
State and event contract
The mechanic's static definition holds the authored noun map; the dynamic noun
and existence remain in state.objects. The settled result adds crumbled,
ordered deterministically by stable entity ID, with { id, from, to }, where
to is the mapped leftover noun or null for removal. The event is cloned
with the snapshot and is presentation-only. Undo and reset restore the prior
object noun/existence exactly; sharing and CommonJS export preserve the
validated mechanic declaration; the solver key includes object ID, noun,
position, existence, and any registered mechanic projection inputs, without a
separate FRAIL counter or per-object leaves value.
Automata and solver
Automata are ordinary movers for departure and handover purposes. Their action
is resolved before FRAIL evaluation, so a word or thing moved by an automaton
can spend a frail cell and a handoff remains safe. The compact solver uses the
same transition order and replays crumbled results through authoritative
restore. A same-board state with a different current noun or object existence
is distinct.
Tidewater proof scenarios
The Tidewater draft fixtures remain world-team owned; these scenarios define the engine acceptance cases without editing the pack:
- Draft A, One plank each:
DRIFT IS FRAIL, threeDRIFTentities, and afrail.leavesentry ofDRIFT: WATER, with a word pushed across them. Each word departure is safe while YOU hands over the cell; after YOU leaves the final plank it becomes water. The accepted fixture pins the FRAIL sentence. - Draft B, Two tides:
frail.leavesentriesSAND: SHOALandSHOAL: WATERprove a chain as YOU crosses out, then pushesISback over the bridge for the second crossing. The accepted fixture has no ROCK. - Word and automaton crossing: a pushed word and an active automaton each leave distinct FRAIL cells; both produce stable crumble events.
- Initial SINK: a
frail.leavesWATER result and a frail object authored on aWATER IS SINKcell use existing reset/contact behavior, with no shielding exception.
Risks and mitigations
- Half-turn evaluation crumbles a handover. Evaluate once after all movement and transforms, then require the destination cell to be empty.
- A leftover chain disappears from the solver. Keep current noun and existence in the authoritative object state and include them in the compact key; keep the noun map level-bound and add encode/decode/restore parity fixtures without introducing a hidden crumble counter.
- Unknown metadata changes sharing. Validate the FRAIL declaration and
every map key/value as a noun token and reject malformed values atomically.
Reject per-object
leavesas unsupported metadata. - Contact ordering becomes ambiguous. Keep crumble before the existing contact phase and test SINK, DEFEAT, and WIN with a same-turn leftover.
Open questions
- When several entities crumble in one accepted turn, is stable ID ordering sufficient for the renderer's animation and event list, or should authored cell/reading order be the presentation order? The simulation result is independent of that ordering, but the final presentation contract needs a design choice before the renderer is implemented.
- The exact Tidewater fixture IDs and visual glyphs for
DRIFT,SAND, andSHOALremain with the worlds/design teams.