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

构建 Puzzle 15 - 演练

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (27投票s)

2012年2月19日

Ms-PL

4分钟阅读

viewsIcon

87079

downloadIcon

4277

如何构建 Puzzle 15( n-Puzzle)。

引言

在我的一篇文章《我的第一个Windows 8应用程序 – Metro Puzzle》中,我讨论了Windows 8 Metro应用程序,并演示了如何构建Win 8应用程序,我构建的应用程序就是Puzzle 15。后来,我开始收到要求解释构建该游戏的步骤。

因此,我将写下构建Puzzle 15游戏的步骤。

1.png

背景

15拼图(也称为宝石拼图、老板拼图、十五游戏、神秘方块等)是一种滑动拼图,由一个带有数字方块的框架组成,方块顺序随机,只有一个方块缺失。该拼图也有其他尺寸,特别是较小的8拼图。如果尺寸是3×3方块,则该拼图称为8拼图或9拼图;如果是4×4方块,则该拼图分别称为15拼图或16拼图,以方块数量和空格数量命名。拼图的目标是通过利用空格进行滑动来按顺序排列方块(参见图)。

步骤1:拼图基础

构建拼图层有几种方法,在我的演示中,我选择了一种简单的方法:一个带有16个StackPanel子项的Canvas。

基本上,我有一个带有棋盘图像的底层Canvas,在其上方放置另一个Canvas,上面有16个StackPanel,每个Panel展开为100X100。

2.png

现在,当我有主要的拼图结构后,我向每个StackPanel添加了一个95x95大小的图像。(之所以不是100X100,是为了在每个图像之间留出空间),对于每个图像,我都设置了Tag属性,其值为图像——1.png Tag = 1。

3.png

我还添加了一个计时器来计算解决此拼图所需的时间,以及另一个int属性来计算用户移动的次数。

步骤2:查找

代码中最常用的事情之一就是查找。

  • 按图像ID查找StackPanel
  • 查找空Panel
  • 按位置查找值

查找具有特定Tag值的图像的父级

StackPanel FindStackPanelByTagId(int tag)
{
  if (tag == 16)
  {
     return (from stackPanel in ContentPanel.Children.OfType<StackPanel>() 
          where stackPanel.Children.Count == 0 select stackPanel).First();
  }
  else
  {
     return (from stackPanel in ContentPanel.Children.OfType<StackPanel>()
             from img in stackPanel.Children.OfType<Image>()
             where Convert.ToInt32(img.Tag) == tag
             select stackPanel).First();
  }
}  

查找没有子项的StackPanel的位置

int FindEmptyItemPosition()
{
    int index = 15;
    for (int i = 0; i < 15; i++)
    {
        if (((StackPanel)ContentPanel.Children[i]).Children.Count == 0)
            return index;
 
        index--;
    }
    return 0;
} 

按StackPanel位置获取Tag值

int FindItemValueByPosition(int position)
{
  return ((StackPanel)ContentPanel.Children[position]).Children.Count > 0 
          ? Convert.ToInt32(((Image)((StackPanel)ContentPanel.Children
            [position]).Children[0]).Tag) : 16;
} 

步骤3:打乱

现在我们有了拼图结构和查找方法助手,第一件事是打乱或混合拼图。

所以我写了一个运行n次并生成1到16之间随机数的函数,对于每个数字,查找当前持有它的StackPanel。(FindStackPanelByTagId)。

如果第一个和第二个数字小于16,则交换图像和Tag值。

如果其中一个值为16,则交换——一个StackPanel将清空Items。

 void Scrambles()
{
    var count = 0;
    while (count < 25)
    {
        var a = _rnd.Next(1, 17);
        var b = _rnd.Next(1, 17);
 
        if (a == b) continue;
 
        var stack1 = FindStackPanelByTagId(a);
        var stack2 = FindStackPanelByTagId(b);
 
        if (a == 16)
        {
            var image2 = stack2.Children[0];
            stack2.Children.Clear();
            stack1.Children.Add(image2);
        }
        else if (b == 16)
        {
            var image1 = stack1.Children[0];
            stack1.Children.Clear();
            stack2.Children.Add(image1);
        }
        else
        {
            var image1 = stack1.Children[0];
            var image2 = stack2.Children[0];
 
            stack1.Children.Clear();
            stack2.Children.Clear();
 
            stack1.Children.Add(image2);
            stack2.Children.Add(image1);
        }
 
        count++;
    }
} 

步骤4:检查棋盘

用户进行的每一次移动,都会执行一个循环并检查1到16的值。如果数字不在正确顺序,则什么也不发生。

否则,停止游戏计时器并显示获胜消息。

 void CheckBoard()
{
    var index = 1;
    for (var i = 15; i > 0; i--)
    {
        if (FindItemValueByPosition(i) != index) return;
        index++;
    }
 
    _timer.Stop();
    WinGrid.Visibility = System.Windows.Visibility.Visible;
} 

步骤5:移动物品

在我们能够应用物品移动之前,我们需要检查几件事,第一件事是

