, WrappedBoardDelegates> & {
board?: React.ComponentType;
loading?: React.ComponentType;
};
@@ -71,8 +70,8 @@ type ReactClientOpts<
export function Client<
G extends any = any,
P extends BoardProps = BoardProps,
- ContextWithPlugins extends Ctx = Ctx
->(opts: ReactClientOpts) {
+ PluginAPIs extends Record = Record
+>(opts: ReactClientOpts) {
let { game, numPlayers, loading, board, multiplayer, enhancer, debug } = opts;
// Component that is displayed before the client has synced
diff --git a/src/client/transport/local.test.ts b/src/client/transport/local.test.ts
index 0fccf2aaf..ff10ed769 100644
--- a/src/client/transport/local.test.ts
+++ b/src/client/transport/local.test.ts
@@ -19,8 +19,8 @@ const sleep = (ms = 500) => new Promise((resolve) => setTimeout(resolve, ms));
describe('bots', () => {
const game: Game = {
moves: {
- A: (_, ctx) => {
- ctx.events.endTurn();
+ A: ({ events }) => {
+ events.endTurn();
},
},
ai: {
@@ -176,10 +176,10 @@ describe('Local', () => {
});
describe('with localStorage persistence', () => {
- const game = {
+ const game: Game = {
setup: () => ({ count: 0 }),
moves: {
- A: (G: any) => {
+ A: ({ G }) => {
G.count++;
},
},
diff --git a/src/core/flow.test.ts b/src/core/flow.test.ts
index 53769e008..ce0f5f40d 100644
--- a/src/core/flow.test.ts
+++ b/src/core/flow.test.ts
@@ -32,24 +32,24 @@ describe('phases', () => {
phases: {
A: {
start: true,
- onBegin: (s) => ({ ...s, setupA: true }),
- onEnd: (s) => ({ ...s, cleanupA: true }),
+ onBegin: ({ G }) => ({ ...G, setupA: true }),
+ onEnd: ({ G }) => ({ ...G, cleanupA: true }),
next: 'B',
},
B: {
- onBegin: (s) => ({ ...s, setupB: true }),
- onEnd: (s) => ({ ...s, cleanupB: true }),
+ onBegin: ({ G }) => ({ ...G, setupB: true }),
+ onEnd: ({ G }) => ({ ...G, cleanupB: true }),
next: 'A',
},
},
turn: {
order: {
- first: (G) => {
+ first: ({ G }) => {
if (G.setupB && !G.cleanupB) return 1;
return 0;
},
- next: (_, ctx: Ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
+ next: ({ ctx }) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
},
},
});
@@ -104,9 +104,9 @@ describe('phases', () => {
let client: ReturnType;
beforeAll(() => {
- const game = {
+ const game: Game = {
endIf: () => true,
- onEnd: (G) => {
+ onEnd: ({ G }) => {
G.onEnd = true;
},
};
@@ -205,7 +205,7 @@ describe('phases', () => {
describe('turn', () => {
test('onEnd', () => {
- const onEnd = jest.fn((G) => G);
+ const onEnd = jest.fn(({ G }) => G);
const flow = Flow({
turn: { onEnd },
});
@@ -243,7 +243,7 @@ describe('turn', () => {
test('ctx with playerID', () => {
const playerID = 'playerID';
const flow = Flow({
- turn: { onMove: (G, ctx) => ({ playerID: ctx.playerID }) },
+ turn: { onMove: ({ playerID }) => ({ playerID }) },
});
let state = { G: {}, ctx: flow.ctx(2) } as State;
state = flow.processMove(
@@ -496,12 +496,12 @@ describe('turn', () => {
describe('endIf', () => {
test('global', () => {
- const game = {
+ const game: Game = {
moves: {
A: () => ({ endTurn: true }),
- B: (G) => G,
+ B: ({ G }) => G,
},
- turn: { endIf: (G) => G.endTurn },
+ turn: { endIf: ({ G }) => G.endTurn },
};
const client = Client({ game });
@@ -513,13 +513,13 @@ describe('turn', () => {
});
test('phase specific', () => {
- const game = {
+ const game: Game = {
moves: {
A: () => ({ endTurn: true }),
- B: (G) => G,
+ B: ({ G }) => G,
},
phases: {
- A: { start: true, turn: { endIf: (G) => G.endTurn } },
+ A: { start: true, turn: { endIf: ({ G }) => G.endTurn } },
},
};
const client = Client({ game });
@@ -532,9 +532,9 @@ describe('turn', () => {
});
test('return value', () => {
- const game = {
+ const game: Game = {
moves: {
- A: (G) => G,
+ A: ({ G }) => G,
},
turn: { endIf: () => ({ next: '2' }) },
};
@@ -550,7 +550,7 @@ describe('turn', () => {
const flow = Flow({
turn: { endIf: () => true },
phases: {
- A: { start: true, endIf: (G) => G.endPhase, next: 'B' },
+ A: { start: true, endIf: ({ G }) => G.endPhase, next: 'B' },
B: {},
},
});
@@ -584,7 +584,7 @@ describe('stages', () => {
const A = () => {};
const B = () => {};
- const game = {
+ const game: Game = {
moves: { A },
turn: {
stages: {
@@ -655,11 +655,11 @@ describe('stages', () => {
stages: {
A: {
moves: {
- leaveStage: (G, ctx) => void ctx.events.endStage(),
+ leaveStage: ({ events }) => void events.endStage(),
},
},
},
- endIf: (G, ctx) => ctx.activePlayers === null,
+ endIf: ({ ctx }) => ctx.activePlayers === null,
},
},
});
@@ -690,7 +690,7 @@ describe('stages', () => {
currentPlayer: 'A',
maxMoves: 1,
},
- endIf: (G, ctx) => ctx.activePlayers === null,
+ endIf: ({ ctx }) => ctx.activePlayers === null,
stages: {
A: {
moves: {
@@ -813,8 +813,8 @@ describe('stage events', () => {
});
describe('disallowed in hooks', () => {
- const setStage: MoveFn = (G, ctx) => {
- ctx.events.setStage('A');
+ const setStage: MoveFn = ({ events }) => {
+ events.setStage('A');
};
test('phase.onBegin', () => {
@@ -990,8 +990,8 @@ describe('stage events', () => {
});
describe('disallowed in hooks', () => {
- const endStage: MoveFn = (G, ctx) => {
- ctx.events.endStage();
+ const endStage: MoveFn = ({ events }) => {
+ events.endStage();
};
test('phase.onBegin', () => {
@@ -1064,13 +1064,13 @@ describe('stage events', () => {
numPlayers: 3,
game: {
turn: {
- onBegin: (G, ctx) => {
- ctx.events.setActivePlayers({ currentPlayer: 'A' });
+ onBegin: ({ events }) => {
+ events.setActivePlayers({ currentPlayer: 'A' });
},
},
moves: {
- updateActivePlayers: (G, ctx) => {
- ctx.events.setActivePlayers({ others: 'B' });
+ updateActivePlayers: ({ events }) => {
+ events.setActivePlayers({ others: 'B' });
},
},
},
@@ -1084,8 +1084,8 @@ describe('stage events', () => {
});
describe('in hooks', () => {
- const setActivePlayers: MoveFn = (G, ctx) => {
- ctx.events.setActivePlayers({ currentPlayer: 'A' });
+ const setActivePlayers: MoveFn = ({ events }) => {
+ events.setActivePlayers({ currentPlayer: 'A' });
};
test('disallowed in phase.onBegin', () => {
@@ -1186,7 +1186,7 @@ test('next', () => {
describe('endIf', () => {
test('basic', () => {
- const flow = Flow({ endIf: (G) => G.win });
+ const flow = Flow({ endIf: ({ G }) => G.win });
let state = flow.init({ G: {}, ctx: flow.ctx(2) } as State);
state = flow.processEvent(state, gameEvent('endTurn'));
@@ -1206,17 +1206,17 @@ describe('endIf', () => {
});
test('phase automatically ends', () => {
- const game = {
+ const game: Game = {
phases: {
A: {
start: true,
moves: {
A: () => ({ win: 'A' }),
- B: (G) => G,
+ B: ({ G }) => G,
},
},
},
- endIf: (G) => G.win,
+ endIf: ({ G }) => G.win,
};
const client = Client({ game });
@@ -1268,7 +1268,7 @@ test('isPlayerActive', () => {
describe('endGame', () => {
let client: ReturnType;
beforeEach(() => {
- const game = {
+ const game: Game = {
events: { endGame: true },
};
client = Client({ game });
@@ -1382,11 +1382,11 @@ describe('pass args', () => {
});
test('undoable moves', () => {
- const game = {
+ const game: Game = {
moves: {
A: {
move: () => ({ A: true }),
- undoable: (G, ctx) => {
+ undoable: ({ ctx }) => {
return ctx.phase == 'A';
},
},
@@ -1437,7 +1437,7 @@ test('undoable moves', () => {
});
describe('moveMap', () => {
- const game = {
+ const game: Game = {
moves: { A: () => {} },
turn: {
@@ -1478,7 +1478,7 @@ describe('moveMap', () => {
describe('infinite loops', () => {
test('infinite loop of self-ending phases via endIf', () => {
const endIf = () => true;
- const game = {
+ const game: Game = {
phases: {
A: { endIf, next: 'B', start: true },
B: { endIf, next: 'A' },
@@ -1489,15 +1489,15 @@ describe('infinite loops', () => {
});
test('infinite endPhase loop from phase.onBegin', () => {
- const onBegin = (G, ctx) => ctx.events.endPhase();
- const game = {
+ const onBegin = ({ events }) => void events.endPhase();
+ const game: Game = {
phases: {
A: {
onBegin,
next: 'B',
start: true,
moves: {
- a: (G, ctx) => void ctx.events.endPhase(),
+ a: ({ events }) => void events.endPhase(),
},
},
B: { onBegin, next: 'C' },
@@ -1532,9 +1532,9 @@ describe('infinite loops', () => {
});
test('double phase ending from client event and turn.onEnd', () => {
- const game = {
+ const game: Game = {
turn: {
- onEnd: (G, ctx) => void ctx.events.endPhase(),
+ onEnd: ({ events }) => void events.endPhase(),
},
phases: {
A: { next: 'B', start: true },
@@ -1555,14 +1555,14 @@ describe('infinite loops', () => {
});
test('infinite turn endings from turn.onBegin', () => {
- const game = {
+ const game: Game = {
moves: {
- endTurn: (G, ctx) => {
- ctx.events.endTurn();
+ endTurn: ({ events }) => {
+ events.endTurn();
},
},
turn: {
- onBegin: (G, ctx) => void ctx.events.endTurn(),
+ onBegin: ({ events }) => void events.endTurn(),
},
};
const client = Client({ game });
@@ -1582,10 +1582,10 @@ describe('infinite loops', () => {
});
test('double turn ending from event and endIf', () => {
- const game = {
+ const game: Game = {
moves: {
- endTurn: (G, ctx) => {
- ctx.events.endTurn();
+ endTurn: ({ events }) => {
+ events.endTurn();
},
},
turn: {
@@ -1610,8 +1610,8 @@ describe('infinite loops', () => {
const game: Game = {
phases: {
A: {
- endIf: (G, ctx) => {
- ctx.events.setActivePlayers({ currentPlayer: 'A' });
+ endIf: ({ events }) => {
+ events.setActivePlayers({ currentPlayer: 'A' });
},
},
},
@@ -1633,10 +1633,10 @@ describe('events in hooks', () => {
};
describe('endTurn', () => {
- const conditionalEndTurn = (G, ctx) => {
+ const conditionalEndTurn = ({ G, events }) => {
if (!G.shouldEnd) return;
G.shouldEnd = false;
- ctx.events.endTurn();
+ events.endTurn();
};
test('can end turn from turn.onBegin', () => {
@@ -1761,10 +1761,10 @@ describe('events in hooks', () => {
});
describe('endPhase', () => {
- const conditionalEndPhase = (G, ctx) => {
+ const conditionalEndPhase = ({ G, events }) => {
if (!G.shouldEnd) return;
G.shouldEnd = false;
- ctx.events.endPhase();
+ events.endPhase();
};
test('can end phase from turn.onBegin', () => {
@@ -1883,7 +1883,7 @@ describe('events in hooks', () => {
describe('activePlayers', () => {
test('sets activePlayers at each turn', () => {
- const game = {
+ const game: Game = {
turn: {
stages: { A: {}, B: {} },
activePlayers: {
@@ -1914,15 +1914,15 @@ describe('activePlayers', () => {
});
test('events in hooks triggered by moves should be processed', () => {
- const game = {
+ const game: Game = {
turn: {
- onBegin: (G, ctx) => {
- ctx.events.setActivePlayers({ currentPlayer: 'A' });
+ onBegin: ({ events }) => {
+ events.setActivePlayers({ currentPlayer: 'A' });
},
},
moves: {
- endTurn: (G, ctx) => {
- ctx.events.endTurn();
+ endTurn: ({ events }) => {
+ events.endTurn();
},
},
};
@@ -1943,7 +1943,7 @@ test('events in hooks triggered by moves should be processed', () => {
});
test('stage events should not be processed out of turn', () => {
- const game = {
+ const game: Game = {
phases: {
A: {
start: true,
@@ -1954,15 +1954,15 @@ test('stage events should not be processed out of turn', () => {
stages: {
A1: {
moves: {
- endStage: (G, ctx) => {
+ endStage: ({ G, events }) => {
G.endStage = true;
- ctx.events.endStage();
+ events.endStage();
},
},
},
},
},
- endIf: (G) => G.endStage,
+ endIf: ({ G }) => G.endStage,
next: 'B',
},
B: {
@@ -2070,16 +2070,16 @@ describe('hook execution order', () => {
game: {
moves: {
move: () => void calls.push('move'),
- setStage: (G, ctx) => {
- ctx.events.setStage('A');
+ setStage: ({ events }) => {
+ events.setStage('A');
calls.push('moves.setStage');
},
- endStage: (G, ctx) => {
- ctx.events.endStage();
+ endStage: ({ events }) => {
+ events.endStage();
calls.push('moves.endStage');
},
- setActivePlayers: (G, ctx) => {
- ctx.events.setActivePlayers({ all: 'A', minMoves: 1, maxMoves: 1 });
+ setActivePlayers: ({ events }) => {
+ events.setActivePlayers({ all: 'A', minMoves: 1, maxMoves: 1 });
calls.push('moves.setActivePlayers');
},
},
diff --git a/src/core/flow.ts b/src/core/flow.ts
index f104d4698..02cfd279c 100644
--- a/src/core/flow.ts
+++ b/src/core/flow.ts
@@ -23,6 +23,7 @@ import type {
ActivePlayersArg,
State,
Ctx,
+ FnContext,
LogEntry,
Game,
PhaseConfig,
@@ -61,7 +62,7 @@ export function Flow({
}
if (!endIf) endIf = () => undefined;
- if (!onEnd) onEnd = (G) => G;
+ if (!onEnd) onEnd = ({ G }) => G;
if (!turn) turn = {};
const phaseMap = { ...phases };
@@ -79,20 +80,29 @@ export function Flow({
Object.keys(moves).forEach((name) => moveNames.add(name));
const HookWrapper = (
- hook: (G: any, ctx: Ctx) => any,
+ hook: (context: FnContext) => any,
hookType: GameMethod
) => {
const withPlugins = plugin.FnWrap(hook, hookType, plugins);
- return (state: State) => {
- const ctxWithAPI = plugin.EnhanceCtx(state);
- return withPlugins(state.G, ctxWithAPI);
+ return (state: State & { playerID?: PlayerID }) => {
+ const pluginAPIs = plugin.GetAPIs(state);
+ return withPlugins({
+ ...pluginAPIs,
+ G: state.G,
+ ctx: state.ctx,
+ playerID: state.playerID,
+ });
};
};
- const TriggerWrapper = (trigger: (G: any, ctx: Ctx) => any) => {
+ const TriggerWrapper = (trigger: (context: FnContext) => any) => {
return (state: State) => {
- const ctxWithAPI = plugin.EnhanceCtx(state);
- return trigger(state.G, ctxWithAPI);
+ const pluginAPIs = plugin.GetAPIs(state);
+ return trigger({
+ ...pluginAPIs,
+ G: state.G,
+ ctx: state.ctx,
+ });
};
};
@@ -119,10 +129,10 @@ export function Flow({
phaseConfig.endIf = () => undefined;
}
if (phaseConfig.onBegin === undefined) {
- phaseConfig.onBegin = (G) => G;
+ phaseConfig.onBegin = ({ G }) => G;
}
if (phaseConfig.onEnd === undefined) {
- phaseConfig.onEnd = (G) => G;
+ phaseConfig.onEnd = ({ G }) => G;
}
if (phaseConfig.turn === undefined) {
phaseConfig.turn = turn;
@@ -131,16 +141,16 @@ export function Flow({
phaseConfig.turn.order = TurnOrder.DEFAULT;
}
if (phaseConfig.turn.onBegin === undefined) {
- phaseConfig.turn.onBegin = (G) => G;
+ phaseConfig.turn.onBegin = ({ G }) => G;
}
if (phaseConfig.turn.onEnd === undefined) {
- phaseConfig.turn.onEnd = (G) => G;
+ phaseConfig.turn.onEnd = ({ G }) => G;
}
if (phaseConfig.turn.endIf === undefined) {
phaseConfig.turn.endIf = () => false;
}
if (phaseConfig.turn.onMove === undefined) {
- phaseConfig.turn.onMove = (G) => G;
+ phaseConfig.turn.onMove = ({ G }) => G;
}
if (phaseConfig.turn.stages === undefined) {
phaseConfig.turn.stages = {};
@@ -183,11 +193,22 @@ export function Flow({
return ctx.phase ? phaseMap[ctx.phase] : phaseMap[''];
}
- function OnMove(s) {
- return s;
+ function OnMove(state: State) {
+ return state;
}
- function Process(state: State, events): State {
+ function Process(
+ state: State,
+ events: {
+ fn: (state: State, opts: any) => State;
+ arg?: any;
+ turn?: Ctx['turn'];
+ phase?: Ctx['phase'];
+ automatic?: boolean;
+ playerID?: PlayerID;
+ force?: boolean;
+ }[]
+ ): State {
const phasesEnded = new Set();
const turnsEnded = new Set();
@@ -712,11 +733,7 @@ export function Flow({
state = {
...state,
- ctx: {
- ...ctx,
- numMoves,
- _activePlayersNumMoves,
- },
+ ctx: { ...ctx, numMoves, _activePlayersNumMoves },
};
if (
@@ -727,10 +744,7 @@ export function Flow({
}
const phaseConfig = GetPhase(ctx);
- const G = phaseConfig.turn.wrapped.onMove({
- ...state,
- ctx: { ...ctx, playerID },
- });
+ const G = phaseConfig.turn.wrapped.onMove({ ...state, playerID });
state = { ...state, G };
const events = [{ fn: OnMove }];
diff --git a/src/core/game.test.ts b/src/core/game.test.ts
index 99d1b3a90..7aa9a9dcb 100644
--- a/src/core/game.test.ts
+++ b/src/core/game.test.ts
@@ -22,7 +22,7 @@ describe('basic', () => {
beforeAll(() => {
game = ProcessGameConfig({
moves: {
- A: (G) => G,
+ A: ({ G }) => G,
B: () => null,
C: {
move: () => 'C',
@@ -66,11 +66,11 @@ describe('basic', () => {
// Following turn order is often used in worker placement games like Agricola and Viticulture.
test('rounds with starting player token', () => {
- const game = {
+ const game: Game = {
setup: () => ({ startingPlayerToken: 0 }),
moves: {
- takeStartingPlayerToken: (G, ctx) => {
+ takeStartingPlayerToken: ({ G, ctx }) => {
G.startingPlayerToken = ctx.currentPlayer;
},
},
@@ -80,8 +80,8 @@ test('rounds with starting player token', () => {
start: true,
turn: {
order: {
- first: (G) => G.startingPlayerToken,
- next: (G, ctx) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
+ first: ({ G }) => G.startingPlayerToken,
+ next: ({ ctx }) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
},
},
},
@@ -115,14 +115,14 @@ test('rounds with starting player token', () => {
// The following pattern is used in Catan, Twilight Imperium, and (sort of) Powergrid.
test('serpentine setup phases', () => {
- const game = {
+ const game: Game = {
phases: {
'first setup round': {
start: true,
turn: {
order: {
first: () => 0,
- next: (G, ctx) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
+ next: ({ ctx }) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
},
},
next: 'second setup round',
@@ -130,8 +130,8 @@ test('serpentine setup phases', () => {
'second setup round': {
turn: {
order: {
- first: (G, ctx) => ctx.playOrder.length - 1,
- next: (G, ctx) => (+ctx.playOrderPos - 1) % ctx.playOrder.length,
+ first: ({ ctx }) => ctx.playOrder.length - 1,
+ next: ({ ctx }) => (+ctx.playOrderPos - 1) % ctx.playOrder.length,
},
},
next: 'main phase',
@@ -140,7 +140,7 @@ test('serpentine setup phases', () => {
turn: {
order: {
first: () => 0,
- next: (G, ctx) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
+ next: ({ ctx }) => (+ctx.playOrderPos + 1) % ctx.playOrder.length,
},
},
},
diff --git a/src/core/game.ts b/src/core/game.ts
index c55bd4cbf..3429a8631 100644
--- a/src/core/game.ts
+++ b/src/core/game.ts
@@ -53,7 +53,7 @@ export function ProcessGameConfig(game: Game | ProcessedGame): ProcessedGame {
if (game.disableUndo === undefined) game.disableUndo = false;
if (game.setup === undefined) game.setup = () => ({});
if (game.moves === undefined) game.moves = {};
- if (game.playerView === undefined) game.playerView = (G) => G;
+ if (game.playerView === undefined) game.playerView = ({ G }) => G;
if (game.plugins === undefined) game.plugins = [];
game.plugins.forEach((plugin) => {
@@ -89,15 +89,17 @@ export function ProcessGameConfig(game: Game | ProcessedGame): ProcessedGame {
if (moveFn instanceof Function) {
const fn = plugins.FnWrap(moveFn, GameMethod.MOVE, game.plugins);
- const ctxWithAPI = {
- ...plugins.EnhanceCtx(state),
- playerID: action.playerID,
- };
let args = [];
if (action.args !== undefined) {
args = Array.isArray(action.args) ? action.args : [action.args];
}
- return fn(state.G, ctxWithAPI, ...args);
+ const context = {
+ ...plugins.GetAPIs(state),
+ G: state.G,
+ ctx: state.ctx,
+ playerID: action.playerID,
+ };
+ return fn(context, ...args);
}
logging.error(`invalid move object: ${action.type}`);
diff --git a/src/core/initialize.ts b/src/core/initialize.ts
index c25708f8e..f7d93beb7 100644
--- a/src/core/initialize.ts
+++ b/src/core/initialize.ts
@@ -41,10 +41,10 @@ export function InitializeGame({
// Run plugins over initial state.
state = plugins.Setup(state, { game });
- state = plugins.Enhance(state as State, { game, playerID: undefined });
+ state = plugins.Enhance(state, { game, playerID: undefined });
- const enhancedCtx = plugins.EnhanceCtx(state);
- state.G = game.setup(enhancedCtx, setupData);
+ const pluginAPIs = plugins.GetAPIs(state);
+ state.G = game.setup({ ...pluginAPIs, ctx: state.ctx }, setupData);
let initial: State = {
...state,
diff --git a/src/core/player-view.test.ts b/src/core/player-view.test.ts
index 959fd32d6..4d97e5057 100644
--- a/src/core/player-view.test.ts
+++ b/src/core/player-view.test.ts
@@ -11,13 +11,13 @@ import type { Ctx } from '../types';
test('no change', () => {
const G = { test: true };
- const newG = PlayerView.STRIP_SECRETS(G, {} as Ctx, '0');
+ const newG = PlayerView.STRIP_SECRETS({ G, ctx: {} as Ctx, playerID: '0' });
expect(newG).toEqual(G);
});
test('secret', () => {
const G = { secret: true };
- const newG = PlayerView.STRIP_SECRETS(G, {} as Ctx, '0');
+ const newG = PlayerView.STRIP_SECRETS({ G, ctx: {} as Ctx, playerID: '0' });
expect(newG).toEqual({});
});
@@ -30,17 +30,21 @@ describe('players', () => {
};
test('playerID: "0"', () => {
- const newG = PlayerView.STRIP_SECRETS(G, {} as Ctx, '0');
+ const newG = PlayerView.STRIP_SECRETS({ G, ctx: {} as Ctx, playerID: '0' });
expect(newG.players).toEqual({ '0': {} });
});
test('playerID: "1"', () => {
- const newG = PlayerView.STRIP_SECRETS(G, {} as Ctx, '1');
+ const newG = PlayerView.STRIP_SECRETS({ G, ctx: {} as Ctx, playerID: '1' });
expect(newG.players).toEqual({ '1': {} });
});
test('playerID: null', () => {
- const newG = PlayerView.STRIP_SECRETS(G, {} as Ctx, null);
+ const newG = PlayerView.STRIP_SECRETS({
+ G,
+ ctx: {} as Ctx,
+ playerID: null,
+ });
expect(newG.players).toEqual({});
});
});
diff --git a/src/core/player-view.ts b/src/core/player-view.ts
index 6d8f8d7f5..006deea71 100644
--- a/src/core/player-view.ts
+++ b/src/core/player-view.ts
@@ -6,12 +6,12 @@
* https://opensource.org/licenses/MIT.
*/
-import type { Ctx, PlayerID } from '../types';
+import type { Game, PlayerID } from '../types';
/**
* PlayerView reducers.
*/
-export const PlayerView = {
+export const PlayerView: { STRIP_SECRETS: Game['playerView'] } = {
/**
* STRIP_SECRETS
*
@@ -19,7 +19,7 @@ export const PlayerView = {
* removes all the keys in `players`, except for the one
* corresponding to the current playerID.
*/
- STRIP_SECRETS: (G: any, ctx: Ctx, playerID: PlayerID | null) => {
+ STRIP_SECRETS: ({ G, playerID }: { G: any; playerID: PlayerID | null }) => {
const r = { ...G };
if (r.secret !== undefined) {
diff --git a/src/core/reducer.test.ts b/src/core/reducer.test.ts
index 67358ab0d..027a67fe7 100644
--- a/src/core/reducer.test.ts
+++ b/src/core/reducer.test.ts
@@ -21,7 +21,7 @@ import {
patch,
} from './action-creators';
import { error } from '../core/logger';
-import type { Ctx, Game, State, SyncInfo } from '../types';
+import type { Game, State, SyncInfo } from '../types';
jest.mock('../core/logger', () => ({
info: jest.fn(),
@@ -30,12 +30,12 @@ jest.mock('../core/logger', () => ({
const game: Game = {
moves: {
- A: (G) => G,
+ A: ({ G }) => G,
B: () => ({ moved: true }),
C: () => ({ victory: true }),
Invalid: () => INVALID_MOVE,
},
- endIf: (G, ctx) => (G.victory ? ctx.currentPlayer : undefined),
+ endIf: ({ G, ctx }) => (G.victory ? ctx.currentPlayer : undefined),
};
const reducer = CreateGameReducer({ game });
const initialState = InitializeGame({ game });
@@ -49,7 +49,7 @@ test('_stateID is incremented', () => {
});
test('move returns INVALID_MOVE', () => {
- const game = {
+ const game: Game = {
moves: {
A: () => INVALID_MOVE,
},
@@ -179,7 +179,7 @@ test('endTurn', () => {
test('light client when multiplayer=true', () => {
const game: Game = {
moves: { A: () => ({ win: true }) },
- endIf: (G) => G.win,
+ endIf: ({ G }) => G.win,
};
{
@@ -200,7 +200,7 @@ test('light client when multiplayer=true', () => {
});
test('disable optimistic updates', () => {
- const game = {
+ const game: Game = {
moves: {
A: {
move: () => ({ A: true }),
@@ -269,7 +269,7 @@ test('deltalog', () => {
});
describe('Events API', () => {
- const fn = (G: any, ctx: Ctx) => (ctx.events ? {} : { error: true });
+ const fn = ({ events }) => (events ? {} : { error: true });
const game: Game = {
setup: () => ({}),
@@ -314,7 +314,7 @@ describe('Plugin Invalid Action API', () => {
},
],
moves: {
- setValue: (G, _ctx, arg) => {
+ setValue: ({ G }, arg: number) => {
G.value = arg;
},
},
@@ -419,9 +419,9 @@ describe('undo / redo', () => {
const game: Game = {
seed: 0,
moves: {
- move: (G, ctx, arg) => ({ ...G, [arg]: true }),
- roll: (G, ctx) => {
- G.roll = ctx.random.D6();
+ move: ({ G }, arg: string) => ({ ...G, [arg]: true }),
+ roll: ({ G, random }) => {
+ G.roll = random.D6();
},
},
turn: {
@@ -439,12 +439,13 @@ describe('undo / redo', () => {
const initialState = InitializeGame({ game });
+ // TODO: Check if this test is still actually required after removal of APIs from ctx
test('plugin APIs are not included in undo state', () => {
let state = reducer(initialState, makeMove('move', 'A', '0'));
state = reducer(state, makeMove('move', 'B', '0'));
expect(state.G).toMatchObject({ A: true, B: true });
- expect(state._undo[1].ctx.events).toBeUndefined();
- expect(state._undo[1].ctx.random).toBeUndefined();
+ expect(state._undo[1].ctx).not.toHaveProperty('events');
+ expect(state._undo[1].ctx).not.toHaveProperty('random');
});
test('undo restores previous state after move', () => {
@@ -546,7 +547,7 @@ test('disable undo / redo', () => {
seed: 0,
disableUndo: true,
moves: {
- move: (G, ctx, arg) => ({ ...G, [arg]: true }),
+ move: ({ G }, arg: string) => ({ ...G, [arg]: true }),
},
};
@@ -584,8 +585,8 @@ describe('undo stack', () => {
const game: Game = {
moves: {
basic: () => {},
- endTurn: (_, ctx) => {
- ctx.events.endTurn();
+ endTurn: ({ events }) => {
+ events.endTurn();
},
},
};
@@ -662,8 +663,8 @@ describe('redo stack', () => {
const game: Game = {
moves: {
basic: () => {},
- endTurn: (_, ctx) => {
- ctx.events.endTurn();
+ endTurn: ({ events }) => {
+ events.endTurn();
},
},
};
@@ -731,19 +732,19 @@ describe('undo / redo with stages', () => {
start: {
moves: {
moveA: {
- move: (G, ctx, moveAisReversible) => {
- ctx.events.setStage('A');
+ move: ({ G, events }, moveAisReversible) => {
+ events.setStage('A');
return { ...G, moveAisReversible, A: true };
},
- undoable: (G) => G.moveAisReversible > 0,
+ undoable: ({ G }) => G.moveAisReversible > 0,
},
},
},
A: {
moves: {
moveB: {
- move: (G, ctx) => {
- ctx.events.setStage('B');
+ move: ({ G, events }) => {
+ events.setStage('B');
return { ...G, B: true };
},
undoable: false,
@@ -753,8 +754,8 @@ describe('undo / redo with stages', () => {
B: {
moves: {
moveC: {
- move: (G, ctx) => {
- ctx.events.setStage('C');
+ move: ({ G, events }) => {
+ events.setStage('C');
return { ...G, C: true };
},
undoable: true,
diff --git a/src/core/reducer.ts b/src/core/reducer.ts
index 2648ad77d..b8cf544bc 100644
--- a/src/core/reducer.ts
+++ b/src/core/reducer.ts
@@ -60,7 +60,7 @@ const CanUndoMove = (G: any, ctx: Ctx, move: Move): boolean => {
}
if (IsFunction(move.undoable)) {
- return move.undoable(G, ctx);
+ return move.undoable({ G, ctx });
}
return move.undoable;
diff --git a/src/core/turn-order.test.ts b/src/core/turn-order.test.ts
index d27b37f5e..ef47655f4 100644
--- a/src/core/turn-order.test.ts
+++ b/src/core/turn-order.test.ts
@@ -18,7 +18,7 @@ import { makeMove, gameEvent } from './action-creators';
import { CreateGameReducer } from './reducer';
import { InitializeGame } from './initialize';
import { error } from '../core/logger';
-import type { State } from '../types';
+import type { Game, State } from '../types';
jest.mock('../core/logger', () => ({
info: jest.fn(),
@@ -311,12 +311,12 @@ describe('turn orders', () => {
test('override', () => {
const even = {
first: () => 0,
- next: (G, ctx) => (+ctx.currentPlayer + 2) % ctx.numPlayers,
+ next: ({ ctx }) => (+ctx.currentPlayer + 2) % ctx.numPlayers,
};
const odd = {
first: () => 1,
- next: (G, ctx) => (+ctx.currentPlayer + 2) % ctx.numPlayers,
+ next: ({ ctx }) => (+ctx.currentPlayer + 2) % ctx.numPlayers,
};
const flow = Flow({
@@ -343,7 +343,7 @@ test('override', () => {
});
test('playOrder', () => {
- const game = {};
+ const game: Game = {};
const reducer = CreateGameReducer({ game });
let state = InitializeGame({ game, numPlayers: 3 });
@@ -414,17 +414,17 @@ describe('setActivePlayers', () => {
});
test('once', () => {
- const game = {
+ const game: Game = {
moves: {
- B: (G, ctx) => {
- ctx.events.setActivePlayers({
+ B: ({ G, events }) => {
+ events.setActivePlayers({
value: { '0': Stage.NULL, '1': Stage.NULL },
minMoves: 1,
maxMoves: 1,
});
return G;
},
- A: (G) => G,
+ A: ({ G }) => G,
},
};
@@ -440,17 +440,17 @@ describe('setActivePlayers', () => {
});
test('others', () => {
- const game = {
+ const game: Game = {
moves: {
- B: (G, ctx) => {
- ctx.events.setActivePlayers({
+ B: ({ G, events }) => {
+ events.setActivePlayers({
minMoves: 1,
maxMoves: 1,
others: Stage.NULL,
});
return G;
},
- A: (G) => G,
+ A: ({ G }) => G,
},
};
@@ -472,11 +472,11 @@ describe('setActivePlayers', () => {
});
test('set stages to Stage.NULL', () => {
- const game = {
+ const game: Game = {
moves: {
- A: (G) => G,
- B: (G, ctx) => {
- ctx.events.setActivePlayers({
+ A: ({ G }) => G,
+ B: ({ G, events }) => {
+ events.setActivePlayers({
minMoves: 1,
maxMoves: 1,
currentPlayer: 'start',
@@ -494,8 +494,8 @@ describe('setActivePlayers', () => {
stages: {
start: {
moves: {
- S: (G, ctx) => {
- ctx.events.setStage(Stage.NULL);
+ S: ({ G, events }) => {
+ events.setStage(Stage.NULL);
return G;
},
},
@@ -523,7 +523,7 @@ describe('setActivePlayers', () => {
describe('reset behavior', () => {
test('start of turn', () => {
- const game = {
+ const game: Game = {
moves: {
A: () => {},
},
@@ -551,10 +551,10 @@ describe('setActivePlayers', () => {
describe('revert', () => {
test('resets to previous', () => {
- const game = {
+ const game: Game = {
moves: {
- A: (G, ctx) => {
- ctx.events.setActivePlayers({
+ A: ({ events }) => {
+ events.setActivePlayers({
currentPlayer: 'stage2',
minMoves: 1,
maxMoves: 1,
@@ -600,10 +600,10 @@ describe('setActivePlayers', () => {
});
test('restores move limits and counts', () => {
- const game = {
+ const game: Game = {
moves: {
- A: (G, ctx) => {
- ctx.events.setActivePlayers({
+ A: ({ events }) => {
+ events.setActivePlayers({
currentPlayer: 'stage2',
minMoves: 1,
maxMoves: 1,
@@ -681,7 +681,7 @@ describe('setActivePlayers', () => {
});
test('set to next', () => {
- const game = {
+ const game: Game = {
moves: {
A: () => {},
},
@@ -741,7 +741,7 @@ describe('setActivePlayers', () => {
describe('move limits', () => {
test('shorthand syntax', () => {
- const game = {
+ const game: Game = {
turn: {
activePlayers: {
all: 'play',
@@ -795,7 +795,7 @@ describe('setActivePlayers', () => {
});
test('long-form syntax', () => {
- const game = {
+ const game: Game = {
turn: {
activePlayers: {
currentPlayer: { stage: 'play', minMoves: 1, maxMoves: 2 },
@@ -842,7 +842,7 @@ describe('setActivePlayers', () => {
});
test('player-specific limit overrides move limit args', () => {
- const game = {
+ const game: Game = {
turn: {
activePlayers: {
all: { stage: 'play', minMoves: 2, maxMoves: 2 },
@@ -866,7 +866,7 @@ describe('setActivePlayers', () => {
});
test('value syntax', () => {
- const game = {
+ const game: Game = {
turn: {
activePlayers: {
value: {
@@ -911,7 +911,7 @@ describe('setActivePlayers', () => {
});
test('move counts reset on turn end', () => {
- const game = {
+ const game: Game = {
turn: {
activePlayers: {
all: 'play',
@@ -948,10 +948,10 @@ describe('setActivePlayers', () => {
let state;
let reducer;
beforeAll(() => {
- const game = {
+ const game: Game = {
moves: {
- militia: (G, ctx) => {
- ctx.events.setActivePlayers({
+ militia: ({ events }) => {
+ events.setActivePlayers({
others: 'discard',
minMoves: 1,
maxMoves: 1,
@@ -964,7 +964,7 @@ describe('setActivePlayers', () => {
stages: {
discard: {
moves: {
- discard: (G) => G,
+ discard: ({ G }) => G,
},
},
},
@@ -1087,15 +1087,15 @@ describe('Random API is available', () => {
const turn = {
order: {
- first: (_, ctx) => {
- if (ctx.random !== undefined) {
+ first: ({ random }) => {
+ if (random !== undefined) {
first = true;
}
return 0;
},
- next: (_, ctx) => {
- if (ctx.random !== undefined) {
+ next: ({ random }) => {
+ if (random !== undefined) {
next = true;
}
return 0;
@@ -1103,7 +1103,7 @@ describe('Random API is available', () => {
},
};
- const game = { turn };
+ const game: Game = { turn };
beforeEach(() => {
first = next = false;
diff --git a/src/core/turn-order.ts b/src/core/turn-order.ts
index acc1a331d..a0a2e459e 100644
--- a/src/core/turn-order.ts
+++ b/src/core/turn-order.ts
@@ -15,6 +15,7 @@ import type {
PlayerID,
State,
TurnConfig,
+ FnContext,
} from '../types';
import { supportDeprecatedMoveLimit } from './backwards-compatibility';
@@ -254,15 +255,16 @@ function getCurrentPlayer(
export function InitTurnOrderState(state: State, turn: TurnConfig) {
let { G, ctx } = state;
const { numPlayers } = ctx;
- const ctxWithAPI = plugin.EnhanceCtx(state);
+ const pluginAPIs = plugin.GetAPIs(state);
+ const context = { ...pluginAPIs, G, ctx };
const order = turn.order;
let playOrder = [...Array.from({ length: numPlayers })].map((_, i) => i + '');
if (order.playOrder !== undefined) {
- playOrder = order.playOrder(G, ctxWithAPI);
+ playOrder = order.playOrder(context);
}
- const playOrderPos = order.first(G, ctxWithAPI);
+ const playOrderPos = order.first(context);
const posType = typeof playOrderPos;
if (posType !== 'number') {
logging.error(
@@ -316,8 +318,9 @@ export function UpdateTurnOrderState(
}
});
} else {
- const ctxWithAPI = plugin.EnhanceCtx(state);
- const t = order.next(G, ctxWithAPI);
+ const pluginAPIs = plugin.GetAPIs(state);
+ const context = { ...pluginAPIs, G, ctx };
+ const t = order.next(context);
const type = typeof t;
if (t !== undefined && type !== 'number') {
logging.error(
@@ -360,11 +363,11 @@ export const TurnOrder = {
* The default round-robin turn order.
*/
DEFAULT: {
- first: (G: any, ctx: Ctx) =>
+ first: ({ ctx }: FnContext) =>
ctx.turn === 0
? ctx.playOrderPos
: (ctx.playOrderPos + 1) % ctx.playOrder.length,
- next: (G: any, ctx: Ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
+ next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
},
/**
@@ -374,7 +377,7 @@ export const TurnOrder = {
*/
RESET: {
first: () => 0,
- next: (G: any, ctx: Ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
+ next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
},
/**
@@ -383,8 +386,8 @@ export const TurnOrder = {
* Similar to DEFAULT, but starts with the player who ended the last phase.
*/
CONTINUE: {
- first: (G: any, ctx: Ctx) => ctx.playOrderPos,
- next: (G: any, ctx: Ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
+ first: ({ ctx }: FnContext) => ctx.playOrderPos,
+ next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
},
/**
@@ -395,7 +398,7 @@ export const TurnOrder = {
*/
ONCE: {
first: () => 0,
- next: (G: any, ctx: Ctx) => {
+ next: ({ ctx }: FnContext) => {
if (ctx.playOrderPos < ctx.playOrder.length - 1) {
return ctx.playOrderPos + 1;
}
@@ -413,7 +416,7 @@ export const TurnOrder = {
CUSTOM: (playOrder: string[]) => ({
playOrder: () => playOrder,
first: () => 0,
- next: (G: any, ctx: Ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
+ next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
}),
/**
@@ -426,9 +429,9 @@ export const TurnOrder = {
* @param {string} playOrderField - Field in G.
*/
CUSTOM_FROM: (playOrderField: string) => ({
- playOrder: (G: any) => G[playOrderField],
+ playOrder: ({ G }: FnContext) => G[playOrderField],
first: () => 0,
- next: (G: any, ctx: Ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
+ next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
}),
};
diff --git a/src/master/filter-player-view.test.ts b/src/master/filter-player-view.test.ts
index c7b9fb1af..ab65f8b14 100644
--- a/src/master/filter-player-view.test.ts
+++ b/src/master/filter-player-view.test.ts
@@ -4,7 +4,7 @@ import { Master } from './master';
import { InMemory } from '../server/db/inmemory';
import { PlayerView } from '../core/player-view';
import { INVALID_MOVE } from '../core/constants';
-import type { Ctx, SyncInfo } from '../types';
+import type { Game, SyncInfo } from '../types';
function TransportAPI(send = jest.fn(), sendAll = jest.fn()) {
return { send, sendAll };
@@ -19,9 +19,9 @@ function validateNotTransientState(state: any) {
describe('playerView - update', () => {
const send = jest.fn();
const sendAll = jest.fn();
- const game = {
- playerView: (G, ctx, player) => {
- return { ...G, player };
+ const game: Game = {
+ playerView: ({ G, playerID }) => {
+ return { ...G, player: playerID };
},
};
const master = new Master(game, new InMemory(), TransportAPI(send, sendAll));
@@ -66,7 +66,7 @@ describe('playerView - patch', () => {
const send = jest.fn();
const sendAll = jest.fn();
const db = new InMemory();
- const game = {
+ const game: Game = {
seed: 0,
deltaState: true,
setup: () => {
@@ -94,17 +94,17 @@ describe('playerView - patch', () => {
},
A: {
client: false,
- move: (G, ctx: Ctx) => {
- const card = G.players[ctx.playerID].cards.shift();
+ move: ({ G, playerID }) => {
+ const card = G.players[playerID].cards.shift();
G.discardedCards.push(card);
},
},
B: {
client: false,
ignoreStaleStateID: true,
- move: (G, ctx: Ctx) => {
+ move: ({ G, playerID }) => {
const card = G.cards.pop();
- G.players[ctx.playerID].cards.push(card);
+ G.players[playerID].cards.push(card);
},
},
},
@@ -256,11 +256,11 @@ describe('redactLog', () => {
});
test('make sure filter player view redacts the log', async () => {
- const game = {
+ const game: Game = {
moves: {
- A: (G) => G,
+ A: ({ G }) => G,
B: {
- move: (G) => G,
+ move: ({ G }) => G,
redact: true,
},
},
diff --git a/src/master/filter-player-view.ts b/src/master/filter-player-view.ts
index ff3022067..8aec64806 100644
--- a/src/master/filter-player-view.ts
+++ b/src/master/filter-player-view.ts
@@ -9,7 +9,7 @@ const applyPlayerView = (
state: State
): State => ({
...state,
- G: game.playerView(state.G, state.ctx, playerID),
+ G: game.playerView({ G: state.G, ctx: state.ctx, playerID }),
plugins: PlayerView(state, { playerID, game }),
deltalog: undefined,
_undo: [],
diff --git a/src/master/master.test.ts b/src/master/master.test.ts
index 9000ad660..8b8a2b42b 100644
--- a/src/master/master.test.ts
+++ b/src/master/master.test.ts
@@ -11,7 +11,7 @@ import { InitializeGame } from '../core/initialize';
import { InMemory } from '../server/db/inmemory';
import { Master } from './master';
import { error } from '../core/logger';
-import type { Game, Server, State, Ctx, LogEntry } from '../types';
+import type { Game, Server, State, LogEntry } from '../types';
import { Auth } from '../server/auth';
import * as StorageAPI from '../server/db/base';
import * as dateMock from 'jest-date-mock';
@@ -78,7 +78,7 @@ class InMemoryAsync extends StorageAPI.Async {
}
}
-const game = { seed: 0 };
+const game: Game = { seed: 0 };
function TransportAPI(send = jest.fn(), sendAll = jest.fn()) {
return { send, sendAll };
@@ -173,9 +173,9 @@ describe('sync', () => {
describe('update', () => {
const send = jest.fn();
const sendAll = jest.fn();
- const game = {
+ const game: Game = {
moves: {
- A: (G) => G,
+ A: ({ G }) => G,
},
};
let db;
@@ -260,7 +260,7 @@ describe('update', () => {
});
test('allow execution of moves with ignoreStaleStateID truthy', async () => {
- const game = {
+ const game: Game = {
setup: () => {
const G = {
players: {
@@ -282,14 +282,14 @@ describe('update', () => {
stages: {
A: {
moves: {
- A: (G, ctx: Ctx) => {
- const card = G.players[ctx.playerID].cards.shift();
+ A: ({ G, playerID }) => {
+ const card = G.players[playerID].cards.shift();
G.discardedCards.push(card);
},
B: {
- move: (G, ctx: Ctx) => {
+ move: ({ G, playerID }) => {
const card = G.cards.pop();
- G.players[ctx.playerID].cards.push(card);
+ G.players[playerID].cards.push(card);
},
ignoreStaleStateID: true,
},
@@ -534,17 +534,17 @@ describe('patch', () => {
},
A: {
client: false,
- move: (G, ctx: Ctx) => {
- const card = G.players[ctx.playerID].cards.shift();
+ move: ({ G, playerID }) => {
+ const card = G.players[playerID].cards.shift();
G.discardedCards.push(card);
},
},
B: {
client: false,
ignoreStaleStateID: true,
- move: (G, ctx: Ctx) => {
+ move: ({ G, playerID }) => {
const card = G.cards.pop();
- G.players[ctx.playerID].cards.push(card);
+ G.players[playerID].cards.push(card);
},
},
},
diff --git a/src/plugins/events/events.test.ts b/src/plugins/events/events.test.ts
index df7e02892..56fc8b785 100644
--- a/src/plugins/events/events.test.ts
+++ b/src/plugins/events/events.test.ts
@@ -58,10 +58,10 @@ test('dispatch', () => {
});
test('update ctx', () => {
- const game = {
+ const game: Game = {
moves: {
- A: (G, ctx) => {
- ctx.events.endTurn();
+ A: ({ G, events }) => {
+ events.endTurn();
return G;
},
},
@@ -73,10 +73,10 @@ test('update ctx', () => {
});
test('no duplicate endTurn', () => {
- const game = {
+ const game: Game = {
turn: {
- onEnd: (G, ctx) => {
- ctx.events.endTurn();
+ onEnd: ({ events }) => {
+ events.endTurn();
},
},
};
@@ -88,12 +88,12 @@ test('no duplicate endTurn', () => {
});
test('no duplicate endPhase', () => {
- const game = {
+ const game: Game = {
phases: {
A: {
start: true,
- onEnd: (G, ctx) => {
- ctx.events.setPhase('C');
+ onEnd: ({ events }) => {
+ events.setPhase('C');
},
},
B: {},
diff --git a/src/plugins/main.test.ts b/src/plugins/main.test.ts
index ca748c8f5..218c2ef94 100644
--- a/src/plugins/main.test.ts
+++ b/src/plugins/main.test.ts
@@ -8,65 +8,71 @@
import { Client } from '../client/client';
import { Local } from '../client/transport/local';
-import type { Game } from '../types';
+import type { Game, Plugin } from '../types';
import { GameMethod } from '../../packages/core';
describe('basic', () => {
let client: ReturnType;
beforeAll(() => {
- const game = {
+ interface TestPluginAPI {
+ get(): number;
+ increment(): number;
+ }
+
+ const TestPlugin = (init: {
+ n: number;
+ }): Plugin => ({
+ name: 'test',
+
+ setup: () => init,
+
+ api: ({ data }) => {
+ const state = { value: data.n };
+ const increment = () => state.value++;
+ const get = () => state.value;
+ return { increment, get };
+ },
+
+ flush: ({ api }) => ({ n: api.get() }),
+
+ fnWrap: (fn) => (context) => {
+ const G = fn(context);
+ return { ...G, wrap: true };
+ },
+ });
+
+ const game: Game<
+ { beginA: number; endA: number; onMove: number; onTurnEnd: number },
+ { test: TestPluginAPI }
+ > = {
moves: {
- A: (G, ctx) => {
- G.beginA = ctx.test.get();
- ctx.test.increment();
- G.endA = ctx.test.get();
+ A: ({ G, test }) => {
+ G.beginA = test.get();
+ test.increment();
+ G.endA = test.get();
},
},
- endIf: (_, ctx) => {
- if (ctx.test === undefined) {
+ endIf: ({ test }) => {
+ if (test === undefined) {
throw new Error('API is not defined');
}
},
turn: {
- onMove: (G, ctx) => {
- G.onMove = ctx.test.get();
- ctx.test.increment();
+ onMove: ({ G, test }) => {
+ G.onMove = test.get();
+ test.increment();
},
- onEnd: (G, ctx) => {
- G.onTurnEnd = ctx.test.get();
- ctx.test.increment();
+ onEnd: ({ G, test }) => {
+ G.onTurnEnd = test.get();
+ test.increment();
},
},
- pluginRelated: 10,
-
- plugins: [
- {
- name: 'test',
-
- setup: ({ game }) => ({
- n: game.pluginRelated,
- }),
-
- api: ({ data }) => {
- const state = { value: data.n };
- const increment = () => state.value++;
- const get = () => state.value;
- return { increment, get };
- },
-
- flush: ({ api }) => ({ n: api.get() }),
-
- fnWrap: (fn) => (G, ctx) => {
- G = fn(G, ctx);
- return { ...G, wrap: true };
- },
- },
- ],
+ plugins: [TestPlugin({ n: 10 })],
};
client = Client({ game });
@@ -126,7 +132,10 @@ describe('default values', () => {
noClient: () => false,
};
- const game = { moves: { A: () => {} }, plugins: [plugin, anotherPlugin] };
+ const game: Game = {
+ moves: { A: () => {} },
+ plugins: [plugin, anotherPlugin],
+ };
test('are used if no setup is present', () => {
const client = Client({ game, playerID: '0', multiplayer: Local() });
@@ -180,11 +189,11 @@ describe('isInvalid method', () => {
},
],
moves: {
- good: (_, ctx) => {
- ctx.test.set('good', 'nice');
+ good: ({ test }) => {
+ test.set('good', 'nice');
},
- bad: (_, ctx) => {
- ctx.test.set('bad', 'not ok');
+ bad: ({ test }) => {
+ test.set('bad', 'not ok');
},
},
};
@@ -200,10 +209,10 @@ describe('isInvalid method', () => {
});
describe('actions', () => {
- let client;
+ let client: ReturnType;
beforeAll(() => {
- const game = {
+ const game: Game = {
plugins: [
{
name: 'test',
@@ -245,6 +254,8 @@ describe('actions', () => {
});
describe('plugins are accessible in events triggered from moves', () => {
+ type TestPluginAPI = { get: () => boolean };
+ type PluginAPIs = { test: TestPluginAPI };
const plugins = [
{
name: 'test',
@@ -255,7 +266,7 @@ describe('plugins are accessible in events triggered from moves', () => {
flush: () => ({ initial: true }),
- api: ({ data }) => {
+ api: ({ data }): TestPluginAPI => {
return {
get: () => data.initial,
};
@@ -264,15 +275,15 @@ describe('plugins are accessible in events triggered from moves', () => {
];
test('turn/onBegin', () => {
- const game = {
+ const game: Game = {
plugins,
moves: {
- stop: (G, ctx) => ctx.events.endTurn(),
+ stop: ({ events }) => events.endTurn(),
},
turn: {
- onBegin: (G, ctx) => {
- G.onBegin = ctx.random.Die(1);
- G.test = ctx.test.get();
+ onBegin: ({ G, random, test }) => {
+ G.onBegin = random.Die(1);
+ G.test = test.get();
},
},
};
@@ -286,15 +297,15 @@ describe('plugins are accessible in events triggered from moves', () => {
});
test('turn/onEnd', () => {
- const game = {
+ const game: Game = {
plugins,
moves: {
- stop: (G, ctx) => ctx.events.endTurn(),
+ stop: ({ events }) => events.endTurn(),
},
turn: {
- onEnd: (G, ctx) => {
- G.onEnd = ctx.random.Die(1);
- G.test = ctx.test.get();
+ onEnd: ({ G, random, test }) => {
+ G.onEnd = random.Die(1);
+ G.test = test.get();
},
},
};
@@ -308,19 +319,19 @@ describe('plugins are accessible in events triggered from moves', () => {
});
test('phase/onBegin', () => {
- const game = {
+ const game: Game = {
plugins,
moves: {
- stop: (G, ctx) => ctx.events.setPhase('second'),
+ stop: ({ events }) => events.setPhase('second'),
},
phases: {
first: {
start: true,
},
second: {
- onBegin: (G, ctx) => {
- G.onBegin = ctx.random.Die(1);
- G.test = ctx.test.get();
+ onBegin: ({ G, random, test }) => {
+ G.onBegin = random.Die(1);
+ G.test = test.get();
},
},
},
@@ -335,17 +346,17 @@ describe('plugins are accessible in events triggered from moves', () => {
});
test('phase/onEnd', () => {
- const game = {
+ const game: Game = {
plugins,
moves: {
- stop: (G, ctx) => ctx.events.endPhase(),
+ stop: ({ events }) => events.endPhase(),
},
phases: {
first: {
start: true,
- onEnd: (G, ctx) => {
- G.onEnd = ctx.random.Die(1);
- G.test = ctx.test.get();
+ onEnd: ({ G, random, test }) => {
+ G.onEnd = random.Die(1);
+ G.test = test.get();
},
},
},
@@ -367,13 +378,13 @@ describe('plugins can use events in fnWrap', () => {
name: 'test',
fnWrap:
(fn, type) =>
- (G, ctx, ...args) => {
- G = fn(G, ctx, ...args);
+ (context, ...args) => {
+ const G = fn(context, ...args);
if (G.endTurn && type === GameMethod.MOVE) {
- ctx.events.endTurn();
+ context.events.endTurn();
}
if (G.endGame) {
- ctx.events.endGame(G.endGame);
+ context.events.endGame(G.endGame);
}
return G;
},
diff --git a/src/plugins/main.ts b/src/plugins/main.ts
index 7e220d790..f0fc9ba96 100644
--- a/src/plugins/main.ts
+++ b/src/plugins/main.ts
@@ -13,11 +13,11 @@ import PluginLog from './plugin-log';
import PluginSerializable from './plugin-serializable';
import type {
AnyFn,
+ DefaultPluginAPIs,
PartialGameState,
State,
Game,
Plugin,
- Ctx,
ActionShape,
PlayerID,
} from '../types';
@@ -64,7 +64,7 @@ export const ProcessAction = (
};
/**
- * The API's created by various plugins are stored in the plugins
+ * The APIs created by various plugins are stored in the plugins
* section of the state object:
*
* {
@@ -78,17 +78,14 @@ export const ProcessAction = (
* }
* }
*
- * This function takes these API's and stuffs them back into
- * ctx for consumption inside a move function or hook.
+ * This function retrieves plugin APIs and returns them as an object
+ * for consumption as used by move contexts.
*/
-export const EnhanceCtx = (state: PartialGameState): Ctx => {
- const ctx = { ...state.ctx };
- const plugins = state.plugins || {};
- Object.entries(plugins).forEach(([name, { api }]) => {
- ctx[name] = api;
- });
- return ctx;
-};
+export const GetAPIs = ({ plugins }: PartialGameState) =>
+ Object.entries(plugins || {}).reduce((apis, [name, { api }]) => {
+ apis[name] = api;
+ return apis;
+ }, {} as DefaultPluginAPIs);
/**
* Applies the provided plugins to the given move / flow function.
@@ -144,10 +141,10 @@ export const Setup = (
* the `plugins` section of the state (which is subsequently
* merged into ctx).
*/
-export const Enhance = (
- state: State,
+export const Enhance = (
+ state: S,
opts: PluginOpts & { playerID: PlayerID }
-): State => {
+): S => {
[...DEFAULT_PLUGINS, ...opts.game.plugins]
.filter((plugin) => plugin.api !== undefined)
.forEach((plugin) => {
diff --git a/src/plugins/plugin-events.ts b/src/plugins/plugin-events.ts
index 011c70d8c..d306d152a 100644
--- a/src/plugins/plugin-events.ts
+++ b/src/plugins/plugin-events.ts
@@ -24,10 +24,10 @@ const EventsPlugin: Plugin = {
// endings to dispatch the current turn and phase correctly.
fnWrap:
(method, methodType) =>
- (G, ctx, ...args) => {
- const api = ctx.events as EventsAPI & PrivateEventsAPI;
- if (api) api._private.updateTurnContext(ctx, methodType);
- G = method(G, ctx, ...args);
+ (context, ...args) => {
+ const api = context.events as EventsAPI & PrivateEventsAPI;
+ if (api) api._private.updateTurnContext(context.ctx, methodType);
+ const G = method(context, ...args);
if (api) api._private.unsetCurrentMethod();
return G;
},
diff --git a/src/plugins/plugin-immer.test.ts b/src/plugins/plugin-immer.test.ts
index 2946e8e34..45c2ac868 100644
--- a/src/plugins/plugin-immer.test.ts
+++ b/src/plugins/plugin-immer.test.ts
@@ -20,10 +20,10 @@ describe('immer', () => {
client = Client({
game: {
moves: {
- A: (G) => {
+ A: ({ G }) => {
G.moveBody = true;
},
- invalid: (G) => {
+ invalid: ({ G }) => {
G.madeInvalidMove = true;
return INVALID_MOVE;
},
@@ -32,23 +32,23 @@ describe('immer', () => {
phases: {
A: {
start: true,
- onBegin: (G) => {
+ onBegin: ({ G }) => {
G.onPhaseBegin = true;
},
- onEnd: (G) => {
+ onEnd: ({ G }) => {
G.onPhaseEnd = true;
},
},
},
turn: {
- onBegin: (G) => {
+ onBegin: ({ G }) => {
G.onTurnBegin = true;
},
- onEnd: (G) => {
+ onEnd: ({ G }) => {
G.onTurnEnd = true;
},
- onMove: (G) => {
+ onMove: ({ G }) => {
G.onMove = true;
},
},
diff --git a/src/plugins/plugin-immer.ts b/src/plugins/plugin-immer.ts
index b79f7b16f..e2b1105ae 100644
--- a/src/plugins/plugin-immer.ts
+++ b/src/plugins/plugin-immer.ts
@@ -7,7 +7,7 @@
*/
import produce from 'immer';
-import type { AnyFn, Ctx, Plugin } from '../types';
+import type { Plugin } from '../types';
import { INVALID_MOVE } from '../core/constants';
/**
@@ -18,11 +18,11 @@ const ImmerPlugin: Plugin = {
name: 'plugin-immer',
fnWrap:
- (move: AnyFn) =>
- (G: any, ctx: Ctx, ...args: any[]) => {
+ (move) =>
+ (context, ...args) => {
let isInvalid = false;
- const newG = produce(G, (G) => {
- const result = move(G, ctx, ...args);
+ const newG = produce(context.G, (G) => {
+ const result = move({ ...context, G }, ...args);
if (result === INVALID_MOVE) {
isInvalid = true;
return;
diff --git a/src/plugins/plugin-log.test.ts b/src/plugins/plugin-log.test.ts
index 5e9cf355f..c3f710fdf 100644
--- a/src/plugins/plugin-log.test.ts
+++ b/src/plugins/plugin-log.test.ts
@@ -7,17 +7,18 @@
*/
import { Client } from '../client/client';
+import type { Game } from '../types';
describe('log-metadata', () => {
test('It sets metadata in a move and then clears the metadata', () => {
- const game = {
+ const game: Game = {
moves: {
- setMetadataMove: (G, ctx) => {
- ctx.log.setMetadata({
+ setMetadataMove: ({ log }) => {
+ log.setMetadata({
message: 'test',
});
},
- doNothing: (G) => G,
+ doNothing: ({ G }) => G,
},
};
const client = Client({ game });
diff --git a/src/plugins/plugin-player.test.ts b/src/plugins/plugin-player.test.ts
index db652382a..335f760e9 100644
--- a/src/plugins/plugin-player.test.ts
+++ b/src/plugins/plugin-player.test.ts
@@ -7,12 +7,14 @@
*/
import PluginPlayer from './plugin-player';
+import type { PlayerAPI } from './plugin-player';
import { Client } from '../client/client';
+import type { Game } from '../types';
describe('default values', () => {
test('playerState is not passed', () => {
const plugin = PluginPlayer();
- const game = {
+ const game: Game = {
plugins: [plugin],
};
const client = Client({ game });
@@ -23,7 +25,7 @@ describe('default values', () => {
test('playerState is passed', () => {
const plugin = PluginPlayer({ setup: () => ({ A: 1 }) });
- const game = {
+ const game: Game = {
plugins: [plugin],
};
const client = Client({ game });
@@ -37,16 +39,16 @@ describe('2 player game', () => {
let client;
beforeAll(() => {
- const game = {
+ const game: Game = {
moves: {
- A: (_, ctx) => {
- ctx.player.set({ field: 'A1' });
- ctx.player.opponent.set({ field: 'A2' });
+ A: ({ player }) => {
+ player.set({ field: 'A1' });
+ player.opponent.set({ field: 'A2' });
},
- B: (G, ctx) => {
- G.playerValue = ctx.player.get().field;
- G.opponentValue = ctx.player.opponent.get().field;
+ B: ({ G, player }) => {
+ G.playerValue = player.get().field;
+ G.opponentValue = player.opponent.get().field;
},
},
@@ -90,10 +92,10 @@ describe('3 player game', () => {
let client;
beforeAll(() => {
- const game = {
+ const game: Game = {
moves: {
- A: (_, ctx) => {
- ctx.player.set({ field: 'A' });
+ A: ({ player }) => {
+ player.set({ field: 'A' });
},
},
@@ -119,7 +121,7 @@ describe('game with phases', () => {
let client;
beforeAll(() => {
- const game = {
+ const game: Game = {
plugins: [PluginPlayer({ setup: (id) => ({ id }) })],
phases: {
phase: {},
diff --git a/src/plugins/plugin-serializable.test.ts b/src/plugins/plugin-serializable.test.ts
index 85163b660..38256a2ad 100644
--- a/src/plugins/plugin-serializable.test.ts
+++ b/src/plugins/plugin-serializable.test.ts
@@ -1,10 +1,11 @@
import { Client } from '../client/client';
+import type { Game } from '../types';
describe('plugin-serializable', () => {
- let client;
+ let client: ReturnType;
beforeAll(() => {
- const game = {
+ const game: Game = {
moves: {
serializable: () => {
return { hello: 'world' };
diff --git a/src/plugins/plugin-serializable.ts b/src/plugins/plugin-serializable.ts
index ec7062267..e5319b1fa 100644
--- a/src/plugins/plugin-serializable.ts
+++ b/src/plugins/plugin-serializable.ts
@@ -1,4 +1,4 @@
-import type { Plugin, AnyFn, Ctx } from '../types';
+import type { Plugin } from '../types';
import isPlainObject from 'lodash.isplainobject';
/**
@@ -38,9 +38,9 @@ const SerializablePlugin: Plugin = {
name: 'plugin-serializable',
fnWrap:
- (move: AnyFn) =>
- (G: unknown, ctx: Ctx, ...args: any[]) => {
- const result = move(G, ctx, ...args);
+ (move) =>
+ (context, ...args) => {
+ const result = move(context, ...args);
// Check state in non-production environments.
if (process.env.NODE_ENV !== 'production' && !isSerializable(result)) {
throw new Error(
diff --git a/src/plugins/random/random.test.ts b/src/plugins/random/random.test.ts
index 724f1256d..f55a7c3a2 100644
--- a/src/plugins/random/random.test.ts
+++ b/src/plugins/random/random.test.ts
@@ -11,6 +11,7 @@ import { makeMove } from '../../core/action-creators';
import { CreateGameReducer } from '../../core/reducer';
import { InitializeGame } from '../../core/initialize';
import { Client } from '../../client/client';
+import type { Game } from '../../types';
import { PlayerView } from '../main';
function Init(seed) {
@@ -100,10 +101,10 @@ test('Random.Shuffle', () => {
});
test('Random API is not executed optimisitically', () => {
- const game = {
+ const game: Game = {
seed: 0,
moves: {
- rollDie: (G, ctx) => ({ ...G, die: ctx.random.D6() }),
+ rollDie: ({ G, random }) => ({ ...G, die: random.D6() }),
},
};
@@ -128,7 +129,7 @@ test('Random API works when its state is redacted by playerView', () => {
const game = {
seed: 0,
moves: {
- rollDie: (G, ctx) => ({ ...G, die: ctx.random.D6() }),
+ rollDie: ({ G, random }) => ({ ...G, die: random.D6() }),
},
};
@@ -146,11 +147,11 @@ test('turn.onBegin has ctx APIs at the beginning of the game', () => {
let random = null;
let events = null;
- const game = {
+ const game: Game = {
turn: {
- onBegin: (G, ctx) => {
- random = ctx.random;
- events = ctx.events;
+ onBegin: (context) => {
+ random = context.random;
+ events = context.events;
},
},
};
diff --git a/src/server/index.test.ts b/src/server/index.test.ts
index 36511d913..2dcee7944 100644
--- a/src/server/index.test.ts
+++ b/src/server/index.test.ts
@@ -11,9 +11,9 @@ import request from 'supertest';
import { Server, createServerRunConfig, getPortFromServer } from '.';
import type { KoaServer } from '.';
import type { SocketIO } from './transport/socketio';
-import type { StorageAPI } from '../types';
+import type { Game, StorageAPI } from '../types';
-const game = { seed: 0 };
+const game: Game = { seed: 0 };
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
beforeEach(warn.mockReset);
@@ -50,21 +50,21 @@ jest.mock('koa-socket-2', () => {
describe('new', () => {
test('custom db implementation', () => {
- const game = {};
+ const game: Game = {};
const db = {} as StorageAPI.Sync;
const server = Server({ games: [game], db });
expect(server.db).toBe(db);
});
test('custom transport implementation', () => {
- const game = {};
+ const game: Game = {};
const transport = { init: jest.fn() } as unknown as SocketIO;
Server({ games: [game], transport });
expect(transport.init).toBeCalled();
});
test('custom auth implementation', () => {
- const game = {};
+ const game: Game = {};
const authenticateCredentials = () => true;
const server = Server({ games: [game], authenticateCredentials });
expect(server.db).not.toBeNull();
diff --git a/src/server/transport/socketio-simultaneous.test.ts b/src/server/transport/socketio-simultaneous.test.ts
index df8aca53d..321f7a879 100644
--- a/src/server/transport/socketio-simultaneous.test.ts
+++ b/src/server/transport/socketio-simultaneous.test.ts
@@ -17,7 +17,7 @@ import * as ActionCreators from '../../core/action-creators';
import { InitializeGame } from '../../core/initialize';
import { PlayerView } from '../../core/player-view';
import type { Master } from '../../master/master';
-import type { Ctx, LogEntry, Server, State, StorageAPI } from '../../types';
+import type { Game, LogEntry, Server, State, StorageAPI } from '../../types';
type SyncArgs = Parameters;
type UpdateArgs = Parameters;
@@ -162,23 +162,20 @@ jest.mock('koa-socket-2', () => {
});
describe('simultaneous moves on server game', () => {
- const game = {
+ const game: Game = {
name: 'test',
- setup: () => {
- const G = {
- players: {
- '0': {
- cards: ['card3'],
- },
- '1': {
- cards: [],
- },
+ setup: () => ({
+ players: {
+ '0': {
+ cards: ['card3'],
},
- cards: ['card0', 'card1', 'card2'],
- discardedCards: [],
- };
- return G;
- },
+ '1': {
+ cards: [],
+ },
+ },
+ cards: ['card0', 'card1', 'card2'],
+ discardedCards: [],
+ }),
playerView: PlayerView.STRIP_SECRETS,
turn: {
activePlayers: { currentPlayer: { stage: 'A' } },
@@ -187,17 +184,17 @@ describe('simultaneous moves on server game', () => {
moves: {
A: {
client: false,
- move: (G, ctx: Ctx) => {
- const card = G.players[ctx.playerID].cards.shift();
+ move: ({ G, playerID }) => {
+ const card = G.players[playerID].cards.shift();
G.discardedCards.push(card);
},
},
B: {
client: false,
ignoreStaleStateID: true,
- move: (G, ctx: Ctx) => {
+ move: ({ G, playerID }) => {
const card = G.cards.pop();
- G.players[ctx.playerID].cards.push(card);
+ G.players[playerID].cards.push(card);
},
},
},
@@ -463,12 +460,12 @@ describe('simultaneous moves on server game', () => {
});
describe('inauthentic clients', () => {
- const game = {
+ const game: Game = {
setup: () => ({
0: 'foo',
1: 'bar',
}),
- playerView: (G, _ctx, playerID) => ({ [playerID]: G[playerID] }),
+ playerView: ({ G, playerID }) => ({ [playerID]: G[playerID] }),
};
let app;
diff --git a/src/types.ts b/src/types.ts
index 75e2c4f71..b6e2c60da 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,4 @@
-import type { Object } from 'ts-toolbelt';
+import type { Object, Misc } from 'ts-toolbelt';
import type Koa from 'koa';
import type { Store as ReduxStore } from 'redux';
import type * as ActionCreators from './core/action-creators';
@@ -18,9 +18,9 @@ export type { StorageAPI };
export type AnyFn = (...args: any[]) => any;
// "Public" state to be communicated to clients.
-export interface State {
+export interface State {
G: G;
- ctx: Ctx | CtxWithPlugins;
+ ctx: Ctx;
deltalog?: Array;
plugins: {
[pluginName: string]: PluginState;
@@ -47,10 +47,7 @@ export type ActionResult = any;
// "Private" state that may include garbage that should be stripped before
// being handed back to a client.
-export interface TransientState<
- G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
-> extends State {
+export interface TransientState extends State {
transients?: TransientMetadata;
}
@@ -90,7 +87,6 @@ export interface Ctx {
numPlayers: number;
playOrder: Array;
playOrderPos: number;
- playerID?: PlayerID;
activePlayers: null | ActivePlayers;
currentPlayer: PlayerID;
numMoves?: number;
@@ -110,13 +106,12 @@ export interface Ctx {
_random?: {
seed: string | number;
};
+}
- // TODO public api should have these as non-optional
- // internally there are two contexts, one is a serialized POJO and another
- // "enhanced" context that has plugin api methods attached
- events?: EventsAPI;
- log?: LogAPI;
- random?: RandomAPI;
+export interface DefaultPluginAPIs {
+ events: EventsAPI;
+ log: LogAPI;
+ random: RandomAPI;
}
export interface PluginState {
@@ -158,147 +153,162 @@ export interface Plugin<
> {
name: string;
noClient?: (context: PluginContext) => boolean;
+ setup?: (setupCtx: { G: G; ctx: Ctx; game: Game }) => Data;
isInvalid?: (
context: Omit, 'api'>
) => false | string;
- setup?: (setupCtx: { G: G; ctx: Ctx; game: Game }) => Data;
action?: (data: Data, payload: ActionShape.Plugin['payload']) => Data;
api?: (context: {
G: G;
ctx: Ctx;
- game: Game;
+ game: Game;
data: Data;
playerID?: PlayerID;
}) => API;
flush?: (context: PluginContext) => Data;
dangerouslyFlushRawState?: (flushCtx: {
- state: State;
- game: Game;
+ state: State;
+ game: Game;
api: API;
data: Data;
- }) => State;
+ }) => State;
fnWrap?: (
- moveOrHook: (G: G, ctx: Ctx, ...args: any[]) => any,
+ moveOrHook: (context: FnContext, ...args: SerializableAny[]) => any,
methodType: GameMethod
- ) => (G: G, ctx: Ctx, ...args: any[]) => any;
+ ) => (context: FnContext, ...args: SerializableAny[]) => any;
playerView?: (context: {
G: G;
ctx: Ctx;
- game: Game;
+ game: Game;
data: Data;
playerID?: PlayerID | null;
}) => any;
}
-export type MoveFn = (
- G: G,
- ctx: CtxWithPlugins,
- ...args: any[]
-) => any;
+export type FnContext<
+ G extends any = any,
+ PluginAPIs extends Record = Record
+> = PluginAPIs &
+ DefaultPluginAPIs & {
+ G: G;
+ ctx: Ctx;
+ };
+
+type SerializableAny = Misc.JSON.Value;
+export type MoveFn<
+ G extends any = any,
+ PluginAPIs extends Record = Record
+> = (
+ context: FnContext & { playerID: PlayerID },
+ ...args: SerializableAny[]
+) => void | G | typeof INVALID_MOVE;
export interface LongFormMove<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
- move: MoveFn;
+ move: MoveFn;
redact?: boolean;
noLimit?: boolean;
client?: boolean;
- undoable?: boolean | ((G: G, ctx: CtxWithPlugins) => boolean);
+ undoable?: boolean | ((context: { G: G; ctx: Ctx }) => boolean);
ignoreStaleStateID?: boolean;
}
-export type Move =
- | MoveFn
- | LongFormMove;
+export type Move<
+ G extends any = any,
+ PluginAPIs extends Record = Record
+> = MoveFn | LongFormMove;
export interface MoveMap<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
- [moveName: string]: Move;
+ [moveName: string]: Move;
}
export interface PhaseConfig<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
start?: boolean;
- next?: ((G: G, ctx: CtxWithPlugins) => string | void) | string;
- onBegin?: (G: G, ctx: CtxWithPlugins) => any;
- onEnd?: (G: G, ctx: CtxWithPlugins) => any;
- endIf?: (G: G, ctx: CtxWithPlugins) => boolean | void | { next: string };
- moves?: MoveMap;
- turn?: TurnConfig;
+ next?: ((context: FnContext) => string | void) | string;
+ onBegin?: (context: FnContext) => void | G;
+ onEnd?: (context: FnContext) => void | G;
+ endIf?: (
+ context: FnContext
+ ) => boolean | void | { next: string };
+ moves?: MoveMap;
+ turn?: TurnConfig;
wrapped?: {
- endIf?: (
- state: State
- ) => boolean | void | { next: string };
- onBegin?: (state: State) => any;
- onEnd?: (state: State) => any;
- next?: (state: State) => string | void;
+ endIf?: (state: State) => boolean | void | { next: string };
+ onBegin?: (state: State) => void | G;
+ onEnd?: (state: State) => void | G;
+ next?: (state: State) => string | void;
};
}
export interface StageConfig<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
- moves?: MoveMap;
+ moves?: MoveMap;
next?: string;
}
export interface StageMap<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
- [stageName: string]: StageConfig;
+ [stageName: string]: StageConfig;
}
export interface TurnOrderConfig<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
- first: (G: G, ctx: CtxWithPlugins) => number;
- next: (G: G, ctx: CtxWithPlugins) => number | undefined;
- playOrder?: (G: G, ctx: CtxWithPlugins) => PlayerID[];
+ first: (context: FnContext) => number;
+ next: (context: FnContext) => number | undefined;
+ playOrder?: (context: FnContext) => PlayerID[];
}
export interface TurnConfig<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
activePlayers?: ActivePlayersArg;
minMoves?: number;
maxMoves?: number;
/** @deprecated Use `minMoves` and `maxMoves` instead. */
moveLimit?: number;
- onBegin?: (G: G, ctx: CtxWithPlugins) => any;
- onEnd?: (G: G, ctx: CtxWithPlugins) => any;
- endIf?: (G: G, ctx: CtxWithPlugins) => boolean | void | { next: PlayerID };
- onMove?: (G: G, ctx: CtxWithPlugins) => any;
- stages?: StageMap;
- order?: TurnOrderConfig;
+ onBegin?: (context: FnContext) => void | G;
+ onEnd?: (context: FnContext) => void | G;
+ endIf?: (
+ context: FnContext
+ ) => boolean | void | { next: PlayerID };
+ onMove?: (
+ context: FnContext & { playerID: PlayerID }
+ ) => void | G;
+ stages?: StageMap;
+ order?: TurnOrderConfig;
wrapped?: {
- endIf?: (
- state: State
- ) => boolean | void | { next: PlayerID };
- onBegin?: (state: State) => any;
- onEnd?: (state: State) => any;
- onMove?: (state: State) => any;
+ endIf?: (state: State) => boolean | void | { next: PlayerID };
+ onBegin?: (state: State) => void | G;
+ onEnd?: (state: State) => void | G;
+ onMove?: (state: State & { playerID: PlayerID }) => void | G;
};
}
export interface PhaseMap<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx
+ PluginAPIs extends Record = Record
> {
- [phaseName: string]: PhaseConfig;
+ [phaseName: string]: PhaseConfig;
}
export interface Game<
G extends any = any,
- CtxWithPlugins extends Ctx = Ctx,
+ PluginAPIs extends Record = Record,
SetupData extends any = any
> {
name?: string;
@@ -307,14 +317,17 @@ export interface Game<
deltaState?: boolean;
disableUndo?: boolean;
seed?: string | number;
- setup?: (ctx: CtxWithPlugins, setupData?: SetupData) => G;
+ setup?: (
+ context: PluginAPIs & DefaultPluginAPIs & { ctx: Ctx },
+ setupData?: SetupData
+ ) => G;
validateSetupData?: (
setupData: SetupData | undefined,
numPlayers: number
) => string | undefined;
- moves?: MoveMap;
- phases?: PhaseMap;
- turn?: TurnConfig;
+ moves?: MoveMap;
+ phases?: PhaseMap;
+ turn?: TurnConfig;
events?: {
endGame?: boolean;
endPhase?: boolean;
@@ -325,9 +338,9 @@ export interface Game<
pass?: boolean;
setActivePlayers?: boolean;
};
- endIf?: (G: G, ctx: CtxWithPlugins) => any;
- onEnd?: (G: G, ctx: CtxWithPlugins) => any;
- playerView?: (G: G, ctx: CtxWithPlugins, playerID: PlayerID | null) => any;
+ endIf?: (context: FnContext) => any;
+ onEnd?: (context: FnContext) => void | G;
+ playerView?: (context: { G: G; ctx: Ctx; playerID: PlayerID | null }) => any;
plugins?: Array>;
ai?: {
enumerate: (
@@ -342,9 +355,9 @@ export interface Game<
>;
};
processMove?: (
- state: State,
+ state: State,
action: ActionPayload.MakeMove
- ) => State | typeof INVALID_MOVE;
+ ) => State | typeof INVALID_MOVE;
flow?: ReturnType;
}