boardgame.io
Version:
library for turn-based games
724 lines (713 loc) • 30.2 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
require('nanoid/non-secure');
require('./Debug-710a6cb3.js');
require('redux');
require('./turn-order-4ab12333.js');
require('immer');
require('./plugin-random-7425844d.js');
require('lodash.isplainobject');
require('./reducer-6f7cf6b0.js');
require('rfc6902');
require('./initialize-648ccd94.js');
require('./transport-b1874dfa.js');
var client = require('./client-dde37916.js');
require('flatted');
require('setimmediate');
var ai = require('./ai-e933e60d.js');
var client$1 = require('./client-76dec77b.js');
var React = _interopDefault(require('react'));
var PropTypes = _interopDefault(require('prop-types'));
var Cookies = _interopDefault(require('react-cookies'));
require('./util-abef9b9f.js');
var socketio = require('./socketio-638b66b8.js');
require('./master-9bf9c1d4.js');
require('./filter-player-view-a8eeb11e.js');
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.
*/
/**
* Client
*
* boardgame.io React client.
*
* @param {...object} game - The return value of `Game`.
* @param {...object} numPlayers - The number of players.
* @param {...object} board - The React component for the game.
* @param {...object} loading - (optional) The React component for the loading state.
* @param {...object} multiplayer - Set to a falsy value or a transportFactory, e.g., SocketIO()
* @param {...object} debug - Enables the Debug UI.
* @param {...object} enhancer - Optional enhancer to send to the Redux store
*
* Returns:
* A React component that wraps board and provides an
* API through props for it to interact with the framework
* and dispatch actions such as MAKE_MOVE, GAME_EVENT, RESET,
* UNDO and REDO.
*/
function Client(opts) {
var _a;
const { game, numPlayers, board, multiplayer, enhancer } = opts;
let { loading, debug } = opts;
// Component that is displayed before the client has synced
// with the game master.
if (loading === undefined) {
const Loading = () => React.createElement("div", { className: "bgio-loading" }, "connecting...");
loading = Loading;
}
/*
* WrappedBoard
*
* The main React component that wraps the passed in
* board component and adds the API to its props.
*/
return _a = class WrappedBoard extends React.Component {
constructor(props) {
super(props);
if (debug === undefined) {
debug = props.debug;
}
this.client = client.Client({
game,
debug,
numPlayers,
multiplayer,
matchID: props.matchID,
playerID: props.playerID,
credentials: props.credentials,
enhancer,
});
}
componentDidMount() {
this.unsubscribe = this.client.subscribe(() => this.forceUpdate());
this.client.start();
}
componentWillUnmount() {
this.client.stop();
this.unsubscribe();
}
componentDidUpdate(prevProps) {
if (this.props.matchID != prevProps.matchID) {
this.client.updateMatchID(this.props.matchID);
}
if (this.props.playerID != prevProps.playerID) {
this.client.updatePlayerID(this.props.playerID);
}
if (this.props.credentials != prevProps.credentials) {
this.client.updateCredentials(this.props.credentials);
}
}
render() {
const state = this.client.getState();
if (state === null) {
return React.createElement(loading);
}
let _board = null;
if (board) {
_board = React.createElement(board, {
...state,
...this.props,
isMultiplayer: !!multiplayer,
moves: this.client.moves,
events: this.client.events,
matchID: this.client.matchID,
playerID: this.client.playerID,
reset: this.client.reset,
undo: this.client.undo,
redo: this.client.redo,
log: this.client.log,
matchData: this.client.matchData,
sendChatMessage: this.client.sendChatMessage,
chatMessages: this.client.chatMessages,
});
}
return React.createElement("div", { className: "bgio-client" }, _board);
}
},
_a.propTypes = {
// The ID of a game to connect to.
// Only relevant in multiplayer.
matchID: PropTypes.string,
// The ID of the player associated with this client.
// Only relevant in multiplayer.
playerID: PropTypes.string,
// This client's authentication credentials.
// Only relevant in multiplayer.
credentials: PropTypes.string,
// Enable / disable the Debug UI.
debug: PropTypes.any,
},
_a.defaultProps = {
matchID: 'default',
playerID: null,
credentials: null,
debug: true,
},
_a;
}
/*
* 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.
*/
class _LobbyConnectionImpl {
constructor({ server, gameComponents, playerName, playerCredentials, }) {
this.client = new client$1.LobbyClient({ server });
this.gameComponents = gameComponents;
this.playerName = playerName || 'Visitor';
this.playerCredentials = playerCredentials;
this.matches = [];
}
async refresh() {
try {
this.matches = [];
const games = await this.client.listGames();
for (const game of games) {
if (!this._getGameComponents(game))
continue;
const { matches } = await this.client.listMatches(game);
this.matches.push(...matches);
}
}
catch (error) {
throw new Error('failed to retrieve list of matches (' + error + ')');
}
}
_getMatchInstance(matchID) {
for (const inst of this.matches) {
if (inst['matchID'] === matchID)
return inst;
}
}
_getGameComponents(gameName) {
for (const comp of this.gameComponents) {
if (comp.game.name === gameName)
return comp;
}
}
_findPlayer(playerName) {
for (const inst of this.matches) {
if (inst.players.some((player) => player.name === playerName))
return inst;
}
}
async join(gameName, matchID, playerID) {
try {
let inst = this._findPlayer(this.playerName);
if (inst) {
throw new Error('player has already joined ' + inst.matchID);
}
inst = this._getMatchInstance(matchID);
if (!inst) {
throw new Error('game instance ' + matchID + ' not found');
}
const json = await this.client.joinMatch(gameName, matchID, {
playerID,
playerName: this.playerName,
});
inst.players[Number.parseInt(playerID)].name = this.playerName;
this.playerCredentials = json.playerCredentials;
}
catch (error) {
throw new Error('failed to join match ' + matchID + ' (' + error + ')');
}
}
async leave(gameName, matchID) {
try {
const inst = this._getMatchInstance(matchID);
if (!inst)
throw new Error('match instance not found');
for (const player of inst.players) {
if (player.name === this.playerName) {
await this.client.leaveMatch(gameName, matchID, {
playerID: player.id.toString(),
credentials: this.playerCredentials,
});
delete player.name;
delete this.playerCredentials;
return;
}
}
throw new Error('player not found in match');
}
catch (error) {
throw new Error('failed to leave match ' + matchID + ' (' + error + ')');
}
}
async disconnect() {
const inst = this._findPlayer(this.playerName);
if (inst) {
await this.leave(inst.gameName, inst.matchID);
}
this.matches = [];
this.playerName = 'Visitor';
}
async create(gameName, numPlayers) {
try {
const comp = this._getGameComponents(gameName);
if (!comp)
throw new Error('game not found');
if (numPlayers < comp.game.minPlayers ||
numPlayers > comp.game.maxPlayers)
throw new Error('invalid number of players ' + numPlayers);
await this.client.createMatch(gameName, { numPlayers });
}
catch (error) {
throw new Error('failed to create match for ' + gameName + ' (' + error + ')');
}
}
}
/**
* LobbyConnection
*
* Lobby model.
*
* @param {string} server - '<host>:<port>' of the server.
* @param {Array} gameComponents - A map of Board and Game objects for the supported games.
* @param {string} playerName - The name of the player.
* @param {string} playerCredentials - The credentials currently used by the player, if any.
*
* Returns:
* A JS object that synchronizes the list of running game instances with the server and provides an API to create/join/start instances.
*/
function LobbyConnection(opts) {
return new _LobbyConnectionImpl(opts);
}
/*
* 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.
*/
class LobbyLoginForm extends React.Component {
constructor() {
super(...arguments);
this.state = {
playerName: this.props.playerName,
nameErrorMsg: '',
};
this.onClickEnter = () => {
if (this.state.playerName === '')
return;
this.props.onEnter(this.state.playerName);
};
this.onKeyPress = (event) => {
if (event.key === 'Enter') {
this.onClickEnter();
}
};
this.onChangePlayerName = (event) => {
const name = event.target.value.trim();
this.setState({
playerName: name,
nameErrorMsg: name.length > 0 ? '' : 'empty player name',
});
};
}
render() {
return (React.createElement("div", null,
React.createElement("p", { className: "phase-title" }, "Choose a player name:"),
React.createElement("input", { type: "text", value: this.state.playerName, onChange: this.onChangePlayerName, onKeyPress: this.onKeyPress }),
React.createElement("span", { className: "buttons" },
React.createElement("button", { className: "buttons", onClick: this.onClickEnter }, "Enter")),
React.createElement("br", null),
React.createElement("span", { className: "error-msg" },
this.state.nameErrorMsg,
React.createElement("br", null))));
}
}
LobbyLoginForm.defaultProps = {
playerName: '',
};
/*
* 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.
*/
class LobbyMatchInstance extends React.Component {
constructor() {
super(...arguments);
this._createSeat = (player) => {
return player.name || '[free]';
};
this._createButtonJoin = (inst, seatId) => (React.createElement("button", { key: 'button-join-' + inst.matchID, onClick: () => this.props.onClickJoin(inst.gameName, inst.matchID, '' + seatId) }, "Join"));
this._createButtonLeave = (inst) => (React.createElement("button", { key: 'button-leave-' + inst.matchID, onClick: () => this.props.onClickLeave(inst.gameName, inst.matchID) }, "Leave"));
this._createButtonPlay = (inst, seatId) => (React.createElement("button", { key: 'button-play-' + inst.matchID, onClick: () => this.props.onClickPlay(inst.gameName, {
matchID: inst.matchID,
playerID: '' + seatId,
numPlayers: inst.players.length,
}) }, "Play"));
this._createButtonSpectate = (inst) => (React.createElement("button", { key: 'button-spectate-' + inst.matchID, onClick: () => this.props.onClickPlay(inst.gameName, {
matchID: inst.matchID,
numPlayers: inst.players.length,
}) }, "Spectate"));
this._createInstanceButtons = (inst) => {
const playerSeat = inst.players.find((player) => player.name === this.props.playerName);
const freeSeat = inst.players.find((player) => !player.name);
if (playerSeat && freeSeat) {
// already seated: waiting for match to start
return this._createButtonLeave(inst);
}
if (freeSeat) {
// at least 1 seat is available
return this._createButtonJoin(inst, freeSeat.id);
}
// match is full
if (playerSeat) {
return (React.createElement("div", null, [
this._createButtonPlay(inst, playerSeat.id),
this._createButtonLeave(inst),
]));
}
// allow spectating
return this._createButtonSpectate(inst);
};
}
render() {
const match = this.props.match;
let status = 'OPEN';
if (!match.players.some((player) => !player.name)) {
status = 'RUNNING';
}
return (React.createElement("tr", { key: 'line-' + match.matchID },
React.createElement("td", { key: 'cell-name-' + match.matchID }, match.gameName),
React.createElement("td", { key: 'cell-status-' + match.matchID }, status),
React.createElement("td", { key: 'cell-seats-' + match.matchID }, match.players.map((player) => this._createSeat(player)).join(', ')),
React.createElement("td", { key: 'cell-buttons-' + match.matchID }, this._createInstanceButtons(match))));
}
}
/*
* 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.
*/
class LobbyCreateMatchForm extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedGame: 0,
numPlayers: 2,
};
this._createGameNameOption = (game, idx) => {
return (React.createElement("option", { key: 'name-option-' + idx, value: idx }, game.game.name));
};
this._createNumPlayersOption = (idx) => {
return (React.createElement("option", { key: 'num-option-' + idx, value: idx }, idx));
};
this._createNumPlayersRange = (game) => {
return Array.from({ length: game.maxPlayers + 1 })
.map((_, i) => i)
.slice(game.minPlayers);
};
this.onChangeNumPlayers = (event) => {
this.setState({
numPlayers: Number.parseInt(event.target.value),
});
};
this.onChangeSelectedGame = (event) => {
const idx = Number.parseInt(event.target.value);
this.setState({
selectedGame: idx,
numPlayers: this.props.games[idx].game.minPlayers,
});
};
this.onClickCreate = () => {
this.props.createMatch(this.props.games[this.state.selectedGame].game.name, this.state.numPlayers);
};
/* fix min and max number of players */
for (const game of props.games) {
const matchDetails = game.game;
if (!matchDetails.minPlayers) {
matchDetails.minPlayers = 1;
}
if (!matchDetails.maxPlayers) {
matchDetails.maxPlayers = 4;
}
console.assert(matchDetails.maxPlayers >= matchDetails.minPlayers);
}
this.state = {
selectedGame: 0,
numPlayers: props.games[0].game.minPlayers,
};
}
render() {
return (React.createElement("div", null,
React.createElement("select", { value: this.state.selectedGame, onChange: (evt) => this.onChangeSelectedGame(evt) }, this.props.games.map((game, index) => this._createGameNameOption(game, index))),
React.createElement("span", null, "Players:"),
React.createElement("select", { value: this.state.numPlayers, onChange: this.onChangeNumPlayers }, this._createNumPlayersRange(this.props.games[this.state.selectedGame].game).map((number) => this._createNumPlayersOption(number))),
React.createElement("span", { className: "buttons" },
React.createElement("button", { onClick: this.onClickCreate }, "Create"))));
}
}
/*
* 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.
*/
var LobbyPhases;
(function (LobbyPhases) {
LobbyPhases["ENTER"] = "enter";
LobbyPhases["PLAY"] = "play";
LobbyPhases["LIST"] = "list";
})(LobbyPhases || (LobbyPhases = {}));
/**
* Lobby
*
* React lobby component.
*
* @param {Array} gameComponents - An array of Board and Game objects for the supported games.
* @param {string} lobbyServer - Address of the lobby server (for example 'localhost:8000').
* If not set, defaults to the server that served the page.
* @param {string} gameServer - Address of the game server (for example 'localhost:8001').
* If not set, defaults to the server that served the page.
* @param {function} clientFactory - Function that is used to create the game clients.
* @param {number} refreshInterval - Interval between server updates (default: 2000ms).
* @param {bool} debug - Enable debug information (default: false).
*
* Returns:
* A React component that provides a UI to create, list, join, leave, play or
* spectate matches (game instances).
*/
class Lobby extends React.Component {
constructor(props) {
super(props);
this.state = {
phase: LobbyPhases.ENTER,
playerName: 'Visitor',
runningMatch: null,
errorMsg: '',
credentialStore: {},
};
this._createConnection = (props) => {
const name = this.state.playerName;
this.connection = LobbyConnection({
server: props.lobbyServer,
gameComponents: props.gameComponents,
playerName: name,
playerCredentials: this.state.credentialStore[name],
});
};
this._updateCredentials = (playerName, credentials) => {
this.setState((prevState) => {
// clone store or componentDidUpdate will not be triggered
const store = Object.assign({}, prevState.credentialStore);
store[playerName] = credentials;
return { credentialStore: store };
});
};
this._updateConnection = async () => {
await this.connection.refresh();
this.forceUpdate();
};
this._enterLobby = (playerName) => {
this._startRefreshInterval();
this.setState({ playerName, phase: LobbyPhases.LIST });
};
this._exitLobby = async () => {
this._clearRefreshInterval();
await this.connection.disconnect();
this.setState({ phase: LobbyPhases.ENTER, errorMsg: '' });
};
this._createMatch = async (gameName, numPlayers) => {
try {
await this.connection.create(gameName, numPlayers);
await this.connection.refresh();
// rerender
this.setState({});
}
catch (error) {
this.setState({ errorMsg: error.message });
}
};
this._joinMatch = async (gameName, matchID, playerID) => {
try {
await this.connection.join(gameName, matchID, playerID);
await this.connection.refresh();
this._updateCredentials(this.connection.playerName, this.connection.playerCredentials);
}
catch (error) {
this.setState({ errorMsg: error.message });
}
};
this._leaveMatch = async (gameName, matchID) => {
try {
await this.connection.leave(gameName, matchID);
await this.connection.refresh();
this._updateCredentials(this.connection.playerName, this.connection.playerCredentials);
}
catch (error) {
this.setState({ errorMsg: error.message });
}
};
this._startMatch = (gameName, matchOpts) => {
const gameCode = this.connection._getGameComponents(gameName);
if (!gameCode) {
this.setState({
errorMsg: 'game ' + gameName + ' not supported',
});
return;
}
let multiplayer = undefined;
if (matchOpts.numPlayers > 1) {
multiplayer = this.props.gameServer
? socketio.SocketIO({ server: this.props.gameServer })
: socketio.SocketIO();
}
if (matchOpts.numPlayers == 1) {
const maxPlayers = gameCode.game.maxPlayers;
const bots = {};
for (let i = 1; i < maxPlayers; i++) {
bots[i + ''] = ai.MCTSBot;
}
multiplayer = socketio.Local({ bots });
}
const app = this.props.clientFactory({
game: gameCode.game,
board: gameCode.board,
debug: this.props.debug,
multiplayer,
});
const match = {
app: app,
matchID: matchOpts.matchID,
playerID: matchOpts.numPlayers > 1 ? matchOpts.playerID : '0',
credentials: this.connection.playerCredentials,
};
this._clearRefreshInterval();
this.setState({ phase: LobbyPhases.PLAY, runningMatch: match });
};
this._exitMatch = () => {
this._startRefreshInterval();
this.setState({ phase: LobbyPhases.LIST, runningMatch: null });
};
this._getPhaseVisibility = (phase) => {
return this.state.phase !== phase ? 'hidden' : 'phase';
};
this.renderMatches = (matches, playerName) => {
return matches.map((match) => {
const { matchID, gameName, players } = match;
return (React.createElement(LobbyMatchInstance, { key: 'instance-' + matchID, match: { matchID, gameName, players: Object.values(players) }, playerName: playerName, onClickJoin: this._joinMatch, onClickLeave: this._leaveMatch, onClickPlay: this._startMatch }));
});
};
this._createConnection(this.props);
}
componentDidMount() {
const cookie = Cookies.load('lobbyState') || {};
if (cookie.phase && cookie.phase === LobbyPhases.PLAY) {
cookie.phase = LobbyPhases.LIST;
}
if (cookie.phase && cookie.phase !== LobbyPhases.ENTER) {
this._startRefreshInterval();
}
this.setState({
phase: cookie.phase || LobbyPhases.ENTER,
playerName: cookie.playerName || 'Visitor',
credentialStore: cookie.credentialStore || {},
});
}
componentDidUpdate(prevProps, prevState) {
const name = this.state.playerName;
const creds = this.state.credentialStore[name];
if (prevState.phase !== this.state.phase ||
prevState.credentialStore[name] !== creds ||
prevState.playerName !== name) {
this._createConnection(this.props);
this._updateConnection();
const cookie = {
phase: this.state.phase,
playerName: name,
credentialStore: this.state.credentialStore,
};
Cookies.save('lobbyState', cookie, { path: '/' });
}
if (prevProps.refreshInterval !== this.props.refreshInterval) {
this._startRefreshInterval();
}
}
componentWillUnmount() {
this._clearRefreshInterval();
}
_startRefreshInterval() {
this._clearRefreshInterval();
this._currentInterval = setInterval(this._updateConnection, this.props.refreshInterval);
}
_clearRefreshInterval() {
clearInterval(this._currentInterval);
}
render() {
const { gameComponents, renderer } = this.props;
const { errorMsg, playerName, phase, runningMatch } = this.state;
if (renderer) {
return renderer({
errorMsg,
gameComponents,
matches: this.connection.matches,
phase,
playerName,
runningMatch,
handleEnterLobby: this._enterLobby,
handleExitLobby: this._exitLobby,
handleCreateMatch: this._createMatch,
handleJoinMatch: this._joinMatch,
handleLeaveMatch: this._leaveMatch,
handleExitMatch: this._exitMatch,
handleRefreshMatches: this._updateConnection,
handleStartMatch: this._startMatch,
});
}
return (React.createElement("div", { id: "lobby-view", style: { padding: 50 } },
React.createElement("div", { className: this._getPhaseVisibility(LobbyPhases.ENTER) },
React.createElement(LobbyLoginForm, { key: playerName, playerName: playerName, onEnter: this._enterLobby })),
React.createElement("div", { className: this._getPhaseVisibility(LobbyPhases.LIST) },
React.createElement("p", null,
"Welcome, ",
playerName),
React.createElement("div", { className: "phase-title", id: "match-creation" },
React.createElement("span", null, "Create a match:"),
React.createElement(LobbyCreateMatchForm, { games: gameComponents, createMatch: this._createMatch })),
React.createElement("p", { className: "phase-title" }, "Join a match:"),
React.createElement("div", { id: "instances" },
React.createElement("table", null,
React.createElement("tbody", null, this.renderMatches(this.connection.matches, playerName))),
React.createElement("span", { className: "error-msg" },
errorMsg,
React.createElement("br", null))),
React.createElement("p", { className: "phase-title" }, "Matches that become empty are automatically deleted.")),
React.createElement("div", { className: this._getPhaseVisibility(LobbyPhases.PLAY) },
runningMatch && (React.createElement(runningMatch.app, { matchID: runningMatch.matchID, playerID: runningMatch.playerID, credentials: runningMatch.credentials })),
React.createElement("div", { className: "buttons", id: "match-exit" },
React.createElement("button", { onClick: this._exitMatch }, "Exit match"))),
React.createElement("div", { className: "buttons", id: "lobby-exit" },
React.createElement("button", { onClick: this._exitLobby }, "Exit lobby"))));
}
}
Lobby.propTypes = {
gameComponents: PropTypes.array.isRequired,
lobbyServer: PropTypes.string,
gameServer: PropTypes.string,
debug: PropTypes.bool,
clientFactory: PropTypes.func,
refreshInterval: PropTypes.number,
};
Lobby.defaultProps = {
debug: false,
clientFactory: Client,
refreshInterval: 2000,
};
exports.Client = Client;
exports.Lobby = Lobby;