Rework backend #1
@@ -20,7 +20,7 @@ const fs = require('fs');
|
||||
|
||||
// ---------- parsing ----------
|
||||
|
||||
const TURN_RE = /^\s*turn\s+(\d+)\s+\(round\s+(\d+)\):\s+(.+?)(?:\s*\|.*)?\s*$/;
|
||||
const TURN_RE = /^\s*turn\s+(\d+)\s+\(round\s+(\d+)\):\s+(.+?)(?:\s*\|\s*order=\[(.*)\](?:\s*cur=.*)?)?\s*$/;
|
||||
const DEACTIVATE_RE = /^\s*\[(?:deactivate)\s+(.+?)\]\s*$/;
|
||||
const REACTIVATE_RE = /^\s*\[(?:revive-reactivate|reactivate)\s+(.+?)\]\s*$/;
|
||||
const ADD_RE = /^\s*\[(?:add)\s+(.+?)\]\s*$/;
|
||||
@@ -29,12 +29,19 @@ const PAUSE_RE = /^\s*\[pause\]\s*$/;
|
||||
const RESUME_RE = /^\s*\[resume\]\s*$/;
|
||||
const ROUND_COMPLETE_RE = /^\s*---\s*round\s+(\d+)\s+complete/;
|
||||
const FIRST_RE = /^combat started:\s+round\s+\d+,\s+first=(.+?)\s*$/;
|
||||
const REORDER_RE = /^\s*\[reorder\s+(.+?)→before\s+(.+?)\]\s*$/;
|
||||
const POINTER_RE = /^\s*\[pointer\s+(.+?)→(.+?)( wrap)?\]\s*$/;
|
||||
|
||||
function parseLine(line) {
|
||||
if (TURN_RE.test(line)) {
|
||||
const m = line.match(TURN_RE);
|
||||
return { kind: 'turn', turn: +m[1], round: +m[2], actor: m[3].trim() };
|
||||
const orderStr = m[4] || '';
|
||||
// parse Name:init pairs
|
||||
const order = orderStr.split(',').map(s => s.trim()).filter(Boolean).map(pair => {
|
||||
const [name, init] = pair.split(':');
|
||||
return { name: name.trim(), init: init !== undefined ? +init : null };
|
||||
});
|
||||
return { kind: 'turn', turn: +m[1], round: +m[2], actor: m[3].trim(), order };
|
||||
}
|
||||
if (FIRST_RE.test(line)) {
|
||||
const m = line.match(FIRST_RE);
|
||||
@@ -50,6 +57,10 @@ function parseLine(line) {
|
||||
const m = line.match(POINTER_RE);
|
||||
return { kind: 'pointer', from: m[1].trim(), to: m[2].trim(), wrap: m[3] === ' wrap' };
|
||||
}
|
||||
if (REORDER_RE.test(line)) {
|
||||
const m = line.match(REORDER_RE);
|
||||
return { kind: 'reorder', dragged: m[1].trim(), target: m[2].trim() };
|
||||
}
|
||||
if (ROUND_COMPLETE_RE.test(line)) return { kind: 'round-complete', round: +line.match(ROUND_COMPLETE_RE)[1] };
|
||||
return null;
|
||||
}
|
||||
@@ -91,6 +102,9 @@ function reconstruct(events) {
|
||||
if (ev.wrap) curRound += 1;
|
||||
const r = rounds.get(curRound) || rounds.set(curRound, { turns: [], events: [], complete: false }).get(curRound);
|
||||
r.events.push({ ...ev, idx: r.events.length });
|
||||
} else if (ev.kind === 'reorder') {
|
||||
const r = rounds.get(curRound) || rounds.set(curRound, { turns: [], events: [], complete: false }).get(curRound);
|
||||
r.events.push({ ...ev, idx: r.events.length });
|
||||
} else if (ev.kind === 'round-complete') {
|
||||
if (rounds.has(ev.round)) rounds.get(ev.round).complete = true;
|
||||
}
|
||||
@@ -198,6 +212,52 @@ function analyzeRoundWithCarry(roundN, r, activeAtStart, currentAtStart) {
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- order-shift detection ----------
|
||||
// Compare order+init between consecutive turn lines. Flag shifts NOT explained
|
||||
// by: logged reorder, add/remove (roster change), or initiative change.
|
||||
// DM drag-reorder = legit (logged reorder line). Phantom shifts = display/rotation
|
||||
// divergence bug (invariant: display === turnOrderIds === nextTurn).
|
||||
function detectOrderShifts(events) {
|
||||
const shifts = [];
|
||||
let prev = null;
|
||||
let prevTurnNo = null;
|
||||
// mutations since last turn (reorder/add/remove/reactivate/pointer)
|
||||
let pending = [];
|
||||
let initMap = {}; // name -> last known initiative
|
||||
|
||||
for (const ev of events) {
|
||||
if (ev.kind === 'turn' && ev.order && ev.order.length) {
|
||||
const curNames = ev.order.map(o => o.name);
|
||||
const curInits = {};
|
||||
ev.order.forEach(o => { curInits[o.name] = o.init; });
|
||||
|
||||
if (prev) {
|
||||
const sameRoster = prev.length === curNames.length &&
|
||||
prev.every((n, i) => n === curNames[i]);
|
||||
if (!sameRoster) {
|
||||
// roster change (add/remove) — skip, expected order shift
|
||||
} else {
|
||||
// same roster, different order → explainable by reorder OR init change?
|
||||
const orderChanged = JSON.stringify(prev) !== JSON.stringify(curNames);
|
||||
const initChanged = ev.order.some(o => initMap[o.name] !== null && initMap[o.name] !== undefined && initMap[o.name] !== o.init);
|
||||
const hasReorder = pending.some(p => p.kind === 'reorder');
|
||||
if (orderChanged && !hasReorder && !initChanged) {
|
||||
shifts.push({ turn: ev.turn, from: prev, to: curNames, reason: 'no logged reorder/init change' });
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = curNames;
|
||||
curInits && Object.keys(curInits).forEach(k => { initMap[k] = curInits[k]; });
|
||||
pending = [];
|
||||
prevTurnNo = ev.turn;
|
||||
} else if (ev.kind === 'reorder' || ev.kind === 'add' || ev.kind === 'remove' ||
|
||||
ev.kind === 'reactivate' || ev.kind === 'pointer') {
|
||||
pending.push(ev);
|
||||
}
|
||||
}
|
||||
return shifts;
|
||||
}
|
||||
|
||||
// ---------- CLI ----------
|
||||
|
||||
function readInput() {
|
||||
@@ -232,12 +292,24 @@ function main() {
|
||||
console.log(` sequence: ${rep.turns.join(' -> ')}`);
|
||||
}
|
||||
|
||||
// order-shift detection: flag unexplained display/rotation divergence
|
||||
const shifts = detectOrderShifts(events);
|
||||
if (shifts.length) {
|
||||
console.log(`\n--- order shifts (${shifts.length}) ---`);
|
||||
for (const s of shifts.slice(0, 10)) {
|
||||
console.log(` turn ${s.turn}: [${s.from.join(',')}] → [${s.to.join(',')}] (${s.reason})`);
|
||||
}
|
||||
if (shifts.length > 10) console.log(` ... +${shifts.length - 10} more`);
|
||||
}
|
||||
|
||||
console.log(`\n=== ${reports.length} rounds analyzed ===`);
|
||||
console.log(`real skips: ${totalSkips}`);
|
||||
console.log(`double-acts: ${totalDoubles}`);
|
||||
console.log(totalSkips === 0 && totalDoubles === 0 ? 'CLEAN — no rotation bugs' : 'ISSUES FOUND');
|
||||
console.log(`order shifts: ${shifts.length}`);
|
||||
const clean = totalSkips === 0 && totalDoubles === 0 && shifts.length === 0;
|
||||
console.log(clean ? 'CLEAN — no rotation bugs' : 'ISSUES FOUND');
|
||||
|
||||
process.exit(totalSkips === 0 && totalDoubles === 0 ? 0 : 1);
|
||||
process.exit(clean ? 0 : 1);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
+22
-10
@@ -178,7 +178,13 @@ async function main() {
|
||||
const actorName = firstActiveName(enc);
|
||||
const actor = currentParticipant(enc);
|
||||
|
||||
console.log(` turn ${totalTurns} (round ${enc.round}): ${actorName} | order=[${enc.turnOrderIds.map(id=>enc.participants.find(p=>p.id===id)?.name||id).join(',')}] cur=${enc.currentTurnParticipantId}`);
|
||||
// Dump turn line with order AND initiative (DM drag may reorder without
|
||||
// changing init — log both so parser can flag unexplained shifts).
|
||||
const ordStr = enc.turnOrderIds.map(id => {
|
||||
const p = enc.participants.find(x => x.id === id);
|
||||
return p ? `${p.name}:${p.initiative}` : id;
|
||||
}).join(',');
|
||||
console.log(` turn ${totalTurns} (round ${enc.round}): ${actorName} | order=[${ordStr}] cur=${enc.currentTurnParticipantId}`);
|
||||
|
||||
// 1. damage: actor hits a random living, active target.
|
||||
if (actor) {
|
||||
@@ -302,19 +308,25 @@ async function main() {
|
||||
lastPaused = true;
|
||||
}
|
||||
|
||||
// 10. reorderParticipants: every 8 turns, shuffle initiative slightly.
|
||||
// 10. reorderParticipants: every 8 turns, drag one past next (DM reorder).
|
||||
if (totalTurns % 8 === 0 && lastReorder !== totalTurns) {
|
||||
const living = enc.participants.filter(p => p.currentHp > 0 && p.isActive !== false);
|
||||
if (living.length >= 2) {
|
||||
// bump first participant's initiative by +1 (deterministic reorder).
|
||||
const tgt = living[0];
|
||||
const newInit = (tgt.initiative || 0) + 1;
|
||||
if (living.length >= 3) {
|
||||
// drag first past second (same-or-cross init, exercises reorder).
|
||||
const dragged = living[0];
|
||||
const target = living[1];
|
||||
try {
|
||||
const reordered = [...enc.participants].map(p => p.id === tgt.id ? { ...p, initiative: newInit } : p);
|
||||
const r = reorderParticipants(enc, reordered);
|
||||
enc = await patch(encounterPath, enc, r, `reorder (${tgt.name} init→${newInit})`);
|
||||
const r = reorderParticipants(enc, dragged.id, target.id);
|
||||
enc = await patch(encounterPath, enc, r, `reorder ${dragged.name}→before ${target.name}`);
|
||||
lastReorder = totalTurns;
|
||||
} catch (e) { /* ignore */ }
|
||||
} catch (e) { /* same-init only — try same-init pair */
|
||||
const sameInit = living.find(p => p !== dragged && p.initiative === dragged.initiative);
|
||||
if (sameInit) {
|
||||
const r = reorderParticipants(enc, dragged.id, sameInit.id);
|
||||
enc = await patch(encounterPath, enc, r, `reorder ${dragged.name}→before ${sameInit.name}`);
|
||||
lastReorder = totalTurns;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user