test: BUG-10 deact+reactivate double-act (already fixed by 1-list model)
1-list model keeps slot position on toggle. Reactivate does not re-insert by initiative, so no second turn. computeTurnOrderAfterAddition is dead code (no call sites). Tests prove no double-act in both scenarios.
This commit is contained in:
@@ -8,15 +8,6 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md.
|
|||||||
- `reorderParticipants` returns `log: null`. Other ops return `log.undo`.
|
- `reorderParticipants` returns `log: null`. Other ops return `log.undo`.
|
||||||
- Cannot undo drag-drop. Candidate for undo system (M6).
|
- Cannot undo drag-drop. Candidate for undo system (M6).
|
||||||
|
|
||||||
### BUG-10: deact+reactivate same round double-acts participant
|
|
||||||
- Discovered in 500-round replay (3 occurrences). DISTINCT from BUG-5.
|
|
||||||
- Pattern: participant acts → DM deactivates them → DM reactivates them
|
|
||||||
same round → re-inserts by initiative (front) → acts AGAIN before round ends.
|
|
||||||
- No "acted-this-round" guard. Slot-array model has no per-round-acted set.
|
|
||||||
- Edge case (DM deact+reactivate same participant same round).
|
|
||||||
- Fix candidate: track actedThisRound set, skip re-acted; OR insertion
|
|
||||||
places reactivate AFTER current position (not by initiative).
|
|
||||||
|
|
||||||
### BUG-13: reorderParticipants crossing current pointer = ambiguous acted-semantics
|
### BUG-13: reorderParticipants crossing current pointer = ambiguous acted-semantics
|
||||||
- `reorderParticipants` (shared/turn.js) = pure drag, no pointer logic.
|
- `reorderParticipants` (shared/turn.js) = pure drag, no pointer logic.
|
||||||
- Swapping two actors across current pointer mid-round = ambiguous
|
- Swapping two actors across current pointer mid-round = ambiguous
|
||||||
@@ -95,6 +86,10 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md.
|
|||||||
### BUG-8: server adapter has no reconnect
|
### BUG-8: server adapter has no reconnect
|
||||||
- FIXED — onclose reconnects + re-subscribes existing paths.
|
- FIXED — onclose reconnects + re-subscribes existing paths.
|
||||||
|
|
||||||
|
### BUG-10: deact+reactivate same round double-acts participant
|
||||||
|
- FIXED — 1-list model keeps slot position on toggle. Reactivate does not
|
||||||
|
grant second turn. Test: turn.bug10.test.js.
|
||||||
|
|
||||||
### BUG-11: FE Combat.scenario test crashes
|
### BUG-11: FE Combat.scenario test crashes
|
||||||
- FIXED — moved to shared/turn.combat.test.js, pure functions, 100 rounds.
|
- FIXED — moved to shared/turn.combat.test.js, pure functions, 100 rounds.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// BUG-10: deact+reactivate same round double-acts participant.
|
||||||
|
// OLD bug: reactivation re-inserted by initiative (front of pointer) →
|
||||||
|
// participant acted again before round end.
|
||||||
|
//
|
||||||
|
// 1-list model: toggleActive keeps slot position. Reactivation does NOT
|
||||||
|
// grant second turn. Each participant acts at most once per round.
|
||||||
|
//
|
||||||
|
// Test: walk rounds, count visits. Deactivate+reactivate mid-round must
|
||||||
|
// not cause any participant to act twice in same round.
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const shared = require('@ttrpg/shared');
|
||||||
|
const {
|
||||||
|
makeParticipant,
|
||||||
|
startEncounter, nextTurn, toggleParticipantActive,
|
||||||
|
} = shared;
|
||||||
|
|
||||||
|
function p(id, init) {
|
||||||
|
return makeParticipant({ id, name: id, type: 'monster',
|
||||||
|
initiative: init, maxHp: 100, currentHp: 100 });
|
||||||
|
}
|
||||||
|
function enc(ps) {
|
||||||
|
return { name:'t', participants:ps, isStarted:false, isPaused:false,
|
||||||
|
round:0, currentTurnParticipantId:null, turnOrderIds:[] };
|
||||||
|
}
|
||||||
|
const apply = (e, r) => r && r.patch ? { ...e, ...r.patch } : e;
|
||||||
|
|
||||||
|
describe('BUG-10: deact+reactivate same round', () => {
|
||||||
|
test('no participant acts twice in a round after deact+reactivate', () => {
|
||||||
|
// [a(10), b(7), c(3)]
|
||||||
|
let e = enc([p('a',10),p('b',7),p('c',3)]);
|
||||||
|
e = apply(e, startEncounter(e)); // a current, r1
|
||||||
|
|
||||||
|
const r1 = [];
|
||||||
|
r1.push(e.currentTurnParticipantId); // a acts
|
||||||
|
e = apply(e, nextTurn(e)); r1.push(e.currentTurnParticipantId); // b acts
|
||||||
|
|
||||||
|
// b already acted. Deactivate b (NOT current now — b just became current
|
||||||
|
// via nextTurn, so b IS current). Toggle off advances pointer to c.
|
||||||
|
e = apply(e, toggleParticipantActive(e, 'b')); // b off
|
||||||
|
// current advanced to c (b was current)
|
||||||
|
r1.push(e.currentTurnParticipantId); // c "becomes" active turn
|
||||||
|
// reactivate b same round
|
||||||
|
e = apply(e, toggleParticipantActive(e, 'b')); // b on
|
||||||
|
|
||||||
|
// nextTurn from c → round 2 (a). b must NOT re-act in round 1.
|
||||||
|
e = apply(e, nextTurn(e));
|
||||||
|
expect(e.round).toBe(2);
|
||||||
|
expect(e.currentTurnParticipantId).toBe('a');
|
||||||
|
|
||||||
|
// b acted once in r1, must act once in r2
|
||||||
|
e = apply(e, nextTurn(e));
|
||||||
|
expect(e.currentTurnParticipantId).toBe('b');
|
||||||
|
e = apply(e, nextTurn(e));
|
||||||
|
expect(e.currentTurnParticipantId).toBe('c');
|
||||||
|
|
||||||
|
// per-round visit count
|
||||||
|
const bCountR1 = r1.filter(id => id === 'b').length;
|
||||||
|
expect(bCountR1).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deactivate+reactivate non-current who already acted: no re-act', () => {
|
||||||
|
// [a(10), b(7), c(3)]. a acts, b acts, c acts (round 1 done).
|
||||||
|
// Then deactivate a (already acted, not current since c is current).
|
||||||
|
// Reactivate a. a must not act again until round 2.
|
||||||
|
let e = enc([p('a',10),p('b',7),p('c',3)]);
|
||||||
|
e = apply(e, startEncounter(e)); // a
|
||||||
|
e = apply(e, nextTurn(e)); // b
|
||||||
|
e = apply(e, nextTurn(e)); // c (r1 full: a,b,c)
|
||||||
|
// c is current. deactivate a (not current, already acted).
|
||||||
|
e = apply(e, toggleParticipantActive(e, 'a')); // a off, pointer stays c
|
||||||
|
expect(e.currentTurnParticipantId).toBe('c');
|
||||||
|
e = apply(e, toggleParticipantActive(e, 'a')); // a on
|
||||||
|
expect(e.currentTurnParticipantId).toBe('c');
|
||||||
|
|
||||||
|
// nextTurn from c → a (round 2). a acts in r2, once.
|
||||||
|
e = apply(e, nextTurn(e));
|
||||||
|
expect(e.round).toBe(2);
|
||||||
|
expect(e.currentTurnParticipantId).toBe('a');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user