| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
import { expect, test } from "bun:test"; |
| 7 |
import { |
| 8 |
acquire, |
| 9 |
aimCone, |
| 10 |
canFire, |
| 11 |
CONE_FLOOR, |
| 12 |
crossRate, |
| 13 |
drift, |
| 14 |
emptyTrack, |
| 15 |
FORGET, |
| 16 |
heldFor, |
| 17 |
lose, |
| 18 |
REACTION_MAX, |
| 19 |
REACTION_MIN, |
| 20 |
reactionTime, |
| 21 |
} from "./aim.ts"; |
| 22 |
|
| 23 |
const rolls = (...values: number[]) => { |
| 24 |
let at = 0; |
| 25 |
return () => values[at++ % values.length]!; |
| 26 |
}; |
| 27 |
|
| 28 |
test("the reaction a bot draws is between 200 and 350ms, whatever the roll", () => { |
| 29 |
expect(reactionTime(0)).toBeCloseTo(REACTION_MIN, 9); |
| 30 |
expect(reactionTime(1)).toBeCloseTo(REACTION_MAX, 9); |
| 31 |
expect(reactionTime(0.5)).toBeCloseTo((REACTION_MIN + REACTION_MAX) / 2, 9); |
| 32 |
|
| 33 |
expect(reactionTime(-3)).toBe(REACTION_MIN); |
| 34 |
expect(reactionTime(9)).toBe(REACTION_MAX); |
| 35 |
}); |
| 36 |
|
| 37 |
test("the trigger gate is a pure function of when acquisition happened", () => { |
| 38 |
const track = emptyTrack(); |
| 39 |
|
| 40 |
expect(canFire(track, 100)).toBe(false); |
| 41 |
|
| 42 |
acquire(track, "bot-red-1", 10, { x: 0, y: 0 }, rolls(0, 0.5)); |
| 43 |
expect(track.reaction).toBeCloseTo(REACTION_MIN, 9); |
| 44 |
expect(canFire(track, 10)).toBe(false); |
| 45 |
expect(canFire(track, 10.19)).toBe(false); |
| 46 |
expect(canFire(track, 10.21)).toBe(true); |
| 47 |
expect(canFire(track, 12)).toBe(true); |
| 48 |
expect(heldFor(track, 12)).toBeCloseTo(2, 9); |
| 49 |
}); |
| 50 |
|
| 51 |
test("a new target starts the reaction again; the same one does not", () => { |
| 52 |
const track = emptyTrack(); |
| 53 |
acquire(track, "one", 5, { x: 100, y: 100 }, rolls(1, 0.5)); |
| 54 |
expect(track.since).toBe(5); |
| 55 |
acquire(track, "one", 5.3, { x: 180, y: 100 }, rolls(0, 0.5)); |
| 56 |
expect(track.since).toBe(5); |
| 57 |
expect(canFire(track, 5.4)).toBe(true); |
| 58 |
acquire(track, "two", 5.4, { x: 0, y: 0 }, rolls(0, 0.5)); |
| 59 |
expect(track.since).toBe(5.4); |
| 60 |
expect(canFire(track, 5.5)).toBe(false); |
| 61 |
}); |
| 62 |
|
| 63 |
test("a hold survives a blink out of sight, and no longer", () => { |
| 64 |
const track = emptyTrack(); |
| 65 |
acquire(track, "one", 1, { x: 0, y: 0 }, rolls(0, 0.5)); |
| 66 |
lose(track, 1 + FORGET / 2); |
| 67 |
expect(track.id).toBe("one"); |
| 68 |
|
| 69 |
acquire(track, "one", 1 + FORGET / 2, { x: 0, y: 0 }, rolls(0, 0.5)); |
| 70 |
expect(track.since).toBe(1); |
| 71 |
|
| 72 |
lose(track, 3); |
| 73 |
expect(track.id).toBe(null); |
| 74 |
acquire(track, "one", 3, { x: 0, y: 0 }, rolls(0, 0.5)); |
| 75 |
expect(track.since).toBe(3); |
| 76 |
expect(canFire(track, 3.1)).toBe(false); |
| 77 |
}); |
| 78 |
|
| 79 |
test("the cone starts wide, settles the longer one target is held, and never closes", () => { |
| 80 |
const swing = aimCone(0, 0); |
| 81 |
const second = aimCone(1, 0); |
| 82 |
const settled = aimCone(3, 0); |
| 83 |
expect(swing).toBeGreaterThan(0.08); |
| 84 |
expect(second).toBeLessThan(swing / 2); |
| 85 |
expect(settled).toBeLessThan(second); |
| 86 |
expect(settled).toBeGreaterThanOrEqual(CONE_FLOOR); |
| 87 |
expect(aimCone(99, 0)).toBeCloseTo(CONE_FLOOR, 6); |
| 88 |
}); |
| 89 |
|
| 90 |
test("a target crossing the view costs the bot accuracy", () => { |
| 91 |
expect(aimCone(3, 1)).toBeGreaterThan(aimCone(3, 0) * 3); |
| 92 |
expect(aimCone(3, 0.5)).toBeGreaterThan(aimCone(3, 0)); |
| 93 |
expect(aimCone(3, 0.5)).toBeLessThan(aimCone(3, 1)); |
| 94 |
|
| 95 |
expect(aimCone(3, 50)).toBeCloseTo(aimCone(3, 1), 9); |
| 96 |
}); |
| 97 |
|
| 98 |
test("crossing is measured across the sight line, not along it", () => { |
| 99 |
const bot = { x: 0, y: 0 }; |
| 100 |
const walking = emptyTrack(); |
| 101 |
acquire(walking, "t", 0, { x: 300, y: 0 }, rolls(0, 0.5)); |
| 102 |
|
| 103 |
expect(crossRate(walking, bot, { x: 260, y: 0 }, 0.1)).toBeCloseTo(0, 6); |
| 104 |
const strafing = emptyTrack(); |
| 105 |
acquire(strafing, "t", 0, { x: 300, y: 0 }, rolls(0, 0.5)); |
| 106 |
|
| 107 |
expect(crossRate(strafing, bot, { x: 300, y: 25 }, 0.1)).toBeCloseTo(250, -1); |
| 108 |
|
| 109 |
expect(crossRate(strafing, bot, { x: 300, y: 25 }, 0.1)).toBeCloseTo(0, 6); |
| 110 |
}); |
| 111 |
|
| 112 |
test("the error wanders inside the cone instead of being redrawn every tick", () => { |
| 113 |
const track = emptyTrack(); |
| 114 |
acquire(track, "one", 0, { x: 0, y: 0 }, rolls(0, 1)); |
| 115 |
|
| 116 |
expect(Math.abs(track.error)).toBeCloseTo(aimCone(0, 0), 9); |
| 117 |
|
| 118 |
const before = track.error; |
| 119 |
const after = drift(track, aimCone(0, 0), 1 / 30, rolls(0.9)); |
| 120 |
expect(Math.abs(after - before)).toBeLessThan(aimCone(0, 0) / 4); |
| 121 |
|
| 122 |
for (let i = 0; i < 90; i++) drift(track, aimCone(3, 0), 1 / 30, rolls(0.5, 1, 0)); |
| 123 |
expect(Math.abs(track.error)).toBeLessThanOrEqual(aimCone(3, 0) + 1e-12); |
| 124 |
}); |