/** * The bot's aim (`engine/aim.ts`), once through a whole bot to prove the * reaction gate is actually wired to the trigger and the cone to the barrel. */ import { expect, test } from "bun:test"; import { acquire, crossRate, emptyTrack, REACTION_MAX, REACTION_MIN } from "./engine/aim.ts"; import { botThink } from "./bots.ts"; import { newMatch, RUN_SPEED } from "./match.ts"; import type { MatchState, Player } from "./state.ts"; const rolls = (...values: number[]) => { let at = 0; return () => values[at++ % values.length]!; }; const facing = (state: MatchState): { bot: Player; prey: Player } => { const bot = state.players.find((p) => p.side === "def")!; const prey = state.players.find((p) => p.side === "atk")!; // Nose to nose in the open, with nothing between them. bot.x = 1200; bot.y = 640; prey.x = 1200 + 200; prey.y = 640; bot.angle = 0; bot.input.angle = 0; bot.weapon = "vandal"; bot.ammo = 25; return { bot, prey }; }; const live = (): MatchState => { const state = newMatch(4242); state.phase = "live"; state.clock = 100; return state; }; test("a bot that has just seen somebody does not shoot for a reaction", () => { const state = live(); const { bot } = facing(state); let firstFire = -1; for (let i = 0; i < 30 && firstFire < 0; i++) { botThink(state, bot, 1 / 30); state.now += 1 / 30; if (bot.input.fire) firstFire = i / 30; } expect(firstFire).toBeGreaterThanOrEqual(REACTION_MIN); expect(firstFire).toBeLessThanOrEqual(REACTION_MAX + 0.15); }); test("a bot that has been tracking you is far tighter than one that just swung on", () => { const state = live(); const { bot, prey } = facing(state); const truth = () => Math.atan2(prey.y - bot.y, prey.x - bot.x); const spread: number[] = []; for (let i = 0; i < 120; i++) { botThink(state, bot, 1 / 30); state.now += 1 / 30; bot.angle = bot.input.angle; // the match loop's job, done by hand spread.push(Math.abs(bot.angle - truth())); } const swing = Math.max(...spread.slice(0, 15)); const held = Math.max(...spread.slice(80)); expect(swing).toBeGreaterThan(held * 2); expect(held).toBeLessThan(0.03); }); test("a strafing target keeps a settled bot honest", () => { const still = live(); const a = facing(still); const strafing = live(); const b = facing(strafing); let stillWorst = 0; let strafeWorst = 0; for (let i = 0; i < 180; i++) { botThink(still, a.bot, 1 / 30); botThink(strafing, b.bot, 1 / 30); still.now += 1 / 30; strafing.now += 1 / 30; a.bot.angle = a.bot.input.angle; b.bot.angle = b.bot.input.angle; // One stands in the open; the other runs across the sight line. b.prey.y = 640 + Math.sin(i / 8) * 90; if (i > 60) { stillWorst = Math.max(stillWorst, Math.abs(a.bot.angle - Math.atan2(a.prey.y - a.bot.y, a.prey.x - a.bot.x))); strafeWorst = Math.max(strafeWorst, Math.abs(b.bot.angle - Math.atan2(b.prey.y - b.bot.y, b.prey.x - b.bot.x))); } } expect(strafeWorst).toBeGreaterThan(stillWorst); }); test("the aim is still deterministic in the state: one seed, one round", () => { const play = (): number[] => { const state = live(); const { bot, prey } = facing(state); const out: number[] = []; for (let i = 0; i < 60; i++) { botThink(state, bot, 1 / 30); state.now += 1 / 30; bot.angle = bot.input.angle; prey.x = 1400 + i; out.push(bot.angle, bot.input.fire ? 1 : 0); } return out; }; expect(play()).toEqual(play()); }); test("a full run of speed is what the crossing term is measured against", () => { expect(RUN_SPEED).toBeGreaterThan(0); const track = emptyTrack(); acquire(track, "t", 0, { x: 400, y: 0 }, rolls(0, 0.5)); const rate = crossRate(track, { x: 0, y: 0 }, { x: 400, y: RUN_SPEED / 30 }, 1 / 30); expect(rate / RUN_SPEED).toBeCloseTo(1, 3); });