134 lines
4.7 KiB
JavaScript
134 lines
4.7 KiB
JavaScript
#!/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 <log.json> [-v]
|
||
|
|
// combat verify <log.json>
|
||
|
|
// 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 <log.json> [-v]');
|
||
|
|
console.error(' --out <log.json> 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 <log.json>');
|
||
|
|
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 <log.json> [-v]');
|
||
|
|
console.error(' combat verify <log.json>');
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|