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
Check · Review · Land · Live done — Landed and live in production.
scout/cs:scout/187-a-bot-reloads-only-when-its-magazine-is → main 1 commit fork
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 | ||
| 728 | 728 | off its path (`worthTheDetour` in `plan.ts` is the whole arithmetic) but never |
| 729 | 729 | crosses the map, the site, or the bomb carrier's own plan for one. |
| 730 | 730 | |
| 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 | ||
| 731 | 746 | The clock is part of it, and both sides read it. `plan.ts` holds the two |
| 732 | 747 | numbers that say how late is late, and nothing else in the code decides it. |
| 733 | 748 | `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", () => { | ||
| 88 | 88 | expect(immediate.fire).toBe(false); |
| 89 | 89 | }); |
| 90 | 90 | |
| 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 | ||
| 91 | 133 | // --- hearing -------------------------------------------------------------- |
| 92 | 134 | |
| 93 | 135 | /** |
src/client/bots.ts +39 −4
| @@ -255,6 +255,42 @@ function hunting(world: World, bot: Player, brain: BotBrain): boolean { | ||
| 255 | 255 | */ |
| 256 | 256 | const HUNT_SPREAD = 700; |
| 257 | 257 | |
| 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 | ||
| 258 | 294 | /** A death this far from a site centre is a fight over that site. */ |
| 259 | 295 | const CONTACT_RANGE = 1600; |
| 260 | 296 | |
| @@ -460,8 +496,7 @@ export function scriptedFight( | ||
| 460 | 496 | turnTowards(bot, { yaw: aim.yaw + wobble, pitch: aim.pitch + wobble * 0.4 }, brain.aimSpeed, dt); |
| 461 | 497 | const ready = world.time - brain.seenAt > brain.reaction; |
| 462 | 498 | 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); | |
| 465 | 500 | if (ready && onTarget && world.time > brain.restUntil) { |
| 466 | 501 | input.fire = true; |
| 467 | 502 | 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 { | ||
| 635 | 670 | if (!brain.useLearned) return withBomb(world, bot, scriptedFight(world, bot, brain, target, input, dt)); |
| 636 | 671 | const intent = think(world, bot, brain, target, dt); |
| 637 | 672 | 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); | |
| 640 | 674 | // Two things the policy does not get to decide. A bot that has only just |
| 641 | 675 | // seen you does not shoot yet — that beat is what skill buys. And the |
| 642 | 676 | // 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 { | ||
| 663 | 697 | |
| 664 | 698 | // --- moving |
| 665 | 699 | repath(world, bot, brain); |
| 700 | topUp(bot, input); | |
| 666 | 701 | const off = follow(bot, brain, input); |
| 667 | 702 | if (off !== null) { |
| 668 | 703 | const look = brain.suspect !== null ? aimAt(bot, brain.suspect) : { yaw: bot.yaw + off, pitch: 0 }; |