Last modified: June 06, 2026

This article is written in: 🇺🇸

AI Architecture

This document describes the current enemy AI in Standard of Iron: what it already does well, how it is configured, where mission JSON plugs into it, and which expansions still remain before it reaches a fuller professional RTS standard.

Current state

The AI is no longer a passive "units occasionally wander" layer. It now has a cheap centralized planner that can:

It is still intentionally lightweight: one AI brain per player, throttled updates, immutable snapshots, small force-role heuristics, and behavior modules instead of expensive per-unit thinking.

Design goals

The system optimizes for four things:

  1. Cheap execution: one planner per AI player, not one behavior tree per unit.
  2. Visible activity: the AI should keep producing, gathering, defending, harassing, attacking, and expanding instead of stalling in idle loops.
  3. Authorable variation: missions can shape AI with JSON through strategy, personality, and difficulty.
  4. Extensibility: reserve, harass, and outpost logic are foundations for future siege groups, multi-base roles, difficulty ladders, and personality packs.

High-level update loop

The AI runs in a snapshot -> reason -> execute -> apply pipeline.

world state
   |
   v
AISnapshotBuilder
   |
   v
AISnapshot (immutable, thread-safe)
   |
   v
AIReasoner
  - updates persistent context
  - refreshes force roles
  - advances state machine
   |
   v
AIExecutor
  - runs eligible behaviors by priority
  - emits AICommand list
   |
   v
AICommandFilter / AICommandApplier
   |
   v
game world

The expensive part is the thinking, so it is throttled and handed to a worker thread. The snapshot is immutable specifically so AI code can reason off-thread without touching live world state.

Main files

Most AI code lives in game/systems/ai_system/.

