game.rules — the law and the referee of Verdigris Cloister.
Owns everything in §3 of the design spec: the grid state, initiative, action
points, path cost, line of sight, three-state directional cover, flanking,
elevation, the itemised hit chance, overwatch, movement undo, damage, death,
mission objectives and outcome.
This module WRITES `Core.state()`. No other slice does. Everyone else reads
it, or better, reads `Director:snapshot()`.
Nothing in here runs on a timer. Every state change is the direct
consequence of a call. The Director component paces the blight's own turn so
a human can watch it, which is the enemy acting, not a clock on the player.
blightForm(id: string) → any
--!desc Look up a Blight form record by id.
--!arg id A blight form id — "mote", "shard" or "hulk".
--!return The form record, or nil.
--!example local f = Rules.blightForm("hulk")
| arg | type | description |
|---|
| id | string | |
rng( ) → number
Deterministic randomness. Stored in state so a reload keeps the thread.
setSeed(seed: number) → void
--!desc Reseed the referee's dice so a sequence of rolls can be repeated.
--!arg seed Any integer.
--!example Rules.setSeed(7)
| arg | type | description |
|---|
| seed | number | |
setPhaseHandler(name: string, fn: ((any) → void
--!desc Register the function that resolves a phase. Return "defer" from it
--! to keep the phase open and close it later with Rules.phaseDone(name).
--!arg name "garden" or "blight".
--!arg fn Called with the live state table.
--!example Rules.setPhaseHandler("garden", function(s) ... end)
| arg | type | description |
|---|
| name | string | |
| fn | ((any | |
setActionHandler(name: string, fn: ((any, any, number, number) → void
--!desc Register the effect of a gardening action. The referee still owns
--! legality and the AP spend; this owns what happens to the tile.
--!arg name "sow", "burn" or "flood".
--!arg fn Called with (state, unit, x, y).
--!example Rules.setActionHandler("sow", function(s, u, x, y) ... end)
| arg | type | description |
|---|
| name | string | |
| fn | ((any, any, number, number | |
hasHandler(kind: string, name: string) → boolean
--!desc Whether a phase or action handler is currently registered.
--!arg kind "phase" or "action".
--!arg name The handler name.
--!return true when a slice has claimed it.
--!example if Rules.hasHandler("phase", "garden") then ... end
| arg | type | description |
|---|
| kind | string | |
| name | string | |
sign(v: number) → number
| arg | type | description |
|---|
| v | number | |
facingFrom(dx: number, dy: number) → string
| arg | type | description |
|---|
| dx | number | |
| dy | number | |
clamp(v: number, lo: number, hi: number) → number
| arg | type | description |
|---|
| v | number | |
| lo | number | |
| hi | number | |
tileOf(unitId: string) → any
--!desc The tile a unit stands on.
--!arg unitId A unit id.
--!return The tile table, or nil.
--!example local t = Rules.tileOf("anselm")
| arg | type | description |
|---|
| unitId | string | |
unitAt(x: number, y: number) → any
--!desc The living unit standing on a tile.
--!arg x Tile column.
--!arg y Tile row.
--!return The unit table, or nil.
--!example local u = Rules.unitAt(4, 6)
| arg | type | description |
|---|
| x | number | |
| y | number | |
elevAt(x: number, y: number) → number
--!desc Elevation in whole steps of a tile, 0 when off the board.
--!arg x Tile column.
--!arg y Tile row.
--!return The elevation.
--!example local e = Rules.elevAt(3, 3)
| arg | type | description |
|---|
| x | number | |
| y | number | |
coverProvided(t: any) → number
How much cover the CONTENTS of a tile give to somebody standing beside it.
blocksSight(x: number, y: number) → boolean
--!desc Whether a tile stops a sightline through it.
--!arg x Tile column.
--!arg y Tile row.
--!return true when nothing can be seen through this tile.
--!example if Rules.blocksSight(4, 5) then ... end
| arg | type | description |
|---|
| x | number | |
| y | number | |
recompute( ) → void
Derived board data. Recomputed after anything touches a tile.
--!desc Recompute every derived tile property: what cover each tile gives,
--! what cover its occupant receives, and the Blight's share of the board.
--!example Rules.recompute()
lineOfSight(ax: number, ay: number, bx: number, by: number) → void
Line of sight — a supercover walk between two tile centres.
--!desc Whether one tile can see another. The endpoints never block.
--!arg ax Origin column.
--!arg ay Origin row.
--!arg bx Target column.
--!arg by Target row.
--!return Two values: true/false, and the {x, y} of the blocker when false.
--!example local ok, blocker = Rules.lineOfSight(2, 2, 6, 8)
| arg | type | description |
|---|
| ax | number | |
| ay | number | |
| bx | number | |
| by | number | |
coverAgainst(tx: number, ty: number, fx: number, fy: number) → void
Cover, from a direction. This is the three-state property of §3.
--!desc The cover a unit standing on (tx, ty) gets against a shot coming
--! from (fx, fy). None / half / full, from Core.COVER.
--!arg tx Target column.
--!arg ty Target row.
--!arg fx Shooter column.
--!arg fy Shooter row.
--!return Two values: the cover value, and the {x, y} tile giving it.
--!example local c, src = Rules.coverAgainst(4, 5, 4, 1)
| arg | type | description |
|---|
| tx | number | |
| ty | number | |
| fx | number | |
| fy | number | |
flankOf(target: any, fx: number, fy: number) → string
--!desc How a shooter stands relative to a target's facing: "front", "side"
--! or "behind". This is the flanking read.
--!arg target The target unit table.
--!arg fx Shooter column.
--!arg fy Shooter row.
--!return "front", "side" or "behind".
--!example local f = Rules.flankOf(unit, 3, 3)
| arg | type | description |
|---|
| target | any | |
| fx | number | |
| fy | number | |
coverVsThreats(x: number, y: number, side: string) → any
--!desc The cover a unit of `side` would actually get standing on a tile,
--! measured against the enemies that can see it — the WORST of them, so
--! the number is the protection the tile really buys. A tile whose only
--! cover faces the wrong way reads as open, which is the honest answer.
--!arg x Tile column.
--!arg y Tile row.
--!arg side The side standing there, from Core.SIDE.
--!return { cover, coverName, exposedTo, worstFrom, seenBy }.
--!example local t = Rules.coverVsThreats(4, 6, Core.SIDE.monk)
| arg | type | description |
|---|
| x | number | |
| y | number | |
| side | string | |
moveOf(u: any) → number
Movement: cost, reachability, path.
damageOf(u: any) → number
stepCost(u: any, a: any, b: any) → number
Cost of stepping from tile `a` onto tile `b`. Returns nil when impassable.
| arg | type | description |
|---|
| u | any | |
| a | any | |
| b | any | |
reachable(unitId: string) → void
--!desc Every tile a unit can reach with the action points it has left,
--! with the movement cost, the AP the move would spend, and the cover
--! the tile gives.
--!arg unitId A unit id.
--!return An array of { x, y, cost, ap, cover, dangerous } tables.
--!example for _, t in ipairs(Rules.reachable("anselm")) do ... end
| arg | type | description |
|---|
| unitId | string | |
pathTo(unitId: string, x: number, y: number) → any
--!desc The cheapest path a unit can walk to a tile, with the AP it costs
--! and the cover waiting at the end — everything the UI needs to preview
--! a move BEFORE the player commits to it.
--!arg unitId A unit id.
--!arg x Destination column.
--!arg y Destination row.
--!return A table { ok, reason, path, cost, ap, cover, coverName, elev,
--! dangerous }, where path is an array of { x, y } starting at the unit.
--!example local p = Rules.pathTo("anselm", 4, 6)
| arg | type | description |
|---|
| unitId | string | |
| x | number | |
| y | number | |
hitChance(unitId: string, targetId: string, opts: any?) → any
The hit chance, itemised.
--!desc An honest hit chance with every modifier named, exactly as the UI
--! must show it. Works for any attacker/target pair on either side.
--!arg unitId The attacker.
--!arg targetId The target.
--!arg opts Optional { reaction = true } for an overwatch shot.
--!return { ok, reason, chance, mods = { { id, label, value } }, cover,
--! coverName, flank, dist, crit, damage, blocked }
--!example local h = Rules.hitChance("vespera", "blight_2")
| arg | type | description |
|---|
| unitId | string | |
| targetId | string | |
| opts | any? | |
add(id: string, label: string, value: number) → void
| arg | type | description |
|---|
| id | string | |
| label | string | |
| value | number | |
pushLog(text: string) → void
Damage and death.
| arg | type | description |
|---|
| text | string | |
applyDamage(unitId: string, amount: number, opts: any?) → number
--!desc Take health off a unit, announce it, and kill it if it runs out.
--!arg unitId The unit taking the damage.
--!arg amount How much.
--!arg opts Optional { from = unitId, crit = bool, cause = string }.
--!return The damage actually dealt.
--!example Rules.applyDamage("anselm", 3, { cause = "thornbed" })
| arg | type | description |
|---|
| unitId | string | |
| amount | number | |
| opts | any? | |
heal(unitId: string, amount: number) → number
--!desc Put health back on a unit, up to its maximum.
--!arg unitId The unit.
--!arg amount How much.
--!return The healing actually applied.
--!example Rules.heal("cassian", 2)
| arg | type | description |
|---|
| unitId | string | |
| amount | number | |
rebuildOrder( ) → void
The initiative order.
--!desc Rebuild the live initiative queue from the units that are still
--! standing, front first, with the active unit at the front.
--!example Rules.rebuildOrder()
queue(count: number?) → void
--!desc The next several turns, for the portrait queue. Each entry names
--! the unit and the round its turn falls in.
--!arg count How many entries (default 8).
--!return An array of { unitId, side, kind, round, isActive }.
--!example local q = Rules.queue(6)
| arg | type | description |
|---|
| count | number? | |
activeCheck(unitId: string) → void
The can* predicates. Every one refuses in plain words.
| arg | type | description |
|---|
| unitId | string | |
canMove(unitId: string, x: number, y: number) → void
--!desc Whether a unit may walk to a tile, and why not when it may not.
--!arg unitId A unit id.
--!arg x Destination column.
--!arg y Destination row.
--!return Two values: true/false, and the plain-words reason when false.
--!example local ok, why = Rules.canMove("anselm", 4, 6)
| arg | type | description |
|---|
| unitId | string | |
| x | number | |
| y | number | |
canAttack(unitId: string, targetId: string) → void
--!desc Whether a unit may attack a target, and why not when it may not.
--!arg unitId The attacker.
--!arg targetId The target.
--!return Two values: true/false, and the plain-words reason when false.
--!example local ok, why = Rules.canAttack("vespera", "blight_1")
| arg | type | description |
|---|
| unitId | string | |
| targetId | string | |
canOverwatch(unitId: string) → void
--!desc Whether a unit may arm overwatch.
--!arg unitId A unit id.
--!return Two values: true/false, and the reason when false.
--!example local ok, why = Rules.canOverwatch("meridia")
| arg | type | description |
|---|
| unitId | string | |
canGarden(unitId: string, action: string, x: number, y: number) → void
--!desc Whether a unit may work a tile with sow / burn / flood.
--!arg unitId A unit id.
--!arg action "sow", "burn" or "flood".
--!arg x Target column.
--!arg y Target row.
--!return Two values: true/false, and the reason when false.
--!example local ok, why = Rules.canGarden("meridia", "sow", 4, 6)
| arg | type | description |
|---|
| unitId | string | |
| action | string | |
| x | number | |
| y | number | |
canUndo(unitId: string) → void
--!desc Whether the last move can be taken back. A committed attack can
--! never be undone; movement always can, until something is committed.
--!arg unitId A unit id.
--!return Two values: true/false, and the reason when false.
--!example local ok, why = Rules.canUndo("anselm")
| arg | type | description |
|---|
| unitId | string | |
inOverwatchCone(w: any, x: number, y: number) → boolean
Overwatch.
| arg | type | description |
|---|
| w | any | |
| x | number | |
| y | number | |
overwatchCone(unitId: string) → void
--!desc The tiles an armed overwatch is currently covering — for the UI to
--! draw the cone.
--!arg unitId A unit id.
--!return An array of { x, y }; empty when the unit is not watching.
--!example local cone = Rules.overwatchCone("meridia")
| arg | type | description |
|---|
| unitId | string | |
triggerOverwatch(mover: any, x: number, y: number) → boolean
Fire every armed hostile overwatch that covers (x, y). Returns true when the
mover was killed, so the caller stops walking it.
| arg | type | description |
|---|
| mover | any | |
| x | number | |
| y | number | |
move(unitId: string, x: number, y: number) → any
The actions.
--!desc Walk a unit to a tile along the cheapest path, spending the action
--! points the path costs and letting every armed overwatch that covers
--! the route take its shot.
--!arg unitId A unit id.
--!arg x Destination column.
--!arg y Destination row.
--!return { ok, reason, path, ap, interrupted, killed }
--!example Rules.move("anselm", 4, 6)
| arg | type | description |
|---|
| unitId | string | |
| x | number | |
| y | number | |
undoMove(unitId: string) → any
--!desc Take back the unit's last move. Refuses once anything has been
--! committed — an attack, an overwatch shot, a garden action.
--!arg unitId A unit id.
--!return { ok, reason, x, y }
--!example Rules.undoMove("anselm")
| arg | type | description |
|---|
| unitId | string | |
attack(unitId: string, targetId: string) → any
--!desc Commit an attack. This is the one thing that can never be undone.
--!arg unitId The attacker.
--!arg targetId The target.
--!return { ok, reason, hit, chance, roll, crit, damage, killed, mods }
--!example Rules.attack("vespera", "blight_1")
| arg | type | description |
|---|
| unitId | string | |
| targetId | string | |
overwatch(unitId: string, dx: number, dy: number) → any
--!desc Arm overwatch along a direction. The unit's turn ends; the shot
--! fires when an enemy walks into the cone during THEIR turn.
--!arg unitId A unit id.
--!arg dx Cone direction, x. Normalised for you.
--!arg dy Cone direction, y.
--!return { ok, reason, cone }
--!example Rules.overwatch("meridia", 0, 1)
| arg | type | description |
|---|
| unitId | string | |
| dx | number | |
| dy | number | |
garden(unitId: string, action: string, x: number, y: number) → any
--!desc Work a tile: sow growth on it, burn what is on it, or flood it.
--! The referee owns the legality and the action point; the garden slice
--! owns what actually happens when it has registered a handler.
--!arg unitId A unit id.
--!arg action "sow", "burn" or "flood".
--!arg x Target column.
--!arg y Target row.
--!return { ok, reason }
--!example Rules.garden("meridia", "sow", 4, 6)
| arg | type | description |
|---|
| unitId | string | |
| action | string | |
| x | number | |
| y | number | |
hold(unitId: string) → any
--!desc End the active unit's turn without spending anything. Always legal.
--!arg unitId A unit id.
--!return { ok, reason }
--!example Rules.hold("cassian")
| arg | type | description |
|---|
| unitId | string | |
endOfTurnTileEffects(u: any) → void
Turns, rounds, and the garden's own turn.
What the ground does to whoever is standing on it when their turn closes.
beginTurn(unitId: string) → void
| arg | type | description |
|---|
| unitId | string | |
endTurn( ) → any
--!desc Close the active unit's turn and hand initiative to the next one.
--! When the queue wraps, the round closes and the garden takes its turn.
--!return { ok, activeUnit, phase, round }
--!example Rules.endTurn()
closeRound( ) → void
--!desc Close the round: the garden takes its turn, then the Blight takes
--! its own, then the next round begins. Called for you by endTurn.
--!example Rules.closeRound()
phaseDone(name: string) → void
--!desc Close a deferred phase. A phase handler that returned "defer" calls
--! this when it has finished, and the round carries on from here.
--!arg name The phase that has finished.
--!example Rules.phaseDone("garden")
| arg | type | description |
|---|
| name | string | |
registerMission(def: any) → void
--!desc Register or replace a mission definition. The campaign slice owns
--! the real ones; this is how they arrive.
--!arg def A mission definition table with an `id`.
--!example Rules.registerMission({ id = "brass_nave", ... })
mission(id: string) → any
--!desc Look up a mission definition by id.
--!arg id A mission id.
--!return The definition, or nil.
--!example local m = Rules.mission("propagation_hall")
| arg | type | description |
|---|
| id | string | |
objectiveStatus( ) → any
--!desc How far along the mission's objective is, in words and in numbers.
--!return { kind, text, progress, goal, done }
--!example local o = Rules.objectiveStatus()
checkOutcome( ) → any
--!desc Decide whether the mission is over, and set the outcome if it is.
--!return nil, "won" or "lost".
--!example Rules.checkOutcome()
finish(outcome: string, reason: string) → void
| arg | type | description |
|---|
| outcome | string | |
| reason | string | |
makeMonk(rec: any, x: number, y: number, carried: any?) → any
Starting a mission.
| arg | type | description |
|---|
| rec | any | |
| x | number | |
| y | number | |
| carried | any? | |
makeBlight(form: any, n: number, x: number, y: number) → any
| arg | type | description |
|---|
| form | any | |
| n | number | |
| x | number | |
| y | number | |
startMission(missionOrId: any, carried: any?) → any
--!desc Lay out a mission on a fresh board: the squad, the Blight, the
--! growth already on the floor, the initiative order, and round one.
--!arg missionOrId A mission id, or a whole mission definition table.
--!arg carried Optional map of monk id -> carried wounds, for the campaign.
--!return { ok, reason, mission, activeUnit }
--!example Rules.startMission("propagation_hall")
| arg | type | description |
|---|
| missionOrId | any | |
| carried | any? | |
nearestMonk(u: any) → any
The Blight's own turn. The referee plays it, one action per call, so the
Director can pace it for a human to watch without ever putting a clock on
the player's own turn.
aiStep( ) → any
--!desc Play one action for the unit whose turn it is, when that unit is
--! the Blight's. Returns what it did, so a caller can pace the turn.
--!return { acted, action, done } — `done` when the turn is over.
--!example Rules.aiStep()
copyUnit(u: any) → any
Reading the board from outside.
snapshot(opts: any?) → any
--!desc The whole board as plain data, safe to hand to any other slice or
--! return from an `execute` call.
--!arg opts Optional { tiles = false } to leave the 80 tiles out.
--!return The snapshot table.
--!example local s = Rules.snapshot({ tiles = false })
| arg | type | description |
|---|
| opts | any? | |
tileInfo(x: number, y: number) → any
--!desc Everything the tile inspector needs to name a tile: its terrain,
--! its cover, its elevation, who is on it — and what it becomes next.
--!arg x Tile column.
--!arg y Tile row.
--!return A description table.
--!example local i = Rules.tileInfo(4, 6)
| arg | type | description |
|---|
| x | number | |
| y | number | |
targets(unitId: string) → void
--!desc Every legal target for a unit right now, each with its full itemised
--! hit chance — enough for the UI to draw a target list without a second
--! call per enemy.
--!arg unitId A unit id.
--!return An array of hit-chance tables, best chance first.
--!example for _, t in ipairs(Rules.targets("vespera")) do ... end
| arg | type | description |
|---|
| unitId | string | |
abilities(unitId: string) → void
--!desc The actions the active unit can take right now, each with whether
--! it is legal and the plain-words reason when it is not — the ability
--! bar reads this directly.
--!arg unitId A unit id.
--!return An array of { id, label, ap, ok, reason }.
--!example for _, a in ipairs(Rules.abilities("anselm")) do ... end
| arg | type | description |
|---|
| unitId | string | |
push(id: string, label: string, ap: number, ok: boolean, reason: string?) → void
| arg | type | description |
|---|
| id | string | |
| label | string | |
| ap | number | |
| ok | boolean | |
| reason | string? | |