fix(turn): block cross-pointer reorder (BUG-13)

reorderParticipants allowed dragging across current pointer mid-round.
nextTurn walks turnOrderIds forward from pointer, so cross-pointer drag
= ambiguous who-acts-next: upcoming dragged ahead of pointer = skipped
(walk wraps past), acted dragged behind pointer = double-act.

Full fix needs actedThisRound tracking. Pragmatic now: block both
directions during active encounter. Pre-combat: free reorder (no pointer).

reorderParticipants: added pointer check (currentTurn index). If drag
crosses pointer, return null patch (no-op). Same-side reorder allowed.
syncTurnOrder now always mirrors (was started-only — pre-combat reorder
left turnOrderIds stale).

Tests:
- turn.bug13.test.js: cross-pointer blocked (both dirs), same-side allowed,
  pre-combat free.
- turn.reorder.test.js: 3 tests updated to pre-combat (cross-pointer now
  blocked post-start).
- turn.invariant.test.js: reorder test uses same-side upcoming pair.

214 tests green.
This commit is contained in:
david raistrick
2026-07-04 17:12:13 -04:00
parent 0ff50561c7
commit 942545c085
5 changed files with 148 additions and 52 deletions
+9 -6
View File
@@ -90,12 +90,15 @@ describe('1-list model: turnOrderIds === participants.map(id), no re-sort', () =
expect(e.turnOrderIds).toEqual(['a','b','c']); // unchanged
});
test('reorder same-init: rotation follows new list order', () => {
let e = enc([p('a',10),p('b',10),p('c',3)]); // a,b tie
e = apply(e, startEncounter(e)); // [a,b,c], cur=a
e = apply(e, reorderParticipants(e, 'b', 'a')); // [b,a,c], cur still a
const rot = walkRotation(e); // start a, next c (wrap), next b, back a
expect(rot).toEqual(['a','c','b']);
test('reorder same-init: within upcoming side of pointer follows new order', () => {
let e = enc([p('a',10),p('b',10),p('c',10),p('d',3)]); // a,b,c tie
e = apply(e, startEncounter(e)); // [a,b,c,d], cur=a (idx0)
// Reorder c before b (both upcoming idx1,2). Within same side = allowed.
e = apply(e, reorderParticipants(e, 'c', 'b')); // [a,c,b,d], cur=a
expect(e.turnOrderIds).toEqual(['a','c','b','d']);
// walk: a current, next c, next b, next d, wrap a
e = apply(e, nextTurn(e));
expect(e.currentTurnParticipantId).toBe('c');
});
test('toggle inactive: list unchanged (stays in rotation slot)', () => {