Проблемы с оптимизацией алгоритма MiniMax для шахматного AI - PullRequest
0 голосов
/ 09 апреля 2020

Я работал над игрой в шахматы для проекта с использованием Unity Engine и пришел в секцию AI по шахматам. Я хотел использовать базовый AI c для своей игры, так как я не нацелен на хардкорных шахматистов. Поэтому я избегал использования любых сторонних библиотек, таких как stockfi sh или действующего шахматного движка (также потому, что эта игра должна была быть универсальной). Я просто выбрал минимаксный алгоритм и использовал майские ссылки и исходные коды для его завершения. Но вот подвох. Мой алгоритм занимает много времени, чтобы найти следующий ход (около 10-15 секунд на глубине 3, около 10 секунд на глубине 2 и около 4-7 секунд на глубине 1). Который я думаю много! Я попробовал каждую технику оптимизации, в настоящее время использующую технику отсечения AlphaBeta, но, похоже, она не сильно помогла. Я нашел руководство, объясняющее шахматный ИИ, использующее те же минимакс и альфа-бета для его / ее веб-игры, и оно работает безупречно и намного быстрее! ( Проверьте здесь ) Что я делаю не так в оптимизации? Любые предложения и рекомендации будут оценены.

Спасибо.

Фрагменты кода (только минимакс)

//This is what calls the Minimax and finds result.
 IEnumerator IterativeDeepeningSearch(ChessBoardSnapshot boardPosition)
    {
        isRunningItDeep = true;

        float startTime = Time.unscaledTime;
        bool isOutTime = false;

        for (int depth = 1; (depth <= minDepth || depth <= maxDepth && !isOutTime); depth++)
        {
            StartCoroutine(MinimaxAB(boardPosition, depth));
            hasRunMinimax = true;
            isRunningMinimax = true;
            iterativeDepth = depth;
            yield return new WaitUntil(() => isRunningMinimax == false);
            hasRunMinimax = false;
            isOutTime = Time.unscaledTime - startTime >= maxTime;
        }

        isRunningItDeep = false;
    }

//The Minimax function
    IEnumerator MinimaxAB(ChessBoardSnapshot boardPosition, int depth)
    {
        isRunningMinimax = true;
        highestScore = int.MinValue;
        highestScoreIndex = -1;

        ChessBoardSnapshot[] nextBoardPositions = FindPossibleMoves(boardPosition, playerType);
        totalIterations = nextBoardPositions.Length;

        for (int i = 0; i < nextBoardPositions.Length; i++)
        {
            int score = AlphaBeta(nextBoardPositions[i], depth, int.MinValue, int.MaxValue, false);
            if (score > highestScore)
            {
                highestScore = score;
                highestScoreIndex = i;
            }

            lastScore = score;
            lastIteration = i;
            minimaxResult = nextBoardPositions[highestScoreIndex];
            yield return null;
        }

        isRunningMinimax = false;
    }

Весь код шахматного AI: (PS: функция MakeMove - это место, где AI начинает работать)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Chess;

public class AIManager : MonoBehaviour
{
    private static AIManager _instance;
    public static AIManager Instance
    {
        get
        {
            if (_instance == null)
                _instance = GameObject.FindGameObjectWithTag("AIManager").GetComponent<AIManager>();

            return _instance;
        }
    }

    [Header("Visuals")]
    public GameObject gear;

    [Header("Settings")]
    public ChessPlayerType playerType;
    //public int fixedDepthLimit = 5;
    public float maxTime = 20.0f;
    public int minDepth = 2;
    public int maxDepth = 40;
    public int levelUpEvery = 10;

    [Header("Minimax")]
    public int growth = 0;
    public ChessBoardSnapshot[] outcomes;
    public bool isRunningItDeep = false;
    public bool hasRunItDeep = false;
    public bool isRunningMinimax = false;
    public bool hasRunMinimax = false;

    public int iterativeDepth;

    public int lastScore;
    public int lastIteration;
    public int totalIterations;

