华容道程序制作(解题部分)

华容道游戏制作成后(请参见华容道制作基本部分),接下来要尝试的是让计算机自动解题。解题的基本思路是采用穷举走法,列举每走一步可能出现的所有情况,直至成功移出或已经没有新局面出现(即本题无解了)为止。

 
1.在进行解题之前先要对之前做的Game类进行一些补充:为了判断当前游戏是否是一种新的情况,需要为Game类补充Equals和GetHashCode方法。
public override bool Equals(object obj)
        {
            if (obj == null || !(obj is Game)) { return false; }
            Game g = (Game)obj;
            if (finishPoint != g.finishPoint) { return false; } // 结束点必须相同
            if (Blocks.Count == 0 && g.Blocks.Count == 0) { return true; } // 都为空游戏
            if (Blocks.Count != g.Blocks.Count) { return false; } // 块数量必须相同
            if (!Blocks[0].Equals(g.Blocks[0])) { return false; } // 主要块位置必须相同
 
            for (int i = 1; i < Blocks.Count; i++)
            {
                if (!g.Blocks.Contains(Blocks[i])) { return false; }
            }
            return true;
        }
 
        public override int GetHashCode()
        {
            int hash = 0;
            foreach (Block b in Blocks)
            {
                hash += b.GetHashCode();
            }
            hash += finishPoint.GetHashCode();
            return hash;
        }
这里要注意一下比较的方法,Equals方法中并没有逐一比较集合中每个序号的块是否相同(0号除外,因为是比较特殊的),而是看另一个游戏的块集合中是否包含所有本游戏集合中的块(是否包含是由Block类的Equals方法决定的,而Block类的Equals方法中仅比较类型和位置)。这是因为有可能在走过几步后两个同类型的方块位置互换了,但这对华容道来说并没有本质的区别,应当视作同一种局面,因此将其作为相等能为之后解题判断是否出现过这种局面增加速度。
 
2.自动解题需要有记录解题步骤的方法,在这里将新建MoveStep(一个步骤)和MoveProcess(一系列步骤)两个类来用于对解题步骤的记录:
MoveStep类相对比较简单,打包一个移动块的编号和一个移动方向:
         public class MoveStep
         {
                  public int MoveBlockId; // 移动块 Id
                  public Direction MoveDirection; // 移动方向
                  public MoveStep(int id, Direction dir)
                  {
                           MoveBlockId = id; MoveDirection = dir;
                  }
 
                  public override string ToString()
                  {
                           return MoveBlockId + " >> " + MoveDirection.ToString();
                  }
         }
 
