Skip to content

Commit

Permalink
remove pass event and make it a standard move
Browse files Browse the repository at this point in the history
  • Loading branch information
darthfiddler committed Jan 29, 2018
1 parent f3da742 commit 5b34c5d
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 95 deletions.
4 changes: 2 additions & 2 deletions packages/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Game from '../src/core/game.js';
import { Flow, FlowWithPhases } from '../src/core/flow.js';
import { TurnOrder } from '../src/core/turn-order.js';
import { TurnOrder, Pass } from '../src/core/turn-order.js';
import { PlayerView } from '../src/core/player-view.js';

export { Game, Flow, FlowWithPhases, TurnOrder, PlayerView };
export { Game, Flow, FlowWithPhases, TurnOrder, Pass, PlayerView };
3 changes: 2 additions & 1 deletion packages/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import Client from '../src/client/client.js';
import Game from '../src/core/game.js';
import { Flow, FlowWithPhases } from '../src/core/flow.js';
import { TurnOrder } from '../src/core/turn-order.js';
import { TurnOrder, Pass } from '../src/core/turn-order.js';

export default {
Client,
Game,
Flow,
FlowWithPhases,
TurnOrder,
Pass,
};
46 changes: 3 additions & 43 deletions src/core/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function Flow({ ctx, events, init, validator, processMove }) {
* Triggers are processed one after the other in the
* order they are defined at the end of each move.
*
* @param {...object} events - A list of events in ['endTurn', 'endPhase', 'pass']
* @param {...object} events - A list of events in ['endTurn', 'endPhase']
* to enable for this flow. If not passed, all relevant
* events are enabled.
*
Expand All @@ -136,9 +136,6 @@ export function Flow({ ctx, events, init, validator, processMove }) {
* // The phase can also end when the `endPhase` game event happens.
* endPhaseIf: (G, ctx) => {},
*
* // Any code to run when a player passes in this phase.
* onPass: (G, ctx) => G,
*
* Phase-specific options that override their global equivalents:
*
* // A phase-specific endTurnIf.
Expand Down Expand Up @@ -175,7 +172,7 @@ export function FlowWithPhases({
if (!phases) {
events = ['endTurn'];
} else {
events = ['endTurn', 'endPhase', 'pass'];
events = ['endTurn', 'endPhase'];
}
}
if (!phases) phases = [{ name: 'default' }];
Expand All @@ -201,9 +198,6 @@ export function FlowWithPhases({
if (conf.onPhaseEnd === undefined) {
conf.onPhaseEnd = G => G;
}
if (conf.onPass === undefined) {
conf.onPass = G => G;
}
if (conf.movesPerTurn === undefined) {
conf.movesPerTurn = movesPerTurn;
}
Expand Down Expand Up @@ -231,7 +225,7 @@ export function FlowWithPhases({

// Helper to perform start-of-phase initialization.
const startPhase = function(state, phaseConfig) {
const ctx = { ...state.ctx, passMap: {}, allPassed: false };
const ctx = { ...state.ctx };
const G = phaseConfig.onPhaseBegin(state.G, ctx);
ctx.currentPlayer = phaseConfig.turnOrder.first(G, ctx);
return { ...state, G, ctx };
Expand Down Expand Up @@ -311,34 +305,6 @@ export function FlowWithPhases({
return { ...state, G, ctx };
}

/**
* pass (game event)
*
* The current player passes (and ends the turn).
*/
function pass(state) {
let G = state.G;
let ctx = state.ctx;
const conf = phaseMap[state.ctx.phase];
G = conf.onPass(G, ctx);

// Mark that the player has passed.
const playerID =
ctx.currentPlayer == 'any' ? this.playerID : ctx.currentPlayer;

if (playerID !== undefined) {
let passMap = { ...ctx.passMap };
passMap[playerID] = true;
ctx = { ...ctx, passMap };

if (Object.keys(passMap).length >= ctx.numPlayers) {
ctx.allPassed = true;
}
}

return endTurn({ ...state, G, ctx });
}

function processMove(state, action, dispatch) {
// Update currentPlayerMoves.
const currentPlayerMoves = state.ctx.currentPlayerMoves + 1;
Expand Down Expand Up @@ -398,10 +364,6 @@ export function FlowWithPhases({
enabledEvents[e] = endPhase;
break;
}
case 'pass': {
enabledEvents[e] = pass;
break;
}
}
}

Expand All @@ -412,8 +374,6 @@ export function FlowWithPhases({
currentPlayer: '0',
currentPlayerMoves: 0,
phase: phases[0].name,
passMap: {},
allPassed: false,
}),
init: state => startPhase(state, phases[0]),
events: enabledEvents,
Expand Down
19 changes: 0 additions & 19 deletions src/core/flow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { createStore } from 'redux';
import { createGameReducer } from './reducer';
import { makeMove, gameEvent } from './action-creators';
import { Flow, FlowWithPhases, createEventDispatchers } from './flow';
import { TurnOrder } from './turn-order';

test('Flow', () => {
const flow = Flow({});
Expand Down Expand Up @@ -39,14 +38,12 @@ test('FlowWithPhases', () => {
});

test('callbacks', () => {
const onPass = jest.fn(G => G);
const onPhaseBegin = jest.fn(G => G);
const onPhaseEnd = jest.fn(G => G);

let flow = FlowWithPhases({
phases: [
{
onPass,
onPhaseBegin,
onPhaseEnd,
},
Expand All @@ -55,16 +52,12 @@ test('callbacks', () => {

let state = { ctx: flow.ctx(2) };

expect(onPass).not.toHaveBeenCalled();
expect(onPhaseBegin).not.toHaveBeenCalled();
expect(onPhaseEnd).not.toHaveBeenCalled();

flow.init(state);
expect(onPhaseBegin).toHaveBeenCalled();

flow.processGameEvent(state, { type: 'pass' });
expect(onPass).toHaveBeenCalled();

flow.processGameEvent(state, { type: 'endPhase' });
expect(onPhaseEnd).toHaveBeenCalled();
});
Expand Down Expand Up @@ -176,18 +169,6 @@ test('init', () => {
expect(state.G).toEqual({ done: true });
});

test('pass', () => {
let flow = FlowWithPhases({
phases: [{ name: 'A', turnOrder: TurnOrder.ANY }],
});
let state = { ctx: flow.ctx(2) };
expect(state.ctx.allPassed).toBe(false);
state = flow.processGameEvent(state, { type: 'pass', playerID: '0' });
expect(state.ctx.allPassed).toBe(false);
state = flow.processGameEvent(state, { type: 'pass', playerID: '1' });
expect(state.ctx.allPassed).toBe(true);
});

test('onPhaseBegin / onPhaseEnd', () => {
const flow = FlowWithPhases({
phases: [
Expand Down
28 changes: 26 additions & 2 deletions src/core/turn-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@
* https://opensource.org/licenses/MIT.
*/

/**
* Standard move that simulates passing.
*
* Creates two objects in G:
* passMap - A map from playerID -> boolean capturing passes.
* allPassed - Set to true when all players have passed.
*/
export const Pass = (G, ctx) => {
let passMap = {};
if (G.passMap !== undefined) {
passMap = { ...G.passMap };
}
const playerID =
ctx.currentPlayer === 'any' ? ctx.playerID : ctx.currentPlayer;
passMap[playerID] = true;
G = { ...G, passMap };
if (Object.keys(passMap).length >= ctx.numPlayers) {
G.allPassed = true;
}
return G;
};

/**
* Set of different turn orders possible in a phase.
* These are meant to be passed to the `turnOrder` setting
Expand Down Expand Up @@ -40,15 +62,17 @@ export const TurnOrder = {
* SKIP
*
* Round-robin, but skips over any players that have passed.
* Meant to be used with Move.PASS above.
*/

SKIP: {
first: (G, ctx) => ctx.currentPlayer,
next: (G, ctx) => {
if (ctx.allPassed) return;
if (G.allPassed) return;
let nextPlayer = ctx.currentPlayer;
for (let i = 0; i < ctx.numPlayers; i++) {
nextPlayer = (+nextPlayer + 1) % ctx.numPlayers + '';
if (!(nextPlayer in ctx.passMap)) {
if (!(nextPlayer in G.passMap)) {
return nextPlayer;
}
}
Expand Down
75 changes: 55 additions & 20 deletions src/core/turn-order.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
*/

import { FlowWithPhases } from './flow';
import { TurnOrder } from './turn-order';
import { TurnOrder, pass } from './turn-order';
import Game from './game';
import { makeMove, gameEvent } from './action-creators';
import { createGameReducer } from './reducer';

test('turnOrder', () => {
let flow = FlowWithPhases({
Expand Down Expand Up @@ -39,36 +42,68 @@ test('turnOrder', () => {
expect(state.ctx.currentPlayer).toBe('10');
state = flow.processGameEvent(state, { type: 'endTurn' });
expect(state.ctx.currentPlayer).toBe('3');
});

flow = FlowWithPhases({
test('passing', () => {
const flow = FlowWithPhases({
phases: [{ name: 'A', turnOrder: TurnOrder.SKIP }],
});
const game = Game({
flow,
moves: { pass },
});
const reducer = createGameReducer({ game, numPlayers: 3 });
let state = reducer(undefined, { type: 'init' });

state = { ctx: flow.ctx(3) };
state = flow.init(state);
expect(state.ctx.allPassed).toBe(false);
expect(state.ctx.currentPlayer).toBe('0');
state = reducer(state, makeMove('pass'));
state = reducer(state, gameEvent('endTurn'));
expect(state.G.allPassed).toBe(undefined);

state = flow.processGameEvent(state, { type: 'pass' });
expect(state.ctx.allPassed).toBe(false);
expect(state.ctx.currentPlayer).toBe('1');
state = reducer(state, gameEvent('endTurn'));
expect(state.G.allPassed).toBe(undefined);

state = flow.processGameEvent(state, { type: 'endTurn' });
expect(state.ctx.allPassed).toBe(false);
expect(state.ctx.currentPlayer).toBe('2');
state = reducer(state, gameEvent('endTurn'));
expect(state.G.allPassed).toBe(undefined);

state = flow.processGameEvent(state, { type: 'pass' });
expect(state.ctx.allPassed).toBe(false);
expect(state.ctx.currentPlayer).toBe('1');
state = reducer(state, makeMove('pass'));
state = reducer(state, gameEvent('endTurn'));
expect(state.G.allPassed).toBe(undefined);

state = flow.processGameEvent(state, { type: 'endTurn' });
expect(state.ctx.allPassed).toBe(false);
expect(state.ctx.currentPlayer).toBe('1');
expect(state.ctx.currentPlayer).toBe('2');
state = reducer(state, gameEvent('endTurn'));
expect(state.G.allPassed).toBe(undefined);

expect(state.ctx.currentPlayer).toBe('2');
state = reducer(state, makeMove('pass'));
expect(state.G.allPassed).toBe(true);

state = flow.processGameEvent(state, { type: 'pass' });
expect(state.ctx.allPassed).toBe(true);
expect(state.ctx.currentPlayer).toBe(undefined);
expect(state.ctx.currentPlayer).toBe('2');
state = reducer(state, gameEvent('endTurn'));
expect(state.G.allPassed).toBe(true);
});

state = flow.processGameEvent(state, { type: 'pass' });
expect(state.ctx.allPassed).toBe(true);
expect(state.ctx.currentPlayer).toBe(undefined);
test('end game after everyone passes', () => {
const flow = FlowWithPhases({
phases: [
{ name: 'A', turnOrder: TurnOrder.ANY, endGameIf: G => G.allPassed },
],
});
const game = Game({
flow,
moves: { pass },
});
const reducer = createGameReducer({ game, numPlayers: 2 });

let state = reducer(undefined, { type: 'init' });
expect(state.ctx.currentPlayer).toBe('any');
state = reducer(state, makeMove('pass', null, '0'));
expect(state.ctx.gameover).toBe(undefined);
state = reducer(state, makeMove('pass', null, '1'));
expect(state.ctx.gameover).toBe(true);
});

test('override', () => {
Expand Down
8 changes: 0 additions & 8 deletions src/server/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,18 @@ test('action', () => {
_id: 0,
_initial: {},
ctx: {
allPassed: false,
currentPlayer: '0',
currentPlayerMoves: 0,
numPlayers: 2,
passMap: {},
phase: 'default',
turn: 0,
},
log: [],
},
ctx: {
allPassed: false,
currentPlayer: '1',
currentPlayerMoves: 0,
numPlayers: 2,
passMap: {},
phase: 'default',
turn: 1,
},
Expand Down Expand Up @@ -181,22 +177,18 @@ test('playerView', () => {
_id: 0,
_initial: {},
ctx: {
allPassed: false,
currentPlayer: '0',
currentPlayerMoves: 0,
numPlayers: 2,
passMap: {},
phase: 'default',
turn: 0,
},
log: [],
},
ctx: {
allPassed: false,
currentPlayer: '0',
currentPlayerMoves: 0,
numPlayers: 2,
passMap: {},
phase: 'default',
turn: 0,
},
Expand Down

0 comments on commit 5b34c5d

Please sign in to comment.