STRATEGOS

A tactical command simulation built on topographic maps, NATO APP-6D symbology, and multi-echelon command — from a fireteam to a theatre command.

What concepts we follow — the recurring design decisions behind the code, not a feature list. Sourced from this repository's own internal documentation (CLAUDE.md, docs/command-architecture.md).

Echelon is the difficulty curve

The scale you command determines how much of the command problem exists at all

Commanding a fireteam has no C2 problem — you are the unit, you see what it sees, and orders are instant. By battalion, subordinates are out of sight and reports mediate what you know. At corps and above, you work from reports already stale and issue intent rather than instructions.

CommandingWhat command feels like
Fireteam / squadNo C2 problem — you are the unit. You see what it sees; orders are instant.
Platoon / companySubordinates exist but are close. Orders are near-immediate.
BattalionSubordinates out of sight. Reports mediate what you know. Delay begins to bite.
Brigade / divisionYou command through subordinate headquarters. You cannot see the front.
Corps / armyYou work from reports already stale. You issue intent, not instructions.
TheaterCoalition. Some formations you cannot order at all, only ask.

Communications degradation, order propagation delay and intelligence fusion aren't features bolted onto a finished game — they're what makes each echelon feel different rather than merely bigger. Introduced all at once at squad level they would be noise; unlocked as the player climbs, they're the learning curve. The consequence for the code: these effects are parameterised by echelon, not toggled — order delay is a function of echelon, distance and terrain that returns approximately zero at squad level and grows from there, one code path from tutorial to theatre rather than a mode switch.

Factory builds the base; decorators append layers

The symbol system

NatoSymbolComposer.Compose is the entry point and fixes the order — later stages read what earlier ones wrote:

SIDCParser → SIDCCode
    └─ SymbolFactory.CreateBase      frame + fill
       └─ IconDecorator              entity icon + entity-type variant mark
          └─ SectorModifierDecorator sector 1 (upper) / sector 2 (lower)
             └─ AmplifierDecorator   echelon, HQ staff, TF bracket, feint
                └─ ConditionDecorator    condition bar + combat-power bar
                   └─ TextAmplifierDecorator  fields T, M, F
                      → NatoSymbolBaker.Bake → one Sprite

INatoSymbol is data only — an ordered list of draw layers plus text amplifiers. Nothing rasterises until Bake runs, once, at the end.

The map system mirrors it

Same shape: an ordered pipeline over a data-only container, and nothing rasterises until the renderer runs. MapGenerator.Generate hard-codes its stage order for the same reason Compose does:

MapGenerationSettings → ReliefParameters
    └─ TectonicStage      base landform, sea level
       └─ (authored relief hook)
          └─ ErosionStage      weathers the surface
             └─ HydrologyStage  fill, D8 routing, lakes, rivers, moisture
                └─ LandcoverStage  classification from slope + moisture
                   └─ SettlementStage  towns, stamped into landcover
                      └─ NetworkStage  roads between them, bridges and fords
                         └─ (authored features hook)

MapData is data only. MapRasterizer.RenderPixels is the only thing that draws, and its layer order is fixed too — ground, contours, areas, lines, point marks, labels, grid. Labels come after every mark because placement can only avoid collisions it can see; the grid comes last because a grid line that gives way to a road has stopped being a coordinate reference.

The two systems share one primitive library. Symbols bake into a square buffer, maps into a rectangular one, so every drawing primitive has a (w, h) overload with the square one forwarding to it — one place to add a new shape, not two.

Orders and reports are messages, not calls

Why

A direct call from the UI into a unit is the obvious implementation and the wrong one: drawing in-progress orders, replaying a saved battle, shipping orders over a network for multiplayer, and an AI issuing orders through the same path a player does all need to observe or inject orders. Each is painful to retrofit onto direct calls and nearly free if an order is a message on a topic. Two topics, matching the direction of real command flow:

                    ┌─────────────────────┐
   player / AI ────►│   COMMAND topic     │────► units (filter by addressee)
                    │   (orders down)     │────► UI            (read-only)
                    └─────────────────────┘────► recorder      (read-only)

                    ┌─────────────────────┐
   units ──────────►│   SITUATION topic   │────► UI            (read-only)
                    │   (reports up)      │────► other units   (reaction)
                    └─────────────────────┘────► intel system  (planned)

Four delivery rules, kept because determinism depends on them

  • Publish to the next step. A message published during step N is delivered at step N+1, never within the step that produced it — otherwise a report can trigger a command that triggers another report, unbounded, inside one step. The same mechanism is the degenerate case of order propagation delay: a uniform one-step delay today, a function of echelon/distance/terrain later, one mechanism throughout.
  • Dispatch synchronously, in a defined order. Not async, not thread-pooled. Determinism requires a single stable order, every time.
  • Only the owner mutates. Any subscriber may observe; only a unit may modify its own queue, and only the executor may change world state. Otherwise dispatch order becomes semantics, and the resulting bugs reproduce at only one interleaving.
  • Subscribe for events, read for state. If a subscriber has to remember what it heard to stay correct, it should be reading state instead — the live plan in PLAY draws by walking each unit's actual queue every frame, not by reconstructing a shadow copy from messages it happened to see.

The payoff: the whole simulation is deterministic and replayable from the command log alone. Two different machines, or the same machine twice, produce the identical run from the identical order stream.

Generated at runtime, verified as a picture

Nothing visual is an imported file

Every sprite, texture and glyph — symbols, map sheets, UI chrome, the aged-paper stock the field manual is drawn on — is generated procedurally at runtime, not authored in an image editor and imported. It's a deliberate property, not an accident of what got built first: it means any symbol/echelon/condition combination or any map at any seed exists without an artist drawing it, and it's why introducing the project's first real imported binary asset (audio) gets treated as a threshold crossed deliberately, not a formality.

A generator's output is a picture, so read the picture

This project verifies changes by building the player, capturing a screenshot, and reading it — not by reading the code and assuming it's correct. Several real bugs (a symbol rendering at a barely-visible alpha, an objective one cell inside a lake) were invisible to code review and obvious in one frame. The same discipline applies to generators directly: contact sheets bake every combination of a system — every symbol permutation, every relief profile against every render mode — to one image, checked before the numbers next to it, because a generator whose output no one looked at is a generator no one has actually verified.

This page summarises; the source documents are the reference — CLAUDE.md for the map/symbol pipelines and the project's verification discipline, docs/command-architecture.md for the full messaging design.