Skip to content

Commit

Permalink
Triggers
Browse files Browse the repository at this point in the history
A mechanism to register actions to be run automatically when certain game conditions are met at the end of each move.
  • Loading branch information
darthfiddler committed Jan 10, 2018
1 parent e4d1021 commit 774e540
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 8 deletions.
29 changes: 29 additions & 0 deletions docs/api/Game.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ game state and the moves. The moves are converted to a
The turn automatically ends if this function returns true (checked after each move).
- `flow.onTurnEnd` (*function*): *(G, ctx) => G*
Code to run at the end of a turn.
- `flow.triggers` (*array*): An array of objects with the format:
`{ condition: (G, ctx) => boolean, action: (G, ctx) => G }`
At the end of each move, if `condition` is `true`, then the corresponding
`action` is executed.
- `flow.phases` (*array*): Optional list of game phases. See
[Phases](/phases) for more information.

Expand Down Expand Up @@ -122,3 +126,28 @@ const game = Game({
}
});
```

#### With triggers

```js
import { Game } from 'boardgame.io/core';
const game = Game({
setup: (numPlayers) => {
...
},
moves: {
...
},
flow: {
triggers: [
{
condition: G => isMilestone(G),
action: G => ({ ...G, milestone: true })
}
]
}
});
```
55 changes: 47 additions & 8 deletions src/core/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,23 @@ import { TurnOrder } from './turn-order';
* returns anything (checked after each move).
* The return value is available at `ctx.gameover`.
* (G, ctx) => {}
* @param {...object} triggers - An array of objects with the format:
* {
* condition: (G, ctx) => boolean,
* action: (G, ctx) => action,
* }
* Whenever `condition` is true the `action` is run.
* Triggers are processed one after the other in the
* order they are defined at the end of each move.
*/
export function Flow({ctx, events, init, validator, endTurnIf, endGameIf}) {
export function Flow({ ctx, events, init, validator, endTurnIf, endGameIf, triggers }) {
if (!ctx) ctx = () => ({});
if (!events) events = {};
if (!init) init = state => state;
if (!validator) validator = () => true;
if (!endTurnIf) endTurnIf = () => false;
if (!endGameIf) endGameIf = () => undefined;
if (!triggers) triggers = [];

const dispatch = (state, action) => {
if (events.hasOwnProperty(action.type)) {
Expand All @@ -68,15 +77,25 @@ export function Flow({ctx, events, init, validator, endTurnIf, endGameIf}) {
eventNames: Object.getOwnPropertyNames(events),

processMove: (state, action) => {
// Process triggers.
for (const trigger of triggers) {
if (trigger.condition(state.G, state.ctx)) {
const G = trigger.action(state.G, state.ctx);
state = { ...state, G };
}
}

// End the game automatically if endGameIf is true.
const gameover = endGameIf(state.G, state.ctx);
if (gameover !== undefined) {
return { ...state, ctx: { ...state.ctx, gameover } };
}

// End the turn automatically if endTurnIf is true.
if (endTurnIf(state.G, state.ctx)) {
return dispatch(state, { type: 'endTurn', playerID: action.move.playerID });
}

return state;
},

Expand All @@ -99,8 +118,16 @@ export function Flow({ctx, events, init, validator, endTurnIf, endGameIf}) {
* (G, ctx) => {}
* @param {...object} onTurnEnd - Any code to run when a turn ends.
* (G, ctx) => G
* @param {...object} triggers - An array of objects with the format:
* {
* condition: (G, ctx) => boolean,
* action: (G, ctx) => action,
* }
* Whenever `condition` is true the `action` is run.
* Triggers are processed one after the other in the
* order they are defined at the end of each move.
*/
export function SimpleFlow({ endTurnIf, endGameIf, onTurnEnd }) {
export function SimpleFlow({ endTurnIf, endGameIf, onTurnEnd, triggers }) {
if (!endTurnIf) endTurnIf = () => false;
if (!endGameIf) endGameIf = () => undefined;
if (!onTurnEnd) onTurnEnd = G => G;
Expand Down Expand Up @@ -138,6 +165,7 @@ export function SimpleFlow({ endTurnIf, endGameIf, onTurnEnd }) {
events: { endTurn },
endTurnIf,
endGameIf,
triggers,
});
}

Expand Down Expand Up @@ -172,6 +200,7 @@ export function SimpleFlow({ endTurnIf, endGameIf, onTurnEnd }) {
* // If the return value is the name of another phase,
* // that will be chosen as the next phase (as opposed
* // to the next one in round-robin order).
* // The phase can also end when the `endPhase` game event happens.
* endPhaseIf: (G, ctx) => {},
*
* // A phase-specific endTurnIf.
Expand All @@ -194,29 +223,38 @@ export function SimpleFlow({ endTurnIf, endGameIf, onTurnEnd }) {
* allowedMoves: ['moveA', ...],
* }
*
* The phase ends when endPhaseIf is met, or if the
* game reducer receives a `endPhase` game event.
*
* Global options (not associated with any phase):
* Most of these can be overriden on a per-phase basis (except triggers).
*
* @param {...object} endTurnIf - The turn automatically ends if this function
* returns true (checked after each move).
* This can be overriden on a per-phase basis.
* (G, ctx) => boolean
*
* @param {...object} endGameIf - The game automatically ends if this function
* returns anything (checked after each move).
* The return value is available at ctx.gameover.
* This can be overriden on a per-phase basis.
* (G, ctx) => {}
*
* @param {...object} onTurnEnd - Any code to run when a turn ends.
* This can be overriden on a per-phase basis.
* (G, ctx) => G
*
* @param {...object} triggers - An array of objects with the format:
* {
* condition: (G, ctx) => boolean,
* action: (G, ctx) => action,
* }
* Whenever `condition` is true the `action` is run.
* Triggers are processed one after the other in the
* order they are defined at the end of each move.
*/
export function FlowWithPhases({ phases, endTurnIf, endGameIf, onTurnEnd }) {
export function FlowWithPhases({ phases, endTurnIf, endGameIf, onTurnEnd, triggers }) {
// Attach defaults.
if (!phases) phases = [{ name: 'default' }];
if (!endTurnIf) endTurnIf = () => false;
if (!endGameIf) endGameIf = () => undefined;
if (!onTurnEnd) onTurnEnd = G => G;
if (!triggers) triggers = [];

let phaseKeys = [];
let phaseMap = {};
Expand Down Expand Up @@ -393,6 +431,7 @@ export function FlowWithPhases({ phases, endTurnIf, endGameIf, onTurnEnd }) {
validator,
endTurnIf: endTurnIfWrap,
endGameIf: endGameIfWrap,
triggers,
});
}

Expand Down
31 changes: 31 additions & 0 deletions src/core/flow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,37 @@ test('onTurnEnd', () => {
}
});

test('triggers', () => {
const triggers = [
{
condition: G => G.milestone,
action: G => ({ ...G, action: true }),
}
];

{
let flow = SimpleFlow({ triggers });
let state = { G: {}, ctx: flow.ctx(2) };

state = flow.processMove(state);
expect(state.G.action).toBe(undefined);
state.G.milestone = true;
state = flow.processMove(state);
expect(state.G.action).toBe(true);
}

{
let flow = FlowWithPhases({ triggers });
let state = { G: {}, ctx: flow.ctx(2) };

state = flow.processMove(state);
expect(state.G.action).toBe(undefined);
state.G.milestone = true;
state = flow.processMove(state);
expect(state.G.action).toBe(true);
}
});

test('init', () => {
let flow = FlowWithPhases({
phases: [
Expand Down

0 comments on commit 774e540

Please sign in to comment.