检查物品是否可以移动,检查特定物品周围的所有Panel,使用-1 +1 -4 +4,如果其中一个为空,则可以移动。

 StackPanel CanMove(UIElement itemToMove)
{
    var count = ContentPanel.Children.Count;
    for (var i = 0; i < count; i++)
    {
        if (!(ContentPanel.Children[i] is StackPanel)) continue;
 
        var stackPanel = (StackPanel)ContentPanel.Children[i];
        if (!stackPanel.Children.Contains(itemToMove)) continue;
 
        if (!IsBorderSwich(i, i + 1) && i + 1 <= 15 && ContentPanel.
            Children[i + 1] != null && 
           ((StackPanel)ContentPanel.Children[i + 1]).Children.Count == 0)
            return ((StackPanel)ContentPanel.Children[i + 1]);
 
        if (!IsBorderSwich(i, i - 1) && i - 1 > -1 && ContentPanel.
            Children[i - 1] != null && 
           ((StackPanel)ContentPanel.Children[i - 1]).Children.Count == 0)
            return ((StackPanel)ContentPanel.Children[i - 1]);
 
        if (i + 4 <= 15 && ContentPanel.Children[i + 4] != null && 
           ((StackPanel)ContentPanel.Children[i + 4]).Children.Count == 0)
            return ((StackPanel)ContentPanel.Children[i + 4]);
 
        if (i - 4 > -1 && ContentPanel.Children[i - 4] != null && 
           ((StackPanel)ContentPanel.Children[i - 4]).Children.Count == 0)
            return ((StackPanel)ContentPanel.Children[i - 4]);
 
    }
    return null;
} 

第二——如果想要交换的两个物品都在棋盘边缘,则什么也不做。

private readonly int[] _bordersNums = { 0, 4, 8, 12, 3, 7, 11, 15 };

bool IsBorderSwich(int a, int b)
{
    return _bordersNums.Contains(a) && _bordersNums.Contains(b);
}

4.png

现在,在我们有了这些安全方法之后,我们可以根据用户点击来移动物品:只需注册整个窗口的ItemManipulationStarted事件,对于每个事件,检查物品是否不是图像,如果是则什么也不做,

如果不是,则调用CanMove方法来验证该物品是否可以移动。

 private void ItemManipulationStarted(object sender, 
                                          ManipulationStartedEventArgs e)
{
    var item = (UIElement)e.OriginalSource;
    if (!(item is Image)) return;
 
    var to = CanMove(item);
 
    if (to != null)
    {
        _moves++;
        txtMoves.Text = _moves.ToString();
        MoveItem(item, to);
        CheckBoard();
    }
 
    e.Handled = true;
    e.Complete();
} 

MoveItem – 将物品从一个StackPanel移动到另一个。

 void MoveItem(UIElement item, StackPanel targetPanel)
{
    foreach (var stackPanel in
        ContentPanel.Children.OfType<StackPanel>().Where(stackPanel => 
    stackPanel.Children.Count > 0 && stackPanel.Children.Contains(item)))
    {
        stackPanel.Children.Remove(item);
    }
 
    targetPanel.Children.Add(item);
} 

步骤6:检查拼图是否可解

这一部分非常重要,因为n拼图的起始位置有一半是无法解决的。

Johnson & Story (1879) 使用了 奇偶性 参数来证明,n拼图的起始位置有一半是无法解决的,无论进行多少次移动。这是通过考虑一个在任何有效移动下都不变的方块配置函数来实现的,然后使用它将所有可能的标记状态空间划分为两个 等价类,即可达状态和不可达状态。

15拼图(n拼图)是建模涉及启发式算法的经典问题。对此问题常用的启发式方法包括计算错位方块的数量和计算每个方块与其在目标配置中位置之间的曼哈顿距离之和。请注意,两者都是可接受的,即它们从不过度估计剩余的移动次数,这可以确保某些搜索算法(如A*)的最优性。

bool CheckIfSolvable()
{
    var n = 0;
    for (var i = 1; i <= 16; i++)
    {
        if (!(ContentPanel.Children[i] is StackPanel)) continue;
 
        var num1 = FindItemValueByPosition(i);
        var num2 = FindItemValueByPosition(i - 1);
 
        if (num1 > num2)
        {
            n++;
        }
    }
 
    var emptyPos = FindEmptyItemPosition();
    return n % 2 == (emptyPos + emptyPos / 4) % 2 ? true : false;
}

步骤7:设置新游戏

现在,当我们定义好所有必需的东西后,让我们编写New Game方法,将所有计时器和移动次数重置为0,调用Scrambles方法,并且只要游戏无法解决就继续打乱游戏,一旦可解就启动计时器。

public void NewGame()
{
    _moves = 0;
    txtMoves.Text = "0";
    txtTime.Text = Const.DefaultTimeValue;
 
    Scrambles();
    while (!CheckIfSolvable())
    {
        Scrambles();
    }
 
    _startTime = DateTime.Now.AddSeconds(1);
    _timer.Start();
 
    GridScrambling.Visibility = System.Windows.Visibility.Collapsed;
}

5.png

尽情享受吧

© . All rights reserved.