Gridponder DSL — Game Schema
Defines all gameplay content shared across levels: entity kinds, systems, actions, rules, and the level sequence.
Purpose
game.json contains all gameplay content shared across levels: entity kinds, systems, actions, rules, defaults, and the level sequence. It does not repeat identity or presentation metadata — those live exclusively in manifest.json.
Visual presentation and input bindings are defined separately in theme.json — see 06_theme.md.
Top-Level Structure
{
"layers": [ ... ],
"actions": [ ... ],
"entityKinds": { ... },
"systems": [ ... ],
"rules": [ ... ],
"levelSequence": [ ... ],
"defaults": { ... }
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
layers |
array | yes | Board layer definitions. |
actions |
array | yes | Available player action types. |
entityKinds |
object | yes | Catalog of entity kind definitions. |
systems |
array | yes | Enabled engine systems with configuration. |
rules |
array | no | Game-level rules (shared across levels). |
levelSequence |
array | yes | Ordered list of levels and story screens. |
defaults |
object | no | Default values for level fields. |
Layers
Defines the board's layer stack. Each layer has a name and occupancy rule.
"layers": [
{ "id": "ground", "occupancy": "exactly_one", "default": "empty" },
{ "id": "objects", "occupancy": "zero_or_one" },
{ "id": "markers", "occupancy": "zero_or_one" },
{ "id": "actors", "occupancy": "zero_or_one" },
{ "id": "structures","occupancy": "zero_or_one" }
]
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Layer identifier. |
occupancy |
string | yes | "exactly_one" (every cell has a value) or "zero_or_one" (cells may be null). |
default |
string | no | Default cell value for exactly_one layers. Default: "empty". |
The layer order defines rendering order (first = bottom). The avatar is not part of any layer — it is rendered separately on top.
Actions
Declares the abstract action types available in this game.
"actions": [
{
"id": "move",
"params": { "direction": { "type": "direction", "values": ["up", "down", "left", "right"] } }
},
{
"id": "diagonal_swap",
"params": { "direction": { "type": "direction", "values": ["up_left", "up_right", "down_left", "down_right"] } }
},
{
"id": "rotate",
"params": { "rotation": { "type": "enum", "values": ["clockwise", "counterclockwise"] } }
},
{
"id": "flip",
"params": { "axis": { "type": "enum", "values": ["vertical", "horizontal"] } }
},
{
"id": "flood",
"params": {}
},
{
"id": "tap_cell",
"params": { "position": { "type": "position" } }
}
]
Each action:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique action identifier. Referenced by controls and systems. |
params |
object | yes | Parameter definitions. Keys are param names, values describe the type. |
Parameter types:
direction— a direction value from the givenvalueslistposition— a[x, y]board coordinateenum— one of the givenvaluesinteger— an integer value
Entity Kinds
Defines all entity types used by this game. Levels reference kinds by their key name.
"entityKinds": {
"empty": {
"layer": "ground",
"tags": ["walkable"],
"sprite": null,
"description": "Walkable floor"
},
"void": {
"layer": "ground",
"tags": [],
"sprite": "assets/sprites/void.png",
"description": "Non-playable area"
},
"wall": {
"layer": "ground",
"tags": ["solid"],
"sprite": "assets/sprites/wall.png"
},
"water": {
"layer": "ground",
"tags": ["liquid", "walkable"],
"sprite": "assets/sprites/water.png"
},
"rock": {
"layer": "objects",
"tags": ["solid", "breakable"],
"sprite": "assets/sprites/rock.png"
},
"wood": {
"layer": "objects",
"tags": ["solid", "burnable", "pushable"],
"sprite": "assets/sprites/wood.png"
},
"torch": {
"layer": "objects",
"tags": ["pickup"],
"sprite": "assets/sprites/torch.png"
},
"pickaxe": {
"layer": "objects",
"tags": ["pickup"],
"sprite": "assets/sprites/pickaxe.png"
},
"metal_crate": {
"layer": "objects",
"tags": ["solid", "pushable"],
"sprite": "assets/sprites/metal_crate.png"
},
"flag": {
"layer": "markers",
"tags": ["goal_target"],
"sprite": "assets/sprites/flag.png"
},
"portal": {
"layer": "objects",
"tags": ["teleport"],
"sprite": "assets/sprites/portal.png",
"params": { "channel": { "type": "string", "required": true } }
},
"number": {
"layer": "objects",
"tags": ["mergeable"],
"sprite": "assets/sprites/number.png",
"params": { "value": { "type": "integer", "required": true } }
},
"spirit": {
"layer": "actors",
"tags": ["npc"],
"sprite": "assets/sprites/spirit.png",
"params": {
"behavior": { "type": "string", "required": true },
"targetTag": { "type": "string" }
}
},
"colored": {
"layer": "objects",
"tags": ["pushable"],
"sprite": "assets/sprites/colored.png",
"params": { "color": { "type": "string", "required": true } }
}
}
Entity Kind Fields
| Field | Type | Required | Description |
|---|---|---|---|
layer |
string | yes | Which board layer this kind belongs to. Must reference a defined layer. |
tags |
array of strings | yes | Semantic labels. Systems use tags for entity selection. |
sprite |
string or null | no | Path to sprite asset. null means invisible/transparent. May contain a {paramName} placeholder when used with spriteParam. |
spriteParam |
string | no | If set, the {paramName} placeholder in sprite is substituted with this instance parameter's value at render time. Used for entities whose appearance varies by a numeric or string param (e.g. box fragments show different PNG tiles per sides bitmask). sprite must contain {paramName} when this field is set. |
symbol |
string | yes | Single Unicode character used in text grid representations (TextRenderer). Must be unique within a game; @ is reserved for the avatar. Use narrow (display-width 1) characters only — basic ASCII, box-drawing (═║╬), math symbols (≈), or similar. Wide characters (emoji, CJK) break grid alignment and must not be used. |
symbolParam |
string | no | If set, the symbol is taken from this instance parameter at render time instead of symbol. Used for entities whose symbol varies by value (e.g. number tiles show their numeric digit). symbol acts as the legend label and fallback. |
params |
object | no | Parameterized fields that instances may set. Each param defines its type and optionally required. |
description |
string | no | Human-readable description. |
uiName |
string | no | Display name for UI. Defaults to the kind key. |
animations |
object | no | Named animation sequences for this entity. See Animations. |
motion |
object | no | Motion timings used by the renderer when this kind appears in a motion event (tile_moved, tiles_merged with sources, etc.). See Motion. Optional — engine has sensible defaults. |
outline |
object | no | Render hint: stroke a visible border along the perimeter of every contiguous region of cells of this kind. See Outline. Pure render hint — no engine impact. |
render |
object | no | Additional rendering hints (opacity, tint). |
Animations
Entity kinds may define named animation sequences triggered by effects or systems. Each animation is a named key mapping to a frame sequence.
"wood": {
"layer": "objects",
"tags": ["solid", "burnable", "pushable"],
"sprite": "assets/sprites/wood.png",
"animations": {
"burning": {
"frames": ["assets/sprites/wood_fire_ignites.png", "assets/sprites/wood_full_burn.png", "assets/sprites/wood_smoke_and_ash.png"],
"duration": 1500,
"mode": "once"
}
}
}
"rock": {
"layer": "objects",
"tags": ["solid", "breakable"],
"sprite": "assets/sprites/rock.png",
"animations": {
"breaking": {
"frames": ["assets/sprites/rock_with_cracks.png", "assets/sprites/rock_broken.png"],
"duration": 1000,
"mode": "once"
}
}
}
Animation fields:
| Field | Type | Required | Description |
|---|---|---|---|
frames |
array of strings | yes | Ordered sprite paths. Played sequentially. |
duration |
integer | yes | Total animation duration in milliseconds. Divided evenly across frames. |
mode |
string | no | "once" (default) — plays once. "loop" — repeats until state changes. |
Effects reference animations by name via the animation field (see 05_rules.md §5). When an effect specifies an animation, the engine plays it before (or during) the state change. If no animation is specified, the state change is instant.
The animation name is scoped to the entity kind at the target position. For example, "animation": "burning" on a destroy effect at a cell containing wood looks up wood.animations.burning.
Motion
A motion block on an entity kind sets per-cell motion durations the renderer uses when this kind participates in a multi-step cascade (e.g. number tiles sliding under slide_merge, items emitted from a queued_emitters pipe). All keys are optional; the renderer uses engine defaults when omitted. Behaviour is unaffected — these timings only control visual pacing.
"number": {
"layer": "objects",
"tags": ["mergeable"],
"sprite": "assets/sprites/number_{value}.png",
"spriteParam": "value",
"motion": {
"moveDurationMs": 130,
"mergeDurationMs": 200,
"pathStepDurationMs": 80,
"spawnDurationMs": 120
}
}
| Key | Used when | Default |
|---|---|---|
moveDurationMs |
Engine emits tile_moved (slide leg of slide_merge, push, portal exit). Per cell (matches the ice-slide pacing convention). |
130 |
mergeDurationMs |
Engine emits tiles_merged with sources (post-slide merge in slide_merge). |
200 |
pathStepDurationMs |
Engine emits entity_path (multi-cell traversal, e.g. queued-emitter routes). Per cell. |
80 |
spawnDurationMs |
Engine emits entity_spawn (e.g. queued-emitter spawn). |
120 |
How motion events are sequenced
Each turn produces a list of AnimationSteps grouped by an integer stage. Steps with the same stage play in parallel; the next stage starts after the longest step of the previous stage finishes. The default stage is 0, so legacy avatar/entity animations behave exactly as before.
Within a single phase the engine assigns:
motionevents (avatar_move,tile_moved,entity_spawn,entity_path) → stagebase + 0mergeevents (tiles_mergedwith sources) → stagebase + 1destroy/transformanimations (object_removedwithanimation) → stagebase + 2
Each subsequent phase (movement → cascade) advances base past whatever stages the prior phase emitted, so a slide cannot overlap a downstream cascade animation.
Opting out
Systems that emit motion expose an emitMotion config flag (default true). Set it to false to fall back to the legacy snap-to-final-state behaviour:
{ "id": "merge", "type": "slide_merge",
"config": { "emitMotion": false, "...": "..." } }
This is useful for fast-paced packs where the cascade animation would slow play, or for headless contexts where animations are irrelevant.
Outline
An outline block on an entity kind tells the renderer to stroke a visible border along the perimeter of every contiguous region of cells of that kind. Useful for highlighting an owned or connected region whose cells are otherwise indistinguishable from their neighbours — e.g. the flooded territory in flood_colors, the merged cluster in box_builder, or the player's painted area in any colouring puzzle.
"cell_flooded": {
"layer": "objects",
"tags": ["flooded"],
"outline": {
"color": "#222222",
"width": 3
}
}
| Key | Type | Default | Notes |
|---|---|---|---|
color |
string (CSS hex) | "#222222" |
Stroke colour. #RRGGBB or #AARRGGBB. |
width |
integer | 2 |
Stroke width in logical pixels. |
Algorithm: for every cell whose entity is of this kind, the renderer draws a line on each side whose orthogonal neighbour is not of the same kind (or out of bounds). Stitched together this traces the region boundary exactly once. Multiple disconnected regions of the same kind get separate outlines. Layer is taken from the kind's layer field, so the outline picks the right grid automatically.
Pure render hint: no engine logic, no events, no Python solver impact. Implemented in both the Flutter renderer and the Python image renderer used for vision-mode benchmarks.
Tags
Tags are simple semantic labels. They do not carry behavior on their own — systems read tags to decide what to do.
Standard tags for v0.5:
| Tag | Meaning |
|---|---|
solid |
Blocks avatar movement (unless a system handles the interaction) |
walkable |
Avatar can enter this cell |
pickup |
Can be picked up by avatar |
pushable |
Can be pushed by avatar |
breakable |
Can be destroyed by a tool |
burnable |
Can be burned by a fire tool |
liquid |
A terrain type with liquid behavior |
bridge |
Walkable surface created over liquid |
teleport |
Triggers portal system |
mergeable |
Can be merged with matching entities |
goal_target |
Target for reach-type goals |
npc |
Non-player character with autonomous behavior |
target_marker |
Non-blocking marker for NPC targeting |
Games may define custom tags beyond these.
Multi-Cell Object Kinds
Multi-cell objects (e.g., pipes) referenced in level multiCellObjects arrays should also have an entity kind definition in entityKinds. The kind defines tags, sprite, and parameter schema. The layer for multi-cell objects is typically "structures".
"pipe": {
"layer": "structures",
"tags": ["emitter"],
"sprite": "assets/sprites/pipe.png",
"params": {
"queue": { "type": "array", "required": true },
"currentIndex": { "type": "integer" },
"exitPosition": { "type": "position", "required": true },
"exitDirection": { "type": "string" },
"exit2Position": { "type": "position" },
"exit2Direction": { "type": "string" },
"exit2Index": { "type": "integer" }
}
}
When exit2Position is present the pipe is bidirectional: it has two open exits and routes numbers toward the nearer open exit each turn. See §2.5 queued_emitters for the full routing rules.
Systems
Declares which engine systems are active and their configuration.
"systems": [
{
"id": "movement",
"type": "avatar_navigation",
"config": {
"directions": ["up", "down", "left", "right"],
"solidHandling": "delegate"
}
},
{
"id": "push",
"type": "push_objects",
"config": {
"pushableTags": ["pushable"],
"chainPush": false
}
}
]
Each system entry:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique instance identifier for this system. Used by levels for overrides. |
type |
string | yes | Built-in system type. See System Catalog. |
config |
object | yes | System-specific configuration. |
enabled |
boolean | no | Whether the system is active. Default: true. |
See 04_systems.md for the complete system type catalog and config schemas.
Rules (Game-Level)
Game-level rules apply to all levels unless overridden. See 05_rules.md for the full rules model.
"rules": [
{
"id": "water_clears_inventory",
"on": "avatar_entered",
"where": { "position_has_tag": { "layer": "ground", "tag": "liquid" } },
"if": { "avatar": { "hasItem": true } },
"then": [
{ "clear_inventory": {} }
]
}
]
Level Sequence
An ordered list defining the game's playable progression. Entries are either level references or story screens.
"levelSequence": [
{ "type": "story", "title": "Chapter 1", "text": "The rabbit sets out on a journey...", "image": "assets/story/intro.png" },
{ "type": "level", "ref": "fw_001" },
{ "type": "level", "ref": "fw_002" },
{ "type": "story", "text": "The rabbit reaches the river...", "image": "assets/story/river.png" },
{ "type": "level", "ref": "fw_003" },
{ "type": "level", "ref": "fw_004" },
{ "type": "story", "text": "Victory!", "image": "assets/story/victory.png" }
]
Level Entry
| Field | Type | Required | Description |
|---|---|---|---|
type |
"level" |
yes | Marks this as a level. |
ref |
string | yes | Level id. Must match a file in the levels directory. |
Story Entry
| Field | Type | Required | Description |
|---|---|---|---|
type |
"story" |
yes | Marks this as a story screen. |
title |
string | no | Optional heading text. |
text |
string | no | Body text. |
image |
string | no | Path to an illustration. |
At least one of text or image must be present. Story screens are displayed between levels and dismissed by tapping.
Defaults
Default values applied to levels when those fields are not specified. This reduces repetition.
"defaults": {
"avatar": {
"enabled": true,
"facing": "right",
"inventory": { "slot": null }
},
"maxCascadeDepth": 3
}
| Field | Type | Description |
|---|---|---|
avatar |
object | Default avatar configuration. Levels can override. |
maxCascadeDepth |
integer | Maximum cascade passes during rule resolution. Default: 3. |
Complete Example
{
"id": "com.gridponder.flag_worlds",
"dslVersion": "0.5",
"title": "Flag Worlds",
"description": "Guide the rabbit to the flag using tools, crates, and portals.",
"layers": [
{ "id": "ground", "occupancy": "exactly_one", "default": "empty" },
{ "id": "objects", "occupancy": "zero_or_one" },
{ "id": "markers", "occupancy": "zero_or_one" },
{ "id": "actors", "occupancy": "zero_or_one" }
],
"actions": [
{ "id": "move", "params": { "direction": { "type": "direction", "values": ["up","down","left","right"] } } }
],
"entityKinds": {
"empty": { "layer": "ground", "tags": ["walkable"], "sprite": null },
"void": { "layer": "ground", "tags": [], "sprite": "assets/sprites/void.png" },
"wall": { "layer": "ground", "tags": ["solid"], "sprite": "assets/sprites/wall.png" },
"water": { "layer": "ground", "tags": ["liquid", "walkable"], "sprite": "assets/sprites/water.png" },
"bridge":{ "layer": "ground", "tags": ["walkable"], "sprite": "assets/sprites/bridge.png" },
"rock": { "layer": "objects", "tags": ["solid", "breakable"], "sprite": "assets/sprites/rock.png",
"animations": { "breaking": { "frames": ["assets/sprites/rock_with_cracks.png", "assets/sprites/rock_broken.png"], "duration": 1000, "mode": "once" } } },
"wood": { "layer": "objects", "tags": ["solid", "burnable", "pushable"], "sprite": "assets/sprites/wood.png",
"animations": { "burning": { "frames": ["assets/sprites/wood_fire_ignites.png", "assets/sprites/wood_full_burn.png", "assets/sprites/wood_smoke_and_ash.png"], "duration": 1500, "mode": "once" } } },
"torch": { "layer": "objects", "tags": ["pickup"], "sprite": "assets/sprites/torch.png" },
"pickaxe": { "layer": "objects", "tags": ["pickup"], "sprite": "assets/sprites/pickaxe.png" },
"metal_crate": { "layer": "objects", "tags": ["solid", "pushable"], "sprite": "assets/sprites/metal_crate.png" },
"flag": { "layer": "markers", "tags": ["goal_target"], "sprite": "assets/sprites/flag.png" },
"portal": { "layer": "objects", "tags": ["teleport"], "sprite": "assets/sprites/portal.png",
"params": { "channel": { "type": "string", "required": true } } }
},
"systems": [
{ "id": "movement", "type": "avatar_navigation", "config": { "directions": ["up","down","left","right"], "solidHandling": "delegate" } },
{ "id": "push", "type": "push_objects", "config": { "pushableTags": ["pushable"], "chainPush": false } },
{ "id": "portals", "type": "portals", "config": { "teleportTags": ["teleport"], "matchKey": "channel", "endMovement": true } }
],
"rules": [
{
"id": "pickup_item",
"on": "avatar_entered",
"where": { "position_has_tag": { "layer": "objects", "tag": "pickup" } },
"then": [
{ "set_inventory": { "item": "$cell.objects.kind" } },
{ "destroy": { "position": "$event.position", "layer": "objects" } }
]
},
{
"id": "torch_burns",
"on": "move_blocked",
"where": { "position_has_tag": { "layer": "objects", "tag": "burnable" } },
"if": { "avatar": { "hasItem": "torch" } },
"then": [
{ "destroy": { "position": "$event.position", "layer": "objects", "animation": "burning" } },
{ "clear_inventory": {} },
{ "resolve_move": {} }
]
},
{
"id": "pickaxe_breaks",
"on": "move_blocked",
"where": { "position_has_tag": { "layer": "objects", "tag": "breakable" } },
"if": { "avatar": { "hasItem": "pickaxe" } },
"then": [
{ "destroy": { "position": "$event.position", "layer": "objects", "animation": "breaking" } },
{ "clear_inventory": {} },
{ "resolve_move": {} }
]
},
{
"id": "object_creates_bridge",
"on": "object_placed",
"where": { "all_of": [
{ "position_has_tag": { "layer": "ground", "tag": "liquid" } },
{ "position_has_tag": { "layer": "objects", "tag": "pushable" } }
]},
"then": [
{ "destroy": { "position": "$event.position", "layer": "objects" } },
{ "transform": { "position": "$event.position", "layer": "ground", "toKind": "bridge" } }
]
},
{
"id": "water_clears_items",
"on": "avatar_entered",
"where": { "position_has_tag": { "layer": "ground", "tag": "liquid" } },
"if": { "avatar": { "hasItem": true } },
"then": [
{ "clear_inventory": {} }
]
}
],
"levelSequence": [
{ "type": "level", "ref": "fw_001" },
{ "type": "level", "ref": "fw_002" },
{ "type": "level", "ref": "fw_003" }
],
"defaults": {
"avatar": { "enabled": true, "facing": "right", "inventory": { "slot": null } },
"maxCascadeDepth": 3
}
}