Context
createGame() owns authoritative settled state, history, restore, and turn
resolution. A turn currently moves player-controlled objects, reparses and
transforms rules, advances automata, reparses and transforms again, then
resolves contacts and victory. state.objects and automata phases are carried
through snapshots, while the solver's codec can accept dynamic fields only
through an explicit scalar registry. The missing seam is a mechanic lifecycle
that all of those boundaries can consume.
Goals and non-goals
Goals
- Keep mechanic definitions data-only, deterministic, and validated before play.
- Give every mechanic one authoritative state envelope and one turn lifecycle.
- Preserve that state through reset, accepted and rejected turns, Undo, restore, solver replay, pack export, and data-only share links.
- Make event and solver registration requirements visible before a mechanic is eligible for bundled-world acceptance.
- Keep mechanics host-defined. A CommonJS pack declares supported metadata but does not provide callbacks or executable behavior.
Non-goals
- Decide the semantics of gates, switches, plates, FRAIL, crumbling, counters, or floor layers.
- Add a renderer, audio cue, or visual language for a mechanic.
- Permit remote or untrusted code to implement mechanics.
- Replace the existing object, automaton, rule, or contact model.
Decisions
Use a normalized declarative definition
After mod normalization, a level may contain a mechanics array. Each entry
has a stable lowercase id, a host-supported type, a version, static
configuration, and state: 'derived' or state: 'stateful'. The host adapter,
not the mod, owns a stateful scalar schema and initial values. Static configuration may
identify authored object IDs or channels, but it cannot contain functions,
URLs, or values outside the host schema. The host assigns a unique order by
mechanic ID and validates all object references before creating a game.
The exact fields for a concrete mechanic are defined by its later capability spec. This foundation only requires that the host can validate the definition, initialize state when needed, and supply either a state codec registration or a derived no-hidden-state assertion.
Separate derived projections from stateful mechanics
The settled snapshot may contain mechanics, keyed by mechanic ID, for
stateful adapters. Each stateful value is owned by the host implementation and
contains only declared fields. The engine initializes it from the normalized
level, clones it with the same transaction snapshot as objects and automata,
and rejects unknown IDs or fields during restore.
A derived adapter declares an empty state contract and recomputes its projection from settled objects, rules, occupancy, and authored metadata. Derived outputs such as gate openness, plate pressure, jam status, and FRAIL existence are not stored as a second mutable truth and are not redundantly encoded in solver frontier bytes. A mechanic must not hide mutable state in renderer, juice, module globals, or authored object fields.
For levels without mechanics, the existing public behavior remains unchanged; the envelope may be omitted from serialized data when empty. Stateful mechanics must have a complete initialized value; derived mechanics must have an explicit empty state contract.
Define explicit phase hooks, not arbitrary callbacks
The engine exposes a host-internal lifecycle with deterministic phases:
beforeMovement: inspect the settled state and prepare blockers or intent.afterMovement: observe player movement before the first rule refresh.afterTransforms: observe the final noun/rule projection after automata movement and the second rule refresh and transform.beforeContacts: apply mechanic effects that intentionally precede SINK or DEFEAT contacts.afterContacts: observe destruction and final occupancy.settled: finalize state and event data.
The phase order is part of the engine contract. The existing player refresh and
transform, automata advance, and final refresh and transform remain between
afterMovement and afterTransforms; no hook sees a half-resolved state in
that interval. A concrete mechanic must state which hooks it uses. Hook
implementations are registered host code selected by a validated type; mod
data cannot add a callback.
All stateful hook mutation is transactional. Rejected or complexity-blocked turns do not commit mechanic state. A mechanic must be deterministic from the authored definition, the prior settled state, and the current phase inputs.
Treat events as the presentation boundary
Settled results retain the existing movement, rule, transformation,
destruction, and automata fields and gain a normalized mechanics collection
only when a mechanic reports a transition. Each record contains the mechanic
ID, stable subject ID when applicable, from and to state values or a
declared event payload, and a bounded cause/type token. The engine returns
cloned event data so presentation cannot mutate authoritative state.
The foundation does not prescribe names such as pressed or crumbled.
Concrete mechanic specs define their event vocabulary and ordering.
Make solver registration mandatory
The solver codec receives the same compiled host-owned mechanic plan as the engine. Each stateful registration has a stable mechanic ID, an ordered schema, and a codec version. The registry must encode every dynamic field that can affect a future turn. A derived registration instead identifies its authoritative projection inputs and asserts that no additional mutable bit is required. Unknown mechanic IDs, fields, types, missing values, or unregistered runtime state fail with actionable errors. Static authored metadata stays outside the frontier blob.
The plan supplies explicit codec noun tokens used by a mechanic's static configuration. A leftover noun may not occur in the initial board or authored vocabulary, so the FRAIL adapter must add both map keys and values before codec token indices are assigned. Arbitrary mechanic strings are not assumed to be nouns.
The registry may use fixed authored slots and scalar fields already supported by
the codec. If a future mechanic needs arrays, maps, or more than the scalar
range, it must add an explicit deterministic codec representation rather than
falling back to JSON key order. Encoding and decoding must round-trip through
game.restore() without applying a transform or advancing an automaton.
Preserve data through pack and share boundaries
Mod validation, normalization, catalog registration, export, and data-only sharing preserve the normalized mechanic definitions. Shared links never carry executable mechanic code. A pack declaring a mechanic type unsupported by the host fails with a feature diagnostic before installation; it is not treated as an ordinary noun transformation.
Risks and mitigations
- State omission merges solver futures. Fail closed on unknown dynamic fields and require a same-board/different-state identity fixture per later mechanic.
- Hook order diverges from gameplay. Add public/fast parity tests at every lifecycle boundary before enabling solver acceptance.
- Metadata drifts during sharing. Round-trip normalized pack, exported CommonJS, and shared JSON fixtures through the same validator.
- A generic schema becomes an accidental plugin API. Keep type handlers host-owned and reject functions, callbacks, and unknown fields.
- A future mechanic needs richer state than scalar fields. Version the registry and require a mechanic-specific codec extension before content use.
Migration and rollback
- Add metadata validation and normalization with no existing mechanic entries.
- Add the engine envelope and lifecycle adapter with empty-mechanic parity.
- Connect restore, Undo, event cloning, pack/share, and solver registry tests.
- Add later gate and FRAIL behavior changes one at a time.
Rollback removes the foundation consumers and leaves existing packs valid. A later mechanic must not be enabled until this contract and its codec fixtures are present.
Open questions for later mechanic specs
- Which static metadata fields identify entities, channels, or authored slots?
- Which mechanics need
beforeContactsversusafterContactssemantics? - Does a mechanic's state transition remain valid when a referenced object is transformed or destroyed?
- What event payload is sufficient for presentation without exposing internal mutable state?
- Which concrete mechanic types should be included in the host feature list?