Split The code is public; the app answers only to the crew.

A bot reloads only when its magazine is empty, so it reloads in the middle of the fight it just started #231

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/cs:scout/187-a-bot-reloads-only-when-its-magazine-is → main 1 commit fork

  • Merged. scout/187-a-bot-reloads-only-when-its-magazine-is was merged into main as 0c4e051.

Review

Sign in to review this change.
3 files changed +96 −4
README.md +15
@@ -728,6 +728,21 @@ A bot with an empty primary slot now detours to a dropped rifle a few steps
728728 off its path (`worthTheDetour` in `plan.ts` is the whole arithmetic) but never
729729 crosses the map, the site, or the bomb carrier's own plan for one.
730730
731 A bot tops up its own magazine, too — but only the moment `pickTarget` finds
732 nobody to see, which is the same trace the fight already runs, so this can
733 never be the thing that starts a reload in front of someone. `TOP_UP_SHARE`
734 in `bots.ts` is the threshold: half a magazine, because a duel is decided in a
735 handful of shots and waiting for less would mean topping up mid-fight almost
736 as often as waiting for empty does. A bot that does run dry mid-fight draws
737 its sidearm instead of standing through a rifle's reload, if the sidearm
738 still has bullets in it — `outOfAmmo` in `bots.ts` — and only reloads once
739 that, too, is what is left in its hands. Neither of these reads the
740 difficulty: a full magazine going into a fight is worth the same whatever
741 skill is holding it. Measured headless over 30 seeded `SHORT` matches on
742 seed 41, before and after: the plant rate rises from 67% to 81%, and rounds
743 ending in an elimination fall from 26% to 12% — the fight a bot used to lose
744 standing still through its own reload.
745
731746 The clock is part of it, and both sides read it. `plan.ts` holds the two
732747 numbers that say how late is late, and nothing else in the code decides it.
733748 `PUSH_WINDOW` (12 seconds) is the fighting the terrorists can still afford on
src/client/bots.test.ts +42
@@ -88,6 +88,48 @@ test("a bot that has only just seen you does not shoot yet", () => {
8888 expect(immediate.fire).toBe(false);
8989 });
9090
91 // --- reloading --------------------------------------------------------------
92
93 test("a bot alone with a half-empty magazine tops it up", () => {
94 const { world, me, bot } = duel();
95 me.alive = false; // nobody to see, and nobody to reload in front of
96 bot.primary = "ak47";
97 bot.weapon = "ak47";
98 bot.ammo.ak47 = { mag: 10, reserve: 80 }; // under half of the AK's 30
99 const input = botThink(world, bot, 1 / 64);
100 expect(input.reload).toBe(true);
101 });
102
103 test("the same bot does not start that reload with an enemy in sight", () => {
104 const { world, bot } = duel(); // me is alive and visible, unlike above
105 bot.primary = "ak47";
106 bot.weapon = "ak47";
107 bot.ammo.ak47 = { mag: 10, reserve: 80 };
108 const input = botThink(world, bot, 1 / 64);
109 expect(input.reload).toBe(false);
110 });
111
112 test("a bot that runs dry in a fight draws its loaded sidearm instead of reloading", () => {
113 const { world, bot } = duel();
114 bot.primary = "ak47";
115 bot.weapon = "ak47";
116 bot.ammo.ak47 = { mag: 0, reserve: 80 };
117 const input = botThink(world, bot, 1 / 64);
118 expect(input.switchTo).toBe(bot.secondary);
119 expect(input.reload).toBe(false);
120 });
121
122 test("a bot with a full magazine asks for nothing", () => {
123 const { world, me, bot } = duel();
124 me.alive = false;
125 bot.primary = "ak47";
126 bot.weapon = "ak47";
127 bot.ammo.ak47 = { mag: 30, reserve: 80 };
128 const input = botThink(world, bot, 1 / 64);
129 expect(input.reload).toBe(false);
130 expect(input.switchTo).toBeNull();
131 });
132
91133 // --- hearing --------------------------------------------------------------
92134
93135 /**
src/client/bots.ts +39 −4
@@ -255,6 +255,42 @@ function hunting(world: World, bot: Player, brain: BotBrain): boolean {
255255 */
256256 const HUNT_SPREAD = 700;
257257
258 /**
259 * A magazine at this share of capacity or lower is worth topping up between
260 * fights. Half a mag is still a whole engagement's worth of bullets — a duel
261 * is won or lost in a handful of shots, not thirty — so waiting for anything
262 * lower would top up mid-fight almost as often as waiting for empty does; any
263 * higher and a bot interrupts a walk to reload a gun that did not need it.
264 */
265 const TOP_UP_SHARE = 0.5;
266
267 /**
268 * Ask for a reload while nobody is watching. Only ever called from the branch
269 * where `pickTarget` already found no enemy in sight, so this can never be
270 * the thing that starts a reload in front of someone — the one way this fix
271 * could have made a bot easier to kill rather than harder.
272 */
273 function topUp(bot: Player, input: Input): void {
274 const w = WEAPONS[bot.weapon];
275 const ammo = bot.ammo[bot.weapon];
276 if (ammo === undefined || w === undefined || w.magazine === 0) return;
277 if (ammo.mag <= w.magazine * TOP_UP_SHARE && ammo.reserve > 0) input.reload = true;
278 }
279
280 /**
281 * A magazine that hit zero mid-fight: draw the sidearm instead of standing
282 * still through a rifle's reload, if there is a loaded one to draw. Reloading
283 * is still what happens once the sidearm itself is the gun that ran dry —
284 * there is nothing left in the bot's hands to switch to.
285 */
286 function outOfAmmo(bot: Player, input: Input): void {
287 const ammo = bot.ammo[bot.weapon];
288 if (ammo === undefined || ammo.mag > 0) return;
289 const sidearm = bot.ammo[bot.secondary];
290 if (bot.weapon !== bot.secondary && sidearm !== undefined && sidearm.mag > 0) input.switchTo = bot.secondary;
291 else input.reload = true;
292 }
293
258294 /** A death this far from a site centre is a fight over that site. */
259295 const CONTACT_RANGE = 1600;
260296
@@ -460,8 +496,7 @@ export function scriptedFight(
460496 turnTowards(bot, { yaw: aim.yaw + wobble, pitch: aim.pitch + wobble * 0.4 }, brain.aimSpeed, dt);
461497 const ready = world.time - brain.seenAt > brain.reaction;
462498 const onTarget = Math.abs(angleDelta(bot.yaw, aim.yaw)) < 3 + brain.aimError;
463 const ammo = bot.ammo[bot.weapon];
464 if (ammo !== undefined && ammo.mag === 0) input.reload = true;
499 outOfAmmo(bot, input);
465500 if (ready && onTarget && world.time > brain.restUntil) {
466501 input.fire = true;
467502 if (brain.burstUntil < world.time) brain.burstUntil = world.time + 0.15 + brain.skill * 0.35;
@@ -635,8 +670,7 @@ export function botThink(world: World, bot: Player, dt: number): Input {
635670 if (!brain.useLearned) return withBomb(world, bot, scriptedFight(world, bot, brain, target, input, dt));
636671 const intent = think(world, bot, brain, target, dt);
637672 applyIntent(world, bot, brain, intent, dt);
638 const ammo = bot.ammo[bot.weapon];
639 if (ammo !== undefined && ammo.mag === 0) input.reload = true;
673 outOfAmmo(bot, input);
640674 // Two things the policy does not get to decide. A bot that has only just
641675 // seen you does not shoot yet — that beat is what skill buys. And the
642676 // policy trained with a rifle, so it holds the trigger down; on a pistol
@@ -663,6 +697,7 @@ export function botThink(world: World, bot: Player, dt: number): Input {
663697
664698 // --- moving
665699 repath(world, bot, brain);
700 topUp(bot, input);
666701 const off = follow(bot, brain, input);
667702 if (off !== null) {
668703 const look = brain.suspect !== null ? aimAt(bot, brain.suspect) : { yaw: bot.yaw + off, pitch: 0 };

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs