Appearance
Movement & Piece Actions
For grid-based board games (Chess, Checkers, and similar), writing a separate top-level Action for every piece type gets repetitive fast — a Bishop, a Rook, and a Queen all "slide," they just differ in which directions they're allowed to slide along. ATLAS lets you describe this shape declaratively, directly on the piece, instead.
Pieces
pieces is the entity registry key used for movement-driven board games — it behaves identically to entities (same blueprint/instantiation rules, same properties bag), it's simply the conventional name when every blueprint in the array is a board piece.
yaml
pieces:
- id: Knight
symbol: N
movement:
- type: Leap
direction: [2, 1]
symmetry: Full
condition: $target.is_empty or $target.has_enemyid remains the canonical identifier used by selectors, actions, and mutations. An optional symbol gives compact board setup notation a declared token that resolves back to that ID; for example, symbol: N maps an N in a setup string to the Knight blueprint.
The Movement Grammar
Each entry in a piece's movement array describes one legal way that piece can travel. An entry combines a shape (type, plus direction/range) with a condition guard, and optionally mutations that run automatically when a move of that shape is taken.
Movement Types
| Type | Description |
|---|---|
Step | A single move of exactly range squares along direction (default range: 1). |
Slide | Repeated movement along direction, up to range squares (or Unlimited), stopping at the first occupied square. |
Leap | A fixed-offset jump described by a literal direction: [x, y] vector, ignoring any pieces in between. Add symmetry: Full to expand a single asymmetric offset into every rotation/reflection of it (e.g. the Knight's [2, 1] becomes all 8 L-shaped jumps) — see Symmetry. |
Capture | The same shape vocabulary as Step/Slide, but declared separately for pieces whose capture geometry differs from their normal movement (e.g. a Pawn's diagonal capture vs. its forward move). |
EnPassant | A specialized capture that reads the state of an adjacent square/entity rather than the destination square. |
Direction
direction accepts a single value or a list (combining axes for pieces that move along more than one). A value is either one of the named presets below, or a literal [x, y] vector for a custom offset not covered by a preset (used by Leap, e.g. the Knight's [2, 1]):
Orthogonal: along the grid's rank/file axes.Diagonal: along the four diagonal axes.Omnidirectional:Orthogonal+Diagonalcombined, for pieces like the Queen or King — equivalent to[Orthogonal, Diagonal]but preferred as a single readable value.Forward/ForwardDiagonal: relative to the owning player's perspective — the engine mirrors these automatically per side, which is why a single Pawn definition works for both White and Black.
yaml
movement:
- type: Slide
direction: Omnidirectional
range: Unlimited
condition: $path.is_clear and ($target.is_empty or $target.has_enemy)Directional Sets (Masks)
To keep the YAML human-readable, each named direction maps to a predefined set of [x, y] vectors ("mask") hardcoded in the engine — authors never write raw coordinate offsets for the common cases:
| Direction | Vectors | Typical piece |
|---|---|---|
Orthogonal | [0, 1] [0, -1] [1, 0] [-1, 0] | Rook |
Diagonal | [1, 1] [1, -1] [-1, 1] [-1, -1] | Bishop |
Omnidirectional | Orthogonal + Diagonal (all 8 vectors) | Queen, King |
Orthogonal, Diagonal, and Omnidirectional are grid-relative — they mean the same thing regardless of which player owns the piece. Each is really just a base vector ([1, 0] or [1, 1]) expanded to its full rotational/reflective set — the presets are simply pre-expanded convenience values for the common cases; Leap uses the same direction field with a literal, un-expanded vector instead (see below).
Symmetry (Expanding a Vector)
When direction is given as a literal vector rather than a named preset — the case for Leap — add symmetry: Full to expand that single offset into its complete set of rotations and reflections, instead of only ever jumping along the one literal vector written:
yaml
pieces:
- id: Knight
movement:
- type: Leap
direction: [2, 1]
symmetry: Full
condition: $target.is_empty or $target.has_enemysymmetry: Full on direction: [2, 1] produces the 8 Knight-style offsets ([2, 1] [1, 2] [-2, 1] [-1, 2] [2, -1] [1, -2] [-2, -1] [-1, -2]) — this is a generic geometric operation, not a piece-specific rule, so it applies equally to any asymmetric custom leap a game designer invents. Omit symmetry (or set it to None, the default) to keep the vector literal, e.g. for a piece that can only leap in one fixed direction. symmetry has no effect on the named presets (Orthogonal, etc.) — they're already fully expanded.
Player-Perspective Directions
Forward and ForwardDiagonal are the only masks resolved relative to the owning player instead of the raw grid — the engine flips the axis that points toward the opponent's back rank for each player, so the same piece definition covers every side:
| Direction | White vectors | Black vectors (mirrored) |
|---|---|---|
Forward | [0, 1] | [0, -1] |
ForwardDiagonal | [1, 1] [-1, 1] | [1, -1] [-1, -1] |
This mirroring applies to any player role in a game with more than two players too — the engine derives "forward" per-player from each Player's configured facing/seat, not just a fixed White/Black pair.
Range
An integer number of squares, or Unlimited for slides that travel until they hit the edge of the board or a blocking piece.
Condition Context
condition uses the same boolean expression language used throughout ATLAS, plus a few accessors scoped to the move currently being evaluated:
self: the piece being moved (e.g.self.has_moved).$target: the destination square/entity (e.g.$target.is_empty,$target.has_enemy).$path: the squares between origin and destination, only meaningful forSlide(e.g.$path.is_clear).$engine.state/$engine.board: broader read access for guards that need more than the immediate origin/destination (e.g.$engine.board.adjacent_enemy.en_passant_vulnerable).
Mutations
A movement entry's mutations list is applied automatically whenever that specific movement shape is the one selected/executed — for example, flagging a Pawn en_passant_vulnerable after its two-square opening move, or destroying the captured Pawn as part of an EnPassant capture.
yaml
pieces:
- id: Pawn
properties:
has_moved: false
en_passant_vulnerable: false
movement:
- type: Step
direction: Forward
range: 2
condition: $target.is_empty and $path.is_clear and self.has_moved == false
mutations: [ set self.en_passant_vulnerable = true ]
- type: EnPassant
direction: ForwardDiagonal
range: 1
condition: $target.is_empty and $engine.board.adjacent_enemy.en_passant_vulnerable == true
mutations: [ destroy $engine.board.adjacent_enemy ]Reactive Triggers
triggers is a terse, piece-scoped shorthand for "when X happens to this piece, do Y" — for simple reactions that don't need the full reactions/triggered actions machinery.
yaml
pieces:
- id: Pawn
triggers:
- event: AfterMove
condition: self.y == $board.final_rank(self.owner)
action: Promote(Queen, Rook, Bishop, Knight)Each trigger entry supports:
event: the lifecycle hook to react to (e.g.AfterMove).condition: a boolean guard evaluated against the piece and its context.action: the engine action to invoke when the condition is met, written as a function-call expression.
Entity-Owned Actions with Targets
Some moves — Castling being the canonical example — can't be expressed as a single origin/destination movement entry because they involve two pieces at once. For these, declare an ordinary entity-owned action with a targets list describing what else must be selected and validated:
yaml
pieces:
- id: King
properties:
has_moved: false
actions:
- id: Castle
condition: self.has_moved == false and $engine.state.is_in_check(self.owner) == false
targets:
- entity: Rook
condition: >
target.has_moved == false and
target.owner == self.owner and
$path.is_clear(self, target) and
$engine.state.is_path_safe(self, target) == true
mutations:
- move self 2 steps toward target
- move target 1 step past self final_positiontargetsentries constrain which other entity may be selected (entity) and under whatcondition(which can reference bothselfandtarget).mutationsfor multi-piece actions like this may be written as short imperative move descriptions rather than a singleoperation/valuepair, since the move can't be reduced to one entity's origin/destination.
Movement vs. Actions
Use movement for the common case — a single piece moving from one square to another under a shape rule. Reach for an entity-owned action with targets only when a move fundamentally involves more than one entity, like Castling.