UNPKG

minmax-wt-alpha-beta-pruning

Version:

A generic minmax algorithm engine (with alpha-beta pruning) that can work with any game supplied by the user

119 lines (109 loc) 6.8 kB
'use strict'; // The rationale behind using this idiom is described in: // http://stackoverflow.com/a/36628148/274677 // /* not needed in this project: if (!global._babelPolyfill) // https://github.com/s-panferov/awesome-typescript-loader/issues/121 require('babel-polyfill'); */ // The above is important as Babel only transforms syntax (e.g. arrow functions) // so you need this in order to support new globals or (in my experience) well-known Symbols, e.g. the following: // // console.log(Object[Symbol.hasInstance]); // // ... will print 'undefined' without the the babel-polyfill being required. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // This class is only used internally by the algorithm (in the recursive call) var EvaluationAndMove = function EvaluationAndMove(move, evaluation) { _classCallCheck(this, EvaluationAndMove); this.move = move; this.evaluation = evaluation; }; function minmax(gameState, gameRules, evaluate, plies) { var alpha = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : Number.NEGATIVE_INFINITY; var beta = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : Number.POSITIVE_INFINITY; var statisticsHook = arguments[6]; function _minmax(gameState, pliesRemaining, alpha, beta, maximizing) { if (statisticsHook != null) statisticsHook.visitedNode(gameState); var vForTerminal = gameRules.terminalStateEval(gameState); if (vForTerminal != null || pliesRemaining === 0) { if (statisticsHook != null) statisticsHook.evaluatedLeafNode(gameState); var v2 = vForTerminal != null ? vForTerminal : evaluate(gameState); return new EvaluationAndMove(null, v2 * (maximizing ? 1 : -1)); } else { // construct the children and evaluate them var moves = gameRules.listMoves(gameState); var NUM_OF_MOVES = moves.length; if (NUM_OF_MOVES <= 0) throw 'weird number of moves (' + NUM_OF_MOVES + ') in non-terminal state'; // one can add cleverness and squeeze the two branches into one at the expense of readability if (maximizing) { var v = Number.NEGATIVE_INFINITY; var bestMove = null; for (var i = 0; i < NUM_OF_MOVES; i++) { var nextState = gameRules.nextState(gameState, moves[i]); var nextStateEval = _minmax(nextState, pliesRemaining - 1, Math.max(v, alpha), beta, !maximizing); if (nextStateEval != null) { if (nextStateEval.evaluation > v) { if (nextStateEval.evaluation === Number.POSITIVE_INFINITY) // no need to look any further return new EvaluationAndMove(moves[i], nextStateEval.evaluation); v = nextStateEval.evaluation; bestMove = moves[i]; } } else throw new Error('impossible at this point'); if (v >= beta && i !== NUM_OF_MOVES - 1) { /* sse-1512513725: in the various resources on the algorithm I always see this as (v>beta) but I am confident there is no reason not to use ">=" instead as this is better (it increases the likelihood of pruning). Also, if this is the last child, we don't consider it a true pruning incident for statistical purposes (the logic remains effectively the same as for the last child we are going to break out of the loop anyways */ if (statisticsHook != null) statisticsHook.pruningIncident(gameState, true, v, beta, i); break; } } if (!(v === Number.NEGATIVE_INFINITY || bestMove != null)) throw 'maximizing node, v is ' + (v == null ? 'null' : v) + ', bestMove is: ' + (bestMove == null ? 'null' : bestMove) + ' - this makes no sense'; return new EvaluationAndMove(bestMove !== null ? bestMove : moves[0], v); // if all moves are equally bad, return the first one } else { var v = Number.POSITIVE_INFINITY; var bestMove = null; for (var _i = 0; _i < NUM_OF_MOVES; _i++) { var _nextState = gameRules.nextState(gameState, moves[_i]); var _nextStateEval = _minmax(_nextState, pliesRemaining - 1, alpha, Math.min(v, beta), !maximizing); if (_nextStateEval != null) { if (_nextStateEval.evaluation === Number.NEGATIVE_INFINITY) // no need to look any further return new EvaluationAndMove(moves[_i], _nextStateEval.evaluation); if (_nextStateEval.evaluation < v) { v = _nextStateEval.evaluation; bestMove = moves[_i]; } } else throw new Error('impossible at this point'); if (v <= alpha && _i !== NUM_OF_MOVES - 1) { // see sse-1512513725 (mutatis mutandis) if (statisticsHook != null) statisticsHook.pruningIncident(gameState, false, v, alpha, _i); break; } } if (!(v === Number.POSITIVE_INFINITY || bestMove != null)) throw 'minimizing node, v is ' + (v == null ? 'null' : v) + ', bestMove is: ' + (bestMove == null ? 'null' : bestMove) + ' - this makes no sense'; return new EvaluationAndMove(bestMove !== null ? bestMove : moves[0], v); // if all moves are equally bad, return the first one } } } var v = gameRules.terminalStateEval(gameState); if (v != null) return { bestMove: null, evaluation: v };else { if (!(Number.isInteger(plies) && plies >= 0)) throw 'illegal plies for minmax: ' + plies; var evalAndMove = _minmax(gameState, plies, alpha, beta, true); // in the min-max algorithm the player who is to make the move is the maximizing player if (!(plies === 0 || evalAndMove.move != null)) throw 'this is not a terminal state, plies were not 0 (they were ' + plies + ') and yet, no move was found, this makes no sense'; return { bestMove: evalAndMove.move, evaluation: evalAndMove.evaluation }; } } minmax; exports.minmax = minmax; //# sourceMappingURL=minmax-impl.js.map