#!/usr/bin/env node // scripts/combat.js // ONE tool. Replaces replay-combat.js + analyze-turns.js + replay-from-logs.js. // // Two modes (same binary): // // combat replay — drive a fresh combat through LIVE backend, write log // combat verify — read any log file, check combat rotated correctly // // WHY ONE TOOL: // Same data, same words. Old 3-tool split = format + naming nightmare. // Replay writes the log, verify reads it. Same log shape both ends. // // LOG FORMAT: // JSON array. Same as app download. One mental model. NOT jsonl. // Each entry = canonical lean event (shared/logEvent.js shape): // { id, ts, type, message, encounterId, encounterName, encounterPath, // participantId, participantName, delta, undo, undone, snapshot } // snapshot = { round, currentTurnParticipantId, turnOrderIds, activeIds } // // USAGE: // combat replay [rounds] [delayMs] --out [-v] // combat verify // cat events.json | combat verify // // EXIT: // replay: 0 success, 1 error, 2 bad args // verify: 0 all checks pass, 1 bugs found, 2 bad args/no input // // WHAT VERIFY CHECKS (in DM words, not internals): // - Round count go up correctly (no skip, no jump, no backward) // - Turn pass to right person each step // - Nobody act twice in same round // - Nobody get skipped who was active whole round // - Turn order stay stable (no random reshuffle) // - Initiative slot order hold (replay logs only — need full roster) 'use strict'; const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const shared = require('../shared'); const { normalizeEvent, serializeEvents } = shared.logEvent; // ============================================================================= // ARGS // ============================================================================= function parseArgs(argv) { const mode = argv[0]; const rest = argv.slice(1); if (mode !== 'replay' && mode !== 'verify') return { mode: null }; const out = { mode, verbose: false, out: null, positional: [] }; for (let i = 0; i < rest.length; i++) { const a = rest[i]; if (a === '-v' || a === '--verbose') { out.verbose = true; continue; } if (a === '--out' && rest[i + 1]) { out.out = rest[i + 1]; i++; continue; } out.positional.push(a); } return out; } function usageReplay() { console.error('Usage: combat replay [rounds] [delayMs] --out [-v]'); console.error(' --out REQUIRED. Path you control.'); console.error(' rounds default 20, delayMs default 200'); console.error(' -v / --verbose log every action per turn'); process.exit(2); } function usageVerify() { console.error('Usage: combat verify '); console.error(' cat events.json | combat verify'); process.exit(2); } const args = parseArgs(process.argv.slice(2)); if (!args.mode) { console.error('combat — unified replay + verify for the TTRPG combat tracker'); console.error(''); console.error('Usage:'); console.error(' combat replay [rounds] [delayMs] --out [-v]'); console.error(' combat verify '); console.error(' cat events.json | combat verify'); process.exit(2); } // ============================================================================= // REPLAY MODE // ============================================================================= // Drive fresh combat through live backend. Each mutation = one lean log entry // written to --out path. DM-facing stdout: round headers + per-turn actions. // Same log shape as app download. verify reads it back. if (args.mode === 'replay') { if (!args.out) usageReplay(); require('./combat/replay.js')(args).catch(err => { console.error('replay failed:', err); process.exit(1); }); } // ============================================================================= // VERIFY MODE // ============================================================================= // Read a combat log (app download OR combat replay output). Check rotation // correctness. DM-facing output: combat name, rounds, per-round turn list, // checks pass/fail. No "mutation", "pointer", "invariant" in output. if (args.mode === 'verify') { const logPath = args.positional[0]; let text; if (logPath) { if (!fs.existsSync(logPath)) { console.error(`combat verify: file not found: ${logPath}`); process.exit(2); } text = fs.readFileSync(logPath, 'utf8'); } else if (!process.stdin.isTTY) { text = fs.readFileSync(0, 'utf8'); } if (!text || !text.trim()) usageVerify(); try { const result = require('./combat/verify.js')(text, { verbose: args.verbose }); process.exit(result); } catch (err) { console.error('verify failed:', err); process.exit(1); } }