草莓♭布丁

导航

基于AStar算法的纸牌接龙求解工具(C#实现)

一、游戏规则介绍

  纸牌接龙是一个很经典的游戏了,相信很多人小时候都玩过。

规则如下:

1,一共52张牌,初始牌堆是1~7张,只有最下面一张是翻开的,下面的牌挪走之后上一张翻开。

2,右上角有24张牌,每次翻开3张,只能操作最上面的一张。

3,不同颜色的牌,若数字相差1,可以接在一起。接在一起的牌可以一起拖动。

4,只有K可以拖到空列上

5,左上角每种花色分别从小到大收集,把52张牌全部收集算作成功

 

  AStar算法原本用于求解最短路径问题,也适用于很多游戏的求解问题。对于其他类似游戏,稍作修改可以使用。纸牌接龙要100多步才能解出,每步都有若干分支,搜索树极其庞大,使用深度优先或者广度优先搜索是行不通的。

 

二、交互界面设计

  设计环境:VS2019,.Net Framework4.7.2

  拖入两个TextBox和三个按钮,添加按钮的点击事件,添加退出事件。

 

 

   由于计算线程会卡住主线程,需要新开一个计算线程。

  上下两个TextBox的名字分别是textBox_Create和textBox_Result。交互窗口代码如下(其中很多类还没有,后面慢慢介绍):

public partial class Form_Main : Form
    {
        AStarGameAnalyze analyze_AStar;

        public Form_Main()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 随机生成52张牌
        /// </summary>
        private void button_RandomCreate_Click(object sender, EventArgs e)
        {
            CardsGameData cardsGame = new CardsGameData();
            cardsGame.CreateRandomCards();                      //创建一局纯随机游戏
            textBox_Create.Text = cardsGame.PrintGameData();
        }

        /// <summary>
        /// 求解
        /// </summary>
        private void button_Console_Click(object sender, EventArgs e)
        {
            string[] data = textBox_Create.Text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            if(data.Length<8)
            {
                textBox_Result.Text = "行数错误";
            }
            else
            {
                string[] colData = new string[7];
                for (int i = 0; i < colData.Length; i++)
                {
                    colData[i] = data[i];
                }
                CardsGameData cardsGame = new CardsGameData(colData, data[7]);

                AbortThread();                   //先关闭之前的线程
                analyze_AStar = new AStarGameAnalyze();
                analyze_AStar.SolveGame(this, cardsGame);
            }
        }

        /// <summary>
        /// 增加控制台的内容
        /// </summary>
        public void AddConcole(string str,bool needNewLine=true)
        {
            textBox_Result.Text += str;
            if(needNewLine)
            {
                textBox_Result.Text += "\r\n";
            }
        }

        /// <summary>
        /// 清空控制台
        /// </summary>
        public void ClearConcole()
        {
            textBox_Result.Text = "";
        }

        /// <summary>
        /// 杀死线程按钮
        /// </summary>
        private void button_Abort_Click(object sender, EventArgs e)
        {
            AbortThread();
        }

        /// <summary>
        /// 关闭窗口
        /// </summary>
        private void Form_Main_FormClosing(object sender, FormClosingEventArgs e)
        {
            AbortThread();
        }

        /// <summary>
        /// 停止计算
        /// </summary>
        public void AbortThread()
        {
            analyze_AStar?.StopAnalyze();
        }
    }

  需要注意的是,除了“杀死计算线程”按钮以外,在开始计算之前,还有关闭窗口的时候都要停止计算,别让野线程在后台挂一大堆。

 

三,纸牌和牌局类设计,牌局的随机生成

  由于新增节点时需要大量的复制操作,纸牌和牌局类需要设计拷贝构造函数

1,纸牌类Card设计

  首先我们需要规定牌的输出格式,实现String和Card类的转换,不然也看不懂到底随机了个什么。规定牌的数字是1-13,即A为1,JQK为11,12,13。规定花色红桃为T,方块为F,黑桃为H,梅花为M。(基本都是拼音首字母,红桃黑桃都是H,红桃就换成桃桃的首字母了0.0)

  例如:红桃5——T5,梅花Q——M12。

具体代码如下:

    enum CardType
    {
        HongTao = 0,
        FangKuai = 1,
        HeiTao = 2,
        MeiHua = 3,
        Unknown = 4,
    }

    class Card
    {
        public CardType cardType;
        public int num;
        public bool canMove;                //是否翻开
       
        public Card()
        {

        }

        /// <summary>
        /// 拷贝构造
        /// </summary>
        public Card(Card otherCard)
        {
            cardType = otherCard.cardType;
            num = otherCard.num;
            canMove = otherCard.canMove;
        }

        public Card(CardType type,int num)
        {
            this.cardType = type;
            this.num = num;
            this.canMove = true;
        }

        /// <summary>
        /// 判断两张牌是否相同
        /// </summary>
        public static bool IsTwoCardEqual(Card card1,Card card2)
        {
            if (card1 == null || card2 == null)
            {
                return card1 == null && card2 == null;
            }
            else
            {
                return card1.cardType == card2.cardType && card1.num == card2.num;
            }
        }

        /// <summary>
        /// 从字符串解析
        /// </summary>
        public Card(string data,bool canMove=true)
        {
            cardType = String2CardType(data[0]);
            num = Convert.ToInt32(data.Substring(1));
            this.canMove = canMove;
        }

        public string PrintCard()
        {
            string cardData = CardType2String(cardType) + num;
            return cardData;
        }

        /// <summary>
        /// 判断两张牌是否花色不同
        /// </summary>
        public bool IsDifferentColor(Card otherCard)
        {
            return IsDifferentColor(otherCard.cardType);
        }

        /// <summary>
        /// 判断两张牌是否花色不同
        /// </summary>
        public bool IsDifferentColor(CardType other_CardType)
        {
            if (cardType == CardType.HongTao || cardType == CardType.FangKuai)
            {
                return other_CardType == CardType.HeiTao || other_CardType == CardType.MeiHua;
            }
            else
            {
                return other_CardType == CardType.HongTao || other_CardType == CardType.FangKuai;
            }
        }

        /// <summary>
        /// 判断上下两张牌是否是一组
        /// </summary>
        /// <returns></returns>
        public bool IsOneGroup(Card upCard)
        {
            if(upCard.num - this.num == 1)
            {
                return IsDifferentColor(upCard);
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// 类型转字符串
        /// </summary>
        public static string CardType2String(CardType cardType)
        {
            string cardData = "";
            switch (cardType)
            {
                case CardType.HongTao:
                    {
                        cardData = "T";
                        break;
                    }
                case CardType.FangKuai:
                    {
                        cardData = "F";
                        break;
                    }
                case CardType.HeiTao:
                    {
                        cardData = "H";
                        break;
                    }
                case CardType.MeiHua:
                    {
                        cardData = "M";
                        break;
                    }
            }
            return cardData;
        }

        /// <summary>
        /// 字符串转纸牌类型
        /// </summary>
        public static CardType String2CardType(char typeStr)
        {
            CardType cardType = CardType.Unknown;
            switch (typeStr)
            {
                case 'T':
                    {
                        cardType = CardType.HongTao;
                        break;
                    }
                case 'F':
                    {
                        cardType = CardType.FangKuai;
                        break;
                    }
                case 'H':
                    {
                        cardType = CardType.HeiTao;
                        break;
                    }
                case 'M':
                    {
                        cardType = CardType.MeiHua;
                        break;
                    }
            }
            return cardType;
        }
    }

 

2,牌局类CardsGameData设计

变量如下:

        private List<List<Card>> cardCols;           //纸牌列
        private List<Card> cardPile;                 //纸牌堆

        private List<int> CollectAreaTop;           //收集区每种花色
        private int curPilePos;                 //当前牌堆翻开的位置(3的倍数)
        private int curPileTop;                 //当前牌堆顶

        public const int cardTypeNum = 4;

纸牌列:每一列都是一个List<Card>,一共7列。

纸牌堆:右上角的牌堆。

收集区:每一格用一个int表示该花色最大收集到几了。规定四个收集格子的顺序:红桃,方块,黑桃,梅花,即红桃只能放到第0列,方块只能放到第1列

翻开的位置curPilePos:没翻是0,翻1次是3,翻2次是6……

牌堆顶curPileTop:实际最上面一张牌。比如说翻了3次,之后挪走一张,就是8。如果遇到已经挪走的牌,则继续往前挪。反正就是最上面一个能挪动的牌的index

 

  拷贝构造函数和string解析没啥好说的,打乱牌局使用Knuth-Durstenfeld Shuffle打乱算法,时间复杂度O(n)。我之前写过一篇博客专门介绍这个,大致思路就是每次随机一个元素挪到数组最后面,然后缩小随机范围。ps:在打乱时并不会考虑纸牌是否已经翻开这种问题。打乱只是为了输出牌局,真正的牌局是点了求解之后,通过字符串解析的。解析时才设置牌的翻开状态。

完整代码如下:

        #region 生成与读取
        public CardsGameData()
        {

        }

        /// <summary>
        /// 深拷贝构造函数
        /// </summary>
        public CardsGameData(CardsGameData otherData)
        {
            //拷贝纸牌列表
            cardCols = new List<List<Card>>();
            foreach (List<Card> item_Col in otherData.cardCols)
            {
                List<Card> singleCol = new List<Card>();
                foreach (Card item_Card in item_Col)
                {
                    if(item_Card == null)
                    {
                        singleCol.Add(null);
                    }
                    else
                    {
                        singleCol.Add(new Card(item_Card));
                    }
                }
                cardCols.Add(singleCol);
            }
            //拷贝牌堆
            cardPile = new List<Card>();
            foreach (Card item_Card in otherData.cardPile)
            {
                if (item_Card == null)
                {
                    cardPile.Add(null);
                }
                else
                {
                    cardPile.Add(new Card(item_Card));
                }
            }
            //拷贝收集区
            CollectAreaTop = new List<int>();
            foreach (int item in otherData.CollectAreaTop)
            {
                CollectAreaTop.Add(item);
            }
            //拷贝翻牌状态
            curPilePos = otherData.curPilePos;
            curPileTop = otherData.curPileTop;
        }

        /// <summary>
        /// 从字符串读取数据
        /// </summary>
        public CardsGameData(string[] cardColsData,string cardPileData)
        {
            cardCols = new List<List<Card>>();
            cardPile = new List<Card>();
            //读取每一列的数据
            for (int i = 0; i < cardColsData.Length; i++)
            {
                List<Card> cardCol = new List<Card>();
                string[] colData = cardColsData[i].Split(' ');
                for (int index = 0; index < colData.Length; index++)
                {
                    Card card = new Card(colData[index], index == colData.Length - 1);
                    cardCol.Add(card);
                }
                cardCols.Add(cardCol);
            }
            //读取牌堆数据
            string[] pileData = cardPileData.Split(' ');
            for (int index = 0; index < pileData.Length; index++)
            {
                Card card = new Card(pileData[index]);
                cardPile.Add(card);
            }
        }

        /// <summary>
        /// 纯随机纸牌
        /// </summary>
        public void CreateRandomCards()
        {
            //生成52张牌
            List<Card> AllCards = new List<Card>();
            for (int i = 0; i < cardTypeNum; i++)
            {
                for (int j = 1; j <= 13; j++)
                {
                    Card card = new Card((CardType)i, j);
                    AllCards.Add(card);
                }
            }
            KnuthDurstenfeld(AllCards);         //纯随机洗牌
            //把牌放进列表和牌堆
            int temp = 0;
            cardCols = new List<List<Card>>();
            cardPile = new List<Card>();
            for (int col = 0; col < 7; col++)
            {
                List<Card> cardColList = new List<Card>();
                while (cardColList.Count < col + 1)
                {
                    cardColList.Add(AllCards[temp++]);
                }
                cardCols.Add(cardColList);
            }
            for (int index = 0; index < 24; index++)
            {
                cardPile.Add(AllCards[temp++]);
            }
        }

        /// <summary>
        /// Knuth-Durstenfeld Shuffle打乱算法
        /// </summary>
        public void KnuthDurstenfeld<T>(List<T> targetList)
        {
            Random random = new Random();
            for (int i = targetList.Count - 1; i > 0; i--)
            {
                int exchange = random.Next(0, i + 1);
                T temp = targetList[i];
                targetList[i] = targetList[exchange];
                targetList[exchange] = temp;
            }
        }
        #endregion

        #region 输出
        /// <summary>
        /// 输出牌局信息
        /// </summary>
        /// <returns></returns>
        public string PrintGameData()
        {
            string gameStr = "";
            //输出每一列的牌
            for (int col = 0; col < 7; col++)
            {
                for (int i = 0; i < cardCols[col].Count; i++)
                {
                    gameStr += cardCols[col][i].PrintCard();
                    if (i != cardCols[col].Count - 1)
                        gameStr += " ";
                }
                gameStr += "\r\n";
            }
            //输出牌堆的牌
            for (int i = 0; i < cardPile.Count; i++)
            {
                gameStr += cardPile[i].PrintCard();
                if (i != cardPile.Count - 1)
                    gameStr += " ";
            }
            return gameStr;
        }
        #endregion
    }

现在随机生成按钮的相关功能已经完成,来看看效果

 

 

 

四、纸牌移动问题

1,移动操作类CardOperate设计

纸牌的移动分为6种:牌堆翻牌(右上),从牌堆拿牌(右上拿到下面),从牌堆直接收集(右上拿到左上),移动牌(下面一列拿到另一列),从列表收集牌(下面拿到左上),从收集区拿回列表(左上拿到下面)。

具体代码如下:

enum OperateType
    {
        Flop = 0,           //翻牌
        GetFormPile = 1,    //从牌堆拿牌
        DirectionCollect=2, //从牌堆直接收集牌
        Move = 3,           //移动牌
        Collect = 4,        //从列表收集牌
        Back = 5,           //从收集区把牌拿回列表
        Unknown = 6,
    }

    class CardOperate
    {
        public OperateType operateType;
        public int OriIndex;                   //原来挪动的下标
        public int CurIndex;                   //挪动之后的下标

        public CardOperate()
        {

        }

        public CardOperate(OperateType operateType, int OriIndex, int CurIndex)
        {
            this.operateType = operateType;
            this.OriIndex = OriIndex;
            this.CurIndex = CurIndex;
        }

        public string PrintOperate()
        {
            string ope = "";
            switch(operateType)
            {
                case OperateType.Flop:
                    {
                        ope = "F";
                        break;
                    }
                case OperateType.GetFormPile:
                    {
                        ope = "G" + CurIndex;
                        break;
                    }
                case OperateType.DirectionCollect:
                    {
                        ope = "D" + CurIndex;
                        break;
                    }
                case OperateType.Move:
                    {
                        ope = "M" + OriIndex + "_" + CurIndex;
                        break;
                    }
                case OperateType.Collect:
                    {
                        ope = "C" + OriIndex + "_" + CurIndex;
                        break;
                    }
                case OperateType.Back:
                    {
                        ope = "B" + OriIndex + "_" + CurIndex;
                        break;
                    }
            }
            return ope;
        }
    }

输出格式和牌类似,前面是操作首字母,后面数字是挪之前的下标,挪之后的下标(两个数字用_分隔,可以没有数字)

例如:翻牌——F,从牌堆直接收集红桃A——D0(之前规定了红桃收集到第0列),从第2列移动到第4列——M2_4

 

2,牌局类CardsGameData的移动函数

  再回到之前的牌局类,需要两大功能:检查当前局面能够进行哪些操作;对当前局面进行一步具体操作。

  我们规定:翻牌之后必须操作右上角的牌堆。这样可以避免很多冗余的分支(比如说我翻一下牌堆,然后又回到下面移动牌列。这样其实和先移动牌列,再翻牌的分支重复)。虽然说AStar算法会对相同局面重新规划路线,但是这无疑浪费了大量的计算(大约70%)。

  之前使用深度优先搜索时,搜索树分支的顺序会影响搜索,所以在获取当前局面所有操作时,有先后顺序。这个顺序在AStar算法中应该是不会产生影响的。

  具体代码如下:

        #region 游戏操作
        /// <summary>
        /// 初始化游戏
        /// </summary>
        public void InitGame()
        {
            CollectAreaTop = new List<int>{ 0, 0, 0, 0 };
            curPilePos = 0;
            curPileTop = 0;
        }

        /// <summary>
        /// 获取当前局面的所有操作
        /// </summary>
        /// <param name="OnlyPileOperates">只允许牌堆操作</param>
        /// <returns></returns>
        public List<CardOperate> GetAllOperates(bool OnlyPileOperates = false)
        {
            List<CardOperate> AllOperates = new List<CardOperate>();
            //获取当前牌堆顶的牌
            Card curPileCard = null;
            if (curPileTop > 0 && curPileTop <= cardPile.Count)
            {
                curPileCard = cardPile[curPileTop - 1];
            }
            //1.优先把牌收集上去——从牌堆直接收集
            if (curPileCard != null)
            {
                if (CollectAreaTop[(int)curPileCard.cardType] == curPileCard.num - 1)
                {
                    CardOperate curOperate = new CardOperate(OperateType.DirectionCollect, 0, (int)curPileCard.cardType);
                    AllOperates.Add(curOperate);
                }
            }

            if(!OnlyPileOperates)
            {
                //2.优先把牌收集上去——从列表收集
                for (int col = 0; col < cardCols.Count; col++)
                {
                    if (cardCols[col].Count > 0)
                    {
                        Card endCard = cardCols[col][cardCols[col].Count - 1];  //最底下一张牌
                        if (CollectAreaTop[(int)endCard.cardType] == endCard.num - 1)
                        {
                            CardOperate curOperate = new CardOperate(OperateType.Collect, col, (int)endCard.cardType);
                            AllOperates.Add(curOperate);
                        }
                    }
                }

                //3.移动牌
                for (int col = 0; col < cardCols.Count; col++)
                {
                    for (int index = cardCols[col].Count - 1; index >= 0; index--)
                    {
                        //判断这张牌是否能能带着下面的牌一起动
                        bool canMove = false;
                        //暂时让最上面的K不动
                        if(index == 0 && cardCols[col][index].num == 13)
                        {
                            canMove = false;
                        }
                        else if (index == cardCols[col].Count - 1)
                        {
                            canMove = true;            //最后一张牌肯定能动
                        }
                        else
                        {
                            if (!cardCols[col][index].canMove)
                            {
                                canMove = false;                //还没翻开的牌
                            }
                            else
                            {
                                canMove = cardCols[col][index + 1].IsOneGroup(cardCols[col][index]);
                            }
                        }
                        //看看可移动的牌能不能移到其他地方
                        if (canMove)
                        {
                            for (int otherCol = 0; otherCol < cardCols.Count; otherCol++)
                            {
                                if (otherCol == col)
                                {
                                    continue;
                                }

                                if (CheckMove(cardCols[col][index], otherCol))
                                {
                                    CardOperate curOperate = new CardOperate(OperateType.Move, col, otherCol);
                                    AllOperates.Add(curOperate);
                                }
                            }
                        }
                        else
                        {
                            break;              //一张牌不能动,上面肯定也不能动
                        }
                    }
                }
            }

            //4.从牌堆拿牌
            if (curPileCard != null)
            {
                for (int col = 0; col < cardCols.Count; col++)
                {
                    if (CheckMove(curPileCard, col))
                    {
                        CardOperate curOperate = new CardOperate(OperateType.GetFormPile, 0, col);
                        AllOperates.Add(curOperate);
                    }
                }
            }

            //5.翻牌
            if (cardPile.Count > 0)
            {
                CardOperate curOperate = new CardOperate(OperateType.Flop, 0, 0);
                AllOperates.Add(curOperate);
            }

            if (!OnlyPileOperates)
            {
                //6.从收集槽拿回来
                for (int i = 0; i < CollectAreaTop.Count; i++)
                {
                    if (CollectAreaTop[i] == 0)
                    {
                        continue;               //牌堆已经空了
                    }
                    CardType cur_CardType = (CardType)i;
                    for (int col = 0; col < cardCols.Count; col++)
                    {
                        if (cardCols[col].Count == 0)
                        {
                            continue;               //一般情况下,已经收集的K不会拿到空槽
                        }
                        Card endCard = cardCols[col][cardCols[col].Count - 1];          //某列的最后一张牌
                        if (endCard.IsDifferentColor(cur_CardType) && (CollectAreaTop[i] + 1 == endCard.num))
                        {
                            CardOperate curOperate = new CardOperate(OperateType.Back, i, col);
                            AllOperates.Add(curOperate);
                        }
                    }
                }
            }

            return AllOperates;
        }

        /// <summary>
        /// 进行一步操作
        /// </summary>
        public bool DoOperate(CardOperate cardOperate)
        {
            switch(cardOperate.operateType)
            {
                //翻牌
                case OperateType.Flop:
                    {
                        curPilePos += 3;
                        if(curPilePos - cardPile.Count >= 3)
                        {
                            curPilePos = 0;
                            //移除空元素
                            cardPile.RemoveAll(card => card == null);
                        }
                        
                        if(curPilePos > cardPile.Count)
                        {
                            curPileTop = cardPile.Count;        //结尾不足3张,牌堆顶是最后一张
                        }
                        else
                        {
                            curPileTop = curPilePos;            //其他情况牌堆顶和翻牌位置保持一致
                        }
                        break;
                    }
                //从牌堆拿牌
                case OperateType.GetFormPile:
                    {
                        Card curPileCard = GetCardFromPile();           //从牌堆取一张牌
                        if(curPileCard == null)
                        {
                            return false;
                        }

                        //把牌从牌堆挪下来
                        if (CheckMove(curPileCard, cardOperate.CurIndex))
                        {
                            cardCols[cardOperate.CurIndex].Add(curPileCard);
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                //直接从牌堆收集
                case OperateType.DirectionCollect:
                    {
                        Card curPileCard = GetCardFromPile();           //从牌堆取一张牌
                        if (curPileCard == null)
                        {
                            return false;
                        }

                        //挪到收集区,收集区只存最大数字,+1即可
                        if ((int)curPileCard.cardType == cardOperate.CurIndex
                            && CollectAreaTop[cardOperate.CurIndex] + 1 == curPileCard.num)
                        {
                            CollectAreaTop[cardOperate.CurIndex]++;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                //在两列之间移动牌
                case OperateType.Move:
                    {
                        bool checkMoveSuccess = false;
                        int index = cardCols[cardOperate.OriIndex].Count - 1;
                        for (; index >= 0; index--)
                        {
                            //判断这张牌是否能能带着下面的牌一起动
                            bool canMove = false;
                            if (index == cardCols[cardOperate.OriIndex].Count - 1)
                            {
                                canMove = true;            //最后一张牌肯定能动
                            }
                            else
                            {
                                if (!cardCols[cardOperate.OriIndex][index].canMove)
                                {
                                    canMove = false;                //还没翻开的牌
                                }
                                else
                                {
                                    canMove = cardCols[cardOperate.OriIndex][index + 1].IsOneGroup(cardCols[cardOperate.OriIndex][index]);
                                }
                            }
                            //看看可移动的牌能不能移到目标列
                            if (canMove)
                            {
                                checkMoveSuccess = CheckMove(cardCols[cardOperate.OriIndex][index], cardOperate.CurIndex);
                                if (checkMoveSuccess)
                                {
                                    break;
                                }
                            }
                            else
                            {
                                break;              //一张牌不能动,上面肯定也不能动
                            }
                        }

                        //取出之前一列的需要移动的一组牌
                        if (checkMoveSuccess)
                        {
                            //把牌加入另一列
                            for (int i = index; i < cardCols[cardOperate.OriIndex].Count; i++)
                            {
                                cardCols[cardOperate.CurIndex].Add(cardCols[cardOperate.OriIndex][i]);
                            }
                            //移除之前一列的牌
                            cardCols[cardOperate.OriIndex].RemoveRange(index, cardCols[cardOperate.OriIndex].Count - index);
                        }
                        else
                        {
                            return false;
                        }    

                        //翻开上一张牌
                        if(cardCols[cardOperate.OriIndex].Count > 0)
                        {
                            cardCols[cardOperate.OriIndex][cardCols[cardOperate.OriIndex].Count - 1].canMove = true;
                        }
                        break;
                    }
                //从列表收集牌
                case OperateType.Collect:
                    {
                        //取出之前一列的最后一张牌
                        if (cardCols[cardOperate.OriIndex].Count == 0)
                        {
                            return false;
                        }
                        Card endCard = cardCols[cardOperate.OriIndex][cardCols[cardOperate.OriIndex].Count - 1];
                        cardCols[cardOperate.OriIndex].Remove(endCard);
                        //翻开上一张牌
                        if (cardCols[cardOperate.OriIndex].Count > 0)
                        {
                            cardCols[cardOperate.OriIndex][cardCols[cardOperate.OriIndex].Count - 1].canMove = true;
                        }

                        //挪到收集区,收集区只存最大数字,+1即可
                        if ((int)endCard.cardType == cardOperate.CurIndex
                            && CollectAreaTop[cardOperate.CurIndex] + 1 == endCard.num)
                        {
                            CollectAreaTop[cardOperate.CurIndex]++;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                //从收集区挪回列表
                case OperateType.Back:
                    {
                        Card backCard = new Card((CardType)cardOperate.OriIndex, CollectAreaTop[cardOperate.OriIndex]);
                        //把牌挪回目标列
                        if (CheckMove(backCard, cardOperate.CurIndex))
                        {
                            cardCols[cardOperate.CurIndex].Add(backCard);
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
            }
            return true;
        }

        /// <summary>
        /// 从牌堆取一张牌
        /// </summary>
        public Card GetCardFromPile()
        {
            Card curPileCard = null;
            if (curPileTop > 0 && curPileTop <= cardPile.Count)
            {
                curPileCard = cardPile[curPileTop - 1];
                cardPile[curPileTop - 1] = null;
            }

            //寻找牌堆的上一张牌
            int index = curPileTop - 1;
            for (; index > 0; index--)
            {
                if (cardPile[index - 1] != null)
                {
                    break;
                }
            }
            curPileTop = index;
            return curPileCard;
        }

        /// <summary>
        /// 检查某张牌是否能挪到另一列
        /// </summary>
        /// <returns></returns>
        public bool CheckMove(Card card,int targetColIndex)
        {
            bool canMove = false;
            if (cardCols[targetColIndex].Count == 0)
            {
                //空槽只能挪K
                canMove = card.num == 13;
            }
            else
            {
                //校验上下两张牌是否是同一种颜色
                canMove = card.IsOneGroup(cardCols[targetColIndex][cardCols[targetColIndex].Count - 1]);
            }
            return canMove;
        }
        #endregion

 

五、牌局类剩余问题

牌局类还剩下一些小问题:

  比较两个局面是否相同:按照收集区、牌堆、纸牌列的顺序依次比较。

 

  是否过关:查看收集区每一列是否都收集到了13

 

  计算局面分后面再一起说,先把这块代码贴上:

 

        #region 比较
        /// <summary>
        /// 比较两个局面是否相同
        /// </summary>
        public bool EqualsTo(CardsGameData other)
        {
            //比较收集区,比较翻牌位置
            for (int i = 0; i < CollectAreaTop.Count; i++)
            {
                if (CollectAreaTop[i] != other.CollectAreaTop[i])
                    return false;
            }
            if (curPilePos != other.curPilePos)
                return false;
            if (curPileTop != other.curPileTop)
                return false;

            //比较纸牌堆
            if (cardPile.Count != other.cardPile.Count)
                return false;
            for (int i = 0; i < cardPile.Count; i++)
            {
                if (!Card.IsTwoCardEqual(cardPile[i], other.cardPile[i]))
                    return false;
            }

            //比较纸牌列
            for (int col = 0; col < cardCols.Count; col++)
            {
                if (cardCols[col].Count != other.cardCols[col].Count)
                    return false;
                for (int i = 0; i < cardCols[col].Count; i++)
                {
                    if (!Card.IsTwoCardEqual(cardCols[col][i], other.cardCols[col][i]))
                        return false;
                }
            }
            return true;
        }

        /// <summary>
        /// 检查是否过关
        /// </summary>
        public bool CheckSuccess()
        {
            for (int i = 0; i < CollectAreaTop.Count; i++)
            {
                if (CollectAreaTop[i] != 13)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// 计算局面分
        /// </summary>
        public int GetScore()
        {
            int score = 0;
            //收集区每收集一张+14分,四张全部收集+100分
            int min = 100;
            for (int i = 0; i < CollectAreaTop.Count; i++)
            {
                if(CollectAreaTop[i]<min)
                {
                    min = CollectAreaTop[i];
                }
            }
            score += 100 * min;
            for (int i = 0; i < CollectAreaTop.Count; i++)
            {
                //score += 15 * (CollectAreaTop[i] - min);
                //每超出1级-4分,避免某一种花色收集太多
                int addScore = 14;
                for (int collectNum = min + 1; collectNum <= CollectAreaTop[i]; collectNum++)
                {
                    score += addScore;
                    if (addScore > 4)
                    {
                        addScore -= 4;
                    }
                }
            }

            //牌列有序+8分,否则-2分
            for (int col = 0; col < cardCols.Count; col++)
            {
                for (int i = cardCols[col].Count - 1; i > 0; i--)
                {
                    if (cardCols[col][i - 1].canMove && cardCols[col][i].IsOneGroup(cardCols[col][i - 1]))
                    {
                        score += 8;
                    }
                    else
                    {
                        score -= 2;
                    }
                }
                if (cardCols[col].Count > 0)
                {
                    if (cardCols[col][0].canMove && cardCols[col][0].num == 13)
                    {
                        score += 8;
                    }
                    else
                    {
                        score -= 2;
                    }
                }
            }
            return score;
        }
        #endregion

 

六、节点评分和搜索节点类AStarSearchNode设计

1,节点评分问题

  AStar算法的节点评分是F=H+G,H是当前节点距离终点的期望,也就是局面分。G是步数消耗,即从初始点走过来消耗的代价。每次展开时,优先展开评分最高的节点。注意评分增减要平衡,不能增长过快,否则会一条路走到黑,就和深度优先差不多了。

  最初,我设计的评分规则是:

H:
  四张牌全部收集:+100分
  收集了单独一张牌:+15分
  牌列有序(能带着下面一起挪):每张+8分
  牌列无序:每张-2分
G:
  走1步:-4分
  把牌从收集区挪回去:-40分

  

  后来发现有2个问题,一是K不会优先挪到空列,二是只要一有机会就往收集区挪,常常导致收集区某个花色堆得特别高,然后解不出来。于是优化出了二代评分:

H:
  四张牌全部收集到n:+100n分
  收集了单独一张牌x:+14-4(x-n-1)分,最低为2分,不会出现负分
  牌列有序(能带着下面一起挪):每张+8分
  牌列无序:每张-2分
  每列最顶上一张是K(且已翻开)+8分,否则-2分
G:
  走1步:-4分
  把牌从收集区挪回去:-40分

2,节点类设计

  AStar搜索时,经常会更换父节点,此时需要刷新当前节点所有子节点的得分,直接递归深度优先遍历即可。完整代码如下:

  class AStarSearchNode
    {
        public CardsGameData gameData;              //当前局面
        public CardOperate curOperate;              //经过何种操作到达当前局面
        public List<AStarSearchNode> childNodes;    //操作一步可以达到的子节点

        public int depth;               //当前节点的搜索深度
        public int score_H;             //当前游戏的局面分
        public int score_G;             //得分的步数修正
        public int score_Final;         //最终得分

        public AStarSearchNode fatherNode;         //父节点
        public bool isOpen;             //当前节点是否已经展开

        public AStarSearchNode(CardsGameData gameData)
        {
            //构建根节点
            this.gameData = gameData;

            depth = 0;
            score_H = gameData.GetScore();            //计算局面分
            score_G = 0;
            score_Final = score_G + score_H;            //最终得分
            isOpen = false;
        }

        public AStarSearchNode(CardsGameData gameData,AStarSearchNode father,CardOperate curOperate)
        {
            this.gameData = gameData;
            this.fatherNode = father;
            this.curOperate = curOperate;

            depth = father.depth + 1;
            score_H = gameData.GetScore();            //计算局面分
            if(curOperate.operateType == OperateType.Back)
            {
                score_G = fatherNode.score_G - 40;              //挪回去-40分,不鼓励往回挪
            }
            else
            {
                score_G = fatherNode.score_G - 4;             //每走一步-4分,减太多了展不开,减太少一条路走到黑
            }
            score_Final = score_G + score_H;            //最终得分
            isOpen = false; 
        }

        #region 节点展开与子节点操作
        /// <summary>
        /// 展开节点
        /// </summary>
        public void OpenNode()
        {
            if(isOpen)
            {
                return;         //该节点已经展开
            }

            List<CardOperate> allOperates;
            if (curOperate==null)
            {
                allOperates = gameData.GetAllOperates();            //根节点直接展开
            }    
            else
            {
                bool isFlop = curOperate.operateType == OperateType.Flop;
                allOperates = gameData.GetAllOperates(isFlop);
            }

            childNodes = new List<AStarSearchNode>();
            foreach (var item in allOperates)
            {
                CardsGameData childGame = new CardsGameData(gameData);          //拷贝一份
                childGame.DoOperate(item);                                  //构建子游戏局面

                AStarSearchNode childNode = new AStarSearchNode(childGame, this,item);       //构建子节点
                childNodes.Add(childNode);
            }
            isOpen = true;
        }

        /// <summary>
        /// 更改父节点
        /// </summary>
        public void ChangeFather(AStarSearchNode father, Action<AStarSearchNode> OnChangeScore)
        {
            this.fatherNode = father;
            RefreshScore(OnChangeScore);
        }

        /// <summary>
        /// 刷新得分
        /// </summary>
        private void RefreshScore(Action<AStarSearchNode> OnChangeScore)
        {
            if (curOperate.operateType == OperateType.Back)
            {
                score_G = fatherNode.score_G - 40;              //挪回去-40分,不鼓励往回挪
            }
            else
            {
                score_G = fatherNode.score_G - 4;             //每走一步-4分,减太多了展不开,减太少一条路走到黑
            }
            score_Final = score_G + score_H;            //最终得分

            if(isOpen)
            {
                foreach (var item in childNodes)
                {
                    item.RefreshScore(OnChangeScore);
                }
            }
            else
            {
                OnChangeScore?.Invoke(this);             //未开启的节点需要调整在开启列表的顺序
            }
        }

        /// <summary>
        /// 移除子节点
        /// </summary>
        public void RemoveChild(AStarSearchNode child)
        {
            childNodes.Remove(child);
        }

        /// <summary>
        /// 移除子节点
        /// </summary>
        public void RemoveChild(List<AStarSearchNode> childs)
        {
            foreach (var item in childs)
            {
                RemoveChild(item);
            }
        }
        #endregion

        /// <summary>
        /// 获取节点数量
        /// </summary>
        public int GetNodeNum()
        {
            int num = 1;
            if(isOpen)
            {
                foreach (var item in childNodes)
                {
                    num += item.GetNodeNum();
                }
            }
            return num;
        }
    }

 

七,AStar算法设计

1,开启列表问题

  开启列表使用链表LinkedList<AStarSearchNode>存储,主要是为了方便插入和删除。

  开启列表降序排列,每次取出第一个节点进行展开。

  每次新增局面时都需要排序,自然联想到插入排序。链表的插入排序很简单,找到要插入的节点之间往后面插入就行了。

2,相同局面问题

  从动辄几万,几十万的搜索树中排查是否有相同局面是非常困难的,但是局面相同的前提是局面分H相同,所以把相同局面分的节点放在一起,使用Dictionary<int, List<AStarSearchNode>>存储,方便比较。

  当遇到相同局面时,保留深度较浅的局面。当加入新节点时,如果有重复节点,先看哪个深度浅。如果重复节点深度浅,则直接把刚加入的节点移除就完事了。如果当前节点深度浅,不能直接移除重复的节点,因为重复节点很可能展开过,那样展开的搜索树就没了,所以必须把重复节点整个挪过来。这也就是AStar算法中的重新规划路线。

  我们规定,挪动节点时,未开启节点在2个及以下的,在开启列表删除,重新插入排序。未开启节点大于2个的,对整个开启列表重新排序。可以调用链表LinkedList自带的OrderByDescending函数进行降序排序。(这里的2其实影响不大,设置成1,3,5差距都不太大)

3,无解的情况

  除了那种开局就挪不动的,其实很难算到无解,因为搜索树实在太大了。大概算个半小时一小时还算不出来的,多半就无解了。

具体代码如下:

    class AStarGameAnalyze
    {
        public Form_Main mainForm;      //主界面索引
        private CardsGameData OriGameData;          //原始游戏数据

        //求解信息
        private Thread thread = null;
        public AStarSearchNode rootNode;            //根节点
        public LinkedList<AStarSearchNode> openList;        //开启列表
        public Dictionary<int, List<AStarSearchNode>> ScoreNodeDict;            //根据局面分查找节点的表

        DateTime startTime;
        /// 停止当前计算
        /// </summary>
        public void StopAnalyze()
        {
            if (thread != null && thread.IsAlive)
            {
                thread.Abort();                 //Framework框架直接杀线程即可,无需挂后台
            }
        }

        #region 排序和查找
        /// <summary>
        /// 打开列表插入排序,得分大的在前面,方便取出和移除
        /// </summary>
        public void AddToOpenList(AStarSearchNode curNode)
        {
            LinkedListNode<AStarSearchNode> tempNode = openList.First;
            while(tempNode!=null)
            {
                if (tempNode.Value.score_Final >= curNode.score_Final)
                {
                    tempNode = tempNode.Next;
                }
                else
                {
                    openList.AddBefore(tempNode, curNode);              //往前加
                    return;
                }
            }
            //没加进去,说明新节点得分最小,放在最后
            openList.AddLast(curNode);
        }

        /// <summary>
        /// 节点得分更新后刷新位置
        /// </summary>
        public void RefreshNodePosInOpenList(AStarSearchNode curNode)
        {
            openList.Remove(curNode);
            AddToOpenList(curNode);
        }

        /// <summary>
        /// 节点得分更新后刷新位置
        /// </summary>
        public void RefreshNodePosInOpenList(List<AStarSearchNode> curNodes)
        {
            if (curNodes.Count == 0)
                return;
            foreach (var item in curNodes)
            {
                openList.Remove(item);
            }
            foreach (var item in curNodes)
            {
                AddToOpenList(item);
            }
        }

        /// <summary>
        /// 将节点插入得分列表
        /// </summary>
        public void AddNodeToScoreDict(AStarSearchNode curNode)
        {
            //获取得分表
            List<AStarSearchNode> scoreList;
            ScoreNodeDict.TryGetValue(curNode.score_H, out scoreList);
            if(scoreList==null)
            {
                //没有相应得分的表,创建一个新的
                scoreList = new List<AStarSearchNode>();
                scoreList.Add(curNode);
                ScoreNodeDict.Add(curNode.score_H, scoreList);
            }
            else
            {
                //加入已有的得分表
                scoreList.Add(curNode);
            }
        }

        /// <summary>
        /// 查找重复节点
        /// </summary>
        public AStarSearchNode FindRepeatNodeFromScoreDict(AStarSearchNode curNode)
        {
            List<AStarSearchNode> scoreList;
            ScoreNodeDict.TryGetValue(curNode.score_H, out scoreList);
            if (scoreList == null)
            {
                //没有相应得分的表,不存在重复的
                return null;
            }
            else
            {
                //遍历得分表,查看有无重复元素
                foreach (var item in scoreList)
                {
                    if(item.gameData.EqualsTo(curNode.gameData))
                    {
                        return item;
                    }
                }
                return null;
            }
        }
        #endregion

        #region 求解相关
        /// <summary>
        /// 求解
        /// </summary>
        public void SolveGame(Form_Main mainForm, CardsGameData gameData)
        {
            this.mainForm = mainForm;
            this.OriGameData = gameData;
            mainForm.ClearConcole();
            mainForm.AddConcole("开始A*求解");
            startTime = DateTime.Now;
            //TrySolveGame();
            thread = new Thread(TrySolveGame);
            thread.Start();
        }

        /// <summary>
        /// 尝试求解
        /// </summary>
        public void TrySolveGame()
        {
            OriGameData.InitGame();
            rootNode = new AStarSearchNode(OriGameData);
            rootNode.OpenNode();
            openList = new LinkedList<AStarSearchNode>();           //开启列表
            ScoreNodeDict = new Dictionary<int, List<AStarSearchNode>>();           //得分列表,用于查找重复节点

            AddNodeToScoreDict(rootNode);
            foreach (var item in rootNode.childNodes)
            {
                AddToOpenList(item);
                AddNodeToScoreDict(item);
            }

            AStarSearchNode resultNode = null;
            AStarSearchNode depthNode = rootNode;
            int step = 0;
            int depth = 0;
            int repeatNodeNum = 0;
            //AStar搜索
            while (openList.Count > 0 && resultNode == null)
            {
                step++;
                if (step % 1000 == 0)
                {
                    mainForm.AddConcole("已进行" + step + "次计算,当前搜索树大小:" + rootNode.GetNodeNum() +
                        ",最大搜索深度:" + depth + ",重复节点数量:" + repeatNodeNum);
                }
                if (step % 10000 == 0)
                {
                    mainForm.AddConcole("最深处节点路径:");
                    mainForm.AddConcole(PrintNodePath(depthNode));
                }

                //已经插入排序,最高的节点是第一个
                AStarSearchNode maxNode = openList.First.Value;

                //展开对应的节点
                //openList.Remove(maxNode);                   //移除已经展开的节点
                openList.RemoveFirst();                   //移除已经展开的节点
                maxNode.OpenNode();
                if(maxNode.depth > depth)
                {
                    depth = maxNode.depth;
                    depthNode = maxNode;
                }

                List<AStarSearchNode> removeChildList = new List<AStarSearchNode>();        //需要移除的子节点
                for (int i = 0; i < maxNode.childNodes.Count; i++)
                {
                    //检查子节点是否重复
                    AStarSearchNode repeatNode = FindRepeatNodeFromScoreDict(maxNode.childNodes[i]);
                    if (repeatNode != null)
                    {
                        repeatNodeNum++;
                        if (repeatNode.depth <= maxNode.childNodes[i].depth)
                        {
                            //重复节点深度更浅,移除当前节点
                            removeChildList.Add(maxNode.childNodes[i]);
                        }
                        else
                        {
                            //当前节点深度更浅,把重复节点整体挪过来
                            repeatNode.fatherNode.RemoveChild(repeatNode);
                            repeatNode.curOperate = maxNode.childNodes[i].curOperate;
                            maxNode.childNodes[i] = repeatNode;

                            //repeatNode.ChangeFather(maxNode, curNode => RefreshNodePosInOpenList(curNode));
                            List<AStarSearchNode> refreshNodes = new List<AStarSearchNode>();
                            repeatNode.ChangeFather(maxNode, curNode => refreshNodes.Add(curNode));
                            openList.OrderByDescending(item => item.score_Final);
                            if (refreshNodes.Count <= 2)
                            {
                                RefreshNodePosInOpenList(refreshNodes);
                            }
                            else
                            {
                                openList.OrderByDescending(item => item.score_Final);
                            }
                        }
                    }
                    else
                    {
                        AddToOpenList(maxNode.childNodes[i]);                 //将子节点加入开启列表
                        AddNodeToScoreDict(maxNode.childNodes[i]);              //将子节点加入得分表,便于后续查找重复节点
                    }

                    //检查子节点是否有完成的
                    if (maxNode.childNodes[i].gameData.CheckSuccess())
                    {
                        resultNode = maxNode.childNodes[i];
                    }
                }
                maxNode.RemoveChild(removeChildList);               //移除重复的子节点
            }

            //统计计算时间
            DateTime curTime = DateTime.Now;
            var deltaTime = curTime - startTime;
            mainForm.AddConcole("总时间" + deltaTime.TotalSeconds + "s");
            //判断是否无解
            if (resultNode == null)
            {
                mainForm.AddConcole("无解");
            }
            else
            {
                mainForm.AddConcole("已找到一个解,步数=" + resultNode.depth + ",当前解如下:");
                mainForm.AddConcole(PrintNodePath(resultNode));
            }
        }
        #endregion

        #region 输出
        /// <summary>
        /// 输出某个节点的路径
        /// </summary>
        public string PrintNodePath(AStarSearchNode targetNode)
        {
            string res = "";
            Stack<CardOperate> pathStack = new Stack<CardOperate>();            //路径栈
            AStarSearchNode curNode = targetNode;
            //从终点开始,将路径倒着入栈
            while (curNode.fatherNode != null)
            {
                pathStack.Push(curNode.curOperate);
                curNode = curNode.fatherNode;
            }
            //起点的路径在栈顶了,输出路径
            while(pathStack.Count>0)
            {
                CardOperate curOperate = pathStack.Pop();
                res += curOperate.PrintOperate() + " ";
            }
            return res;
        }
        #endregion
    }

  最后再补充一点,关于停止线程的,Abort函数只能用在Framework里,如果是.net Core,请把线程扔到后台,这个问题之前在数织游戏(Nonogram)求解的帖子中说过。

  来看一下效果:

 

 

八、优化思路

1,节点评分那块还可以优化,我最初写的评分算法很烂,算几个小时都算不出一局。后来经过多次优化才有了现在的评分算法,不过仍然有很大的优化空间。如果能把局面分H打得更散,不仅仅是走的分支不同,查找相同局面的效率也会提升。

2,开启列表可以不要链表,改用二叉堆,在插入和删除上都可以提升效率

 

posted on 2022-02-08 17:07  草莓♭布丁  阅读(982)  评论(0编辑  收藏  举报

Live2D