什么用于无流动游戏随机级别创建?

时间:2012-10-17 02:10:40

标签: algorithm corona graph-algorithm

我需要一些建议。我正在开发一款类似于Flow Free的游戏,其中游戏板由网格和彩色点组成,用户必须将相同颜色的点连接在一起而不与其他线重叠,并且用尽所有板中的空闲空间。 / p>

我的问题是关于水平创造。我希望随机生成各级(并且至少应该能够自己解决,以便它可以给玩家提示)并且我在使用什么算法的问题上。有什么建议吗?

Game objective

注意:图像显示了Flow Free的目标,它与我正在开发的目标相同。

感谢您的帮助。 :)

5 个答案:

答案 0 :(得分:28)

考虑使用一对更简单,更易于管理的算法来解决您的问题:一种算法可靠地创建简单的预解决板,另一种算法重新排列流量,使简单的板更复杂。

如果您在n x n网格上使用n流,那么构建一个简单的预解块板的第一部分是微不足道的(如果您希望这样) :

  • 每个流程......
    • 将头点放在第一个打开的柱子的顶部。
    • 将尾点放在该列的底部。

或者,您可以提供自己的手工制作的启动板以传递到第二部分。这个阶段的唯一目标是建立一个有效的板,即使它只是微不足道或预先确定,所以值得保持简单。

第二部分,重新​​排列流量,涉及在每个流上循环,看看哪个流可以与其相邻流一起生长和收缩:

  • 进行一定数量的迭代......
    • 选择随机流f
    • 如果f处于最小长度(比如说3个方格),请跳到下一次迭代,因为我们现在无法缩小f
    • 如果f的头点位于另一个流g的点旁边(如果有多个g可供选择,请随机选择一个)...
      • 沿着它的流动将f的头部点移动一个方格(即,朝向尾部走一个方格)。 f现在缩短了一个方格,并且有一个空方块。 (这个难题现在尚未解决。)
      • 将相邻点从g移动到由f腾出的空方框中。现在有一个空格子,g的点移动了。
      • 使用g的流量填充该空白点。现在g比这次迭代开始时长一平方。 (这个难题也回归了。)
    • f的尾点重复上一步。

目前的方法是有限的(点将永远是邻居),但它很容易扩展:

  • 添加一个循环遍历流体f的步骤,寻找与其他流交换空间的棘手方法......
  • 添加一个阻止点移动到旧位置的步骤...
  • 添加您提出的任何其他想法。

这里的整体解决方案可能不是你想要的理想解决方案,但现在你有两个简单的算法,你可以进一步充实,以发挥一个大的,无所不包的算法的作用。最后,我认为这种方法是可管理的,不是神秘的,也很容易调整,如果不是其他的话,这是一个很好的起点。


更新:我根据上述步骤编写了概念验证。从下面的第一个5x5网格开始,该过程产生了随后的5个不同的板。有些是有趣的,有些则不是,但它们总是对一个已知的解决方案有效。

起点
Image 1 - starting frame

5个随机结果(抱歉未对齐的屏幕截图)
Image 2 Image 3 Image 4 Image 5 Image 6

随机8x8,以获得良好的衡量标准。起点与上述相同的简单列方法相同。

Image 7

答案 1 :(得分:8)

创建这样一个级别最直接的方法是找到解决它的方法。这样,您基本上可以生成任何随机启动配置,并通过尝试解决它来确定它是否是有效级别。这将产生最多样化的水平。

即使您找到了以其他方式生成关卡的方法,您仍然希望应用此求解算法来证明生成的关卡是否正常;)

强力枚举

如果电路板的NxN单元大小,并且还有N种可用颜色,则强制枚举所有可能的配置(无论它们是否形成起始节点和结束节点之间的实际路径)都需要:

  • N ^ 2个细胞总数
  • 2N个单元已经占用了开始和结束节点
  • N ^ 2 - 2N细胞,其颜色尚未确定
  • N种颜色。
  • N ^(N ^ 2 - 2N)种可能的组合。

所以,

  • 对于N = 5,这意味着5 ^ 15 = 30517578125组合。
  • 对于N = 6,这意味着6 ^ 24 = 4738381338321616896组合。

换句话说,开始时可能的组合数量相当高,但一旦开始使电路板变大,也会变得非常快。

约束每种颜色的细胞数

