EXP-17 · 2026-08-30
Teach the bots to Glidekeep
EXP-16's own conclusion, and the owner's next task: every remaining objective target is bounded by travel. Skerry's stands are 540 m apart and its scripted middle Glide line runs stand to stand in 13.5 s (peak 54.2 m/s, never under 49.7 on the floor, tests/arena_lines.gd); bots crossed the same ground at 10-15 m/s, so a full flag cycle cost 45-70 s and a 240 s match held four of them.
- Hypothesis
- the gap is not the wave rules, it is that the bots walk a polyline instead of playing the terrain — so give them a movement layer that reads the ground ahead along their own velocity and a route layer that prices a line by the speed it can be skied at, and the pickups and cap attempts follow. Constraint (design law): bots use the same
mach_movement.gd, the same energy budget and the same resist curves as a player, so a bot that beats the scripted line is a bug, not a result. ### What changed (bot_brain.gd, bot_nav.gd, harnesses only; movement, world, net, frames untouched) 1. A Glide layer in bot_brain.gd (replaces the EXP-8/14/16 glide and burn rules). It decides the two keys from the terrain the frame is actually on, the way arena_lines.gd's scripted skier does: - *Hold the glide.* Above 12 m/s the Glide is held whatever the ground does. Releasing it puts traction on the next contact and brakes back toward the run cap, which gives back every metre of height the line just bought. EXP-16 already held a glide above 14 m/s, but only while the velocity stayed within 25 degrees of the wanted heading — on rolling ground with a route waypoint every 16 m that test failed constantly and every hop threw the speed away. The heading test is now 70 degrees: past that no glide can take the corner and the bot runs it. Below 12 m/s the EXP-8 rule still stands (descents along the fall line only), which is what stops a bot sliding off a traverse. - *Read the ground ahead along the velocity, not along the route.* _climb_ahead() is arena_lines' test, ported: sample the ground 8, 16, 24 and 32 m ahead along the horizontal velocity and compare it with where the ballistic flight will be. A rise the flight would land on is flown over instead of landed on. This is the one place the A* node list is not consulted: a glide has no steering, so where the frame is going is not where the route says to go. - *Leave the lip with the tank shut.* EXP-16 had a rule "crest: launch off the drop — burn". It is gone. Thrust at a lip floats the frame over the descent and gives back the height it came for; arena_lines never does it. A new descending test (the ground ahead drops, or the frame is falling, and nothing is rising ahead) shuts off the flats burst, the pursuit burn and the air-sustain burn. - *One burn ladder instead of six overlapping rules.* Each reason to jet now names both the tank it needs to start (burn_start) and the tank it runs down to (burn_floor): a wall climb waits for 75 % of the tank and burns to 4; a climb the line is about to hit starts at 10 and burns to 4; a steering correction (the lateral part of the move key vectors the thrust, EXP-11) starts at 25 and stops at 12; the flats burst starts at 30 and stops at 12. So a climb is never starved by a burst that emptied the tank on the flats, and a burst never degenerates into the trickle that pinned EXP-14's carriers at 12 energy. - *A leash that grows with the speed.* The open-field route lookahead was a flat 40 m — 0.9 s of warning at 45 m/s. It is now 1.4 x the speed, 40-90 m, so a gliding bot is not steered corner to corner. The arrival brake is speed-scaled the same way (a Glide has no brakes; 25 m at 45 m/s was half a second). - *Line choice inside the corridor* (_ski_line): of five headings within +-23 degrees of the route heading, take the one that gives up the most height over the next 24-60 m, and only if it is worth 11 m of drop. Kept because removing it costs 11 s on the Skerry carry run below; made aggressive (a 3 m or 7 m threshold) it costs 2-8 s, because it pulls the frame off the route into troughs it then has to climb out of. Choosing the *line* is mostly the A*'s job, which is the next change. 2. bot_nav.gd prices an edge in seconds, not metres. The old cost was distance x (1 + uphill) + distance x 2 x steepness + 400 m per wall, with a distance heuristic. On Skerry that priced the launch faces — 30-degree slopes, the one feature the whole map is built on — as a 2x sidehill penalty and routed around them. Now every edge costs d / v where v is the speed a gliding frame can hold on that slope (45 m/s down the fall line, 26 rolling, 10 on a runnable climb, 5 on a face that has to be flown), and the sidehill penalty only counts the part of the slope the edge runs across (|grad x edge|): sliding down the fall line is the line, sliding across it puts the frame on the floor. A wall is 15 s. The heuristic is the straight line at 45 m/s, which stays admissible, and MAX_ITERS went 12000 → 40000 to pay for the weaker heuristic (routes are cached and shared, so the tick cost is unchanged). 3. Harnesses. tests/bot_ski.gd (new) is the head-to-head with the scripted line: --carry puts one bot on the *enemy* stand with no opposition at all, it takes the flag and runs home, and the run is timed on arena_lines' convention (from more than 15 m off the stand it leaves to within 25 m of the stand it is going to, at roof height). Without --carry it runs four bots of one team through their real roles on an empty map. tests/bot_match.gd gained a speed block: the distribution of horizontal speed in the open field (more than 90 m from either stand) and the stand-to-stand crossings each bot completes alive. Both are pure observation; the match harness stays deterministic. ### Speed first (the thing the pass is for) A. One bot, no opposition, stand to stand (tests/bot_ski.gd --carry, 240 s, deterministic): the bot is put on the enemy stand, takes the flag and runs home. Timed on arena_lines.gd's convention, so the two rows are the same measurement of the same span. The scripted human line on Skerry is 13.5 s with a 54.2 m/s peak. Skerry: 1.94x the scripted line → 1.69x, peak 0.74x → 0.87x of it. The bot is still slower than the human line and never beats it, which is the invariant this pass had to keep. Tallow's single carry trajectory is 1.4 s slower while its speed distribution moves a long way (p90 26.9 → 51.3): the carry there is 925 m through a valley and the time goes at the two base ends, not in the middle. B. In a 16-bot match (tests/bot_match.gd, 240 s, seeds 1-6, per side; "open field" = more than 90 m from either stand; a "crossing" is one bot getting alive from one stand's 90 m circle to the other's, 360 m on Skerry and 745 m on Tallow): Over 12 sides per map: Skerry median 14.8 → 16.0 m/s, mean peak 48.0 → 48.6, crossings completed 79 → 134 (+70 %); Tallow median 17.8 → 18.6 m/s, mean peak 59.8 → 65.1, crossings 53 → 89 (+68 %). The p90 column is where the change lives: Skerry 24-28 → 29-33, Tallow 24-27 → 40-52. ### Objectives (bot_match, 16 bots, 240 s, seeds 1-6 both maps) Totals, 12 sides per map: Tick cost 1.70-2.74 ms with 16 brains (before 1.43-1.95); the machine was heavily loaded during the after runs, so treat the upper end as noise -- the extra work per think is four height samples for _climb_ahead plus the line-choice probe. combat_check 0 failures on Skerry and Tallow; standoff_check 0 failures on both; arena_lines 0 failures and unchanged (13.5 s, peak 54.2, floor 49.7 -- the Glide layer does not touch it); a 60 s 16-bot Tallow server replay is deterministic (1919 ticks, 59 checksums); bot_match --map=skerry --seed=2 --log twice gives an identical log apart from the wall-clock ms/tick line. ### Result, against the targets set for this pass - Cross-map speed, isolated: target met. Skerry median 30.6 m/s (target >= 25) and peak 47.3 (target >= 45) on the one-bot carry run, at 1.69x the scripted 13.5 s line (target: within about 2x, never faster). Before: 22.4 / 40.1 and 1.94x. - Cross-map speed, in a match: not met on the median. 14.4-18.7 m/s (before 13.6-18.9), p90 29-33 on Skerry and 40-52 on Tallow, peak 45-53. The match median is not the same quantity as the median of a bot that is only travelling: it includes the fights, the ramp approaches, the run-in and the roof patrol, all slow by design. The honest travel numbers there are the crossing count and time, and those move a lot (79 → 134 crossings on Skerry; seed 1 side 1 median 37.7 → 23.0 s). - Cap attempts: target met on Skerry, close on Tallow. Skerry 9 → 18 over 12 sides (0-2 per side per 240 s against a target of 3), Tallow 8 → 13. Caps 11 → 18 over the 24 sides. Carriers survive: 25 carrier deaths → 15 on Skerry, 24 → 11 on Tallow, and six sides finish a match without losing a carrier at all. - Pickups: not met, and Tallow went down. Skerry 37 → 37, Tallow 40 → 30 (target 6 per side per 240 s, i.e. 72). The reason is in the same table: mean carry time roughly doubles (Skerry 28 → 51 s, Tallow 52 → 60 s with two sides over 80 s) because carriers survive. A flag being carried cannot be picked up, so a livelier flag path with longer carries produces *fewer* grabs and more attempts and caps. Pickups per 240 s is the wrong meter for what this pass changed; attempts and caps are the ones that moved. - Handoffs: 2 → 3 over 24 sides, still rare for EXP-16's reason (the killer stands on the flag). ### What was tried and dropped, with the number - *A local line picker with real authority.* Bending up to 23 degrees toward the biggest drop ahead helps only while it is nearly inert (an 11 m threshold; removing it entirely costs 11 s on the Skerry carry run, 22.8 → 34.1 s). Lowering the threshold to 7 m costs 8 s and to 3 m costs 2 s: the bend pulls the frame off the route into troughs it then has to climb out of. Kept at 11 m; choosing the line properly is the A*'s job. - *A turn threshold that widens with speed* (32 degrees at 14 m/s easing to 78 at 40, on the argument that braking to turn is dearer the faster you go): Skerry carry 22.8 → 27.0 s, Tallow 51.8 → 58.3. Dropped; the flat 70 degrees is better. - *"Below the run cap on flat ground, run instead of Glide"* (a Glide has no propulsion either, so it hops in place): right in principle, measured worse -- Skerry 22.8 → 24.4 s, Tallow 51.8 → 58.3 s. Dropped. - *Keeping the Glide up the home ramp* when the carrier is on the ramp axis at speed: Skerry 22.8 → 30.1 s. Dropped; the ramp block still brakes to a run, which is what an 8 m ramp beside a 12 m wall wants.
- Decision
- keep. Travel went where it was supposed to (isolated crossing 1.94x → 1.69x the human line, in-match crossings +70 %), cap attempts double on Skerry and caps rise from 11 to 18 over 24 sides, and no check regressed. Next, in the order the numbers argue for: (1) the two base ends, which are now most of a carry -- leaving the enemy ring costs 6-8 s at 7-21 m/s (the exit gap points across the way home and the frame has to re-accelerate off the roof onto a flat pad) and the home ramp costs another 4 s of deliberate braking; both are ring/route geometry, not the movement layer. (2) The run-in's damage budget, still EXP-16's second wall: a large share of carrier deaths are 1-10 s after the grab. (3) Pickups will not move until carries get shorter or the flag returns faster, which is a rules question rather than a bot one.
63 lines of internal measurement detail omitted from this entry.
EXP-16 · 2026-08-30
Carrier survival, hunt arrival and wave overlap (Skerry Yard and Tallow)keep
Next from EXP-15: carrier survival through the gate, the hunt's form timeout scaled by travel, and waves that overlap so pickups stop serialising. Measured on Skerry Yard (540 m stands, the default arena) as well as Tallow (920 m). Targets: >= 6 pickups and >= 3 cap attempts per side per 240 s on both maps, seeds 1-3, at least one handoff, no regression in time-to-first-chaser. ### What changed (bot_team.gd, bot_brain.gd, harness tests/bot_match.gd; movement, world, net, frames untouched) - Climb reserve. EXP-14 banked 0.7 of the tank for the home climb anywhere inside 450 m of home at 1.3 energy per metre of lift. On a 540 m arena that is the whole carry: Skerry's roof is 54-62 m above the floor, so the carrier reserved two thirds of its tank from the moment of the grab, never burst on the flats and crossed at 9-12 m/s (traced: 60 s for 540 m against the map's 13.5 s Glide line). Now the reserve is the last 150 m only and is capped at a third of the tank; further out a climb burns down to 20 energy instead of 8, so the tank is never pinned at 10. - Keep a glide that is already fast. With the jump key released a ground contact applies traction and brakes back toward the run cap (~11 m/s), so every jet burst was thrown away at the next touch. Above 14 m/s the bot now holds the glide while its velocity stays within ~25 degrees of the heading it wants (the EXP-8 "descents only" rule still governs slower speeds: it is what stops a bot sliding off a traverse). The flats burst also starts at 32 energy instead of 40 and counts a descent as flat. - Egress by threat. One way home (plus a "back door" that differed only in the last 100 m) became five: front, back door, a middle line and one route through each flank (a via point 0.32 of the base-to-base distance off the stand-to-stand line - the Skerry side crests). escape_pick() scores every candidate over its whole length (EXP-10 looked at the last 250 m, the part they all share) at 260 m of detour per known enemy within 80 m of the route, and the carrier re-picks every 2 s with a 150 m margin instead of once at 300 m from home. The run out of the enemy ring bends toward home after 55 m instead of running 110 m straight out on the exit axis (on Skerry that axis points north while home is due west: a 100 m detour across the pad the defenders overlook). - Escort. Lead 18 m / trail 10 m (was 25 / 15: 40 m of gap for a chaser to drop into, and out of reach of the flag when the carrier went down). The body-block stands 16 m from the carrier on the line to the nearest known enemy within 150 m (was 20 m, 120 m). - Handoff (new). When our carrier goes down with the flag, the flag lies where it fell and whoever touches it first owns it (MachWorld._step_flags: an enemy touch is a grab, an owner touch a return), so the killer standing over the body returns it inside about a second. The coordinator picks the nearest teammate within 80 m and gives it a 3 s window in mode handoff: straight at the flag, no route, no fighting, jets on. Logged both ways - team N handoff: carrier X down, bot Y goes for the flag Z m away and handoff done - and the harness marks the pickup HANDOFF (t s after the drop) and counts a handoffs column. - Hunt arrival. The form timeout is now the farthest hunter's travel time (distance / 14 m/s), clamped to 12-30 s, instead of a flat 12 s from the first arrival, and the push also goes as soon as every hunter is there. A hunter that dies while the hunt is forming keeps an attacking slot at its next deal, so it respawns back into the hunt (heading for the enemy stand) instead of being dealt a defender/chaser slot and walking to the home rally. - Wave overlap. A wave now forms as soon as our carrier is past midfield, not only when the flag is back home, and presses the enemy gap until it lands (its clock does not start until the flag is grabbable). An attacker counts as ready anywhere within 260 m of the enemy stand, not only inside the 30 m rally circle 90 m off the route: on Skerry seed 1 the flag was home at 154 s and the wave only launched at 168 s, when the last two attackers finally touched the rally. - Two stalls found by tracing, both fixed in the bot layer: - *A flag dropped on a bunker roof was unreachable.* The 16 m A* grid never sees the 8 m ramp, so every bot below the roof pushed at the bunker wall. Skerry seed 1: flag 2 lay on team 2's own roof from 155 s to 200 s - the whole 45 s return timer - with six team-2 bots in mode return 30 m away and team 1's regrabber doing the same from the other side. Return, regrab, handoff and a runner going for a dropped flag now approach it through the ring gate (ramp foot → gate → flag), the same way a grab does. - *The run-in ignored walls.* The axis-tracking correction was the one direction never wall-checked, and a roof ramp is 10 m wide with a 3 m step at its side: a runner arriving 5 m off the axis is pushed into it. Skerry seed 2, bot 1012 stood at (227, 5) with hs 0.0 for 40 s of a 240 s match, in mode grab the whole time. The tracking now goes through _avoid_walls, and a run-in stopped for 1.5 s holds phase 1 for 3 s (flipping the phase for one tick put it straight back on the wall). ### Results (16 bots, 240 s; "before" is main @2275d72 under the same harness) Seed-to-seed spread is larger than the change, so the same seeds 1-6 were run on both maps for the totals (24 sides per row): Tick cost 1.6-2.7 ms with 16 brains (harness only, unchanged). combat_check 0 failures on Skerry and Tallow; standoff_check 0 failures on both; a 60 s 16-bot Tallow server replay is deterministic (1919 ticks, 59 checksums); bot_match --map=skerry --seed=2 --log twice gives an identical log. ###
- Result
- the two headline targets are not met. Measured, plainly: - Pickups 1-5 per side per 240 s (target 6). Best case 5, typical 3-4; the 24-side totals rise from 68 to 77 (+13 %). The gate is travel time, not the wave rules. Skerry's stands are 540 m apart and its Glide line is 13.5 s (40 m/s); bots cross at 10-15 m/s even after the reserve and glide fixes, so a full cycle (respawn or cap at home → cross → line up → run in → grab) is 45-70 s and a 240 s match holds four of them. Traced examples: spawn to the rally 34 s for 390 m; a wave runner 33 s in mode
grab from the launch to the flag; a carry of 540 m in 46-64 s. Six pickups a side needs a ~30 s cycle, i.e. ~20 m/s sustained, which is a bot gliding pass of its own (glide/jet cadence over the dune field and the launch faces), not a coordination rule. - Cap attempts 0-2 per side (target 3), and they went down on Skerry (16 → 9 over 12 sides) while caps went up (4 → 7). Both moved for the same reason: before this pass a carrier that reached its own stand usually could not cap, because its own flag was lying unreachable on a roof for the full 45 s timer, so it parked there and was counted as an attempt (the before runs show carrier deaths at 0.5, 22 and 45 m from home). With the dropped flag reachable again, our flag comes home, arrivals cap instead of parking, and fewer carries are counted twice. Caps per 24 sides: 7 → 11. - Carrier survival at the gate is still the second wall. Carriers now die a mean 113-926 m from home instead of 22-780, but a large share of the deaths are 1-10 s after the grab, at the enemy stand, to the stand defender plus the chaser: a Strider that has taken 60-90 % damage on the run-in dies to two Impulse hits, and the review's order leaves aim alone. The escort body-block and the tighter lead/trail help the handoff, not this. - Handoffs happen and are logged, but they are rare (2 in 12 matches): the window is about a second because the killer is standing on the body. Skerry seed 4 is the clean one - t6975 team 1 handoff: carrier 1012 down, bot 1000 goes for the flag 19 m away, then t7014 (224 s) team 1 pickup by 1004 HANDOFF (1.2 s after the drop). - Hunt arrival improved where a hunt formed: hunt goes up the ramps with 2 of 2 (formed 6 s, farthest 89 m) instead of EXP-15's "1-2 of 4". Standoffs are rare on Skerry (1 in 12 matches), so this is measured on few samples. - Time to first chaser is unchanged: 0.0-0.1 s on every run, both maps, before and after.
- Decision
- keep. The flag path is livelier (pickups +13 %, caps +57 % over 24 sides), two stalls that each cost 40-45 s of a 240 s match are gone, the handoff exists and is logged, and the hunt forms with the group instead of without it. Next, in the order the numbers argue for: (1) a bot gliding pass - glide and jet cadence over rolling ground, measured against
tests/arena_lines.gd's 13.5 s Skerry middle line, because every remaining target is bounded by 10-15 m/s travel; (2) the run-in's damage budget (arrive at the stand above 50 % health, or grab from the side the defenders are not on), because that is what kills carriers 1-10 s after the grab.
18 lines of internal measurement detail omitted from this entry.
EXP-15 · 2026-08-30
Named bot roles and the standoff return rulekeep
Owner's pick after EXP-14: fix the mutual-carrier standoff at the design level (option a, a return timer for a flag held at home) and give the bots explicit roles.
- Hypothesis
- with a 45 s ceiling on a standoff the flag path never stalls for the rest of the match, and a full-wave push onto the parked carrier ends most standoffs before the timer does; named roles make the roster readable (scoreboard, harness) and put the right frame on the right job. ### Roles (
bot_team.gd ROLES / ROSTER) Roster by team size (dealt in the order defender, chaser, runner, escort, floater; sizes between two rows take the smaller row plus floaters): 4 = 1 runner, 1 chaser, 1 defender, 1 floater; 6 = 1 / 1 / 1 / 2 defenders / 1; 8 = 1 runner, 2 escorts, 1 chaser, 2 defenders, 2 floaters; 12 = 2 / 3 / 1 / 3 / 3; 16 = 2 / 4 / 2 / 4 / 4. A bot is re-dealt at every spawn: the first free slot, preferring one whose frame it already wears. HUNTER is an overlay on runner / escort / floater, not a slot. Frames: bots never chose a frame before (the server's roster gave every fourth bot a heavy frame and the coordinator made the heavies defenders). Now the brain sends the role's frame as the "frame" input; it applies in the first-spawn window (the bot stands still on the ground for 1 s, the same spawn choice a player has) or at a friendly inventory station (a bot in the wrong frame for its role refits at a station inside 100 m, unless the enemy carrier is out). The wave picks a RUNNER-role bot as runner when it is within 60 m of the nearest attacker, a floater as breaker, and the ESCORT-role bots fill the two carry escort slots. The server roster (net_server.roster(), the reliable roster RPC) carries a role string per bot (the hunter overlay when active, "" for players); the snapshot codec is untouched. ### Standoff rule (MachWorld.standoff_return, default on; --no-standoff on the server) A flag held within STANDOFF_HOME_M (30 m) of the captor's own stand for STANDOFF_RETURN_MS (45 s, the dropped-flag timer) while the captor's own flag is away returns home: the carrier loses it, event standoff_return flag N from ID, then the usual flag N returned. The timer resets when the carrier leaves the 30 m or its own flag comes home. Consequence, by construction: the first timer to expire hands the *other* carrier (parked at its own stand with its flag now home) the cap on the next tick, so the side that got home second scores; a standoff lasts at most 45 s after the first arrival. tests/standoff_check.gd covers the timer, the reset, the cap and the option off. ### Results (16 bots, 240 s; "before" is 2275d72's brains and rules under the same harness) (The EXP-14 table's Tallow 1 row, 2 / 2 pickups, does not reproduce at 2275d72 under this harness; the before rows here are re-measured at 2275d72.) Tick cost 1.5-2.4 ms with 16 brains. combat_check 0 failures gives the same log; standoff_check 0 failures. What the standoff rule changed: in the runs where a standoff formed it ended. Tallow 1 before is the EXP-14 picture (both carriers home by 135 s, one pickup per side, nothing for the last 100 s); Tallow 5 after is the rule at work: both carriers home at 140 / 145 s, hunts up both ramps at 157 and 168 s (each went with 1-2 of 4: the hunters arrive over 30 s from up to 900 m away and the 12 s form timeout starts with the first), flag 2 taken from the team 1 carrier at 185 s, team 2 caps on the same tick, pickups follow at 188 and 208 s. A standoff now costs at most 45 s of flag path instead of the rest of the match, and the hunt did not once kill a parked carrier inside that window (the carrier plus two defenders on the roof against 1-2 hunters at a time).
- Result
- targets still not met (Tallow pickups 2-5 per side, attempts 0-1; target 6 and 3). Seeds 1-3 never reach a standoff any more: every carrier of the first waves dies 430-780 m from home to the chaser and the stand defender (the same deaths as EXP-14's second finding), so the rule has nothing to break and the numbers are the run-in / carry survival numbers of EXP-14, which this pass did not touch (aim untouched by the review's order). The seed-to-seed spread (2-5 pickups, 0-1 attempts on both sides of the change) is larger than the change; nothing here is a regression the harness can see, and the frame swaps cost 1 s at match start (4 refits per team in the spawn window) and no station trips in these runs.
- Decision
- keep (the roles are the vocabulary the next passes need, the rule bounds the worst case). Next: carrier survival (arrive at the gate with 40+ energy, an escort that actually meets the chaser), then the hunt's form timeout scaled by the hunters' travel time.
21 lines of internal measurement detail omitted from this entry.
EXP-14 · 2026-08-30
Objective gravity: no rally wait, press the stand, regrab, hunt the standoffkeep
Design review verdict on the bot match: ~1 cap per 4 min is not enough for a solo playtest; wanted >= 3 contested cap attempts per side per match with escorts on the carrier and a chaser on the human carrier within ~2 s, in the order objective gravity > escort/chaser > strip pressure > spawn/wave cadence > readable air rules, aim untouched.
- Hypothesis
- the waves idle (wait for a full wave at the rally, wait 30 s for the "soft" defence, all go home with the carrier, idle at the rally while our carrier is out) and every idle is a lost grab; take the idles out and pickups and attempts follow. Harness (new,
tests/bot_match.gd): two teams of 8 bots (the server's roster and frames), 240 s at 32 ms ticks, deterministic (world seed + brain seeds; two runs of a seed print the same event log). Per team: flag pickups, cap attempts (the carrier got within 30 m of its own stand), caps, kills, deaths, flag returns, waves, stand approaches, time from an enemy pickup to the first defender in chase / intercept / block mode and to the first one within 40 m of the carrier, carrier deaths (mean distance from home) and mean carry time. --log prints the flag/cap events with the carrier's killer and the coordinators' wave log, --verbose every bot's mode every 5 s, --trace=ID one bot per second. godot --headless --path game -s res://tests/bot_match.gd -- --map=tallow --seed=1 [--log] [--verbose] [--trace=ID]. Implementation (bot_team.gd, bot_brain.gd, bot_nav.gd; movement, world, net, frames untouched): - Waves launch with whoever is ready: two at the rally go at once, one goes alone after 5 s; late arrivals join the running wave as escorts; the runner's hold for a "soft" defence is 8 s (was 30); a wave times out at 40 s (was 60). Ready = at the rally or already pressing the enemy stand, and the runner is the ready attacker nearest the stand. - Press: while our carrier is out, attackers not escorting it stand off the enemy stand's entry gap (spot, shell, draw the defenders) instead of waiting at the rally, so the flag is grabbed the tick it comes back (seen: a pickup on the cap tick). Regrab: the enemy flag lying in the field is the nearest attacker's, no wave (in practice the killer returns it within a second, so this rarely fires). - Escort: the wave's two escorts (breaker first) stay assigned for the whole carry (one leads by 25 m, one trails by 15 m; nobody else joins), released when the carrier is home. The shelf defender no longer pursues beyond 100 m: the carrier meets the chaser and the stand defender, not three. - Standoff (both carriers at home, neither can cap): attackers within 300 m of the enemy carrier hunt it; a carrier on its own stand is reached like a grab (ramp, gate) after gathering at the line-up point (3 there or 12 s), the rest keep pressing where the flag will return. - Movement fixes found on the way, each worth more than the wave rules: (1) the flats jet rule wanted the ground under the bot, but a glide hops on every contact, so attackers crossed the Tallow floor at 15 m/s on a full tank: now burn on the flats above 40 energy down to 25 (22 m/s target). (2) The run-in hunted 12 s between phase 1 and 2 on a 4 m cone at the line-up point (cone widened to 3 m + 5 %, commit inside 40 m stays 4 m), and a runner pinned on the wall beside the ramp burned a trickle at 5 energy for 40 s (burn starts above 15; 1.5 s stopped on the ground backs out to phase 1). (3) The team's rally route starts on our own roof and its first waypoints cross the ramp's side: a bot spawned behind bunker 1 joined it across the wall and zigzagged there for two minutes (seed 2 bot 1012, 48-173 s). Team routes are joined only when the walk to the waypoint is wall-free, two stuck episodes inside 20 s drop the route for 30 s, wall avoidance keeps its side (the first open candidate alternated sides every think), and the A* prices walls on every edge, not only climbing ones (an edge across a 10 m outbuilding that lands back on the pad had no net rise and no cost). (4) Carriers burst (start above 30, down to 8) and coast instead of trickling at 12 energy, and keep a reserve for the climb home (1.3 energy per metre of lift inside 450 m). Results (16 bots, 240 s; "before" is 2275d72's brains under the same harness): First pickup on Tallow: 91 / 93 s before, 59 / 62 s after (rally at 41-44 s, was 43-52; line-up to grab ~15 s, was 26). Tick cost 1.5-1.9 ms with 16 brains (harness only). combat_check 0 failures on the same log.
- Result
- targets not met. Tallow pickups 2-4 per side (target 6), cap attempts 0-1 (target 3); the Proving Ground gets 4-5 pickups and 1-3 attempts. The time-to-first-chaser target holds everywhere (a defender at the stand flips to chase on the grab tick; one is within 40 m inside 0-7 s). What still caps the numbers, from the traces: - The standoff. Both first waves grab at ~60 s; both carriers get home at 115-140 s (55-75 s carries on a 920 m map, the last 300 m a climb) and then neither can cap. Killing a carrier on its own roof behind two defenders takes the hunt 20-120 s (seed 1: 60 s to 177 s, the whole middle of the match with one pickup per side). Every gate on pickups sits behind this: the flag is only grabbable when it is home. Nothing in the bot layer resolves it cheaply; it wants a design answer (a carrier that must keep moving, a return timer for a flag held at home, or a map with shorter carries). - Carriers that do not reach the standoff die 6-24 s after the grab to the chaser and the stand defender pursuing by air at 40-70 m (both as fast as a Strider carrier, which arrives at the gate with 15-20 energy after the run-in), or to the enemy attackers staged on the route home. Two escorts do not stop that without better shooting, which this experiment leaves alone. - A regrab is never available: the killer stands on the flag and returns it inside a second.
- Decision
- keep (the idle removals and the movement/route fixes are strictly better: earlier first grab, more waves, no two-minute spawn trap, deterministic harness for the next pass). Next: the standoff rule at the design level, then re-measure; a carrier energy budget through the gate (arrive with 40+); a carrier route that skirts the enemy rally. ### EXP-14 follow-up · owner note Targets (≥6 pickups, ≥3 attempts per side per 4 min on Tallow) not met; chaser-within-2-s met. The blocker is the mutual-carrier standoff (both flags out, neither cappable). Design options for the next pass, owner to pick: (a) held-flag return timer — a flag carried onto its captor's stand while their own flag is out returns after N s unless the carrier keeps moving; (b) shorter Tallow carries (base ring gap / stand placement); (c) bots commit a full wave to the enemy carrier on its stand instead of a 2-man ramp push. Solo human play is not affected in the same way (the human breaks the standoff), so the TITAN feel test can proceed.
11 lines of internal measurement detail omitted from this entry.
EXP-13 · 2026-08-30
Frame identity: Strider lateral, slower Ranger/Bulwark, bigger Bulwark life pool
Chris after the arena build ("so much better"): Strider still needs a bit more lateral; Ranger and Bulwark should be slower, Bulwark the slowest with a large life pool. Changes (frames.json, reference → new): Strider lateralJetControl 1.10→1.20, floor 0.75→0.80; Ranger run 12→11 m/s, jet-forward cap 25→22, jet horizontal share 0.5→0.45, lateral 0.72→0.65 (floor 0.5), maxDamage 1.1→1.2; Bulwark run 7.5→6.5 (back/side 5→4.5), jet-forward cap 20→17, share 0.3→0.25, lateral 0.48→0.40 (floor 0.3), maxDamage 1.32→1.70. Reference traces for the Strider unchanged. ### EXP-13 addendum · 2026-08-30 · Ranger lateral 0.65 → 0.75 (floor 0.5 → 0.55) Design review (research/2026-08-30-review3-grok.md Q2): the EXP-13 spacing put Ranger too close to Bulwark on air authority — "mid" read as a slow brick rather than the all-rounder. Strider 1.2 and Bulwark 0.4 kept. Validate on Proving Ground with the three role tests in docs/REVIEW_2026-08-30_reconciled.md. ## TODO · 2026-08-30 · Wall resolution vs the Glide hop cycle (review 3, code lane 4) Not changed yet. MachWorld.resolve_ground (mach_world.gd ~269-291) classifies walls from the current foot penetration, the previous footing and a steepness heuristic, which can turn a high-speed step contact into a hard stop; it is brittle against the 32 ms hop cycle and the 8 m / 2 m height layers. Planned experiment: sweep horizontally against the heightfield + structure layer in substeps, separate "step up" (<= ~1 m ledge) from "wall slide", and add regression cases for a shallow ramp, a 1 m ledge, a 2 m bunker lip and a high-speed side scrape to combat_check. Deferred because it changes movement feel; do it as its own EXP with traces.
EXP-12 · 2026-08-29
Faster weapon switch + instant switch feedback
Feedback (Chris, same playtest): "switching weapons isn't intuitive, I don't know what weapon I have until I shoot, weapon changing isn't instant or smooth". Two causes: the client polled the 1-5 keys from the 32 ms tick, so at 60 fps about half the presses landed on a frame without a tick and were dropped (fixed: keys are handled per event); and the reference draw (Activate 0.5 s, plus Preactivate on the launcher) with nothing on screen until the new weapon fired.
- Hypothesis
- a 0.28-0.35 s input-to-fire-ready commit with visible progress reads as instant without making the switch free.
- Implementation
- per-weapon
switchTimeScale in tuning/weapons.json (one tunable, 0.6 for all eight; tools/build_tuning.py writes it), applied in MachWorld._image_set_state to the Activate/Preactivate states only; a fire press in the last 50 ms of the draw is buffered into the ready frame (FIRE_BUFFER_MS); re-switching mid-draw restarts the draw on the new slot (where the image allows a change; the Vector cannon's Activate does not, so that one queues). Client: weapon bar (slots 1-5, ammo, pack on R), the requested slot with a progress arc that tracks the replicated img_delay_ms, pulsing while the sim refuses (Fire/Reload), the view model lowers on the request and comes up with the draw, commit and ready cues (tools/gen_sfx.py), wheel cycles and Q is the last weapon. Telemetry (combat_check, before → after): Vector cannon from the launcher with the trigger held, shots in 1.5 s: 3 → 5 (switch 0.5 → 0.3 s, spinup 0.5 s, 150 ms cadence); the spindown re-press check in the same window 2 → 1 shots (pure phase shift: the last burst ends 2 ticks later, spindown 1 s + spinup 0.5 s complete at tick 98 of 102); grenade launcher request → first shot 12 ticks = 384 ms (was ~20 = 640 ms); a tap 120 ms early is dropped, a tap inside the last 50 ms fires on the ready frame; the Fire/Reload switch lock and everything else unchanged (0 failures on both maps).
- Decision
- ship for the next playtest with the bar; lock
switchTimeScale once the bar has been played (candidates 0.5-0.7), then it stops being an experiment. Also in this change (loadout confusion, "I don't know how to change frame"): the L overlay with the three frames, blurbs and both presets; the choice rides in every input and applies at a station (1 s bar + REFIT flash) or inside the first-spawn window (SPAWN_GRACE_MS 6 s, frame strip at the top, default Strider; respawns keep the last applied loadout); station beam markers + a compass marker with the distance.
EXP-10 · 2026-08-29
Rally off the route, the way home by known enemies, two escorts, side intercepts, air fightingkeep
- Hypothesis
- EXP-9's carriers died in the enemy wave staged on their own approach; move the rallies off the routes, let carriers pick the way home by what the team knows, give them two escorts and side-intercepting chasers, and bots cap. Then (owner feedback from a live match) bots must fight and chase in the air and while gliding. Implementation (
bot_brain.gd, bot_team.gd; main's EXP-11 / EXP-11b lateral jet control merged in before measuring): - Rally 90 m off the attack route, on the side with terrain cover from the enemy stand (LOS test from 2 m above it), off the stand-to-stand line, on flat ground (both rallies come out "covered" on both maps); the rally route is its own A*. - Two ways home per team (front: entry side; back door: exit side, where 75 m out is not a flank); the carrier picks at the grab and again inside 300 m of home by the known enemies (marked for us or within 150 m of a live teammate) within 80 m of the last 250 m of each way. Carriers burn the descents to 40 m/s. - Waves of four (breaker, runner, two escorts); an escort with a known enemy inside 120 m of the carrier stands 20 m out between them (one each flank of that line), shooting. - Chasers and (now) both defenders intercept 35 m off the carrier's route on their own side, ahead of it, never inside 60 m of their own rally; pursuit, interception and escort burn below 24 m/s. - Fighting: with an enemy inside 60 m a bot never holds: it strafes across the enemy's line (side swapped every 0.7 s, downhill side preferred), keeps the objective's pull, closes outside 35 m and opens inside, and jets the strafe (lateral jet control). Design-review rules: (1) lead is the intercept from target velocity and flight time (as before), aimed a touch high-side when both are airborne, and a burn for a small altitude edge over an airborne enemy; (2) energy as a state machine: engage above 40 % of the tank in 0.3 s bursts, panic below 15 % (jets off, glide for speed, no fire) until back above 40 %; (3) disengage on geometry: panic energy, the enemy 6 m+ higher and closing at 5 m/s while we are under 40 %, or a second enemy inside 60 m; exit downhill on a glide out of their cone, no fire. - Tick cost: the terrain LOS test per target is cached 5 ticks, and a bot thinks every other tick (repeating its input on the off tick) except in the precision moments (run-in, inside a ring, the Matches (16 bots, 240 s, four in parallel). EXP-9 final = the "before" row. the ring) and 2/2 on Tallow. Time to the rally 45-73 s. are in range (28 / 27; 12.3 / 11.2 m/s against the 12 m/s target, one seed short); the cap targets still fails: pursuing by air (10-35 m away): the air pursuit works, on both sides, and a Strider that has just burned out of the ring (energy 9-12) has nothing left for the duel. The carrier that got clear capped after a 94 s trip. - Tallow: all waves grab and clear (4/4 per match) but only one carrier per match gets up its home ramp; the others still meet the enemy wave at 60-80 m (the 90 m rally offset sits inside the 100 m fire range) or run out of energy on the pad blend. Kills stay under 12 because the teams meet only there and at the stands; combat speed is dragged down by the defenders holding on the stand (25-31 % of the combat samples are under 4 m/s, almost all of them stand and shelf defenders with an attacker at 40-60 m).
- Decision
- keep (caps from 0 to 1 per match everywhere, tick cost under target, no regressions). Next: the carrier's energy budget through the gate (arrive with 40+ and take the exit on a glide, not a burn) so it can duel the pursuit; a defender who moves on the stand instead of holding a point; the rally 120+ m off the route on Tallow.
16 lines of internal measurement detail omitted from this entry.
EXP-11b · 2026-08-29
Lateral share numbers from the design review
Grok: keep the per-frame lateral lever, no free air-strafe; Strider 1.10 / Ranger 0.72 / Bulwark 0.48; shape by speed (full below 35 m/s, easing to a floor of 0.75 / 0.55 / 0.35 at 75 m/s); ticks 0–2 off the ground ×0.4 to stop hop wobble; lateral thrust costs the normal jet energy. Applied as lateralJetControl / lateralJetFloor. A/B for the next playtest: (A) these numbers, (B) Strider 1.0 with a 10% lateral energy discount.
EXP-11 · 2026-08-29
Lateral jet control per frame (first deliberate departure from the reference)
Feedback (Chris, after winning a match on Tallow): a Strider needs too much effort to move left/right with the jets; heavier frames may stay stiff.
- Hypothesis
- the reference vectoring (horizontal share tapered by forward speed, vertical-only for 9 ticks after leaving the ground) gives no lateral authority at speed.
- Implementation
- the part of the move input perpendicular to the velocity gets the frame's horizontal share ×
lateralJetControl (Strider 1.0, Ranger 0.7, Bulwark 0.5) from the 3rd tick off the ground and is not tapered by forward speed; the forward part is unchanged. Tests: movement_acceptance M7 lateral cases; reference traces unchanged (jet_forward 2.35, Glide 1.73, run_up 0.81 — forward-only inputs).
- Decision
- ship for the next playtest; tune the three numbers on feel.
EXP-9 · 2026-08-29
Grab at speed, glide only along the fall line, the way home by the rampkeep
- Hypothesis
- EXP-8's two diagnosed causes (the glide rule slides bots off every traverse; the runner arrives on foot and spends 5 s at 5-10 m from two defenders) are what stops caps; fix them, then make the defenders beatable, and bots cap. Implementation (
bot_brain.gd, bot_nav.gd, bot_team.gd, new harness tests/grab_check.gd; mach_world.gd, mach_movement.gd, the map generator and the ring defenders + 1 chaser + 5 attackers): - Glide (jump held) only when the slope under the bot runs along the direction of travel and down (gradient from normal_at, dot with the move direction > 0.6); flats as before; never on a traverse or a climb (run + burn with a 20/6 reserve). A glide has no steering at all (no traction, no air control without jets), so it is also released whenever the velocity is more than ~25 degrees off the wanted heading. Routes price sidehill cells (bot_nav.gd: +2x by the steepness measured over +-24 m), so they keep ~24 m inside bench edges. - Grab at speed: the runner lines up on the entry axis 60 m out (Tallow: 25 m past the ramp foot, 79 m), then the run-in owns the controls: along the axis with jet-vectored lateral tracking (run key when > 2.5 m off, > 1 m inside 35 m), glide keeps the speed, jets pulse below 22 m/s and lift straight through at speed; 90 degrees apart (Tallow) run via a turn point 3 m out on the exit axis. Committed inside 40 m (no retreat to the line-up point). Exit gate, 110 m out, then home. - Waves: the runner goes when the wave launches (no breaker stage; it never got to the Tallow ring in 40 s), holds only while >= 2 healthy enemies are within 25 m of the stand (30 s at most); a wave launches only for a free flag; the wave's escort leads the carrier by 25 m. - Found on the way, each worth more than the headline changes: (1) bots impulse-jumped themselves to 26 % health (a self-impulse is 0.5 x the frame's 1.5 Impulse scale = 0.74) so every attacker arrived at the stand one hit from death: off. (2) Nearest-waypoint pursuit steered bots back to the waypoint behind them on every slope (breakers hunted between two route points on the pad blend for 40 s): the route index is monotonic now. (3) The 16 m A* never sees an 8 m ramp: a route home ended against the 12 m bunker wall; the escape route now ends line-up (20 m before the foot) → foot → gate → stand with damped axis tracking, and comes in by the exit-side ramp when the ground 75 m out there is not a flank (Tallow team 2). (4) Rally points on a slope (both Tallow rallies sat on the bar's flank; attackers slid 30-66 m to the floor) are now scored by steepness. (5) Carriers crawled the Tallow floor at 12 m/s on a full tank, saving it for a home climb that is a runnable ramp: they burn below 24 m/s the whole way. (6) A 55 degree step read as a wall; the pad blend is runnable: 63 degrees. (7) Stand defender chases only inside 40 m; the shelf one never. Harness (godot --headless --path game -s res://tests/grab_check.gd -- --map=M --team=T [--defenders=2] [--runs=N] [--trace]: one attacker dropped at its rally with the wave already running, traced to the After: Matches (16 bots, 240 s, --seed=N, four in parallel; "before" is EXP-8's final brain on this branch): Tallow 55-77 s (target 60 s; Tallow seed 2's team 1 spawns furthest from its rally). at the end; Tallow 0), and Tallow kills (7-9) are below 20; grabs and clears are up a lot (Tallow: go through the ring in 1-2 s and mostly unhurt). What still fails, from the carry endings in the telemetry (x,y,z are Godot): - Tallow: every carrier dies 90-170 m from home, at 5-25 m/s, with 2-3 enemy attackers 13-60 m away: the enemy's rally point is 120-180 m from our stand on the attack route, which is our way home, so the carrier runs into the next enemy wave staging on our own pad approach (seed 1: bot 1003 died 88 m from home with 1006/1008/1012 at 49-63 m; bot 1010 113 m from home with three at 21-37 m). The exit-side route home (75 m back door) does not clear that group on team 1's side, where the exit ramp faces a 45 degree flank. The escort leads by 25 m but one Strider does not stop three. home) and the stand defender still gets its one point-blank shot at the gate (3-4 "runner died" per match, always inside 2 s of the grab). - Kills on Tallow stay under 10: the teams only meet at the stands and at the rally-side approach. above the 4 ms target; the defenders' 120 m scans and the per-tick normal_at for the glide rule are the additions since EXP-8.
- Decision
- keep (grabs, clears, route and ramp fixes are strictly better and
combat_check holds); caps stay open. Next: put the rally 60-80 m off the attack route (it is the enemy carrier's route home), route a carrier around a staged enemy group, and give the carrier a two-escort wave; on
28 lines of internal measurement detail omitted from this entry.
EXP-8 · 2026-08-29
Bot team coordinator: waves through the ring (Spot → Close → Soften → Secure)keep
- Hypothesis
- EXP-7's solo grabbers die to two defenders within seconds; a team that spots, closes together, softens the defence and secures the exit should cap. Implementation (
bot_team.gd new, bot_brain.gd): - Coordinator per team (one object per world/team, updated once per tick by the first bot that thinks). Roles are dealt at every spawn from live teammates only: 2 defenders (heavy frames first), 1 chaser, the rest attackers. A rally point is the attack route's waypoint 120–180 m short of the enemy stand. - Waves. When ≥ 2 attackers are at the rally (30 m) with > 60 % energy and > 60 % health (after 10 s, whoever is there; a third attacker is waited for up to 15 s as escort) a wave launches: the healthiest is the runner, the next one with impulse rounds the breaker, the rest escorts. Breaker: stands off the ring's entry gap at 16 m, strafes, holds the spot key ("spot": true, marks the nearest defender for the team; marked enemies count 30 m nearer in everyone's target choice) and shells the ring. Runner: closes along the route to 100 m, holds until 2 s after the breaker is inside 50 m (or dead), then enters gate → centre → flag and leaves through the far gap; escorts trail the runner by 20 m; once the flag is up, every attacker within 200 m escorts the carrier. Between waves attackers regroup at the rally; an attacker with an empty launcher walks back to a station. Waves end on a death, a grab that clears 120 m, or 40 s. - Ring geometry from terrain.struct_meta["rings"] (Godot vectors): entry gap = the gap nearest the enemy bearing, exit = the other, shelf = the tower lip. Tallow bunkers add ramp feet (probed along the gap axes: a smooth drop of > 10 m within 80 m) so the way up is ramp foot → gate → centre. Canonical team routes: attack_path (stand → stand, its tail replaced by foot/gate/centre), rally_path (up to the rally), escape_path (centre → exit gap → 70 m out → A* home); a bot that strays > 100 m rejoins the route at its nearest waypoint instead of re-planning from a snapped origin. - Defenders: one on the stand (patrol inside the ring, facing the entry gap), the second on the shelf; both engage at 120 m (fire inside 100 m) and pre-aim at marked enemies they cannot see; the stand defender chases a carrier only inside 80 m. Chaser: v3 route-ahead intercept, and body-blocks the exit gap while an intruder is inside the ring and the chaser is within 60 m of home. - Nav fixes found on the way: route goals other than the stands were snapped to a 160 m grid, so bots parked on a snapped route end (a roof, a pocket) 80 m from their target (_route(..., exact_goal)); walls are now a step of > 2.2 m within 1.5 m, so Tallow's 45° flanks are climbed (runnable, jet-assisted) instead of waited out as walls; arrival at a ring gate is on foot (the 6 m gap cannot be taken at 26 m/s airborne). Telemetry (16 bots, 240 s, --server --bots=16 --duration=240 --seed=N --telemetry; the "4x" rows ran four matches in parallel on one machine, which inflates the tick times by ~0.5 ms): attackers at the rally with full tanks), the breaker reaches the entry gap and holds it for 10–30 s, the runner enters on foot through gate → centre, grabs, and leaves through the far gap; the escort trails it. Grabs per match (a grab is about three flag events) are 1–4, as in v3, waves carry spot marks (58–118 mark events per match, was 0 from bots), kills sit at 28–37 (target 30–80: met), and the tick cost is 3.3–3.9 ms single-run (v3: 3.25; the target of < 3.5 is marginal, the extra is the defenders' 120 m target scans and per-tick mark lookups; the coordinator itself is a few tens of microseconds).
- Result
- acceptance for caps (≥ 3) is not met on either map. Where it fails: defenders (
kill <runner> by <defender> at the stand in every wave of both seeds). The ring is 8 m across with 6 m gaps; a runner on foot spends ~5 s at 5–10 m from a defender who kills a Strider in two impulse hits. Softening does not happen: the breaker at the gap trades with defenders that have 1.1–1.32 damage frames and the shelf, so the "defence soft" go condition almost always falls back to the 12 s timer. The one carrier that got 80 m out (seed 2, t5759) was killed by a defender firing at range, which is the posture the task asked for. Two defenders in a ring beat any wave of Striders that must enter on foot; the next lever is not coordination but the ring's geometry or the defenders' frames/ammo (an experiment on base_ring.py: wider gaps or a lower wall so the runner can take the gap at speed). - Tallow: attackers reach the rally in 50–200 s, so 1–4 waves per match. The route from spawn to the enemy bench is ~950 m with two 45° flanks; bots that fall off the route re-plan from a snapped origin and A* with a 6x slope penalty sends them 900 m along the valley floor instead of 100 m up the flank (bot 1007, t3s2: within 36 m of the rally, then a 950 m loop east). The rally now follows the canonical attack route and strays rejoin it, which cut the loops but the flank climb itself still costs a tank of energy per attempt. Kills 14–18 (below 30): the teams rarely meet. for 76 s per match); no change since EXP-7.
- Decision
- keep the coordinator and the nav fixes (grabs and marks up, kills in range, no regressions in
combat_check on either map); caps remain open. Next experiment: ring geometry (EXP-9), then re-measure. ### EXP-8 follow-up · Tallow reachability (rings regenerated with 10 m gaps on main) Changes: A* uphill cost capped (bot_nav.gd: d * (1 + min(slope, 1.5) * 1.5), was 1 + 6*slope + 8 above 0.9, which priced a 100 m flank above a 900 m valley detour; the stand-to-stand route is now 1034 m for a 926 m straight line and runs along the bench); staged ramp approach (to the ramp foot, then straight up the 8 m axis to the gate, back to the foot when 5 m off the axis; 6 m route lookahead inside 110 m of a ramped stand); grab at speed when lined up with the entry axis (jets on, no on-foot arrival), carrier burst to 60 m out; flank climbs burn with a 20/6 energy reserve instead of waiting for a 75 % tank; the coordinator logs time-to-rally and approaches (BOT_TEAM_DEBUG=1). Measured (16 bots, 240 s, four parallel processes): Not met: the 60 s time-to-rally, and no approach at all. What the traces show (bot 1006, seed 1): the canonical route leaves the bench along x = -160 for 560 m; attackers Glide or jet off the bench edge onto the 45° flank within the first 15 s, slide to the floor 200–300 m east of the route, and every re-plan from the floor to the rally (exact goal) is a fresh A* that crosses the valley floor sideways (x from +177 to -164 and back) around the bar; a bot that climbs back onto the bench slides off again. The flank is runnable in isolation (a Strider walks 90 → 170 m in 8 s in MachMovement, see the flank probe), so the loss is steering: the Glide/glide policy holds the jump key whenever the ground ahead falls away, which on a 45° flank means no traction and no turning until the floor. Map parameter, if changed: the valley flank angle is set in tools/gen_tallow_reach.py by the bench band BENCH_A0, BENCH_A1 = 360.0, 400.0 against FLOOR_HALF = 200.0 (line 164: the flank is a smoothstep over BENCH_A0 - FLOOR_HALF = 160 m for an 88 m rise, so its mid-slope is ~45°). BENCH_A0 = 460 would make it a 260 m, ~27° flank that the glide rule tolerates (EXP-1/2 hills are ≤ 30°); the bases stay where they are because the bunker pads are placed by BASE_U/BASE_A, not by the band. The alternative without a map change is a bot-side "no Glide on slopes steeper than 35°" rule plus a route that stays 30 m inside the bench edge; neither was done in this pass.
18 lines of internal measurement detail omitted from this entry.
EXP-7 · 2026-08-29
Bot brain v3 (roles, routes, stand defence, carrier escape)keep
- Hypothesis
- the 8v8 bot match is dead because the server deals roles by global index while teams alternate (team 1 all cappers, team 2 all defenders/chasers), bots pile into building walls and tower pockets, defenders and chasers loiter at their spawn, and carriers die inside the enemy base. Implementation (
bot_brain.gd, new bot_nav.gd): roles re-dealt per team with heavier frames defending (3 cappers, 2 defenders, 2 chasers, 1 escort); A* with 4 m sub-sampled edges that prices building walls as detours, a binary-heap open list and a height cache (33/9/20/11 ms per base-to-base route, was 79/78/98/40); one route kept per bot, origins and moving goals snapped so 16 bots share cached routes; wall avoidance, height-aware waypoint choice, a progress-based stuck breaker, and a bounded "recharge, then climb" rule for unavoidable walls; defenders patrol the stand roof within 15 m facing the route's entry gap and return dropped flags; chasers intercept on the base-to-base route ahead of the carrier; escort trails the carrier, or leads the capper in at the enemy stand to draw the defenders' fire; refit at friendly stations (health < 50 % or no primary ammo, 60 s cooldown, only stations at the bot's level); carriers burn out through the back gap, save energy for the home climb, jet-cushion hard falls and jink under fire; cappers grab at speed; aim error 3 deg and 30 % lead misjudgement (the exact lead hit 41 % of Impulse Launcher shots). Telemetry (16 bots, 240 s, --server --bots=16 --duration=240 --telemetry): Intermediate v3 runs during tuning: 35 kills / 0 caps, 29 kills / 12 stand approaches, 23 kills / 1 cap, so kills sit at 18–35 by seed. Bot cost: brain 2.1 ms/tick of which nav 0.3 ms (was 8.3 / 6.1 before the route fixes). All 16 bots leave their spawn point inside 5 s in every run (min displacement 3–10 m; a defender that spawns at its own stand stays by design).
- Result
- loitering at spawn is gone and the roof defence, interception and refits work as specified, but the acceptance targets for caps (>= 3) and kills (30–80) are not met, and two bots still average under 8 m/s: - Caps. Both stands sit on a ground-level plateau between two 17 m blocks, open on two sides; the route enters through one gap and both defenders patrol within 15 m of the stand facing it. Cappers now reach the stand 10–19 times a match (was 1–2) but every grabber dies on the plateau within 1–9 s of the grab, at 0–10 m from a defender, having arrived with 60–90 % damage (Impulse hits, 0.385 each; two kill a Strider). A solo capper cannot survive two Vector Cannon at 10 m for the ~5 s a grab takes. The escort-leads tactic did not draw enough fire. Next: coordinated waves (cappers rally and enter together), or a defender posture that engages from range instead of point-blank. - Kills. Fewer point-blank executions of runners, more fire at range with 3 deg aim error; 18–35 by seed. - Speed. The Bulwark (7.5 m/s run cap) and Ranger defenders average 3–7 m/s: a heavy patrolling a 15 m circle cannot average 8 m/s, and one heavy chaser per match is still trapped in the pocket south of the team-2 tower (engine 56, 264) after spawning on the tower roof and falling in: its damaged frame recharges at ~0.3 energy/s, so it cannot jet out and the pocket's only exit is behind it. Next: move the tower spawn off the roof or pathfind the pocket exit at 8 m resolution.
- Decision
- keep v3 (strictly better traffic and no spawn loitering); caps need the coordination pass above.
5 lines of internal measurement detail omitted from this entry.
EXP-6 · 2026-08-29
Lag compensation for hitscan and meleekeep
- Implementation
- the server keeps 16 ticks (512 ms) of player positions; each input carries the server tick the client was rendering remote players at (render delay 4 ticks); Lance and Shock Lance rays test targets at that tick. Projectiles are not rewound (they are visible and lead-able).
- Test
- a target moving 15 m/s hit at its 190 ms-old position with rewind, missed without. Remote players are extrapolated up to 3 ticks along their last velocity when a snapshot is late; corrections above 20 m are classified as teleports (respawns), not prediction error.
- Decision
- keep; fairness cap = 512 ms.
EXP-5 · 2026-08-29
Redundant inputskeep
- Hypothesis
- residual error at 100–150 ms with 2–3% loss is lost input packets starving the queue.
- Implementation
- each input packet carries the last 4 [seq, input] pairs; the server queues unseen seqs.
- Telemetry
- 16p/32p at 100 ms + 2% loss + 20 ms jitter — movement p95 0.77 / 1.20 m, 38 rubber-bands/min → 0.000 m, 0.7/min.
- Decision
- keep. Bandwidth per peer measured 11 KB/s (16p) / 20 KB/s (32p), snapshots 830 B mean.
EXP-4 · 2026-08-29
Server input queue instead of latest-input-per-tickkeep
- Hypothesis
- prediction error under jitter comes from the server duplicating/dropping client inputs while the client re-simulates exactly one tick per input.
- Implementation
- per-client FIFO consumed one input per tick (6-tick cap, repeat last when starved); weapon state machine replicated in the per-recipient block and re-run for in-flight inputs during reconciliation.
- Telemetry
- bench cell 4 clients, 20 ms RTT, 5 ms jitter — movement correction p95 1.2 m / 67 rubberbands per min → 0.000 m / 0; local bot brawl mean 0.165 → 0.001 m.
- Decision
- keep. ADR-3 amended.
EXP-3 · 2026-08-28
Predicted weapon fire and self-impulsekeep
- Hypothesis
- impulse jump snap-back is the largest visible correction.
- Implementation
- local single-player world for forward prediction; impulses logged by input seq and re-applied on reconciliation.
- Telemetry
- bot brawl with periodic impulse jump, mean correction 11.4 cm → 7.2 cm.
- Decision
- keep.
EXP-2 · 2026-08-28
Ground-impact damage only after real airtimekeep
- Hypothesis
- hop-cycle landings on 8 m heightfield creases register as impacts.
- Implementation
- impact damage requires ≥ 8 ticks airborne before landing plus the 45 m/s normal-speed threshold.
- Result
- complaint gone in the same playtest.
- Decision
- keep; revisit when terrain collision becomes triangle-exact.
EXP-1 · 2026-08-28
Ground contact while climbingkeep
- Hypothesis
- forward drive felt weak on hills because contact was dropped when moving upward.
- Implementation
- contact = at surface and not moving away along the surface normal.
- Telemetry
- reference uphill run trace (
run_up): error 13.5 → 0.8 m/s; live playtest.
- Result
- "much better" (Chris).
- Decision
- keep.