feat(replay+parser): log order+init, detect unexplained order shifts

replay-combat.js:
- Turn line now dumps order=[Name:init,...] (both, not names only)
- reorderParticipants call fixed: real drag (dragged→before target), correct
  signature (ids not array). Was broken (passed array, func wants ids,
  swallowed by try/catch silent no-op).

analyze-turns.js:
- Parse order=[Name:init,...] from turn lines
- detectOrderShifts: compare order+init between consecutive turns. Flag shifts
  NOT explained by logged reorder, roster change (add/remove), or init change.
  Catches display/rotation divergence (invariant: display===turnOrderIds===nextTurn).
- Report order shifts count + sample. CLEAN requires 0 shifts.

Result: 100-round replay CLEAN (0 skips, 0 doubles, 0 shifts).
Note: shift detector reads turnOrderIds dump. reorder still leaves turnOrderIds
unchanged (BUG-6) — Path A (step 3) aligns display+rotation, then shift
detector catches true divergence.
This commit is contained in:
david raistrick
2026-07-01 15:37:56 -04:00
parent fcddb58b8b
commit 94b62dc5ab
2 changed files with 98 additions and 14 deletions
+22 -10
View File
@@ -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;
}
}
}
}