Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deprecate moveLimit in favour of minMoves/maxMoves #985

Merged
Merged
187 changes: 180 additions & 7 deletions src/core/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,30 +241,51 @@ describe('turn', () => {
}
});

describe('moveLimit', () => {
describe('minMoves', () => {
test('without phases', () => {
const flow = Flow({
turn: {
moveLimit: 2,
minMoves: 2,
},
});

delucis marked this conversation as resolved.
Show resolved Hide resolved
let state = flow.init({ ctx: flow.ctx(2) } as State);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processEvent(state, gameEvent('endTurn'));

// player 0 could not end their turn because minMoves have not been made yet

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

// player 0 can now end their turn, having made another move to reach minMoves total

state = flow.processEvent(state, gameEvent('endTurn'));

expect(state.ctx.turn).toBe(2);
expect(state.ctx.currentPlayer).toBe('1');
});

test('with phases', () => {
const flow = Flow({
turn: { moveLimit: 2 },
turn: { minMoves: 2 },
phases: {
B: {
turn: {
moveLimit: 1,
minMoves: 1,
},
},
},
Expand All @@ -281,23 +302,133 @@ describe('turn', () => {

state = flow.processEvent(state, gameEvent('endTurn'));

// player 0 could not end their turn because minMoves have not been made yet

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processEvent(state, gameEvent('endTurn'));

expect(state.ctx.turn).toBe(2);
expect(state.ctx.currentPlayer).toBe('1');

state = flow.processEvent(state, gameEvent('setPhase', 'B'));

expect(state.ctx.turn).toBe(3);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processEvent(state, gameEvent('endTurn'));

// player 0 could not end their turn because minMoves have not been made yet

expect(state.ctx.turn).toBe(3);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(3);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processEvent(state, gameEvent('endTurn'));

// player 0 could end their turn after only one move because minMoves in phase B is only 1

expect(state.ctx.turn).toBe(4);
expect(state.ctx.currentPlayer).toBe('1');
});
});

describe('moveLimit', () => {
test('without phases', () => {
const flow = Flow({
turn: {
moveLimit: 2,
},
});
let state = flow.init({ ctx: flow.ctx(2) } as State);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processMove(state, makeMove('move', null, '0').payload);

// player 0 reached moveLimit, turn ended automatically

expect(state.ctx.turn).toBe(2);
expect(state.ctx.currentPlayer).toBe('1');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(2);
expect(state.ctx.currentPlayer).toBe('1');

state = flow.processEvent(state, gameEvent('endTurn'));

// player 1 ended their turn manually, having made less moves than the moveLimit would allow

expect(state.ctx.turn).toBe(3);
expect(state.ctx.currentPlayer).toBe('0');
});

test('with phases', () => {
const flow = Flow({
turn: { moveLimit: 2 },
phases: {
B: {
turn: {
moveLimit: 1,
},
},
},
});
let state = flow.init({ ctx: flow.ctx(2) } as State);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(1);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processEvent(state, gameEvent('endTurn'));

// player 0 ended their turn manually, having made less moves than the moveLimit would allow

expect(state.ctx.turn).toBe(2);
expect(state.ctx.currentPlayer).toBe('1');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(2);
expect(state.ctx.currentPlayer).toBe('1');

state = flow.processEvent(state, gameEvent('setPhase', 'B'));

// player 1 still had moves left, but the phase change automatically ended their turn

expect(state.ctx.phase).toBe('B');
expect(state.ctx.turn).toBe(3);
expect(state.ctx.currentPlayer).toBe('0');

state = flow.processMove(state, makeMove('move', null, '0').payload);

expect(state.ctx.turn).toBe(4);
expect(state.ctx.currentPlayer).toBe('1');

state = flow.processEvent(state, gameEvent('endTurn'));

// player 1 ended their turn without using any move

expect(state.ctx.turn).toBe(5);
expect(state.ctx.currentPlayer).toBe('0');
});

test('with noLimit moves', () => {
Expand Down Expand Up @@ -569,7 +700,7 @@ describe('stage events', () => {
test('with multiple active players', () => {
const flow = Flow({
turn: {
activePlayers: { all: 'A', moveLimit: 5 },
activePlayers: { all: 'A', minMoves: 2, moveLimit: 5 },
},
});
let state = { G: {}, ctx: flow.ctx(3) } as State;
Expand All @@ -578,7 +709,7 @@ describe('stage events', () => {
expect(state.ctx.activePlayers).toEqual({ '0': 'A', '1': 'A', '2': 'A' });
state = flow.processEvent(
state,
gameEvent('setStage', { stage: 'B', moveLimit: 1 })
gameEvent('setStage', { stage: 'B', minMoves: 1 })
);
expect(state.ctx.activePlayers).toEqual({ '0': 'B', '1': 'A', '2': 'A' });

Expand Down Expand Up @@ -606,6 +737,19 @@ describe('stage events', () => {
expect(state.ctx._activePlayersNumMoves).toMatchObject({ '0': 0 });
});

test('with min moves', () => {
const flow = Flow({});
let state = { G: {}, ctx: flow.ctx(2) } as State;
state = flow.init(state);

expect(state.ctx._activePlayersMinMoves).toBeNull();
state = flow.processEvent(
state,
gameEvent('setStage', { stage: 'A', minMoves: 1 })
);
expect(state.ctx._activePlayersMinMoves).toEqual({ '0': 1 });
});

test('with move limit', () => {
const flow = Flow({});
let state = { G: {}, ctx: flow.ctx(2) } as State;
Expand Down Expand Up @@ -727,6 +871,35 @@ describe('stage events', () => {
expect(state.ctx.activePlayers).toEqual({ '1': 'A', '2': 'A' });
});

test('with min moves', () => {
const flow = Flow({
turn: {
activePlayers: { all: 'A', minMoves: 2 },
},
});
let state = { G: {}, ctx: flow.ctx(2) } as State;
state = flow.init(state);

expect(state.ctx.activePlayers).toEqual({ '0': 'A', '1': 'A' });

state = flow.processEvent(state, gameEvent('endStage'));

// player 0 is not allowed to end the stage, they haven't made any move yet
expect(state.ctx.activePlayers).toEqual({ '0': 'A', '1': 'A' });

state = flow.processMove(state, makeMove('move', null, '0').payload);
state = flow.processEvent(state, gameEvent('endStage'));

// player 0 is still not allowed to end the stage, they haven't made the minimum number of moves
expect(state.ctx.activePlayers).toEqual({ '0': 'A', '1': 'A' });

state = flow.processMove(state, makeMove('move', null, '0').payload);
state = flow.processEvent(state, gameEvent('endStage'));

// having made 2 moves, player 0 was allowed to end the stage
expect(state.ctx.activePlayers).toEqual({ '1': 'A' });
});

test('maintains move count', () => {
const flow = Flow({
moves: { A: () => {} },
Expand Down Expand Up @@ -1805,7 +1978,7 @@ describe('hook execution order', () => {
calls.push('moves.endStage');
},
setActivePlayers: (G, ctx) => {
ctx.events.setActivePlayers({ all: 'A', moveLimit: 1 });
ctx.events.setActivePlayers({ all: 'A', minMoves: 1, moveLimit: 1 });
calls.push('moves.setActivePlayers');
},
},
Expand Down
56 changes: 48 additions & 8 deletions src/core/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,12 @@ export function Flow({
if (typeof arg !== 'object') return state;

let { ctx } = state;
let { activePlayers, _activePlayersMoveLimit, _activePlayersNumMoves } =
ctx;
let {
activePlayers,
_activePlayersMinMoves,
_activePlayersMoveLimit,
_activePlayersNumMoves,
} = ctx;

// Checking if stage is valid, even Stage.NULL
if (arg.stage !== undefined) {
Expand All @@ -378,6 +382,13 @@ export function Flow({
activePlayers[playerID] = arg.stage;
_activePlayersNumMoves[playerID] = 0;

if (arg.minMoves) {
if (_activePlayersMinMoves === null) {
_activePlayersMinMoves = {};
}
_activePlayersMinMoves[playerID] = arg.minMoves;
}

if (arg.moveLimit) {
if (_activePlayersMoveLimit === null) {
_activePlayersMoveLimit = {};
Expand All @@ -389,6 +400,7 @@ export function Flow({
ctx = {
...ctx,
activePlayers,
_activePlayersMinMoves,
_activePlayersMoveLimit,
_activePlayersNumMoves,
};
Expand Down Expand Up @@ -496,15 +508,15 @@ export function Flow({
const { currentPlayer, numMoves, phase, turn } = state.ctx;
const phaseConfig = GetPhase(state.ctx);

// Prevent ending the turn if moveLimit hasn't been reached.
// Prevent ending the turn if minMoves haven't been reached.
const currentPlayerMoves = numMoves || 0;
if (
!force &&
phaseConfig.turn.moveLimit &&
currentPlayerMoves < phaseConfig.turn.moveLimit
phaseConfig.turn.minMoves &&
currentPlayerMoves < phaseConfig.turn.minMoves
) {
logging.info(
`cannot end turn before making ${phaseConfig.turn.moveLimit} moves`
`cannot end turn before making ${phaseConfig.turn.minMoves} moves`
);
return state;
}
Expand Down Expand Up @@ -554,12 +566,20 @@ export function Flow({
playerID = playerID || state.ctx.currentPlayer;

let { ctx, _stateID } = state;
let { activePlayers, _activePlayersMoveLimit, phase, turn } = ctx;
let {
activePlayers,
_activePlayersNumMoves,
_activePlayersMinMoves,
_activePlayersMoveLimit,
phase,
turn,
} = ctx;

const playerInStage = activePlayers !== null && playerID in activePlayers;

const phaseConfig = GetPhase(ctx);

if (!arg && playerInStage) {
const phaseConfig = GetPhase(ctx);
const stage = phaseConfig.turn.stages[activePlayers[playerID]];
if (stage && stage.next) arg = stage.next;
}
Expand All @@ -572,10 +592,29 @@ export function Flow({
// If player isn’t in a stage, there is nothing else to do.
if (!playerInStage) return state;

// Prevent ending the stage if minMoves haven't been reached.
const currentPlayerMoves = _activePlayersNumMoves[playerID] || 0;
if (
_activePlayersMinMoves &&
_activePlayersMinMoves[playerID] &&
currentPlayerMoves < _activePlayersMinMoves[playerID]
) {
logging.info(
`cannot end stage before making ${_activePlayersMinMoves[playerID]} moves`
);
return state;
}

// Remove player from activePlayers.
activePlayers = { ...activePlayers };
delete activePlayers[playerID];

if (_activePlayersMinMoves) {
// Remove player from _activePlayersMinMoves.
_activePlayersMinMoves = { ..._activePlayersMinMoves };
delete _activePlayersMinMoves[playerID];
}

if (_activePlayersMoveLimit) {
// Remove player from _activePlayersMoveLimit.
_activePlayersMoveLimit = { ..._activePlayersMoveLimit };
Expand All @@ -585,6 +624,7 @@ export function Flow({
ctx = UpdateActivePlayersOnceEmpty({
...ctx,
activePlayers,
_activePlayersMinMoves,
_activePlayersMoveLimit,
});

Expand Down
Loading