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

forked from benjivers/val

workbenchthis machine
Files 16611dd34ec2ea73a39a2dece0010023cace015e
320 lines · 10.7 KB · ~3.1k tok Raw History
1
import { expect, test } from "bun:test";
2
import { runBots } from "./bots.ts";
3
import {
4
  aliveOn,
5
  buyAbility,
6
  buyShield,
7
  buyWeapon,
8
  DEFUSE_CHECKPOINT,
9
  DEFUSE_TIME,
10
  FUSE_TIME,
11
  newMatch,
12
  PLANT_TIME,
13
  ROUND_TIME,
14
  SWAP_AFTER,
15
  tick,
16
  visible,
17
  WIN_ROUNDS,
18
} from "./match.ts";
19
import { siteCentre } from "./map.ts";
20
import type { MatchState, Player } from "./state.ts";
21
22
const live = (seed = 5): MatchState => {
23
  const state = newMatch(seed);
24
  state.phase = "live";
25
  state.clock = ROUND_TIME;
26
  return state;
27
};
28
const atk = (state: MatchState): Player[] => state.players.filter((p) => p.side === "atk");
29
const def = (state: MatchState): Player[] => state.players.filter((p) => p.side === "def");
30
const run = (state: MatchState, seconds: number): void => {
31
  for (let i = 0; i < Math.round(seconds * 30); i++) tick(state, 1 / 30);
32
};
33
34
/** Put a carrier on a site with the spike, everyone else out of the way. */
35
function onSite(state: MatchState): Player {
36
  const spot = siteCentre("A");
37
  const carrier = atk(state)[0]!;
38
  for (const player of state.players) {
39
    player.spike = false;
40
    player.x = 1200;
41
    player.y = 1320;
42
  }
43
  carrier.spike = true;
44
  carrier.x = spot.x;
45
  carrier.y = spot.y;
46
  state.spike = { carried: carrier.id, x: spot.x, y: spot.y, planted: false, fuse: 0, site: null, defuse: 0, checkpoint: 0 };
47
  return carrier;
48
}
49
50
test("a match starts in the buy phase with ten seats and a spike carrier", () => {
51
  const state = newMatch(1);
52
  expect(state.players.length).toBe(10);
53
  expect(atk(state).length).toBe(5);
54
  expect(def(state).length).toBe(5);
55
  expect(state.phase).toBe("buy");
56
  expect(state.players.some((p) => p.spike)).toBe(true);
57
  expect(state.players.every((p) => p.credits === 800)).toBe(true);
58
});
59
60
test("planting takes four seconds of standing on a site and arms the fuse", () => {
61
  const state = live();
62
  const carrier = onSite(state);
63
  carrier.input.use = true;
64
  run(state, PLANT_TIME - 1);
65
  expect(state.spike.planted).toBe(false);
66
  run(state, 1.2);
67
  expect(state.spike.planted).toBe(true);
68
  expect(state.spike.site).toBe("A");
69
  expect(Math.round(state.spike.fuse)).toBeLessThanOrEqual(FUSE_TIME);
70
  // The plant pays the attacking team and charges the planter's ult.
71
  expect(atk(state).every((p) => p.credits >= 800 + 300)).toBe(true);
72
  expect(carrier.ult).toBeGreaterThan(0);
73
});
74
75
test("a plant off a site does nothing at all", () => {
76
  const state = live();
77
  const carrier = onSite(state);
78
  carrier.x = 1200;
79
  carrier.y = 1320; // attacker spawn, not a site
80
  carrier.input.use = true;
81
  run(state, PLANT_TIME + 2);
82
  expect(state.spike.planted).toBe(false);
83
});
84
85
test("a defuse killed at the half way mark leaves the checkpoint for the next defender", () => {
86
  const state = live();
87
  const carrier = onSite(state);
88
  carrier.input.use = true;
89
  run(state, PLANT_TIME + 0.2);
90
  expect(state.spike.planted).toBe(true);
91
  carrier.input.use = false;
92
93
  const first = def(state)[0]!;
94
  first.x = state.spike.x;
95
  first.y = state.spike.y;
96
  first.input.use = true;
97
  run(state, DEFUSE_CHECKPOINT + 0.2);
98
  expect(state.spike.checkpoint).toBe(DEFUSE_CHECKPOINT);
99
  expect(state.spike.defuse).toBeGreaterThanOrEqual(DEFUSE_CHECKPOINT);
100
101
  // He dies here. The defuse falls back to the checkpoint, not to zero.
102
  first.hp = 1;
103
  first.alive = false;
104
  first.deaths += 1;
105
  state.spike.defuse = state.spike.checkpoint;
106
  expect(state.spike.defuse).toBe(DEFUSE_CHECKPOINT);
107
108
  const second = def(state)[1]!;
109
  second.x = state.spike.x;
110
  second.y = state.spike.y;
111
  second.input.use = true;
112
  // Half a defuse is left, so well under the full seven seconds finishes it.
113
  run(state, DEFUSE_TIME - DEFUSE_CHECKPOINT + 0.3);
114
  expect(state.phase).toBe("end");
115
  expect(state.events.some((e) => e.text.includes("defused"))).toBe(true);
116
});
117
118
test("an interrupted defuse before the checkpoint starts again from nothing", () => {
119
  const state = live();
120
  const carrier = onSite(state);
121
  carrier.input.use = true;
122
  run(state, PLANT_TIME + 0.2);
123
  carrier.input.use = false;
124
  const defender = def(state)[0]!;
125
  defender.x = state.spike.x;
126
  defender.y = state.spike.y;
127
  defender.input.use = true;
128
  run(state, 2);
129
  expect(state.spike.defuse).toBeGreaterThan(1.5);
130
  defender.input.use = false;
131
  run(state, 0.2);
132
  expect(state.spike.defuse).toBe(0);
133
  expect(state.spike.checkpoint).toBe(0);
134
});
135
136
test("the spike detonating wins the round for the attackers", () => {
137
  const state = live();
138
  const carrier = onSite(state);
139
  carrier.input.use = true;
140
  run(state, PLANT_TIME + 0.2);
141
  carrier.input.use = false;
142
  state.spike.fuse = 1;
143
  run(state, 1.5);
144
  expect(state.phase).toBe("end");
145
  expect(state.score[state.attackers]).toBe(1);
146
});
147
148
test("time running out with no plant is a defence win, but the clock stops mattering once it is down", () => {
149
  const timeout = live();
150
  timeout.clock = 0.5;
151
  run(timeout, 1);
152
  expect(timeout.phase).toBe("end");
153
  expect(timeout.score[timeout.attackers]).toBe(0);
154
155
  const planted = live();
156
  const carrier = onSite(planted);
157
  carrier.input.use = true;
158
  run(planted, PLANT_TIME + 0.2);
159
  carrier.input.use = false;
160
  planted.clock = 0.2;
161
  run(planted, 1);
162
  expect(planted.phase).toBe("live");
163
});
164
165
test("eliminating a team ends the round, and a planted spike still wins it", () => {
166
  const wipe = live();
167
  for (const player of def(wipe)) player.alive = false;
168
  run(wipe, 0.1);
169
  expect(wipe.phase).toBe("end");
170
  expect(wipe.score[wipe.attackers]).toBe(1);
171
172
  const traded = live();
173
  const carrier = onSite(traded);
174
  carrier.input.use = true;
175
  run(traded, PLANT_TIME + 0.2);
176
  for (const player of atk(traded)) player.alive = false;
177
  run(traded, 0.1);
178
  // Everyone is dead but the spike is armed: the round is not over yet.
179
  expect(traded.phase).toBe("live");
180
});
181
182
test("a buy is refused server-side when the credits are not there", () => {
183
  const state = newMatch(2);
184
  const player = state.players[0]!;
185
  player.agent = "ember";
186
  player.credits = 800;
187
  expect(buyWeapon(state, player, "vandal")).toBe(false);
188
  expect(player.weapon).toBe("classic");
189
  expect(buyWeapon(state, player, "ghost")).toBe(true);
190
  expect(player.credits).toBe(300);
191
  expect(buyShield(state, player, "heavy")).toBe(false);
192
  // 250 for the flash leaves fifty, and fifty buys nothing.
193
  expect(buyAbility(state, player, "c")).toBe(true);
194
  expect(player.credits).toBe(50);
195
  expect(buyAbility(state, player, "c")).toBe(false);
196
  expect(buyAbility(state, player, "q")).toBe(false);
197
  // And nothing is for sale once the round is live.
198
  player.credits = 9000;
199
  state.phase = "live";
200
  expect(buyWeapon(state, player, "vandal")).toBe(false);
201
});
202
203
test("a basic is bought by the charge and never past its limit", () => {
204
  const state = newMatch(2);
205
  const player = state.players.find((p) => p.agent === "veil")!;
206
  player.credits = 9000;
207
  expect(buyAbility(state, player, "c")).toBe(true);
208
  expect(buyAbility(state, player, "c")).toBe(true);
209
  expect(player.charges.c).toBe(2);
210
  expect(buyAbility(state, player, "c")).toBe(false);
211
  expect(buyAbility(state, player, "e")).toBe(false);
212
  expect(buyAbility(state, player, "x")).toBe(false);
213
});
214
215
test("sides swap after twelve rounds and a thirteenth round wins the match", () => {
216
  const state = newMatch(4);
217
  const started = state.attackers;
218
  state.round = SWAP_AFTER;
219
  state.phase = "live";
220
  state.score[started] = WIN_ROUNDS - 2;
221
  for (const player of def(state)) player.alive = false;
222
  run(state, 0.1);
223
  expect(state.phase).toBe("end");
224
  run(state, 6);
225
  expect(state.round).toBe(SWAP_AFTER + 1);
226
  expect(state.attackers).not.toBe(started);
227
  // One more round, taken by whoever now attacks the old defenders' side.
228
  const winners = state.attackers === started ? started : state.attackers;
229
  state.phase = "live";
230
  state.score[started] = WIN_ROUNDS - 1;
231
  for (const player of state.players) if (player.team !== started) player.alive = false;
232
  run(state, 0.1);
233
  expect(state.score[started]).toBe(WIN_ROUNDS);
234
  expect(state.phase).toBe("over");
235
  expect(state.result).toContain(`${started} win`);
236
  expect(winners.length).toBeGreaterThan(0);
237
});
238
239
test("an ended match never parks: it shows the result and starts a fresh one", () => {
240
  const state = newMatch(6);
241
  state.phase = "live";
242
  state.score[state.attackers] = WIN_ROUNDS - 1;
243
  for (const player of def(state)) player.alive = false;
244
  run(state, 0.1);
245
  expect(state.phase).toBe("over");
246
  expect(state.result).not.toBe(null);
247
  run(state, 13);
248
  expect(state.phase).toBe("buy");
249
  expect(state.round).toBe(1);
250
  expect(state.score).toEqual({ red: 0, blue: 0 });
251
  expect(state.result).toBe(null);
252
  expect(state.players.length).toBe(10);
253
});
254
255
test("line of sight is the server's answer: a smoke hides an enemy in the open", () => {
256
  const state = live();
257
  const viewer = atk(state)[0]!;
258
  const enemy = def(state)[0]!;
259
  viewer.x = 1200;
260
  viewer.y = 1320;
261
  enemy.x = 1200;
262
  enemy.y = 1000;
263
  expect(visible(state, viewer, enemy)).toBe(true);
264
  state.zones.push({
265
    id: 99, kind: "smoke", owner: "x", side: "def", x: 1200, y: 1160, r: 150,
266
    life: 8, fuse: 0, beat: 0, pulses: 0, damage: 0, blocks: true, solid: false,
267
  });
268
  expect(visible(state, viewer, enemy)).toBe(false);
269
  // A revealed enemy is seen through anything; so is a team mate.
270
  enemy.revealed = 2;
271
  expect(visible(state, viewer, enemy)).toBe(true);
272
});
273
274
test("a blinded player sees nothing", () => {
275
  const state = live();
276
  const viewer = atk(state)[0]!;
277
  const enemy = def(state)[0]!;
278
  viewer.x = 1200;
279
  viewer.y = 1320;
280
  enemy.x = 1200;
281
  enemy.y = 1000;
282
  viewer.blind = 1.4;
283
  expect(visible(state, viewer, enemy)).toBe(false);
284
});
285
286
test("a killed carrier drops the spike where they fell", () => {
287
  const state = live();
288
  const carrier = onSite(state);
289
  const killer = def(state)[0]!;
290
  killer.x = carrier.x + 40;
291
  killer.y = carrier.y;
292
  killer.angle = Math.PI;
293
  killer.weapon = "operator";
294
  killer.input.fire = true;
295
  run(state, 1);
296
  if (!carrier.alive) {
297
    expect(state.spike.carried).toBe(null);
298
    expect(Math.hypot(state.spike.x - carrier.x, state.spike.y - carrier.y)).toBeLessThan(80);
299
  }
300
  expect(state.shots.length + state.events.length).toBeGreaterThan(0);
301
});
302
303
test("bots play the round: they buy, they take a site and they use their kit", () => {
304
  const state = newMatch(42);
305
  let casts = 0;
306
  let rounds = 0;
307
  for (let i = 0; i < 30 * 60 * 6; i++) {
308
    runBots(state, 1 / 30);
309
    tick(state, 1 / 30);
310
    casts += state.zones.length > 0 ? 1 : 0;
311
    if (state.round > rounds) rounds = state.round;
312
  }
313
  expect(rounds).toBeGreaterThan(1);
314
  expect(state.score.red + state.score.blue).toBeGreaterThan(1);
315
  // Something was on the map: smokes, flashes, mollies, walls.
316
  expect(casts).toBeGreaterThan(100);
317
  // And somebody bought a real gun with the credits they won.
318
  expect(state.players.some((p) => p.weapon !== "classic")).toBe(true);
319
  expect(aliveOn(state, "atk").length + aliveOn(state, "def").length).toBeGreaterThan(0);
320
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts