Skip to content

Commit 1877268

Browse files
authored
fix(plugins): More Typescript & pass playerID to Enhance (#598)
* refactor(plugins): More TS in plugins & tighter API typing * fix(plugins): Pass playerID to plugin.api() * refactor(plugins): Convert Events class to Typescript * refactor(plugins): Rename flushRaw to dangerouslyFlushRawState * docs(plugins): Update method signatures & document noClient
1 parent 9046ad1 commit 1877268

11 files changed

Lines changed: 129 additions & 49 deletions

File tree

docs/documentation/plugins.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,36 @@ A plugin is an object that contains the following fields.
1616
// Initialize the plugin's data.
1717
// This is stored in a special area of the state object
1818
// and not exposed to the move functions.
19-
setup: ({ ctx }) => object,
19+
setup: ({ G, ctx, game }) => data object,
2020

2121
// Create an object that becomes available in `ctx`
2222
// under `ctx['plugin-name']`.
2323
// This is called at the beginning of a move or event.
2424
// This object will be held in memory until flush (below)
2525
// is called.
26-
api: ({ G, ctx, data }) => object,
26+
api: ({ G, ctx, game, data, playerID }) => api object,
2727

2828
// Return an updated version of data that is persisted
2929
// in the game's state object.
30-
flush: ({ G, ctx, data, api }) => object,
30+
flush: ({ G, ctx, game, data, api }) => data object,
3131

3232
// Function that accepts a move / trigger function
3333
// and returns another function that wraps it. This
3434
// wrapper can modify G before passing it down to
3535
// the wrapped function. It is a good practice to
3636
// undo the change at the end of the call.
37-
fnWrap: (fn, game) => (G, ctx, ...args) => {
37+
fnWrap: (fn) => (G, ctx, ...args) => {
3838
G = preprocess(G);
3939
G = fn(G, ctx, ...args);
4040
G = postprocess(G);
4141
return G;
4242
},
43+
44+
// Function that allows the plugin to indicate that it
45+
// should not be run on the client. If it returns true,
46+
// the client will discard the state update and wait
47+
// for the master instead.
48+
noClient: ({ G, ctx, game, data, api }) => boolean,
4349
}
4450
```
4551

src/core/game.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ export function Game(
9696
...plugins.EnhanceCtx(state),
9797
playerID: action.playerID,
9898
};
99-
let args = [state.G, ctxWithAPI];
99+
let args = [];
100100
if (action.args !== undefined) {
101101
args = args.concat(action.args);
102102
}
103-
return fn(...args);
103+
return fn(state.G, ctxWithAPI, ...args);
104104
}
105105

106106
logging.error(`invalid move object: ${action.type}`);

src/core/initialize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function InitializeGame({
4242

4343
// Run plugins over initial state.
4444
state = plugins.Setup(state, { game });
45-
state = plugins.Enhance(state as State, { game });
45+
state = plugins.Enhance(state as State, { game, playerID: undefined });
4646

4747
const enhancedCtx = plugins.EnhanceCtx(state);
4848
state.G = game.setup(enhancedCtx, setupData);

src/core/reducer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ export function CreateGameReducer({
103103
}
104104

105105
// Execute plugins.
106-
state = plugins.Enhance(state, { game, isClient: false });
106+
state = plugins.Enhance(state, {
107+
game,
108+
isClient: false,
109+
playerID: action.payload.playerID,
110+
});
107111

108112
// Process event.
109113
let newState = game.flow.processEvent(state, action);
@@ -153,6 +157,7 @@ export function CreateGameReducer({
153157
state = plugins.Enhance(state, {
154158
game,
155159
isClient,
160+
playerID: action.payload.playerID,
156161
});
157162

158163
// Process the move.
Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,41 @@
66
* https://opensource.org/licenses/MIT.
77
*/
88

9+
import { State, Ctx, PlayerID, GameConfig } from '../../types';
910
import { automaticGameEvent } from '../../core/action-creators';
1011

12+
export interface EventsAPI {
13+
endGame?(...args: any[]): void;
14+
endPhase?(...args: any[]): void;
15+
endStage?(...args: any[]): void;
16+
endTurn?(...args: any[]): void;
17+
pass?(...args: any[]): void;
18+
setActivePlayers?(...args: any[]): void;
19+
setPhase?(...args: any[]): void;
20+
setStage?(...args: any[]): void;
21+
}
22+
23+
export interface PrivateEventsAPI {
24+
_obj: {
25+
isUsed(): boolean;
26+
update(state: State): State;
27+
};
28+
}
29+
1130
/**
1231
* Events
1332
*/
1433
export class Events {
15-
constructor(flow, playerID) {
34+
flow: GameConfig['flow'];
35+
playerID: PlayerID | undefined;
36+
dispatch: Array<{
37+
key: string;
38+
args: any[];
39+
phase: string;
40+
turn: number;
41+
}>;
42+
43+
constructor(flow: GameConfig['flow'], playerID?: PlayerID) {
1644
this.flow = flow;
1745
this.playerID = playerID;
1846
this.dispatch = [];
@@ -22,18 +50,18 @@ export class Events {
2250
* Attaches the Events API to ctx.
2351
* @param {object} ctx - The ctx object to attach to.
2452
*/
25-
api(ctx) {
26-
const events = {};
53+
api(ctx: Ctx) {
54+
const events: EventsAPI & PrivateEventsAPI = {
55+
_obj: this,
56+
};
2757
const { phase, turn } = ctx;
2858

2959
for (const key of this.flow.eventNames) {
30-
events[key] = (...args) => {
60+
events[key] = (...args: any[]) => {
3161
this.dispatch.push({ key, args, phase, turn });
3262
};
3363
}
3464

35-
events._obj = this;
36-
3765
return events;
3866
}
3967

@@ -45,7 +73,7 @@ export class Events {
4573
* Updates ctx with the triggered events.
4674
* @param {object} state - The state object { G, ctx }.
4775
*/
48-
update(state) {
76+
update(state: State) {
4977
for (let i = 0; i < this.dispatch.length; i++) {
5078
const item = this.dispatch[i];
5179

src/plugins/main.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import PluginImmer from './plugin-immer';
1010
import PluginRandom from './plugin-random';
1111
import PluginEvents from './plugin-events';
1212
import {
13+
AnyFn,
1314
PartialGameState,
1415
State,
1516
GameConfig,
1617
Plugin,
1718
Ctx,
1819
ActionShape,
20+
PlayerID,
1921
} from '../types';
2022

2123
interface PluginOpts {
@@ -88,8 +90,8 @@ export const EnhanceCtx = (state: PartialGameState): Ctx => {
8890
* @param {function} fn - The move function or trigger to apply the plugins to.
8991
* @param {object} plugins - The list of plugins.
9092
*/
91-
export const FnWrap = (fn: (...args: any[]) => any, plugins: Plugin[]) => {
92-
const reducer = (acc, { fnWrap }) => fnWrap(acc, plugins);
93+
export const FnWrap = (fn: AnyFn, plugins: Plugin[]) => {
94+
const reducer = (acc: AnyFn, { fnWrap }: Plugin) => fnWrap(acc);
9395
return [...DEFAULT_PLUGINS, ...plugins]
9496
.filter(plugin => plugin.fnWrap !== undefined)
9597
.reduce(reducer, fn);
@@ -129,7 +131,10 @@ export const Setup = (
129131
* the `plugins` section of the state (which is subsequently
130132
* merged into ctx).
131133
*/
132-
export const Enhance = (state: State, opts: PluginOpts): State => {
134+
export const Enhance = (
135+
state: State,
136+
opts: PluginOpts & { playerID: PlayerID }
137+
): State => {
133138
[...DEFAULT_PLUGINS, ...opts.game.plugins]
134139
.filter(plugin => plugin.api !== undefined)
135140
.forEach(plugin => {
@@ -141,6 +146,7 @@ export const Enhance = (state: State, opts: PluginOpts): State => {
141146
ctx: state.ctx,
142147
data: pluginState.data,
143148
game: opts.game,
149+
playerID: opts.playerID,
144150
});
145151

146152
state = {
@@ -178,8 +184,8 @@ export const Flush = (state: State, opts: PluginOpts): State => {
178184
[plugin.name]: { data: newData },
179185
},
180186
};
181-
} else if (plugin.flushRaw) {
182-
state = plugin.flushRaw({
187+
} else if (plugin.dangerouslyFlushRawState) {
188+
state = plugin.dangerouslyFlushRawState({
183189
state,
184190
game: opts.game,
185191
api: pluginState.api,

src/plugins/plugin-events.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,25 @@
66
* https://opensource.org/licenses/MIT.
77
*/
88

9-
import { Events } from './events/events';
9+
import { Plugin } from '../types';
10+
import { Events, EventsAPI, PrivateEventsAPI } from './events/events';
1011

11-
export interface EventsAPI {
12-
endGame?(...args: any[]): void;
13-
endPhase?(...args: any[]): void;
14-
endStage?(...args: any[]): void;
15-
endTurn?(...args: any[]): void;
16-
pass?(...args: any[]): void;
17-
setActivePlayers?(...args: any[]): void;
18-
setPhase?(...args: any[]): void;
19-
setStage?(...args: any[]): void;
20-
}
12+
export { EventsAPI };
2113

22-
export default {
14+
const EventsPlugin: Plugin<EventsAPI & PrivateEventsAPI> = {
2315
name: 'events',
2416

2517
noClient: ({ api }) => {
2618
return api._obj.isUsed();
2719
},
2820

29-
flushRaw: ({ state, api }) => {
21+
dangerouslyFlushRawState: ({ state, api }) => {
3022
return api._obj.update(state);
3123
},
3224

3325
api: ({ game, playerID, ctx }) => {
3426
return new Events(game.flow, playerID).api(ctx);
3527
},
3628
};
29+
30+
export default EventsPlugin;
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
*/
88

99
import produce from 'immer';
10+
import { Plugin } from '../types';
1011

1112
/**
1213
* Plugin that allows using Immer to make immutable changes
1314
* to G by just mutating it.
1415
*/
15-
export default {
16+
const ImmerPlugin: Plugin = {
1617
name: 'plugin-immer',
1718
fnWrap: move => produce(move),
1819
};
20+
21+
export default ImmerPlugin;

src/plugins/plugin-player.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
* https://opensource.org/licenses/MIT.
77
*/
88

9+
import { Plugin, PlayerID } from '../types';
10+
11+
interface PlayerData {
12+
players: Record<PlayerID, any>;
13+
}
14+
915
export interface PlayerAPI {
10-
state: {
11-
[playerId: string]: object;
12-
};
16+
state: Record<PlayerID, any>;
1317
get(): any;
1418
set(value: any): any;
1519
opponent?: {
@@ -25,7 +29,7 @@ export interface PlayerAPI {
2529
*
2630
* @param {function} initPlayerState - Function of type (playerID) => playerState.
2731
*/
28-
export default {
32+
const PlayerPlugin: Plugin<PlayerAPI, PlayerData> = {
2933
name: 'player',
3034

3135
flush: ({ api }) => {
@@ -60,9 +64,9 @@ export default {
6064
},
6165

6266
setup: ({ ctx, game }) => {
63-
let players = {};
67+
let players: Record<PlayerID, any> = {};
6468
for (let i = 0; i < ctx.numPlayers; i++) {
65-
let playerState = {};
69+
let playerState: any = {};
6670
if (game.playerSetup !== undefined) {
6771
playerState = game.playerSetup(i + '');
6872
}
@@ -71,3 +75,5 @@ export default {
7175
return { players };
7276
},
7377
};
78+
79+
export default PlayerPlugin;

src/plugins/plugin-random.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* https://opensource.org/licenses/MIT.
77
*/
88

9+
import { Plugin } from '../types';
910
import { Random } from './random/random';
1011

1112
export interface RandomAPI {
@@ -25,7 +26,14 @@ export interface RandomAPI {
2526
Shuffle<T>(deck: T[]): T[];
2627
}
2728

28-
export default {
29+
interface PrivateRandomAPI {
30+
_obj: {
31+
isUsed(): boolean;
32+
getState(): any;
33+
};
34+
}
35+
36+
const RandomPlugin: Plugin<RandomAPI & PrivateRandomAPI> = {
2937
name: 'random',
3038

3139
noClient: ({ api }) => {
@@ -49,3 +57,5 @@ export default {
4957
return { seed };
5058
},
5159
};
60+
61+
export default RandomPlugin;

0 commit comments

Comments
 (0)