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

A bot with an empty primary slot walks over the rifle on the floor, when it is on its way #227

Merged opened this

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

Merged

scout/cs:scout/216-a-bot-with-an-empty-primary-slot-walks-o → main 1 commit fork

  • Merged. scout/216-a-bot-with-an-empty-primary-slot-walks-o was merged into main as 56838f9.

Review

Sign in to review this change.
3 files changed +98 −8
README.md +4
@@ -709,6 +709,10 @@ the site the last of them died at — the kill feed and the radar, nothing they
709709 could not see. Rolling one site a side rather than one a bot took the plant
710710 rate from 38% to 65% over twenty seeded `SHORT` matches when it landed.
711711
712 A bot with an empty primary slot now detours to a dropped rifle a few steps
713 off its path (`worthTheDetour` in `plan.ts` is the whole arithmetic) but never
714 crosses the map, the site, or the bomb carrier's own plan for one.
715
712716 The clock is part of it, and both sides read it. `plan.ts` holds the two
713717 numbers that say how late is late, and nothing else in the code decides it.
714718 `PUSH_WINDOW` (12 seconds) is the fighting the terrorists can still afford on
src/client/bots.test.ts +46 −1
@@ -1,6 +1,6 @@
11 import { expect, test } from "bun:test";
22 import {
3 applyIntent, botThink, CARD, DIFFICULTIES, makeBrain, policy, restyleBrain, skillBand, think,
3 applyIntent, botThink, CARD, DIFFICULTIES, gunGoal, makeBrain, policy, restyleBrain, skillBand, think,
44 } from "./bots.ts";
55 import { DE_FORGE, placeAt, worldOf } from "./map.ts";
66 import { dist2d } from "./math.ts";
@@ -270,6 +270,51 @@ test("a bomb on the floor is still picked up by a living terrorist", () => {
270270 expect(bot.hasBomb).toBe(true);
271271 });
272272
273 // --- picking a gun up off the floor ----------------------------------------
274
275 test("a bot with an empty primary slot detours to a rifle a few steps off its path", () => {
276 const world = createWorld(RULES, "you", "t", 1, 4242);
277 world.match.phase = "live";
278 const bot = world.players.find((p) => p.team === "ct") as Player;
279 world.me.alive = false; // nothing for it to fight, nothing to flee
280 expect(bot.primary).toBeNull(); // an eco round: too little money to have bought one
281 world.pickups.push({
282 weapon: "m4a4", pos: [bot.pos[0] + 80, bot.pos[1], bot.pos[2] + 4], ammo: { mag: 30, reserve: 90 },
283 });
284 for (let t = 0; t < 64 * 6 && bot.primary === null; t++) update(world, 1 / 64, NO_INPUT);
285 expect(bot.primary).toBe("m4a4");
286 });
287
288 test("a bot already holding a primary never diverts for one on the floor", () => {
289 const world = createWorld(SHORT, "you", "ct", 1, 1);
290 const bot = world.players[1] as Player;
291 bot.primary = "ak47";
292 world.pickups.push({
293 weapon: "m4a4", pos: [bot.pos[0] + 10, bot.pos[1], bot.pos[2]], ammo: { mag: 30, reserve: 90 },
294 });
295 const goal: [number, number, number] = [123, 456, 0];
296 expect(gunGoal(world, bot, goal)).toBe(goal);
297 });
298
299 test("a rifle across the map is not worth crossing for", () => {
300 const world = createWorld(SHORT, "you", "ct", 1, 1);
301 const bot = world.players[1] as Player;
302 bot.primary = null;
303 bot.pos = [0, 0, 2];
304 world.pickups.push({ weapon: "ak47", pos: [4000, 4000, 2], ammo: { mag: 30, reserve: 90 } });
305 const goal: [number, number, number] = [100, 0, 2];
306 expect(gunGoal(world, bot, goal)).toBe(goal);
307 });
308
309 test("an empty pickups list leaves the goal it was given untouched", () => {
310 const world = createWorld(SHORT, "you", "ct", 1, 1);
311 const bot = world.players[1] as Player;
312 bot.primary = null;
313 const goal: [number, number, number] = [123, 456, 0];
314 expect(world.pickups.length).toBe(0);
315 expect(gunGoal(world, bot, goal)).toBe(goal); // same reference: nothing was computed
316 });
317
273318 // --- the radio ------------------------------------------------------------
274319
275320 /**
src/client/bots.ts +48 −7
@@ -16,14 +16,14 @@ import { fromOther, loudness, reported, type HeardName, type Maker } from "./hea
1616 import { angleDelta, clamp, dist, dist2d, rng, type Vec3 } from "./math.ts";
1717 import { findPath, siteAt, walkable, colOf, rowOf } from "./map.ts";
1818 import { observe, OBS_SIZE, type Memory } from "./observe.ts";
19 import { mustHunt, mustPush, ordersFor, type Site } from "./plan.ts";
19 import { mustHunt, mustPush, ordersFor, worthTheDetour, type Site } from "./plan.ts";
2020 import {
2121 forward, newActivations, sample, unpack, type Activations, type Policy,
2222 } from "./policy.ts";
2323 import { CARD, PACKED } from "./weights.ts";
2424 import { WEAPONS, type WeaponId } from "./weapons.ts";
2525 import type { Input, Player, World } from "./world.ts";
26 import { aimAt, bodyCentre, buy, callout, canSee, NO_INPUT } from "./world.ts";
26 import { aimAt, bodyCentre, buy, callout, canSee, NO_INPUT, pickupSlot } from "./world.ts";
2727
2828 export { CARD };
2929
@@ -290,6 +290,42 @@ function rotateTo(world: World, bot: Player): Site | null {
290290 return site;
291291 }
292292
293 /**
294 * `goal`, or a dropped primary if this bot has an empty primary slot and one
295 * is close enough to be worth the walk. `worthTheDetour` (plan.ts) is the
296 * whole arithmetic; this only finds the candidates and picks the cheapest.
297 * The empty floor — every tick of the training arena — costs one comparison.
298 */
299 export function gunGoal(world: World, bot: Player, goal: Vec3): Vec3 {
300 if (world.pickups.length === 0 || bot.primary !== null) return goal;
301 const toGoal = dist2d(bot.pos, goal);
302 let best: Vec3 | null = null;
303 let bestDetour = Infinity;
304 for (const pickup of world.pickups) {
305 if (pickupSlot(pickup.weapon) !== "primary") continue;
306 const toGun = dist2d(bot.pos, pickup.pos);
307 const gunToGoal = dist2d(pickup.pos, goal);
308 if (!worthTheDetour(toGun, gunToGoal, toGoal)) continue;
309 const detour = toGun + gunToGoal - toGoal;
310 if (detour < bestDetour) {
311 bestDetour = detour;
312 best = pickup.pos;
313 }
314 }
315 return best ?? goal;
316 }
317
318 /**
319 * How many dropped primaries are on the floor right now — the whole gun
320 * signature `repath` needs in its objective string: a rifle appearing or
321 * getting taken is a floor that changed, whatever else this bot is doing.
322 */
323 function primaryPickupCount(world: World): number {
324 let count = 0;
325 for (const pickup of world.pickups) if (pickupSlot(pickup.weapon) === "primary") count += 1;
326 return count;
327 }
328
293329 /** Where this bot is trying to be, given what the round is doing. */
294330 function chooseGoal(world: World, bot: Player, brain: BotBrain): Vec3 {
295331 const bomb = world.bomb;
@@ -304,9 +340,11 @@ function chooseGoal(world: World, bot: Player, brain: BotBrain): Vec3 {
304340 // Hold near the bomb rather than walk onto it.
305341 return nearby(world, brain, bomb.pos, 300);
306342 }
343 // The bomb carrier does not stop for a gun: walking in alone with a rifle
344 // costs the round more than walking in with a pistol on time.
307345 if (bot.hasBomb) return world.map.sites[brain.site];
308346 if (bomb.state === "dropped") return bomb.pos;
309 return nearby(world, brain, world.map.sites[brain.site], 200);
347 return gunGoal(world, bot, nearby(world, brain, world.map.sites[brain.site], 200));
310348 }
311349 if (bomb.state === "planted") return bomb.pos;
312350 // The contact rotation is this side reading the round for itself — and it
@@ -319,9 +357,9 @@ function chooseGoal(world: World, bot: Player, brain: BotBrain): Vec3 {
319357 // meets on the way. A death somewhere is better information than a guess,
320358 // so a contact still wins.
321359 if (contact === null && hunting(world, bot, brain)) {
322 return nearby(world, brain, world.map.muster.t, HUNT_SPREAD);
360 return gunGoal(world, bot, nearby(world, brain, world.map.muster.t, HUNT_SPREAD));
323361 }
324 return nearby(world, brain, world.map.sites[contact ?? brain.site], 250);
362 return gunGoal(world, bot, nearby(world, brain, world.map.sites[contact ?? brain.site], 250));
325363 }
326364
327365 const nearby = (world: World, brain: BotBrain, at: Vec3, spread: number): Vec3 => {
@@ -507,10 +545,13 @@ function repath(world: World, bot: Player, brain: BotBrain): void {
507545 // agree with the roll still has to be a new objective to turn it round.
508546 // The hunt is part of it too: a counter-terrorist that has just run out of
509547 // clock is walking to a goal its site does not name, and nothing else in
510 // this string would have changed to send it there.
548 // this string would have changed to send it there. The gun count only
549 // matters to a bot with an empty primary slot — one that already has a
550 // rifle never diverts, so the floor changing under it is not its news.
511551 const objective =
512552 `${world.bomb.state}:${brain.site}${brain.holding ? ":hold" : ""}` +
513 `${brain.ordered ? ":called" : ""}${hunting(world, bot, brain) ? ":hunt" : ""}`;
553 `${brain.ordered ? ":called" : ""}${hunting(world, bot, brain) ? ":hunt" : ""}` +
554 `${bot.primary === null ? `:guns${primaryPickupCount(world)}` : ""}`;
514555 const stale =
515556 brain.path.length === 0 || brain.step >= brain.path.length ||
516557 world.time > brain.repathAt || objective !== brain.plannedFor;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs