boardgame.io
Version:
library for turn-based games
258 lines (228 loc) • 6.43 kB
text/typescript
/*
* 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 * as ActionCreators from '../../core/action-creators';
import { InMemory } from '../../server/db/inmemory';
import { Master, TransportAPI } from '../../master/master';
import { Transport, TransportOpts } from './transport';
import {
CredentialedActionShape,
Game,
LogEntry,
PlayerID,
State,
SyncInfo,
} from '../../types';
/**
* Returns null if it is not a bot's turn.
* Otherwise, returns a playerID of a bot that may play now.
*/
export function GetBotPlayer(state: State, bots: Record<PlayerID, any>) {
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;
}
interface LocalMasterOpts {
game: Game;
bots: Record<PlayerID, any>;
}
/**
* Creates a local version of the master that the client
* can interact with.
*/
export class LocalMaster extends Master {
connect: (
gameID: string,
playerID: PlayerID,
callback: (...args: any[]) => void
) => void;
constructor({ game, bots }: LocalMasterOpts) {
const clientCallbacks: Record<PlayerID, (...args: any[]) => void> = {};
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: TransportAPI['send'] = ({ playerID, type, args }) => {
const callback = clientCallbacks[playerID];
if (callback !== undefined) {
callback.apply(null, [type, ...args]);
}
};
const transportAPI: TransportAPI = {
send,
sendAll: makePlayerData => {
for (const playerID in clientCallbacks) {
const data = makePlayerData(playerID);
send({ playerID, ...data });
}
},
};
super(game, new InMemory(), transportAPI, false);
this.connect = (gameID, playerID, callback) => {
clientCallbacks[playerID] = callback;
};
this.subscribe(({ state, gameID }) => {
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,
gameID,
botAction.action.payload.playerID
);
}, 100);
}
});
}
}
type LocalTransportOpts = TransportOpts & {
master?: LocalMaster;
};
/**
* Local
*
* Transport interface that embeds a GameMaster within it
* that you can connect multiple clients to.
*/
export class LocalTransport extends Transport {
master: LocalMaster;
/**
* Creates a new Mutiplayer instance.
* @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.
*/
constructor({
master,
store,
gameID,
playerID,
gameName,
numPlayers,
}: LocalTransportOpts) {
super({ store, gameName, playerID, gameID, numPlayers });
this.master = master;
this.isConnected = true;
}
/**
* Called when another player makes a move and the
* master broadcasts the update to other clients (including
* this one).
*/
async onUpdate(gameID: string, state: State, deltalog: LogEntry[]) {
const currentState = this.store.getState();
if (gameID == this.gameID && state._stateID >= currentState._stateID) {
const action = ActionCreators.update(state, deltalog);
this.store.dispatch(action);
}
}
/**
* Called when the client first connects to the master
* and requests the current game state.
*/
onSync(gameID: string, syncInfo: SyncInfo) {
if (gameID == this.gameID) {
const action = ActionCreators.sync(syncInfo);
this.store.dispatch(action);
}
}
/**
* Called when an action that has to be relayed to the
* game master is made.
*/
onAction(state: State, action: CredentialedActionShape.Any) {
this.master.onUpdate(action, state._stateID, this.gameID, this.playerID);
}
/**
* Connect to the master.
*/
connect() {
this.master.connect(this.gameID, this.playerID, (type, ...args) => {
if (type == 'sync') {
this.onSync.apply(this, args);
}
if (type == 'update') {
this.onUpdate.apply(this, args);
}
});
this.master.onSync(this.gameID, this.playerID, this.numPlayers);
}
/**
* Disconnect from the master.
*/
disconnect() {}
/**
* Subscribe to connection state changes.
*/
subscribe() {}
subscribeGameMetadata() {}
/**
* Updates the game id.
* @param {string} id - The new game id.
*/
updateGameID(id: string) {
this.gameID = id;
const action = ActionCreators.reset(null);
this.store.dispatch(action);
this.connect();
}
/**
* Updates the player associated with this client.
* @param {string} id - The new player id.
*/
updatePlayerID(id: PlayerID) {
this.playerID = id;
const action = ActionCreators.reset(null);
this.store.dispatch(action);
this.connect();
}
}
const localMasters = new Map();
export function Local(opts?: Pick<LocalMasterOpts, 'bots'>) {
return (
transportOpts: Pick<LocalMasterOpts, 'game'> &
LocalTransportOpts & { gameKey: Game }
) => {
let master: LocalMaster;
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({ master, ...transportOpts });
};
}