显然,我们应尽量减少配置数量。这样做的一种方法是考虑每种颜色的起始和结束单元格之间的最小距离(" dMin") - 我们知道应该至少有这么多单元格具有该颜色。计算最小距离可以通过简单的洪水填充或Dijkstra算法完成。 (N.B.请注意,整个下一部分仅讨论单元格的数字,但没有说明其位置

在您的示例中,这意味着(不计算开始和结束单元格)

dMin(orange) = 1
dMin(red) = 1
dMin(green) = 5
dMin(yellow) = 3
dMin(blue) = 5

这意味着,在尚未确定颜色的15个细胞中,必须至少有1个橙色,1个红色,5个绿色,3个黄色和5个蓝色细胞,总共还有15个细胞。 对于这个特定的例子,这意味着通过最短路径(一个)连接每个颜色的起始和结束单元填充整个板 - 即,在用最短路径填充板之后,没有未着色的单元保留。 (这应该被视为"运气",并非每个董事会的起始配置都会导致这种情况发生。)

通常,在此步骤之后,我们有许多可以自由着色的单元格,让我们称这个数字为U.对于N = 5,

U = 15 - (dMin(orange) + dMin(red) + dMin(green) + dMin(yellow) + dMin(blue))

因为这些细胞可以采用任何颜色,我们还可以确定可以具有特定颜色的最大细胞数量:

dMax(orange) = dMin(orange) + U
dMax(red)    = dMin(red) + U
dMax(green)  = dMin(green) + U
dMax(yellow) = dMin(yellow) + U
dMax(blue)   = dMin(blue) + U

(在此特定示例中,U = 0,因此每种颜色的最小单元格数也是最大值。)

使用距离约束的路径寻找

如果我们使用这些颜色限制来强力计算所有可能的组合,我们会担心的组合要少得多。更具体地说,在这个特定的例子中,我们将:

  15! / (1! * 1! * 5! * 3! * 5!)
= 1307674368000 / 86400
= 15135120 combinations left, about a factor 2000 less.

然而,这仍然没有给我们实际的路径。所以更好的想法是回溯搜索,我们依次处理每种颜色并尝试找到所有路径:

  • 没有穿过已经有色的细胞
  • 不短于dMin(颜色)且不长于dMax(颜色)。

第二个标准将减少每种颜色报告的路径数量,这会导致尝试的路径总数大大减少(由于组合效应)。

在伪代码中:

function SolveLevel(initialBoard of size NxN)
{
    foreach(colour on initialBoard)
    {
        Find startCell(colour) and endCell(colour)
        minDistance(colour) = Length(ShortestPath(initialBoard, startCell(colour), endCell(colour)))
    }

    //Determine the number of uncoloured cells remaining after all shortest paths have been applied.
    U = N^(N^2 - 2N) - (Sum of all minDistances)

    firstColour = GetFirstColour(initialBoard)
    ExplorePathsForColour(
        initialBoard, 
        firstColour, 
        startCell(firstColour), 
        endCell(firstColour), 
        minDistance(firstColour), 
        U)
    }
}

function ExplorePathsForColour(board, colour, startCell, endCell, minDistance, nrOfUncolouredCells)
{
    maxDistance = minDistance + nrOfUncolouredCells
    paths = FindAllPaths(board, colour, startCell, endCell, minDistance, maxDistance)

    foreach(path in paths)
    {
        //Render all cells in 'path' on a copy of the board
        boardCopy = Copy(board)
        boardCopy = ApplyPath(boardCopy, path)

        uRemaining = nrOfUncolouredCells - (Length(path) - minDistance)

        //Recursively explore all paths for the next colour.
        nextColour = NextColour(board, colour)
        if(nextColour exists)
        {
            ExplorePathsForColour(
                boardCopy, 
                nextColour, 
                startCell(nextColour), 
                endCell(nextColour), 
                minDistance(nextColour), 
                uRemaining)
        }
        else
        {
            //No more colours remaining to draw
            if(uRemaining == 0)
            {
                //No more uncoloured cells remaining
                Report boardCopy as a result
            }
        }
    }
}

FindAllPaths

这只会实现FindAllPaths(board,color,startCell,endCell,minDistance,maxDistance)。这里棘手的问题是我们不会搜索最短路径,而是搜索任何路径,这些路径落在由minDistance和maxDistance确定的范围内。因此,我们不能只使用Dijkstra或A *,因为它们只记录到每个细胞的最短路径,而不是任何可能的弯路。

