boardgame.io
Version:
library for turn-based games
1,124 lines (945 loc) • 30.9 kB
text/typescript
/*
* Copyright 2017 The boardgame.io Authors
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
import { Flow } from './flow';
import { Client } from '../client/client';
import {
UpdateTurnOrderState,
Stage,
TurnOrder,
ActivePlayers,
} from './turn-order';
import { makeMove, gameEvent } from './action-creators';
import { CreateGameReducer } from './reducer';
import { InitializeGame } from './initialize';
import { error } from '../core/logger';
import type { Game, State } from '../types';
jest.mock('../core/logger', () => ({
info: jest.fn(),
error: jest.fn(),
}));
// Let the Typescript compiler know about our custom matcher.
declare global {
namespace jest {
interface Matchers<R> {
toHaveUndefinedProperties(): R;
}
}
}
describe('turn orders', () => {
// Defines a matcher for testing that ctx has no undefined properties.
// Identifies which property is undefined.
expect.extend({
toHaveUndefinedProperties(ctx) {
const undefinedEntry = Object.entries(ctx).find((entry) => {
const [, value] = entry;
return value === undefined;
});
if (undefinedEntry === undefined) {
return {
message: () => `expected some properties of ctx to be undefined`,
pass: false,
};
} else {
const [k] = undefinedEntry;
return {
message: () => `expected ctx.${k} to be defined`,
pass: true,
};
}
},
});
test('DEFAULT', () => {
const flow = Flow({
phases: { A: { start: true, next: 'B' }, B: {} },
});
let state = { ctx: flow.ctx(2) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('0');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.phase).toBe('A');
state = flow.processEvent(state, gameEvent('endPhase'));
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx.phase).toBe('B');
});
test('CONTINUE', () => {
const flow = Flow({
turn: { order: TurnOrder.CONTINUE },
phases: { A: { start: true, next: 'B' }, B: {} },
});
let state = { ctx: flow.ctx(2) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('0');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.phase).toBe('A');
state = flow.processEvent(state, gameEvent('endPhase'));
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.phase).toBe('B');
});
test('RESET', () => {
const flow = Flow({
turn: { order: TurnOrder.RESET },
phases: { A: { start: true, next: 'B' }, B: {} },
});
let state = { ctx: flow.ctx(2) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('0');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.phase).toBe('A');
state = flow.processEvent(state, gameEvent('endPhase'));
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx.phase).toBe('B');
});
test('ONCE', () => {
const flow = Flow({
turn: { order: TurnOrder.ONCE },
phases: { A: { start: true, next: 'B' }, B: {} },
});
let state = { ctx: flow.ctx(2) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx.phase).toBe('B');
});
test('ALL', () => {
const flow = Flow({
turn: { activePlayers: ActivePlayers.ALL },
});
let state = { ctx: flow.ctx(2) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('0');
expect(state.ctx.activePlayers).toEqual({
'0': Stage.NULL,
'1': Stage.NULL,
});
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.activePlayers).toEqual({
'0': Stage.NULL,
'1': Stage.NULL,
});
});
test('ALL_ONCE', () => {
const flow = Flow({
phases: {
A: { start: true, turn: { activePlayers: ActivePlayers.ALL_ONCE } },
},
});
let state = { ctx: flow.ctx(2) } as State;
state = flow.init(state);
expect(state.ctx.phase).toBe('A');
expect(state.ctx.currentPlayer).toBe('0');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0', '1']);
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.phase).toBe('A');
expect(state.ctx.currentPlayer).toBe('1');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0', '1']);
state = flow.processMove(state, makeMove('', null, '0').payload);
expect(state.ctx.phase).toBe('A');
expect(state.ctx.currentPlayer).toBe('1');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['1']);
state = flow.processMove(state, makeMove('', null, '1').payload);
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.activePlayers).toBeNull();
state = flow.processMove(state, makeMove('', null, '1').payload);
});
test('OTHERS', () => {
const flow = Flow({
turn: { activePlayers: ActivePlayers.OTHERS },
});
let state = { ctx: flow.ctx(3) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('0');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['1', '2']);
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0', '2']);
});
test('OTHERS_ONCE', () => {
const flow = Flow({
turn: { activePlayers: ActivePlayers.OTHERS_ONCE },
phases: { A: { start: true } },
});
let state = { ctx: flow.ctx(3) } as State;
state = flow.init(state);
expect(state.ctx.phase).toBe('A');
expect(state.ctx.currentPlayer).toBe('0');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['1', '2']);
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.phase).toBe('A');
expect(state.ctx.currentPlayer).toBe('1');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0', '2']);
state = flow.processMove(state, makeMove('', null, '0').payload);
expect(state.ctx.phase).toBe('A');
expect(state.ctx.currentPlayer).toBe('1');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['2']);
state = flow.processMove(state, makeMove('', null, '2').payload);
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.activePlayers).toBeNull();
state = flow.processMove(state, makeMove('', null, '1').payload);
});
test('CUSTOM', () => {
const flow = Flow({
turn: { order: TurnOrder.CUSTOM(['1', '0']) },
});
let state = { ctx: flow.ctx(2) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('0');
});
test('CUSTOM_FROM', () => {
const flow = Flow({
turn: { order: TurnOrder.CUSTOM_FROM('order') },
});
let state = { G: { order: ['2', '1', '0'] }, ctx: flow.ctx(3) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('2');
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('0');
});
test('manual', () => {
const flow = Flow({
phases: {
A: {
start: true,
turn: {
order: {
first: () => 9,
next: () => 3,
},
},
},
},
});
let state = { ctx: flow.ctx(10) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('9');
expect(state.ctx).not.toHaveUndefinedProperties();
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('3');
});
});
test('override', () => {
const even = {
first: () => 0,
next: ({ ctx }) => (+ctx.currentPlayer + 2) % ctx.numPlayers,
};
const odd = {
first: () => 1,
next: ({ ctx }) => (+ctx.currentPlayer + 2) % ctx.numPlayers,
};
const flow = Flow({
turn: { order: even },
phases: { A: { start: true, next: 'B' }, B: { turn: { order: odd } } },
});
let state = { ctx: flow.ctx(10) } as State;
state = flow.init(state);
expect(state.ctx.currentPlayer).toBe('0');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('2');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('4');
state = flow.processEvent(state, gameEvent('endPhase'));
expect(state.ctx.currentPlayer).toBe('1');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('3');
state = flow.processEvent(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('5');
});
test('playOrder', () => {
const game: Game = {};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
state.ctx = {
...state.ctx,
currentPlayer: '2',
playOrder: ['2', '0', '1'],
};
state = reducer(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('0');
state = reducer(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('1');
state = reducer(state, gameEvent('endTurn'));
expect(state.ctx.currentPlayer).toBe('2');
});
describe('setActivePlayers', () => {
const flow = Flow({});
const state = { ctx: flow.ctx(2) } as State;
test('basic', () => {
const newState = flow.processEvent(
state,
gameEvent('setActivePlayers', [{ value: { '1': Stage.NULL } }])
);
expect(newState.ctx.activePlayers).toMatchObject({ '1': Stage.NULL });
});
test('short form', () => {
const newState = flow.processEvent(
state,
gameEvent('setActivePlayers', [['1', '2']])
);
expect(newState.ctx.activePlayers).toMatchObject({
'1': Stage.NULL,
'2': Stage.NULL,
});
});
test('undefined stage leaves player inactive', () => {
const newState = flow.processEvent(
state,
gameEvent('setActivePlayers', [
{
value: {
'1': {
minMoves: 2,
maxMoves: 2,
},
},
},
])
);
expect(newState.ctx.activePlayers).toBeNull();
});
test('all', () => {
const newState = flow.processEvent(
state,
gameEvent('setActivePlayers', [{ all: Stage.NULL }])
);
expect(newState.ctx.activePlayers).toMatchObject({
'0': Stage.NULL,
'1': Stage.NULL,
});
});
test('once', () => {
const game: Game = {
moves: {
B: ({ G, events }) => {
events.setActivePlayers({
value: { '0': Stage.NULL, '1': Stage.NULL },
minMoves: 1,
maxMoves: 1,
});
return G;
},
A: ({ G }) => G,
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game });
state = reducer(state, makeMove('B', null, '0'));
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0', '1']);
state = reducer(state, makeMove('A', null, '0'));
expect(Object.keys(state.ctx.activePlayers)).toEqual(['1']);
state = reducer(state, makeMove('A', null, '1'));
expect(state.ctx.activePlayers).toBeNull();
});
test('others', () => {
const game: Game = {
moves: {
B: ({ G, events }) => {
events.setActivePlayers({
minMoves: 1,
maxMoves: 1,
others: Stage.NULL,
});
return G;
},
A: ({ G }) => G,
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
// on move B, control switches from player 0 to players 1 and 2
state = reducer(state, makeMove('B', null, '0'));
expect(Object.keys(state.ctx.activePlayers)).toEqual(['1', '2']);
// player 1 makes move
state = reducer(state, makeMove('A', null, '1'));
expect(Object.keys(state.ctx.activePlayers)).toEqual(['2']);
// player 2 makes move
state = reducer(state, makeMove('A', null, '2'));
expect(state.ctx.activePlayers).toBeNull();
});
test('set stages to Stage.NULL', () => {
const game: Game = {
moves: {
A: ({ G }) => G,
B: ({ G, events }) => {
events.setActivePlayers({
minMoves: 1,
maxMoves: 1,
currentPlayer: 'start',
});
return G;
},
},
turn: {
activePlayers: {
currentPlayer: {
stage: 'start',
},
others: Stage.NULL,
},
stages: {
start: {
moves: {
S: ({ G, events }) => {
events.setStage(Stage.NULL);
return G;
},
},
},
},
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
expect(state.ctx.currentPlayer).toBe('0');
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0', '1', '2']);
expect(state.ctx.activePlayers['0']).toEqual('start');
expect(state.ctx.activePlayers['1']).toEqual(Stage.NULL);
expect(state.ctx.activePlayers['2']).toEqual(Stage.NULL);
state = reducer(state, makeMove('S', null, '0'));
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0', '1', '2']);
expect(state.ctx.activePlayers['0']).toEqual(Stage.NULL);
state = reducer(state, makeMove('B', null, '0'));
expect(Object.keys(state.ctx.activePlayers)).toEqual(['0']);
expect(state.ctx.activePlayers['0']).toEqual('start');
});
describe('reset behavior', () => {
test('start of turn', () => {
const game: Game = {
moves: {
A: () => {},
},
turn: {
activePlayers: { currentPlayer: 'stage', minMoves: 1, maxMoves: 1 },
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game });
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage' },
_prevActivePlayers: [],
});
state = reducer(state, makeMove('A', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: null,
_prevActivePlayers: [],
});
});
describe('revert', () => {
test('resets to previous', () => {
const game: Game = {
moves: {
A: ({ events }) => {
events.setActivePlayers({
currentPlayer: 'stage2',
minMoves: 1,
maxMoves: 1,
revert: true,
});
},
B: () => {},
},
turn: {
activePlayers: { currentPlayer: 'stage1' },
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game });
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage1' },
_prevActivePlayers: [],
});
state = reducer(state, makeMove('A', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage2' },
_prevActivePlayers: [
{
activePlayers: { '0': 'stage1' },
_activePlayersMinMoves: null,
_activePlayersMaxMoves: null,
_activePlayersNumMoves: { '0': 1 },
},
],
});
state = reducer(state, makeMove('B', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage1' },
_prevActivePlayers: [],
});
});
test('restores move limits and counts', () => {
const game: Game = {
moves: {
A: ({ events }) => {
events.setActivePlayers({
currentPlayer: 'stage2',
minMoves: 1,
maxMoves: 1,
revert: true,
});
},
B: () => {},
},
turn: {
activePlayers: {
currentPlayer: 'stage1',
minMoves: 2,
maxMoves: 3,
},
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game });
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage1' },
_prevActivePlayers: [],
_activePlayersMinMoves: { '0': 2 },
_activePlayersMaxMoves: { '0': 3 },
_activePlayersNumMoves: {
'0': 0,
},
});
state = reducer(state, makeMove('B', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage1' },
_prevActivePlayers: [],
_activePlayersMinMoves: { '0': 2 },
_activePlayersMaxMoves: { '0': 3 },
_activePlayersNumMoves: {
'0': 1,
},
});
state = reducer(state, makeMove('A', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage2' },
_prevActivePlayers: [
{
activePlayers: { '0': 'stage1' },
_activePlayersNumMoves: { '0': 2 },
_activePlayersMinMoves: { '0': 2 },
_activePlayersMaxMoves: { '0': 3 },
},
],
_activePlayersMinMoves: { '0': 1 },
_activePlayersMaxMoves: { '0': 1 },
_activePlayersNumMoves: {
'0': 0,
},
});
state = reducer(state, makeMove('B', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage1' },
_prevActivePlayers: [],
_activePlayersMinMoves: { '0': 2 },
_activePlayersMaxMoves: { '0': 3 },
_activePlayersNumMoves: {
'0': 2,
},
});
});
});
test('set to next', () => {
const game: Game = {
moves: {
A: () => {},
},
turn: {
activePlayers: {
currentPlayer: 'stage1',
minMoves: 1,
maxMoves: 1,
next: {
currentPlayer: 'stage2',
minMoves: 1,
maxMoves: 1,
next: {
currentPlayer: 'stage3',
},
},
},
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game });
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage1' },
_prevActivePlayers: [],
_nextActivePlayers: {
currentPlayer: 'stage2',
minMoves: 1,
maxMoves: 1,
next: {
currentPlayer: 'stage3',
},
},
});
state = reducer(state, makeMove('A', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage2' },
_prevActivePlayers: [],
_nextActivePlayers: {
currentPlayer: 'stage3',
},
});
state = reducer(state, makeMove('A', null, '0'));
expect(state.ctx).toMatchObject({
activePlayers: { '0': 'stage3' },
_prevActivePlayers: [],
_nextActivePlayers: null,
});
});
});
describe('move limits', () => {
test('shorthand syntax', () => {
const game: Game = {
turn: {
activePlayers: {
all: 'play',
minMoves: 1,
maxMoves: 3,
},
stages: {
play: { moves: { A: () => {} } },
},
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
expect(state.ctx._activePlayersMinMoves).toEqual({
'0': 1,
'1': 1,
'2': 1,
});
expect(state.ctx._activePlayersMaxMoves).toEqual({
'0': 3,
'1': 3,
'2': 3,
});
expect(state.ctx._activePlayersNumMoves).toEqual({
'0': 0,
'1': 0,
'2': 0,
});
state = reducer(state, makeMove('A', null, '0'));
state = reducer(state, makeMove('A', null, '1'));
state = reducer(state, makeMove('A', null, '1'));
state = reducer(state, makeMove('A', null, '2'));
expect(state.ctx._activePlayersNumMoves).toEqual({
'0': 1,
'1': 2,
'2': 1,
});
state = reducer(state, makeMove('A', null, '1'));
expect(state.ctx.activePlayers).toEqual({
'0': 'play',
'2': 'play',
});
});
test('long-form syntax', () => {
const game: Game = {
turn: {
activePlayers: {
currentPlayer: { stage: 'play', minMoves: 1, maxMoves: 2 },
others: { stage: 'play', maxMoves: 1 },
},
stages: {
play: { moves: { A: () => {} } },
},
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
expect(state.ctx._activePlayersMinMoves).toStrictEqual({ '0': 1 });
expect(state.ctx._activePlayersMaxMoves).toEqual({
'0': 2,
'1': 1,
'2': 1,
});
expect(state.ctx._activePlayersNumMoves).toEqual({
'0': 0,
'1': 0,
'2': 0,
});
state = reducer(state, makeMove('A', null, '0'));
state = reducer(state, makeMove('A', null, '1'));
state = reducer(state, makeMove('A', null, '2'));
expect(state.ctx._activePlayersNumMoves).toEqual({
'0': 1,
'1': 1,
'2': 1,
});
expect(state.ctx.activePlayers).toEqual({ '0': 'play' });
state = reducer(state, makeMove('A', null, '0'));
expect(state.ctx.activePlayers).toBeNull();
});
test('player-specific limit overrides move limit args', () => {
const game: Game = {
turn: {
activePlayers: {
all: { stage: 'play', minMoves: 2, maxMoves: 2 },
minMoves: 1,
maxMoves: 1,
},
},
};
const state = InitializeGame({ game, numPlayers: 2 });
expect(state.ctx._activePlayersMinMoves).toEqual({
'0': 2,
'1': 2,
});
expect(state.ctx._activePlayersMaxMoves).toEqual({
'0': 2,
'1': 2,
});
});
test('value syntax', () => {
const game: Game = {
turn: {
activePlayers: {
value: {
'0': { stage: 'play', maxMoves: 1 },
'1': { stage: 'play', minMoves: 1, maxMoves: 2 },
'2': { stage: 'play', minMoves: 2, maxMoves: 3 },
},
},
stages: {
play: { moves: { A: () => {} } },
},
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
expect(state.ctx._activePlayersMinMoves).toStrictEqual({
'1': 1,
'2': 2,
});
expect(state.ctx._activePlayersMaxMoves).toEqual({
'0': 1,
'1': 2,
'2': 3,
});
state = reducer(state, makeMove('A', null, '0'));
state = reducer(state, makeMove('A', null, '1'));
state = reducer(state, makeMove('A', null, '2'));
expect(state.ctx.activePlayers).toEqual({ '1': 'play', '2': 'play' });
state = reducer(state, makeMove('A', null, '1'));
state = reducer(state, makeMove('A', null, '2'));
expect(state.ctx.activePlayers).toEqual({ '2': 'play' });
state = reducer(state, makeMove('A', null, '2'));
expect(state.ctx.activePlayers).toBeNull();
});
test('move counts reset on turn end', () => {
const game: Game = {
turn: {
activePlayers: {
all: 'play',
},
stages: {
play: { moves: { A: () => {} } },
},
},
};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
state = reducer(state, makeMove('A', null, '0'));
state = reducer(state, makeMove('A', null, '1'));
expect(state.ctx._activePlayersNumMoves).toEqual({
'0': 1,
'1': 1,
'2': 0,
});
state = reducer(state, gameEvent('endTurn'));
expect(state.ctx._activePlayersNumMoves).toEqual({
'0': 0,
'1': 0,
'2': 0,
});
});
});
describe('militia', () => {
let state;
let reducer;
beforeAll(() => {
const game: Game = {
moves: {
militia: ({ events }) => {
events.setActivePlayers({
others: 'discard',
minMoves: 1,
maxMoves: 1,
revert: true,
});
},
},
turn: {
stages: {
discard: {
moves: {
discard: ({ G }) => G,
},
},
},
},
};
reducer = CreateGameReducer({ game });
state = InitializeGame({ game, numPlayers: 3 });
});
beforeEach(() => {
(error as jest.Mock).mockClear();
});
test('sanity', () => {
expect(state.ctx.activePlayers).toEqual(null);
});
test('player 1 cannot play the militia card', () => {
state = reducer(state, makeMove('militia', undefined, '1'));
expect(error).toHaveBeenCalledWith('disallowed move: militia');
});
test('player 2 cannot play the militia card', () => {
state = reducer(state, makeMove('militia', undefined, '2'));
expect(error).toHaveBeenCalledWith('disallowed move: militia');
});
test('player 0 cannot discard', () => {
state = reducer(state, makeMove('discard', undefined, '0'));
expect(error).toHaveBeenCalledWith('disallowed move: discard');
});
test('player 1 cannot discard', () => {
state = reducer(state, makeMove('discard', undefined, '1'));
expect(error).toHaveBeenCalledWith('disallowed move: discard');
});
test('player 2 cannot discard', () => {
state = reducer(state, makeMove('discard', undefined, '2'));
expect(error).toHaveBeenCalledWith('disallowed move: discard');
});
test('player 0 plays militia', () => {
state = reducer(state, makeMove('militia', undefined, '0'));
expect(state.ctx.activePlayers).toEqual({
'1': 'discard',
'2': 'discard',
});
});
test('player 0 cannot play militia again', () => {
state = reducer(state, makeMove('militia', undefined, '0'));
expect(error).toHaveBeenCalledWith('disallowed move: militia');
});
test('player 0 still cannot discard', () => {
state = reducer(state, makeMove('discard', undefined, '0'));
expect(error).toHaveBeenCalledWith('disallowed move: discard');
});
test('everyone else discards', () => {
state = reducer(state, makeMove('discard', undefined, '1'));
expect(state.ctx.activePlayers).toEqual({ '2': 'discard' });
state = reducer(state, makeMove('discard', undefined, '2'));
});
test('activePlayers is restored to previous state', () => {
expect(state.ctx.activePlayers).toEqual(null);
});
});
});
describe('UpdateTurnOrderState', () => {
const G = {};
const ctx = {
currentPlayer: '0',
playOrder: ['0', '1', '2'],
playOrderPos: 0,
};
const turn = { order: TurnOrder.DEFAULT };
test('without next player', () => {
const { ctx: t } = UpdateTurnOrderState(
{ G, ctx } as State,
ctx.currentPlayer,
turn
);
expect(t).toMatchObject({ currentPlayer: '1' });
});
test('with next player', () => {
const { ctx: t } = UpdateTurnOrderState(
{ G, ctx } as State,
ctx.currentPlayer,
turn,
{
next: '2',
}
);
expect(t).toMatchObject({ currentPlayer: '2' });
});
test('errors if turn.order.next doesn’t return a number', () => {
UpdateTurnOrderState({ G, ctx } as State, ctx.currentPlayer, {
order: {
first: () => 0,
next: () => '2' as unknown as number,
},
});
expect(error).toHaveBeenCalledWith(
`invalid value returned by turn.order.next — expected number or undefined got string “2”.`
);
});
});
describe('Random API is available', () => {
let first;
let next;
const turn = {
order: {
first: ({ random }) => {
if (random !== undefined) {
first = true;
}
return 0;
},
next: ({ random }) => {
if (random !== undefined) {
next = true;
}
return 0;
},
},
};
const game: Game = { turn };
beforeEach(() => {
first = next = false;
});
test('init', () => {
Client({ game });
expect(first).toBe(true);
});
test('end turn', () => {
const client = Client({ game });
expect(next).toBe(false);
client.events.endTurn();
expect(next).toBe(true);
});
});