Building a deterministic tactics engine in Python, inspired by Kingdom.
Same seed. Same orders. Same pincer maneuver through a mountain pass. Two different outcomes:
Baseline (no luck): WEI wins by attrition (Qin ground down to 20%)
With luck enabled: QIN wins by decapitation
What changed? On turn 4, Wei's guard formation was at 57% strength and Qin's cavalry at 54% — both inside a "close call" band (40-60%). A random event called Sudden Morale (6% probability, tested, not invented after the fact) grants +10% attack to units in that band. The extra hit pushes Wei's guard down to 24% — one point below the 25% rout threshold. Without the bonus, it would have held at ~27%.
The enemy general is left exposed, one step from the cavalry unit already occupying his headquarters. Captured.
It wasn't blind luck. It was a technical tie decided by a hair's breadth — and the seed proves it's reproducible.
Why I built this
I'm a solo developer based in Mozambique, and for the past while I've been building a turn-based tactical battle simulator inspired by the combat philosophy of the anime/manga Kingdom: victory through intelligence over raw numbers, the general as a force multiplier (not just a strong unit), terrain as a weapon, room for genuine reversals.
No graphics yet — just the engine, in Python, to validate the rules before investing time in Godot/mobile.
What got proven, not just implemented
A clean tactical triangle. Formations follow a rock-paper-scissors relationship — wedge beats line, line beats column, column beats wedge — verified all the way down to the mirror match (formation vs. itself = mutual destruction, roughly even losses on both sides).
A quantified ceiling for "intelligence beats force." Through scaled scenario testing, I found that roughly 1.15x numerical advantage is the limit where tactics can still flip a battle. Beyond that, numbers simply overwhelm positioning and formation choice — no amount of clever terrain use saves you.
A luck layer with its own RNG stream. Adding randomness to a deterministic, seed-tested engine is risky — if events draw from the same stream as combat resolution, every existing test sequence shifts. So the luck layer draws from a derived stream (f"luck:{seed}"), gated behind a with_luck=False default. The original 40 core tests remained byte-for-byte identical after adding it.
The exact flip rate of that luck. ~1.7% of seeds in the unstable zone flip the result. Any higher and it stops being tactics and starts being a coin flip. Any lower and it would basically never matter. A dominant army (100% strength) still always wins with luck on. A desperate one (60% strength) still always loses. Luck only speaks when the armies were already close.
The bugs that taught me the most
A BFS pathfinding function that included the origin node in the returned path — the vanguard could never actually enter the mountain pass, because the code thought it was already there.
A variable that stored marching direction instead of entry side — silently flipping front and rear arcs for any unit approaching from the "wrong" way.
A unit stuck in muddy terrain (0 movement) reporting the wrong reason ("this formation is static") instead of the right one ("stuck in the mud this turn"). Not caught by any test — caught by manually reading a battle log and going "wait, that formation isn't supposed to be static."
None of these showed up by accident. They showed up because I tested against full narrative scenarios — a named pincer maneuver with a named general getting captured — not just isolated unit-vs-unit matchups.
Where it stands
87 tests, all passing. Deterministic by seed — same seed, same battle, every time.
Code in English, battle logs in Portuguese — a deliberate split. Anyone reading the code should feel at home; anyone reading a battle log should know exactly where this came from. A rendering layer (KIND_PT, ARC_PT, DIRECTION_PT, STATE_PT) keeps the two vocabularies cleanly separated — AttackArc.REAR in the code, "ataca pela retaguarda" in the log.
CI runs the full suite plus the storm scenario on every push.
Top comments (0)