UNPKG

boardgame.io

Version:
456 lines (450 loc) 14.7 kB
import { T as Transport } from './transport-ce07b771.js'; import { S as Sync } from './util-991e76bb.js'; import { M as Master } from './master-17425f07.js'; import { g as getFilterPlayerView } from './filter-player-view-43ed49b0.js'; import ioNamespace__default from 'socket.io-client'; /* * Copyright 2017 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. */ /** * InMemory data storage. */ class InMemory extends Sync { /** * Creates a new InMemory storage. */ constructor() { super(); this.state = new Map(); this.initial = new Map(); this.metadata = new Map(); this.log = new Map(); } /** * Create a new match. * * @override */ createMatch(matchID, opts) { this.initial.set(matchID, opts.initialState); this.setState(matchID, opts.initialState); this.setMetadata(matchID, opts.metadata); } /** * Write the match metadata to the in-memory object. */ setMetadata(matchID, metadata) { this.metadata.set(matchID, metadata); } /** * Write the match state to the in-memory object. */ setState(matchID, state, deltalog) { if (deltalog && deltalog.length > 0) { const log = this.log.get(matchID) || []; this.log.set(matchID, [...log, ...deltalog]); } this.state.set(matchID, state); } /** * Fetches state for a particular matchID. */ fetch(matchID, opts) { const result = {}; if (opts.state) { result.state = this.state.get(matchID); } if (opts.metadata) { result.metadata = this.metadata.get(matchID); } if (opts.log) { result.log = this.log.get(matchID) || []; } if (opts.initialState) { result.initialState = this.initial.get(matchID); } return result; } /** * Remove the match state from the in-memory object. */ wipe(matchID) { this.state.delete(matchID); this.metadata.delete(matchID); } /** * Return all keys. * * @override */ listMatches(opts) { return [...this.metadata.entries()] .filter(([, metadata]) => { if (!opts) { return true; } if (opts.gameName !== undefined && metadata.gameName !== opts.gameName) { return false; } if (opts.where !== undefined) { if (opts.where.isGameover !== undefined) { const isGameover = metadata.gameover !== undefined; if (isGameover !== opts.where.isGameover) { return false; } } if (opts.where.updatedBefore !== undefined && metadata.updatedAt >= opts.where.updatedBefore) { return false; } if (opts.where.updatedAfter !== undefined && metadata.updatedAt <= opts.where.updatedAfter) { return false; } } return true; }) .map(([key]) => key); } } class WithLocalStorageMap extends Map { constructor(key) { super(); this.key = key; const cache = JSON.parse(localStorage.getItem(this.key)) || []; cache.forEach((entry) => this.set(...entry)); } sync() { const entries = [...this.entries()]; localStorage.setItem(this.key, JSON.stringify(entries)); } set(key, value) { super.set(key, value); this.sync(); return this; } delete(key) { const result = super.delete(key); this.sync(); return result; } } /** * locaStorage data storage. */ class LocalStorage extends InMemory { constructor(storagePrefix = 'bgio') { super(); const StorageMap = (stateKey) => new WithLocalStorageMap(`${storagePrefix}_${stateKey}`); this.state = StorageMap('state'); this.initial = StorageMap('initial'); this.metadata = StorageMap('metadata'); this.log = StorageMap('log'); } } /* * 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. */ /** * Returns null if it is not a bot's turn. * Otherwise, returns a playerID of a bot that may play now. */ function GetBotPlayer(state, bots) { if (state.ctx.gameover !== undefined) { return null; } if (state.ctx.activePlayers) { for (const key of Object.keys(bots)) { if (key in state.ctx.activePlayers) { return key; } } } else if (state.ctx.currentPlayer in bots) { return state.ctx.currentPlayer; } return null; } /** * Creates a local version of the master that the client * can interact with. */ class LocalMaster extends Master { constructor({ game, bots, storageKey, persist }) { const clientCallbacks = {}; const initializedBots = {}; if (game && game.ai && bots) { for (const playerID in bots) { const bot = bots[playerID]; initializedBots[playerID] = new bot({ game, enumerate: game.ai.enumerate, seed: game.seed, }); } } const send = ({ playerID, ...data }) => { const callback = clientCallbacks[playerID]; if (callback !== undefined) { callback(filterPlayerView(playerID, data)); } }; const filterPlayerView = getFilterPlayerView(game); const transportAPI = { send, sendAll: (payload) => { for (const playerID in clientCallbacks) { send({ playerID, ...payload }); } }, }; const storage = persist ? new LocalStorage(storageKey) : new InMemory(); super(game, storage, transportAPI); this.connect = (playerID, callback) => { clientCallbacks[playerID] = callback; }; this.subscribe(({ state, matchID }) => { if (!bots) { return; } const botPlayer = GetBotPlayer(state, initializedBots); if (botPlayer !== null) { setTimeout(async () => { const botAction = await initializedBots[botPlayer].play(state, botPlayer); await this.onUpdate(botAction.action, state._stateID, matchID, botAction.action.payload.playerID); }, 100); } }); } } /** * Local * * Transport interface that embeds a GameMaster within it * that you can connect multiple clients to. */ class LocalTransport extends Transport { /** * Creates a new Mutiplayer instance. * @param {string} matchID - The game ID to connect to. * @param {string} playerID - The player ID associated with this client. * @param {string} gameName - The game type (the `name` field in `Game`). * @param {string} numPlayers - The number of players. */ constructor({ master, ...opts }) { super(opts); this.master = master; } sendChatMessage(matchID, chatMessage) { const args = [ matchID, chatMessage, this.credentials, ]; this.master.onChatMessage(...args); } sendAction(state, action) { this.master.onUpdate(action, state._stateID, this.matchID, this.playerID); } requestSync() { this.master.onSync(this.matchID, this.playerID, this.credentials, this.numPlayers); } connect() { this.setConnectionStatus(true); this.master.connect(this.playerID, (data) => this.notifyClient(data)); this.requestSync(); } disconnect() { this.setConnectionStatus(false); } updateMatchID(id) { this.matchID = id; this.connect(); } updatePlayerID(id) { this.playerID = id; this.connect(); } updateCredentials(credentials) { this.credentials = credentials; this.connect(); } } /** * Global map storing local master instances. */ const localMasters = new Map(); /** * Create a local transport. */ function Local({ bots, persist, storageKey } = {}) { return (transportOpts) => { const { gameKey, game } = transportOpts; let master; const instance = localMasters.get(gameKey); if (instance && instance.bots === bots && instance.storageKey === storageKey && instance.persist === persist) { master = instance.master; } if (!master) { master = new LocalMaster({ game, bots, persist, storageKey }); localMasters.set(gameKey, { master, bots, persist, storageKey }); } return new LocalTransport({ master, ...transportOpts }); }; } /* * Copyright 2017 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. */ const io = ioNamespace__default; /** * SocketIO * * Transport interface that interacts with the Master via socket.io. */ class SocketIOTransport extends Transport { /** * Creates a new Multiplayer instance. * @param {object} socket - Override for unit tests. * @param {object} socketOpts - Options to pass to socket.io. * @param {object} store - Redux store * @param {string} matchID - The game ID to connect to. * @param {string} playerID - The player ID associated with this client. * @param {string} credentials - Authentication credentials * @param {string} gameName - The game type (the `name` field in `Game`). * @param {string} numPlayers - The number of players. * @param {string} server - The game server in the form of 'hostname:port'. Defaults to the server serving the client if not provided. */ constructor({ socket, socketOpts, server, ...opts }) { super(opts); this.server = server; this.socket = socket; this.socketOpts = socketOpts; } sendAction(state, action) { const args = [ action, state._stateID, this.matchID, this.playerID, ]; this.socket.emit('update', ...args); } sendChatMessage(matchID, chatMessage) { const args = [ matchID, chatMessage, this.credentials, ]; this.socket.emit('chat', ...args); } connect() { if (!this.socket) { if (this.server) { let server = this.server; if (server.search(/^https?:\/\//) == -1) { server = 'http://' + this.server; } if (server.slice(-1) != '/') { // add trailing slash if not already present server = server + '/'; } this.socket = io(server + this.gameName, this.socketOpts); } else { this.socket = io('/' + this.gameName, this.socketOpts); } } // Called when another player makes a move and the // master broadcasts the update as a patch to other clients (including // this one). this.socket.on('patch', (matchID, prevStateID, stateID, patch, deltalog) => { this.notifyClient({ type: 'patch', args: [matchID, prevStateID, stateID, patch, deltalog], }); }); // Called when another player makes a move and the // master broadcasts the update to other clients (including // this one). this.socket.on('update', (matchID, state, deltalog) => { this.notifyClient({ type: 'update', args: [matchID, state, deltalog], }); }); // Called when the client first connects to the master // and requests the current game state. this.socket.on('sync', (matchID, syncInfo) => { this.notifyClient({ type: 'sync', args: [matchID, syncInfo] }); }); // Called when new player joins the match or changes // it's connection status this.socket.on('matchData', (matchID, matchData) => { this.notifyClient({ type: 'matchData', args: [matchID, matchData] }); }); this.socket.on('chat', (matchID, chatMessage) => { this.notifyClient({ type: 'chat', args: [matchID, chatMessage] }); }); // Keep track of connection status. this.socket.on('connect', () => { // Initial sync to get game state. this.requestSync(); this.setConnectionStatus(true); }); this.socket.on('disconnect', () => { this.setConnectionStatus(false); }); } disconnect() { this.socket.close(); this.socket = null; this.setConnectionStatus(false); } requestSync() { if (this.socket) { const args = [ this.matchID, this.playerID, this.credentials, this.numPlayers, ]; this.socket.emit('sync', ...args); } } updateMatchID(id) { this.matchID = id; this.requestSync(); } updatePlayerID(id) { this.playerID = id; this.requestSync(); } updateCredentials(credentials) { this.credentials = credentials; this.requestSync(); } } function SocketIO({ server, socketOpts } = {}) { return (transportOpts) => new SocketIOTransport({ server, socketOpts, ...transportOpts, }); } export { Local as L, SocketIO as S };