protected override MinimaxSpot[,] GetInsight(MinimaxSpot[,] state, MinimaxMove move, bool isLightPlayer) { MinimaxSpot[,] insightState = new MinimaxSpot[m_numRows, m_numCols]; for (int i = 0; i < m_numRows; i++) { for (int j = 0; j < m_numCols; j++) { insightState[i, j] = state[i, j]; } } insightState[move.Row, move.Col] = isLightPlayer ? MinimaxSpot.Light : MinimaxSpot.Dark; return(insightState); }
public ReversiGame(int numRows, int numCols) { m_numRows = numRows; m_numCols = numCols; m_board = new MinimaxSpot[numRows, numCols]; for (int i = 0; i < numRows; i++) { for (int j = 0; j < numCols; j++) { m_board[i, j] = MinimaxSpot.Empty; } } m_board[3, 3] = MinimaxSpot.Light; m_board[4, 4] = MinimaxSpot.Light; m_board[4, 3] = MinimaxSpot.Dark; m_board[3, 4] = MinimaxSpot.Dark; m_isLightMove = false; }
/// <summary> /// Should only be called through the public Search method. /// </summary> /// <param name="state">The game state to consider.</param> /// <param name="isLightPlayer">The player to move.</param> /// <param name="alpha">The alpha pruning value.</param> /// <param name="beta">The beta pruning value.</param> /// <param name="depth">The current search depth.</param> /// <returns>A MinimaxMove that represents the best move found.</returns> /// <remarks> /// The initial alpha value should be Int32.MinValue, the initial beta value /// should be Int32.MaxValue, and the initial depth value should be 0. /// /// The search will terminate ASAP if the m_ct cancellation token is signaled. /// /// This method is thread-safe. /// </remarks> private MinimaxMove InternalSearch(MinimaxSpot[,] state, bool isLightPlayer, int alpha, int beta, int depth) { // Stop the search if... if (TerminalTest(state) || depth >= m_maxDepth || m_ct.IsCancellationRequested) { m_movesConsidered++; return new MinimaxMove(EvaluateHeuristic(state)); } // Initialize the best move for this recursive call. MinimaxMove bestMove = new MinimaxMove(isLightPlayer ? Int32.MinValue : Int32.MaxValue); // Get the valid moves for this recursive call. IEnumerable<MinimaxMove> validMoves = GetValidMoves(state, isLightPlayer); // If there are valid moves, recurse on each. bool consideredLocalMoves = false; foreach (MinimaxMove move in validMoves) { consideredLocalMoves = true; MinimaxMove curMove = move; curMove.Value = InternalSearch(GetInsight(state, curMove, isLightPlayer), !isLightPlayer, alpha, beta, depth + 1).Value; if (isLightPlayer) { if (curMove.Value > bestMove.Value) bestMove = curMove; if (bestMove.Value >= beta) break; alpha = Math.Max(alpha, bestMove.Value.Value); } else { if (curMove.Value < bestMove.Value) bestMove = curMove; if (bestMove.Value <= alpha) break; beta = Math.Min(beta, bestMove.Value.Value); } } // If there were no valid moves, still calculate the value. if (!consideredLocalMoves) { bestMove.Value = InternalSearch(state, !isLightPlayer, alpha, beta, depth + 1).Value; } return bestMove; }
/// <summary> /// Returns the game state that results when the given player plays the given move on the given /// state. If the move is invalid, the new state should be the same as the old state. /// Must be thread-safe. /// </summary> /// <param name="state">The state to play a move on.</param> /// <param name="move">The move to play.</param> /// <param name="isLightPlayer">The player to play the move.</param> /// <returns>A MinimaxSpot matrix that represents the insight state.</returns> protected abstract MinimaxSpot[,] GetInsight(MinimaxSpot[,] state, MinimaxMove move, bool isLightPlayer);
/// <summary> /// Returns a collection containing the valid moves for /// the given player on the given game state. /// Must be thread-safe. /// </summary> /// <param name="state">The game state to consider.</param> /// <param name="isLightPlayer">The bool indicating which player.</param> /// <returns>An enumerable of MinimaxMove, representing the valid moves.</returns> protected abstract IEnumerable<MinimaxMove> GetValidMoves(MinimaxSpot[,] state, bool isLightPlayer);
/// <summary> /// Returns the value of the given state, where a positive value indicates /// an advantage for the light player. /// Must be thread-safe. /// </summary> /// <param name="state">The game state to evaluate.</param> /// <returns>A number representing the value of the given state.</returns> protected abstract int EvaluateHeuristic(MinimaxSpot[,] state);
/// <summary> /// Returns whether the given state represents a finished game. /// Must be thread-safe. /// </summary> /// <param name="state">The game state to check.</param> /// <returns>True if the state represents a finished game, /// false otherwise.</returns> protected abstract bool TerminalTest(MinimaxSpot[,] state);
/// <summary> /// Returns the best move resulting from a Minimax, alpha-beta pruning search on the given state. /// </summary> /// <param name="state">The state to consider.</param> /// <param name="isLightPlayer">The player to move.</param> /// <param name="inParallel">A boolean indicating whether to use the parallel algorithm.</param> /// <returns>A MinimaxMove that represents the best move found.</returns> /// <remarks> /// This method will only return a MinimaxMove(-1...) if there are no valid moves. /// </remarks> public MinimaxMove Search(MinimaxSpot[,] state, bool isLightPlayer, bool inParallel) { // Initialize a bunch of state. m_maxDepth = MaxDepth == -1 ? Int32.MaxValue : MaxDepth; m_degOfParallelism = DegreeOfParallelism; m_timeLimit = TimeLimit; m_taskCount = 0; m_movesConsidered = 0; var curCts = m_cts = new CancellationTokenSource(); m_ct = m_cts.Token; MinimaxMove aiMove = new MinimaxMove(-1, -1, null); // Start the timeout timer. Done using a dedicated thread to minimize delay // in cancellation due to lack of threads in the pool to run the callback. var timeoutTask = Task.Factory.StartNew(() => { Thread.Sleep(m_timeLimit); curCts.Cancel(); }, TaskCreationOptions.LongRunning); // Do the search aiMove = inParallel ? InternalSearchTPL(state, isLightPlayer, Int32.MinValue, Int32.MaxValue, 0, CancellationToken.None) : InternalSearch(state, isLightPlayer, Int32.MinValue, Int32.MaxValue, 0); // Make sure that MinimaxMove(-1...) is only returned if there are no valid moves, because // InternalSearch* may return MinimaxMove(-1...) if none of the valid moves beats Int32.Min/Max. if (aiMove.Row == -1) { foreach (var move in GetValidMoves(state, isLightPlayer)) { aiMove = move; aiMove.Value = isLightPlayer ? Int32.MinValue : Int32.MaxValue; break; } } return aiMove; }
/// <summary> /// Should only be called through the public Search method. /// </summary> /// <param name="state">The game state to consider.</param> /// <param name="isLightPlayer">The player to move.</param> /// <param name="alpha">The alpha pruning value.</param> /// <param name="beta">The beta pruning value.</param> /// <param name="depth">The current search depth.</param> /// <param name="token">The pruning token.</param> /// <returns>A MinimaxMove that represents the best move found.</returns> /// <remarks> /// The initial alpha value should be Int32.MinValue, the initial beta value /// should be Int32.MaxValue, the initial depth value should be 0, and the /// initial token should be a non-settable token. /// /// The search will terminate ASAP if the m_ct cancellation token is signaled. /// /// This method is thread-safe. /// </remarks> private MinimaxMove InternalSearchTPL(MinimaxSpot[,] state, bool isLightPlayer, int alpha, int beta, int depth, CancellationToken token) { // Stop the search if... if (TerminalTest(state) || depth >= m_maxDepth || m_ct.IsCancellationRequested) { m_movesConsidered++; // NOTE: this is racy and may be lower than the actual count, but it only needs to be an appx return new MinimaxMove(EvaluateHeuristic(state)); } // Initialize the best move for this recursive call. MinimaxMove bestMove = new MinimaxMove(isLightPlayer ? Int32.MinValue : Int32.MaxValue); // Get the valid moves for this recursive call. IEnumerable<MinimaxMove> validMoves = GetValidMoves(state, isLightPlayer); bool consideredLocalMoves = false; Queue<Task> workers = new Queue<Task>(); object bigLock = new object(); CancellationTokenSource cts = new CancellationTokenSource(); foreach (MinimaxMove move in validMoves) { // SHARED STATE // The local variables (bestMove, alpha, beta) are protected by a lock. // The non-local variables (m_taskCount) are modified using Interlocked consideredLocalMoves = true; // If the pruning token is signaled, stop this loop. if (token.IsCancellationRequested) { cts.Cancel(); break; } MinimaxMove curMove = move; if (m_taskCount < m_degOfParallelism && depth <= m_maxDepth - 1) { Interlocked.Increment(ref m_taskCount); workers.Enqueue(Task.Factory.StartNew(() => { curMove.Value = InternalSearchTPL(GetInsight(state, curMove, isLightPlayer), !isLightPlayer, alpha, beta, depth + 1, cts.Token).Value; lock (bigLock) { if (isLightPlayer) { if (curMove.Value > bestMove.Value) bestMove = curMove; if (bestMove.Value >= beta) cts.Cancel(); alpha = Math.Max(alpha, bestMove.Value.Value); } else { if (curMove.Value < bestMove.Value) bestMove = curMove; if (bestMove.Value <= alpha) cts.Cancel(); beta = Math.Min(beta, bestMove.Value.Value); } } Interlocked.Decrement(ref m_taskCount); })); } else { bool isPruning = false; curMove.Value = InternalSearchTPL(GetInsight(state, curMove, isLightPlayer), !isLightPlayer, alpha, beta, depth + 1, cts.Token).Value; // If there are no tasks, no need to lock. bool lockTaken = false; try { if (workers.Count > 0) Monitor.Enter(bigLock, ref lockTaken); if (isLightPlayer) { if (curMove.Value > bestMove.Value) bestMove = curMove; if (bestMove.Value >= beta) isPruning = true; alpha = Math.Max(alpha, bestMove.Value.Value); } else { if (curMove.Value < bestMove.Value) bestMove = curMove; if (bestMove.Value <= alpha) isPruning = true; beta = Math.Min(beta, bestMove.Value.Value); } } finally { if (lockTaken) Monitor.Exit(bigLock); } if (isPruning) { cts.Cancel(); break; } } } Task.WaitAll(workers.ToArray()); // If there were no valid moves, still calculate the value. if (!consideredLocalMoves) { bestMove.Value = InternalSearchTPL(state, !isLightPlayer, alpha, beta, depth + 1, token).Value; } return bestMove; }
/// <summary> /// Returns whether a move on the given row and column on the given state by the given player /// is valid. /// </summary> /// <param name="state">The state to consider.</param> /// <param name="isLightPlayer">The player ot move.</param> /// <param name="row">The move row.</param> /// <param name="col">The move column.</param> /// <returns></returns> private bool IsValidMove(MinimaxSpot[,] state, bool isLightPlayer, int row, int col) { if (state[row, col] != MinimaxSpot.Empty) { return(false); } MinimaxSpot you = isLightPlayer ? MinimaxSpot.Light : MinimaxSpot.Dark; MinimaxSpot enemy = isLightPlayer ? MinimaxSpot.Dark : MinimaxSpot.Light; // Check above. if (row + 1 < m_numRows && state[row + 1, col] == enemy) { for (int r = row + 2; r < m_numRows; r++) { if (state[r, col] == you) { return(true); } if (state[r, col] == MinimaxSpot.Empty) { break; } } } // Check below. if (row - 1 >= 0 && state[row - 1, col] == enemy) { for (int r = row - 2; r >= 0; r--) { if (state[r, col] == you) { return(true); } if (state[r, col] == MinimaxSpot.Empty) { break; } } } // Check right. if (col + 1 < m_numCols && state[row, col + 1] == enemy) { for (int c = col + 2; c < m_numCols; c++) { if (state[row, c] == you) { return(true); } if (state[row, c] == MinimaxSpot.Empty) { break; } } } // Check left. if (col - 1 >= 0 && state[row, col - 1] == enemy) { for (int c = col - 2; c >= 0; c--) { if (state[row, c] == you) { return(true); } if (state[row, c] == MinimaxSpot.Empty) { break; } } } // Check above-right if (row + 1 < m_numRows && col + 1 < m_numCols && state[row + 1, col + 1] == enemy) { for (int r = row + 2, c = col + 2; r < m_numRows && c < m_numCols; r++, c++) { if (state[r, c] == you) { return(true); } if (state[r, c] == MinimaxSpot.Empty) { break; } } } // Check above-left if (row + 1 < m_numRows && col - 1 >= 0 && state[row + 1, col - 1] == enemy) { for (int r = row + 2, c = col - 2; r < m_numRows && c >= 0; r++, c--) { if (state[r, c] == you) { return(true); } if (state[r, c] == MinimaxSpot.Empty) { break; } } } // Check below-right if (row - 1 >= 0 && col + 1 < m_numCols && state[row - 1, col + 1] == enemy) { for (int r = row - 2, c = col + 2; r >= 0 && c < m_numCols; r--, c++) { if (state[r, c] == you) { return(true); } if (state[r, c] == MinimaxSpot.Empty) { break; } } } // Check below-left if (row - 1 >= 0 && col - 1 >= 0 && state[row - 1, col - 1] == enemy) { for (int r = row - 2, c = col - 2; r >= 0 && c >= 0; r--, c--) { if (state[r, c] == you) { return(true); } if (state[r, c] == MinimaxSpot.Empty) { break; } } } return(false); }
/// <summary> /// Makes a move on the given row and column. /// </summary> /// <param name="row">The move row</param> /// <param name="col">The move column</param> /// <returns>Whether or not the operation succeeded.</returns> public bool MakeMove(int row, int col) { if (!IsValidMove(m_board, m_isLightMove, row, col)) { return(false); } MinimaxSpot you = m_isLightMove ? MinimaxSpot.Light : MinimaxSpot.Dark; MinimaxSpot enemy = m_isLightMove ? MinimaxSpot.Dark : MinimaxSpot.Light; var backup = new MinimaxSpot[8, 8]; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { backup[i, j] = m_board[i, j]; } } m_board[row, col] = you; // Conquer above. if (row + 1 < m_numRows && m_board[row + 1, col] == enemy) { bool b = false; for (int r = row + 2; r < m_numRows; r++) { if (m_board[r, col] == MinimaxSpot.Empty) { break; } if (m_board[r, col] == you) { b = true; } } if (b) { for (int r = row + 1; m_board[r, col] != you; r++) { m_board[r, col] = you; } } } // Conquer below. if (row - 1 >= 0 && m_board[row - 1, col] == enemy) { bool b = false; for (int r = row - 2; r >= 0; r--) { if (m_board[r, col] == MinimaxSpot.Empty) { break; } if (m_board[r, col] == you) { b = true; } } if (b) { for (int r = row - 1; m_board[r, col] != you; r--) { m_board[r, col] = you; } } } // Conquer right. if (col + 1 < m_numCols && m_board[row, col + 1] == enemy) { bool b = false; for (int c = col + 2; c < m_numCols; c++) { if (m_board[row, c] == MinimaxSpot.Empty) { break; } if (m_board[row, c] == you) { b = true; } } if (b) { for (int c = col + 1; m_board[row, c] != you; c++) { m_board[row, c] = you; } } } // Conquer left. if (col - 1 >= 0 && m_board[row, col - 1] == enemy) { bool b = false; for (int c = col - 2; c >= 0; c--) { if (m_board[row, c] == MinimaxSpot.Empty) { break; } if (m_board[row, c] == you) { b = true; } } if (b) { for (int c = col - 1; m_board[row, c] != you; c--) { m_board[row, c] = you; } } } // Conquer above-right if (row + 1 < m_numRows && col + 1 < m_numCols && m_board[row + 1, col + 1] == enemy) { bool b = false; for (int r = row + 2, c = col + 2; r < m_numRows && c < m_numCols; r++, c++) { if (m_board[r, c] == MinimaxSpot.Empty) { break; } if (m_board[r, c] == you) { b = true; } } if (b) { for (int r = row + 1, c = col + 1; m_board[r, c] != you; r++, c++) { m_board[r, c] = you; } } } // Conquer above-left if (row + 1 < m_numRows && col - 1 >= 0 && m_board[row + 1, col - 1] == enemy) { bool b = false; for (int r = row + 2, c = col - 2; r < m_numRows && c >= 0; r++, c--) { if (m_board[r, c] == MinimaxSpot.Empty) { break; } if (m_board[r, c] == you) { b = true; } } if (b) { for (int r = row + 1, c = col - 1; m_board[r, c] != you; r++, c--) { m_board[r, c] = you; } } } // Conquer below-right if (row - 1 >= 0 && col + 1 < m_numCols && m_board[row - 1, col + 1] == enemy) { bool b = false; for (int r = row - 2, c = col + 2; r >= 0 && c < m_numCols; r--, c++) { if (m_board[r, c] == MinimaxSpot.Empty) { break; } if (m_board[r, c] == you) { b = true; } } if (b) { for (int r = row - 1, c = col + 1; m_board[r, c] != you; r--, c++) { m_board[r, c] = you; } } } // Conquer below-left if (row - 1 >= 0 && col - 1 >= 0 && m_board[row - 1, col - 1] == enemy) { bool b = false; for (int r = row - 2, c = col - 2; r >= 0 && c >= 0; r--, c--) { if (m_board[r, c] == MinimaxSpot.Empty) { break; } if (m_board[r, c] == you) { b = true; } } if (b) { for (int r = row - 1, c = col - 1; m_board[r, c] != you; r--, c--) { m_board[r, c] = you; } } } m_isLightMove = !m_isLightMove; return(true); }