/// <summary>
        /// Returns the next move to make based on the state of the specified game, or null to make no move.
        /// </summary>
        public ShiftDirection?GetNextMove(FastBoard board, FastDeck deck, NextCardHint nextCardHint, ref long movesEvaluated)
        {
            ulong knownNextCardIndex;

            switch (nextCardHint)
            {
            case NextCardHint.One:
                knownNextCardIndex = 1;
                break;

            case NextCardHint.Two:
                knownNextCardIndex = 2;
                break;

            case NextCardHint.Three:
                knownNextCardIndex = 3;
                break;

            case NextCardHint.Bonus:
                knownNextCardIndex = ulong.MaxValue;                         // MaxValue means "bonus" and is calculated specially.
                break;

            default:
                throw new NotSupportedException("Unknown NextCardHint '" + nextCardHint + "'.");
            }

            float quality;

            return(GetBestMoveForBoard(board, deck, knownNextCardIndex, _moveSearchDepth - 1, out quality, ref movesEvaluated));
        }
        /// <summary>
        /// Returns the next move to make based on the state of the specified game, or null to make no move.
        /// </summary>
        public ShiftDirection?GetNextMove(FastBoard board, FastDeck deck, NextCardHint nextCardHint, ref long movesEvaluated)
        {
            movesEvaluated = 0;
            List <ShiftDirection> validDirs = new List <ShiftDirection>(Board.AllShiftDirections.Where(d => TestShiftDirection(board, d)));

            if (validDirs.Count > 0)
            {
                return(validDirs[_rand.Int32(0, validDirs.Count - 1)]);
            }
            else
            {
                return(null);
            }
        }
        /// <summary>
        /// Returns the best move to make for the specified board, or null if there are no moves to make.
        /// Outputs the quality of the returned move.
        /// </summary>
        private ShiftDirection?GetBestMoveForBoard(FastBoard board, FastDeck deck, ulong knownNextCardIndex, int recursionsLeft, out float moveQuality, ref long movesEvaluated)
        {
            float?leftQuality = null;
            float?rightQuality = null;
            float?upQuality = null;
            float?downQuality = null;
            long  moves1 = 0, moves2 = 0;

            Parallel.Invoke(
                () =>
            {
                leftQuality  = EvaluateMoveForBoard(board, deck, knownNextCardIndex, ShiftDirection.Left, recursionsLeft, ref moves1);
                rightQuality = EvaluateMoveForBoard(board, deck, knownNextCardIndex, ShiftDirection.Right, recursionsLeft, ref moves1);
            },
                () =>
            {
                upQuality   = EvaluateMoveForBoard(board, deck, knownNextCardIndex, ShiftDirection.Up, recursionsLeft, ref moves2);
                downQuality = EvaluateMoveForBoard(board, deck, knownNextCardIndex, ShiftDirection.Down, recursionsLeft, ref moves2);
            });
            movesEvaluated += moves1 + moves2;

            float?         bestQuality = leftQuality;
            ShiftDirection?bestDir     = leftQuality != null ? ShiftDirection.Left : (ShiftDirection?)null;

            if (rightQuality != null && (bestQuality == null || rightQuality.Value > bestQuality.Value))
            {
                bestQuality = rightQuality;
                bestDir     = ShiftDirection.Right;
            }
            if (upQuality != null && (bestQuality == null || upQuality.Value > bestQuality.Value))
            {
                bestQuality = upQuality;
                bestDir     = ShiftDirection.Up;
            }
            if (downQuality != null && (bestQuality == null || downQuality.Value > bestQuality.Value))
            {
                bestQuality = downQuality;
                bestDir     = ShiftDirection.Down;
            }
            moveQuality = bestQuality ?? float.MinValue;
            return(bestDir);
        }
        /// <summary>
        /// Returns the next move to make based on the state of the specified game, or null to make no move.
        /// </summary>
        public ShiftDirection?GetNextMove(FastBoard board, FastDeck deck, NextCardHint nextCardHint)
        {
            long movesEvaluated = 0;

            return(GetNextMove(board, deck, nextCardHint, ref movesEvaluated));
        }
        /// <summary>
        /// Returns the quality value for shifting the specified board in the specified direction.
        /// Returns null if shifting in that direction is not possible.
        /// </summary>
        private unsafe float?EvaluateMoveForBoard(FastBoard board, FastDeck deck, ulong knownNextCardIndex, ShiftDirection dir, int recursionsLeft, ref long movesEvaluated)
        {
            FastBoard    shiftedBoard = board;
            IntVector2D *newCardCells = stackalloc IntVector2D[4];

            if (shiftedBoard.ShiftInPlace(dir, newCardCells))
            {
                float totalQuality = 0;
                float totalWeight  = 0;

                if (knownNextCardIndex == ulong.MaxValue)                // Special value for bonus card.
                {
                    ByteList12 indexes = new ByteList12();
                    Game.GetPossibleBonusCardIndexes(board.GetMaxCardIndex(), ref indexes);
                    for (int i = 0; i < indexes.Count; i++)
                    {
                        ulong cardIndex = indexes.Items[i];
                        for (int j = 0; j < 4; j++)
                        {
                            IntVector2D cell = newCardCells[j];
                            if (cell.X < 0)
                            {
                                continue;
                            }

                            FastBoard newBoard = shiftedBoard;
                            newBoard.SetCardIndex(cell, cardIndex);

                            float quality;
                            if (recursionsLeft == 0 || GetBestMoveForBoard(newBoard, deck, 0, recursionsLeft - 1, out quality, ref movesEvaluated) == null)
                            {
                                quality = _evaluator(newBoard);
                                movesEvaluated++;
                            }

                            totalQuality += quality;
                            totalWeight  += 1;
                        }
                    }
                }
                else if (knownNextCardIndex > 0)
                {
                    FastDeck newDeck = deck;
                    newDeck.Remove(knownNextCardIndex);
                    for (int i = 0; i < 4; i++)
                    {
                        IntVector2D cell = newCardCells[i];
                        if (cell.X < 0)
                        {
                            continue;
                        }

                        FastBoard newBoard = shiftedBoard;
                        newBoard.SetCardIndex(cell, knownNextCardIndex);

                        float quality;
                        if (recursionsLeft == 0 || GetBestMoveForBoard(newBoard, newDeck, 0, recursionsLeft - 1, out quality, ref movesEvaluated) == null)
                        {
                            quality = _evaluator(newBoard);
                            movesEvaluated++;
                        }

                        totalQuality += quality;
                        totalWeight  += 1;
                    }
                }
                else if (_moveSearchDepth - recursionsLeft - 1 < _cardCountDepth)
                {
                    if (deck.Ones > 0)
                    {
                        FastDeck newDeck = deck;
                        newDeck.RemoveOne();
                        for (int i = 0; i < 4; i++)
                        {
                            IntVector2D cell = newCardCells[i];
                            if (cell.X < 0)
                            {
                                continue;
                            }

                            FastBoard newBoard = shiftedBoard;
                            newBoard.SetCardIndex(cell, 1);

                            float quality;
                            if (recursionsLeft == 0 || GetBestMoveForBoard(newBoard, newDeck, 0, recursionsLeft - 1, out quality, ref movesEvaluated) == null)
                            {
                                quality = _evaluator(newBoard);
                                movesEvaluated++;
                            }

                            totalQuality += quality * deck.Ones;
                            totalWeight  += deck.Ones;
                        }
                    }

                    if (deck.Twos > 0)
                    {
                        FastDeck newDeck = deck;
                        newDeck.RemoveTwo();
                        for (int i = 0; i < 4; i++)
                        {
                            IntVector2D cell = newCardCells[i];
                            if (cell.X < 0)
                            {
                                continue;
                            }

                            FastBoard newBoard = shiftedBoard;
                            newBoard.SetCardIndex(cell, 2);

                            float quality;
                            if (recursionsLeft == 0 || GetBestMoveForBoard(newBoard, newDeck, 0, recursionsLeft - 1, out quality, ref movesEvaluated) == null)
                            {
                                quality = _evaluator(newBoard);
                                movesEvaluated++;
                            }

                            totalQuality += quality * deck.Twos;
                            totalWeight  += deck.Twos;
                        }
                    }

                    if (deck.Threes > 0)
                    {
                        FastDeck newDeck = deck;
                        newDeck.RemoveThree();
                        for (int i = 0; i < 4; i++)
                        {
                            IntVector2D cell = newCardCells[i];
                            if (cell.X < 0)
                            {
                                continue;
                            }

                            FastBoard newBoard = shiftedBoard;
                            newBoard.SetCardIndex(cell, 3);

                            float quality;
                            if (recursionsLeft == 0 || GetBestMoveForBoard(newBoard, newDeck, 0, recursionsLeft - 1, out quality, ref movesEvaluated) == null)
                            {
                                quality = _evaluator(newBoard);
                                movesEvaluated++;
                            }

                            totalQuality += quality * deck.Threes;
                            totalWeight  += deck.Threes;
                        }
                    }

                    // Note that we're not taking the chance of getting a bonus card into consideration. That would be way too expensive at not much benefit.
                }
                else
                {
                    float quality;
                    if (recursionsLeft == 0 || GetBestMoveForBoard(shiftedBoard, deck, 0, recursionsLeft - 1, out quality, ref movesEvaluated) == null)
                    {
                        quality = _evaluator(shiftedBoard);
                        movesEvaluated++;
                    }

                    totalQuality += quality;
                    totalWeight  += 1;
                }

                return(totalQuality / totalWeight);
            }
            else
            {
                return(null);
            }
        }