Skip to content

Commit

Permalink
undoableMoves implementation (#149)
Browse files Browse the repository at this point in the history
* make the changelog look nice again, fix small typo and have a consitent look

* add tests for undoableMoves

* implementing undoableMoves

* updating undo/redo docs to include undoableMoves

* update tests to get rid of the `undo` boolean

* remove `undo` boolean

* update docs to show usage without the `undo` boolean

* assuming all moves come with actions and payload, so updated failing test accordingly and got rid of check if action and payload is there
  • Loading branch information
sladwig authored and nicolodavis committed Mar 18, 2018
1 parent 1e7d7b4 commit a240e45
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 36 deletions.
2 changes: 2 additions & 0 deletions docs/api/Game.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ game state and the moves. The moves are converted to a
Code to run at the end of a move.
* `flow.movesPerTurn` (_number_): Ends the turn automatically if a certain number
of moves have been made.
* `flow.undoableMoves` (_array_): Enables undo and redo of listed moves.
Leave `undefined` if all moves should be undoable.
* `flow.phases` (_array_): Optional list of game phases. See
[Phases](/phases) for more information.

Expand Down
49 changes: 36 additions & 13 deletions docs/undo.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,53 @@ before committing to one.

#### Usage

In order to activate this feature, all you need to do is
add `undo: true` to your `flow` section:
You can dispatch the `undo` and `redo` events in a similar
manner like `endTurn`:

```
onClickUndoButton() {
this.props.events.undo();
}
onClickRedoButton() {
this.props.events.redo();
}
```

##### Restricting Undoable Moves

In case you just want specific moves to be undoable,
for example to prevent peeking at cards or rerolling of
dices, you can instead add `undoableMoves` to your `flow`
section similar to `allowedMoves` in `phases`:

```js
Game({
moves: {
...
rollDice: (G, ctx) => ...
playCard: (G, ctx) => ...
},

flow: {
undo: true
undoableMoves: ['playCard'],
}
});
```

This will enable two new events `undo` and `redo` that you
can dispatch in a manner similar to that used for `endTurn`.
This way only `playCard` will be undoable, but not `rollDice`.

```
onClickUndoButton() {
this.props.events.undo();
}
##### Deactivating Undo / Redo Functionality

onClickRedoButton() {
this.props.events.undo();
}
In order to deactivate this feature completely initialize
`undoableMoves` with an empty array and the `undo` and `redo`
events will not be available in the game.

```js
Game({
...

flow: {
undoableMoves: [],
}
});
```
30 changes: 27 additions & 3 deletions src/client/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ test('event dispatchers', () => {
const reducer = createGameReducer({ game, numPlayers: 2 });
const store = createStore(reducer);
const api = createEventDispatchers(game.flow.eventNames, store);
expect(Object.getOwnPropertyNames(api)).toEqual(['endTurn']);
expect(Object.getOwnPropertyNames(api)).toEqual([
'undo',
'redo',
'endTurn',
]);
expect(store.getState().ctx.turn).toBe(0);
api.endTurn();
expect(store.getState().ctx.turn).toBe(1);
Expand All @@ -82,7 +86,6 @@ test('event dispatchers', () => {
{
const game = Game({
flow: {
undo: true,
endPhase: true,
},
});
Expand All @@ -105,7 +108,7 @@ test('event dispatchers', () => {
flow: {
endPhase: false,
endTurn: false,
undo: false,
undoableMoves: [],
},

phases: [{ name: 'default' }],
Expand All @@ -115,6 +118,27 @@ test('event dispatchers', () => {
const api = createEventDispatchers(game.flow.eventNames, store);
expect(Object.getOwnPropertyNames(api)).toEqual([]);
}

{
const game = Game({
flow: {
endPhase: true,
undoableMoves: ['A'],
},
});
const reducer = createGameReducer({ game, numPlayers: 2 });
const store = createStore(reducer);
const api = createEventDispatchers(game.flow.eventNames, store);
expect(Object.getOwnPropertyNames(api)).toEqual([
'undo',
'redo',
'endTurn',
'endPhase',
]);
expect(store.getState().ctx.turn).toBe(0);
api.endTurn();
expect(store.getState().ctx.turn).toBe(1);
}
});

test('move dispatchers', () => {
Expand Down
18 changes: 11 additions & 7 deletions src/core/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ export function Flow({
*
* @param {...object} endPhase - Set to false to disable the `endPhase` event.
*
* @param {...object} undo - Set to true to enable the undo/redo events.
* @param {...object} undoableMoves - List of moves that are undoable,
* (default: undefined, i.e. all moves are undoable).
*
* @param {...object} optimisticUpdate - (G, ctx, move) => boolean
* Control whether a move should
Expand Down Expand Up @@ -228,7 +229,7 @@ export function FlowWithPhases({
turnOrder,
endTurn,
endPhase,
undo,
undoableMoves,
optimisticUpdate,
canMakeMove,
}) {
Expand All @@ -239,9 +240,6 @@ export function FlowWithPhases({
if (endTurn === undefined) {
endTurn = true;
}
if (undo === undefined) {
undo = false;
}
if (optimisticUpdate === undefined) {
optimisticUpdate = () => true;
}
Expand Down Expand Up @@ -442,6 +440,11 @@ export function FlowWithPhases({
const last = _undo[_undo.length - 1];
const restore = _undo[_undo.length - 2];

// only allow undoableMoves to be undoable
if (undoableMoves && !undoableMoves.includes(last.moveType)) {
return state;
}

return {
...state,
G: restore.G,
Expand Down Expand Up @@ -507,9 +510,10 @@ export function FlowWithPhases({
// Update undo / redo state.
if (!endTurn) {
const undo = state._undo || [];
const moveType = action.payload.type;
state = {
...state,
_undo: [...undo, { G: state.G, ctx: state.ctx }],
_undo: [...undo, { G: state.G, ctx: state.ctx, moveType }],
_redo: [],
};
}
Expand All @@ -535,7 +539,7 @@ export function FlowWithPhases({
};

let enabledEvents = {};
if (undo) {
if (undoableMoves === undefined || undoableMoves.length > 0) {
enabledEvents['undo'] = undoEvent;
enabledEvents['redo'] = redoEvent;
}
Expand Down
69 changes: 56 additions & 13 deletions src/core/flow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ test('movesPerTurn', () => {
let flow = FlowWithPhases({ movesPerTurn: 2 });
let state = { ctx: flow.ctx(2) };
expect(state.ctx.turn).toBe(0);
state = flow.processMove(state, { move: {} });
state = flow.processMove(state, { move: {}, payload: {} });
expect(state.ctx.turn).toBe(0);
state = flow.processGameEvent(state, { type: 'endTurn' });
expect(state.ctx.turn).toBe(0);
state = flow.processMove(state, { move: {} });
state = flow.processMove(state, { move: {}, payload: {} });
expect(state.ctx.turn).toBe(1);
}

Expand All @@ -89,17 +89,17 @@ test('movesPerTurn', () => {
});
let state = { ctx: flow.ctx(2) };
expect(state.ctx.turn).toBe(0);
state = flow.processMove(state, { move: {} });
state = flow.processMove(state, { move: {}, payload: {} });
expect(state.ctx.turn).toBe(0);
state = flow.processGameEvent(state, { type: 'endTurn' });
expect(state.ctx.turn).toBe(0);
state = flow.processMove(state, { move: {} });
state = flow.processMove(state, { move: {}, payload: {} });
expect(state.ctx.turn).toBe(1);

state = flow.processGameEvent(state, { type: 'endPhase' });

expect(state.ctx.turn).toBe(1);
state = flow.processMove(state, { move: {} });
state = flow.processMove(state, { move: {}, payload: {} });
expect(state.ctx.turn).toBe(2);
}
});
Expand Down Expand Up @@ -187,7 +187,7 @@ test('onMove', () => {
{
let flow = FlowWithPhases({ onMove });
let state = { G: {}, ctx: flow.ctx(2) };
state = flow.processMove(state);
state = flow.processMove(state, { payload: {} });
expect(state.G).toEqual({ A: true });
}

Expand All @@ -197,10 +197,10 @@ test('onMove', () => {
phases: [{ name: 'A' }, { name: 'B', onMove: () => ({ B: true }) }],
});
let state = { G: {}, ctx: flow.ctx(2) };
state = flow.processMove(state);
state = flow.processMove(state, { payload: {} });
expect(state.G).toEqual({ A: true });
state = flow.processGameEvent(state, { type: 'endPhase' });
state = flow.processMove(state);
state = flow.processMove(state, { payload: {} });
expect(state.G).toEqual({ B: true });
}
});
Expand Down Expand Up @@ -272,7 +272,7 @@ test('endPhaseIf', () => {
}

{
const t = flow.processMove(state, { type: 'move' });
const t = flow.processMove(state, { type: 'move', payload: {} });
expect(t.ctx.phase).toBe('B');
}

Expand Down Expand Up @@ -481,10 +481,6 @@ test('undo / redo', () => {
moves: {
move: (G, ctx, arg) => ({ ...G, [arg]: true }),
},

flow: {
undo: true,
},
});

const reducer = createGameReducer({ game, numPlayers: 2 });
Expand Down Expand Up @@ -534,6 +530,53 @@ test('undo / redo', () => {
expect(state.G).toEqual({ A: true });
});

test('undo / redo restricted by undoableMoves', () => {
let game = Game({
moves: {
undoableMove: (G, ctx, arg) => ({ ...G, [arg]: true }),
move: (G, ctx, arg) => ({ ...G, [arg]: true }),
},

flow: {
undoableMoves: ['undoableMove'],
},
});

const reducer = createGameReducer({ game, numPlayers: 2 });

let state = reducer(undefined, { type: 'init' });

state = reducer(state, makeMove('move', 'A'));
expect(state.G).toEqual({ A: true });

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

state = reducer(state, gameEvent('undo'));
expect(state.G).toEqual({ A: true });

state = reducer(state, gameEvent('redo'));
expect(state.G).toEqual({ A: true, B: true });

state = reducer(state, gameEvent('redo'));
expect(state.G).toEqual({ A: true, B: true });

state = reducer(state, gameEvent('undo'));
expect(state.G).toEqual({ A: true });

state = reducer(state, gameEvent('undo'));
expect(state.G).toEqual({ A: true });

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

state = reducer(state, gameEvent('undo'));
expect(state.G).toEqual({ A: true });

state = reducer(state, gameEvent('redo'));
expect(state.G).toEqual({ A: true, C: true });
});

test('canMakeMove', () => {
// default behaviour
const pid = { playerID: 0 };
Expand Down

0 comments on commit a240e45

Please sign in to comment.