65.9K
CodeProject 正在变化。 阅读更多。
Home

汉诺塔

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (46投票s)

2012年5月28日

CPOL

3分钟阅读

viewsIcon

113478

downloadIcon

9632

汉诺塔问题的解决方案。

引言

本文包含汉诺塔问题的递归解决方案。该应用程序使用 C# 编写,用户界面使用 Windows Forms 完成。

要求

  1. 使用 Windows Forms 对该谜题进行图形化表示。
  2. 用户应该能够选择使用 3、4、5、6 个盘片*来玩这个谜题。总是会有三根柱子**。
  3. 应用程序应该只允许有效的移动——由以下规则定义
    • 一次只能移动一个盘片
    • 你不能将一个较大的盘片放在一个较小的盘片上
  4. 应用程序必须有一个“演示”功能,应用程序将逐步向用户展示所选盘片数量的解决方案。

有关该谜题的更多信息,请参见:汉诺塔

* 盘片一词在整篇文章中用于描述谜题的可移动部分。

** 柱子一词用于描述包含盘片的桩子。

设计

我希望 UI 和后端之间有一个清晰的分离。我创建了 MoveCalculator 类,其唯一目的是处理解决方案。 GameState 类将处理游戏机制,而 GameForm 将在一些控件的帮助下驱动前端。

我希望 MoveCalculatorGameState 返回一些有用的东西,因此引入了 Move 类。 MoveCalculator 将返回一个移动列表,GameState 将执行它们。Smile | <img src=

UI 元素

GameForm 包含所有图形组件,并用作应用程序的驱动程序。我想使用 PictureBox 控件作为盘片和柱子的基本对象,但需要更多,因此我扩展了 PictureBox 控件。 Pole PictureBox 必须通过维护盘片的排序列表来跟踪其上的盘片。 Disk PictureBox 负责四处移动自己。我还添加了拖放功能,使游戏更有趣。

求解器算法

用于解决该谜题的算法是一个非常简单的递归函数。在该函数中,每个移动都会添加到列表中。该列表用于解决该谜题。

在游戏的开始状态,所有盘片都将在“起始柱子”上。在执行列表中的所有移动后,所有盘片都应该在“结束柱子”上。

代码

以下是来自应用程序的五个代码片段

MoveCalculator 类接收用户想要玩的盘片数量,并返回解决该谜题所需的移动列表。

代码片段 1:递归 Calculate 方法

public static class MoveCalculator
{         
    private static void Calculate(int n, int fromPole, int toPole)
    {
        if (n == -1)
        {
            return; // We are done
        }
        int intermediatePole = GetIntermediatePole(fromPole, toPole);
        
        Calculate(n - 1, fromPole, intermediatePole);
        moves.Add(new Move(fromPole, toPole));
        Calculate(n - 1, intermediatePole, toPole);
    }    
    ... 
}   

代码片段 2:Move 包含 FromPoleToPole。由于我们将始终移动 FromPole 顶部的盘片,因此我们只需要这两个柱子。

Move 还包含有关自身的信息,例如 AffectCountIsValid

public class Move 
{
    public Pole FromPole { get; set; }
    public Pole ToPole { get; set; }
    
    public bool AffectCount()
    {
        //If the poles don't change the move should not be counted
        if (ToPole.Equals(FromPole))
        {
            return false;
        }
        return IsValid();
    }            

    public bool IsValid()
    {
        //Allow a move where the pole dont change
        if (ToPole.Equals(FromPole))
        {
            return true;
        }
        return ToPole.AllowDisk(FromPole.getTopDisk());
    }    
    ...
}

代码片段 3:UI 使用两个自定义 PictureBox 控件:PoleDisk

其中 Disk 可从一个 Pole 移动到下一个 Pole。而 Pole 包含一个 Disk 列表。

public class Disk : PictureBox
{
    public int Number { get; set; }

    public Disk(int Number) : base()
    { ... }

    public void MoveToPole(Pole DestinationPole)
    { ... } 
}

public class Pole : PictureBox
{
    public SortedList<int, Disk> Disks { get; set; }
    public int Number { get; set; }

    public Pole(int number)
    { 
    ...    
    }

