UNPKG

boardgame.io

Version:
238 lines (217 loc) 5.87 kB
/* * Copyright 2018 The boardgame.io Authors * * Use of this source code is governed by a MIT-style * license that can be found in the LICENSE file or at * https://opensource.org/licenses/MIT. */ import PluginImmer from './plugin-immer'; import PluginRandom from './plugin-random'; import PluginEvents from './plugin-events'; import { AnyFn, PartialGameState, State, Game, Plugin, Ctx, ActionShape, PlayerID, } from '../types'; interface PluginOpts { game: Game; isClient?: boolean; } /** * List of plugins that are always added. */ const DEFAULT_PLUGINS = [PluginImmer, PluginRandom, PluginEvents]; /** * Allow plugins to intercept actions and process them. */ export const ProcessAction = ( state: State, action: ActionShape.Plugin, opts: PluginOpts ): State => { opts.game.plugins .filter(plugin => plugin.action !== undefined) .filter(plugin => plugin.name === action.payload.type) .forEach(plugin => { const name = plugin.name; const pluginState = state.plugins[name] || { data: {} }; const data = plugin.action(pluginState.data, action.payload); state = { ...state, plugins: { ...state.plugins, [name]: { ...pluginState, data }, }, }; }); return state; }; /** * The API's created by various plugins are stored in the plugins * section of the state object: * * { * G: {}, * ctx: {}, * plugins: { * plugin-a: { * data: {}, // this is generated by the plugin at Setup / Flush. * api: {}, // this is ephemeral and generated by Enhance. * } * } * } * * This function takes these API's and stuffs them back into * ctx for consumption inside a move function or hook. */ export const EnhanceCtx = (state: PartialGameState): Ctx => { let ctx = { ...state.ctx }; const plugins = state.plugins || {}; Object.entries(plugins).forEach(([name, { api }]) => { ctx[name] = api; }); return ctx; }; /** * Applies the provided plugins to the given move / flow function. * * @param {function} fn - The move function or trigger to apply the plugins to. * @param {object} plugins - The list of plugins. */ export const FnWrap = (fn: AnyFn, plugins: Plugin[]) => { const reducer = (acc: AnyFn, { fnWrap }: Plugin) => fnWrap(acc); return [...DEFAULT_PLUGINS, ...plugins] .filter(plugin => plugin.fnWrap !== undefined) .reduce(reducer, fn); }; /** * Allows the plugin to generate its initial state. */ export const Setup = ( state: PartialGameState, opts: PluginOpts ): PartialGameState => { [...DEFAULT_PLUGINS, ...opts.game.plugins] .filter(plugin => plugin.setup !== undefined) .forEach(plugin => { const name = plugin.name; const data = plugin.setup({ G: state.G, ctx: state.ctx, game: opts.game, }); state = { ...state, plugins: { ...state.plugins, [name]: { data }, }, }; }); return state; }; /** * Invokes the plugin before a move or event. * The API that the plugin generates is stored inside * the `plugins` section of the state (which is subsequently * merged into ctx). */ export const Enhance = ( state: State, opts: PluginOpts & { playerID: PlayerID } ): State => { [...DEFAULT_PLUGINS, ...opts.game.plugins] .filter(plugin => plugin.api !== undefined) .forEach(plugin => { const name = plugin.name; const pluginState = state.plugins[name] || { data: {} }; const api = plugin.api({ G: state.G, ctx: state.ctx, data: pluginState.data, game: opts.game, playerID: opts.playerID, }); state = { ...state, plugins: { ...state.plugins, [name]: { ...pluginState, api }, }, }; }); return state; }; /** * Allows plugins to update their state after a move / event. */ export const Flush = (state: State, opts: PluginOpts): State => { // Note that we flush plugins in reverse order, to make sure that plugins // that come before in the chain are still available. [...DEFAULT_PLUGINS, ...opts.game.plugins].reverse().forEach(plugin => { const name = plugin.name; const pluginState = state.plugins[name] || { data: {} }; if (plugin.flush) { const newData = plugin.flush({ G: state.G, ctx: state.ctx, game: opts.game, api: pluginState.api, data: pluginState.data, }); state = { ...state, plugins: { ...state.plugins, [plugin.name]: { data: newData }, }, }; } else if (plugin.dangerouslyFlushRawState) { state = plugin.dangerouslyFlushRawState({ state, game: opts.game, api: pluginState.api, data: pluginState.data, }); // Remove everything other than data. const data = state.plugins[name].data; state = { ...state, plugins: { ...state.plugins, [plugin.name]: { data }, }, }; } }); return state; }; /** * Allows plugins to indicate if they should not be materialized on the client. * This will cause the client to discard the state update and wait for the * master instead. */ export const NoClient = (state: State, opts: PluginOpts): boolean => { return [...DEFAULT_PLUGINS, ...opts.game.plugins] .filter(plugin => plugin.noClient !== undefined) .map(plugin => { const name = plugin.name; const pluginState = state.plugins[name]; if (pluginState) { return plugin.noClient({ G: state.G, ctx: state.ctx, game: opts.game, api: pluginState.api, data: pluginState.data, }); } return false; }) .some(value => value === true); };