    public int highestScore = int.MinValue;
    public int highestScoreIndex = -1;

    public ChessBoardSnapshot minimaxResult;
    public Dictionary<ulong, MinimaxNode> tTable = new Dictionary<ulong, MinimaxNode>();

    // Use this for initialization
    void Awake ()
    {
        if (_instance == null)
            _instance = this;
        else if (_instance != this)
            Destroy(this.gameObject);
    }

    void Start()
    {
        growth = 0;
    }

    public ChessBoardSnapshot[] FindPossibleMoves(ChessBoardSnapshot boardSnapshot, ChessPlayerType playerType)
    {
        List<ChessBoardSnapshot> ret = new List<ChessBoardSnapshot>();
        Dictionary<int, ChessPosition> boardDict = boardSnapshot.ToDictionary();

        foreach(KeyValuePair<int, ChessPosition> kvp in boardDict)
        {
            ChessPosition position = kvp.Value;

            if(position.type.IsDifferentTeamAs(playerType))
                continue;

            ChessCoordinate from = position.coord;

            ChessPieceMove[] possibleMoves = GameManager.Instance.profilesDict[position.type].possibleMoves;

            for (int j = 0; j < possibleMoves.Length; j++)
            {
                ChessCoordinate to = from + possibleMoves[j].move;
                int k = 1;

                while
                (
                    to.IsWithinRange() &&
                    (possibleMoves[j].repeatTimes < 0 || k <= possibleMoves[j].repeatTimes)
                )
                {
                    if (GameManager.Instance.IsValidSpecialRule(possibleMoves[j].specialRule, position, from, to))
                    {
                        // When there are only finite amount of moves, set only when reached destination
                        if (possibleMoves[j].repeatTimes < 0 || k == possibleMoves[j].repeatTimes)
                        {
                            bool isLastMove = false;

                            if (possibleMoves[j].pattern == ChessPieceMovePattern.Normal)
                            {
                                if (boardDict.ContainsKey(to.ToArrayCoord()))
                                {
                                    if (boardDict[to.ToArrayCoord()].type.IsSameTeamAs(position.type))
                                        break;

                                    isLastMove = true;
                                }
                            }
                            else if (possibleMoves[j].pattern == ChessPieceMovePattern.MoveOnly)
                            {
                                if (boardDict.ContainsKey(to.ToArrayCoord()))
                                    break;
                            }
                            else if (possibleMoves[j].pattern == ChessPieceMovePattern.CaptureOnly)
                            {
                                if (!boardDict.ContainsKey(to.ToArrayCoord()))
                                    break;

                                if (boardDict[to.ToArrayCoord()].type.IsSameTeamAs(position.type))
                                    break;
                            }

                            // Pawn Promotion
                            if (position.type.IsPawn())
                            {
                                if
                                (
                                    position.type == ChessPieceType.WhitePawn &&
                                    position.coord.y == 0
                                )
                                {
                                    position.type = ChessPieceType.WhiteQueen;
                                }
                                else if
                                (
                                    position.type == ChessPieceType.BlackPawn &&
                                    position.coord.y == 7
                                )
                                {
                                    position.type = ChessPieceType.BlackQueen;
                                }
                            }

                            ret.Add(GameManager.Instance.AdjustBoard
                            (
                                boardSnapshot,
                                position.type.ToIcon() + " " + from.x + "," + from.y + "-->" + to.x + "," + to.y,
                                new ChessPosition(ChessPieceType.None, from, true),
                                new ChessPosition(position.type, to, true)
                            ));

                            if (isLastMove)
                                break;
                        }
                        else
                        {
                            if (boardDict.ContainsKey(to.ToArrayCoord()))
                                break;
                        }
                    }

                    to += possibleMoves[j].move;
                    k++;
                }
            }
        }

        return ret.ToArray();
    }

