diff --git a/src/client/client.test.js b/src/client/client.test.js index 9cccddbac..7120c9b9e 100644 --- a/src/client/client.test.js +++ b/src/client/client.test.js @@ -175,7 +175,7 @@ describe('multiplayer', () => { test('onAction called', () => { jest.spyOn(client.transport, 'onAction'); - client.store.dispatch(sync({ G: {}, ctx: { phase: 'default' } }, [])); + client.store.dispatch(sync({ G: {}, ctx: { phase: '' } }, [])); client.moves.A(); expect(client.transport.onAction).toHaveBeenCalled(); }); @@ -439,13 +439,13 @@ describe('log handling', () => { { action: makeMove('A', [], '0'), _stateID: 0, - phase: 'default', + phase: '', turn: 0, }, { action: makeMove('A', [], '0'), _stateID: 1, - phase: 'default', + phase: '', turn: 0, }, ]); diff --git a/src/client/log/log.test.js b/src/client/log/log.test.js index 792b120e5..978e39f91 100644 --- a/src/client/log/log.test.js +++ b/src/client/log/log.test.js @@ -19,9 +19,8 @@ Enzyme.configure({ adapter: new Adapter() }); describe('layout', () => { const game = { - startingPhase: 'A', phases: { - A: { next: 'B' }, + A: { next: 'B', start: true }, B: { next: 'A' }, }, }; diff --git a/src/core/flow.js b/src/core/flow.js index 296ff1ad1..3969faf36 100644 --- a/src/core/flow.js +++ b/src/core/flow.js @@ -174,16 +174,13 @@ export function Flow({ * * // A phase-specific turn structure that overrides the global. * turn: { ... }, + * + * // Set to true to begin the game in this phase. Only one phase + * // can have this set to true. + * start: false, * } */ -export function FlowWithPhases({ - phases, - startingPhase, - endIf, - turn, - events, - plugins, -}) { +export function FlowWithPhases({ phases, endIf, turn, events, plugins }) { // Attach defaults. if (events === undefined) { events = {}; @@ -203,23 +200,28 @@ export function FlowWithPhases({ if (plugins === undefined) { plugins = []; } - if (!startingPhase) startingPhase = 'default'; + if (!endIf) endIf = () => undefined; if (!turn) turn = {}; const phaseMap = phases || {}; - if ('default' in phaseMap) { - logging.error('cannot specify phase with name "default"'); + if ('' in phaseMap) { + logging.error('cannot specify phase with empty name'); } - phaseMap['default'] = {}; + phaseMap[''] = {}; let moveMap = {}; + let startingPhase = ''; for (let phase in phaseMap) { const conf = phaseMap[phase]; + if (conf.start === true) { + startingPhase = phase; + } + if (conf.moves !== undefined) { for (let move of Object.keys(conf.moves)) { moveMap[phase + '.' + move] = conf.moves[move]; @@ -258,13 +260,17 @@ export function FlowWithPhases({ conf.turn.onEnd = plugin.FnWrap(conf.turn.onEnd, plugins); } + function GetPhase(ctx) { + return phaseMap[ctx.phase]; + } + const shouldEndPhase = ({ G, ctx }) => { - const conf = phaseMap[ctx.phase]; + const conf = GetPhase(ctx); return conf.endIf(G, ctx); }; const shouldEndTurn = ({ G, ctx }) => { - const conf = phaseMap[ctx.phase]; + const conf = GetPhase(ctx); const currentPlayerMoves = ctx.stats.turn.numMoves[ctx.currentPlayer] || 0; if (conf.turn.moveLimit && currentPlayerMoves >= conf.turn.moveLimit) { @@ -317,12 +323,7 @@ export function FlowWithPhases({ }; const startGame = function(state) { - if (!(state.ctx.phase in phaseMap)) { - logging.error('invalid startingPhase: ' + state.ctx.phase); - return state; - } - - const conf = phaseMap[state.ctx.phase]; + const conf = GetPhase(state.ctx); state = startPhase(state, conf); state = startTurn(state, conf); return state; @@ -346,7 +347,7 @@ export function FlowWithPhases({ let ctx = state.ctx; // Run any cleanup code for the phase that is about to end. - const conf = phaseMap[ctx.phase]; + const conf = GetPhase(ctx); G = conf.onEnd(G, ctx); const gameover = endIf(G, ctx); @@ -354,23 +355,21 @@ export function FlowWithPhases({ return { ...state, G, ctx: { ...ctx, gameover } }; } - const prevPhase = ctx.phase; - // Update the phase. if (arg && arg !== true) { if (arg.next in phaseMap) { - ctx = { ...ctx, phase: arg.next, prevPhase }; + ctx = { ...ctx, phase: arg.next }; } else { logging.error('invalid argument to endPhase: ' + arg); } } else if (conf.next !== undefined) { - ctx = { ...ctx, phase: conf.next, prevPhase }; + ctx = { ...ctx, phase: conf.next }; } else { - ctx = { ...ctx, phase: ctx.prevPhase, prevPhase }; + ctx = { ...ctx, phase: '' }; } // Run any setup code for the new phase. - state = startPhase({ ...state, G, ctx }, phaseMap[ctx.phase]); + state = startPhase({ ...state, G, ctx }, GetPhase(ctx)); const origTurn = state.ctx.turn; @@ -387,7 +386,7 @@ export function FlowWithPhases({ state, automaticGameEvent( 'endPhase', - [{ next: 'default' }, visitedPhases], + [{ next: '' }, visitedPhases], this.playerID ) ); @@ -424,7 +423,7 @@ export function FlowWithPhases({ function endTurnEvent(state, arg) { let { G, ctx } = state; - const conf = phaseMap[ctx.phase]; + const conf = GetPhase(ctx); // Prevent ending the turn if moveLimit haven't been made. const currentPlayerMoves = ctx.stats.turn.numMoves[ctx.currentPlayer] || 0; @@ -503,7 +502,7 @@ export function FlowWithPhases({ } function processMove(state, action, dispatch) { - let conf = phaseMap[state.ctx.phase]; + let conf = GetPhase(state.ctx); state = updateStats(state, 'turn', action.playerID); state = updateStats(state, 'phase', action.playerID); @@ -542,7 +541,7 @@ export function FlowWithPhases({ automaticGameEvent('endPhase', [endPhase], action.playerID) ); // Update to the new phase configuration - conf = phaseMap[state.ctx.phase]; + conf = GetPhase(state.ctx); } // End the turn automatically if turn.endIf is true or if endIf returns. @@ -610,7 +609,6 @@ export function FlowWithPhases({ stats: { turn: { numMoves: {} }, phase: { numMoves: {} } }, allPlayed: false, phase: startingPhase, - prevPhase: 'default', stage: {}, }), init: state => { diff --git a/src/core/flow.test.js b/src/core/flow.test.js index 633dbc48a..07a051ef8 100644 --- a/src/core/flow.test.js +++ b/src/core/flow.test.js @@ -29,32 +29,19 @@ test('Flow', () => { }); describe('phases', () => { - test('invalid startingPhase', () => { - const flow = FlowWithPhases({ - startingPhase: 'A', - phases: { B: {} }, - }); - flow.init({ ctx: flow.ctx(2) }); - expect(error).toHaveBeenCalledWith(`invalid startingPhase: A`); - }); - test('invalid phase name', () => { const flow = FlowWithPhases({ - startingPhase: 'A', - phases: { default: {} }, + phases: { '': {} }, }); flow.init({ ctx: flow.ctx(2) }); - expect(error).toHaveBeenCalledWith( - `cannot specify phase with name "default"` - ); + expect(error).toHaveBeenCalledWith('cannot specify phase with empty name'); }); test('onBegin / onEnd', () => { const flow = FlowWithPhases({ - startingPhase: 'A', - phases: { A: { + start: true, onBegin: s => ({ ...s, setupA: true }), onEnd: s => ({ ...s, cleanupA: true }), next: 'B', @@ -101,8 +88,7 @@ describe('phases', () => { test('endIf', () => { const flow = FlowWithPhases({ - startingPhase: 'A', - phases: { A: { endIf: () => true, next: 'B' }, B: {} }, + phases: { A: { start: true, endIf: () => true, next: 'B' }, B: {} }, }); const state = { ctx: flow.ctx(2) }; @@ -126,9 +112,8 @@ describe('phases', () => { test('infinite loop', () => { const endIf = () => true; const flow = FlowWithPhases({ - startingPhase: 'A', phases: { - A: { endIf, next: 'B' }, + A: { endIf, next: 'B', start: true }, B: { endIf, next: 'A' }, }, }); @@ -138,7 +123,7 @@ describe('phases', () => { expect(state.ctx.phase).toBe('A'); state = flow.processGameEvent(state, gameEvent('endPhase')); - expect(state.ctx.phase).toBe('default'); + expect(state.ctx.phase).toBe(''); }); test('end phase on move', () => { @@ -147,9 +132,9 @@ describe('phases', () => { const onMove = () => ({ A: true }); const flow = FlowWithPhases({ - startingPhase: 'A', phases: { A: { + start: true, turn: { endIf: () => true, onMove }, endIf: () => true, onEnd: () => ++endPhaseACount, @@ -175,8 +160,7 @@ describe('phases', () => { test('end turn when final phase is reached', () => { const flow = FlowWithPhases({ turn: { endIf: (G, ctx) => ctx.phase === 'C' }, - startingPhase: 'A', - phases: { A: { next: 'B' }, B: { next: 'C' }, C: {} }, + phases: { A: { start: true, next: 'B' }, B: { next: 'C' }, C: {} }, }); let state = { G: {}, ctx: flow.ctx(2) }; @@ -292,8 +276,7 @@ test('onMove', () => { test('init', () => { let flow = FlowWithPhases({ - startingPhase: 'A', - phases: { A: { onEnd: () => ({ done: true }) } }, + phases: { A: { start: true, onEnd: () => ({ done: true }) } }, }); const orig = flow.ctx(2); @@ -302,8 +285,7 @@ test('init', () => { expect(state).toEqual({ G: {}, ctx: orig }); flow = FlowWithPhases({ - startingPhase: 'A', - phases: { A: { onBegin: () => ({ done: true }) } }, + phases: { A: { start: true, onBegin: () => ({ done: true }) } }, }); state = { ctx: orig }; @@ -381,9 +363,8 @@ describe('turn.endIf', () => { A: () => ({ endTurn: true }), B: G => G, }, - startingPhase: 'A', phases: { - A: { turn: { endIf: G => G.endTurn } }, + A: { start: true, turn: { endIf: G => G.endTurn } }, }, }; const client = Client({ game }); @@ -471,8 +452,7 @@ test('endGame', () => { describe('endTurn / endPhase args', () => { const flow = FlowWithPhases({ - startingPhase: 'A', - phases: { A: { next: 'B' }, B: {}, C: {} }, + phases: { A: { start: true, next: 'B' }, B: {}, C: {} }, }); const state = { ctx: flow.ctx(3) }; @@ -530,9 +510,8 @@ test('undoable moves', () => { C: () => ({ C: true }), }, - startingPhase: 'A', phases: { - A: {}, + A: { start: true }, B: {}, }, }; @@ -574,8 +553,7 @@ test('undoable moves', () => { test('endTurn is not called twice in one move', () => { const flow = FlowWithPhases({ turn: { endIf: () => true }, - startingPhase: 'A', - phases: { A: { endIf: G => G.endPhase, next: 'B' }, B: {} }, + phases: { A: { start: true, endIf: G => G.endPhase, next: 'B' }, B: {} }, }); let state = flow.init({ G: {}, ctx: flow.ctx(2) }); @@ -628,8 +606,7 @@ test('allPlayed', () => { describe('endPhase returns to previous phase', () => { let state; const flow = FlowWithPhases({ - startingPhase: 'A', - phases: { A: {}, B: {}, C: {} }, + phases: { A: { start: true }, B: {}, C: {} }, }); beforeEach(() => { @@ -640,19 +617,6 @@ describe('endPhase returns to previous phase', () => { test('returns to default', () => { expect(state.ctx.phase).toBe('A'); state = flow.processGameEvent(state, gameEvent('endPhase')); - expect(state.ctx.phase).toBe('default'); - }); - - test('returns to previous', () => { - expect(state.ctx.phase).toBe('A'); - state = flow.processGameEvent( - state, - gameEvent('endPhase', [{ next: 'B' }]) - ); - expect(state.ctx.phase).toBe('B'); - state = flow.processGameEvent(state, gameEvent('endPhase')); - expect(state.ctx.phase).toBe('A'); - state = flow.processGameEvent(state, gameEvent('endPhase')); - expect(state.ctx.phase).toBe('B'); + expect(state.ctx.phase).toBe(''); }); }); diff --git a/src/core/game.test.js b/src/core/game.test.js index 433c4281a..84c9d1783 100644 --- a/src/core/game.test.js +++ b/src/core/game.test.js @@ -73,9 +73,9 @@ test('rounds with starting player token', () => { }, }, - startingPhase: 'main', phases: { main: { + start: true, turn: { order: { first: G => G.startingPlayerToken, @@ -115,9 +115,9 @@ test('rounds with starting player token', () => { // The following pattern is used in Catan, Twilight Imperium, and (sort of) Powergrid. test('serpentine setup phases', () => { const game = { - startingPhase: 'first setup round', phases: { 'first setup round': { + start: true, turn: { order: { first: () => 0, diff --git a/src/core/reducer.test.js b/src/core/reducer.test.js index 943bd9f20..660e04a7b 100644 --- a/src/core/reducer.test.js +++ b/src/core/reducer.test.js @@ -208,7 +208,7 @@ test('deltalog', () => { { action: actionA, _stateID: 0, - phase: 'default', + phase: '', turn: 0, }, ]); @@ -217,7 +217,7 @@ test('deltalog', () => { { action: actionB, _stateID: 1, - phase: 'default', + phase: '', turn: 0, }, ]); @@ -226,7 +226,7 @@ test('deltalog', () => { { action: actionC, _stateID: 2, - phase: 'default', + phase: '', turn: 0, }, ]); @@ -306,9 +306,8 @@ test('undo / redo', () => { ctx.events.endPhase({ next: 'phase2' }); }, }, - startingPhase: 'phase1', phases: { - phase1: {}, + phase1: { start: true }, phase2: {}, }, }; diff --git a/src/core/turn-order.test.js b/src/core/turn-order.test.js index b33424210..1b576c9ea 100644 --- a/src/core/turn-order.test.js +++ b/src/core/turn-order.test.js @@ -38,8 +38,7 @@ describe('turn orders', () => { test('DEFAULT', () => { const flow = FlowWithPhases({ - startingPhase: 'A', - phases: { A: {}, B: {} }, + phases: { A: { start: true }, B: {} }, }); let state = { ctx: flow.ctx(2) }; @@ -60,8 +59,7 @@ describe('turn orders', () => { test('ONCE', () => { const flow = FlowWithPhases({ turn: { order: TurnOrder.ONCE }, - startingPhase: 'A', - phases: { A: { next: 'B' }, B: {} }, + phases: { A: { start: true, next: 'B' }, B: {} }, }); let state = { ctx: flow.ctx(2) }; @@ -98,8 +96,7 @@ describe('turn orders', () => { test('ANY_ONCE', () => { const flow = FlowWithPhases({ turn: { order: TurnOrder.ANY_ONCE }, - startingPhase: 'A', - phases: { A: {} }, + phases: { A: { start: true } }, }); let state = { ctx: flow.ctx(2) }; @@ -124,7 +121,7 @@ describe('turn orders', () => { state = flow.processMove(state, makeMove('', null, '1').payload); - expect(state.ctx.phase).toBe('default'); + expect(state.ctx.phase).toBe(''); expect(state.ctx.currentPlayer).toBe('0'); expect(state.ctx.actionPlayers).toEqual(['0', '1']); }); @@ -148,8 +145,7 @@ describe('turn orders', () => { test('OTHERS_ONCE', () => { const flow = FlowWithPhases({ turn: { order: TurnOrder.OTHERS_ONCE }, - startingPhase: 'A', - phases: { A: {} }, + phases: { A: { start: true } }, }); let state = { ctx: flow.ctx(3) }; @@ -174,7 +170,7 @@ describe('turn orders', () => { state = flow.processMove(state, makeMove('', null, '2').payload); - expect(state.ctx.phase).toBe('default'); + expect(state.ctx.phase).toBe(''); expect(state.ctx.currentPlayer).toBe('0'); expect(state.ctx.actionPlayers).toEqual(['1', '2']); }); @@ -213,9 +209,9 @@ describe('turn orders', () => { test('manual', () => { const flow = FlowWithPhases({ - startingPhase: 'A', phases: { A: { + start: true, turn: { order: { first: () => 9, @@ -241,8 +237,7 @@ describe('turn orders', () => { test('passing', () => { const game = { moves: { pass: Pass }, - startingPhase: 'A', - phases: { A: { turn: { order: TurnOrder.SKIP } } }, + phases: { A: { start: true, turn: { order: TurnOrder.SKIP } } }, }; const reducer = CreateGameReducer({ game }); let state = InitializeGame({ game, numPlayers: 3 }); @@ -316,8 +311,7 @@ test('override', () => { let flow = FlowWithPhases({ turn: { order: even }, - phases: { A: { next: 'B' }, B: { turn: { order: odd } } }, - startingPhase: 'A', + phases: { A: { start: true, next: 'B' }, B: { turn: { order: odd } } }, }); let state = { ctx: flow.ctx(10) }; @@ -434,10 +428,9 @@ describe('SetActionPlayers', () => { test('militia', () => { const game = { - startingPhase: 'A', - phases: { A: { + start: true, moves: { playMilitia: (G, ctx) => { ctx.events.endPhase({ next: 'B' }); diff --git a/src/master/master.test.js b/src/master/master.test.js index 676cb8c15..072d14e1b 100644 --- a/src/master/master.test.js +++ b/src/master/master.test.js @@ -119,7 +119,7 @@ describe('update', () => { currentPlayer: '0', currentPlayerMoves: 0, numPlayers: 2, - phase: 'default', + phase: '', playOrder: ['0', '1'], playOrderPos: 0, stats: { @@ -139,7 +139,7 @@ describe('update', () => { currentPlayer: '1', currentPlayerMoves: 0, numPlayers: 2, - phase: 'default', + phase: '', playOrder: ['0', '1'], playOrderPos: 1, stats: { diff --git a/src/plugins/plugin-immer.test.js b/src/plugins/plugin-immer.test.js index 12e35464c..51db11edb 100644 --- a/src/plugins/plugin-immer.test.js +++ b/src/plugins/plugin-immer.test.js @@ -20,10 +20,9 @@ describe('immer', () => { }, }, - startingPhase: 'A', - phases: { A: { + start: true, onBegin: G => { G.onPhaseBegin = true; },