wren/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine
Files 2e870ef8994867709476cca0f461c0ff071a8c69
1
/**
2
 * How a bot points a gun, tested as the pure maths it is. That the gate is
3
 * actually wired to a trigger is the game's to prove: `src/aim.test.ts`.
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
  // A roll out of range is still a reaction, not a bot that never shoots.
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
  // Nothing in sight is never a shot, however long the round has run.
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); // the hold carries on
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); // swinging onto somebody else costs again
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
  // Back a moment later: the same hold, already through its reaction.
69
  acquire(track, "one", 1 + FORGET / 2, { x: 0, y: 0 }, rolls(0, 0.5));
70
  expect(track.since).toBe(1);
71
  // Gone for longer than that and it is a fresh acquisition.
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); // several degrees on the swing
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
  // Nonsense from the caller is clamped, not multiplied out.
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
  // Straight down the barrel: nothing to track.
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
  // Straight across it at a run: all of it but the turn of the sight line.
107
  expect(crossRate(strafing, bot, { x: 300, y: 25 }, 0.1)).toBeCloseTo(250, -1);
108
  // And it remembers where the target went, so the next tick measures again.
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
  // Drawn at the edge of the swing cone on acquisition.
116
  expect(Math.abs(track.error)).toBeCloseTo(aimCone(0, 0), 9);
117
  // One tick moves it a little, not to somewhere unrelated.
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
  // And it is squeezed toward the barrel as the cone closes around it.
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
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts