Skip to content

Commit 7fcdbfe

Browse files
rzuliannicolodavis
authored andcommitted
Custom turn order (#130)
* custom order on G * playerOrder in ctx, aligned TurnOrder, passMap modified to passOrder as array * renamed to playOrder, added playOrderPos, currentPlayer is calculated from playOrder[playOrderPos] * add getCurrentPlayer() * remove onPhaseBegin from test * reduce test boilerplate * update docs
1 parent 748f36f commit 7fcdbfe

6 files changed

Lines changed: 130 additions & 41 deletions

File tree

docs/turn-order.md

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
# Turn Order
22

3-
You can customize the order in which the turn gets passed between players
4-
by using the `turnOrder` option. This is passed inside a `flow` section of
5-
the `Game` configuration.
3+
The framework maintains the turn order using the following fields:
64

7-
The default turn order (round-robin) is called `TurnOrder.DEFAULT`.
5+
```
6+
ctx: {
7+
currentPlayer: '0',
8+
playOrder: ['0', '1', '2', ...],
9+
playOrderPos: 0,
10+
}
11+
```
12+
13+
`playOrderPos` is an index into `playOrder` and the way in which it
14+
is updated is determined by a particular `TurnOrder`. The default
15+
behavior is to just increment it in a round-robin fashion.
16+
`currentPlayer` is just `playerOrder[playOrderPos]`.
17+
18+
If you need something different, you can customize this behavior
19+
by using the `turnOrder` option. This is passed inside a `flow`
20+
section of the `Game` configuration. The framework comes bundled
21+
with a few turn orders in the `TurnOrder` object, and you can
22+
even provide your own implementation.
823

924
```js
1025
import { Game, TurnOrder } from 'boardgame.io/core';
@@ -20,17 +35,42 @@ Game({
2035
}
2136
```
2237
38+
!> Turn orders can also be specified on a per-phase level.
39+
40+
#### Custom Turn Order
41+
2342
A `TurnOrder` object has the following structure:
2443
2544
```js
2645
{
27-
// Get the first player.
28-
first: (G, ctx) => startingPlayer,
46+
// Get the initial value of playOrderPos,
47+
first: (G, ctx) => 0,
48+
49+
// Get the next value of playOrderPos when endTurn is called.
50+
next: (G, ctx) => (ctx.playOrderPos + 1) % ctx.numPlayers,
51+
}
52+
```
2953
30-
// Get the next player when endTurn is called.
31-
next: (G, ctx) => nextPlayer
54+
The implementation above shows the default round-robin order.
55+
If you want to skip over every other player (for example), do
56+
something like this:
57+
58+
```js
59+
import { Game } from 'boardgame.io/core';
60+
61+
Game({
62+
moves: {
63+
...
64+
},
65+
66+
flow: {
67+
turnOrder: {
68+
first: () => 0,
69+
next: (G, ctx) => (ctx.playOrderPos + 2) % ctx.numPlayers,
70+
}
71+
}
3272
}
3373
```
3474
35-
!> `TurnOrder.ANY` implements a turn order where any player can play,
36-
and there isn't really a concept of a current player.
75+
!> If you would like any player to play, then return `undefined` from
76+
these functions. `TurnOrder.ANY` implements this.

src/core/flow.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,19 @@ export function FlowWithPhases({
300300
return conf.endTurnIf(G, ctx);
301301
};
302302

303+
const getCurrentPlayer = (playOrder, playOrderPos) => {
304+
if (playOrderPos === undefined) {
305+
return 'any';
306+
}
307+
return playOrder[playOrderPos] + '';
308+
};
309+
303310
// Helper to perform start-of-phase initialization.
304311
const startPhase = function(state, phaseConfig) {
305312
const ctx = { ...state.ctx };
306313
const G = phaseConfig.onPhaseBegin(state.G, ctx);
307-
ctx.currentPlayer = phaseConfig.turnOrder.first(G, ctx);
314+
ctx.playOrderPos = phaseConfig.turnOrder.first(G, ctx);
315+
ctx.currentPlayer = getCurrentPlayer(ctx.playOrder, ctx.playOrderPos);
308316
ctx.actionPlayers = [ctx.currentPlayer];
309317
return { ...state, G, ctx };
310318
};
@@ -400,12 +408,20 @@ export function FlowWithPhases({
400408
}
401409

402410
// Update current player.
403-
const currentPlayer = conf.turnOrder.next(G, ctx);
411+
const playOrderPos = conf.turnOrder.next(G, ctx);
412+
const currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos);
404413
const actionPlayers = [currentPlayer];
405414
// Update turn.
406415
const turn = ctx.turn + 1;
407416
// Update state.
408-
ctx = { ...ctx, currentPlayer, actionPlayers, turn, currentPlayerMoves: 0 };
417+
ctx = {
418+
...ctx,
419+
playOrderPos,
420+
currentPlayer,
421+
actionPlayers,
422+
turn,
423+
currentPlayerMoves: 0,
424+
};
409425

410426
// End phase if condition is met.
411427
const end = conf.endPhaseIf(G, ctx);
@@ -532,6 +548,8 @@ export function FlowWithPhases({
532548
turn: 0,
533549
currentPlayer: '0',
534550
currentPlayerMoves: 0,
551+
playOrder: Array.from(Array(numPlayers), (d, i) => i),
552+
playOrderPos: 0,
535553
phase: phases[0].name,
536554
}),
537555
init: state => {

src/core/game.test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ test('rounds with starting player token', () => {
5858
{
5959
name: 'main',
6060
turnOrder: {
61-
first: G => G.startingPlayerToken + '',
62-
next: (G, ctx) => (+ctx.currentPlayer + 1) % ctx.numPlayers + '',
61+
first: G => G.startingPlayerToken,
62+
next: (G, ctx) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
6363
},
6464
},
6565
],
@@ -101,22 +101,22 @@ test('serpentine setup phases', () => {
101101
{
102102
name: 'first setup round',
103103
turnOrder: {
104-
first: () => '0',
105-
next: (G, ctx) => (+ctx.currentPlayer + 1) % ctx.numPlayers + '',
104+
first: () => 0,
105+
next: (G, ctx) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
106106
},
107107
},
108108
{
109109
name: 'second setup round',
110110
turnOrder: {
111-
first: (G, ctx) => ctx.numPlayers - 1 + '',
112-
next: (G, ctx) => (+ctx.currentPlayer - 1) % ctx.numPlayers + '',
111+
first: (G, ctx) => ctx.playOrder.length - 1,
112+
next: (G, ctx) => (+ctx.playOrderPos - 1) % ctx.playOrder.length,
113113
},
114114
},
115115
{
116116
name: 'main phase',
117117
turnOrder: {
118-
first: () => '0',
119-
next: (G, ctx) => (+ctx.currentPlayer + 1) % ctx.numPlayers + '',
118+
first: () => 0,
119+
next: (G, ctx) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
120120
},
121121
},
122122
],

src/core/turn-order.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@
1010
* Standard move that simulates passing.
1111
*
1212
* Creates two objects in G:
13-
* passMap - A map from playerID -> boolean capturing passes.
13+
* passOrder - An array of playerIDs capturing passes in the pass order.
1414
* allPassed - Set to true when all players have passed.
1515
*/
1616
export const Pass = (G, ctx) => {
17-
let passMap = {};
18-
if (G.passMap !== undefined) {
19-
passMap = { ...G.passMap };
17+
let passOrder = [];
18+
if (G.passOrder !== undefined) {
19+
passOrder = G.passOrder;
2020
}
2121
const playerID =
2222
ctx.currentPlayer === 'any' ? ctx.playerID : ctx.currentPlayer;
23-
passMap[playerID] = true;
24-
G = { ...G, passMap };
25-
if (Object.keys(passMap).length >= ctx.numPlayers) {
23+
passOrder.push(playerID);
24+
G = { ...G, passOrder };
25+
if (passOrder.length >= ctx.numPlayers) {
2626
G.allPassed = true;
2727
}
2828
return G;
@@ -44,8 +44,8 @@ export const TurnOrder = {
4444
* The default round-robin turn order.
4545
*/
4646
DEFAULT: {
47-
first: (G, ctx) => ctx.currentPlayer,
48-
next: (G, ctx) => (+ctx.currentPlayer + 1) % ctx.numPlayers + '',
47+
first: () => 0,
48+
next: (G, ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
4949
},
5050

5151
/**
@@ -54,8 +54,8 @@ export const TurnOrder = {
5454
* Any player can play and there isn't a currentPlayer really.
5555
*/
5656
ANY: {
57-
first: () => 'any',
58-
next: () => 'any',
57+
first: () => undefined,
58+
next: () => undefined,
5959
},
6060

6161
/**
@@ -66,14 +66,14 @@ export const TurnOrder = {
6666
*/
6767

6868
SKIP: {
69-
first: (G, ctx) => ctx.currentPlayer,
69+
first: () => 0,
7070
next: (G, ctx) => {
7171
if (G.allPassed) return;
72-
let nextPlayer = ctx.currentPlayer;
73-
for (let i = 0; i < ctx.numPlayers; i++) {
74-
nextPlayer = (+nextPlayer + 1) % ctx.numPlayers + '';
75-
if (!(nextPlayer in G.passMap)) {
76-
return nextPlayer;
72+
let playOrderPos = ctx.playOrderPos;
73+
for (let i = 0; i < ctx.playOrder.length; i++) {
74+
playOrderPos = (playOrderPos + 1) % ctx.playOrder.length;
75+
if (!G.passOrder.includes(ctx.playOrder[playOrderPos] + '')) {
76+
return playOrderPos;
7777
}
7878
}
7979
},

src/core/turn-order.test.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ test('turnOrder', () => {
3434
expect(state.ctx.currentPlayer).toBe('any');
3535

3636
flow = FlowWithPhases({
37-
phases: [{ name: 'A', turnOrder: { first: () => '10', next: () => '3' } }],
37+
phases: [{ name: 'A', turnOrder: { first: () => 9, next: () => 3 } }],
3838
});
3939

4040
state = { ctx: flow.ctx(10) };
4141
state = flow.init(state);
42-
expect(state.ctx.currentPlayer).toBe('10');
42+
expect(state.ctx.currentPlayer).toBe('9');
4343
state = flow.processGameEvent(state, { type: 'endTurn' });
4444
expect(state.ctx.currentPlayer).toBe('3');
4545
});
@@ -59,10 +59,12 @@ test('passing', () => {
5959
state = reducer(state, makeMove('pass'));
6060
state = reducer(state, gameEvent('endTurn'));
6161
expect(state.G.allPassed).toBe(undefined);
62+
expect(state.G.passOrder).toEqual(['0']);
6263

6364
expect(state.ctx.currentPlayer).toBe('1');
6465
state = reducer(state, gameEvent('endTurn'));
6566
expect(state.G.allPassed).toBe(undefined);
67+
expect(state.G.passOrder).toEqual(['0']);
6668

6769
expect(state.ctx.currentPlayer).toBe('2');
6870
state = reducer(state, gameEvent('endTurn'));
@@ -72,6 +74,7 @@ test('passing', () => {
7274
state = reducer(state, makeMove('pass'));
7375
state = reducer(state, gameEvent('endTurn'));
7476
expect(state.G.allPassed).toBe(undefined);
77+
expect(state.G.passOrder).toEqual(['0', '1']);
7578

7679
expect(state.ctx.currentPlayer).toBe('2');
7780
state = reducer(state, gameEvent('endTurn'));
@@ -80,10 +83,10 @@ test('passing', () => {
8083
expect(state.ctx.currentPlayer).toBe('2');
8184
state = reducer(state, makeMove('pass'));
8285
expect(state.G.allPassed).toBe(true);
83-
8486
expect(state.ctx.currentPlayer).toBe('2');
8587
state = reducer(state, gameEvent('endTurn'));
8688
expect(state.G.allPassed).toBe(true);
89+
expect(state.G.passOrder).toEqual(['0', '1', '2']);
8790
});
8891

8992
test('end game after everyone passes', () => {
@@ -144,3 +147,23 @@ test('override', () => {
144147
state = flow.processGameEvent(state, { type: 'endTurn' });
145148
expect(state.ctx.currentPlayer).toBe('5');
146149
});
150+
151+
test('custom order', () => {
152+
const game = Game({});
153+
const reducer = createGameReducer({ game, numPlayers: 3 });
154+
155+
let state = reducer(undefined, { type: 'init' });
156+
157+
state.ctx = {
158+
...state.ctx,
159+
currentPlayer: '2',
160+
playOrder: [2, 0, 1],
161+
};
162+
163+
state = reducer(state, gameEvent('endTurn'));
164+
expect(state.ctx.currentPlayer).toBe('0');
165+
state = reducer(state, gameEvent('endTurn'));
166+
expect(state.ctx.currentPlayer).toBe('1');
167+
state = reducer(state, gameEvent('endTurn'));
168+
expect(state.ctx.currentPlayer).toBe('2');
169+
});

src/server/index.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ test('action', async () => {
181181
numPlayers: 2,
182182
phase: 'default',
183183
turn: 0,
184+
playOrder: [0, 1],
185+
playOrderPos: 0,
184186
},
185187
},
186188
],
@@ -192,6 +194,8 @@ test('action', async () => {
192194
numPlayers: 2,
193195
phase: 'default',
194196
turn: 0,
197+
playOrder: [0, 1],
198+
playOrderPos: 0,
195199
},
196200
log: [],
197201
},
@@ -208,6 +212,8 @@ test('action', async () => {
208212
numPlayers: 2,
209213
phase: 'default',
210214
turn: 1,
215+
playOrder: [0, 1],
216+
playOrderPos: 1,
211217
},
212218
},
213219
],
@@ -218,6 +224,8 @@ test('action', async () => {
218224
currentPlayerMoves: 0,
219225
numPlayers: 2,
220226
phase: 'default',
227+
playOrder: [0, 1],
228+
playOrderPos: 1,
221229
turn: 1,
222230
},
223231
log: [{ args: undefined, playerID: undefined, type: 'endTurn' }],

0 commit comments

Comments
 (0)