滑动拼图破解_随机滑动拼图块

滑动拼图破解

上一教程中 ,我演示了如何使用HTML5 canvas创建滑动益智游戏。

为了节省时间,我对起始图块位置进行了硬编码。 如果将图块随机化,则玩法会更好,但这样做会导致复杂性,因此需要单独的教程进行解释。

这是该教程。

有多种方法可以随机分配图块。 我将研究一些选项,并讨论它们的优缺点,以及出现的问题以及如何克服它们。

一种简单的方法是将谜题初始化为已解决状态,然后重复调用函数以将随机片段滑入空白空间。

function initTiles() {
    var slideLoc = new Object;
    var direction = 0;
    for (var i = 0; i < 30; ++i) {
      direction = Math.floor(Math.random()*4);
      slideLoc.x = emptyLoc.x;
      slideLoc.y = emptyLoc.y;
      if (direction == 0 && slideLoc.x > 0) {
        slideLoc.x = slideLoc.x - 1;
      } else if (direction == 1 && slideLoc.y > 0) {
        slideLoc.y = slideLoc.y - 1;
      } else if (direction == 2 && slideLoc.x < (tileCount - 1)) {
        slideLoc.x = slideLoc.x + 1;
      } else if (direction == 3 && slideLoc.y < (tileCount - 1)) {
        slideLoc.y = slideLoc.y + 1;
      }
      slideTile(emptyLoc, slideLoc);
    }
  }

在这种情况下,我们要滑动30个图块,这是4×4拼图中图块总数的两倍,但是大多数块仍保留在其原始位置。 为了获得类似于随机性的任何东西,我们将需要更多的迭代。

这不是随机解决难题的有效方法。 理想情况下,我们只希望将每个部件移动一次。 我们可以将拼图初始化为已解决状态,然后遍历图块,将每个图块与随机选择的图块交换。

function initTiles() {
    for (var i = 0; i < tileCount; ++i) {
      for (var j = 0; j < tileCount; ++j) {
        var k = Math.floor(Math.random() * tileCount);
        var l = Math.floor(Math.random() * tileCount);
        swapTiles(i, j, k, l);
      }
    }
  }

  function swapTiles(i, j, k, l) {
    var temp = new Object();
    temp = boardParts[i][j];
    boardParts[i][j] = boardParts[k][l];
    boardParts[k][l] = temp;
  }

这种方法不仅为我们提供了更多随机外观的配置,而且还用更少的代码行完成了配置。 但是,该算法有两个严重的缺陷。 第一个问题是微妙的。 尽管用随机位置交换每个图块比简单地将片段滑动到空插槽中要有效得多,但这仍然不是真正的随机算法。 一些起始位置会比其他位置频繁出现。

在2×2难题中,某些起始配置的发生频率比其他配置高87%。 添加第三行,某些配置出现的频率是其他配置的五倍,并且随着添加的图块数量的增加,这种配置还会继续恶化。 幸运的是,有一种方法可以在不增加额外复杂性的情况下实现真正的随机性。 它被称为Fisher-Yates算法。

function initTiles() {
    var i = tileCount * tileCount - 1;
    while (i > 0) {
      var j = Math.floor(Math.random() * i);
      var xi = i % tileCount;
      var yi = Math.floor(i / tileCount);
      var xj = j % tileCount;
      var yj = Math.floor(j / tileCount);
      swapTiles(xi, yi, xj, yj);
      --i;
    }
  }

Fisher-Yates的数学功能超出了本教程的范围,但确实为每个图块均等地出现在任何正方形中。 使用此算法,难题是Math.random()函数所能获得的。

但是,使用Fisher-Yates算法或其他算法随机交换磁贴会导致另一个问题。 所有可能的图块配置中的一半给了我们一个永远无法解决的难题。 为了防止对无辜的用户造成无法解决的难题,我们需要另一种算法。

在介绍此算法之前,我需要定义两个术语:反转和极性。 反转是一对磁贴,它们应该以相反的顺序排列。 难题的极性是所有图块之间的反转总数是偶数还是奇数。 具有10个倒置的拼图具有均匀的极性。 具有7个反转的拼图具有奇特的极性。

根据定义,解决的难题具有零个反转(甚至极性)。 如果我们从一个已解决的难题中交换了两个相邻的图块,我们将发生一次反转。

在此游戏中,棋盘配置为二维阵列,每个棋子均由其x / y坐标表示。

图1

但是,为了处理反演和极性,我们将其视为一维数组。 我们可以使用公式n = y * w + x将每个图块的坐标转换为单个数字n,其中w是宽度。 图为一维数组,图块的编号是这样的。

图2

现在让我们考虑一个随机难题。 它可能看起来像这样。

图3

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

有19个反演。 磁贴6反转,所有六个磁贴编号为0到5; 3用0、1和2反转; 2用0和1反转; 4用0和1反转; 7用0、1和5反转; 5被0和1反转; 而1与0倒置。

