Skip to content

Commit

Permalink
make allowedMoves both global and phase-specific
Browse files Browse the repository at this point in the history
Also change the semantics slightly:
- undefined no longer means anything.
- null is used to indicate that all moves are possible.
  • Loading branch information
darthfiddler committed Mar 21, 2018
1 parent cfe3238 commit da4711a
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 17 deletions.
25 changes: 20 additions & 5 deletions src/core/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ export function Flow({
* @param {...object} undoableMoves - List of moves that are undoable,
* (default: undefined, i.e. all moves are undoable).
*
* @param {...object} allowedMoves - List of moves that are allowed.
* This can be either an array of
* move names or a function with the
* signature (G, ctx) => [].
* null (or a function returning
* null) indicates that all moves
* are allowed (this is the default).
*
* @param {...object} optimisticUpdate - (G, ctx, move) => boolean
* Control whether a move should
* be executed optimistically on
Expand Down Expand Up @@ -235,6 +243,7 @@ export function FlowWithPhases({
endPhase,
endGame,
undoableMoves,
allowedMoves,
optimisticUpdate,
}) {
// Attach defaults.
Expand All @@ -257,6 +266,7 @@ export function FlowWithPhases({
if (!onTurnEnd) onTurnEnd = G => G;
if (!onMove) onMove = G => G;
if (!turnOrder) turnOrder = TurnOrder.DEFAULT;
if (allowedMoves === undefined) allowedMoves = null;

let phaseKeys = [];
let phaseMap = {};
Expand Down Expand Up @@ -295,9 +305,12 @@ export function FlowWithPhases({
if (conf.turnOrder === undefined) {
conf.turnOrder = turnOrder;
}
if (conf.allowedMoves && typeof conf.allowedMoves != 'function') {
const { allowedMoves } = conf;
conf.allowedMoves = () => allowedMoves;
if (conf.allowedMoves === undefined) {
conf.allowedMoves = allowedMoves;
}
if (typeof conf.allowedMoves !== 'function') {
const t = conf.allowedMoves;
conf.allowedMoves = () => t;
}
}

Expand Down Expand Up @@ -545,8 +558,10 @@ export function FlowWithPhases({

const canMakeMoveWrap = (G, ctx, opts) => {
const conf = phaseMap[ctx.phase];
if (conf.allowedMoves) {
const set = new Set(conf.allowedMoves({ G, ctx }));
const t = conf.allowedMoves(G, ctx);

if (Array.isArray(t)) {
const set = new Set(t);
if (!set.has(opts.type)) {
return false;
}
Expand Down
42 changes: 30 additions & 12 deletions src/core/flow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,20 +415,22 @@ test('canMakeMove', () => {
moves: {
A: () => ({ A: true }),
B: () => ({ B: true }),
C: () => ({ C: true }),
},

flow: {
allowedMoves: ['A', 'B'],
phases: [
{ name: 'A', allowedMoves: () => ['A'] },
{ name: 'B', allowedMoves: 'B' },
{ name: 'B', allowedMoves: ['B'] },
{ name: 'C' },
{ name: 'D', allowedMoves: null },
],
},
});

const reducer = createGameReducer({ game, numPlayers: 2 });
let state = reducer(undefined, { type: 'init' });
expect(state.ctx.phase).toBe('A');

// Basic.
let flow;
Expand All @@ -437,31 +439,51 @@ test('canMakeMove', () => {
flow = Flow({ canMakeMove: () => false });
expect(flow.canMakeMove(state.G, state.ctx)).toBe(false);

// B is disallowed in phase A.
state = reducer(state, makeMove('B'));
expect(state.G).not.toMatchObject({ A: true });
// Phase A (A is allowed).
expect(state.ctx.phase).toBe('A');

state = reducer(state, makeMove('A'));
expect(state.G).toMatchObject({ A: true });
state = reducer(state, makeMove('B'));
expect(state.G).not.toMatchObject({ B: true });
state = reducer(state, makeMove('C'));
expect(state.G).not.toMatchObject({ C: true });

// Phase B (B is allowed).
state = reducer(state, gameEvent('endPhase'));
state.G = {};
expect(state.ctx.phase).toBe('B');

// A is disallowed in phase B.
state = reducer(state, makeMove('A'));
expect(state.G).not.toMatchObject({ B: true });
expect(state.G).not.toMatchObject({ A: true });
state = reducer(state, makeMove('B'));
expect(state.G).toMatchObject({ B: true });
state = reducer(state, makeMove('C'));
expect(state.G).not.toMatchObject({ C: true });

// Phase C (A and B allowed).
state = reducer(state, gameEvent('endPhase'));
state.G = {};
expect(state.ctx.phase).toBe('C');

// All moves are allowed in phase C.
state = reducer(state, makeMove('A'));
expect(state.G).toMatchObject({ A: true });
state = reducer(state, makeMove('B'));
expect(state.G).toMatchObject({ B: true });
state = reducer(state, makeMove('C'));
expect(state.G).not.toMatchObject({ C: true });

// Phase D (A, B and C allowed).
state = reducer(state, gameEvent('endPhase'));
state.G = {};
expect(state.ctx.phase).toBe('D');

state = reducer(state, makeMove('A'));
expect(state.G).toMatchObject({ A: true });
state = reducer(state, makeMove('B'));
expect(state.G).toMatchObject({ B: true });
state = reducer(state, makeMove('C'));
expect(state.G).toMatchObject({ C: true });

// But not once the game is over.
state.ctx.gameover = true;
Expand All @@ -470,10 +492,6 @@ test('canMakeMove', () => {
expect(state.G).not.toMatchObject({ A: true });
state = reducer(state, makeMove('B'));
expect(state.G).not.toMatchObject({ B: true });

// the flow runs a user-provided validation
flow = FlowWithPhases({ canMakeMove: () => true });
expect(flow.canMakeMove(state.G, state.ctx)).toBe(false);
});

test('undo / redo', () => {
Expand Down

0 comments on commit da4711a

Please sign in to comment.