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
102 lines (79 loc) • 4.6 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.
/*
Guide to naming conventions used
-------------------------------
GTP : Generic Type Parameter
{T/I}SomeType : Interface or Type (respectively) for class or object structural (not nominal) typing
SomeFunctionFT : Function Type
*/
export type ListMovesFT<GameStateGTP, MoveGTP> = (GameStateGTP)=>Array<MoveGTP>
export type NextStateFT<GameStateGTP, MoveGTP> = (GameStateGTP, MoveGTP)=>GameStateGTP
export type IGameRules<GameStateGTP, MoveGTP> = {|
/* The framework will *never* call the listMoveser on a terminal state so you don't have to handle that state.
If you don't trust, me simply return [] on a terminal state even though you can just as well throw an
exception in that case as execution will never reach that path.
*/
listMoves : ListMovesFT<GameStateGTP, MoveGTP>,
nextState : NextStateFT<GameStateGTP, MoveGTP>,
terminalStateEval (gs: GameStateGTP) : ?number
|}
/* The evaluate function will ***always*** evaluate from the perspective of the moving player
(we assume that information on which player is moving is embedded in the GameStateGTP)
positive infinity means WIN or hugely favourable situation for the moving player
negative --------------- LOSS ---------- unfavourable ------------------------------
Note that in the general case it is possible for a game's terminal state to have
an evaluation that's neither positive nor negative infinity (e.g. if the game allows
draws or some other graded outcome).
The evaluate function has no concept of "maximizing" or "minimizing" player. This is an artifact
of the minmax algorithm. The evaluate function simply reports from the perspective of the moving
player and the minmax implementation (that constructs the game tree) takes account of who the
maximizing or minimizing player is and proceeds accordingly. I.e., it effectively multiplies the
return value of the evaluate function by +1 or -1 respectively.
*/
export type EvaluateFT <GameStateGTP> = (gs: GameStateGTP) => number;
export type TMinMaxResult<MoveGTP> =
{|
bestMove : ?MoveGTP,
evaluation: number
|}
export type TMinMaxStatistics<GameStateGTP> =
{
visitedNode (n: GameStateGTP): void,
evaluatedLeafNode (n: GameStateGTP): void,
pruningIncident (n: GameStateGTP, aboveBetaOrBelowAlpha: boolean, v: number, alphaOrBetaValue: number, index: number): void
};
/* The minmax function type (MinMaxFT) returns both the best move and the evaluation of the root
node. It assumes that the moving player at the root is also the maximizing player.
Another way to think of the alpha and beta values is the following:
* alpha: execute the algorithm (with the moving player being the maximizing player) assuming that the
opponnent will be happy with any value less than alpha and will not try to minimize any further
once he finds a move that yields an evaluation less than alpha. Default initial value: negative infinity.
beta: execute the algorithm and (while on a maximizing node) don't bother searching for anything better than
beta. Default initial value: positive infinity.
It should NOT be assumed that the algorithm will return a value in the [alpha, beta] range. In fact it is quite
possible that you invoke the algorithm with alpha >= beta (e.g. see test file minmax-test-pseudo-games.js and the
pseudoGameLogic7 test game). Though I don't know whether that makes any sense.
*/
export type MinMaxFT<GameStateGTP, MoveGTP> =
(gameState : GameStateGTP
, gameRules : IGameRules<GameStateGTP, MoveGTP>
, evaluate: EvaluateFT<GameStateGTP>
, plies: number
, alpha?: number
, beta?: number
, statisticsHook?: TMinMaxStatistics<GameStateGTP>
) => TMinMaxResult<MoveGTP>;