<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joseph Magombe</title>
    <description>The latest articles on DEV Community by Joseph Magombe (@joseph_magombe_1b430cabe3).</description>
    <link>https://dev.to/joseph_magombe_1b430cabe3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4090220%2F70669df1-dc85-4c8d-8b72-636a64e4102d.png</url>
      <title>DEV Community: Joseph Magombe</title>
      <link>https://dev.to/joseph_magombe_1b430cabe3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joseph_magombe_1b430cabe3"/>
    <language>en</language>
    <item>
      <title>Title Information Doesn't Add Aggression — It Removes Calibration Risk</title>
      <dc:creator>Joseph Magombe</dc:creator>
      <pubDate>Sun, 23 Aug 2026 18:52:16 +0000</pubDate>
      <link>https://dev.to/joseph_magombe_1b430cabe3/titleinformation-doesnt-add-aggression-it-removes-calibration-risk-3goc</link>
      <guid>https://dev.to/joseph_magombe_1b430cabe3/titleinformation-doesnt-add-aggression-it-removes-calibration-risk-3goc</guid>
      <description>&lt;p&gt;Follow-up to the storm post — same engine, two new systems, and a number I didn't expect.&lt;/p&gt;

&lt;p&gt;The experiment&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;doctrine                          wins    cavalry lost&lt;br&gt;
cautious fixed clock (turn 35)    0/80    14&lt;br&gt;
tuned fixed clock (turn 45)       16/80    0&lt;br&gt;
single conditional order          17/80    0&lt;br&gt;
real perception (full scripting)  80/80    0&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;What changed since the storm&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Three visibility tiers, by zone distance:&lt;/p&gt;

&lt;p&gt;Visible (your zone, or adjacent): exact position, strength, formation, facing&lt;br&gt;
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&lt;br&gt;
Hidden (three+ zones, off-map reserves): nothing&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Why the conditional-order number is the interesting one&lt;/p&gt;

&lt;p&gt;17/80 isn't a huge win over 16/80. That gap is basically the finding.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Where it stands&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/jo2008-ai/kingdom-battles" rel="noopener noreferrer"&gt;https://github.com/jo2008-ai/kingdom-battles&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>gamedev</category>
      <category>buildinpublic</category>
      <category>python</category>
    </item>
    <item>
      <title>A Storm Didn't Decide This Battle — A Sergeant Did</title>
      <dc:creator>Joseph Magombe</dc:creator>
      <pubDate>Sat, 22 Aug 2026 22:38:39 +0000</pubDate>
      <link>https://dev.to/joseph_magombe_1b430cabe3/a-storm-didnt-decide-this-battle-a-sergeant-did-39m2</link>
      <guid>https://dev.to/joseph_magombe_1b430cabe3/a-storm-didnt-decide-this-battle-a-sergeant-did-39m2</guid>
      <description>&lt;p&gt;Building a deterministic tactics engine in Python, inspired by Kingdom.&lt;/p&gt;

&lt;p&gt;Same seed. Same orders. Same pincer maneuver through a mountain pass. Two different outcomes:&lt;/p&gt;

&lt;p&gt;Baseline (no luck):     WEI wins by attrition (Qin ground down to 20%)&lt;br&gt;
With luck enabled:      QIN wins by decapitation&lt;/p&gt;

&lt;p&gt;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%.&lt;/p&gt;

&lt;p&gt;The enemy general is left exposed, one step from the cavalry unit already occupying his headquarters. Captured.&lt;/p&gt;

&lt;p&gt;It wasn't blind luck. It was a technical tie decided by a hair's breadth — and the seed proves it's reproducible.&lt;/p&gt;

&lt;p&gt;Why I built this&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;No graphics yet — just the engine, in Python, to validate the rules before investing time in Godot/mobile.&lt;/p&gt;

&lt;p&gt;What got proven, not just implemented&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The bugs that taught me the most&lt;br&gt;
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.&lt;br&gt;
A variable that stored marching direction instead of entry side — silently flipping front and rear arcs for any unit approaching from the "wrong" way.&lt;br&gt;
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."&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Where it stands&lt;br&gt;
87 tests, all passing. Deterministic by seed — same seed, same battle, every time.&lt;br&gt;
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.&lt;br&gt;
CI runs the full suite plus the storm scenario on every push.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/jo2008-ai/kingdom-battles" rel="noopener noreferrer"&gt;https://github.com/jo2008-ai/kingdom-battles&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>gamedev</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