    int AlphaBeta(ChessBoardSnapshot boardPosition, int depth, int alpha, int beta, bool maximizingPlayer)
    {
        int origAlpha = alpha;
        int origBeta = beta;
        MinimaxNode node = new MinimaxNode();

        // Transposition Table Lookup; node is the lookup key for ttEntry
        ulong hash = boardPosition.board.ToZobristHash();
        if (tTable.ContainsKey(hash))
        {
            node = tTable[hash];

            if(node.depth >= depth)
            {
                switch(node.flag)
                {
                    case MinimaxNodeFlag.Exact:
                        return node.eval;
                    case MinimaxNodeFlag.LowerBound:
                        if (node.eval > alpha)
                            alpha = node.eval;
                        break;
                    case MinimaxNodeFlag.UpperBound:
                        if (node.eval < beta)
                            beta = node.eval;
                        break;
                }

                if (beta <= alpha)
                    return node.eval;
            }
        }

        // Minimax + Alpha Beta Pruning
        if (depth <= 0 || boardPosition.IsEndGame())
        {
            return GameManager.Instance.CalculateScore(boardPosition, playerType);
        }

        int val = 0;

        if (maximizingPlayer)
        {
            val = int.MinValue;
            ChessBoardSnapshot[] nextBoardPositions = FindPossibleMoves(boardPosition, playerType);

            for (int i = 0; i < nextBoardPositions.Length; i++)
            {
                int newValue = AlphaBeta(nextBoardPositions[i], depth - 1, alpha, beta, false);

                if (newValue > val)
                    val = newValue;
                if (val > alpha)
                    alpha = val;
                if (beta <= alpha)
                    break;
            }
        }
        else
        {
            val = int.MaxValue;
            ChessBoardSnapshot[] nextBoardPositions = FindPossibleMoves(boardPosition, playerType.ToOpposite());

            for (int i = 0; i < nextBoardPositions.Length; i++)
            {
                int newValue = AlphaBeta(nextBoardPositions[i], depth - 1, alpha, beta, true);

                if (newValue < val)
                    val = newValue;
                if (val < beta)
                    beta = val;
                if (beta <= alpha)
                    break;
            }
        }

        // Transposition Table Store; node is the lookup key for ttEntry
        node.hash = hash;
        node.eval = val;

        if(val <= origAlpha)
            node.flag = MinimaxNodeFlag.UpperBound;
        else if(val >= origBeta)
            node.flag = MinimaxNodeFlag.LowerBound;
        else
            node.flag = MinimaxNodeFlag.Exact;

        node.depth = depth;

        if(tTable.ContainsKey(hash))
            tTable[hash] = node;
        else
            tTable.Add(hash, node);

        return val;
    }

    IEnumerator IterativeDeepeningSearch(ChessBoardSnapshot boardPosition)
    {
        isRunningItDeep = true;

        float startTime = Time.unscaledTime;
        bool isOutTime = false;

        for (int depth = 1; (depth <= minDepth || depth <= maxDepth && !isOutTime); depth++)
        {
            StartCoroutine(MinimaxAB(boardPosition, depth));
            hasRunMinimax = true;
            isRunningMinimax = true;
            iterativeDepth = depth;
            yield return new WaitUntil(() => isRunningMinimax == false);
            hasRunMinimax = false;
            isOutTime = Time.unscaledTime - startTime >= maxTime;
        }

        isRunningItDeep = false;
    }

    IEnumerator MinimaxAB(ChessBoardSnapshot boardPosition, int depth)
    {
        isRunningMinimax = true;

        //int highestScore = int.MinValue;
        //int highestScoreIndex = -1;
        highestScore = int.MinValue;
        highestScoreIndex = -1;

        ChessBoardSnapshot[] nextBoardPositions = FindPossibleMoves(boardPosition, playerType);
        totalIterations = nextBoardPositions.Length;

        for (int i = 0; i < nextBoardPositions.Length; i++)
        {
            int score = AlphaBeta(nextBoardPositions[i], depth, int.MinValue, int.MaxValue, false);
            if (score > highestScore)
            {
                highestScore = score;
                highestScoreIndex = i;
            }

            lastScore = score;
            lastIteration = i;
            minimaxResult = nextBoardPositions[highestScoreIndex];
            yield return null;
        }

        isRunningMinimax = false;
    }

