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

forked from benjivers/val

workbenchthis machine
Files 607c66150cc996b915483fc27c9743896a13c0b4
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
  makePlayer,
12
  newMatch,
13
  PLANT_TIME,
14
  ROUND_TIME,
15
  SWAP_AFTER,
16
  tick,
17
  visible,
18
  WIN_ROUNDS,
19
} from "./match.ts";
20
import { siteCentre, spawnCells, wallBetween } from "./map.ts";
21
import type { MatchState, Player } from "./state.ts";
22
23
const live = (seed = 5): MatchState => {
24
  const state = newMatch(seed);
25
  state.phase = "live";
26
  state.clock = ROUND_TIME;
27
  return state;
28
};
29
const atk = (state: MatchState): Player[] => state.players.filter((p) => p.side === "atk");
30
const def = (state: MatchState): Player[] => state.players.filter((p) => p.side === "def");
31
const run = (state: MatchState, seconds: number): void => {
32
  for (let i = 0; i < Math.round(seconds * 30); i++) tick(state, 1 / 30);
33
};
34
35
/** Put a carrier on a site with the spike, everyone else out of the way. */
36
function onSite(state: MatchState): Player {
37
  const spot = siteCentre("A");
38
  const carrier = atk(state)[0]!;
39
  for (const player of state.players) {
40
    player.spike = false;
41
    player.x = 1200;
42
    player.y = 1320;
43
  }
44
  carrier.spike = true;
45
  carrier.x = spot.x;
46
  carrier.y = spot.y;
47
  state.spike = { carried: carrier.id, x: spot.x, y: spot.y, planted: false, fuse: 0, site: null, defuse: 0, checkpoint: 0 };
48
  return carrier;
49
}
50
51
test("a match starts in the buy phase with ten seats and a spike carrier", () => {
52
  const state = newMatch(1);
53
  expect(state.players.length).toBe(10);
54
  expect(atk(state).length).toBe(5);
55
  expect(def(state).length).toBe(5);
56
  expect(state.phase).toBe("buy");
57
  expect(state.players.some((p) => p.spike)).toBe(true);
58
  expect(state.players.every((p) => p.credits === 800)).toBe(true);
59
});
60
61
test("planting takes four seconds of standing on a site and arms the fuse", () => {
62
  const state = live();
63
  const carrier = onSite(state);
64
  carrier.input.use = true;
65
  run(state, PLANT_TIME - 1);
66
  expect(state.spike.planted).toBe(false);
67
  run(state, 1.2);
68
  expect(state.spike.planted).toBe(true);
69
  expect(state.spike.site).toBe("A");
70
  expect(Math.round(state.spike.fuse)).toBeLessThanOrEqual(FUSE_TIME);
71
  // The plant pays the attacking team and charges the planter's ult.
72
  expect(atk(state).every((p) => p.credits >= 800 + 300)).toBe(true);
73
  expect(carrier.ult).toBeGreaterThan(0);
74
});
75
76
test("a plant off a site does nothing at all", () => {
77
  const state = live();
78
  const carrier = onSite(state);
79
  carrier.x = 1200;
80
  carrier.y = 1320; // attacker spawn, not a site
81
  carrier.input.use = true;
82
  run(state, PLANT_TIME + 2);
83
  expect(state.spike.planted).toBe(false);
84
});
85
86
test("a defuse killed at the half way mark leaves the checkpoint for the next defender", () => {
87
  const state = live();
88
  const carrier = onSite(state);
89
  carrier.input.use = true;
90
  run(state, PLANT_TIME + 0.2);
91
  expect(state.spike.planted).toBe(true);
92
  carrier.input.use = false;
93
94
  const first = def(state)[0]!;
95
  first.x = state.spike.x;
96
  first.y = state.spike.y;
97
  first.input.use = true;
98
  run(state, DEFUSE_CHECKPOINT + 0.2);
99
  expect(state.spike.checkpoint).toBe(DEFUSE_CHECKPOINT);
100
  expect(state.spike.defuse).toBeGreaterThanOrEqual(DEFUSE_CHECKPOINT);
101
102
  // He dies here. The defuse falls back to the checkpoint, not to zero.
103
  first.hp = 1;
104
  first.alive = false;
105
  first.deaths += 1;
106
  state.spike.defuse = state.spike.checkpoint;
107
  expect(state.spike.defuse).toBe(DEFUSE_CHECKPOINT);
108
109
  const second = def(state)[1]!;
110
  second.x = state.spike.x;
111
  second.y = state.spike.y;
112
  second.input.use = true;
113
  // Half a defuse is left, so well under the full seven seconds finishes it.
114
  run(state, DEFUSE_TIME - DEFUSE_CHECKPOINT + 0.3);
115
  expect(state.phase).toBe("end");
116
  expect(state.events.some((e) => e.text.includes("defused"))).toBe(true);
117
});
118
119
test("an interrupted defuse before the checkpoint starts again from nothing", () => {
120
  const state = live();
121
  const carrier = onSite(state);
122
  carrier.input.use = true;
123
  run(state, PLANT_TIME + 0.2);
124
  carrier.input.use = false;
125
  const defender = def(state)[0]!;
126
  defender.x = state.spike.x;
127
  defender.y = state.spike.y;
128
  defender.input.use = true;
129
  run(state, 2);
130
  expect(state.spike.defuse).toBeGreaterThan(1.5);
131
  defender.input.use = false;
132
  run(state, 0.2);
133
  expect(state.spike.defuse).toBe(0);
134
  expect(state.spike.checkpoint).toBe(0);
135
});
136
137
test("the spike detonating wins the round for the attackers", () => {
138
  const state = live();
139
  const carrier = onSite(state);
140
  carrier.input.use = true;
141
  run(state, PLANT_TIME + 0.2);
142
  carrier.input.use = false;
143
  state.spike.fuse = 1;
144
  run(state, 1.5);
145
  expect(state.phase).toBe("end");
146
  expect(state.score[state.attackers]).toBe(1);
147
});
148
149
test("time running out with no plant is a defence win, but the clock stops mattering once it is down", () => {
150
  const timeout = live();
151
  timeout.clock = 0.5;
152
  run(timeout, 1);
153
  expect(timeout.phase).toBe("end");
154
  expect(timeout.score[timeout.attackers]).toBe(0);
155
156
  const planted = live();
157
  const carrier = onSite(planted);
158
  carrier.input.use = true;
159
  run(planted, PLANT_TIME + 0.2);
160
  carrier.input.use = false;
161
  planted.clock = 0.2;
162
  run(planted, 1);
163
  expect(planted.phase).toBe("live");
164
});
165
166
test("eliminating a team ends the round, and a planted spike still wins it", () => {
167
  const wipe = live();
168
  for (const player of def(wipe)) player.alive = false;
169
  run(wipe, 0.1);
170
  expect(wipe.phase).toBe("end");
171
  expect(wipe.score[wipe.attackers]).toBe(1);
172
173
  const traded = live();
174
  const carrier = onSite(traded);
175
  carrier.input.use = true;
176
  run(traded, PLANT_TIME + 0.2);
177
  for (const player of atk(traded)) player.alive = false;
178
  run(traded, 0.1);
179
  // Everyone is dead but the spike is armed: the round is not over yet.
180
  expect(traded.phase).toBe("live");
181
});
182
183
test("a buy is refused server-side when the credits are not there", () => {
184
  const state = newMatch(2);
185
  const player = state.players[0]!;
186
  player.agent = "ember";
187
  player.credits = 800;
188
  expect(buyWeapon(state, player, "vandal")).toBe(false);
189
  expect(player.weapon).toBe("classic");
190
  expect(buyWeapon(state, player, "ghost")).toBe(true);
191
  expect(player.credits).toBe(300);
192
  expect(buyShield(state, player, "heavy")).toBe(false);
193
  // 250 for the flash leaves fifty, and fifty buys nothing.
194
  expect(buyAbility(state, player, "c")).toBe(true);
195
  expect(player.credits).toBe(50);
196
  expect(buyAbility(state, player, "c")).toBe(false);
197
  expect(buyAbility(state, player, "q")).toBe(false);
198
  // And nothing is for sale once the round is live.
199
  player.credits = 9000;
200
  state.phase = "live";
201
  expect(buyWeapon(state, player, "vandal")).toBe(false);
202
});
203
204
test("a basic is bought by the charge and never past its limit", () => {
205
  const state = newMatch(2);
206
  const player = state.players.find((p) => p.agent === "veil")!;
207
  player.credits = 9000;
208
  expect(buyAbility(state, player, "c")).toBe(true);
209
  expect(buyAbility(state, player, "c")).toBe(true);
210
  expect(player.charges.c).toBe(2);
211
  expect(buyAbility(state, player, "c")).toBe(false);
212
  expect(buyAbility(state, player, "e")).toBe(false);
213
  expect(buyAbility(state, player, "x")).toBe(false);
214
});
215
216
test("sides swap after twelve rounds and a thirteenth round wins the match", () => {
217
  const state = newMatch(4);
218
  const started = state.attackers;
219
  state.round = SWAP_AFTER;
220
  state.phase = "live";
221
  state.score[started] = WIN_ROUNDS - 2;
222
  for (const player of def(state)) player.alive = false;
223
  run(state, 0.1);
224
  expect(state.phase).toBe("end");
225
  run(state, 6);
226
  expect(state.round).toBe(SWAP_AFTER + 1);
227
  expect(state.attackers).not.toBe(started);
228
  // One more round, taken by whoever now attacks the old defenders' side.
229
  const winners = state.attackers === started ? started : state.attackers;
230
  state.phase = "live";
231
  state.score[started] = WIN_ROUNDS - 1;
232
  for (const player of state.players) if (player.team !== started) player.alive = false;
233
  run(state, 0.1);
234
  expect(state.score[started]).toBe(WIN_ROUNDS);
235
  expect(state.phase).toBe("over");
236
  expect(state.result).toContain(`${started} win`);
237
  expect(winners.length).toBeGreaterThan(0);
238
});
239
240
test("an ended match never parks: it shows the result and starts a fresh one", () => {
241
  const state = newMatch(6);
242
  state.phase = "live";
243
  state.score[state.attackers] = WIN_ROUNDS - 1;
244
  for (const player of def(state)) player.alive = false;
245
  run(state, 0.1);
246
  expect(state.phase).toBe("over");
247
  expect(state.result).not.toBe(null);
248
  run(state, 13);
249
  expect(state.phase).toBe("buy");
250
  expect(state.round).toBe(1);
251
  expect(state.score).toEqual({ red: 0, blue: 0 });
252
  expect(state.result).toBe(null);
253
  expect(state.players.length).toBe(10);
254
});
255
256
test("line of sight is the server's answer: a smoke hides an enemy in the open", () => {
257
  const state = live();
258
  const viewer = atk(state)[0]!;
259
  const enemy = def(state)[0]!;
260
  viewer.x = 1200;
261
  viewer.y = 1320;
262
  enemy.x = 1200;
263
  enemy.y = 1000;
264
  expect(visible(state, viewer, enemy)).toBe(true);
265
  state.zones.push({
266
    id: 99, kind: "smoke", owner: "x", side: "def", x: 1200, y: 1160, r: 150,
267
    life: 8, fuse: 0, beat: 0, pulses: 0, damage: 0, blocks: true, solid: false,
268
  });
269
  expect(visible(state, viewer, enemy)).toBe(false);
270
  // A revealed enemy is seen through anything; so is a team mate.
271
  enemy.revealed = 2;
272
  expect(visible(state, viewer, enemy)).toBe(true);
273
});
274
275
test("a wall hides two players from each other, and open floor does not", () => {
276
  const state = live();
277
  const viewer = atk(state)[0]!;
278
  const enemy = def(state)[0]!;
279
  viewer.blind = 0;
280
  enemy.revealed = 0;
281
  // Either side of the block between A site and the middle of the map.
282
  viewer.x = 400;
283
  viewer.y = 400;
284
  enemy.x = 1200;
285
  enemy.y = 400;
286
  expect(wallBetween(viewer, enemy)).toBe(true);
287
  expect(visible(state, viewer, enemy)).toBe(false);
288
  // The same two down the open back corridor, with nothing in between.
289
  viewer.x = 400;
290
  viewer.y = 1400;
291
  enemy.x = 1000;
292
  enemy.y = 1400;
293
  expect(wallBetween(viewer, enemy)).toBe(false);
294
  expect(visible(state, viewer, enemy)).toBe(true);
295
});
296
297
test("no spawn can see a spawn: mid is staggered, not a corridor", () => {
298
  for (const from of spawnCells("atk"))
299
    for (const to of spawnCells("def")) expect(wallBetween(from, to)).toBe(true);
300
});
301
302
test("a dead player watches through the team, never through walls", () => {
303
  const state = live();
304
  const viewer = atk(state)[0]!;
305
  const mate = atk(state)[1]!;
306
  const enemy = def(state)[0]!;
307
  for (const player of atk(state)) {
308
    player.alive = false;
309
    player.x = 400;
310
    player.y = 400;
311
  }
312
  enemy.revealed = 0;
313
  enemy.x = 1000;
314
  enemy.y = 1400;
315
  // Every attacker is dead: the enemy across the map is nobody's to see.
316
  expect(visible(state, viewer, enemy)).toBe(false);
317
  // One team mate alive with a sight line, and the dead player sees it too.
318
  mate.alive = true;
319
  mate.x = 400;
320
  mate.y = 1400;
321
  expect(visible(state, viewer, enemy)).toBe(true);
322
  // That team mate behind a wall takes the vision away again.
323
  mate.x = 400;
324
  mate.y = 400;
325
  expect(visible(state, viewer, enemy)).toBe(false);
326
});
327
328
test("a blinded player sees nothing", () => {
329
  const state = live();
330
  const viewer = atk(state)[0]!;
331
  const enemy = def(state)[0]!;
332
  viewer.x = 1200;
333
  viewer.y = 1320;
334
  enemy.x = 1200;
335
  enemy.y = 1000;
336
  viewer.blind = 1.4;
337
  expect(visible(state, viewer, enemy)).toBe(false);
338
});
339
340
test("a killed carrier drops the spike where they fell", () => {
341
  const state = live();
342
  const carrier = onSite(state);
343
  const killer = def(state)[0]!;
344
  killer.x = carrier.x + 40;
345
  killer.y = carrier.y;
346
  killer.angle = Math.PI;
347
  killer.weapon = "operator";
348
  killer.input.fire = true;
349
  run(state, 1);
350
  if (!carrier.alive) {
351
    expect(state.spike.carried).toBe(null);
352
    expect(Math.hypot(state.spike.x - carrier.x, state.spike.y - carrier.y)).toBeLessThan(80);
353
  }
354
  expect(state.shots.length + state.events.length).toBeGreaterThan(0);
355
});
356
357
test("bots play the round: they buy, they take a site and they use their kit", () => {
358
  const state = newMatch(42);
359
  let casts = 0;
360
  let rounds = 0;
361
  for (let i = 0; i < 30 * 60 * 6; i++) {
362
    runBots(state, 1 / 30);
363
    tick(state, 1 / 30);
364
    casts += state.zones.length > 0 ? 1 : 0;
365
    if (state.round > rounds) rounds = state.round;
366
  }
367
  expect(rounds).toBeGreaterThan(1);
368
  expect(state.score.red + state.score.blue).toBeGreaterThan(1);
369
  // Something was on the map: smokes, flashes, mollies, walls.
370
  expect(casts).toBeGreaterThan(100);
371
  // And somebody bought a real gun with the credits they won.
372
  expect(state.players.some((p) => p.weapon !== "classic")).toBe(true);
373
  expect(aliveOn(state, "atk").length + aliveOn(state, "def").length).toBeGreaterThan(0);
374
});
375
376
377
test("no match ever has two players with the same name", () => {
378
  // The kill feed, the round log and the scoreboard all address people by
379
  // name; two Kits read as a team kill this game cannot have. A thousand
380
  // rosters, no collision.
381
  for (let seed = 0; seed < 1000; seed++) {
382
    const names = newMatch(seed).players.map((p) => p.name);
383
    expect(names.length).toBe(10);
384
    expect(new Set(names).size).toBe(10);
385
  }
386
});
387
388
test("a seat made on its own still gets a name of its own", () => {
389
  const state = newMatch(1234);
390
  const names = new Set<string>();
391
  for (const team of ["red", "blue"] as const)
392
    for (let i = 0; i < 5; i++) names.add(makePlayer(state, team, i).name);
393
  expect(names.size).toBe(10);
394
});
395
396
test("the deal is the seeded shuffle, so the same match replays the same", () => {
397
  expect(newMatch(77).players.map((p) => p.name)).toEqual(newMatch(77).players.map((p) => p.name));
398
  expect(newMatch(77).players.map((p) => p.name)).not.toEqual(newMatch(78).players.map((p) => p.name));
399
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts