rules
Escapement's law book. Every function here is a pure read of `Core.state()` — nothing in this module writes a unit, a tick or a phase. That is the `Director` component's job, and only its job. This module answers questions; the referee acts on the answers.
rules
Escapement's law book. Every function here is a pure read of Core.state() —
nothing in this module writes a unit, a tick or a phase. That is the
Director component's job, and only its job. This module answers questions;
the referee acts on the answers.
local Rules = require("game.rules")
Adjacency the rules resolve against
Rules.links(ring, sector, tick) -- what the teeth allow, PLUS the counterweights
Core.neighbours is the gears alone. Rules.links is the gears plus whatever
ground a counterweight has dropped, and it is what movement, striking and the
blight's climb resolve against. It is memoised per tick and the whole table is
thrown away the moment the tick changes — a link remembered across a tick is
exactly the bug the board is built to create.
Rules.isLinked(aRing, aSector, bRing, bSector, tick)— are two cells joined right nowRules.within(ring, sector, tick, steps, blocked?)— every cell inside a reachRules.pathToEscapement(ring, sector, tick, blocked?)— the shortest climb in
Counterweights
Every other surface on the board is gear-local, and therefore turning. A counterweight is the one thing that is not: a slab of static ground that falls at a fixed world angle and bridges two rings there for three ticks, no matter where the teeth are. It is aimed where the teeth are not already lined up, so a weight always lands as a crossing that did not exist — and it is gone again three ticks later.
| Call | Answers |
|---|---|
Rules.planksAt(tick) | every plank lying on the board — { angle, inner, outer, dropped, expires } |
Rules.plankDroppingAt(tick) | the plank landing on this tick, for whatever animates it |
Rules.plankFooting(plank, ring, tick) | which cells of one ring that plank serves now |
A landing fires counterweightDropped on the signal bus with
{ plank, tick }.
| Constant | Default |
|---|---|
Rules.COUNTERWEIGHT_PERIOD | 3 — a weight falls on every third tick |
Rules.COUNTERWEIGHT_LIFETIME | 3 — how many ticks its plank stays down |
Rules.PLANK_TOLERANCE | 0.30 rad — how far a tooth may sit from the plank |
Legality
Every question answers the same shape, { ok: boolean, reason: string }, so a
refusal can be shown to the player instead of doing nothing.
Rules.canAct(unit)— the gate every commanded action passes firstRules.canMove(unit, ring, sector)Rules.canStrike(unit, target)Rules.canWind(unit, target)Rules.canHold(unit)Rules.reachable(unit)/Rules.strikeTargets(unit)/Rules.windTargets(unit)
The winding economy
Rules.restoreAmount(unit)— three on a mainspring node, one anywhere else.Rules.restsThisTurn(unit)— whether it stood still all turn. Two things have to hold: it was not commanded, and its winding is no lower than it was when the turn began.unit.markis that starting figure, so this reads true whatever drained the automaton and whoever called it.
The blight
Rules.planBlight(unit) returns one intent — drain, move, breach or
hold — for one blight, without touching anything. It is the referee's own
climb: it takes anything adjacent, and otherwise walks the shortest path to
the escapement, going round whatever is in the way.
How the blight thinks belongs to the units slice. When the unit host
offers a runBlight, that is the blight's turn and this plan is not used.
The referee falls back to it so the rust climbs whoever owns the turn.
Rules.freeVent(), Rules.waveDue(tick) and Rules.waveKind(tick) are the
referee's own reinforcements, used only when it is running the blight itself.
The end of the match
Rules.outcome() answers { outcome, reason } or nil. Lost when the rust
stands on the escapement, lost when every automaton has run down, won when the
tick reaches Core.TICKS_TO_HOUR.
Scoped to this part · feeds back into the world's score.