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
145 lines (130 loc) • 9.51 kB
Flow
// @flow
;
// 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.
import type {
IGameRules, EvaluateFT, MinMaxFT, TMinMaxResult, TMinMaxStatistics
} from './minmax-interface.js'
// This class is only used internally by the algorithm (in the recursive call)
class EvaluationAndMove<MoveGTP> {
move: ?MoveGTP
evaluation: number;
constructor(move: ?MoveGTP, evaluation: number) {
this.move = move;
this.evaluation = evaluation;
}
}
function minmax <GameStateGTP, MoveGTP>
(gameState : GameStateGTP
, gameRules : IGameRules<GameStateGTP, MoveGTP>
, evaluate : EvaluateFT<GameStateGTP>
, plies : number
, alpha : number = Number.NEGATIVE_INFINITY
, beta : number = Number.POSITIVE_INFINITY
, statisticsHook : ?TMinMaxStatistics<GameStateGTP>
)
: TMinMaxResult<MoveGTP> {
function _minmax(gameState : GameStateGTP
, pliesRemaining : number
, alpha : number
, beta : number
, maximizing : boolean
): EvaluationAndMove<MoveGTP> {
if (statisticsHook!=null) statisticsHook.visitedNode(gameState);
const vForTerminal: ?number = gameRules.terminalStateEval(gameState);
if ( (vForTerminal!=null) || (pliesRemaining===0)) {
if (statisticsHook!=null) statisticsHook.evaluatedLeafNode(gameState);
const v2 : number = (vForTerminal!=null?vForTerminal:evaluate(gameState));
return new EvaluationAndMove(null, v2*(maximizing?1:-1));
} else {
// construct the children and evaluate them
const moves: Array<MoveGTP> = gameRules.listMoves(gameState);
const NUM_OF_MOVES: number = 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 = Number.NEGATIVE_INFINITY;
var bestMove: ?MoveGTP = null;
for (let i = 0; i < NUM_OF_MOVES ; i++) {
const nextState: (GameStateGTP) = gameRules.nextState(gameState, moves[i]);
const nextStateEval: ?EvaluationAndMove<MoveGTP> = _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 = Number.POSITIVE_INFINITY;
var bestMove: ?MoveGTP = null;
for (let i = 0; i < NUM_OF_MOVES ; i++) {
const nextState: (GameStateGTP) = gameRules.nextState(gameState, moves[i]);
const nextStateEval: ?EvaluationAndMove<MoveGTP> = _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
}
}
}
const v: ?number = gameRules.terminalStateEval(gameState);
if (v!=null)
return {
bestMove: null,
evaluation: v
};
else {
if (! (Number.isInteger(plies) && (plies>=0) ))
throw `illegal plies for minmax: ${plies}`;
const evalAndMove :EvaluationAndMove<MoveGTP> = _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: MinMaxFT<mixed, mixed>)
exports.minmax = minmax;