public Board(int gridWidth, int gridHeight, IBoardDrawer drawer) { this.drawer = drawer; const int AIPlayer = 0; const int humanPlayer = 1 - AIPlayer; // Test seeds: seed = 1092552428; //seed = 2053617222; //seed = new Random().Next(); players[AIPlayer] = new AIPlayer(AIPlayer, this); players[humanPlayer] = new HumanPlayer(humanPlayer, this); grid = new Grid(gridWidth, gridHeight, seed); grid.Move(3, 0); grid.Move(3, 1); grid.Move(3, 0); grid.Move(3, 1); grid.Move(3, 0); grid.Move(2, 1); }
private void PerftHelper(Grid grid, int depth, int maxDepth, int[] results, HashSet<ulong> visitedStates) { int player = depth & 1; if (depth >= maxDepth || grid.IsGameOver(player) || visitedStates.Contains(grid.Hash)) { return; } results[depth]++; visitedStates.Add(grid.Hash); for (int i = 0; i < grid.Width; i++) { if (grid.IsValidMove(i)) { grid.Move(i, player); PerftHelper(grid, depth + 1, maxDepth, results, visitedStates); grid.UndoMove(i, player); } } }
private SearchResult Negamax(int currentDepth, Grid state, int alpha, int beta, bool allowNullWindow = true) { int currentPlayer = currentDepth & 1; int originalAlpha = alpha; totalNodesSearched++; ulong validMovesMask = state.GetValidMovesMask(); ulong playerThreats = state.GetThreats(currentPlayer); // If the player can win on this move then return immediately, // no more evaluation is needed. ulong currentPlayerThreats = validMovesMask & playerThreats; if (currentPlayerThreats != 0) { endNodesSearched++; if (currentDepth == moveNumber) { finalMove = GetFirstColumnOfThreatBoard(currentPlayerThreats); } return new SearchResult(infinity, currentDepth); } ulong opponentThreats = state.GetThreats(1 - currentPlayer); // If the opponent could win in one move on this position, then the // current player must play that move instead. ulong currentOpponentThreats = validMovesMask & opponentThreats; if (currentOpponentThreats != 0) { int forcedMove = GetFirstColumnOfThreatBoard(currentOpponentThreats); if (currentDepth == moveNumber) { finalMove = forcedMove; } // Remove the forced move from the threat board. currentOpponentThreats &= ~(0x3FUL << (forcedMove * (state.Height + 1))); // If there is another threat return the loss immediately, otherwise // play the forced move. if (currentOpponentThreats != 0) { endNodesSearched++; return new SearchResult(-infinity, currentDepth + 1); } // Take the single forced move. state.Move(forcedMove, currentPlayer); SearchResult childResult = -Negamax(currentDepth + 1, state, -beta, -alpha, allowNullWindow); state.UndoMove(forcedMove, currentPlayer); return childResult; } // This must be a draw if there are no forced moves and only two // moves left in the game. if (currentDepth >= maxMoves - 1) { endNodesSearched++; return new SearchResult(0, currentDepth); } // Will be set to the best move of the lookup. int entryBestMove = -1; // Check if the position or the flipped position should be used for the lookup. ulong hash = Math.Min(state.Hash, state.FlippedHash); bool usingFlippedPosition = hash != state.Hash; // Check if this state has already been visited. ulong entry; if (transpositionTable.Lookup(hash, out entry)) { // If the state has been visited, then return immediately or // improve the alpha and beta values depending on the node type. tableLookups++; // The score is stored in bits 8 to 15 of the entry. int encoded = (int)(((entry >> 8) & 0xFF) - 128); SearchResult lookupResult = new SearchResult { score = encoded }; // The type is stored in bits 6 to 7 of the entry. int entryType = (int)((entry >> 6) & 0x3); // Flip the best move if necessary. entryBestMove = (int)((entry >> 16) & 0x7); if (usingFlippedPosition) { entryBestMove = 6 - entryBestMove; } switch (entryType) { // If the score is exact, there is no need to check anything // else, so return the score of the entry. case NodeTypeExact: if (currentDepth == moveNumber) { finalMove = entryBestMove; } return lookupResult; // If the entry score is an upper bound on the actual score, // see if the current upper bound can be reduced. case NodeTypeUpper: beta = Math.Min(beta, lookupResult.GetValue()); break; // If the entry score is a lower bound on the actual score, // see if the current lower bound can be increased. case NodeTypeLower: alpha = Math.Max(alpha, lookupResult.GetValue()); break; } // At this point alpha or beta may have been improved, so check if // this is a cuttoff. if (alpha >= beta) { alphaBetaCutoffs++; if (currentDepth == moveNumber) { finalMove = entryBestMove; } return lookupResult; } } // A bitmap where a 1 means the corresponding move has already been // checked. Initialised with a 1 at all invalid moves. ulong checkedMoves = state.GetInvalidMovesMask(); SearchResult result = new SearchResult { score = int.MinValue }; int bestMove = -1; int index = -1; bool isFirstChild = true; // If no parent of this position has tried a null-window search yet, // try guessing the score of this position. if (allowNullWindow) { int guess = (currentPlayer == 0) ? GuessScore(0, playerThreats, opponentThreats) : GuessScore(1, opponentThreats, playerThreats); // Check if there is a guess for this position. if (guess != 0) { int nullAlpha; int nullBeta; int expectedScore; // If this is likely to be a win, search this position with a // null window centered at +infinity. if (guess == 1) { nullAlpha = infinity; nullBeta = infinity + 1; expectedScore = infinity; } // If this is likely to be a loss. else { nullAlpha = -infinity - 1; nullBeta = -infinity; expectedScore = -infinity; } allowNullWindow = false; SearchResult nullResult = Negamax( currentDepth, state, nullAlpha, nullBeta, allowNullWindow); // If the guess is correct, return immediately. if (nullResult.GetValue() == expectedScore) { correctGuesses++; return nullResult; } incorrectGuesses++; } } // Find the best move recursively. // checkMoves will be equal to bottomRow when there are no more valid // moves. while (checkedMoves != Grid.bottomRow) { int move = GetNextMove(ref index, ref checkedMoves, entryBestMove, validMovesMask, playerThreats); if (isFirstChild) { bestMove = move; } // Apply the move and recurse. state.Move(move, currentPlayer); SearchResult childResult = -Negamax(currentDepth + 1, state, -beta, -alpha, allowNullWindow); state.UndoMove(move, currentPlayer); if (childResult.score > result.score) { result = childResult; bestMove = move; if (result.GetValue() > alpha) { alpha = result.GetValue(); // Check if this a cutoff. if (alpha >= beta) { betaCutoffs++; if (isFirstChild) { betaCutoffsOnFirstChild++; } if (index <= 0) { betaCutoffsOnOrderedChildren++; } break; } } } isFirstChild = false; } Debug.Assert(bestMove != -1); // Determine the type of node, so the score can be used correctly // after a lookup. int flag; if (result.score <= originalAlpha) { flag = NodeTypeUpper; allNodes++; } else if (result.score >= beta) { flag = NodeTypeLower; cutNodes++; } else { flag = NodeTypeExact; pvNodes++; } // Store the score in the t-table in case the same state is reached later. int moveToStore = (usingFlippedPosition) ? 6 - bestMove : bestMove; transpositionTable.Add(currentDepth, moveToStore, hash, result.score, flag); if (currentDepth == moveNumber) { finalMove = bestMove; } return result; }
public void UndoMoveTest() { for (int player = 0; player < 2; player++) { Grid referenceGrid = new Grid(width, height, seed); Grid testGrid = new Grid(width, height, seed); for (int y = 0; y < height; y++) { for (int move = 0; move < width; move++) { referenceGrid.Move(move, player); testGrid.Move(move, player); testGrid.UndoMove(move, player); testGrid.Move(move, player); Assert.IsTrue(testGrid.Equals(referenceGrid)); Assert.AreEqual(testGrid.Hash, referenceGrid.Hash); } } } }
public void MoveTestPerPlayer() { for (int player = 0; player < 2; player++) { Grid grid = new Grid(width, height, seed); TileState expectedState = (TileState)player; for (int row = 0; row < height; row++) { for (int column = 0; column < width; column++) { grid.Move(column, player); // Test that only the expected tiles are filled. for (int testrow = 0; testrow < height; testrow++) { for (int testcolumn = 0; testcolumn < width; testcolumn++) { if (testrow < row || (testrow == row && testcolumn <= column)) { Assert.AreEqual(expectedState, grid[testrow, testcolumn]); } else { Assert.AreEqual(TileState.Empty, grid[testrow, testcolumn]); } } } } } } }
public void MoveTestAlternatingPlayer() { Grid grid = new Grid(width, height, seed); int currentPlayer = 0; for (int row = 0; row < height; row++) { for (int column = 0; column < width; column++) { grid.Move(column, currentPlayer); currentPlayer = 1 - currentPlayer; // Test that only the expected tiles are filled with the correct player. for (int testrow = 0; testrow < height; testrow++) { for (int testcolumn = 0; testcolumn < width; testcolumn++) { if (testrow < row || (testrow == row && testcolumn <= column)) { // Note: if size is an odd number then the expression below // must be (testcolumn + testrow) % 2. TileState expectedState = (TileState)(testcolumn % 2); Assert.AreEqual(expectedState, grid[testrow, testcolumn]); } else { Assert.AreEqual(TileState.Empty, grid[testrow, testcolumn]); } } } } } }
public void IsGameOverVerticalTest() { Grid grid = new Grid(width, height, seed); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(0, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(0, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(0, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(0, 0); Assert.AreEqual(0, grid.IsGameOver()); Assert.AreEqual(true, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid = new Grid(width, height, seed); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(width - 1, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(width - 1, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(width - 1, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(width - 1, 1); Assert.AreEqual(1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(true, grid.IsGameOver(1)); grid = new Grid(width, height, seed); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(3, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(4, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(3, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(4, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(3, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(4, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(4, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(3, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(0, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(1, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(2, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(5, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(1, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(5, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(1, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(5, 1); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(1, 0); Assert.AreEqual(-1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(false, grid.IsGameOver(1)); grid.Move(5, 1); Assert.AreEqual(1, grid.IsGameOver()); Assert.AreEqual(false, grid.IsGameOver(0)); Assert.AreEqual(true, grid.IsGameOver(1)); }
public void HashAndFlippedHashTestEqualOnSymmetricBoard() { Grid grid = new Grid(7, 6, 0); grid.Move(3, 0); grid.Move(3, 1); grid.Move(3, 0); grid.Move(3, 1); grid.Move(3, 0); grid.Move(3, 1); Assert.AreEqual(grid.Hash, grid.FlippedHash); }