README: Game
Purpose
This document defines the KNAT application profile for a Game — a Janet brand object that runs a self-contained physics or logic sandbox and exposes state, events, and input surfaces to external observers.
The goal of this profile is to allow KNAT clients to:
- observe the complete game state each frame (positions, scores, board cells)
- subscribe to physics body positions and velocities
- monitor lattice grid mutations
- receive transient matcher events (goals scored, pieces placed, patterns matched)
- inject player input through the control surface (where authorized)
- observe contest lifecycle state
This profile intentionally does not model:
- the region or world the game is hosted within
- participant identity or governance beyond role assignments
- brand economics (dust, escrow) beyond contest-level snapshots
- the JTL program source or phase execution internals
Those surfaces exist elsewhere in the system.
This profile exposes the simulation runtime of the game itself.
Conceptual Model
A game is a simulation — a self-contained sandbox that advances state according to declared rules. This is fundamentally different from both regions and biologics:
Region surface → what the land is (topology + chem state)
Biologic surface → what the body is doing (pose + physiology + intent)
Game surface → what the simulation is doing (state + physics + events + input)
Games span two archetypes that share a single surface:
- Continuous (pong-style) — physics bodies with positions, velocities, clipping planes, and collisions. State changes every tick.
- Discrete (naughts-style) — lattice grids with guarded cell writes, pattern matchers, and turn sequencing. State changes on input.
Both archetypes use the same surface families. Continuous games have bodies and empty lattices. Discrete games have lattices and empty bodies. Hybrid games may use both.
Within the KNAT graph, a game exposes three categories of surface:
Structural surfaces — from the spec, stable for the lifetime of the instance:
spec provenance, genome, kind discriminant
schedule tick rate (fps)
roles participant slot names
Live simulation surfaces — updated each frame by the engine:
state open game state map (the primary data surface)
bodies physics body positions/velocities (continuous games)
lattices lattice grid cells (discrete games)
counters named integer counters
events transient matcher emissions (per-frame)
Intent surface — written by participants, read by the engine:
control input envelopes and drive bindings
The intent surface is policy-shaped: it is present in full view, absent or redacted in projected view.
Root Graph
A game instance KNAT surface is rooted at:
/game::<game_id>/
Top-level families:
/game::<game_id>/
/spec
/engine
/schedule
/roles
/state
/bodies
/lattices
/counters
/events
/control
/contest
The view field on the snapshot indicates whether the surface is complete (full) or policy-shaped (projected). Consumers must not assume completeness when view is projected.
Spec Surface
Provenance of the game spec this instance was loaded from. Static after loading.
/game::<game_id>/spec/ref
/game::<game_id>/spec/genome
/game::<game_id>/spec/kind
| node | type | capabilities |
|---|---|---|
ref |
String | read |
genome |
String | read |
kind |
String (enum) | read |
kind values:
simulation continuous physics game (e.g. pong)
game discrete turn-based game (e.g. naughts)
kind determines which subsystems are active. Consumers should read this first to know whether to expect bodies (simulation) or lattices (game) — though both may be present in hybrid designs.
Engine Surface
Identity of the engine presenting this fiction. Present on all Janet brand objects exposed via KNAT.
/game::<game_id>/engine/name
/game::<game_id>/engine/version
| node | type | capabilities |
|---|---|---|
name |
String | read |
version |
String | read |
Required for deterministic replay and debugging differences between engine versions.
Schedule Surface
Timing configuration from the spec. Static after loading.
/game::<game_id>/schedule/fps
| node | type | capabilities |
|---|---|---|
fps |
Integer | read |
Tells observers the target tick rate. Subscription strategy should account for this — a 60fps game produces 60 state updates per second.
Roles Surface
Participant slot names from the spec. Static after loading.
/game::<game_id>/roles
| node | type | capabilities |
|---|---|---|
roles |
Array | read |
Roles define the identity space for control input routing. In pong, roles are ["left", "right"]. In naughts, roles are ["X", "O"]. Input envelopes carry a role identifier that the axis drive layer uses to route inputs to the correct body or lattice write.
State Surface
The live game state map. Primary data surface. Keys are state names corresponding to $/state/<key> paths in the JTL host. Values are arbitrary JSON — nested objects, arrays, or scalars — written by the simulation each frame.
/game::<game_id>/state/_namespace
/game::<game_id>/state/<key>
| node | type | capabilities |
|---|---|---|
_namespace |
String | read |
<key> |
Any | read, subscribe |
Pong state (continuous):
/game::pong_001/state/ball { "pos": { "x": 0.5, "y": 0.5 }, "vel": { "x": 0.01, "y": 0.01 } }
/game::pong_001/state/paddles { "left": { "x": 0.05, "y": 0.5 }, "right": { "x": 0.95, "y": 0.5 } }
/game::pong_001/state/score { "left": 0, "right": 0 }
Naughts state (discrete):
/game::naughts_001/state/board { "cells": [[null, null, null], ...] }
/game::naughts_001/state/game { "status": "playing" }
_namespace is a tooling hint equaling brand::<game_id>. All non-underscore keys are relative to this prefix.
State is open-ended — game authors define whatever state they need. Adding new state keys does not require schema changes. When view='projected', keys may be absent due to policy.
Note that body positions and lattice values are sync'd to state each frame — they appear here as well as in the dedicated bodies and lattices surfaces. The state surface is the raw view; bodies/lattices are the typed views.
Bodies Surface
Physics body snapshots keyed by body_id. Typed projection of PhysicsWorld — provides scheme metadata and velocity information. Empty for discrete games.
/game::<game_id>/bodies/<body_id>/x
/game::<game_id>/bodies/<body_id>/y
/game::<game_id>/bodies/<body_id>/vx
/game::<game_id>/bodies/<body_id>/vy
/game::<game_id>/bodies/<body_id>/scheme
| node | type | capabilities |
|---|---|---|
x |
Float | read, subscribe |
y |
Float | read, subscribe |
vx |
Float | read, subscribe |
vy |
Float | read, subscribe |
scheme |
String (enum) | read |
scheme values from MotionScheme:
kinematic moves under own velocity (ball)
controlled driven by axis drives (paddle)
static does not move
Bodies advance each tick through: sync_in → world.tick() (integrate + apply constraints) → apply_colliders → sync_out. Kinematic bodies move autonomously. Controlled bodies reflect input-driven positions. Static bodies hold their position.
The KNAT bus bridge (PhysicsGameSession) publishes body scalars as knat.game.<body_id>.<axis> subjects per tick.
Lattices Surface
Lattice grid snapshots keyed by lattice_id. Typed projection of the Lattice subsystem. Empty for continuous games.
/game::<game_id>/lattices/<lattice_id>/width
/game::<game_id>/lattices/<lattice_id>/height
/game::<game_id>/lattices/<lattice_id>/cells
| node | type | capabilities |
|---|---|---|
width |
Integer | read |
height |
Integer | read |
cells |
Array | read, subscribe |
cells is a flat array of nullable strings in row-major order (index = y * width + x). Null entries represent empty cells.
For naughts, the board lattice is width: 3, height: 3, cells: [null, null, null, null, null, null, null, null, null]. After two moves: ["X", null, null, null, "O", null, null, null, null].
Lattice values are also sync'd to state via LatticeBinding — subscribe to either surface depending on whether you want raw grid shape or the JTL-visible state representation.
Counters Surface
Named integer counters from the JTL host. Pre-initialised from CounterSpec in GameContentSections. Mutated by console.counter.inc/dec/set during JTL execution.
/game::<game_id>/counters/<name>
| node | type | capabilities |
|---|---|---|
<name> |
Integer | read, subscribe |
Counters are the simplest live surface — a named integer. Use them for scores, turn counts, level trackers, or any game-defined integer state.
Events Surface
Transient events emitted during the current frame. Produced by the MatcherEngine from ConstraintEvents (physics boundary hits) or Lattice pattern matches (win lines, draws).
/game::<game_id>/events
| node | type | capabilities |
|---|---|---|
events |
Array | read, subscribe |
Each event carries:
{ "path": "$/defs/exit_matcher.score", "payload": { "side": "low" } }
{ "path": "$/defs/win_matcher.win", "payload": { "player": "X", "line": "diag" } }
Events are not persistent signals — they exist only for the frame in which they were emitted. After the TriggerEngine consumes them, they are gone. Consumers must subscribe and process events per-frame to avoid missing them.
Events flow through the frame sequence as:
physics.tick() → constraint events → MatcherEngine → EmittedEvent[]
→ write to state → TriggerEngine (set/inc/reset ops) → consumed
Control Surface
Intent injection surface for participants. Current input state written by external agents. Present in full view; absent or redacted in projected view for observers without control authority.
/game::<game_id>/control/_namespace
/game::<game_id>/control/<input_name>
| node | type | capabilities | notes |
|---|---|---|---|
_namespace |
String | read | tooling hint |
<input_name> |
Object | read, write | last input envelope |
Pong control (continuous):
/game::pong_001/control/move_paddle { "player": "left", "y": 0.7 }
The axis drive layer translates this input into a Controlled body's position: input move_paddle.y is routed to left_paddle.y when move_paddle.player == "left".
Naughts control (discrete):
/game::naughts_001/control/choose_cell { "player": "X", "x": 1, "y": 1 }
The discrete write layer consumes this input: if the target cell is empty and game.status == "playing", the value is written to the lattice.
Control values are input to the simulation, not output. They represent what the participant is trying to do. The simulation decides whether the input takes effect (guard evaluation, constraint checking).
When view is projected, the entire control family may be absent. Consumers must treat absence as "control not observable" — not as "no input."
Writing to /control/<input_name> requires input authority for the relevant role. Unauthorized writes are rejected.
Contest Surface
Active contest lifecycle state. Keyed by contest_id. Present only when the game spec declares contests via GameContentSections.contests.
/game::<game_id>/contest/<contest_id>/contest_type
/game::<game_id>/contest/<contest_id>/state
/game::<game_id>/contest/<contest_id>/participants
/game::<game_id>/contest/<contest_id>/winner_id
/game::<game_id>/contest/<contest_id>/escrow_balance
| node | type | capabilities |
|---|---|---|
contest_type |
String (enum) | read |
state |
String (enum) | read, subscribe |
participants |
Array | read, subscribe |
winner_id |
String/null | read, subscribe |
escrow_balance |
Integer | read |
contest_type values from ContestType:
head_to_head two participants, one winner
multi_way N participants, one winner
tournament bracket structure
state lifecycle from ContestState:
open → active → completed | cancelled | resolved
The transition from open to active occurs when the required number of participants have joined. Full contest lifecycle, escrow management, and resolution details are available through the contest authority — the game surface exposes the observable snapshot only.
Cross-Domain Joins
The game KNAT surface is more self-contained than regions or biologics, but connects to the contest authority.
Game ↔ Contest Authority: contest/<id>/state and participants join to janet_contest::Contest exposed through the contest ops authority. The game surface is the observable snapshot; the authority handles mutations (initiate, add participant, record fitness, determine winner).
Game ↔ Roles: roles defines the identity space. Control input envelopes carry a role (player field) that routes through axis drives or discrete writes. Observers can correlate roles entries with contest/<id>/participants[*].agent_id when contests map roles to agents.
Game ↔ Brand Object: Games are loaded as LoadedBrandObject — they have a BundleDocument with a BundleId, version, and fingerprint. The spec ref links back to the bundle registry.
Snapshot Format
The full game KNAT snapshot is defined in defs/games/runtime.json.
A snapshot represents all surfaces at a single frame. Consumers connecting cold receive a full snapshot before subscribing to incremental updates.
view field semantics:
"full"— all surfaces are present, includingcontrol"projected"— policy-shaped;controlmay be absent;statemay be partial
Minimal MVP Implementation
A minimal game KNAT implementation exposes:
spec(ref, genome, kind)engine(name, version)schedule(fps)rolesstate(open game state map)frame
Progressive additions:
bodies— typed physics body view (requires PhysicsWorld integration)lattices— typed lattice grid view (requires Lattice subsystem)counters— named integer counters (requires ConsoleHost)events— transient matcher emissions (requires MatcherEngine)control— input injection surface (requires role-based authorization)contest— contest lifecycle (requires contest authority wiring)
The minimal surface allows tools to observe the full game state, the tick rate, and who can participate — without requiring physics or contest integration.
Example Graph
Continuous game (pong):
/game::pong_001/
spec
ref "plantange:games/pong@v1"
genome "009a1012322ea637"
kind "simulation"
engine
name "janet-game-v1"
version "1.0.0"
schedule
fps 60
roles ["left", "right"]
state
_namespace "brand::pong_001"
ball { "pos": { "x": 0.5, "y": 0.5 }, "vel": { "x": 0.01, "y": 0.01 } }
paddles { "left": { "x": 0.05, "y": 0.5 }, "right": { "x": 0.95, "y": 0.5 } }
score { "left": 3, "right": 1 }
bodies
ball_body
x 0.5
y 0.5
vx 0.01
vy 0.01
scheme "kinematic"
left_paddle
x 0.05
y 0.5
vx 0.0
vy 0.0
scheme "controlled"
right_paddle
x 0.95
y 0.5
vx 0.0
vy 0.0
scheme "controlled"
lattices {}
counters {}
events
[{ "path": "$/defs/exit_matcher.score", "payload": { "side": "low" } }]
control
_namespace "brand::pong_001/control"
move_paddle { "player": "left", "y": 0.7 }
frame 4200
Discrete game (naughts):
/game::naughts_001/
spec
ref "plantange:games/naughts@v1"
genome "009abc12322ea637"
kind "game"
engine
name "janet-game-v1"
version "1.0.0"
schedule
fps 60
roles ["X", "O"]
state
_namespace "brand::naughts_001"
board { "cells": [["X", null, null], [null, "O", null], [null, null, null]] }
game { "status": "playing" }
bodies {}
lattices
board
width 3
height 3
cells ["X", null, null, null, "O", null, null, null, null]
counters {}
events []
control
_namespace "brand::naughts_001/control"
choose_cell { "player": "X", "x": 0, "y": 0 }
contest
match
contest_type "head_to_head"
state "active"
participants
[{ "agent_id": "player_1", "eligible": true },
{ "agent_id": "player_2", "eligible": true }]
winner_id null
escrow_balance 100
frame 3
Frame Tick Sequence
For consumers building real-time game observers, the per-frame execution order matters:
1. JTL phase apply_input read input events, update host state
2. Axis drives route control inputs to Controlled bodies
3. Discrete writes apply guarded lattice writes from input
4. sync_in state → PhysicsWorld body positions
5. world.tick() integrate velocities, apply constraints
6. Colliders detect and resolve body-body collisions
7. Matchers evaluate constraint/lattice patterns → events
8. sync_out PhysicsWorld positions → state
9. Triggers consume events, mutate state (set/inc/reset)
10. JTL phase scoring read updated state, compute scores
State, bodies, lattices, counters, and events in the snapshot reflect the post-scoring state — the final result of the complete tick.
Future Extensions
Possible future surfaces:
/game::<game_id>/replay frame-addressable replay log
/game::<game_id>/constraints structural clipping planes and bounds
/game::<game_id>/interactions collider and discrete write definitions
/game::<game_id>/triggers active trigger rules
/game::<game_id>/schedule/turn turn-order state for discrete games
These are intentionally excluded from the current profile.