Commit Graph

6 Commits

Author SHA1 Message Date
david raistrick 5d3a0607ef refactor: 1-list turn order model (turnOrderIds === participants.map(id))
Single source of truth. No re-sort after startEncounter. Drag overrides
initiative (cross-init drag allowed, DM choice). Display === rotation by
construction — same array.

shared/turn.js:
- syncTurnOrder(participants) helper: turnOrderIds = participants.map(id)
- startEncounter: sort ALL participants by init (active+inactive), inactive
  stay in slot, nextTurn skips them. currentTurn = first active.
- addParticipant: splice into participants[] by init pos, sync turnOrderIds.
  computeTurnOrderAfterAddition returns insertAt (caller splices + syncs).
- removeParticipant: filter participants[], sync turnOrderIds, advance
  current if removed==current.
- toggleParticipantActive: stay in slot (flip isActive only), sync. Advance
  current only if deact hits current.
- reorderParticipants: cross-init drag allowed (remove same-init restriction).
  Splice participants[], sync turnOrderIds. Fixes BUG-6.
- computeTurnOrderAfterRemoval: only handles current-advance now (list sync
  at call site).

Tests updated to 1-list contract:
- turn.invariant.test.js: 10 tests, turnOrderIds===participants.map(id)
  always, cross-init drag, inactive-in-slot, rotation follows list.
- turn.characterization/reorder/round-rotation/undo/remove: updated
  expectations (inactive-in-slot, cross-init drag, turnOrderIds sync on
  reorder, insertAt return).

Results: shared 90 green. 500-round replay CLEAN (0 skips, 0 doubles,
0 order shifts). BUG-6 (reorder divergence) fixed structurally.

FE App.js still has duplicate turn funcs + sortParticipantsByInitiative
display render (step 4: delete dups, render participants[] directly).
2026-07-01 16:00:00 -04:00
david raistrick 494327ff17 fix(BUG-5): unify turn-advance core (DRY), 500 rounds skip-free
Extract shared nextActiveAfter() advance core. Both nextTurn and
computeTurnOrderAfterRemoval delegate to it — single source of truth,
eliminates drift risk where one path changes and the other doesn't.

Previously two separate advance implementations computed the same
target, but any future edit to one would silently desync deact-current
advance from normal nextTurn advance.

Replay (scripts/replay-combat.js):
- Move turn-line print before mutations (event order = reality)
- Emit [pointer X→Y] lines when a mutation advances currentTurnParticipantId
- Emit [pointer X→Y wrap] when round bumps (removal-wrap case)
- Skip pointer emission for nextTurn (label=null) — already logged via turn line

Parser (scripts/analyze-turns.js):
- Parse [pointer X→Y wrap] events
- Credit pointer-target as acted (deact-current advance = turn pointer)
- Wrap pointer credits NEXT round (not current) — fixes cross-round false skip
- Drop currentRemoved special-case — pointer lines make skip check precise

Tests:
- shared/tests/turn.dry.test.js: 3 tests lock deact-current advance ==
  nextTurn advance (mid-round, inactive-skipper, wrap+round-bump). RED
  catches future drift.

Results: 500-round replay now 0 real skips, 0 double-acts (was 5+3).
Shared suite: 79 green + 1 RED (BUG-6 reorder, intentional).
2026-07-01 14:22:02 -04:00
david raistrick c72b88f8bb WIP turn.js: computeTurnOrderAfterRemoval bumps round on wrap
Removal of current participant when no active after → advance to order[0]
+ bump round. Without bump, nextTurn replays whole round (BUG-5 pattern).

Parser 500-40b: 24 skips/1 double (was 46/64). Down not zero. Remaining
skips = replay async stale read (getDoc between turns), not turn.js.
2026-07-01 12:00:23 -04:00
david raistrick 0473eacc1d WIP: BUG-5 slot-array fix + FEAT-1 dead-not-skipped + skip parser
WORK IN PROGRESS — fix not complete. analyze-turns.js on 500-round
replay still finds 46 real skips + 64 double-acts.

turn.js changes:
- computeTurnOrderAfterAddition: insert by initiative (not append end)
- nextTurn wrap: no re-sort, cycle pointer
- togglePause resume: no re-sort, order stable
- addParticipant: patches turnOrderIds when started
- applyHpChange: death no longer flips isActive or touches turnOrderIds
  (FEAT-1 dead-not-skipped)

Tests:
- shared/tests/turn.skip.test.js (NEW): deterministic skip invariants
  pure 100 rounds + 540 rounds w/ mutations, both green
- shared/tests/turn.dead-skip.test.js: 4 green (FEAT-1)
- turn.characterization.test.js: 3 sites updated to new behavior
- turn.combat.test.js: boundary count fixed (wrap-turn attributed to
  new round), debug dump removed

scripts/analyze-turns.js (NEW): deterministic replay-stdout parser.
Reconstructs rounds, reports real skips + double-acts. Exit 1 on issue.
Catches bugs unit tests miss (46 skips/64 double-acts in 500 rounds).

TODO: FEAT-1 marked done, FEAT-2 added (upgrade app logs parseable).
2026-07-01 11:42:43 -04:00
david raistrick d35a730e12 fix(turn): BUG-2 addParticipant rejects duplicate id
Root cause: addParticipant appended participant to participants[] without
checking id uniqueness. Two participants with same id in array. On
togglePause resume, turnOrderIds rebuilt via sort → dup id appears twice.
nextTurn then stuck repeating that id (rotation breaks).

This was the enabling step for BUG-1's full corruption (audit chain):
  pause blocks advance → totalTurns frozen → addParticipant re-adds
  same r${totalTurns} id → resume dup → nextTurn stuck.

Fix: throw on duplicate id in addParticipant. Caller must use fresh id
(crypto.randomUUID in App, replay already does).

Evidence:
- Test: 'addParticipant rejects duplicate id' (was test.skip, now live).
- Pre-fix: 1 RED (Received function did not throw).
- Post-fix: 50 green (shared), 23 green (server), 62 green (FE).
- Reachability in normal app: low (App uses crypto.randomUUID) but no
  guard existed before. Defensive + unblocks BUG-1 isolation.

No other behavior changed.
2026-06-29 16:25:39 -04:00
david raistrick e06adaa081 M1: shared turn logic + characterization tests (39 green)
- npm workspaces: shared/, server/
- shared/turn.js: port turn logic verbatim from App.js (bugs preserved)
- 39 characterization tests lock current behavior
- gitignore: sqlite data, logs
2026-06-28 16:57:43 -04:00