Follow-up to the storm post — same engine, two new systems, and a number I didn't expect.
The experiment
Same blind mountain pass. Same pincer maneuver. Four doctrines, 80 seeded runs each, one question: how much does knowing where the enemy is actually matter, compared to just guessing well?
doctrine wins cavalry lost
cautious fixed clock (turn 35) 0/80 14
tuned fixed clock (turn 45) 16/80 0
single conditional order 17/80 0
real perception (full scripting) 80/80 0
Read that first row again. Zero wins, out of eighty, for the "safe" doctrine — marching cautiously, on a fixed schedule, hoping to avoid disaster. Guessing well (the tuned clock) already beats caution outright. Real information doesn't just help — it goes to 80/80.
The honest line that made it into the README: information doesn't add aggression, it removes calibration risk. You don't need to know the enemy's position to be aggressive. You need it to stop guessing wrong.
What changed since the storm
The storm post was about a random event flipping a close fight. Since then I've added two systems that are about knowledge, not chance:
Fog of war, implemented as a pure projection layer. The engine itself stays omniscient — nothing about combat, movement, or the decapitation mechanic changes. A function, perceive(battle, faction), filters what each side actually knows, computed fresh at the end of every turn. There's a test that diffs the full battle state before and after calling it, to guarantee it can never leak into the simulation itself.
Three visibility tiers, by zone distance:
Visible (your zone, or adjacent): exact position, strength, formation, facing
Suspected (two zones out): you know where they are, not how strong — though if their real strength happens to be in the 40-60% "close call" band the luck system already used, you get a hint (suspected_in_trouble) without the number
Hidden (three+ zones, off-map reserves): nothing
One rule mattered more than I expected: a general's capture zone is always public the instant he's exposed. The engine has no targeting — decapitation is purely positional (cavalry + exposed general in the same zone = capture). Hide that location under fog and the mechanic stops being tactical decapitation and becomes blind wandering. So routs are loud on purpose. My favorite emergent detail: when a formation simply stops appearing in every report, that absence is information. The opponent doesn't need to see the general fall — they can infer it, and ride straight there.
Conditional orders, scoped deliberately small for v1: three pure triggers (enemy presence at or above "suspected," visible strength below a threshold, or total absence of contact), evaluated against the perception snapshot from the end of the previous turn — never against ground truth, and never against information from later in the same turn. An order persists until it fires, and fires once.
I left "general exposed" out of this trigger set on purpose — it interacts with the same capture mechanic that made the visibility rule above tricky, and deserved its own pass rather than getting bolted onto v1.
Why the conditional-order number is the interesting one
17/80 isn't a huge win over 16/80. That gap is basically the finding.
A single conditional order ("advance once you see the enemy guard weakened or gone") ties the tuned fixed-clock doctrine — without any manual tuning. It doesn't need a calibrated turn number, it reacts. But it doesn't come close to full scripting (80/80), because one condition can't choose between two different pursuit paths once it fires. You commit once, to one branch of behavior, and then you're locked in.
The honest read: compromise is the price of pre-commitment. A general standing at a map table with a single standing order does better than one following a rigid schedule — but worse than one making a decision every single turn. That's not a flaw in the implementation. It's what a single conditional order actually buys you, and I'd rather show that number than round it up.
Where it stands
133 tests passing. with_fog defaults to False — every prior test remained byte-for-byte identical when fog was added, same pattern used for the luck layer in the last post. Perception snapshots live on the engine, never on the battle state, so nothing about "what a faction currently believes" can leak into combat resolution.
Repo: https://github.com/jo2008-ai/kingdom-battles
Next up on the roadmap: attrition and supply lines — what happens when an army can't just fight forever. If you want to pick up a piece of this yourself, the open issues on the repo include fog-dependent work (general-exposed triggers, richer conditional logic) and a fully open one: a third faction that can intervene mid-battle. That one needs a design discussion before code — I'd genuinely like other opinions on it.
This article was originally published by DEV Community and written by Joseph Magombe.
Read original article on DEV Community