game6 / client / test / snapshot.test.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// The late-joiner snapshot, both directions. The host packs its whole loop
// rack into "sn"; the newcomer rebuilds it, dropping loops whose owner has
// already left (the host's own always stay) and keeping its local mutes,
// because loop ids survive the swap. Fixtures recorded off the real
// sendSnap/applySnap while the TS was canon.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as lib from "../test-dist/testlib.js";

const v = JSON.parse(readFileSync(new URL("./vectors/snapshot.json", import.meta.url), "utf8"));
const plain = (x) => JSON.parse(JSON.stringify(x));

for (const c of v.sendSnap) {
  test(`sendSnap ${c.name}: byte-identical to TS`, () => {
    const f = c.frame;
    assert.equal(JSON.stringify(lib.mkSn(f.bpm, f.org, f.loops)), c.json);
  });
}

test("sendSnap carries the transport, so a newcomer lands on the same bar grid", () => {
  const c = v.sendSnap.find((e) => e.name === "three-loops");
  assert.equal(c.frame.bpm, 132);
  assert.equal(c.frame.org, 123456.5); // fractional origins ride intact
  assert.equal(c.frame.loops.length, 3);
});

for (const c of v.applySnap) {
  test(`applySnap ${c.name}`, () => {
    if (c.from !== c.host) {
      // not from the host: nothing is adopted at all
      assert.equal(c.bpm, 100);
      assert.deepEqual(c.loopsAfter.map((l) => l[0]), ["stale.1"]);
      assert.deepEqual(c.resyncPings, []);
      return;
    }
    const room = new Set(c.peers);
    assert.deepEqual(plain(lib.snapLoops(c.frameLoops, c.from, room)), c.loopsAfter);
    // the transport is clamped on the way in (the fixture sent bpm 5000)
    assert.equal(lib.clampBpm(5000), c.bpm);
    assert.equal(c.origin, 777.25);
    // and a fresh clock estimate is requested at 0 / 350 / 900 ms
    assert.deepEqual(c.resyncPings, [0, 350, 900]);
  });
}

test("a snapshot loop is kept only for a present owner — or for the host itself", () => {
  const room = new Set(["B"]);
  assert.equal(lib.keepSnapLoop("B", "H", room), true);
  assert.equal(lib.keepSnapLoop("C", "H", room), false);
  assert.equal(lib.keepSnapLoop("H", "H", new Set()), true, "the host's own always ride along");
});

test("local mutes survive the swap, because loop ids do", () => {
  for (const c of v.applySnap) {
    assert.ok(c.mutedAfter.includes("BBBBBB.1"), c.name);
    assert.ok(c.mutedAfter.includes("gone.9"), `${c.name}: a mute for a vanished loop is harmless`);
  }
});

test("the per-player quota applies while adopting a snapshot too", () => {
  const many = Array.from({ length: 5 }, (_, k) => [`B.${k}`, "B", 0, 2, [[k, 60, 90, 1]]]);
  const kept = plain(lib.snapLoops(many, "H", new Set(["B"])));
  assert.deepEqual(kept.map((l) => l[0]), ["B.2", "B.3", "B.4"]);
});

test("snapshot instruments are normalised, bar counts are NOT (unlike lp)", () => {
  const odd = [["x.1", "B", 9, 3, []]];
  const kept = plain(lib.snapLoops(odd, "H", new Set(["B"])));
  assert.equal(kept[0][2], 4, "instrument 9 becomes the audience code");
  assert.equal(kept[0][3], 3, "a 3-bar length rides through verbatim, as the TS did");
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game6.git