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

forked from benjivers/val

workbenchthis machine
Files 5e428dd35c8d9098ba4cf91bebff3eeac07fdf7d
1
import { describe, expect, test } from "bun:test";
2
import { mkdtempSync } from "node:fs";
3
import { tmpdir } from "node:os";
4
import { join } from "node:path";
5
import { fallsBetween, marksOf, trimFeed, type Mark } from "./killfeed.ts";
6
import { newMatch } from "./match.ts";
7
8
process.env.FORGE_APP_DATA ??= mkdtempSync(join(tmpdir(), "app-"));
9
10
const mark = (id: string, over: Partial<Mark> = {}): Mark => ({
11
  id,
12
  name: id,
13
  side: "atk",
14
  alive: true,
15
  kills: 0,
16
  weapon: "vandal",
17
  ...over,
18
});
19
20
const context = (events: string[] = [], shooters: string[] = [], at = 10) => ({
21
  at,
22
  events,
23
  shooters: new Set(shooters),
24
});
25
26
describe("reading falls out of two instants", () => {
27
  test("a shot kill names the killer, the victim and the gun", () => {
28
    const before = [mark("A", { side: "atk" }), mark("B", { side: "def" })];
29
    const after = [mark("A", { side: "atk", kills: 1 }), mark("B", { side: "def", alive: false })];
30
    const falls = fallsBetween(before, after, context(["A killed B"], ["A"]));
31
    expect(falls).toEqual([{ killer: "A", victim: "B", weapon: "vandal", headshot: false, at: 10 }]);
32
  });
33
34
  test("a head shot is marked as one", () => {
35
    const before = [mark("A", { side: "atk" }), mark("B", { side: "def" })];
36
    const after = [mark("A", { side: "atk", kills: 1 }), mark("B", { side: "def", alive: false })];
37
    expect(fallsBetween(before, after, context(["A headshot B"], ["A"]))[0]!.headshot).toBe(true);
38
  });
39
40
  test("a kill with no shot behind it names no gun — a molly is not a Vandal", () => {
41
    const before = [mark("A", { side: "atk" }), mark("B", { side: "def" })];
42
    const after = [mark("A", { side: "atk", kills: 1 }), mark("B", { side: "def", alive: false })];
43
    expect(fallsBetween(before, after, context(["A killed B"]))[0]!.weapon).toBe(null);
44
  });
45
46
  test("dying to nobody — a molly of your own, a fall — has no killer", () => {
47
    const before = [mark("A", { side: "atk" }), mark("B", { side: "def" })];
48
    const after = [mark("A", { side: "atk" }), mark("B", { side: "def", alive: false })];
49
    const falls = fallsBetween(before, after, context(["B died"]));
50
    expect(falls.length).toBe(1);
51
    expect(falls[0]!.killer).toBe(null);
52
    expect(falls[0]!.weapon).toBe(null);
53
  });
54
55
  test("two kills in one tick go to the right killers, by what the loop said", () => {
56
    const before = [
57
      mark("A", { side: "atk" }),
58
      mark("C", { side: "atk" }),
59
      mark("B", { side: "def" }),
60
      mark("D", { side: "def" }),
61
    ];
62
    const after = [
63
      mark("A", { side: "atk", kills: 1 }),
64
      mark("C", { side: "atk", kills: 1 }),
65
      mark("B", { side: "def", alive: false }),
66
      mark("D", { side: "def", alive: false }),
67
    ];
68
    const falls = fallsBetween(before, after, context(["C killed B", "A killed D"], ["A", "C"]));
69
    expect(falls.find((f) => f.victim === "B")!.killer).toBe("C");
70
    expect(falls.find((f) => f.victim === "D")!.killer).toBe("A");
71
  });
72
73
  test("a killer is never somebody on the victim's own side", () => {
74
    const before = [mark("A", { side: "atk" }), mark("B", { side: "atk" }), mark("E", { side: "def" })];
75
    const after = [
76
      mark("A", { side: "atk", kills: 1 }),
77
      mark("B", { side: "atk", alive: false }),
78
      mark("E", { side: "def" }),
79
    ];
80
    expect(fallsBetween(before, after, context([]))[0]!.killer).toBe(null);
81
  });
82
83
  test("a kill with two victims and one killer only credits the one kill", () => {
84
    const before = [mark("A", { side: "atk" }), mark("B", { side: "def" }), mark("C", { side: "def" })];
85
    const after = [
86
      mark("A", { side: "atk", kills: 1 }),
87
      mark("B", { side: "def", alive: false }),
88
      mark("C", { side: "def", alive: false }),
89
    ];
90
    const falls = fallsBetween(before, after, context(["A killed B"], ["A"]));
91
    expect(falls.filter((f) => f.killer === "A").length).toBe(1);
92
    expect(falls.filter((f) => f.killer === null).length).toBe(1);
93
  });
94
95
  test("nothing happening is no falls", () => {
96
    const players = [mark("A"), mark("B", { side: "def" })];
97
    expect(fallsBetween(players, players, context())).toEqual([]);
98
  });
99
100
  test("a body that was already dead does not fall twice", () => {
101
    const before = [mark("B", { side: "def", alive: false })];
102
    expect(fallsBetween(before, before, context())).toEqual([]);
103
  });
104
105
  test("a fall never carries a position", () => {
106
    const before = [mark("A", { side: "atk" }), mark("B", { side: "def" })];
107
    const after = [mark("A", { side: "atk", kills: 1 }), mark("B", { side: "def", alive: false })];
108
    const fall = fallsBetween(before, after, context(["A killed B"], ["A"]))[0]!;
109
    expect(Object.keys(fall).sort()).toEqual(["at", "headshot", "killer", "victim", "weapon"]);
110
  });
111
112
  test("marks come off the real players of a real match", () => {
113
    const marks = marksOf(newMatch().players);
114
    expect(marks.length).toBe(10);
115
    expect(marks.every((m) => m.alive && m.kills === 0 && typeof m.name === "string")).toBe(true);
116
    expect(JSON.stringify(marks)).not.toContain('"x"');
117
  });
118
});
119
120
describe("the feed as it is shown", () => {
121
  const fall = (victim: string, at: number) => ({ killer: "A", victim, weapon: null, headshot: false, at });
122
123
  test("keeps the last few and drops the stale", () => {
124
    const feed = [fall("a", 1), fall("b", 2), fall("c", 9), fall("d", 10)];
125
    expect(trimFeed(feed, 10, 5, 8).map((f) => f.victim)).toEqual(["b", "c", "d"]);
126
  });
127
128
  test("never grows past its slots", () => {
129
    const feed = [1, 2, 3, 4, 5, 6, 7].map((n) => fall(String(n), 10));
130
    expect(trimFeed(feed, 10, 5, 8).map((f) => f.victim)).toEqual(["3", "4", "5", "6", "7"]);
131
  });
132
});
133
134
describe("the feed the arena sends", () => {
135
  test("bots killing each other reach the snapshot as names, with no positions", async () => {
136
    const { advance, match, snapshot } = await import("./arena.ts");
137
    const viewer = match().players[0]!.id;
138
    let feed: { k: string | null; v: string; w: string | null; h: boolean; t: number }[] = [];
139
    for (let seconds = 0; seconds < 400 && feed.length === 0; seconds += 10) {
140
      advance(10);
141
      feed = JSON.parse(snapshot(viewer)).feed;
142
    }
143
    expect(feed.length).toBeGreaterThan(0);
144
    const names = match().players.map((p) => p.name);
145
    for (const line of feed) {
146
      expect(names).toContain(line.v);
147
      if (line.k !== null) expect(names).toContain(line.k);
148
      expect(line.t).toBeLessThanOrEqual(8);
149
      // Names, a gun, an age. Nothing that says where anybody is.
150
      expect(Object.keys(line).sort()).toEqual(["h", "k", "t", "v", "w"]);
151
    }
152
  }, 30000);
153
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts