UNPKG

boardgame.io

Version:
596 lines (494 loc) 14.7 kB
import { _ as _createClass, a as _classCallCheck, b as _defineProperty, m as makeMove, g as gameEvent, c as alea, d as _inherits, f as _possibleConstructorReturn, h as _getPrototypeOf, C as CreateGameReducer, i as _typeof } from './reducer-b8b81041.js'; /** * Base class that bots can extend. */ var Bot = /*#__PURE__*/ function () { function Bot(_ref) { var _this = this; var enumerate = _ref.enumerate, seed = _ref.seed; _classCallCheck(this, Bot); _defineProperty(this, "enumerate", function (G, ctx, playerID) { var actions = _this.enumerateFn(G, ctx, playerID); return actions.map(function (a) { if (a.payload !== undefined) { return a; } if (a.move !== undefined) { return makeMove(a.move, a.args, playerID); } if (a.event !== undefined) { return gameEvent(a.event, a.args, playerID); } }); }); this.enumerateFn = enumerate; this.seed = seed; this.iterationCounter = 0; this._opts = {}; } _createClass(Bot, [{ key: "addOpt", value: function addOpt(_ref2) { var key = _ref2.key, range = _ref2.range, initial = _ref2.initial; this._opts[key] = { range: range, value: initial }; } }, { key: "getOpt", value: function getOpt(key) { return this._opts[key].value; } }, { key: "setOpt", value: function setOpt(key, value) { if (key in this._opts) { this._opts[key].value = value; } } }, { key: "opts", value: function opts() { return this._opts; } }, { key: "random", value: function random(arg) { var number; if (this.seed !== undefined) { var r = null; if (this.prngstate) { r = new alea('', { state: this.prngstate }); } else { r = new alea(this.seed, { state: true }); } number = r(); this.prngstate = r.state(); } else { number = Math.random(); } if (arg) { // eslint-disable-next-line unicorn/explicit-length-check if (arg.length) { var id = Math.floor(number * arg.length); return arg[id]; } else { return Math.floor(number * arg); } } return number; } }]); return Bot; }(); /** * The number of iterations to run before yielding to * the JS event loop (in async mode). */ var CHUNK_SIZE = 25; /** * Bot that uses Monte-Carlo Tree Search to find promising moves. */ var MCTSBot = /*#__PURE__*/ function (_Bot) { _inherits(MCTSBot, _Bot); function MCTSBot(_ref) { var _this; var enumerate = _ref.enumerate, seed = _ref.seed, objectives = _ref.objectives, game = _ref.game, iterations = _ref.iterations, playoutDepth = _ref.playoutDepth, iterationCallback = _ref.iterationCallback; _classCallCheck(this, MCTSBot); _this = _possibleConstructorReturn(this, _getPrototypeOf(MCTSBot).call(this, { enumerate: enumerate, seed: seed })); if (objectives === undefined) { objectives = function objectives() { return {}; }; } _this.objectives = objectives; _this.iterationCallback = iterationCallback || function () {}; _this.reducer = CreateGameReducer({ game: 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 } }); return _this; } _createClass(MCTSBot, [{ key: "createNode", value: function createNode(_ref2) { var state = _ref2.state, parentAction = _ref2.parentAction, parent = _ref2.parent, playerID = _ref2.playerID; var G = state.G, ctx = state.ctx; var actions = []; var objectives = []; if (playerID !== undefined) { actions = this.enumerate(G, ctx, playerID); objectives = this.objectives(G, ctx, playerID); } else if (ctx.activePlayers) { for (var _playerID in ctx.activePlayers) { actions = actions.concat(this.enumerate(G, ctx, _playerID)); objectives = objectives.concat(this.objectives(G, ctx, _playerID)); } } else { actions = actions.concat(this.enumerate(G, ctx, ctx.currentPlayer)); objectives = objectives.concat(this.objectives(G, ctx, ctx.currentPlayer)); } return { // Game state at this node. state: state, // Parent of the node. parent: parent, // Move used to get to this node. parentAction: parentAction, // Unexplored actions. actions: actions, // Current objectives. objectives: objectives, // Children of the node. children: [], // Number of simulations that pass through this node. visits: 0, // Number of wins for this node. value: 0 }; } }, { key: "select", value: function 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; } var selectedChild = null; var best = 0.0; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = node.children[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var child = _step.value; var childVisits = child.visits + Number.EPSILON; var uct = child.value / childVisits + Math.sqrt(2 * Math.log(node.visits) / childVisits); if (selectedChild == null || uct > best) { best = uct; selectedChild = child; } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator["return"] != null) { _iterator["return"](); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return this.select(selectedChild); } }, { key: "expand", value: function expand(node) { var actions = node.actions; if (actions.length == 0 || node.state.ctx.gameover !== undefined) { return node; } var id = this.random(actions.length); var action = actions[id]; node.actions.splice(id, 1); var childState = this.reducer(node.state, action); var childNode = this.createNode({ state: childState, parentAction: action, parent: node }); node.children.push(childNode); return childNode; } }, { key: "playout", value: function playout(node) { var _this2 = this; var state = node.state; var playoutDepth = this.getOpt('playoutDepth'); if (typeof this.playoutDepth === 'function') { playoutDepth = this.playoutDepth(state.G, state.ctx); } var _loop = function _loop(i) { var _state = state, G = _state.G, ctx = _state.ctx; var playerID = ctx.currentPlayer; if (ctx.activePlayers) { playerID = Object.keys(ctx.activePlayers)[0]; } var moves = _this2.enumerate(G, ctx, playerID); // Check if any objectives are met. var objectives = _this2.objectives(G, ctx); var score = Object.keys(objectives).reduce(function (score, key) { var objective = objectives[key]; if (objective.checker(G, ctx)) { return score + objective.weight; } return score; }, 0.0); // If so, stop and return the score. if (score > 0) { return { v: { score: score } }; } if (!moves || moves.length == 0) { return { v: undefined }; } var id = _this2.random(moves.length); var childState = _this2.reducer(state, moves[id]); state = childState; }; for (var i = 0; i < playoutDepth && state.ctx.gameover === undefined; i++) { var _ret = _loop(); if (_typeof(_ret) === "object") return _ret.v; } return state.ctx.gameover; } }, { key: "backpropagate", value: function backpropagate(node) { var result = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 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); } } }, { key: "play", value: function play(state, playerID) { var _this3 = this; var root = this.createNode({ state: state, playerID: playerID }); var numIterations = this.getOpt('iterations'); if (typeof this.iterations === 'function') { numIterations = this.iterations(state.G, state.ctx); } var getResult = function getResult() { var selectedChild = null; var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = root.children[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var child = _step2.value; if (selectedChild == null || child.visits > selectedChild.visits) { selectedChild = child; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { _iterator2["return"](); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } var action = selectedChild && selectedChild.parentAction; var metadata = root; return { action: action, metadata: metadata }; }; return new Promise(function (resolve) { var iteration = function iteration() { for (var i = 0; i < CHUNK_SIZE && _this3.iterationCounter < numIterations; i++) { var leaf = _this3.select(root); var child = _this3.expand(leaf); var result = _this3.playout(child); _this3.backpropagate(child, result); _this3.iterationCounter++; } _this3.iterationCallback({ iterationCounter: _this3.iterationCounter, numIterations: numIterations, metadata: root }); }; _this3.iterationCounter = 0; if (_this3.getOpt('async')) { var asyncIteration = function asyncIteration() { if (_this3.iterationCounter < numIterations) { iteration(); setTimeout(asyncIteration, 0); } else { resolve(getResult()); } }; asyncIteration(); } else { while (_this3.iterationCounter < numIterations) { iteration(); } resolve(getResult()); } }); } }]); return MCTSBot; }(Bot); /** * Bot that picks a move at random. */ var RandomBot = /*#__PURE__*/ function (_Bot) { _inherits(RandomBot, _Bot); function RandomBot() { _classCallCheck(this, RandomBot); return _possibleConstructorReturn(this, _getPrototypeOf(RandomBot).apply(this, arguments)); } _createClass(RandomBot, [{ key: "play", value: function play(_ref, playerID) { var G = _ref.G, ctx = _ref.ctx; var moves = this.enumerate(G, ctx, playerID); return Promise.resolve({ action: this.random(moves) }); } }]); return RandomBot; }(Bot); /* * 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) { var state = client.store.getState(); var playerID = state.ctx.currentPlayer; if (state.ctx.activePlayers) { playerID = Object.keys(state.ctx.activePlayers)[0]; } var _ref = await bot.play(state, playerID), action = _ref.action, metadata = _ref.metadata; if (action) { action.payload.metadata = metadata; client.store.dispatch(action); } return action; } /** * 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(_ref2) { var game = _ref2.game, bots = _ref2.bots, state = _ref2.state, depth = _ref2.depth; if (depth === undefined) depth = 10000; var reducer = CreateGameReducer({ game: game, numPlayers: state.ctx.numPlayers }); var metadata = null; var iter = 0; while (state.ctx.gameover === undefined && iter < depth) { var playerID = state.ctx.currentPlayer; if (state.ctx.activePlayers) { playerID = Object.keys(state.ctx.activePlayers)[0]; } var bot = bots instanceof Bot ? bots : bots[playerID]; var t = await bot.play(state, playerID); if (!t.action) { break; } metadata = t.metadata; state = reducer(state, t.action); iter++; } return { state: state, metadata: metadata }; } export { Bot as B, MCTSBot as M, RandomBot as R, Step as S, Simulate as a };