fertig-classic-games/server/scripts/verifyCribbage.js

129 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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