Ladder
in package
The deterministic decision ladder — "what would a sensible agent do here?" computed straight from an observation, no LLM. Two entry points:
- suggestion() — the full ladder (defend → finish parts → arm → stance nudge → one speculative combine → build → stockpile → sell a glut → reposition). PromptBuilder shows its pick to the model as the "SUGGESTED next action" anchor, and AutoPlayer falls back to it whenever the model's pick is refused by a guardrail.
- defensiveAction() — just rung 0 (combat), which AutoPlayer applies as a hard override before it ever consults the model.
Split out of AgentBrain (which is now only the LLM round-trip) so the
fallback logic has one home and its callers stop reaching into that class's
statics. The rungs are diagrammed in docs/PLAYBOOK.md; keep it in sync.
Tags
Table of Contents
Constants
- CREDIT_FLOOR : mixed = 300
- Economy targets for the ladder.
- HOARD_CAP : mixed = 80
- RESEARCH_SURPLUS : mixed = 60
- Research only fires when at least two raws sit this deep — a genuine surplus, not the stockpile.
- RESOURCE_TARGET : mixed = 30
Methods
-
defensiveAction()
: array{verb: string, args: array
, reason: string}|null - The defensive move when the agent is in (or just came out of) a fight: heal if badly hurt and able, shoot back if armed and the attacker is in range, otherwise break contact. Returns `null` when there is no threat.
-
suggestion()
: array{verb: string, args: array
, why: string}|null - A deterministic "what would the ladder do" pick, surfaced to anchor a weak model. Mirrors {@see Playbook}'s priorities: finish parts → gamble one novel combine → build for reliable points → sell a glut → harvest only when short → reposition. Returns `null` when nothing is obviously right (the model is then on its own).
-
armMove()
: array{verb: string, args: array
, why: string}|null - Rung 1c: buy a minimum survival kit when out of combat and unequipped — a medicine first, then a weapon, then ammo for it. Returns `null` once the kit is covered or the credits are too thin.
- bestMedicine() : string|null
- Best self-heal medicine on hand, strongest first, or `null`.
-
combatMove()
: array{verb: string, args: array
, why: string}|null - Rung 0: the defensive move when the agent is in (or just came out of) a fight — heal if badly hurt and able, shoot back if armed and the attacker is in range, otherwise break contact. Returns `null` when there is no threat.
-
stanceMove()
: array{verb: string, args: array
, why: string}|null - Rung 1d: a deterministic nudge toward the active stance, ahead of the generic ladder. Returns `null` for `homestead` (the generic ladder already plays it) and whenever the stance has nothing pressing to add this turn.
Constants
CREDIT_FLOOR
Economy targets for the ladder.
public
mixed
CREDIT_FLOOR
= 300
CREDIT_FLOOR— keep at least this many credits; onlysellto climb back to it, or to fund a project (buy-to-build / invest).RESOURCE_TARGET— stockpile each raw up to here (harvest / walk toward deposits until reached); neversellbelow it outside a credit emergency.HOARD_CAP— the one non-creditselltrigger: shed the excess above this so a "never sell" rule cannot deadlock into mining forever.
HOARD_CAP
public
mixed
HOARD_CAP
= 80
RESEARCH_SURPLUS
Research only fires when at least two raws sit this deep — a genuine surplus, not the stockpile.
public
mixed
RESEARCH_SURPLUS
= 60
RESOURCE_TARGET
public
mixed
RESOURCE_TARGET
= 30
Methods
defensiveAction()
The defensive move when the agent is in (or just came out of) a fight: heal if badly hurt and able, shoot back if armed and the attacker is in range, otherwise break contact. Returns `null` when there is no threat.
public
static defensiveAction(array<string, mixed> $raw) : array{verb: string, args: array, reason: string}|null
Public so AutoPlayer can apply it as a hard override — combat defence never waits on the LLM.
Parameters
- $raw : array<string, mixed>
-
The normalised observation.
Return values
array{verb: string, args: arraysuggestion()
A deterministic "what would the ladder do" pick, surfaced to anchor a weak model. Mirrors {@see Playbook}'s priorities: finish parts → gamble one novel combine → build for reliable points → sell a glut → harvest only when short → reposition. Returns `null` when nothing is obviously right (the model is then on its own).
public
static suggestion(array<string, mixed> $raw, array<string, bool> $tried[, array<string, bool> $worldKnown = [] ][, bool $allowSpeculation = true ][, string $stance = 'homestead' ]) : array{verb: string, args: array, why: string}|null
Also reused by AutoPlayer as the infrastructure fallback when a
research combine is refused — pass the whole tried+known combine space
as both $tried and $worldKnown and the ladder skips its
speculative-combine rung and drops straight to build / wealth / harvest.
Parameters
- $raw : array<string, mixed>
-
The normalised observation.
- $tried : array<string, bool>
-
a+b => truefor combine sets submitted THIS session. - $worldKnown : array<string, bool> = []
-
a+b => truefor sets the whole world has already invented. - $allowSpeculation : bool = true
-
When false, the speculative-combine rung is skipped entirely — used by the infrastructure fallback, where research is finished.
- $stance : string = 'homestead'
Return values
array{verb: string, args: arrayarmMove()
Rung 1c: buy a minimum survival kit when out of combat and unequipped — a medicine first, then a weapon, then ammo for it. Returns `null` once the kit is covered or the credits are too thin.
private
static armMove(array<string, mixed> $inv, int $credits) : array{verb: string, args: array, why: string}|null
Parameters
- $inv : array<string, mixed>
- $credits : int
Return values
array{verb: string, args: arraybestMedicine()
Best self-heal medicine on hand, strongest first, or `null`.
private
static bestMedicine(array<string|int, mixed> $inv) : string|null
Parameters
- $inv : array<string|int, mixed>
Return values
string|nullcombatMove()
Rung 0: the defensive move when the agent is in (or just came out of) a fight — heal if badly hurt and able, shoot back if armed and the attacker is in range, otherwise break contact. Returns `null` when there is no threat.
private
static combatMove(array<string, mixed> $raw, array<string, mixed> $inv, float $hp, float $hpMax, int $x, int $y, int $tick) : array{verb: string, args: array, why: string}|null
Parameters
- $raw : array<string, mixed>
- $inv : array<string, mixed>
- $hp : float
- $hpMax : float
- $x : int
- $y : int
- $tick : int
Return values
array{verb: string, args: arraystanceMove()
Rung 1d: a deterministic nudge toward the active stance, ahead of the generic ladder. Returns `null` for `homestead` (the generic ladder already plays it) and whenever the stance has nothing pressing to add this turn.
private
static stanceMove(string $stance, array<string, mixed> $raw, array<string, mixed> $inv, array<string, int> $raws, int $credits, int $x, int $y) : array{verb: string, args: array, why: string}|null
Parameters
- $stance : string
- $raw : array<string, mixed>
- $inv : array<string, mixed>
- $raws : array<string, int>
- $credits : int
- $x : int
- $y : int