Files
ttrpg-initiative-tracker/shared/tests/turn.bug10.test.js
T
david raistrick 0ff50561c7 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.
2026-07-04 17:06:07 -04:00

83 lines
3.2 KiB
JavaScript

// 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');
});
});