Here’s an abstract framework for minimax analysis of two-player zero-sum games with perfect information.
package zerosum /** * The GameState trait represents an immutable game state that can be analyzed * by a search algorithm. * * @author Jason Arhart */ trait GameState[M, S <: GameState[M, S]] { /** * Return the result of the next player making the specified move. * * @param move the move * @return a new game state */ def +(move: M): S /** * The move made to arrive at this game state. If this is the initial game * state, lastMove will be None. */ val lastMove: Option[M] /** * All of the moves the next player can legally make from this game state, in * no particular order. */ val legalMoves: Seq[M] }
package zerosum /** * The Evaluating trait represents static evaluation of game states. * * Subtraits should provide game-specific implementations of isTerminal and * staticValue, and optionally sort. * * @author Jason Arhart */ trait Evaluating[S <: GameState[_, S]] { /** * Is state a terminal state? Usually a terminal state would be one where a * player has won or it's a draw. * * @param state the game state to consider * @return true if state is terminal */ protected def isTerminal(state: S): Boolean /** * Return a static evaluation of state from the point of view of the next * player, a higher number meaning a better position. * * @param state the game state to consider * @return a static evaluation of state */ protected def staticValue(state: S): Int /** * Sort the sequence of game states in order to analyze the most likely * candidates first. By default, does a stable sort by the result of applying * staticValue. * * @param s a sequence of game states to be analyzed * @return the game states in the order in which they should be analyzed */ protected def sort(s: Seq[S]) = util.Sorting.stableSort(s, (a: S, b: S) => staticValue(a) < staticValue(b)) }
package zerosum /** * The Searching trait represents a search algorithm for analyzing game states * and finding a favorable move for the next player. * * Subtraits need only implement analysis with the correct algorithm. * * @author Jason Arhart */ trait Searching[M, S <: GameState[M, S]] { this: Evaluating[S] => /** * Return a favorable move for the next player. With a depth of 0, only * consider the next player's move; with 1 consider the next player's * opponent's move, etc. An optional preArrange function arranges the move * order before searching, for example to randomize the choice of equivalent * moves. * * @param state the game state to consider * @param depth the maximum search depth */ def findMove(state: S, depth: Int)(implicit preArrange: Seq[M] => Seq[M]): Option[M] = { require(depth >= 0, "A negative search depth would always result in None") analysis(state, depth+1, -Int.MaxValue, Int.MaxValue)(preArrange) } /** * An Analysis is the result of analyzing a game state from the point of view * of the next player to move. * * @param value the value of a game state * @param move the next move that should be taken */ case class Analysis(value: Int, move: Option[M]) { def max(that: Analysis) = if (this.value < that.value) that else this } implicit def tupleToAnalysis(t: (Int, Option[M])) = Analysis(t._1, t._2) implicit def analysisToInt(a: Analysis): Int = a value implicit def analysisToMove(a: Analysis): Option[M] = a move /** * Return an Analysis of the game state from the point of view of the next * player to move. A higher value in the analysis represents a more favorable * position. The move field, if defined, is the next move in the principal * variation. If state is a terminal state, move will be None. * * @param state the game state to be analyzed * @param depth the maximum search depth; 0 means stop here * @param a the alpha value (the search window's lower bound) * @param b the beta value (the search window's upper bound) * @return an Analysis of the game state from the point of view of the next * player to move */ def analysis(state: S, depth: Int, a: Int, b: Int)(implicit preArrange: Seq[M] => Seq[M]): Analysis protected def childrenOf(state: S)(implicit preArrange: Seq[M] => Seq[M]): Seq[S] = sort((preArrange(state.legalMoves)) map (state + _)) implicit protected val preArrange: Seq[M] => Seq[M] = identity }
And here are two examples of search algorithms.
package zerosum /** * The Negamax traits implements the Negamax search algorithm with alpha-beta * pruning. * * @author Jason Arhart */ trait Negamax[M, S <: GameState[M, S]] extends Searching[M, S] { this: Evaluating[S] => def analysis(state: S, depth: Int, a: Int, b: Int)(implicit preArrange: Seq[M] => Seq[M]): Analysis = if (depth < 1 || isTerminal(state)) (staticValue(state), None) else { var _a = Analysis(a, None) for (child <- childrenOf(state)(preArrange)) { _a = _a max (-analysis(child, depth-1, -b, -_a), child lastMove) if (_a >= b) return _a } _a } }
package zerosum /** * The NegaScout traits implements the NegaScout search algorithm. * * @author Jason Arhart */ trait NegaScout[M, S <: GameState[M, S]] extends Searching[M, S] { this: Evaluating[S] => def analysis(state: S, depth: Int, a: Int, b: Int)(implicit preArrange: Seq[M] => Seq[M]): Analysis = if (depth < 1 || isTerminal(state)) (staticValue(state), None) else { var _a = Analysis(a, None) var _b = b for (child <- childrenOf(state)(preArrange)) { _a = _a max (-analysis(child, depth-1, -_b, -_a), child.lastMove) if (_a >= b) return _a if (_a >= _b) { _a = _a max (-analysis(child, depth-1, -b, -_a), child.lastMove) if (_a >= b) return _a } _b = _a + 1 } _a } }
Now you have everything you need to write your own computer chess player. Go go go!! :)