peer-kit / test / gym.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
// Gym wrappers — determinism and a sanity check that skill shows in outcomes.
import test from "node:test";
import assert from "node:assert/strict";
import { SeanceGym } from "../dist/gym/seance.js";
import { NeonGridGym } from "../dist/gym/neon-grid.js";
import { mulberry32 } from "../dist/games/host/rng.js";

test("séance gym: fixed seed + fixed policy is byte-identical", () => {
  const run = () => {
    const gym = new SeanceGym({ players: 3, seed: 11 });
    const log = [];
    for (let ep = 0; ep < 3; ep++) {
      let views = gym.reset();
      log.push(views);
      for (;;) {
        // deterministic policy: medium takes the safe room, others rotate
        const actions = views.map((v) => (v.isMedium ? v.hintNot : (v.bell + v.seat) % 3));
        const r = gym.step(actions);
        log.push(r);
        if (r.done) break;
        views = r.views;
      }
    }
    return JSON.stringify(log);
  };
  assert.equal(run(), run());
});

test("séance gym: views wear the SeanceView shape and hide the hint from guests", () => {
  const gym = new SeanceGym({ players: 3, seed: 5 });
  const views = gym.reset();
  for (const v of views) {
    assert.deepEqual(Object.keys(v).sort(), ["bell", "candles", "hintNot", "isMedium", "rooms", "seat"]);
    assert.equal(v.bell, 1);
    if (!v.isMedium) assert.equal(v.hintNot, -1, "only the medium hears the spirit");
  }
  assert.equal(views.filter((v) => v.isMedium).length, 1);
});

test("neon-grid gym: wall-dodging beats wall-riding", () => {
  const rng = mulberry32(3);
  const blocked = (v, x, y) => x < 0 || y < 0 || x >= v.gw || y >= v.gh || v.grid[y * v.gw + x] !== 0;
  const DX = [0, 1, 0, -1];
  const DY = [-1, 0, 1, 0];
  const dodge = (v) => {
    if (v.tickN === 0) return 1; // leave the shared column before the head-on
    const [x, y] = v.pos;
    if (!blocked(v, x + DX[v.dir], y + DY[v.dir])) return null;
    const opts = [(v.dir + 1) % 4, (v.dir + 3) % 4].filter((d) => !blocked(v, x + DX[d], y + DY[d]));
    return opts.length ? opts[Math.floor(rng() * opts.length)] : null;
  };
  const gym = new NeonGridGym({ players: 2 });
  let dodgerWins = 0;
  for (let ep = 0; ep < 20; ep++) {
    let views = gym.reset();
    for (;;) {
      const r = gym.step([dodge(views[0]), null]); // seat 1 never turns
      if (r.done) {
        if (r.winner === 0) dodgerWins++;
        break;
      }
      views = r.views;
    }
  }
  assert.ok(dodgerWins >= 18, `dodger should dominate a straight-liner (won ${dodgerWins}/20)`);
});

test("neon-grid gym: rewards shape as documented", () => {
  const gym = new NeonGridGym({ players: 2 });
  gym.reset();
  const first = gym.step([null, null]);
  assert.deepEqual(first.rewards, [0.05, 0.05], "both alive, both paid the living wage");
  // ride to the head-on draw
  let r = first;
  while (!r.done) r = gym.step([null, null]);
  assert.deepEqual(r.rewards, [-1, -1], "a draw pays both riders the death penalty");
  assert.equal(r.winner, null);
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/peer-kit.git