Minimax Analysis

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!! :)

 
code/minimax-analysis.txt · Last modified: 2010/02/11 09:10
 
Recent changes RSS feed Valid XHTML 1.0 Driven by DokuWiki