boardgame.io
Version:
library for turn-based games
456 lines (450 loc) • 14.7 kB
JavaScript
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 };