Skip to content

Military

The military simulation extension, canwu-military, is an engagement engine. It resolves fights between typed forces at a place your world owns, under a combat rulebook your game installs and a step resolver; what it writes is records and signals that your game applies. It owns no doctrine, unit catalogue, terrain table, weather, control or occupation state, store, or report text. Read this page when you decide whether to drive your combat with it, or how to fit your own combat model into it.

In brief: every number that decides a fight is a parameter of your rulebook. Your game places forces and supplies their condition each day. The engine steps the fight, records every per-step quantity, and closes it into an engagement_summary record; a fallen place is a fact in that summary, never an occupation the engine creates.

Crate What it owns Published
canwu-military MilitaryPlugin<R>, the StepResolver trait and ParametricStepResolver, eight record kinds under canwu.military (rulebook, registry, force, engagement, engagement_steps, siege, crossing, engagement_summary), the MilitaryCommandV1 command, and the engagement_report and engagement_outcome knowledge schemas Published on crates.io
canwu-military-reference-content synthetic_rulebook_v1(), one explicitly synthetic rulebook Published on crates.io
canwu-military-reference demo_engagement_scenario(), which installs that rulebook and six forces on the reference world, and the military_starter example Repository only
Concern Who owns it
A force as combat knows it: owner, controllers, location, components with present and lost men, side terms, battle state canwu-military (the force record); your game registers, revises, and places it
Everything else about a force: pay, autonomy, readiness, doctrine, commander, stores Your game, passed in as condition inputs each engagement-day
The engagement process: open, forecast, steps, break, pursuit, close, summary canwu-military, on your rulebook and a resolver
Sieges and contested crossings canwu-military; the site’s walls, supply, naval control, and a gate opened by your own rule are your inputs
Movement, contact, detection, hostility Your game and canwu-movement or canwu-knowledge; the engine checks only that forces stand at the site
Control, occupation, casualties as people, stores of goods Your game, from the summary and the outcome signal
  • Combat rulebook (CombatRulebookV1, 战斗规则册): step profiles, per-kind rules for the six engagement kinds (Field, Assault, Sortie, Relief, FerryFight, BridgeheadFight), forecast terms and bands, the four lines, cohesion, morale, break, pursuit, terrain, night, fortification, siege and crossing rules, and the limits of every collection the engine keeps. It is hashed; the scenario installs it, or a System issuer sends InstallRulebook. A run holds one.
  • Force (ForceStateV1): a force stands at a place your world owns (LocationV1::At { site }); your game places it with PlaceForce when it arrives. Its components carry the values your game compiles from its unit profiles; its side terms carry cohesion cap, morale break, battle command, assessment, credibility, and traits.
  • Engagement (EngagementStateV1, 交战): two sides at a site, each with its forces, its report holders, its latest condition input (ConditionInputV1, 战斗条件输入), the battle’s morale shock, cohesion, and losses; a pre-battle forecast (ForecastV1, 战前预判) from the rulebook’s terms and ratio bands.
  • Engagement step (StepV1, 战斗步): one resolution of the step formula with every per-step quantity, the inputs’ digests, the draws, and the resolver’s identity.
  • Siege (SiegeStateV1, 围城) and contested crossing (CrossingStateV1, 渡河作战): day-grained and wave-grained processes that open their own engagements.
Commands become apply packets, a step packet steps the open work in phase 7, and phase 13 publishes reports. View diagram source.
View diagram source
flowchart TB
  Cmd["MilitaryCommandV1<br/>canonical command ingress"] --> Admit["Admission<br/>static checks, holders"]
  Admit --> Apply["military_apply_v1 packet"]
  Wake["Wake system on your cadence,<br/>the trusted Step command,<br/>or your timed step"] --> StepPacket["Step packet"]
  Apply --> Settle["Phase 7: settle-engagements-v1<br/>apply commands, then step"]
  StepPacket --> Settle
  Settle --> Records["force, engagement, steps,<br/>summary, siege, crossing"]
  Settle --> Signal["Optional outcome signal<br/>to your plugin"]
  Records --> Publish["Phase 13: reports<br/>to each side's holders"]
  1. Your game sends a command; admission checks its shape and its report holders and queues an apply packet for the next boundary at the same instant.
  2. A fight steps only in a boundary that admits a step packet. With with_step_cadence(Some(cadence)) (daily by default) the wake system schedules one whenever work is due, stepped in the next boundary at the same instant; with with_step_cadence(None) your game sends Step { up_to } after its inputs, stepped in the boundary that applies them. Either way your game may instead queue a timed step (timed_step_ingress(due_at)) for a boundary it settles anyway, such as the next daily one: that boundary steps, and stepping adds no boundary of its own. Commands that boundary admits apply in the next cut, after the step; if a day’s orders must apply before the step, send Step with them, or have them admitted in a boundary before the timed step is due, so their apply packets land in the stepping boundary ahead of the step.
  3. In phase 7 the settle system applies the boundary’s commands in admission order, then steps every engagement, siege day, and crossing wave due by then.
  4. Each step builds the resolver’s input from the sides’ components, their latest inputs, the environment, the kind’s rule, and the site; calls the resolver; splits casualties over components by largest remainder (each component takes the whole part of its proportional share, and the places left over go to the largest fractional remainders); applies the break rule and pursuit; and appends the step.
  5. A side that withdraws or routs closes the engagement; its max_minutes without a break (the kind’s, or a shorter time your game named when it opened the fight) closes it Stalled. The close writes a create-only engagement_summary record and, if you asked for one, sends the outcome signal.
  6. In phase 13 the report system publishes each side’s holder-relative report within the knowledge budget.

