Mr. Phil Games’ Blog

Posts for Tag: Debugging

Stellar Throne Devlog #6 — Zig Parity and Production Deployment

Hey everyone, MrPhil here with another devlog update!

Today marks a critical production deployment milestone for Stellar Throne, my 4X strategy game set in a post-empire galaxy. For those new here, I’m building Stellar Throne with a dual-engine architecture — using Godot for the UI client and Zig for the high-performance simulation backend.

This week’s focus: validating that the Zig simulation engine isn’t just fast — but production-ready.


⚙️ Context: The Zig Engine Hits 52.7× Performance

After two weeks of development, the Zig simulation engine is complete — delivering a verified 52.7× speedup over the original Godot implementation. Every one of the 1,069 tests passes cleanly.

The question was no longer “Does it work?” but “Can I ship it?” And even more importantly: “Will it stay stable under real-world stress?”


🧪 Parity Testing and Stress Scenarios

To answer that, I built a comprehensive parity testing system — a harness that runs the exact same turn through both the Godot and Zig engines, then compares every field in the game state to ensure they produce identical results.

Test scenarios ranged from:

  • 10 empires with 10 systems each

  • Up to 100 empires with 250 star systems

That’s when things got interesting — and the bugs started surfacing.


🐛 Three Critical Bugs (and How I Fixed Them)

1. Negative Resource Spirals

Zig’s resource validation could create negative feedback loops if a colony’s food production went below zero — spiraling further negative each turn instead of clamping to zero.
Fix: Added strict validation to prevent negative resource values during calculations.

2. Fleet Upkeep Mismatch

The Godot version used a flat-rate upkeep (10 Energy + 5 Minerals per fleet), but Zig calculated per-ship upkeep — leading to wildly inconsistent costs.
Fix: Standardized both engines to use the flat-rate formula for base upkeep while keeping per-ship maintenance for hull and weapon costs.

3. Maintenance Persistence Loss

Maintenance costs were being overwritten during stat recalculation, resetting values to zero.
Fix: Preserved maintenance data during the recalculation phase.

After fixing these, I expanded the stress tests — pushing edge cases like 200+ fleets per empire, negative production chains, and zero-population planets. That led to 11 parity fixes across both engines — covering resources, fleets, population growth, and infrastructure costs.


🚀 Deployment Roadblocks and Race Conditions

With parity achieved, I turned to production deployment — enabling the Zig backend automatically during game initialization.

Two blockers emerged:

  1. Initialization Timing
    The Zig backend tried to serialize game state before empires existed — crashing when the empires array was still empty.
    Fix: Corrected property references (galaxy.galaxy_width → galaxy.width) and restructured initialization order.

  2. Async Signal Race Condition
    The system awaited a signal after it was emitted — a classic async race.
    Fix: Stored the signal reference before starting the turn, ensuring the listener was connected in time.

To prevent future issues, I implemented:

  • Helper functions in GameScreenCore and GameInitializer

  • Debug logging in TurnProcessor

  • 343-line deployment guide detailing rollback, monitoring, and troubleshooting procedures


🧩 Deployment Decision: Disabled by Default

After reviewing everything, I made the cautious call to keep the Zig backend disabled by default for now.
The timing between empire creation and backend initialization is delicate — and I’d rather avoid subtle production bugs.

That said, the system is fully integrated. Manual enablement is available for testers. The performance boost is real, and parity is 100% confirmed.


📈 Final Stats

  • 20 commits in one day

  • 4,243 new lines of parity infrastructure across 13 files

  • 343-line deployment guide completed

  • 11 parity bugs fixed

  • All 1,069 tests passing

  • 52.7× performance improvement validated

The Zig simulation backend is production-ready, complete with fallback to Godot if anything fails.


🧭 Next Steps

  1. Verify signal-timing fixes in live game sessions

  2. Test deployment in extended campaign scenarios

  3. Decide whether to enable Zig automatically or keep manual control

Either way, the hard part’s done — the Zig engine is live-ready and just one config flag away.


Thanks for following along — and as always, you can find more at MrPhilGames.com.

See you in the next devlog!

When the Bug Breaks Before You Do

A couple days ago was… maddening.

I’d been wrestling with the same bug for days—the kind that sits in your code like a smug little goblin, daring you to come find it. Every time I thought I had it cornered—poof—it would slip away into another layer of logic.

The problem sounded simple enough: combat is simultaneous. Even if a ship gets destroyed, it should still get to fire its weapon that turn. But the UI shouldn’t mark it as destroyed until after all attacks are resolved.

Except in my game, ships weren’t showing their dramatic “destroyed” flair until the start of the next turn. By then, the moment was gone. It was like telling a joke and waiting five minutes for the punchline to land.

I threw everything at it:

  • The “outside consultant” trick—pretending Claude was a hired expert swooping in to save the day.

  • The “you’re a zookeeper” trick. (Don’t ask.)

  • Breaking the workflow into phases.

  • Having Claude explain the code back to me.

  • Running the debugger subagent.

  • Asking it to think hard… harder… ultra-think.

  • Asking Claude to improve my prompt.

  • Diagramming the problem like a detective on a conspiracy board.

  • Adding a ton of debug logs.

  • Even pulling in ChatGPT to craft a better Claude prompt.

  • Describing the issue in painstaking detail—right down to which variables changed on which frame.

Nothing. Worked.

And this wasn’t even a crash bug—the game ran fine. It was just wrong. The kind of subtle pacing flaw that players might not notice consciously, but would feel in their bones. The kind that makes everything feel just slightly “off.”

By about hour six that day, I was leaning back in my chair, staring at my code, wondering if the bug was less about the project and more about me.

Then—somewhere between frustration and surrender—I tried one more approach. Nothing special about it. No perfect galaxy-brain prompt. Just another attempt in a long line of attempts. And this time… it worked. The ships died exactly when they were supposed to, the UI updated cleanly, and the combat flow finally felt right.

I’d love to tell you it was some brilliant debugging insight or the magic words that unlocked Claude’s genius. But it wasn’t. It was just the luck of the dice—one more roll, one more try, and this time it came up in my favor.