Skip to content

Commit 7a411c9

Browse files
committed
introduce namespaced moves that are defined within phases
1 parent bb81535 commit 7a411c9

5 files changed

Lines changed: 118 additions & 7 deletions

File tree

src/client/client.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function createDispatchers(
3131
multiplayer
3232
) {
3333
return innerActionNames.reduce((dispatchers, name) => {
34-
dispatchers[name] = function(...args) {
34+
const fn = function(...args) {
3535
let assumedPlayerID = playerID;
3636

3737
// In singleplayer mode, if the client does not have a playerID
@@ -50,6 +50,20 @@ function createDispatchers(
5050
)
5151
);
5252
};
53+
54+
if (name.includes('.')) {
55+
const [namespace, moveName] = name.split('.', 2);
56+
if (!(namespace in dispatchers)) {
57+
dispatchers[namespace] = {};
58+
}
59+
if (innerActionNames.indexOf(namespace) != -1) {
60+
error(`namespace ${namespace} clashes with top-level move`);
61+
}
62+
dispatchers[namespace][moveName] = fn;
63+
} else {
64+
dispatchers[name] = fn;
65+
}
66+
5367
return dispatchers;
5468
}, {});
5569
}

src/client/client.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,64 @@ test('move api', () => {
3535
expect(client.getState().G).toEqual({ arg: 42 });
3636
});
3737

38+
describe('namespaced moves', () => {
39+
let client;
40+
beforeAll(() => {
41+
client = Client({
42+
game: Game({
43+
moves: {
44+
A: () => {},
45+
},
46+
47+
flow: {
48+
phases: {
49+
PA: {
50+
moves: {
51+
B: () => {},
52+
C: () => {},
53+
},
54+
},
55+
},
56+
},
57+
}),
58+
});
59+
});
60+
61+
test('top-level moves', () => {
62+
expect(client.moves.A).toBeInstanceOf(Function);
63+
});
64+
65+
test('phase-level moves', () => {
66+
expect(client.moves.PA.B).toBeInstanceOf(Function);
67+
expect(client.moves.PA.C).toBeInstanceOf(Function);
68+
});
69+
70+
test('name clash', () => {
71+
Client({
72+
game: Game({
73+
moves: {
74+
A: () => {},
75+
},
76+
77+
flow: {
78+
phases: {
79+
A: {
80+
moves: {
81+
B: () => {},
82+
C: () => {},
83+
},
84+
},
85+
},
86+
},
87+
}),
88+
});
89+
90+
expect(error).toHaveBeenCalledWith(
91+
'namespace A clashes with top-level move'
92+
);
93+
});
94+
});
95+
3896
test('isActive', () => {
3997
const client = Client({
4098
game: Game({

src/core/flow.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import * as logging from './logger';
2121
* Helper to create a reducer that manages ctx (with the
2222
* ability to also update G).
2323
*
24-
* You probably want to use FlowWithPhases below, but you might
25-
* need to use this directly if you are creating a very customized
26-
* game flow that it cannot handle.
24+
* This is mostly around for legacy reasons. The original plan
25+
* was to have two flows, one with phases etc. and another
26+
* simpler one like this. The current state is such that this
27+
* is merely an internal function of FlowWithPhases below.
2728
*
2829
* @param {...object} ctx - Function with the signature
2930
* numPlayers => ctx
@@ -67,6 +68,7 @@ export function Flow({
6768
canMakeMove,
6869
canUndoMove,
6970
redactedMoves,
71+
moveMap,
7072
}) {
7173
if (!ctx) ctx = () => ({});
7274
if (!events) events = {};
@@ -103,6 +105,7 @@ export function Flow({
103105
init,
104106
canUndoMove,
105107
redactedMoves,
108+
moveMap,
106109

107110
eventNames: Object.getOwnPropertyNames(events),
108111
enabledEventNames: Object.getOwnPropertyNames(enabledEvents),
@@ -313,9 +316,17 @@ export function FlowWithPhases({
313316

314317
phaseMap['default'] = {};
315318

319+
let moveMap = {};
320+
316321
for (let phase in phaseMap) {
317322
const conf = phaseMap[phase];
318323

324+
if (conf.moves !== undefined) {
325+
for (let move of Object.keys(conf.moves)) {
326+
moveMap[phase + '.' + move] = conf.moves[move];
327+
}
328+
}
329+
319330
if (conf.endPhaseIf === undefined) {
320331
conf.endPhaseIf = () => undefined;
321332
}
@@ -752,5 +763,6 @@ export function FlowWithPhases({
752763
canMakeMove,
753764
canUndoMove,
754765
redactedMoves,
766+
moveMap,
755767
});
756768
}

src/core/game.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,30 @@ function Game(game) {
105105

106106
return {
107107
...game,
108-
moveNames: Object.getOwnPropertyNames(game.moves),
108+
109+
moveNames: [
110+
...Object.getOwnPropertyNames(game.moves),
111+
...Object.keys(game.flow.moveMap || []),
112+
],
113+
109114
processMove: (G, action, ctx) => {
115+
let moveFn = null;
116+
110117
if (game.moves.hasOwnProperty(action.type)) {
118+
moveFn = game.moves[action.type];
119+
}
120+
121+
if (action.type in game.flow.moveMap) {
122+
moveFn = game.flow.moveMap[action.type];
123+
}
124+
125+
if (moveFn !== null) {
111126
const ctxWithPlayerID = { ...ctx, playerID: action.playerID };
112127
const args = [G, ctxWithPlayerID].concat(action.args);
113-
const fn = FnWrap(game.moves[action.type], game);
128+
const fn = FnWrap(moveFn, game);
114129
return fn(...args);
115130
}
131+
116132
return G;
117133
},
118134
};

src/core/game.test.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,20 @@ const game = Game({
1515
A: G => G,
1616
B: () => null,
1717
},
18+
19+
flow: {
20+
phases: {
21+
PA: {
22+
moves: {
23+
A: () => 'PA.A',
24+
},
25+
},
26+
},
27+
},
1828
});
1929

2030
test('basic', () => {
21-
expect(game.moveNames).toEqual(['A', 'B']);
31+
expect(game.moveNames).toEqual(['A', 'B', 'PA.A']);
2232
expect(typeof game.processMove).toEqual('function');
2333
});
2434

@@ -27,6 +37,7 @@ test('processMove', () => {
2737
expect(game.processMove(testObj, { type: 'A' })).toEqual(testObj);
2838
expect(game.processMove(testObj, { type: 'C' })).toEqual(testObj);
2939
expect(game.processMove(testObj, { type: 'B' })).toEqual(null);
40+
expect(game.processMove(testObj, { type: 'PA.A' })).toEqual('PA.A');
3041
});
3142

3243
test('flow override', () => {

0 commit comments

Comments
 (0)