UNPKG

boardgame.io

Version:
378 lines (371 loc) 12.3 kB
'use strict'; var turnOrder = require('./turn-order-4ab12333.js'); var pluginRandom = require('./plugin-random-7425844d.js'); var reducer = require('./reducer-6f7cf6b0.js'); require('setimmediate'); /* * 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. */ /** * Base class that bots can extend. */ class Bot { constructor({ enumerate, seed, }) { this.enumerateFn = enumerate; this.seed = seed; this.iterationCounter = 0; this._opts = {}; } addOpt({ key, range, initial, }) { this._opts[key] = { range, value: initial, }; } getOpt(key) { return this._opts[key].value; } setOpt(key, value) { if (key in this._opts) { this._opts[key].value = value; } } opts() { return this._opts; } enumerate(G, ctx, playerID) { const actions = this.enumerateFn(G, ctx, playerID); return actions.map((a) => { if ('payload' in a) { return a; } if ('move' in a) { return turnOrder.makeMove(a.move, a.args, playerID); } if ('event' in a) { return turnOrder.gameEvent(a.event, a.args, playerID); } }); } random(arg) { let number; if (this.seed !== undefined) { const seed = this.prngstate ? '' : this.seed; const rand = pluginRandom.alea(seed, this.prngstate); number = rand(); this.prngstate = rand.state(); } else { number = Math.random(); } if (arg) { if (Array.isArray(arg)) { const id = Math.floor(number * arg.length); return arg[id]; } else { return Math.floor(number * arg); } } return number; } } /* * 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. */ /** * The number of iterations to run before yielding to * the JS event loop (in async mode). */ const CHUNK_SIZE = 25; /** * Bot that uses Monte-Carlo Tree Search to find promising moves. */ class MCTSBot extends Bot { constructor({ enumerate, seed, objectives, game, iterations, playoutDepth, iterationCallback, }) { super({ enumerate, seed }); if (objectives === undefined) { objectives = () => ({}); } this.objectives = objectives; this.iterationCallback = iterationCallback || (() => { }); this.reducer = reducer.CreateGameReducer({ game }); this.iterations = iterations; this.playoutDepth = playoutDepth; this.addOpt({ key: 'async', initial: false, }); this.addOpt({ key: 'iterations', initial: typeof iterations === 'number' ? iterations : 1000, range: { min: 1, max: 2000 }, }); this.addOpt({ key: 'playoutDepth', initial: typeof playoutDepth === 'number' ? playoutDepth : 50, range: { min: 1, max: 100 }, }); } createNode({ state, parentAction, parent, playerID, }) { const { G, ctx } = state; let actions = []; let objectives = []; if (playerID !== undefined) { actions = this.enumerate(G, ctx, playerID); objectives = this.objectives(G, ctx, playerID); } else if (ctx.activePlayers) { for (const playerID in ctx.activePlayers) { actions.push(...this.enumerate(G, ctx, playerID)); objectives.push(this.objectives(G, ctx, playerID)); } } else { actions = this.enumerate(G, ctx, ctx.currentPlayer); objectives = this.objectives(G, ctx, ctx.currentPlayer); } return { state, parent, parentAction, actions, objectives, children: [], visits: 0, value: 0, }; } select(node) { // This node has unvisited children. if (node.actions.length > 0) { return node; } // This is a terminal node. if (node.children.length === 0) { return node; } let selectedChild = null; let best = 0; for (const child of node.children) { const childVisits = child.visits + Number.EPSILON; const uct = child.value / childVisits + Math.sqrt((2 * Math.log(node.visits)) / childVisits); if (selectedChild == null || uct > best) { best = uct; selectedChild = child; } } return this.select(selectedChild); } expand(node) { const actions = node.actions; if (actions.length === 0 || node.state.ctx.gameover !== undefined) { return node; } const id = this.random(actions.length); const action = actions[id]; node.actions.splice(id, 1); const childState = this.reducer(node.state, action); const childNode = this.createNode({ state: childState, parentAction: action, parent: node, }); node.children.push(childNode); return childNode; } playout({ state }) { let playoutDepth = this.getOpt('playoutDepth'); if (typeof this.playoutDepth === 'function') { playoutDepth = this.playoutDepth(state.G, state.ctx); } for (let i = 0; i < playoutDepth && state.ctx.gameover === undefined; i++) { const { G, ctx } = state; let playerID = ctx.currentPlayer; if (ctx.activePlayers) { playerID = Object.keys(ctx.activePlayers)[0]; } const moves = this.enumerate(G, ctx, playerID); // Check if any objectives are met. const objectives = this.objectives(G, ctx, playerID); const score = Object.keys(objectives).reduce((score, key) => { const objective = objectives[key]; if (objective.checker(G, ctx)) { return score + objective.weight; } return score; }, 0); // If so, stop and return the score. if (score > 0) { return { score }; } if (!moves || moves.length === 0) { return undefined; } const id = this.random(moves.length); const childState = this.reducer(state, moves[id]); state = childState; } return state.ctx.gameover; } backpropagate(node, result = {}) { node.visits++; if (result.score !== undefined) { node.value += result.score; } if (result.draw === true) { node.value += 0.5; } if (node.parentAction && result.winner === node.parentAction.payload.playerID) { node.value++; } if (node.parent) { this.backpropagate(node.parent, result); } } play(state, playerID) { const root = this.createNode({ state, playerID }); let numIterations = this.getOpt('iterations'); if (typeof this.iterations === 'function') { numIterations = this.iterations(state.G, state.ctx); } const getResult = () => { let selectedChild = null; for (const child of root.children) { if (selectedChild == null || child.visits > selectedChild.visits) { selectedChild = child; } } const action = selectedChild && selectedChild.parentAction; const metadata = root; return { action, metadata }; }; return new Promise((resolve) => { const iteration = () => { for (let i = 0; i < CHUNK_SIZE && this.iterationCounter < numIterations; i++) { const leaf = this.select(root); const child = this.expand(leaf); const result = this.playout(child); this.backpropagate(child, result); this.iterationCounter++; } this.iterationCallback({ iterationCounter: this.iterationCounter, numIterations, metadata: root, }); }; this.iterationCounter = 0; if (this.getOpt('async')) { const asyncIteration = () => { if (this.iterationCounter < numIterations) { iteration(); setImmediate(asyncIteration); } else { resolve(getResult()); } }; asyncIteration(); } else { while (this.iterationCounter < numIterations) { iteration(); } resolve(getResult()); } }); } } /* * 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. */ /** * Bot that picks a move at random. */ class RandomBot extends Bot { play({ G, ctx }, playerID) { const moves = this.enumerate(G, ctx, playerID); return Promise.resolve({ action: this.random(moves) }); } } /* * 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. */ /** * Make a single move on the client with a bot. * * @param {...object} client - The game client. * @param {...object} bot - The bot. */ async function Step(client, bot) { const state = client.store.getState(); let playerID = state.ctx.currentPlayer; if (state.ctx.activePlayers) { playerID = Object.keys(state.ctx.activePlayers)[0]; } const { action, metadata } = await bot.play(state, playerID); if (action) { const a = { ...action, payload: { ...action.payload, metadata, }, }; client.store.dispatch(a); return a; } } /** * Simulates the game till the end or a max depth. * * @param {...object} game - The game object. * @param {...object} bots - An array of bots. * @param {...object} state - The game state to start from. */ async function Simulate({ game, bots, state, depth, }) { if (depth === undefined) depth = 10000; const reducer$1 = reducer.CreateGameReducer({ game }); let metadata = null; let iter = 0; while (state.ctx.gameover === undefined && iter < depth) { let playerID = state.ctx.currentPlayer; if (state.ctx.activePlayers) { playerID = Object.keys(state.ctx.activePlayers)[0]; } const bot = bots instanceof Bot ? bots : bots[playerID]; const t = await bot.play(state, playerID); if (!t.action) { break; } metadata = t.metadata; state = reducer$1(state, t.action); iter++; } return { state, metadata }; } exports.Bot = Bot; exports.MCTSBot = MCTSBot; exports.RandomBot = RandomBot; exports.Simulate = Simulate; exports.Step = Step;