mancala/apps/backend-quarkus/scripts/record-engine-games.js
Halit Aksoy f0ffd6ba87 feat(backend): add the Quarkus project with a Java port of the engine
Quarkus 3.40 on Java 25, built with the Maven wrapper. The engine is a
line-by-line port of packages/mancala.js; a parity test replays games
recorded with the TypeScript engine, including illegal moves, and
compares every board, turn, error and the game JSON.
2026-10-16 11:06:43 +03:00

62 lines
2.9 KiB
JavaScript

// Plays seeded random games with the TypeScript engine (packages/mancala.js) and writes every move and its outcome,
// so EngineParityTest can check that the Java engine plays exactly the same. Run it again after changing the rules:
//
// yarn --cwd ../../packages/mancala.js build && node scripts/record-engine-games.js
//
// Moves include illegal ones (the wrong player, an empty pit, a store, out of range) and moves after the end.
const fs = require("fs");
const path = require("path");
const { CommonMancalaGame } = require("../../../packages/mancala.js");
const GAME_COUNT = 15;
const MAX_MOVES = 400;
const OUTPUT = path.join(__dirname, "../src/test/resources/engine/recorded-games.json");
// mulberry32
function random(seed) {
return () => {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function recordGame(number) {
const next = random(number + 1);
const pick = (items) => items[Math.floor(next() * items.length)];
const game = new CommonMancalaGame(`game-${number}`, `kc:player1-${number}`, `kc:player2-${number}`);
game.turnPlayerId = next() < 0.5 ? game.player1Id : game.player2Id;
const recorded = {
id: game.id, player1Id: game.player1Id, player2Id: game.player2Id, turnPlayerId: game.turnPlayerId, moves: [],
};
let movesAfterEnd = 0;
for (let i = 0; i < MAX_MOVES && movesAfterEnd < 2; i++) {
const roll = next();
const opponent = game.turnPlayerId === game.player1Id ? game.player2Id : game.player1Id;
const playerId = roll < 0.93 ? game.turnPlayerId : opponent;
const pitsWithStones = [0, 1, 2, 3, 4, 5].filter((pit) =>
game.board.pits[game.getBoardIndexByPlayerId(playerId, pit)].stoneCount > 0);
const pitIndex = roll < 0.85 && pitsWithStones.length > 0 ? pick(pitsWithStones)
: roll < 0.97 ? Math.floor(next() * 6) : pick([-1, 6, 7, 13, 14]);
const move = { playerId, pitIndex };
try {
game.moveByPlayerPit(playerId, pitIndex);
} catch (err) {
move.error = err.message;
}
Object.assign(move, { board: game.board.getStoneArray(), turnPlayerId: game.turnPlayerId, state: game.state });
recorded.moves.push(move);
if (game.state === "ended") movesAfterEnd++;
}
recorded.wonPlayerId = game.getWonPlayerId() ?? null;
recorded.game = JSON.parse(JSON.stringify(game));
return recorded;
}
const games = Array.from({ length: GAME_COUNT }, (_, number) => recordGame(number));
fs.writeFileSync(OUTPUT, JSON.stringify({ games }) + "\n");
const moves = games.reduce((sum, game) => sum + game.moves.length, 0);
console.log(`Recorded ${games.length} games, ${moves} moves, to ${path.relative(process.cwd(), OUTPUT)}`);