Skip to content

Commit b2f5160

Browse files
delucisnicolodavis
authored andcommitted
feat: Add endStage and setStage events (#458)
* refactor: Move `activePlayers` reset logic to reuseable function This moves the logic that resets an empty `activePlayers` to next, previous or `null` values into `UpdateActivePlayers`, so that it can be called from an `EndStage` event and not just inside `ProcessMove`. * feat: Add `startStage` and `endStage` events * test: Fix failing tests with new events * test: Write basic tests for `setStage` and `endStage` * feat: Add hypothetical move limit logic If `arg.moveLimit` could reach `UpdateStage` this logic would update `ctx` appropriately. * test: Add failing test for move limit with `setStage` * feat: Bail out of `UpdateStage` early if `arg` is undefined * feat: Allow `setStage` event to receive object argument Supports `moveLimit` with an object argument: `setStage({ stage, moveLimit })` * test: Update `setStage` move limit test with object syntax * test: Add test for `setStage` object syntax without move limit * test: Test how `setStage` & `endStage` mutate `_activePlayersNumMoves` * test: Add test for `endStage` with more than one currently active player * refactor: Use `EndStage` to simplify `ProcessMove` Allow a `playerID` to be passed to `EndStage` and `UpdateStage` so they can be used to end/update any player in `activePlayers`. They default to `ctx.currentPlayer` when called from the client, which could need changing once any active player can call events. * test: Increase `setStage`/`endStage` coverage * refactor: Rename `UpdateActivePlayers` to `UpdateActivePlayersOnceEmpty` * refactor: Don’t call no-op `UpdateStage` when `arg` is undefined * group event defaults * remove redundant statement
1 parent 53eb648 commit b2f5160

4 files changed

Lines changed: 308 additions & 55 deletions

File tree

src/client/client.test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,12 @@ describe('event dispatchers', () => {
335335
test('default', () => {
336336
const game = {};
337337
const client = Client({ game });
338-
expect(Object.keys(client.events)).toEqual(['endTurn', 'setActivePlayers']);
338+
expect(Object.keys(client.events)).toEqual([
339+
'endTurn',
340+
'setActivePlayers',
341+
'endStage',
342+
'setStage',
343+
]);
339344
expect(client.getState().ctx.turn).toBe(1);
340345
client.events.endTurn();
341346
expect(client.getState().ctx.turn).toBe(2);
@@ -355,6 +360,8 @@ describe('event dispatchers', () => {
355360
'setPhase',
356361
'endGame',
357362
'setActivePlayers',
363+
'endStage',
364+
'setStage',
358365
]);
359366
expect(client.getState().ctx.turn).toBe(1);
360367
client.events.endTurn();
@@ -367,6 +374,7 @@ describe('event dispatchers', () => {
367374
endPhase: false,
368375
endTurn: false,
369376
setActivePlayers: false,
377+
endStage: false,
370378
},
371379
};
372380
const client = Client({ game });

src/core/flow.js

Lines changed: 123 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
SetActivePlayersEvent,
1111
SetActivePlayers,
12+
UpdateActivePlayersOnceEmpty,
1213
InitTurnOrderState,
1314
UpdateTurnOrderState,
1415
Stage,
@@ -208,10 +209,12 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
208209
if (events.setActivePlayers === undefined) {
209210
events.setActivePlayers = true;
210211
}
212+
if (events.endStage === undefined) {
213+
events.endStage = true;
214+
events.setStage = true;
215+
}
211216
if (events.endPhase === undefined && phases) {
212217
events.endPhase = true;
213-
}
214-
if (events.setPhase === undefined && phases) {
215218
events.setPhase = true;
216219
}
217220
if (events.endTurn === undefined) {
@@ -498,6 +501,43 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
498501
return state;
499502
}
500503

504+
function UpdateStage(state, { arg, playerID }) {
505+
if (typeof arg === 'string') {
506+
arg = { stage: arg };
507+
}
508+
509+
let { ctx } = state;
510+
let {
511+
activePlayers,
512+
_activePlayersMoveLimit,
513+
_activePlayersNumMoves,
514+
} = ctx;
515+
516+
if (arg.stage) {
517+
if (activePlayers === null) {
518+
activePlayers = {};
519+
}
520+
activePlayers[playerID] = arg.stage;
521+
_activePlayersNumMoves[playerID] = 0;
522+
523+
if (arg.moveLimit) {
524+
if (_activePlayersMoveLimit === null) {
525+
_activePlayersMoveLimit = {};
526+
}
527+
_activePlayersMoveLimit[playerID] = arg.moveLimit;
528+
}
529+
}
530+
531+
ctx = {
532+
...ctx,
533+
activePlayers,
534+
_activePlayersMoveLimit,
535+
_activePlayersNumMoves,
536+
};
537+
538+
return { ...state, ctx };
539+
}
540+
501541
///////////////
502542
// ShouldEnd //
503543
///////////////
@@ -631,6 +671,63 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
631671
return { ...state, G, ctx, deltalog, _undo: [], _redo: [] };
632672
}
633673

674+
function EndStage(state, { arg, next, automatic, playerID }) {
675+
playerID = playerID || state.ctx.currentPlayer;
676+
677+
let { ctx } = state;
678+
let { activePlayers, _activePlayersMoveLimit } = ctx;
679+
680+
if (next && arg) {
681+
next.push({ fn: UpdateStage, arg, playerID });
682+
}
683+
684+
// If player isn’t in a stage, there is nothing else to do.
685+
if (activePlayers === null || !(playerID in activePlayers)) {
686+
return state;
687+
}
688+
689+
// Remove player from activePlayers.
690+
activePlayers = Object.keys(activePlayers)
691+
.filter(id => id !== playerID)
692+
.reduce((obj, key) => {
693+
obj[key] = activePlayers[key];
694+
return obj;
695+
}, {});
696+
697+
if (_activePlayersMoveLimit) {
698+
// Remove player from _activePlayersMoveLimit.
699+
_activePlayersMoveLimit = Object.keys(_activePlayersMoveLimit)
700+
.filter(id => id !== playerID)
701+
.reduce((obj, key) => {
702+
obj[key] = _activePlayersMoveLimit[key];
703+
return obj;
704+
}, {});
705+
}
706+
707+
ctx = UpdateActivePlayersOnceEmpty({
708+
...ctx,
709+
activePlayers,
710+
_activePlayersMoveLimit,
711+
});
712+
713+
// Add log entry.
714+
const action = gameEvent('endStage', arg);
715+
const logEntry = {
716+
action,
717+
_stateID: state._stateID,
718+
turn: state.ctx.turn,
719+
phase: state.ctx.phase,
720+
};
721+
722+
if (automatic) {
723+
logEntry.automatic = true;
724+
}
725+
726+
const deltalog = [...(state.deltalog || []), logEntry];
727+
728+
return { ...state, ctx, deltalog };
729+
}
730+
634731
/**
635732
* Retrieves the relevant move that can be played by playerID.
636733
*
@@ -683,57 +780,11 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
683780
let conf = GetPhase(state.ctx);
684781

685782
let { ctx } = state;
686-
let {
687-
activePlayers,
688-
_activePlayersMoveLimit,
689-
_activePlayersNumMoves,
690-
_prevActivePlayers,
691-
} = ctx;
783+
let { _activePlayersNumMoves } = ctx;
692784

693785
const { playerID } = action;
694786

695-
if (activePlayers) _activePlayersNumMoves[playerID]++;
696-
697-
if (
698-
_activePlayersMoveLimit &&
699-
_activePlayersNumMoves[playerID] >= _activePlayersMoveLimit[playerID]
700-
) {
701-
activePlayers = Object.keys(activePlayers)
702-
.filter(id => id !== playerID)
703-
.reduce((obj, key) => {
704-
obj[key] = activePlayers[key];
705-
return obj;
706-
}, {});
707-
_activePlayersMoveLimit = Object.keys(_activePlayersMoveLimit)
708-
.filter(id => id !== playerID)
709-
.reduce((obj, key) => {
710-
obj[key] = _activePlayersMoveLimit[key];
711-
return obj;
712-
}, {});
713-
}
714-
715-
if (activePlayers && Object.keys(activePlayers).length == 0) {
716-
if (ctx._nextActivePlayers) {
717-
ctx = SetActivePlayers(ctx, ctx._nextActivePlayers);
718-
({
719-
activePlayers,
720-
_activePlayersMoveLimit,
721-
_activePlayersNumMoves,
722-
_prevActivePlayers,
723-
} = ctx);
724-
} else if (_prevActivePlayers.length > 0) {
725-
const lastIndex = _prevActivePlayers.length - 1;
726-
({
727-
activePlayers,
728-
_activePlayersMoveLimit,
729-
_activePlayersNumMoves,
730-
} = _prevActivePlayers[lastIndex]);
731-
_prevActivePlayers = _prevActivePlayers.slice(0, lastIndex);
732-
} else {
733-
activePlayers = null;
734-
_activePlayersMoveLimit = null;
735-
}
736-
}
787+
if (ctx.activePlayers) _activePlayersNumMoves[playerID]++;
737788

738789
let numMoves = state.ctx.numMoves;
739790
if (playerID == state.ctx.currentPlayer) {
@@ -744,14 +795,18 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
744795
...state,
745796
ctx: {
746797
...ctx,
747-
activePlayers,
748-
_activePlayersMoveLimit,
749-
_activePlayersNumMoves,
750-
_prevActivePlayers,
751798
numMoves,
799+
_activePlayersNumMoves,
752800
},
753801
};
754802

803+
if (
804+
ctx._activePlayersMoveLimit &&
805+
_activePlayersNumMoves[playerID] >= ctx._activePlayersMoveLimit[playerID]
806+
) {
807+
state = EndStage(state, { playerID, automatic: true });
808+
}
809+
755810
const G = conf.turn.onMove(state.G, state.ctx, action);
756811
state = { ...state, G };
757812

@@ -772,6 +827,14 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
772827
return ProcessEvents(state, events);
773828
}
774829

830+
function SetStageEvent(state, arg) {
831+
return ProcessEvents(state, [{ fn: EndStage, arg }]);
832+
}
833+
834+
function EndStageEvent(state) {
835+
return ProcessEvents(state, [{ fn: EndStage }]);
836+
}
837+
775838
function SetPhaseEvent(state, newPhase) {
776839
return ProcessEvents(state, [
777840
{
@@ -802,6 +865,8 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
802865
}
803866

804867
const eventHandlers = {
868+
endStage: EndStageEvent,
869+
setStage: SetStageEvent,
805870
endTurn: EndTurnEvent,
806871
endPhase: EndPhaseEvent,
807872
setPhase: SetPhaseEvent,
@@ -823,6 +888,10 @@ export function Flow({ moves, phases, endIf, turn, events, plugins }) {
823888
if (events.setActivePlayers) {
824889
enabledEvents['setActivePlayers'] = true;
825890
}
891+
if (events.endStage) {
892+
enabledEvents['endStage'] = true;
893+
enabledEvents['setStage'] = true;
894+
}
826895

827896
return FlowInternal({
828897
ctx: numPlayers => ({

0 commit comments

Comments
 (0)