// Valley-blocks gym — piece-synchronous self-play over the real referee.
import test from "node:test";
import assert from "node:assert/strict";
import { ValleyBlocksGym } from "../dist/gym/valley-blocks.js";
function playEpisode(gym, pick = () => null) {
let views = gym.reset();
const log = [];
for (let step = 0; step < 500; step++) {
const res = gym.step(views.map((v, seat) => pick(v, seat)));
log.push(res.rewards.join(","));
if (res.done) return { res, log, steps: step + 1 };
views = res.views;
}
return { res: null, log, steps: 500 };
}
test("two stock seats race to a decided verdict", () => {
const gym = new ValleyBlocksGym({ players: 2, seed: 11, mode: "race-survival", target: 1500 });
const { res, steps } = playEpisode(gym);
assert.ok(res, "episode ended");
assert.ok(res.winner === 0 || res.winner === 1);
assert.ok(["target", "survivor", "score"].includes(res.reason));
assert.ok(steps < 300, `Dellacherie reaches 1500 briskly (took ${steps})`);
const winnerView = res.views[res.winner];
if (res.reason === "target") assert.ok(winnerView.score >= 1500);
});
test("same gym seed → an identical episode (rewards and verdict)", () => {
const a = playEpisode(new ValleyBlocksGym({ players: 2, seed: 77, target: 1200 }));
const b = playEpisode(new ValleyBlocksGym({ players: 2, seed: 77, target: 1200 }));
assert.deepEqual(a.log, b.log);
assert.equal(a.res.winner, b.res.winner);
assert.equal(a.res.reason, b.res.reason);
});
test("views carry the live-client fields and both seats see the same pieces", () => {
const gym = new ValleyBlocksGym({ players: 3, seed: 5 });
const views = gym.reset();
assert.equal(views.length, 3);
for (const [seat, v] of views.entries()) {
assert.equal(v.seat, seat);
assert.equal(v.over, false);
assert.equal(v.mode, "race-survival");
assert.equal(v.target, 5000);
assert.equal(v.rivals.length, 2);
assert.ok(v.candidates.length > 0);
assert.ok(v.candidates.some((c) => c.useHold), "hold branch offered");
assert.equal(v.piece, views[0].piece, "shared seed deals identical pieces");
}
});
test("the placement cap forces a highest-score verdict", () => {
const gym = new ValleyBlocksGym({ players: 2, seed: 3, mode: "race", target: 10_000_000, maxPlacements: 10 });
let views = gym.reset();
let res = null;
for (let i = 0; i < 20; i++) {
res = gym.step(views.map(() => null));
if (res.done) break;
views = res.views;
}
assert.ok(res.done, "capped episode ends");
assert.equal(res.reason, "score");
});
test("rewards: score deltas flow every step, the winner gets the bonus", () => {
const gym = new ValleyBlocksGym({ players: 2, seed: 21, target: 800 });
let views = gym.reset();
let sawPositive = false;
for (let i = 0; i < 400; i++) {
const res = gym.step(views.map(() => null));
for (const r of res.rewards) if (r > 0 && r < 1) sawPositive = true;
if (res.done) {
assert.ok(res.rewards[res.winner] >= 1, "win bonus lands on the winner's last step");
break;
}
views = res.views;
}
assert.ok(sawPositive, "hard drops and clears show up as shaped reward");
});