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);
});
|