The resolver is a type: MilitaryPlugin<R: StepResolver>. ParametricStepResolver computes four lines per side (fire, shock, melee, and defense: one integer term each) from the components, terrain, night, fortification, and supply, and a per-step rate each side inflicts; every per-step quantity (inflicted share, cohesion loss, morale shock) scales that rate once by the profile’s step_scale_bp (in basis points, hundredths of a percent), so a daily profile can compress many short steps into one. A game with its own formula implements StepResolver once and may ask for keyed draws through StepDraws. Activation refuses a rulebook that names another resolver than the plugin runs.

The engine owns everything around the call: it clamps the outcome to the rulebook’s ranges, applies the break rule (a defender behind closed walls of the rulebook’s grade never breaks), applies pursuit on a rout, and decides whether the fight continues. Step profiles let one fight run on a daily step while another is promoted to half-hour steps (ReviseStepProfile).

A siege day computes investment from the besiegers’ men and any naval control, emplaces guns and then bombards the walls toward the grade’s breach threshold, and opens an assault when ordered or, with storm_on: BreachOrGate, the day a breach or a gate opens. A sortie opens when ordered and the defenders’ cooldown has passed; a relief is an engagement you open against the besiegers. Defenders breaking at the wall make the siege fall and disperse the defenders still there; besiegers routed lift it. A second force joins a besieged site with JoinSiege, and enters on its side any assault or sortie under way; ReviseSiege { release } lets a force on either side that is not fighting leave, so you can place it elsewhere. A siege whose last defender leaves falls Abandoned at its next siege day, and LiftSiege ends one whose last besieger leaves.

A crossing sends one wave per wave interval. An interceptor whose control is high enough catches a wave by threshold or by a keyed draw; a caught wave fights a ferry fight and turns back; a landed wave may fight a bridgehead fight against the defenders on the far bank, which the order names and ReviseCrossing { defenders } revises when a force reaches that bank later. Your game moves the force when the crossing completes.

A command that fails admission is a rejected command attempt. A command that fails in phase 7 (an unknown or engaged force, a stale expected_host_revision, an issuer who controls no force, a limit, an unknown posture or profile) is recorded with a canwu.military.command_rejected.v1 event whose summary begins with the error code. A step the engine cannot take (an oversized step, a failing resolver, too many draws) holds its engagement with canwu.military.engagement_held.v1 until a SupplyInput, ReviseStepProfile, or OrderAction { Abandon }. None of these fails the boundary.

Each side names its report holders: people, governments, or the force records themselves. Holders are checked when the command is admitted and again when a report is published, so a holder who has died is skipped. Progress reports (engagement_report) supersede the side’s last; closing reports (engagement_outcome) are kept. A fight, siege, or crossing whose sides name no holder queues no report, so a game that writes its own reports from the records pays the report system nothing. A side sees its own losses exactly and the enemy’s as bands from the rulebook’s cuts.

Two random streams, crossing-interception and step-contingency, are drawn by keyed address with the admitted step packet as evidence, so a live, sealed, restored, and replayed run draw alike. Every command that changes a record names that record’s expected_host_revision, which only host commands advance: a step in the same instant never makes your command stale. The wake cadence, the signal target, and the publication reservation are part of the plugin descriptor, so a run restores and replays only with the same choices.

  • The rulebook, compiled from your content.
  • Forces and their revisions (muster, desertion, wounded returns) and their placement on arrival.
  • Engagements, sieges, and crossings opened from your own contact rules, with each side’s report holders.
  • One condition input per side and one environment input a day, derived from your supply, pay, command, and weather records, cited as evidence.
  • Your own rules the engine does not decide: betrayal (ReviseSiege { gate_open_steps }), storming doctrine, rations, naval patrols, promotion of a fight.
  • What follows a fight: control, occupation, population, stores, and player text, from the summary or the outcome signal.

Every collection the engine keeps has a cap in the rulebook’s limits: forces, components per force, forces and holders per side, open engagements, sieges, and crossings, step bytes, draws per step, and the report queue. A boundary without a step packet does no stepping work; a stepping boundary costs in proportion to the open engagements and the forces in them. A closed fight leaves about 2 KB of records for the life of the run. The engine has two sides per engagement, no automatic contact, and banded enemy figures in its reports.

终端窗口
cargo run -p canwu-military-reference --example military_starter

The walkthrough Run the engagement engine explains what the starter does at each 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.