为了获得总数,我们需要一个函数来计算每个图块的反转数。

function countInversions(i, j) {
    var inversions = 0;
    var tileNum = j * tileCount + i;
    var lastTile = tileCount * tileCount;
    var tileValue = boardParts[i][j].y * tileCount + boardParts[i][j].x;
    for (var q = tileNum + 1; q < lastTile; ++q) {
      var k = q % tileCount;
      var l = Math.floor(q / tileCount);
  
      var compValue = boardParts[k][l].y * tileCount + boardParts[k][l].x;
      if (tileValue > compValue && tileValue != (lastTile - 1)) {
        ++inversions;
      }
    }
    return inversions;
  }

现在我们可以遍历图块并保持反转的连续总和。

function sumInversions() {
    var inversions = 0;
    for (var j = 0; j < tileCount; ++j) {
      for (var i = 0; i < tileCount; ++i) {
        inversions += countInversions(i, j);
      }
    }
    return inversions;
  }

向侧面滑动磁贴不会改变反转的次数; 空方格没有数字,因此将其与相邻图块交换将始终使我们具有相同数量的反转。 但是,当向上或向下滑动磁贴时,我们可能会更改反转次数。 例如,如果我们向下滑动6个图块,则会将反转次数从19减少到17。

图4

规则是向上或向下滑动瓷砖将更改其与w – 1瓷砖的关系,其中w是拼图的宽度。 因此,对于3×3拼图,我们正在更改图块与其他两个图块的关系。 这可能会导致两个反演次数减少,两个反演次数增加或不变。 例如,在上面的难题中,向上滑动瓦片5将使我们产生19个反转,因为它将获得4的反转,而失去7的反转。

一个以偶数个反转开始的难题将始终具有偶数个反转。 一个具有奇数个反转数的难题将始终具有奇数个反转数。 这不仅适用于3×3拼图,而且适用于任何宽度奇数的拼图。 如果我们要达到零反转,则必须以偶数开头。

由于我们已经计算了反转次数,因此一个简单的函数将告诉我们难题是否可以解决。

function isSolvable() {
    return (sumInversions() % 2 == 0)
  }

上面的示例不可解,因为19不能为偶数。 但是,假设前两个磁贴被颠倒了吗?

图5

现在,我们从18个反演开始。 3和6不再倒置,但其他所有内容保持不变。 我们有一个可以解决的难题。

这为我们提供了一种优雅的解决方案,该解决方案保留了拼图的真正随机性-每个无法解决的拼图都与唯一可解决的拼图配对,仅在前两个磁贴中有所不同。

if (!isSolvable()) {
    swapTiles(0, 0, 1, 0);
    initEmpty();
  }

不幸的是,如果交换的图块之一是空正方形,则此方法将无效。 我们需要特殊的代码来处理这种情况。

if (!isSolvable()) {
    if (emptyLoc.y == 0 && emptyLoc.x <= 1) {
      swapTiles(tileCount - 2, tileCount - 1, tileCount - 1, tileCount - 1);
    } else {
      swapTiles(0, 0, 1, 0);
    }
    initEmpty();
  }

如果空白方块位于前两个位置之一,我们将交换后两个图块。 这稍微有点歪曲了随机性,但是我们仍然比任何其他算法都能使我们更接近。

剩下的只有一个问题。 如果拼图的宽度是偶数,则向上或向下滑动方块会反转极性。 这是因为,如我们上面所见,图块更改了与w – 1图块的关系。

图6

为了使谜题能够解决,当空白方块位于最下面一行时,它必须具有均匀的极性(假设解决谜题时,空白方块位于最下面一行)。 当空方格位于下一行时,如果极性为奇数,则难题可以解决。 因此,对于一个偶数宽度的拼图,我们必须将求反求加空行与底行之间的距离。

function isSolvable(width, height, emptyRow) {
    if (width % 2 == 1) {
      return (sumInversions() % 2 == 0)
    } else {
      return ((sumInversions() + height - emptyRow) % 2 == 0)
    }
  }

现在,我们必须编辑调用此函数的行。

if (!isSolvable(tileCount, tileCount, emptyLoc.y + 1))

这里有几件事要注意。

首先,由于emptyLoc数组是从零开始的,因此我们需要在将其与高度进行比较之前添加一个。

其次,对于正方形拼图,技术上我们不需要高度和宽度这两个参数。 它们是相同的值,我们将tileCount变量传递给这两个变量。 但是在函数中将它们分开可以澄清每个方程式中使用的维数。 如果要制作矩形拼图,我们会知道在哪里使用宽度和在哪里使用高度。

与首先创建拼图相比,随机分配滑动拼图要花更多的工夫,但是要为其提供更好的游戏性值得付出努力。 您可以在此处看到随机拼图的示例。

翻译自: https://www.sitepoint.com/randomizing-sliding-puzzle-tiles/

滑动拼图破解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值