Drop7 Research
src/core/typescript/gray-throughput-rollout.test.tsTypeScript118 lines · 3.0 KB
import assert from "node:assert/strict";
import test from "node:test";
import {
  BOARD_SIZE,
  EMPTY,
  MOVES_PER_LEVEL,
  SOLID,
  boardFromRows,
  type Board,
  type Cell,
  type GameState,
} from "./engine.ts";
import { evaluateGrayRolloutMoves } from "./gray-throughput-rollout.ts";

const E = EMPTY;
const row = (...cells: Cell[]) => cells;
const blank = () => row(E, E, E, E, E, E, E);

function position(board: Board, overrides: Partial<GameState> = {}): GameState {
  return {
    board,
    nextDisc: 5,
    score: 0,
    level: 1,
    movesRemaining: MOVES_PER_LEVEL,
    movesPlayed: 0,
    gameOver: false,
    ...overrides,
  };
}

test("gray rollouts are deterministic, score independent, and mirror exact", () => {
  const board = boardFromRows([
    blank(),
    blank(),
    blank(),
    blank(),
    row(E, E, 2, E, E, E, E),
    row(4, E, 6, E, E, E, E),
    row(SOLID, E, 6, 7, E, E, E),
  ]);
  const options = {
    scenarios: 2,
    horizon: 3,
    guideSamples: 1,
    policySeed: 0x1234_5678,
  };
  const forward = evaluateGrayRolloutMoves(position(board), options);
  const repeated = evaluateGrayRolloutMoves(position(board), options);
  const rescored = evaluateGrayRolloutMoves(
    position(board, { score: 1_000_000 }),
    options,
  );
  const mirrored = evaluateGrayRolloutMoves(
    position(mirrorBoard(board)),
    options,
  );
  const mirroredValues = new Map(
    mirrored.columns.map((evaluation) => [evaluation.column, evaluation]),
  );

  assert.deepEqual(repeated, forward);
  assert.deepEqual(rescored, forward);
  for (const evaluation of forward.columns) {
    const opposite = mirroredValues.get(BOARD_SIZE - 1 - evaluation.column);
    assert.ok(opposite);
    assert.equal(evaluation.mean, opposite.mean);
    assert.equal(evaluation.utility, opposite.utility);
  }
  assert.equal(
    forward.bestColumn,
    mirrored.bestColumn === null
      ? null
      : BOARD_SIZE - 1 - mirrored.bestColumn,
  );
});

test("gray rollout options and terminal states are bounded", () => {
  const state = position(boardFromRows([
    blank(),
    blank(),
    blank(),
    blank(),
    blank(),
    blank(),
    row(SOLID, SOLID, SOLID, SOLID, SOLID, SOLID, SOLID),
  ]));

  assert.throws(
    () => evaluateGrayRolloutMoves(state, { scenarios: 0 }),
    /scenarios/,
  );
  assert.throws(
    () => evaluateGrayRolloutMoves(state, { horizon: 41 }),
    /horizon/,
  );
  assert.throws(
    () => evaluateGrayRolloutMoves(state, { riskAversion: -1 }),
    /riskAversion/,
  );
  assert.throws(
    () => evaluateGrayRolloutMoves(state, { policySeed: 0x1_0000_0000 }),
    /uint32/,
  );
  const terminal = evaluateGrayRolloutMoves({ ...state, gameOver: true });
  assert.equal(terminal.bestColumn, null);
  assert.deepEqual(terminal.columns, []);
});

function mirrorBoard(board: Board): Board {
  const result: Cell[] = [];
  for (let rowIndex = 0; rowIndex < BOARD_SIZE; rowIndex += 1) {
    for (let column = BOARD_SIZE - 1; column >= 0; column -= 1) {
      result.push(board[rowIndex * BOARD_SIZE + column]);
    }
  }
  return result;
}