game6 / client / test / clock.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
73
74
75
76
77
78
79
80
81
82
83
84
85
// The shared band clock: bar/beat arithmetic (including the negative-phase
// wrap the deployed clients already produce), the bpm rebase that keeps the
// groove from jumping, and the ping/pong offset estimator with its decaying
// RTT ceiling. Fixtures recorded off the real src/game/clock.ts.
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/clock.json", import.meta.url), "utf8"));

test("beat / bar / step lengths match TS at every tempo", () => {
  for (const c of v.math) {
    assert.equal(lib.beatMsOf(c.bpm), c.beatMs, `bpm ${c.bpm}`);
    assert.equal(lib.barMsOf(c.bpm), c.barMs, `bpm ${c.bpm}`);
    assert.equal(lib.stepMsOf(c.bpm), c.stepMs, `bpm ${c.bpm}`);
  }
});

test("the grid is 4 beats to the bar, 16 steps to the bar", () => {
  assert.equal(v.beatsPerBar, 4);
  assert.equal(v.stepsPerBar, 16);
  assert.equal(lib.barMsOf(100) / lib.beatMsOf(100), v.beatsPerBar);
  assert.equal(lib.barMsOf(100) / lib.stepMsOf(100), v.stepsPerBar);
});

test("nextBarAt, phases and beatIndex reproduce TS exactly (negatives included)", () => {
  for (const c of v.math) {
    const hostNow = c.now + c.offset;
    const bar = lib.barMsOf(c.bpm);
    const beat = lib.beatMsOf(c.bpm);
    assert.equal(hostNow, c.hostNow, `hostNow bpm=${c.bpm}`);
    assert.equal(lib.nextBarAt(hostNow, c.origin, bar), c.nextBarAt, `nextBarAt bpm=${c.bpm}`);
    // armRecording leaves 120 ms of breathing room this side of the boundary
    assert.equal(lib.nextBarAt(hostNow + 120, c.origin, bar), c.nextBarAtPlus120, `arm bpm=${c.bpm}`);
    assert.equal(lib.phaseOf(hostNow, c.origin, bar), c.barPhase, `barPhase bpm=${c.bpm}`);
    assert.equal(lib.phaseOf(hostNow, c.origin, beat), c.beatPhase, `beatPhase bpm=${c.bpm}`);
    assert.equal(lib.beatIndexOf(hostNow, c.origin, bar, beat), c.beatIndex, `beatIndex bpm=${c.bpm}`);
    assert.equal(lib.rebasedOriginOf(hostNow, c.origin, bar), c.rebasedOrigin, `rebase bpm=${c.bpm}`);
  }
});

test("a rebased origin lands on a bar boundary and keeps the phase", () => {
  for (const c of v.math) {
    const bar = lib.barMsOf(c.bpm);
    const reb = lib.rebasedOriginOf(c.hostNow, c.origin, bar);
    assert.ok(reb <= c.hostNow, `bpm ${c.bpm}`);
    assert.ok(c.hostNow - reb < bar, `bpm ${c.bpm}`);
  }
});

test("addSyncSample: best RTT wins, worse ones are rejected, the ceiling decays", () => {
  for (const s of v.sync) {
    let offset = 0;
    let rtt = s.rttBefore === "Infinity" ? Infinity : s.rttBefore;
    let synced = s.syncedBefore;
    for (const step of s.steps) {
      const [o, r, y] = lib.syncSample(offset, rtt, synced, step.c, step.h, step.now);
      offset = o;
      rtt = r;
      synced = y;
      assert.equal(offset, step.offset, `${s.name} offset`);
      assert.equal(rtt, step.rtt, `${s.name} rtt`);
      assert.equal(synced, step.synced, `${s.name} synced`);
    }
  }
});

test("the RTT ceiling never runs past 2 s", () => {
  const [, rtt] = lib.syncSample(0, 1990, true, 500_000, 900_000, 503_000);
  assert.equal(rtt, 2000);
});

test("becoming the clock authority re-expresses the origin in my own clock", () => {
  for (const c of v.authority) {
    assert.equal(lib.authorityOrigin(c.originBefore, c.offsetBefore), c.origin);
    assert.equal(c.offset, 0);
    assert.equal(c.synced, true);
    assert.equal(c.rtt, "Infinity");
  }
});

test("setTransport is a plain adoption of the host's bpm and origin", () => {
  assert.deepEqual(v.setTransport, { bpm: 137, origin: 42.5 });
});

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