MoveProcess类则通过链的方式记录一系列步骤,包含之前一系列步骤的引用,然后再添加一步,并保留一个进行过这些步骤以后的游戏局面引用,以表示这一系列步骤后游戏进行到了怎样的局面。两个构造方法中仅Game作为参数的创建一个步骤序列头,其中之前的步骤和最后的步骤都为null(因为定义CurrentGame是移动后的游戏局面,因此要保留最初的游戏局面,则作为头的MoveProcess对象必须没有LastStep)。
MoveProcess中的GetSteps方法用回溯的方法查找链中所有的步骤,将它们制作成一个有序集合。通过这个方法就可以读取整个进行过程中的每个步骤。
         public class MoveProcess
         {
                  public MoveProcess ProcessBefore; // 之前序列
                  public MoveStep LastStep; // 最后一步
                  public Game CurrentGame; // 最后一步进行后的游戏
 
                  public MoveProcess(Game orgGame)
                  {
                           ProcessBefore = null;
                           LastStep = null;
                           CurrentGame = orgGame;
                  }
 
                  public MoveProcess(MoveProcess processBefore, MoveStep lastStep, Game currentGame)
                  {
                           ProcessBefore = processBefore;
                           LastStep = lastStep;
                           CurrentGame = currentGame;
                  }
 
                  ///<summary>
                  /// 获取所有步骤的集合
                  ///</summary>
                  ///<returns></returns>
                  public List<MoveStep> GetSteps()
                  {
                           List<MoveStep> steps = new List<MoveStep>();
                           MoveProcess process = this;
                           while (process.ProcessBefore != null)
                           {
                                    steps.Add(process.LastStep);
                                    process = process.ProcessBefore;
                           }
                           steps.Reverse();
                           return steps;
                  }
 
                  ///<summary>
                  /// 获取对应的初始游戏
                  ///</summary>
                  ///<returns></returns>
                  public Game GetOriginGame()
                  {
                           MoveProcess process = this;
                           while (process.ProcessBefore != null)
                           {
                                    process = process.ProcessBefore;
                           }
                           return process.CurrentGame;
                  }
 
                  public override string ToString()
                  {
                           StringBuilder sb = new StringBuilder();
                           List<MoveStep> steps = GetSteps();
                           sb.Append("All together " + steps.Count + " Steps:/r/n/r/n");
                           int count = 0;
                           foreach (MoveStep ms in steps)
                           {
                                    sb.Append("Step" + (++count) + ": " + ms.ToString() + "/r/n");
                           }
                           return sb.ToString();
                  }
         }
}
3.解题类Solver
最关键的部分在解题类Solver部分,其主要部分为SolveGame方法,该方法接受一个Game对象为参数,返回该游戏局面解决需要的步骤,或null表示该游戏无解:
    public class Solver
    {
        static Direction[] directions = new Direction[] { Direction.Up, Direction.Down, Direction.Left, Direction.Right };
 
        ///<summary>
        /// 解决一个华容道问题
        ///</summary>
        ///<param name="game"> 华容道游戏 </param>
        ///<returns> 解决步骤 </returns>
        public MoveProcess SolveGame(Game game)
        {
            int blockCount = game.Blocks.Count; // 游戏中有几个块
            Dictionary<Game, object> appearedGames = new Dictionary<Game, object>(); // 出现过的游戏局面
            appearedGames.Add(game, null);
            List<MoveProcess> currentMoveProcesses = new List<MoveProcess>(); // 本轮处理的步骤组
            MoveProcess mp = new MoveProcess(game);
            currentMoveProcesses.Add(mp);
 
            // 完全搜索可能走法,出现新布局则记录走法
            while (true)
            {
                if (currentMoveProcesses.Count == 0) { return null; } // 无需要处理的即无解,退出返回
                List<MoveProcess> newMoveProcesses = new List<MoveProcess>(); // 下一轮要处理的移动步骤组
                Game newGame = null;
                foreach (MoveProcess process in currentMoveProcesses) // 每个要处理步骤组
                {
                    for (int blockId = 0; blockId < blockCount; blockId++) // 尝试每一块
                    {
                        foreach (Direction direction in directions) // 尝试每个方向
                        {
                            if (newGame == null) { newGame = new Game(process.CurrentGame); } // 复制一份游戏
                            if (newGame.MoveBlock(blockId, direction))
                            {
                                if (!appearedGames.ContainsKey(newGame)) // 运动后成为新布局
                                {
                                    MoveStep newStep = new MoveStep(blockId, direction);
                                    MoveProcess newProcess = new MoveProcess(process, newStep, newGame);
                                    if (newGame.GameWin())
                                    {
                                        return newProcess;
                                    }
                                    newMoveProcesses.Add(newProcess);
                                    appearedGames.Add(newGame, null);
                                }
                                newGame = null; // 如果移动过了要重新复制一份游戏
                            }
                        }
                    }
                    newGame = null; // 换下一个走法时也要重新复制游戏
                }
                currentMoveProcesses = newMoveProcesses; // 本轮完毕后本轮结果作为下轮开始
            }
        }
    }
 
解决华容道问题的基本思路是在现有游戏局面的基础上尝试每一种走法,直至找到解法或不再出现新的游戏局面为止。在SolveGame方法中,appearedGames集合记录所有出现过的游戏局面(只用其key部分,value在这里无意义),用于判断一种游戏局面是否出现过。currentMoveProcesses集合则是一个MoveProcess集合,表示当前一轮中要处理的所有步骤序列。最初currentMoveProcess只添加了一个空步骤(用MoveProcess类一个参数的构造方法),之后每一轮中,将逐一取出currentMoveProcess中的每一个步骤序列,以各种可能的方式尝试向其添加一步(要尝试任意一块,任意一个方向),如果出现新游戏局面则将新步骤记录在newMoveProcess中作为下一轮要处理的步骤序列。直至如果找到已获胜的游戏则可立即退出循环并返回得到这个获胜游戏的步骤序列,或是最终不再有要处理的步骤序列(即上一轮未找到出现新局面的情况)则表示无解。
这里appearedGames集合不使用List而使用Dictionary是因为List的Contains方法只通过Equals逐一比较,华容道计算到后来几步,出现的局面数很可能要上万甚至更多。逐一比较效率十分低下。而Dictionary使用了Hash算法,能很有效加快比较速度,当然这也要求Game类必须提供合适的GetHashCode方法,这在本程序的Game类中已经实现了。
 
至此Solver类也已经完成了,可以编写代码进行测试一下,不过最好要在SolveGame方法中适当增加一些控制台输出,一般执行一次需要一分钟左右,不添加输出要等很久才会有反应。

当然这只是很基础的一种算法,基本没有做过优化,内存占用较高(其实也还可以,就26m左右,相比经典算法几百k的占用要高很多)速度也相当慢(网上的华容道经典算法只需零点几秒就能算出,这种算法可能要1分钟左右),网上已经有很多华容道经典算法的资料了,相比这个要难理解一些,有兴趣的话也可以去看一下。

源代码请见http://www.itcast.net/course/play/9438

<<如果您想和我交流,请点击和我成为好友>>

你可能感兴趣的:(游戏,算法,object,null,equals,Dictionary)