peer-kit / test / valley-blocks-engine.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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Valley-blocks engine — rule fidelity against game2's browser core/game.ts.
import test from "node:test";
import assert from "node:assert/strict";
import { ValleyBlocksEngine } from "../dist/games/host/valley-blocks-engine.js";
import { Bag, PIECE_TYPES, COLS, TOTAL_ROWS, HIDDEN_ROWS } from "../dist/games/host/valley-blocks-core.js";
import { mulberry32 } from "../dist/games/host/rng.js";
import { boardStateOf } from "../dist/games/host/valley-blocks-protocol.js";
import { enumeratePlacements, executePlacement } from "../dist/games/valley-blocks-brain.js";

test("the 7-bag deals every tetromino once per bag", () => {
  const bag = new Bag(mulberry32(42));
  const first = bag.peek(7);
  assert.deepEqual([...first].sort(), [...PIECE_TYPES].sort());
  const second = [];
  for (let i = 0; i < 14; i++) second.push(bag.next());
  assert.deepEqual(second.slice(7).sort(), [...PIECE_TYPES].sort(), "second bag is a fresh permutation");
});

test("same seed + same inputs → identical boards (the multiplayer contract)", () => {
  const a = new ValleyBlocksEngine(1234);
  const b = new ValleyBlocksEngine(1234);
  for (const e of [a, b]) {
    for (let piece = 0; piece < 12; piece++) {
      e.rotate(true);
      e.move(piece % 2 === 0 ? -1 : 1);
      e.move(piece % 2 === 0 ? -1 : 1);
      e.hardDrop();
      e.update(0.1);
    }
  }
  assert.deepEqual(boardStateOf(a), boardStateOf(b));
  assert.equal(a.score, b.score);
  assert.deepEqual(a.nextQueue, b.nextQueue);
});

test("hard drop scores 2 points per row and locks immediately", () => {
  const e = new ValleyBlocksEngine(7);
  const dist = e.ghostY - e.active.y;
  assert.ok(dist > 0);
  e.hardDrop();
  assert.equal(e.score, dist * 2);
});

test("a full row clears, scores by level, and the level curve follows lines", () => {
  const e = new ValleyBlocksEngine(7);
  // hand-fill the bottom row except one gap, then check clearLines does its job
  for (let x = 0; x < COLS - 1; x++) e.board.grid[TOTAL_ROWS - 1][x] = "N";
  e.board.grid[TOTAL_ROWS - 1][COLS - 1] = "N";
  const cleared = e.board.clearLines();
  assert.deepEqual(cleared, [TOTAL_ROWS - 1]);
  assert.ok(e.board.grid[TOTAL_ROWS - 1].every((c) => c === 0), "rows collapse down");
});

test("locking into the hidden rows tops the board out", () => {
  const e = new ValleyBlocksEngine(99);
  // a solid stack up to the hidden rows, with one un-clearable gap per row
  for (let y = HIDDEN_ROWS; y < TOTAL_ROWS; y++) {
    for (let x = 0; x < COLS - 1; x++) e.board.grid[y][x] = "N";
  }
  e.hardDrop();
  assert.equal(e.over, true);
});

test("the snake arrives on the seeded schedule and freezes into N cells", () => {
  const seed = 555;
  const rng = mulberry32(seed ^ 0x5f356495);
  const r = rng();
  const gap = 6 + Math.floor(15 * r * r);
  const e = new ValleyBlocksEngine(seed);
  // park pieces at the walls so the center entry column stays clear
  for (let piece = 0; piece < gap; piece++) {
    assert.equal(e.snake, null, `no snake before piece ${gap}`);
    const dir = piece % 2 === 0 ? -1 : 1;
    for (let i = 0; i < COLS; i++) if (!e.move(dir)) break;
    e.hardDrop();
    assert.equal(e.over, false, "the parked stacks must not top out during the schedule");
  }
  assert.ok(e.snake, `snake spawns after ${gap} locked pieces`);
  assert.equal(e.snake.segments.length, 5);
  e.hardDrop(); // commit where it is
  assert.equal(e.snake, null);
  let frozen = 0;
  for (const row of e.board.grid) for (const c of row) if (c === "N") frozen++;
  assert.equal(frozen, 5, "the snake froze into N cells");
});

test("every enumerated candidate's afterstate matches the executed engine", () => {
  const seed = 2024;
  const reference = new ValleyBlocksEngine(seed);
  const candidates = enumeratePlacements(reference);
  assert.ok(candidates.length >= 17, "a fresh board offers rotations × columns");
  assert.ok(candidates.some((c) => c.useHold), "the hold branch is offered");
  for (const c of candidates) {
    const e = new ValleyBlocksEngine(seed);
    executePlacement(e, c);
    const got = boardStateOf(e).grid;
    // render the candidate's occupancy the same way (visible rows, dots/blocks)
    let want = "";
    for (let y = HIDDEN_ROWS; y < TOTAL_ROWS; y++) {
      for (let x = 0; x < COLS; x++) want += c.grid[y * COLS + x] === 0 ? "." : "#";
    }
    const gotMask = got.replace(/[^.]/g, "#");
    assert.equal(gotMask, want, `candidate rot=${c.rotation} x=${c.x} hold=${c.useHold}`);
  }
});

test("boardStateOf serializes the visible field", () => {
  const e = new ValleyBlocksEngine(3);
  const s = boardStateOf(e);
  assert.equal(s.grid.length, 200);
  assert.equal(s.over, false);
  assert.equal(s.snake, null);
});

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