    public bool IsEmpty()
    {
        return Disks.Count == 0;
    }

    public bool AllowDisk(Disk disk)
    {
        if (disk == null)
        {
            return false;
        }
        if (Disks.Count == 0)
        {
            return true;
        }
        return getTopDisk().Number > disk.Number;
    }
 
    public Disk getTopDisk()
    {
        if (Disks.Count > 0)
        {
            return Disks.First().Value;
        }
        return null;
    }

    public void RemoveDisk()
    {
        Disks.Remove(Disks.First().Key);
    }

    public void AddDisk(Disk disk)
    {

        if (AllowDisk(disk))
        {
            disk.MoveToPole(this);
            Disks.Add(disk.Number, disk);
        }
    }
    ...
}

代码片段 4:为了保存有关游戏状态的信息,添加了 GameState 类。

GameState 应该启动游戏,执行移动并检查是否完成。

public static class GameState
    {
        public static List<Pole> Poles = new List<Pole>();
        public static List<Bitmap> ImageList = new List<Bitmap>();
        public static int MoveCount { get; set; }
        public static int NumberOfDisks { get; set; }
        
        // Start the game
        static GameState()
        {
            LoadImagesFromFile();
            RestartGame(3);
        }
 
        public static int Move(Move move)
        {
         if (move.AffectCount())
            {
                MoveCount++;
            }
            
            if (move.IsValid())
            {
                Disk disk = move.FromPole.getTopDisk();
                Poles[move.FromPole.Number].RemoveDisk();
                Poles[move.ToPole.Number].AddDisk(disk);
                return MoveCount;
            }
 
            else //Invalid move
            {
                return -1;
            }  
        }
 
        public static bool IsSolved()
        {
            return (Poles[Properties.Settings.Default.EndPole].Disks.Count == NumberOfDisks);
        } 
}

代码片段 5:包含最多逻辑的两个类的单元测试:MoveCalculator & GameState

[TestClass()]
public class MoveCalculatorTest {
    ...
    [TestMethod()]
    public void GetMoveCountTest()
    {
        int actualMoveCount = MoveCalculator.GetMoveCount(3);
        int expectedMoveCount = 7;
        Assert.AreEqual(expectedMoveCount, actualMoveCount);

        actualMoveCount = MoveCalculator.GetMoveCount(4);
        expectedMoveCount = 15;
        Assert.AreEqual(expectedMoveCount, actualMoveCount);

        actualMoveCount = MoveCalculator.GetMoveCount(5);
        expectedMoveCount = 31;
        Assert.AreEqual(expectedMoveCount, actualMoveCount);
    }  
}

[TestClass()] 
public class GameStateTest  {
...
    [TestMethod()]
    public void IsSolvedTest()
    {
        GameState.RestartGame(numberOfDisks);

        bool expectedBefore = false; 
        bool actualBefore = GameState.IsSolved();
        solveGame();
        bool expectedAfter = true;
        bool actualAfter = GameState.IsSolved();

        //Assert 
        Assert.AreEqual(expectedBefore, actualBefore);
        Assert.AreEqual(expectedAfter, actualAfter);
    }
}

注释

我想将每个盘片的位置等信息封装在 Disk 类本身中。

public void MoveToPole(Pole DestinationPole)
{
    int numberOfRungsOnSelectedPole = DestinationPole.Disks.Count;

    int x = (DestinationPole.Location.X + DestinationPole.Width) - 
              (DestinationPole.Width / 2)  - (this.Size.Width / 2);
    int y = DestinationPole.Location.Y + DestinationPole.Size.Height - 
      ((numberOfRungsOnSelectedPole + 1) * this.Size.Height) - 
      toh.Properties.Resources._base.Size.Height;
    this.Location = new Point(x, y);
}   

我添加了 MoveToPole 方法,该方法基本上通过简单地设置其位置将盘片移动到柱子上。为了获得盘片移动到的正确点,我需要知道它要移动到哪个柱子以及它应该坐在柱子的哪个位置。由于每个 Pole 都包含有关其包含的盘片数量的信息,因此我只需接收 DestinationPole。该代码假定所有盘片的高度相同;与当前盘片相同。

© . All rights reserved.