Esempio n. 1
0
        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);
        }
Esempio n. 2
0
        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);
                }
            }
        }
Esempio n. 3
0
        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;
        }
Esempio n. 4
0
        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);
                    }
                }
            }
        }
Esempio n. 5
0
        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]);
                                }
                            }
                        }
                    }
                }
            }
        }
Esempio n. 6
0
        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]);
                            }
                        }
                    }
                }
            }
        }
Esempio n. 7
0
        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));
        }
Esempio n. 8
0
        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);
        }