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); }
public override void BeginMove(Grid grid) { board.Move(logReader.GetNextMove(), player); }
public virtual void BeginMove(Grid grid) { }
private void PrintMoveStatistics(double runtime, Grid grid, SearchResult result, int[] scores, int[] scoreTypes) { Debug.Assert(scores.Length == scoreTypes.Length); log.WriteLine("Done"); // Print the grid before the AI's move to the log file. log.WriteLineToLog(); log.WriteLineToLog("Grid before AI move:"); log.WriteLineToLog(grid.ToString()); log.WriteLineToLog(); // Print node statistics. double cutoffsOnFirstChild = betaCutoffsOnFirstChild * 100D / betaCutoffs; double cutoffsOnOrderedChildren = betaCutoffsOnOrderedChildren * 100D / betaCutoffs; log.Write("Analysed "); WriteWithColor("{0:N0}", ConsoleColor.White, totalNodesSearched); log.WriteLine(" states, including {0:N0} end states.", endNodesSearched); log.WriteLine(" Searched {0:N0} pv-nodes, {1:N0} cut-nodes and {2:N0} all-nodes.", pvNodes, cutNodes, allNodes); log.WriteLine(" {0:N0} cutoffs from lookups.", alphaBetaCutoffs); log.WriteLine(" {0:N0} beta cutoffs ({1:N2}% on first child, {2:N2}% on " + "ordered children).", betaCutoffs, cutoffsOnFirstChild, cutoffsOnOrderedChildren); // Print runtime statistics. double nodesPerMillisecond = Math.Round(totalNodesSearched / runtime, 4); string minutes = (runtime > 60000) ? String.Format(" ({0:N2} minutes)", runtime / 60000.0) : ""; log.Write("Runtime {0:N} ms{1} (", runtime, minutes); WriteWithColor("{0:N}", ConsoleColor.White, nodesPerMillisecond); log.WriteLine(" states / ms)."); log.WriteLine(" Total runtime {0:N} ms.", totalRuntime); // Print the scores of each valid move. log.Write("The move scores are"); for (int i = 0; i < scores.Length; i++) { if (grid.IsValidMove(i)) { log.Write(" {0}:", i); if (scoreTypes[i] == -1) { log.Write("-"); } else { if (scores[i] == infinity) { log.WriteToLog("+\u221E"); Console.ForegroundColor = ConsoleColor.Green; Console.Write("+Inf"); Console.ResetColor(); } else if (scores[i] == -infinity) { log.WriteToLog("-\u221E"); Console.ForegroundColor = ConsoleColor.Red; Console.Write("-Inf"); Console.ResetColor(); } else { log.Write("{0}", scores[i]); } switch (scoreTypes[i]) { case NodeTypeExact: log.Write("E"); break; case NodeTypeLower: log.Write("L"); break; case NodeTypeUpper: log.Write("U"); break; } } } } log.WriteLine(); // Print the score of the chosen move. if (result.score > 0) { WriteLineWithColor(" AI will win latest on move {0}.", ConsoleColor.Green, result.GetDepth()); } else if (result.score < 0) { WriteLineWithColor(" AI will lose on move {0} (assuming perfect play).", ConsoleColor.Red, result.GetDepth()); } else { log.WriteLine(" Move score is {0}.", result.score); } log.WriteLine(" Move is {0:N0}.", finalMove); log.WriteLine("Correct/Incorrect guesses: {0:N0}/{1:N0}", correctGuesses, incorrectGuesses); // Print transposition table statistics. log.WriteLine(); log.WriteLine("Transposition table:"); log.WriteLine(" Size: {0:N0}", TranspositionTable.TableSize); log.WriteLine(" Memory Space: {0:N0} MB", TranspositionTable.MemorySpaceBytes / 1024 / 1024); log.WriteLine(" Shallow Lookups: {0:N0}", shallowTableLookups); log.WriteLine(" Lookups: {0:N0}", tableLookups); log.WriteLine(" Requests: {0:N0}", transpositionTable.Requests); log.WriteLine(" Insertions: {0:N0}", transpositionTable.Insertions); log.WriteLine(" Collisions: {0:N0}", transpositionTable.Collisions); log.WriteLine(" Items: {0:N0} ({1:N4}% full)", transpositionTable.Size, 100.0 * transpositionTable.Size / TranspositionTable.TableSize); transpositionTable.ResetStatistics(); log.WriteLine(); log.WriteLine(); }
public override void BeginMove(Grid grid) { // Run CalculateNextMove in a worker thread, then call MakeMove in the // main thread. BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += new DoWorkEventHandler(CalculateNextMove); worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(MakeMove); worker.RunWorkerAsync(grid); }
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 Perft() { Grid grid = new Grid(7, 6, 0); const int maxDepth = 10; int[] results = new int[maxDepth]; int[] expected = new int[] { 1, 7, 49, 238, 1120, 4263, 16422, 54859, 184275, 558186, 1662623, 4568683 }; HashSet<ulong> visitedStates = new HashSet<ulong>(); PerftHelper(grid, 0, maxDepth, results, visitedStates); for (int i = 0; i < maxDepth; i++) { Assert.AreEqual(expected[i], results[i]); } }
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); }
public Player(string name, Grid.Colors colour) { this.Name = name; this.colour = colour; }