1
0
Fork 0
mirror of https://github.com/imjasonh/uno synced 2026-07-06 22:52:55 +00:00
uno/simulate.js
Jason Hall e8c13752e2 Add Milestone 4: Human player interaction
- Add HumanStrategy.js with Promise-based async input
- Add Setup.jsx for game configuration (player count, human position)
- Make Game.js async to support human player turns
- Update GameController with human turn handling
- Add human-basic.spec.js E2E tests with screenshots
- Update existing tests for async game engine and setup screen

Signed-off-by: Jason Hall <jason@chainguard.dev>
2026-01-16 17:04:34 -05:00

252 lines
7 KiB
JavaScript
Executable file

#!/usr/bin/env node
import { Game } from "./src/game/Game.js";
import { BasicStrategy } from "./src/strategies/BasicStrategy.js";
import { Reporter } from "./src/report/Reporter.js";
import { mkdirSync, writeFileSync } from "fs";
import { dirname } from "path";
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const config = {
players: 2,
verbose: false,
count: 1,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--players" && i + 1 < args.length) {
config.players = parseInt(args[i + 1], 10);
i++;
} else if (args[i] === "--count" && i + 1 < args.length) {
config.count = parseInt(args[i + 1], 10);
i++;
} else if (args[i] === "--verbose") {
config.verbose = true;
} else if (args[i] === "--help" || args[i] === "-h") {
console.log(`
Uno Game Simulator
Usage: node simulate.js [options]
Options:
--players N Number of players (2-6, default: 2)
--count N Number of games to simulate (default: 1)
--verbose Enable verbose output showing each turn (ignored when count > 1)
--help, -h Show this help message
Examples:
node simulate.js
node simulate.js --players 4
node simulate.js --verbose
node simulate.js --players 6 --verbose
node simulate.js --count 100
node simulate.js --count 1000 --players 4
`);
process.exit(0);
}
}
return config;
}
/**
* Run a single game simulation
* @param {Object} config - Configuration object with players and verbose settings
* @returns {Object} Game result with statistics
*/
async function runSingleGame(config) {
const { players, verbose } = config;
// Create player configurations
const playerConfigs = [];
for (let i = 0; i < players; i++) {
playerConfigs.push({
name: `Player ${i + 1}`,
strategy: new BasicStrategy(),
});
}
// Create game and reporter
const game = new Game(playerConfigs);
const reporter = new Reporter();
// Attach event listeners for verbose mode
if (verbose) {
game.onTurnStart = (player, topCard, activeColor) => {
console.log(
`\n[Turn ${game.turnCount + 1}] ${player.name}'s turn | Top card: ${topCard.toString()} | Active color: ${activeColor} | Hand: ${player.handSize()} cards`,
);
if (game.pendingDraw > 0) {
console.log(
` Pending draw: ${game.pendingDraw} cards (${game.pendingDrawType})`,
);
}
};
game.onCardPlayed = (player, card) => {
console.log(`${player.name} plays ${card.toString()}`);
};
game.onCardDrawn = (player, count) => {
console.log(`${player.name} draws ${count} card(s)`);
};
game.onUno = (player) => {
console.log(`${player.name} calls UNO!`);
};
game.onGameEnd = (winner) => {
console.log(`\n🎉 ${winner.name} wins in ${game.turnCount} turns!`);
};
}
// Attach reporter listeners
reporter.recordGameStart(players);
game.onCardPlayed = ((existingHandler) => {
return (player, card) => {
if (existingHandler) existingHandler(player, card);
reporter.recordCardPlayed(player, card);
};
})(game.onCardPlayed);
game.onCardDrawn = ((existingHandler) => {
return (player, count) => {
if (existingHandler) existingHandler(player, count);
reporter.recordCardDrawn(player, count);
};
})(game.onCardDrawn);
game.onUno = ((existingHandler) => {
return (player) => {
if (existingHandler) existingHandler(player);
reporter.recordUno(player);
};
})(game.onUno);
// Run the game
if (!verbose) {
console.log(`Starting Uno game with ${players} players...`);
}
const winner = await game.run();
reporter.recordGameEnd(winner, game.turnCount);
// Print result
if (!verbose) {
console.log(`${winner.name} wins in ${game.turnCount} turns!`);
}
// Return game result
return reporter.toJSON();
}
/**
* Run multiple game simulations and generate aggregate report
* @param {Object} config - Configuration object
* @returns {Object} Aggregate statistics
*/
async function runBatchGames(config) {
const { count, players } = config;
const results = [];
const startTime = Date.now();
console.log(`Running ${count} games with ${players} players...`);
for (let i = 0; i < count; i++) {
// Show progress every 10% or every 100 games, whichever is smaller
const progressInterval = Math.min(100, Math.max(1, Math.floor(count / 10)));
if (i > 0 && i % progressInterval === 0) {
const percentage = ((i / count) * 100).toFixed(0);
console.log(`Progress: ${i}/${count} (${percentage}%)`);
}
// Run game without verbose output
const result = await runSingleGame({ players, verbose: false });
results.push(result);
}
const endTime = Date.now();
const totalDurationMs = endTime - startTime;
console.log(`Progress: ${count}/${count} (100%)`);
console.log(`Completed in ${(totalDurationMs / 1000).toFixed(1)}s`);
return Reporter.aggregateResults(results, totalDurationMs);
}
/**
* Main simulation function
*/
async function main() {
const config = parseArgs();
// Validate player count
if (config.players < 2 || config.players > 6) {
console.error("Error: Player count must be between 2 and 6");
process.exit(1);
}
// Validate count
if (config.count < 1 || !Number.isInteger(config.count)) {
console.error("Error: Count must be a positive integer");
process.exit(1);
}
// Run batch or single game
if (config.count === 1) {
// Single game mode
const result = await runSingleGame(config);
// Write report
const reportPath = "./reports/report.json";
try {
mkdirSync(dirname(reportPath), { recursive: true });
const json = JSON.stringify(result, null, 2);
writeFileSync(reportPath, json, "utf8");
console.log(`\nReport written to ${reportPath}`);
} catch (error) {
console.error(`Error writing report: ${error.message}`);
process.exit(1);
}
} else {
// Batch mode
const aggregateReport = await runBatchGames(config);
// Write aggregate report with timestamp
const timestamp = new Date()
.toISOString()
.replace(/[-:]/g, "")
.replace(/\..+/, "")
.replace("T", "-");
const reportPath = `./reports/aggregate-report-${timestamp}.json`;
try {
mkdirSync(dirname(reportPath), { recursive: true });
const json = JSON.stringify(aggregateReport, null, 2);
writeFileSync(reportPath, json, "utf8");
// Print summary
console.log(`\nWinner distribution:`);
for (const [player, stats] of Object.entries(aggregateReport.wins)) {
console.log(
` ${player}: ${stats.count} wins (${stats.percentage.toFixed(1)}%)`,
);
}
console.log(
`\nAverage game length: ${aggregateReport.turns.mean.toFixed(1)} turns (min: ${aggregateReport.turns.min}, max: ${aggregateReport.turns.max})`,
);
console.log(`\nReport written to ${reportPath}`);
} catch (error) {
console.error(`Error writing report: ${error.message}`);
process.exit(1);
}
}
}
main();