找到这些路径的一种方法是使用多维数组用于电路板,其中 每个单元格能够存储多个航点,航点被定义为该对(前一个航点,到原点的距离)。一旦我们到达目的地,以及到原点的距离,前一个航路点需要能够重建整个路径 防止我们超过maxDistance。

然后可以通过从startCell向外使用类似探测的泛洪填充来查找所有路径,其中对于给定的单元格,每个无色或相同的当前颜色的neigbour被递归地探索(除了在我们到达endCell或超过maxDistance之前,形成我们当前到达原点的路径的那些。

这个策略的改进是我们不会从startCell向外探索endCell,但是我们使用Floor(maxDistance / 2)和Ceil(maxDistance)从startCell和endCell向外探索并行/ 2)作为各自的最大距离。对于较大的maxDistance值,这应该将探索的单元格数量从2 * maxDistance ^ 2减少到maxDistance ^ 2.

答案 2 :(得分:8)

我已在numberlink solver and generator中实施了以下算法。在执行规则时,路径永远不会触及自身,这在大多数“硬核”数字链接应用和谜题中是正常的

  1. 首先,该板以简单,确定的方式平铺2x1多米诺骨牌。 如果这不可能(在奇数区域纸上),右下角是   留下单身人士。
  2. 然后通过旋转随机对邻居随机改组多米诺骨牌。 在宽度或高度等于1的情况下不会这样做。
  3. 现在,在奇数区域纸张的情况下,右下角连接到 其中一个邻居多米诺骨牌。这将永远是可能的。
  4. 最后,我们可以开始通过多米诺骨牌找到随机路径,并将它们组合起来 当我们经过。特别注意不要连接'neighboour flow' 这将创造“双重回归”的谜题。
  5. 在拼图打印之前,我们尽可能“紧凑”使用的颜色范围。
  6. 通过用a。替换所有非流动头的位置来打印拼图。
  7. 我的号码链接格式使用ascii字符而不是数字。这是一个例子:

    $ bin/numberlink --generate=35x20
    Warning: Including non-standard characters in puzzle
    
    35 20
    ....bcd.......efg...i......i......j
    .kka........l....hm.n....n.o.......
    .b...q..q...l..r.....h.....t..uvvu.
    ....w.....d.e..xx....m.yy..t.......
    ..z.w.A....A....r.s....BB.....p....
    .D.........E.F..F.G...H.........IC.
    .z.D...JKL.......g....G..N.j.......
    P...a....L.QQ.RR...N....s.....S.T..
    U........K......V...............T..
    WW...X.......Z0..M.................
    1....X...23..Z0..........M....44...
    5.......Y..Y....6.........C.......p
    5...P...2..3..6..VH.......O.S..99.I
    ........E.!!......o...."....O..$$.%
    .U..&&..J.\\.(.)......8...*.......+
    ..1.......,..-...(/:.."...;;.%+....
    ..c<<.==........)./..8>>.*.?......@
    .[..[....]........:..........?..^..
    ..._.._.f...,......-.`..`.7.^......
    {{......].....|....|....7.......@..
    

    在这里,我通过我的解算器(同一种子)运行它:

    $ bin/numberlink --generate=35x20 | bin/numberlink --tubes
    Found a solution!
    ┌──┐bcd───┐┌──efg┌─┐i──────i┌─────j
    │kka│└───┐││l┌─┘│hm│n────n┌o│┌────┐
    │b──┘q──q│││l│┌r└┐│└─h┌──┐│t││uvvu│
    └──┐w┌───┘d└e││xx│└──m│yy││t││└──┘│
    ┌─z│w│A────A┌┘└─r│s───┘BB││┌┘└p┌─┐│
    │D┐└┐│┌────E│F──F│G──┐H┐┌┘││┌──┘IC│
    └z└D│││JKL┌─┘┌──┐g┌─┐└G││N│j│┌─┐└┐│
    P──┐a││││L│QQ│RR└┐│N└──┘s││┌┘│S│T││
    U─┐│┌┘││└K└─┐└─┐V││└─────┘││┌┘││T││
    WW│││X││┌──┐│Z0││M│┌──────┘││┌┘└┐││
    1┐│││X│││23││Z0│└┐││┌────M┌┘││44│││
    5│││└┐││Y││Y│┌─┘6││││┌───┐C┌┘│┌─┘│p
    5││└P│││2┘└3││6─┘VH│││┌─┐│O┘S┘│99└I
    ┌┘│┌─┘││E┐!!│└───┐o┘│││"│└─┐O─┘$$┌%
    │U┘│&&│└J│\\│(┐)┐└──┘│8││┌*└┐┌───┘+
    └─1└─┐└──┘,┐│-└┐│(/:┌┘"┘││;;│%+───┘
    ┌─c<<│==┌─┐││└┐│)│/││8>>│*┌?│┌───┐@
    │[──[└─┐│]││└┐│└─┘:┘│└──┘┌┘┌┘?┌─^││
    └─┐_──_│f││└,│└────-│`──`│7┘^─┘┌─┘│
    {{└────┘]┘└──┘|────|└───7└─────┘@─┘
    

    我已经测试了用迭代地随机合并两个相邻路径的函数替换步骤(4)。然而,它游戏更加密集,我已经认为上面几乎太密集而不困难。

    以下是我生成的不同大小的问题列表:https://github.com/thomasahle/numberlink/blob/master/puzzles/inputs3

答案 3 :(得分:2)

我想你想分两步完成这项工作。步骤1)找到一组连接所有点的非相交路径,然后2)增大/移动这些路径以填充整个板

我对第1步的想法是基本上在所有点上同时执行Dijkstra算法,将路径一起增长。与Dijkstra类似,我想你会想要从你的每个点填充,选择使用一些启发式搜索下一个节点(我的预感说首先选择具有最小自由度的点,然后是距离,可能是一个好的)。与Dijkstra截然不同但我认为当我们有多条路径试图进入同一节点时,我们可能会不得不回溯。 (这对于较大的地图来说当然可能会有相当大的问题,但在上面的小地图上可能不是什么大问题。)

在开始上述算法之前,您还可以解决一些更简单的路径,主要是为了减少所需的回溯数量。具体来说,如果你可以在沿着板边缘的点之间进行跟踪,你可以保证以这种方式连接这两个点永远不会干扰其他路径,所以你可以简单地填写那些并将这些人带出方程。然后,您可以进一步对此进行迭代,直到通过沿着板的边界或现有路径的边界进行跟踪来找到所有这些“快速且简单”的路径。该算法实际上完全可以解决上面的示例板,但无疑会在其他地方失败..但是,执行它会非常便宜并且会缩短您之前算法的搜索时间。

替代地

你可以简单地在每组点之间做一个真正的Dijkstra算法,首先找出最近的点(或者在一些随机顺序中尝试几次)。这可能适用于相当多的案例,当它失败时,只需丢弃地图并生成新的地图。

一旦你解决了第1步,第2步应该更容易,但不一定是微不足道的。为了增长你的路径,我想你会想要向外扩展你的路径(所以最先靠近墙壁的路径,朝向墙壁增长,然后向外延伸到其他内部路径等等)。为了成长,我认为你将有两个基本操作,翻转角落,并扩展到相邻的空方块对...也就是说,如果你有像

这样的线
.v<<.
v<...
v....
v....

首先,您需要翻转角落以填充边缘空间

v<<<.
v....
v....
v....

然后你会想要扩展到相邻的开放空间对

v<<v.
v.^<.
v....
v....

v<<v.
>v^<.
v<...
v....

等。

请注意,我所概述的内容并不能保证解决方案(如果存在),但我认为如果一个存在,您应该能够找到大部分时间,然后在地图没有解决方案的情况下,或者算法无法找到一个,只是抛出地图并尝试不同的一个:)

答案 4 :(得分:1)

您有两种选择:

  1. 编写自定义解算器
  2. 蛮力。
  3. 我使用选项(2)来生成Boggle类型的电路板,它非常成功。如果你选择Option(2),你就是这样做的:

    所需工具:

    • 写一个A *求解器。
    • 写一个随机板创建者

    要解决:

    1. 生成仅由端点组成的随机板
    2. 当董事会未解决时:
      • 获得两个尚未解决的最接近的端点
      • 运行A *以生成路径
      • 更新板,以便下一个A *知道新的电路板布局,新路径标记为不可穿越。
    3. 退出循环时,检查成功/失败(使用整板/等)并在需要时再次运行
    4. 10x10上的A *应该以百分之一秒为单位运行。你可以解决1k +板/秒。因此,10秒钟运行应该可以获得几个“可用”的板。

      奖励积分:

      • 在为IAP(在应用程序购买中)级别包生成级别时,请记住检查镜像/旋转/反射/等等,这样就不会有一块板的副本(这只是跛脚)。
      • 提出一个指标,确定两块电路板是否“相似”,如果是,则抛弃其中一块。