8. AI opponents (optional, server/crates/ai)
Computer opponents are opt-in: a room has none unless the host adds them from the lobby
(addAi / removeAi, host-only, lobby phase only). addAi accepts an optional teamId for
scripted team setup; when omitted, the server seats the AI into the next deterministic slot for
the current preset. The lobby also has a host-only
setQuickstart toggle labeled “Debug mode”, which causes the next match to begin
with 99,999 steel and 99,999 oil for every player plus a prebuilt human-only army/base loadout.
They are capped with humans at
MAX_PLAYERS = 4 (the bundled maps have v2 spawn layouts for one through four active players).
AI players are seated after the humans in the lobby player list; their colors come
from the tail of PLAYER_PALETTE so they never collide with human colors. They persist across rematches and are cleared only when the room
empties of humans.
Where it runs. rts-ai owns one AiController per AI player, while Game remains AI-free.
The room task invokes controllers before game.tick(), gives each controller the same
fog-filtered snapshot_for(player) plus the static start_payload(), then enqueues emitted
ordinary SimCommands. Every AI action therefore goes through the identical validation / cost /
supply / placement path in services/commands.rs — the AI has no special authority over the
simulation and can’t cheat economy, placement, or fog rules. Outbound attacks target enemy
start tiles, which are public via the start payload; direct attacks only target currently
visible enemy units/buildings during local defense.
The worker direct-hit retreat reflex is the one extra live input: Game::worker_retreat_commands_for
projects recent own-worker damage metadata into ordinary Move commands, and the controller emits
them alongside profile decisions without reading private sim state.
rts-ai may import rts-sim public API, rts-rules, rts-protocol, and rts-contract. It must
not import the server shell, lobby internals, Axum/Tokio transport, or private sim modules through
path tricks. If AI needs more observations, add a public, fog-respecting Game/snapshot surface
instead of reaching into entity stores from the server layer.
Strategy. Each controller, on a staggered cadence
(DECISION_INTERVAL ticks), builds a constrained snapshot-backed AiObservation and delegates RTS
decisions to rts_ai::ai_core::decision::decide_profile. Live lobby AIs use the promoted
ai_1_1_tank_mg profile by default and keep that profile for the whole match. Hosts can select
ai_1_0_tech or ai_1_1_tank_mg per AI seat from the lobby before countdown/start; unsupported
profile ids are ignored or defaulted to the highest supported live AI version. Team relationships are observation-only safety
inputs: player summaries carry teamId, visible allied entities are classified separately from
visible_enemies, public base targeting ignores allied starts, and live decisions receive the
current living player set so attack waves keep choosing living enemies. AI teammates still do not
share economy, production, command authority, build orders, attack plans, or a team controller.
It does not micro, scout, or choose hidden enemy unit positions. A local per-think budget in the
shared action layer prevents it from over-committing resources/supply it does not have.
Shared AI core. rts_ai::ai_core has deterministic profile data (profiles.rs) and a generic
ranked decision loop (decision.rs) that emits ordinary SimCommands through shared action helpers.
The decision loop also emits manager traces: every think records typed strategic goals for economy,
supply, expansion, tech, production, local defense, frontal attack, and harassment, plus stable
blocker labels, high-level intent labels, command labels emitted through AiActionContext, and
budget/reservation deltas. Economy, expansion, and frontal-wave attack now have explicit plan
records. The economy plan owns worker targets, steel/oil assignment counts, occupied resource
nodes, and post-expansion local-assignment bounds. The expansion plan owns due/save decisions,
tech-blocking state, and blocked reasons such as defensive panic, missing prerequisite building,
missing defenders, pending City Centre, no candidate resources, or no valid site. The frontal-wave
plan owns ready combat groups, required-unit readiness, attack reissue cadence, staging, visible
combat target selection, and blockers such as waiting for units, waiting for a required Tank,
waiting for Methamphetamines, and cadence. Final command emission still goes through
AiActionContext and ai_core::actions.
The economy plan is backed by an AI-owned resource availability model derived only from the
fog-filtered observation, public start-payload resource positions, completed own City Centres,
visible resource deltas, current worker latches, and AI-owned reservations. The model keeps
known resources separate from resources that are mineable now: a steel or oil node is assignable
only when it has remaining resources, is in range of a completed own City Centre, is not occupied by
a latched worker, and is not already reserved by the current think. Known but non-mineable nodes
remain visible to expansion planning as future candidates, but economy worker assignment suppresses
oil demand when there is no free mineable oil and passes only free mineable node ids to
assign_workers_to_resource. The action layer also requires callers to provide that assignable set,
so an upstream economy mistake cannot knowingly emit a Gather command to non-mineable oil while
free mineable steel exists. Self-play regression coverage preserves the pre-expansion case where
oil is known but outside completed-City-Centre mining range, and the post-expansion case where oil
assignment begins after the expansion City Centre completes.
The AI 1.0 profile is ai_1_0_tech; it parameterizes worker targets,
supply buffers, building/tech goals, production priorities, resource timing, expansion timing, and
attack thresholds without providing its own think() function. It opens with
four-Rifleman frontal waves, expands off a completed Training Centre, builds Research
Complex and Factory without adding Machine Gunners, Anti-Tank Guns, Artillery, or Command Cars,
produces Scout Cars while Tank research or Methamphetamines is blocked or pending, then prioritizes
Tanks once both Tank research and Methamphetamines complete. Scout Cars are not reserved for
harassment, flank routes, or threat evasion; if they are present in the ready combat group, they use
the same frontal-wave attack-move behavior as Tanks and Riflemen. It does not focus workers, ignore
hidden buildings, regroup, or use Scout Car smoke in AI 1.0. Tank frontal waves require a Tank in the
ready group and Methamphetamines before launch; while waiting, ready Tank groups stage toward the
enemy instead of dribbling into attack orders. Methamphetamines is enforced before first Tank
production, not only before Tank attack launch, so Tank production and Tank-wave readiness cannot
race ahead of the upgrade.
The profile includes a defensive panic mode. Visible enemy units near the AI’s base, home resource
line, or workers temporarily suspend expansion, worker training, and non-defensive tech spending
only when their steel+oil value is at least 75% of the AI’s own local unit value. While panicking,
the AI classifies the visible local threat by weapon DPS: tank-dominated pressure (75%+ of visible
local DPS) prioritizes Anti-Tank Guns, infantry-dominated pressure prioritizes Machine Gunners, mixed
pressure asks for a support mix, and no-DPS pressure falls back to Riflemen. Support panic only uses
already-completed support tech: Machine Gunners need a Training Centre and Anti-Tank Guns need a
Gun Works plus Anti-Tank Gun Crews research. It may pull workers onto oil for those support counters; if
the relevant support tech is absent, production falls back to Riflemen and panic mode does not
create tech buildings.
If the pressure persists through the panic window, the AI asks for an additional Barracks before
resuming its normal profile once the threat has cleared.
Developer self-play tooling also registers ai_1_1_tank_mg for direct comparison through
ai-matchup and related profile-backed scripts. AI 1.1 is a close AI 1.0 fork that keeps the same
expansion timing, Tank tech path, Methamphetamines-before-Tanks gate, and Tank-required
frontal-wave posture, but removes Scout Car production and harassment, caps ordinary Barracks growth
at one, trains a bounded defensive Machine Gunner group, pushes toward full two-base steel
saturation, and can add a second Factory once Tank production is active. Its Tank-era production and
frontal-wave composition are Tank-only, so Riflemen remain an opening/defensive Barracks output
rather than a continuing mid-game spend. It reserves up to four ready Machine Gunners before
frontal-wave readiness is calculated, so those MGs do not satisfy Tank wave sizes.
When there is no local base threat, the reserved MGs receive deterministic individual attack-move
stage orders roughly 20 tiles past the main steel line toward the nearest living public enemy start,
using public resource geometry rather than hidden enemy positions. This pushes the defensive group
out far enough to contest approaches before attackers reach the expansion. Visible threats near the
base, home resource line, or workers still take priority over passive perimeter staging.
The aliases ai_1_1 and ai11 resolve to ai_1_1_tank_mg; ai_1_0, ai_1_0_tech, and ai1
resolve to ai_1_0_tech; ai and default resolve to the live default.
The live lobby AI uses this shared core through AiController, which only owns live identity,
profile id, cadence, and persistent decision memory. Unknown live profile ids resolve to the
highest supported live AI version, currently ai_1_1_tank_mg. The ordinary lobby exposes only
AI 1.0 and AI 1.1; older experimental profile ids are no longer listed or accepted by developer
tooling. AI 1.1 is the live lobby default.
Self-play scorecards. The ai-matchup and ai-balance-matrix developer tools emit
profile-agnostic baseline scorecards from public self-play commands and snapshots. Per-player
results include army value, building value, final worker count, final unit counts, command count,
attack command count, damage events dealt, deduplicated deaths, first attack command, first
Rifleman attack command, first Scout Car completion, first legacy Scout Car harassment-style Move
command, first expansion City Centre planned/completed, and first Tank completion. Match-level results include
winner or tick-cap status, first damage, attack events, death events, replay verification status,
and optional replay artifact path. Compact baseline scenario metadata for opening pressure,
mid-game expansion, tank tech, and blocked-goal pressure lives in
server/crates/ai/src/selfplay/scenarios.rs so later AI changes can compare the same authored
fixtures without rewriting the harness.
Profile matchup JSON also includes a bounded aiTraceTail of compact trace entries for recent
profile-backed thinks. The tail is diagnostic output only; deterministic replay artifacts continue
to use the command log as the source of player intent.
Spectators never count toward win/elimination and receive a neutral final scoreboard result.
Win/elimination. AI players count as match players: a 1-human + N-AI match is a real match
(it resolves to a winner), while a lone human with no AI remains a never-ending sandbox. They have
one special elimination rule: an AI with no units left is defeated even if it still owns buildings,
because it has no player input path back into the game. The lobby’s match_player_count is humans
+ AIs.