BallOrcs Battle: an army you can see, orders you can give, robots to give them
Last time, BallOrcs Battle was an engine with no picture — a war simulated purely in numbers. This week it got a face.
In plain words
Last time, BallOrcs Battle was an engine with no picture — a war simulated purely in numbers. This week it got a face. Thousands of units now draw on screen under an isometric camera, and you can actually order them about: click to select, right-click to move, hotkeys to fire off abilities. Two abilities exist so far — a self-buff and a short teleport hop. There's a pause menu that genuinely freezes the world rather than just hiding it. And because a strategy game is far too big to test by hand, we built robot commanders instead: one that fires random orders, one that follows a written script, and one that replays a recorded match tick for tick.
Under the hood
- Phase 2 — world on screen (
5ebc196):SimServerbridges the pure C++ sim into Godot, with instanced unit rendering and an isometric camera. The sim core stays engine-agnostic; the renderer reads aRenderSnapshotand never writes back. - Phase 3 — the command stream (
95f4651,881d151,c1a4557,27d1bdb), in slices.SimServergained a GDScript-facing command API so bots drive units the way a player does — through the command stream, not by poking the sim. Then the determinism bug worth knowing about: the bot's decision cadence and its exec-tick stamp both depended on which physics frame observed a sim tick, not the tick itself. Invisible under matched frame timing, fatal under any variance. Fixed by construction withmove_units_at/stop_units_at/debug_spawn_at— the command executes at a fixed offset from a caller-supplied decision tick and never readsworld_->tick()at call time — plus a catch-up loop so a frame jumping tick 6 → 9 still fires the tick-8 decision. Verified: identicalsim_hashbetween--fixed-fps 60and--fixed-fps 5. - Three robot commanders.
rts-monkeyfuzzes MOVE/STOP against random selections;rts-scriptreplays a deterministic JSON scenario — reusing thetests/sim/scripts/*.jsonshape verbatim, so a scenario committed for the C++ determinism test doubles as a playtest scenario unmodified;rts-replayre-issues a recorded run's commands at their exact sim ticks. Documented fidelity caveat: traces record{t, cmd, n, q, r}, not per-unit handles, so replay re-selects the first n living units — deterministic, but not byte-identical to the source run's hash. A grace-period bug was caught pre-ship: the first draft compared a sim-tick value against a physics-tick quantity and would have cut runs off before their own last command executed. - Abilities and menus (
174fead):CommandType::ABILITY(role field = ability id, wire format unchanged), a static v1 table —0surge self-buff,1leap hex teleport — with per-unit cooldowns hashed and serialized atSNAPSHOT_VERSION 2, executed in fixed tick order (commands → ability timers → movement → spatial). The pause menu freezes ticks viaset_running(false)with the sim hash unchanged across pause/resume. Golden hashes legitimately moved — the hash stream now covers ability state. New values recorded on GCC13 and reproduced on GCC11; MSVC agreement still needs confirming on the next Windows run to re-establish the cross-compiler determinism proof. - Playtest infrastructure: a trace-corpus regression suite now
asserts an opt-in
rtsexpectation block —sim_hashas an exact determinism anchor (it holds where float physics would drift) plus tick/unit/command counts. Also a performance oracle gating on regressions, a frontier exploration mode biasing bots toward never-visited cells, GlitchTip tagging so bot crashes dedupe apart from players', and a headless-Chromium web soak. - Build and CI fixes. The weekly soak wasn't exercising the game at
all:
main.tscnhosts aSimServermatch with noPlayerBall, so the swarm's ball-avatar default failedno_playerafter 5s — CI now drivesrts-monkey. The web export shipped a thread-enabled GDExtension against a thread-disabled engine (GODOT_SCONS_EXTRAwas never forwarded tobuild_gdextension()), producing amismatch in shared state of memoryat runtime; the library map also still pointed at the pre-nothreadsfilename. The 2026-07-20 release train built all four platforms and then died atgit tag -awith Committer identity unknown — the ephemeral job container has no git identity — now backported from the platformer'sci/release.shalong with tag auto-publish viaRELEASE_PUSH_TOKEN.
Green this week: 64/64 sim tests on GCC13 and GCC11, 73/73 tooling tests.