UNPKG

boardgame.io

Version:
607 lines (525 loc) 16.1 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var reducer = require('./reducer-d2994695.js'); var base = require('./base-bdd9c13b.js'); var master = require('./master-b42e812a.js'); var io = _interopDefault(require('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 base.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 game. */ createGame(gameID, opts) { this.initial.set(gameID, opts.initialState); this.setState(gameID, opts.initialState); this.setMetadata(gameID, opts.metadata); } /** * Write the game metadata to the in-memory object. */ setMetadata(gameID, metadata) { this.metadata.set(gameID, metadata); } /** * Write the game state to the in-memory object. */ setState(gameID, state, deltalog) { if (deltalog && deltalog.length > 0) { const log = this.log.get(gameID) || []; this.log.set(gameID, log.concat(deltalog)); } this.state.set(gameID, state); } /** * Fetches state for a particular gameID. */ fetch(gameID, opts) { let result = {}; if (opts.state) { result.state = this.state.get(gameID); } if (opts.metadata) { result.metadata = this.metadata.get(gameID); } if (opts.log) { result.log = this.log.get(gameID) || []; } if (opts.initialState) { result.initialState = this.initial.get(gameID); } return result; } /** * Remove the game state from the in-memory object. */ wipe(gameID) { this.state.delete(gameID); this.metadata.delete(gameID); } /** * Return all keys. */ listGames(opts) { if (opts && opts.gameName !== undefined) { let gameIDs = []; this.metadata.forEach((metadata, gameID) => { if (metadata.gameName === opts.gameName) { gameIDs.push(gameID); } }); return gameIDs; } return [...this.metadata.keys()]; } } /* * 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. */ var Transport = function Transport(_ref) { var store = _ref.store, gameName = _ref.gameName, playerID = _ref.playerID, gameID = _ref.gameID, numPlayers = _ref.numPlayers; reducer._classCallCheck(this, Transport); this.store = store; this.gameName = gameName || 'default'; this.playerID = playerID || null; this.gameID = gameID || 'default'; this.numPlayers = numPlayers || 2; }; /** * 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.stage) { for (var _i = 0, _Object$keys = Object.keys(bots); _i < _Object$keys.length; _i++) { var key = _Object$keys[_i]; if (key in state.ctx.stage) { 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. */ function LocalMaster(_ref) { var game = _ref.game, bots = _ref.bots; var clientCallbacks = {}; var initializedBots = {}; if (game && game.ai && bots) { for (var playerID in bots) { var bot = bots[playerID]; initializedBots[playerID] = new bot({ game: game, enumerate: game.ai.enumerate, seed: game.seed }); } } var send = function send(_ref2) { var type = _ref2.type, playerID = _ref2.playerID, args = _ref2.args; var callback = clientCallbacks[playerID]; if (callback !== undefined) { callback.apply(null, [type].concat(reducer._toConsumableArray(args))); } }; var sendAll = function sendAll(arg) { for (var _playerID in clientCallbacks) { var _arg = arg(_playerID), type = _arg.type, args = _arg.args; send({ type: type, playerID: _playerID, args: args }); } }; var master$1 = new master.Master(game, new InMemory(), { send: send, sendAll: sendAll }, false); master$1.connect = function (gameID, playerID, callback) { clientCallbacks[playerID] = callback; }; master$1.subscribe(function (_ref3) { var state = _ref3.state, gameID = _ref3.gameID; if (!bots) { return; } var botPlayer = GetBotPlayer(state, initializedBots); if (botPlayer !== null) { setTimeout(async function () { var botAction = await initializedBots[botPlayer].play(state, botPlayer); await master$1.onUpdate(botAction.action, state._stateID, gameID, botAction.action.payload.playerID); }, 100); } }); return master$1; } /** * Local * * Transport interface that embeds a GameMaster within it * that you can connect multiple clients to. */ var LocalTransport = /*#__PURE__*/ function (_Transport) { reducer._inherits(LocalTransport, _Transport); /** * Creates a new Mutiplayer instance. * @param {object} socket - Override for unit tests. * @param {object} socketOpts - Options to pass to socket.io. * @param {string} gameID - 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. * @param {string} server - The game server in the form of 'hostname:port'. Defaults to the server serving the client if not provided. */ function LocalTransport(_ref4) { var _this; var master = _ref4.master, game = _ref4.game, store = _ref4.store, gameID = _ref4.gameID, playerID = _ref4.playerID, gameName = _ref4.gameName, numPlayers = _ref4.numPlayers; reducer._classCallCheck(this, LocalTransport); _this = reducer._possibleConstructorReturn(this, reducer._getPrototypeOf(LocalTransport).call(this, { store: store, gameName: gameName, playerID: playerID, gameID: gameID, numPlayers: numPlayers })); _this.master = master; _this.game = game; _this.isConnected = true; return _this; } /** * Called when another player makes a move and the * master broadcasts the update to other clients (including * this one). */ reducer._createClass(LocalTransport, [{ key: "onUpdate", value: async function onUpdate(gameID, state, deltalog) { var currentState = this.store.getState(); if (gameID == this.gameID && state._stateID >= currentState._stateID) { var action = reducer.update(state, deltalog); this.store.dispatch(action); } } /** * Called when the client first connects to the master * and requests the current game state. */ }, { key: "onSync", value: function onSync(gameID, syncInfo) { if (gameID == this.gameID) { var action = reducer.sync(syncInfo); this.store.dispatch(action); } } /** * Called when an action that has to be relayed to the * game master is made. */ }, { key: "onAction", value: function onAction(state, action) { this.master.onUpdate(action, state._stateID, this.gameID, this.playerID); } /** * Connect to the master. */ }, { key: "connect", value: function connect() { var _this2 = this; this.master.connect(this.gameID, this.playerID, function (type) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } if (type == 'sync') { _this2.onSync.apply(_this2, args); } if (type == 'update') { _this2.onUpdate.apply(_this2, args); } }); this.master.onSync(this.gameID, this.playerID, this.numPlayers); } /** * Disconnect from the master. */ }, { key: "disconnect", value: function disconnect() {} /** * Subscribe to connection state changes. */ }, { key: "subscribe", value: function subscribe() {} }, { key: "subscribeGameMetadata", value: function subscribeGameMetadata(_metadata) {} // eslint-disable-line no-unused-vars /** * Updates the game id. * @param {string} id - The new game id. */ }, { key: "updateGameID", value: function updateGameID(id) { this.gameID = id; var action = reducer.reset(null); this.store.dispatch(action); this.connect(); } /** * Updates the player associated with this client. * @param {string} id - The new player id. */ }, { key: "updatePlayerID", value: function updatePlayerID(id) { this.playerID = id; var action = reducer.reset(null); this.store.dispatch(action); this.connect(); } }]); return LocalTransport; }(Transport); var localMasters = new Map(); function Local(opts) { return function (transportOpts) { var master; if (localMasters.has(transportOpts.gameKey) & !opts) { master = localMasters.get(transportOpts.gameKey); } else { master = new LocalMaster({ game: transportOpts.game, bots: opts && opts.bots }); localMasters.set(transportOpts.gameKey, master); } return new LocalTransport(reducer._objectSpread2({ master: master }, transportOpts)); }; } /** * SocketIO * * Transport interface that interacts with the Master via socket.io. */ var SocketIOTransport = /*#__PURE__*/ function (_Transport) { reducer._inherits(SocketIOTransport, _Transport); /** * Creates a new Mutiplayer instance. * @param {object} socket - Override for unit tests. * @param {object} socketOpts - Options to pass to socket.io. * @param {string} gameID - 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. * @param {string} server - The game server in the form of 'hostname:port'. Defaults to the server serving the client if not provided. */ function SocketIOTransport() { var _this; var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, socket = _ref.socket, socketOpts = _ref.socketOpts, store = _ref.store, gameID = _ref.gameID, playerID = _ref.playerID, gameName = _ref.gameName, numPlayers = _ref.numPlayers, server = _ref.server; reducer._classCallCheck(this, SocketIOTransport); _this = reducer._possibleConstructorReturn(this, reducer._getPrototypeOf(SocketIOTransport).call(this, { store: store, gameName: gameName, playerID: playerID, gameID: gameID, numPlayers: numPlayers })); _this.server = server; _this.socket = socket; _this.socketOpts = socketOpts; _this.isConnected = false; _this.callback = function () {}; _this.gameMetadataCallback = function () {}; return _this; } /** * Called when an action that has to be relayed to the * game master is made. */ reducer._createClass(SocketIOTransport, [{ key: "onAction", value: function onAction(state, action) { this.socket.emit('update', action, state._stateID, this.gameID, this.playerID); } /** * Connect to the server. */ }, { key: "connect", value: function connect() { var _this2 = this; if (!this.socket) { if (this.server) { var server = this.server; if (server.search(/^https?:\/\//) == -1) { server = 'http://' + this.server; } if (server.substr(-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 to other clients (including // this one). this.socket.on('update', function (gameID, state, deltalog) { var currentState = _this2.store.getState(); if (gameID == _this2.gameID && state._stateID >= currentState._stateID) { var action = reducer.update(state, deltalog); _this2.store.dispatch(action); } }); // Called when the client first connects to the master // and requests the current game state. this.socket.on('sync', function (gameID, syncInfo) { if (gameID == _this2.gameID) { var action = reducer.sync(syncInfo); _this2.gameMetadataCallback(syncInfo.filteredMetadata); _this2.store.dispatch(action); } }); // Initial sync to get game state. this.socket.emit('sync', this.gameID, this.playerID, this.numPlayers); // Keep track of connection status. this.socket.on('connect', function () { _this2.isConnected = true; _this2.callback(); }); this.socket.on('disconnect', function () { _this2.isConnected = false; _this2.callback(); }); } /** * Disconnect from the server. */ }, { key: "disconnect", value: function disconnect() { this.socket.close(); this.socket = null; this.isConnected = false; this.callback(); } /** * Subscribe to connection state changes. */ }, { key: "subscribe", value: function subscribe(fn) { this.callback = fn; } }, { key: "subscribeGameMetadata", value: function subscribeGameMetadata(fn) { this.gameMetadataCallback = fn; } /** * Updates the game id. * @param {string} id - The new game id. */ }, { key: "updateGameID", value: function updateGameID(id) { this.gameID = id; var action = reducer.reset(null); this.store.dispatch(action); if (this.socket) { this.socket.emit('sync', this.gameID, this.playerID, this.numPlayers); } } /** * Updates the player associated with this client. * @param {string} id - The new player id. */ }, { key: "updatePlayerID", value: function updatePlayerID(id) { this.playerID = id; var action = reducer.reset(null); this.store.dispatch(action); if (this.socket) { this.socket.emit('sync', this.gameID, this.playerID, this.numPlayers); } } }]); return SocketIOTransport; }(Transport); function SocketIO() { var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, server = _ref2.server, socketOpts = _ref2.socketOpts; return function (transportOpts) { return new SocketIOTransport(reducer._objectSpread2({ server: server, socketOpts: socketOpts }, transportOpts)); }; } exports.Local = Local; exports.SocketIO = SocketIO;