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
JavaScript
// 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
;