Files
ttrpg-initiative-tracker/scripts/combat.js
T

134 lines
4.7 KiB
JavaScript
Raw Normal View History

#!/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);
}
}