129 lines
6.1 KiB
JavaScript
129 lines
6.1 KiB
JavaScript
// Headless verification for Cribbage.
|
||
// node server/scripts/verifyCribbage.js [--games=N]
|
||
// Exits non-zero on any failure.
|
||
//
|
||
// 1. Fixture tests: canonical hands (perfect 29, flush, nobs) and pegging
|
||
// combinations (fifteen, pairs, runs) score the known values.
|
||
// 2. Self-play: full games driven by the heuristic AI on both seats, asserting
|
||
// invariants (scores never pass 121 before a win, the play always terminates,
|
||
// a winner is reached) over many seeded games.
|
||
|
||
import { Card } from '../../public/src/games/cribbage/CribbageData.js';
|
||
import { CribbageLogic, scoreHand, scorePlay } from '../../public/src/games/cribbage/CribbageLogic.js';
|
||
import { chooseDiscard, choosePlay } from '../../public/src/games/cribbage/CribbageAI.js';
|
||
|
||
let failures = 0;
|
||
function check(name, cond, detail = '') {
|
||
if (cond) { console.log(` ok ${name}`); return; }
|
||
failures++;
|
||
console.error(` FAIL ${name}${detail ? ` — ${detail}` : ''}`);
|
||
}
|
||
|
||
function mulberry32(seed) {
|
||
let a = seed >>> 0;
|
||
return () => {
|
||
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||
};
|
||
}
|
||
const C = (rank, suit) => new Card(rank, suit);
|
||
|
||
// ── 1. Scoring fixtures ─────────────────────────────────────────────────────
|
||
console.log('Scoring fixtures:');
|
||
{
|
||
// Perfect 29: J♥ + 5♠ 5♣ 5♦, cut 5♥.
|
||
const hand = [C('J', 'h'), C('5', 's'), C('5', 'c'), C('5', 'd')];
|
||
const r = scoreHand(hand, C('5', 'h'), false);
|
||
check('perfect hand scores 29', r.total === 29, `got ${r.total}`);
|
||
check(' → 16 in fifteens', r.fifteens === 16, `got ${r.fifteens}`);
|
||
check(' → 12 in pairs', r.pairs === 12, `got ${r.pairs}`);
|
||
check(' → 1 for nobs', r.nobs === 1, `got ${r.nobs}`);
|
||
}
|
||
{
|
||
// Four-card hand flush, starter off-suit → 4; on-suit → 5.
|
||
const hand = [C('2', 's'), C('4', 's'), C('6', 's'), C('9', 's')];
|
||
check('4-card flush, off-suit cut → 4', scoreHand(hand, C('K', 'h')).flush === 4);
|
||
check('5-card flush, on-suit cut → 5', scoreHand(hand, C('K', 's')).flush === 5);
|
||
}
|
||
{
|
||
// Crib needs all five suited; a 4-card crib flush does not count.
|
||
const crib = [C('2', 's'), C('4', 's'), C('6', 's'), C('9', 's')];
|
||
check('crib 4-flush off-suit → 0', scoreHand(crib, C('K', 'h'), true).flush === 0);
|
||
check('crib 5-flush on-suit → 5', scoreHand(crib, C('K', 's'), true).flush === 5);
|
||
}
|
||
{
|
||
// Nobs: Jack matching the starter suit.
|
||
const hand = [C('J', 'd'), C('3', 's'), C('7', 'c'), C('9', 'h')];
|
||
check('nobs when J matches starter suit', scoreHand(hand, C('A', 'd')).nobs === 1);
|
||
check('no nobs when J off-suit', scoreHand(hand, C('A', 'c')).nobs === 0);
|
||
}
|
||
{
|
||
// A double run of three with a pair = 8 (run 3×2 + pair 2) plus fifteens.
|
||
const r = scoreHand([C('3', 's'), C('4', 's'), C('5', 'c'), C('5', 'd')], C('6', 'h'));
|
||
// runs: 3-4-5-6 doubled (two 5s) = 8; pair of 5s = 2; fifteens: (4+5+6),(4+5+6),(5+5+... )
|
||
check('double run + pair runs = 8', r.runs === 8, `got ${r.runs}`);
|
||
check('double run pair = 2', r.pairs === 2, `got ${r.pairs}`);
|
||
}
|
||
|
||
// ── 2. Pegging fixtures ─────────────────────────────────────────────────────
|
||
console.log('Pegging fixtures:');
|
||
check('fifteen pegs 2', scorePlay([C('7', 's'), C('8', 'd')]).points === 2);
|
||
check('thirty-one pegs 2', scorePlay([C('K', 's'), C('T', 'd'), C('6', 'c'), C('5', 'h')]).points === 2);
|
||
check('pair pegs 2', scorePlay([C('4', 's'), C('4', 'd')]).points === 2);
|
||
check('pair-royal pegs 6', scorePlay([C('4', 's'), C('4', 'd'), C('4', 'c')]).points === 6);
|
||
check('run of 3 pegs 3', scorePlay([C('3', 's'), C('5', 'd'), C('4', 'c')]).points === 3);
|
||
check('run of 4 pegs 4', scorePlay([C('3', 's'), C('5', 'd'), C('4', 'c'), C('6', 'h')]).points === 4);
|
||
check('broken run scores no run', scorePlay([C('3', 's'), C('5', 'd'), C('8', 'c')]).points === 0);
|
||
|
||
// ── 3. Self-play ────────────────────────────────────────────────────────────
|
||
const games = Number((process.argv.find((a) => a.startsWith('--games=')) || '').split('=')[1]) || 500;
|
||
console.log(`Self-play (${games} games):`);
|
||
|
||
function runPlay(g) {
|
||
let guard = 0;
|
||
while (g.phase === 'play') {
|
||
if (++guard > 200) throw new Error('pegging did not terminate');
|
||
const p = g.turn;
|
||
const legal = g.legalPlays(p);
|
||
if (legal.length === 0) g.go(p);
|
||
else g.play(p, choosePlay(legal, g.pile, g.count, 3 + (p % 2)));
|
||
}
|
||
}
|
||
|
||
let wins = [0, 0], maxRounds = 0, overflow = false, exceptions = 0;
|
||
for (let s = 1; s <= games; s++) {
|
||
try {
|
||
const g = new CribbageLogic({ rng: mulberry32(s * 2654435761), startDealer: s % 2 });
|
||
let rounds = 0;
|
||
while (g.winner === null) {
|
||
if (++rounds > 300) throw new Error('game did not terminate');
|
||
g.newDeal();
|
||
for (const p of [0, 1]) g.discard(p, chooseDiscard(g.hands[p], p === g.dealer, p === 0 ? 4 : 3));
|
||
g.cut();
|
||
if (g.scores[0] > 121 || g.scores[1] > 121) overflow = true;
|
||
if (g.winner !== null) break;
|
||
runPlay(g);
|
||
if (g.scores[0] > 121 || g.scores[1] > 121) overflow = true;
|
||
if (g.winner !== null) break;
|
||
g.show();
|
||
if (g.scores[0] > 121 || g.scores[1] > 121) overflow = true;
|
||
if (g.winner !== null) break;
|
||
g.dealer = 1 - g.dealer;
|
||
}
|
||
wins[g.winner]++;
|
||
maxRounds = Math.max(maxRounds, rounds);
|
||
} catch (e) {
|
||
exceptions++;
|
||
if (exceptions <= 3) console.error(` game ${s}: ${e.message}`);
|
||
}
|
||
}
|
||
check('no exceptions during self-play', exceptions === 0, `${exceptions} games threw`);
|
||
check('scores never exceed 121', !overflow);
|
||
check('both seats win some games', wins[0] > 0 && wins[1] > 0, `wins ${wins[0]}/${wins[1]}`);
|
||
console.log(` results: human ${wins[0]} / ai ${wins[1]}, longest game ${maxRounds} deals`);
|
||
|
||
console.log(failures ? `\n${failures} check(s) FAILED` : '\nAll checks passed.');
|
||
process.exit(failures ? 1 : 0);
|