    IEnumerator Minimax(ChessBoardSnapshot boardPosition, int depth)
    {
        isRunningMinimax = true;

        //int highestScore = int.MinValue;
        //int highestScoreIndex = -1;
        highestScore = int.MinValue;
        highestScoreIndex = -1;

        //getAllBoardPositions returns a list of next possible board positions, the boolean flag is to tell whether the current move is Max or Min
        ChessBoardSnapshot[] nextBoardPositions = FindPossibleMoves(boardPosition, playerType);
        totalIterations = nextBoardPositions.Length;

        for (int i = 0; i < nextBoardPositions.Length; i++)
        {
            ChessBoardSnapshot board = nextBoardPositions[i];
            int score = Min(board, depth);
            if (score > highestScore)
            {
                highestScore = score;
                highestScoreIndex = i;
            }

            lastScore = score;
            lastIteration = i;
            minimaxResult = nextBoardPositions[highestScoreIndex];
            yield return null;
        }

        isRunningMinimax = false;
    }

    int Min(ChessBoardSnapshot boardPosition, int depth)
    {
        if (depth <= 0 || boardPosition.IsEndGame())
        {
            return GameManager.Instance.CalculateScore(boardPosition, playerType);
        }

        int lowestScore = int.MaxValue;

        ChessBoardSnapshot[] nextBoardPositions = FindPossibleMoves(boardPosition, playerType.ToOpposite());
        for (int i = 0; i < nextBoardPositions.Length; i++)
        {
            ChessBoardSnapshot board = nextBoardPositions[i];
            int score = Max(board, depth - 1);
            if (score < lowestScore)
                lowestScore = score;
        }
        return lowestScore;
    }

    int Max(ChessBoardSnapshot boardPosition, int depth)
    {
        if (depth <= 0 || boardPosition.IsEndGame())
        {
            return GameManager.Instance.CalculateScore(boardPosition, playerType);
        }

        int highestScore = int.MinValue;

        ChessBoardSnapshot[] nextBoardPositions = FindPossibleMoves(boardPosition, playerType);
        for (int i = 0; i < nextBoardPositions.Length; i++)
        {
            ChessBoardSnapshot board = nextBoardPositions[i];
            int score = Min(board, depth - 1);
            if (score > highestScore)
                highestScore = score;
        }

        return highestScore;
    }

    [ContextMenu("[Debug] Generate Possible Moves")]
    public void GeneratePossibleMoves()
    {
        outcomes = FindPossibleMoves(GameManager.Instance.LatestSnapshot, playerType);
        Debug.Log("Generated possible moves for " + playerType + " Player (" + outcomes.Length + " outcomes)");
    }

    [ContextMenu("[Debug] Make Move now")]
    public void MakeMove()
    {
        growth++;
        if (growth % levelUpEvery == 0)
            minDepth++;

        StartCoroutine(IterativeDeepeningSearch(GameManager.Instance.LatestSnapshot));
        hasRunItDeep = true;
        isRunningItDeep = true;
    }

    public void PostGenerateMinimax()
    {
        GameManager.Instance.GenNextSnapshot(minimaxResult);
        GameManager.Instance.LoadFromSnapshot(GameManager.Instance.LatestSnapshot);
        //playerType = playerType.ToOpposite();
    }

    void Update()
    {
        if(Input.GetKeyDown(KeyCode.Space))
        {
            GeneratePossibleMoves();
        }

        if(hasRunItDeep)
        {
            // If it's not running anymore
            if(!isRunningItDeep)
            {
                PostGenerateMinimax();
                hasRunItDeep = false;
            }
        }
    }

    private void OnGUI()
    {
        gear.SetActive(isRunningItDeep);
    }
}
...