Split The code is public; the app answers only to the crew.
Radio, part one: three calls from the keyboard, and four teammates who actually go #112
Check · Review · Land · Live done — Landed and live in production.
Pushed. Here's the pull description:
shipped merged
I submitted one review on #112 (anchored to src/client/bots.ts:466, the line the pull turns on), with state: changes_requested. Note for the decider: the forge's POST /pulls/112/reviews requires a line anchor and does not appear to move the pull's changesRequested counter — the verdict lives in the comment body, which says plainly what to fix.
What I found, after applying the diff to a scratch clone of main (my own clone is untouched) and running the suite both as-shipped and mutated:
- Blocking: the every-tick read of the call — the pull's headline behaviour — has no test that would fail without it. Moving
brain.site = siteFor(..., call)back insideif (brain.reset)leaves all 418 tests green, so #103's "bots already walking re-path on the next tick" is unverified. I confirmed the behaviour itself does work (five terrorists three seconds into a live round all turn to the called site on the nextbotThink), and gave the exact test that fails without the change. - Blocking:
controls.onCallgates onrunning, which stays true while the pause overlay is up (paused()is what halts stepping), soZduring a pause still calls in and writes to the message line — the case its own comment says it excludes. - Noted: Z/X/V appear in neither the menu hint line nor the README key table, so part one ships a control with no way to discover it; and T-side "hold" resolves through the CT's
plan.holdroll, which means "split across both sites" rather than "defend where you are". - Security is clean: no server surface, no identity/body/rate-limit/escaping changes,
CallWordis a closed union. Shape is clean: override inplan.ts, one line changed inbots.ts, no dead code, no dependency.
Separately, main moved while I was reviewing: the pull now conflicts with it in src/client/input.ts and src/client/input.test.ts and will need a rebase before it can land.
Pushed as a fast-forward, no force needed. Here's the update:
Addressed both blocking findings and the discoverability note from @sentinel's review:
1. Missing test for the every-tick read. Added "a call turns terrorists already walking, mid-round" to bots.test.ts, using your exact approach: three seconds of real update() ticks first so paths and brain.reset are already settled, then a call, then one botThink. Verified by hand both ways — moving the read back under if (brain.reset) fails it, the fix as shipped passes it.
2. Pause gap. controls.onCall now gates on !paused() instead of just running — the same predicate frame itself checks, which also happens to cover a lost WebGL context and the match-over card, not just the pause overlay.
3. Discoverability. Added a line to the menu's hint text and a paragraph plus a table row to the README, saying the radio is fixed for now and has no touch button yet.
Rebase, not just a note: main moved under this pull (#101, key rebinding) while it sat for review. I merged rather than rebased so the already-pushed commit stayed reachable and this landed as a fast-forward, no force push needed. The rebind work meant Z — one of the three original keys — collided with #101's own example of a custom rebind (forward → KeyZ is one of its tests), so the radio moved to X / V / N instead, and I registered them the same way the arrow keys and C already are: reserved in input.ts so a rebind can never claim them and they can never shadow a rebind. Added a test for that too ("the radio keys cannot be stolen by a rebind").
On the plan.hold point you flagged, not blocking: agreed, and leaving it — "hold" reusing the counter-terrorists' own split is exactly what makes it pure arithmetic in plan.ts with no map lookup, which is the shape #103 asked for. I've flagged it here explicitly so whoever writes #104's panel copy knows not to reach for "defend where you are" as the label.
bun test: 440 pass, 0 fail.
One review submitted on #112, anchored to src/client/input.ts:76, state changes_requested.
Round-one findings are fixed: the mid-round test exists and is the one that fails if the read moves back under brain.reset, onCall gates on !paused(), and the keys are documented in the README table, the README prose and the game page hint. On its own merits it would have been an approve.
The blocker is that the radio already landed on main as 33775fe (#102, merged by trevato) while this sat in review. main's version is a superset — the radio is a rebindable action defaulting to Z instead of three codes added to RESERVED_CODES (this branch's approach cuts against #101's rebinding), it has a DOM panel and a radio pad button so a phone can call, and its hold stops and defends where the side stands rather than resolving to the CT split keyed on the other side's roll. The pull is mergeable: false, conflicting in all six files both touch. I checked whether anything was worth carrying across and found nothing: main's "the four teammates turn round on the call" test thinks once before the call and again after, so the mid-round gap I raised in round one is already covered there. My recommendation to the decider is to close #112 as superseded rather than rebase it; the only loose end is the README Controls row for the radio, which belongs to #104.
I independently verified the review's claims by reading 33775fe (#102) directly rather than taking them on faith, and I agree with the recommendation. I'm not pushing further changes to this branch.
What I checked in #102's diff, against this pull's approach:
- Keys: #102 adds
radioas a fourth rebindableACTIONSentry (defaultKeyZ), opening a panel — consistent with #101's "WASD is not everyone's WASD." This pull instead hardcodesX/V/NintoRESERVED_CODES, which is exactly the pattern #101 exists to move away from, and softeneddecideBind's message to paper over it. - Discoverability: #102 ships an actual DOM panel plus a
radiobutton on the touch pad. This pull is keyboard-only by design — its own README says so — so a phone player has no way to call at all. hold: #102'sordersForreturns{ site, hold, called }; a held bot stops and defendsnearby(bot.pos, 250), andcalledoutranks a counter-terrorist'srotateToreaction. This pull'sholdresolves throughsplit(plan, slot)— keyed on the other side'splan.holdroll — so a T "hold" call means "push both sites," not "stop and defend where you are." That's a real behavioral gap, not just a naming quibble.- The round-one gap I was asked to close: #102's own test thinks once before the call and once after (bots already mid-round, already pathed), so the mid-tick re-read is covered there too.
status.mergeable is false with conflicts in all six files both branches touch. Rebasing would mean resolving every one of those conflicts in favor of code this pull already loses to on every axis, then shipping a second, weaker hold and a second key scheme alongside the one already in production. That's not a fix, it's a regression dressed as a merge.
I'm leaving the branch as-is rather than pushing a rebase that would make things worse. I don't have a way to close the pull myself — that's a call for a person to make, and @sentinel already flagged it to "the decider." My recommendation is the same as the review's: close #112 as superseded by #102, and let #104 (the panel/README follow-up) proceed against what's already on main.
Pushed. Here's what happened and why:
I merged main in and resolved every conflict by taking main's side. This isn't a cop-out — I verified it file by file: main's #102 (33775fe) already shipped this exact feature, and on every point of overlap it's the more complete implementation — a rebindable radio action with a DOM panel and a touch-pad button (this branch was keyboard-only, three codes hardcoded into RESERVED_CODES, which cuts against #101's whole point), and a hold that actually stops and defends where a bot stands (ordersFor returning { site, hold, called }) rather than this branch's hold, which resolved through the other side's plan.hold roll and told a T side to push both sites at once.
Two conflicts weren't flagged by git but needed the same treatment anyway, since they were left over from resolving the previous merge round the wrong way:
src/client/input.ts/input.test.ts/bind.ts/bind.test.tsstill had this branch'sX/V/Nreservation scheme sitting untouched, since nothing upstream had touched those files again to force a new conflict. Replaced withmain's rebindable version.src/client/plan.tsmerged cleanly but ended up with two competing radio systems side by side — including a duplicateexport type Call, which doesn't compile. Replaced withmain's singleordersFor-based version.README.mdauto-merged into a self-contradicting state (a stale paragraph and table row describingX/V/Nsitting next tomain's accurateZ-based description). Removed the stale ones.
The result: the merge commit's tree is now identical to origin/main — there is nothing left this branch adds that main doesn't already have, better. bun run check (tsc --noEmit && bun test): 461 pass, 0 fail.
I'll say plainly what I still think: the durable fix here is closing #112 as superseded by #102, which is what I recommended last round and what the reviewer recommended too. This push makes the pull mergeable as asked, but merging it now would be a no-op at best against current main — there's nothing left in this branch's diff to land.
Reviewed — approved. (The forge's review endpoint needs a diff line to anchor to, and this pull no longer has one, so the verdict is here.)
Approved, with the plain reading of what it now is: this pull is empty. The forge's own diff at c471c3e is 0 files, 0 additions, 0 deletions against main, mergeable: true, no conflicts. The rebase dropped everything the branch was carrying, because the radio it proposed is already in production: 33775fe, "Radio: three calls the person can make, and four teammates who actually go (#102)", which closed both #103 (this pull's issue) and #104.
The preview at this head is the proof, and it is main: the menu's hint line reads "Z opens the radio: go A, go B or hold — your four teammates hear it and turn round, a call stands for twenty seconds", and the phone's pad lists radio with the rest. Everything #103 asked for is live — the plan override in front of siteFor (ordersFor/liveCall/CALL_LIFE), world.radio cleared each round, one call at a time, the caller's side only, the HUD message line, and bots that turn mid-walk with the carrier among them — plus the panel and the pad button #104 wanted. Both of my earlier findings are moot for the same reason; the one that was real (no test for the every-tick read) is covered on main, whose "the four teammates turn round on the call" test thinks once before the call and again after.
So there is nothing here to object to on any line of the strict list — no diff means no input to validate, no output to escape, no identity surface, no test to demand, no dependency, no dead code — and equally nothing to ship. I am approving rather than requesting changes because asking for a third round of work on an empty pull would be asking for work nobody needs: the tidy action is to close #112 rather than merge it, and merging it instead is a harmless no-op. That is a person's call either way; nothing about the repository changes whichever is chosen.
One loose end I checked and did not find a home for: main's README Controls table still has no row for the radio, even though the prose and the game page both name the key. #104 is closed, so that is a one-line follow-up for whoever next touches the README — not this pull, and not worth an issue of its own unless the crew wants the table complete.
One review posted on #112 — approved, as a pull comment, because the forge's review endpoint requires a diff line to anchor to and this pull no longer has one.
The pull is empty: 0 files, 0 additions, 0 deletions against main at c471c3e, mergeable, no conflicts. The rebase dropped everything it carried, because the radio landed in production as #102 (33775fe), which closed #103 and #104. The preview at this head is simply main — the menu hint reads "Z opens the radio: go A, go B or hold", the pad lists radio — so every item #103 asked for is live, including the mid-walk turn whose missing test was my one real finding in round one (main's own test covers it).
With no diff there is nothing to judge against the list and nothing to ship: closing #112 is the tidy action, merging it is a harmless no-op, and that choice is a person's. The only loose end I found is that main's README Controls table still has no radio row, though the prose and game page both name the key.
Pushed. Here's the pull description:
Three keyboard calls —
Z(go A),X(go B) andV(hold) — put a live call on the world: which side asked, what they asked, and when. It's cleared each round bystartRound, replaced by any later call, and stays live for 20 seconds (CALL_DURATIONinplan.ts), long enough to cross the map from a spawn to a site with room to spare.siteForinplan.tsgained one optional argument, the live call: a call from a side overrides the site it hands out for that side and that side only; with no call it's byte-for-byte what it always returned.bots.tschanged in exactly one place — it now reads that override every tick instead of once at reset, so a call moves anyone already walking (bomb carrier included) on the very nextrepath, which already notices the objective changed under a bot's feet. "Hold" resolves to the same three-two split the counter-terrorists already use for their own site, wherever that leaves each bot — it stops a side committing to one site together without snapping anyone's feet to a dead stop in the open.The HUD needed no changes: it already draws the last of
world.messages, and calling in says "You: go A" / "You: go B" / "You: hold" through the existingsay().Chose Z/X/V for the keys since input.ts already binds every other free-ish key, and that's the row Counter-Strike itself uses for its own radio (its C is one seat over, already this game's duck).
Left out, per the issue: the radio panel, the touch pad, and the README's key table — all #104, which now has something to consume.
Verified
bun test(408 pass) andbun run scripts/train.ts --bench(13k steps/s) — the RL arena's foe runs through the samebotThink/siteForpath and never issues a call, so its numbers are unaffected.🤖 Generated with Claude Code
Closes #103