Military
The military simulation extension, canwu-military, keeps forces, operations, combat, and occupation as plugin-owned domain records and changes them only through validated MilitaryCommand input. Two companion crates add synthetic rulesets and a runnable composition with the reference world. Read this page when you decide whether to use the extension as it ships, wrap it in your own integration, or replace it.
In brief: marches arrive one minute after the order, combat uses fixed built-in formulas, supply is one number per force that only goes down, and occupation stages are labels. A successful special operation currently stops the run (see Known issue).
Crates and ownership
Section titled “Crates and ownership”| Crate | What it owns | Published |
|---|---|---|
canwu-military |
MilitaryPlugin, eight record kinds under the canwu.military namespace, the MilitaryCommand domain command, the provider acknowledgement ingress, and the military_report knowledge schema |
Published on crates.io |
canwu-military-reference-content |
Two synthetic MilitaryRulesetV1 values, riverine_preindustrial() and industrial_front(), both marked synthetic_reference |
Published on crates.io |
canwu-military-reference |
demo_military_scenario(), which adds a military catalog to the reference world scenario, and the military_starter example |
Repository only |
View diagram source
flowchart TB
Host["Host application<br/>scenario, clock, commands,<br/>provider outcomes"]
Ref["canwu-military-reference<br/>demo scenario, starter"]
Content["canwu-military-reference-content<br/>synthetic rulesets"]
World["canwu-reference-world"]
Military["canwu-military<br/>records, commands, systems"]
Supply["canwu-force-supply-reference<br/>separate supply consumer"]
Resource["canwu-resource"]
Api["canwu-api"]
Host --> Ref
Host -- "MilitaryCommand,<br/>ProviderOutcome" --> Military
Ref --> Content
Ref --> World
Ref --> Military
Content --> Military
Military --> Api
World --> Api
Supply --> Resource
Resource --> Api
Arrows point from a crate to what it depends on. canwu-military builds on canwu-api alone. The force-supply reference consumer sits beside it on canwu-resource; neither crate depends on the other.
The plugin that registers a record schema owns that state, so only canwu-military writes canwu.military records. Everything else stays with its owner:
| State | Owner | What canwu-military does with it |
|---|---|---|
| Time, canonical ingress, commit and rollback, random draws, snapshots, replay | Simulation core | Uses them through canwu-api |
| Actor knowledge ledgers | Simulation core | Publishes military_report records to force commanders |
| Routes, travel time, custody, transport capacity | canwu-routing, canwu-transport, canwu-movement |
None. A march stores route_digest, a hash of force and destination, and arrives one minute after the order. |
| Resource stocks, reservation, consumption | canwu-resource |
None. A force’s supply_per_mille is a local number. |
| Population and recruits | canwu-society or your application |
None. Recruit carries an optional society_operation string only as command input. |
| Administrative, legal, and fiscal effects of an occupation | The provider plugin you name | Queues a pending effect and waits for a ProviderOutcome |
Supply is one number on each force, supply_per_mille, which starts at 1,000. An arriving march or special operation subtracts 100, and a force below 100 fails the operation and routs. No military command raises the value. A game that needs resource-backed supply models it with canwu-resource, as the force-supply consumer in the production economy case does, and extends or replaces canwu-military to carry the result into force records.
Core model
Section titled “Core model”Each payload carries a MilitaryRecordMeta with schema version, revision, semantic digest, and establishment time. Typed helpers such as force_reference and ledger_reference build the record references.
| Type | Record name | What it represents |
|---|---|---|
MilitaryCatalog |
catalog (ID root) |
Scenario content: one MilitaryRulesetV1, MilitaryNodeProfile entries, and CommanderProfile entries keyed by person |
ForceState |
force |
One force: owner entity, location node, commander, subunits (SubunitState), authorized and actual strength, per-mille training, equipment, fatigue, supply, morale, discipline, cohesion, and loyalty, loss counters, active operation, prepared ambush, and ForceStatus |
OperationState |
operation |
One march, special, or strategic operation: its single force, an optional opposing force, OperationPhase, start and destination nodes, and due time |
CombatState |
combat |
One battle opened on contact: attacker, defender, CombatStage, round, preparation, casualties, CombatResult, and a RandomEnvelope copy of each round’s draw |
OccupationState |
occupation |
Control of one node: occupying force, garrison, per-mille security, administrative reach, legitimacy, collaboration, resistance, and extraction burden, IntegrationStage, and policy revision |
MilitaryLedger |
ledger (ID root) |
One MilitaryOutcome per operation key for idempotency, plus PendingMilitaryEffect entries that wait for other domains |
ProviderOutcome |
provider_outcome |
A retained copy of one acknowledged result from another domain, with its ProviderDisposition |
MilitaryKnowledge |
knowledge |
A registered schema for military estimates (KnowledgeFact); no current system reads or writes it |
Every MilitaryCommand variant carries a MilitaryOperationKey. Admission checks the issuer and expected_force_revision only when the command names a force that already exists; the issuer must be an actor who commands it.
| Variant | Also checked at admission | What the phase-7 system writes |
|---|---|---|
CreateForce |
Branch is in the ruleset | A force with one subunit whose ID ends in :initial, status Forming, supply 1,000, morale 500 |
AssignCommander |
Issuer, revision | Replaces the commander |
Recruit |
Issuer, revision, branch | A new subunit, capped at the remaining authorized strength |
TrainAndEquip |
Issuer, revision | Adds to force training and equipment, capped at 1,000 |
PlanOperation |
Issuer, tactic | A strategic operation in Planned; nothing advances it later |
OrderMarch |
Issuer, revision, tactic | Force Moving, a march operation, and a tick one minute later |
PrepareAmbush |
Issuer, revision, tactic | A prepared_ambush at a node for seven days |
Recon |
Issuer, revision | One draw and the event canwu.military.transition_applied.v1 |
ExecuteSpecialOperation |
Issuer | Force Moving, a special operation, and a tick one day later |
EstablishOccupation |
Issuer, revision | An occupation if the force stands at the node and is not Routing, plus a daily tick |
SetOccupationPolicy |
Values up to 1,000 | Security, collaboration, and extraction burden, if policy_revision matches |
MilitaryAdministrationAction |
Nothing | A PendingMilitaryEffect in the ledger |
AdvanceTick |
Nothing | Advances one operation or occupation; the plugin schedules it for itself |
The branch and tactic checks run only when the scenario installs a catalog.
How it runs
Section titled “How it runs”View diagram source
flowchart TB
Cmd["military_command()<br/>Command::Plugin"] --> Queue["enqueue_command<br/>canonical ingress"]
Queue --> Admit["admit_command<br/>digest, issuer, revision,<br/>branch, tactic"]
Admit -- "rejected" --> Rejected["Rejected command,<br/>nothing queued"]
Admit -- "accepted" --> Packet["military_command_v1<br/>zero delay"]
Ack["enqueue_provider_outcome<br/>military_provider_ack_v1"] --> Apply
Packet --> Apply["Phase 7<br/>apply-military-ingress-v1"]
Apply -- "SchedulePluginIngress" --> Tick["AdvanceTick packet<br/>1 minute or 1 day later"]
Tick --> Apply
Apply --> Commit["Phase 9<br/>atomic commit"]
Commit --> Report["Phase 13<br/>materialize-military-reports-v1"]
military_command(command)wraps the command in aMilitaryCommandEnvelopewith aninput_digestof the command and returns aCommand::Plugin. The host enqueues it withenqueue_commandand advances the clock, for example withadvance_canonical. The directsubmitpath is rejected withMixedCommandIngress.- When a boundary admits the command, the handler
admit_commandrecomputes the digest and runs the checks in the table above. On success it queues amilitary_command_v1packet (ingress classDecision) with zero delay, which the next boundary admits. - In phase 7 (
DomainDeltaProposal), the event-driven systemapply-military-ingress-v1reads each admitted military packet. - For a command other than
AdvanceTick, it first looks up the operation key in the ledger. The same key with the same digest is a no-op; the same key with a different digest fails withIdempotencyConflict. - It then stages record creates and updates with
SameBoundaryvisibility, draws from the military stream, and schedules follow-upAdvanceTickpackets withSchedulePluginIngress. Every command exceptMilitaryAdministrationActionandAdvanceTickalso records aMilitaryOutcomeunder its key. - The same system applies
military_provider_ack_v1packets, described under effects in other domains. - The kernel validates the staged writes in phase 8 and commits them together in phase 9. Any error rolls back the whole boundary.
- In phase 13 (
PerspectiveAndReportMaterialization), the event-driven systemmaterialize-military-reports-v1publishes amilitary_reportto each force’s commander. - A scheduled tick returns as a
military_command_v1packet at its due time and runs through phase 7 again.
Marches and combat
Section titled “Marches and combat”View diagram source
flowchart TB
Order["OrderMarch<br/>operation Moving"] --> Arrive{"Arrival tick:<br/>supply 100 or more?"}
Arrive -- "no" --> Failed["Failed<br/>force Routing"]
Arrive -- "yes, no opponent" --> Done["Completed<br/>force Ready at destination"]
Arrive -- "yes, opponent named" --> Contact["Engaged<br/>combat record at Contact"]
Contact --> Round["Daily round<br/>one draw"]
Round -- "both sides remain,<br/>under four rounds" --> Round
Round -- "defender at 0" --> Win["Completed<br/>AttackerVictory,<br/>occupation created"]
Round -- "attacker at 0<br/>or fourth round" --> Withdraw["Withdrawing<br/>DefenderVictory or<br/>MutualDisengagement"]
- On arrival, a force with supply of 100 or more loses 100 supply, gains 20 fatigue, loses 10 morale, and moves to the destination.
- Contact creates the combat record
canwu.military:combat:followed by the operation ID. A defender with an unexpired ambush prepared at that node enters with preparation 850 in place of 500, and the ambush is consumed. The record stores the fixed tactic labelsscreen-and-advanceandhold; the tactic named in the order is checked at admission and then unused. - Combat resolves one round a day, at most four rounds. Each round draws a value
sbelow 1,000. The attacker loses one twentieth of the defender’s strength (at least 1) plussmod 8; the defender loses one eighteenth of the attacker’s strength (at least 1) plus (999 minuss) mod 8. Both sides also lose morale equal to half their loss and gain 35 fatigue. Preparation is stored on the record; the round formula reads only the two strengths and the draw. - When the battle ends, a side at 0 strength becomes
Routingand a side with strength left becomesReady. AnAttackerVictorycreates an occupation of the destination with IDcanwu.military:occupation:followed by the operation ID.Withdrawingis a final label: no further tick runs and no force moves.
A special operation resolves one day after the order. After the same supply step it draws a value below 1,000. A draw below 650 leaves the force Routing at the target and the operation Failed. A draw of 650 or more is meant to return the force to its start as Completed, but see the known issue below.
Retreat movement, pursuit, supply reservation, route travel time, and transfers of casualties to population are outside the current plugin. Some ForceStatus, OperationPhase, CombatStage, and CombatResult variants declared in model.rs are never set by the current systems.
Occupation
Section titled “Occupation”EstablishOccupation, or an attacker victory, creates an occupation with military control 700, a garrison of one third of the force’s strength, security 500, resistance 500, and stage MilitaryControl. The occupation updates once a day until its stage reaches Intergenerational. Each update adds 25 security, 20 administrative reach, and 10 collaboration, and removes 10 resistance, each within 0 to 1,000. The stage then advances by at most one step:
| From | To | Needs administrative reach | Needs security |
|---|---|---|---|
MilitaryControl |
AdministrativeTakeover |
300 | 600 |
AdministrativeTakeover |
LegalRecognition |
500 | 650 |
LegalRecognition |
FiscalIntegration |
650 | 700 |
FiscalIntegration |
SocialIntegration |
800 | 750 |
SocialIntegration |
CulturalPractice |
900 | 800 |
CulturalPractice |
Intergenerational |
1,000 | 850 |
Stage names are labels on the occupation record. Advancing a stage writes no legal, fiscal, or social record. SetOccupationPolicy overwrites security, collaboration, and extraction burden and raises policy_revision; later daily updates continue from the new values.
Effects in other domains
Section titled “Effects in other domains”MilitaryAdministrationAction asks another domain to act for an occupation. The phase-7 system adds a PendingMilitaryEffect to the ledger with the operation key, the provider plugin name, the provider record version it expects, and the occupation. The ledger must already exist; the first recorded command outcome creates it.
The provider settles the effect in its own records. Your integration then reports the result with enqueue_provider_outcome(canwu, due_at, outcome), which queues a military_provider_ack_v1 packet (ingress class Acknowledgement). In phase 7 the military plugin:
- finds the pending effect for
outcome.operation, or fails withInvalidAuthority; - requires the provider name and version to match the pending effect, or fails with
InvalidAuthority; - requires a non-empty
digestandprovider_record, or fails withInvalidPayload; - removes the pending effect, records the outcome in the ledger, and keeps a copy as a
provider_outcomerecord; - updates the occupation.
AcceptedorCommittedmovesMilitaryControltoAdministrativeTakeover, orAdministrativeTakeovertoLegalRecognition.RejectedorCompensatingreturns the stage toMilitaryControland lowers legitimacy by 50.
The check compares the names and versions stored in the ledger. The military plugin never reads the provider’s record, so your integration is responsible for passing on a real outcome.
When a command fails
Section titled “When a command fails”- A wrong issuer (
InvalidAuthority), a digest mismatch or an unknown branch or tactic (InvalidPayload), and direct submission (MixedCommandIngress) reject the command at admission. Nothing is queued. - A stale
expected_force_revisionfails admission withDomainRecordVersionConflict. The engine handles that code as a boundary failure:advance_canonicalreturns the error and the boundary rolls back. - Any error in the phase-7 system also fails the boundary. Examples are a reused operation key with different input, a recruit with no capacity left, a missing force, and an acknowledgement with no matching pending effect.
After a boundary failure, the failing input stays in the queue, so each later advance_canonical call fails the same way. To continue, restore a snapshot or checkpoint saved before that input was enqueued. Check the force revision, and the provider name and version of an acknowledgement, before you enqueue. A repeated command with the same key and input is a no-op only when admission accepts it; a command that carries expected_force_revision fails as stale once its first copy has raised the force revision.
Knowledge and visibility
Section titled “Knowledge and visibility”The phase-13 system publishes one military_report knowledge record (schema canwu.military / military_report, version 1) for each force that has a commander. The holder is the commander, and the subject is the force record. The payload gives the force ID, location, strength as a range of plus or minus one tenth, supply as a range of plus or minus 100 per mille, and the observation time. Confidence is 900 per mille, and the origin cites the exact force record version.
A player-facing client reads these reports through viewer_for_actor(person) and CanwuViewer::query_knowledge. Other actors, including an opposing commander, receive nothing from the military plugin: Recon records a draw and an event but publishes no knowledge. Canwu::typed_domain_record, used by the starter to read combat and occupation records, returns ground truth and belongs in trusted host code.
Randomness, persistence, and replay
Section titled “Randomness, persistence, and replay”All draws come from one random stream, canwu-military / military-operation / 1 (military_random_stream()), declared by the phase-7 system. Each draw is an operation-keyed draw with a bound of 1,000:
| Draw | Operation kind | Target and slot | Purpose | How the value is used |
|---|---|---|---|---|
Recon command |
military_command |
Canonical key of the operation key, slot 0 | resolve military operation uncertainty |
Recorded only |
| Special operation | special-operation |
Canonical key of the operation ID, slot 0 | special-operation-success |
Success at 650 or more |
| Combat round | combat-round |
Exact combat record version, slot = round | combat-round-remainder |
Extra losses from the value mod 8 |
Your application supplies no probability. The 650 threshold and the loss formula are constants in canwu-military. From the catalog the plugin reads only branch and tactic names; the ruleset’s numeric profiles, node profiles, and commander profiles are never read.
All military state lives in domain records, so snapshots save it with the knowledge ledgers, draw evidence, and the queue of scheduled ticks. The plugin identity is the name canwu-military, plugin version 0.1.0, and a fixed semantic hash; restoring a snapshot or replaying a journal requires the same identity. The lifecycle test complete_military_lifecycle_is_persisted_and_replayed checks that a restored run and a run replayed with replay_from_journal reach the same checkpoint hash as the live run.
What your application supplies
Section titled “What your application supplies”- Scenario content. Build a
MilitaryCatalog, install it withrecord_from(catalog_reference(), ...)inscenario.domain_records, and callMilitaryRulesetV1::validateyourself. The ruleset hash covers the ruleset ID, profile name, and the counts of branches, tactics, and terrain modifiers. - Node identities. The plugin compares
MilitaryNodeIdstrings for equality and never checks them against catalog nodes. Travel, routes, and adjacency belong to your integration. - Authority. Give each force a commander at creation: a force without one accepts no later force command,
AssignCommanderincluded. Decide in your host who may sendCreateForce,SetOccupationPolicy,MilitaryAdministrationAction, andAdvanceTick, because admission checks no issuer for them. - Unique keys and current revisions. Use a new
MilitaryOperationKeyfor each intent, and read the force revision just before you enqueue. - Provider plugins for administration effects, plus the code that calls
enqueue_provider_outcome. - Supply, casualty flows, and richer combat. These need your own plugins, or a replacement for
canwu-military. - Presentation: maps, reports for non-commanders, and intelligence estimates.
Limits and budgets
Section titled “Limits and budgets”| Constant or rule | Value | Enforcement |
|---|---|---|
MAX_SUBUNITS |
64 | ForceState::validate rejects a force with more subunits |
SCHEMA_VERSION |
1 | Required on rulesets and force records |
| Military IDs | 1 to 192 bytes, with a : |
ASCII letters, digits, and . _ - : / |
| Per-mille values | At most 1,000 | Force metrics, occupation metrics, and occupation policy |
| Report scan | 256 force records per boundary | The phase-13 system reads the first 256 force records in canonical order |
ForceState::validate also requires the subunit strengths to add up to the actual strength, and losses to stay within the authorized strength.
MAX_RECORDS (4,096), MAX_COMPOSITION_ENTRIES (64), and MAX_OPERATION_PARTICIPANTS (128) are declared but not enforced.
Try it
Section titled “Try it”cargo run -p canwu-military-reference --example military_startercargo test -p canwu-militarycargo test -p canwu-military-reference-contentThe starter creates a field force, recruits, plans an operation, marches on a defended node, wins the battle, and checks snapshot and replay. It prints military_gameplan=complete with the two ruleset names and the checkpoint hash. The walkthrough Run the military extension: from contact to occupation covers it step by step.
For resource-backed supply, run cargo run -p canwu-economy-reference --example grain_loop and read the grain, production, and military supply case. For stream declarations and draw evidence, see randomness.
Further reading
Section titled “Further reading”- Settlement system: the fourteen phases, staged writes, and rollback
- Event system: admission order and zero-delay packets
- Extend with plugins: writing a plugin to replace or extend this one
- Military domain design notes: a larger design than the current code implements; the code in
crates/extensions/canwu-militaryis the reference for behavior