File Responsibility
ai_types.h Snapshot, context, strategy config, commands
ai_snapshot_builder.cpp Reads visible world state into AISnapshot
ai_reasoner.cpp Updates persistent AI context and state
ai_executor.cpp Runs behaviors and collects commands
ai_worker.cpp Background worker wrapper
ai_command_filter.cpp Prevents duplicate/spammy commands
ai_command_applier.cpp Applies AI commands back to the game
ai_strategy.cpp Strategy presets, personality shaping, difficulty tuning
ai_utils.h Assignment cleanup and force-role helper functions
behaviors/*.cpp Tactical and macro behavior implementations
game/systems/ai_system.cpp Owns AI instances and update cadence

What the AI knows

The AI uses two data models:

Snapshot

AISnapshot is intentionally compact:

The important change is strategic_objectives: the AI keeps enemy structures and commanders as long-range objectives even when they are outside tactical vision. That prevents the classic RTS failure mode where the army stops doing anything just because no enemy is currently visible.

Context

AIContext is where most of the AI’s current strength lives. In addition to basic unit counts and state, it tracks:

This is still heuristic AI, not a heavyweight planner, but the persistent context makes it feel much more intentional.

State machine

The AI operates in these strategic states:

The transitions are driven by cheap battlefield signals:

The important modern behavior is that Defending is no longer sticky forever. It decays from local threat memory instead of global enemy visibility, so the AI can leave defense mode once the base area has actually calmed down.

Behaviors and priorities

Behaviors are modular and ordered by priority.

Behavior Priority Concurrent? Current job
RetreatBehavior Critical No Pull damaged armies back to safety
DefendBehavior Critical No React to local threats, prefer reserve first
ProductionBehavior High Yes Keep barracks producing from style-aware targets
BuilderBehavior High Yes Build homes, barracks, towers, catapults, and outposts
CommanderBehavior High Yes Move commanders and trigger rally ability
ExpandBehavior High No Capture neutral barracks or escort the main force to an outpost site
AttackBehavior Normal No Main-army pushes, target chasing, blind marches to strategic objectives
HarassBehavior Low Yes Raider detachment against isolated or strategic targets
GatherBehavior Low No Assemble the main army around the rally area

Three concurrency rules matter:

  1. Production, builder, and commander logic keep running during attacks and defenses.
  2. HarassBehavior can run alongside the main strategic behavior.
  3. Exclusive force behaviors still rely on unit claiming so they do not fight each other for the same troops.

Force organization

This is the current force model.

Main army

The main army is the attack-capable pool after excluding:

GatherBehavior organizes this force near the rally point, and AttackBehavior uses it for proactive attacks and objective marches.

Reserve force

The reserve is a stable home-defense group stored in reserve_unit_ids.

Current rules:

This is the first real "do not commit everything" rule in the AI.

Harass force

The harass force is a separate detachment stored in harass_unit_ids.

Current rules:

This gives the AI a second offensive layer without making the main planner much heavier.

Commanders

Commanders are handled separately:

Macro and building logic

The AI now uses shared macro targets instead of scattered hardcoded thresholds.

AIStrategyConfig feeds these targets into context:

BuilderBehavior then builds toward the largest deficit while preserving important early priorities like homes and the first barracks.

ProductionBehavior also reads from the same config, so unit production and structure growth are at least pulling in the same strategic direction.

Expansion logic

The AI now has a first outpost planner.

What it currently does

What it does not do yet

So this is a real step beyond passive single-base AI, but it is not yet a full multi-base RTS economy.

Style, personality, and difficulty

The system now deliberately separates what the AI wants from how efficiently it executes.

Strategy preset

The strategy preset is the coarse style template:

These presets set the default shape of the AI:

Personality inputs

Mission JSON can then nudge a preset using three normalized floats:

These values tune things like:

Difficulty tuning

Difficulty now affects execution efficiency, not strategic identity.

Supported values:

Difficulty currently changes:

That means a defensive AI on hard is still defensive; it just reacts and scales more efficiently.

Mission JSON usage

Mission files are the current authoring surface for AI setup. The loader reads strategy, personality, difficulty, team_id, starting spawns, and optional mission waves from ai_setups.

Example: balanced frontline opponent

{
  "id": "roman_legion_alpha",
  "nation": "roman_republic",
  "faction": "roman",
  "color": "red",
  "team_id": 1,
  "difficulty": "hard",
  "strategy": "balanced",
  "personality": {
    "aggression": 0.62,
    "defense": 0.55,
    "harassment": 0.30
  },
  "starting_buildings": [
    {
      "type": "barracks",
      "position": { "x": 132, "z": 84 },
      "max_population": 180
    }
  ],
  "starting_units": [
    {
      "type": "spearman",
      "count": 8,
      "position": { "x": 128, "z": 86 }
    },
    {
      "type": "builder",
      "count": 2,
      "position": { "x": 134, "z": 82 }
    }
  ],
  "commander_troop": "roman_field_commander"
}

Resulting feel:

Example: forward pressure harasser

{
  "id": "numidian_raiders",
  "nation": "carthage",
  "faction": "carthaginian",
  "color": "yellow",
  "difficulty": "medium",
  "strategy": "harasser",
  "personality": {
    "aggression": 0.74,
    "defense": 0.28,
    "harassment": 0.84
  },
  "starting_units": [
    {
      "type": "horse_swordsman",
      "count": 5,
      "position": { "x": 18, "z": 80 }
    },
    {
      "type": "builder",
      "count": 1,
      "position": { "x": 16, "z": 82 }
    }
  ],
  "starting_buildings": [
    {
      "type": "barracks",
      "position": { "x": 14, "z": 80 },
      "max_population": 120
    }
  ]
}

Resulting feel:

Notes for authors

Debugging and validation

The main regression coverage lives in tests/systems/ai_system_test.cpp.

Current AI test coverage includes:

For repo validation, the reliable test binary is:

./build/bin/standard_of_iron_tests --gtest_color=no --gtest_brief=1

What is already strong

Relative to the original passive AI, the current system is much better at:

Biggest remaining gaps

The AI is improved, but it is not yet "finished RTS AI." The most important remaining gaps are:

  1. True multi-base planning
  2. multiple active bases
  3. per-base rally points
  4. per-base production roles
  5. better outpost abandonment / retarget logic

  6. Richer force planner

  7. siege groups
  8. flankers
  9. synchronized attack waves
  10. regroup / reform logic after failed pushes

  11. Data-driven profiles

  12. move strategy presets out of code into assets/data
  13. let designers tune AI personalities without recompiling

  14. Stronger strategic economy awareness

  15. more explicit resource pressure
  16. better builder safety / routing
  17. broader structure placement logic

  18. Team and campaign coordination

  19. allied AI timing
  20. shared fronts
  21. mission-aware operational goals

If you want the next biggest gains per engineering effort, the recommended order is:

  1. Data-driven AI profiles so design can iterate quickly
  2. True multi-base roles built on the current outpost foundation
  3. Richer force planner for siege / flank / regroup behavior
  4. Coordinated allied AI for campaign-scale scenarios

That sequence builds on the current architecture instead of fighting it.