アドバイスが必要です。私はFlow Freeに似たゲームを開発しています。ゲームボードはグリッドと色付きのドットで構成されており、ユーザーは他の線を重ねずに同じ色のドットを接続し、ボード内のすべての空きスペースを使用する必要があります。
レベル作成についての質問です。ランダムに生成されるレベルを作成したい(そして、プレイヤーにヒントを与えることができるように、少なくともレベルを解決できる必要があります)と、使用するアルゴリズムについて切り株になっています。助言がありますか?
注:画像はFlow Freeの目的を示しており、私が開発しているものと同じ目的です。
ご協力いただきありがとうございます。 :)
よりシンプルで管理しやすいアルゴリズムのペアで問題を解決することを検討してください。1つは単純な事前解決済みボードを確実に作成するアルゴリズムで、もう1つはフローを再配置してシンプルなボードをより複雑にします。
n
x n
グリッドでn
フローを使用している場合、最初の部分は、単純な事前解決済みボードの作成です(必要な場合)は簡単です。
または、独自のスターターボードを用意して、次のパートに渡すこともできます。この段階の唯一の目標は、たとえそれがほんの些細なことでも、事前に決定されていても、有効なボードを構築することです。
2番目の部分は、フローの再配置であり、各フローをループして、どのフローが隣接フローと連携して成長および縮小できるかを確認します。
f
を選択します。f
が最小の長さ(たとえば3平方長)である場合、f
を現時点では縮小できないため、次の反復にスキップします。f
の先頭のドットが別のフローのドットの隣にある場合g
(複数のg
から選択する場合は、ランダムに1つ選択します)... f
の頭の点を1マス移動します(つまり、尾に向かって1マス移動します)。 f
が1マス短くなり、空のマスができました。 (パズルは現在未解決です。)g
からf
の空きスペースに移動します。これで、g
のドットの移動元の空の四角形ができました。g
からのフローを入力します。これで、g
は、この反復の開始時よりも1平方長くなります。 (パズルはまた解決されるように戻っています。)f
の末尾のドットについて前の手順を繰り返します。現状のアプローチは限られていますが(ドットは常に隣接します)、次のように拡張するのは簡単です。
f
の本体をループするステップを追加し、他のフローとスペースを交換するためのよりトリッキーな方法を探します...ここでの全体的なソリューションは、おそらくあなたが目指している理想的なものよりも少ないですが、今では、2つの単純なアルゴリズムがあり、さらに具体化して、1つの大きな包括的アルゴリズムの役割を果たすことができます。結局のところ、このアプローチは扱いやすく、不可解ではなく、簡単に変更でき、他に何もないとしても、始めるのに良い場所だと思います。
pdate:上記の手順に基づいて概念実証をコーディングしました。以下の最初の5x5グリッドから始めて、プロセスは後続の5つの異なるボードを作成しました。興味深いものもあれば、そうでないものもありますが、既知の解決策が1つあれば常に有効です。
出発点
5ランダムな結果(スクリーンショットが正しく配置されていないため申し訳ありません)
そして、適切な尺度としてランダムな8x8。開始点は、上記と同じ単純な列アプローチでした。
numberlinkソルバーおよびジェネレーター に次のアルゴリズムを実装しました。では、パスがそれ自体に触れることができないというルールが適用されます。これは、ほとんどの「ハードコア」番号リンクアプリとパズルでは通常です
私の番号リンク形式は、数字ではなく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)を2つの隣接するパスをランダムにランダムにマージする関数に置き換えるテストを行いました。しかし、それははるかに密度の高いパズルをゲームし、私はすでに上記は密度が高すぎて難しいと思っています。
これが私が生成したさまざまなサイズの問題のリストです: https://github.com/thomasahle/numberlink/blob/master/puzzles/inputs
そのようなレベルを作成する最も簡単な方法は、それを解決する方法を見つけることです。このようにして、基本的にランダムな開始構成を生成し、それを解決しようとすることにより、それが有効なレベルかどうかを判断できます。これにより、最も多様なレベルが生成されます。
他の方法でレベルを生成する方法を見つけた場合でも、生成されたレベルが適切であることを証明するために、この解決アルゴリズムを適用する必要があります;)
ボードのサイズがNxNセルで、使用可能なN色もある場合、可能なすべての構成をブルートフォースで列挙すると(開始ノードと終了ノード間の実際のパスを形成するかどうかに関係なく)、次のようになります。
そう、
言い換えれば、可能な組み合わせの数は最初はかなり多いですが、ボードを大きくし始めると途方もなく速く成長します。
明らかに、構成の数をできるだけ減らすようにする必要があります。これを行う1つの方法は、各色の開始セルと終了セル間の最小距離( "dMin")を考慮することです。少なくとも、その色のセルがこれだけあるはずです。最小距離の計算は、単純な塗りつぶしまたはダイクストラのアルゴリズムで実行できます。 (注:この次のセクション全体では、セルのnumberについてのみ説明し、セルのlocationsについては触れていません)
あなたの例では、これは(開始セルと終了セルを数えない)ことを意味します
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.
ただし、これでも実際のパスはわかりません。したがって、より良いアイデアは、各色を順番に処理し、次のすべてのパスを見つけようとするバックトラック検索にあります。
2番目の基準は、色ごとに報告されるパスの数を減らします。これにより、(組み合わせ効果により)試行されるパスの総数が大幅に削減されます。
疑似コード:
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(board、color、startCell、endCell、minDistance、maxDistance)のみが実装されます。ここでトリッキーなことは、最短パスを検索するのではなく、min-DistanceとMaxDistanceによって決定される範囲内にあるanyパスを検索することです。したがって、ダイクストラまたはA *を使用することはできません。これらは、各セルへの最短経路のみを記録し、可能な迂回は記録しないためです。
これらのパスを見つける1つの方法は、ボードに多次元配列を使用することです。各セルは複数のウェイポイントを格納でき、ウェイポイントはペア(以前のウェイポイント、原点までの距離)として定義されます。以前のウェイポイントは、目的地に到達したらパス全体を再構築できるようにするために必要であり、Originまでの距離がmaxDistanceを超えることを防ぎます。
次に、startCellから外側に向かって探索のようなフラッドフィルを使用してすべてのパスを見つけることができます。特定のセルについて、色付けされていない、または現在の色と同じ隣人が再帰的に探索されます(フォームを形成するものを除く) endCellに到達するか、maxDistanceを超えるまで、Originへの現在のパス)。
この戦略の改善点は、startCellからendCellまで外側に向かって探索するのではなく、Floor(maxDistance/2)およびCeil(maxDistance/2)を使用して、startCellとendCellの両方から並行して外側に探索することです。それぞれの最大距離。 maxDistanceの値が大きい場合、探索されるセルの数を2 * maxDistance ^ 2からmaxDistance ^ 2に減らす必要があります。
2つのステップでこれを実行する必要があると思います。ステップ1)すべてのポイントを接続する交差しない一連のパスを見つけ、2)パスを成長/シフトしてボード全体を埋める
ステップ1についての私の考えは、本質的にすべてのポイントで同時にダイクストラのようなアルゴリズムを実行し、パスを一緒に成長させることです。ダイクストラと同様に、各ノードから塗りつぶして、ヒューリスティックを使用して次にどのノードを検索するかを選択します(私の直感では、最初に自由度が最も少ないポイントを選択し、次に距離で選択すると、いいもの)。ダイクストラとは非常に異なりますが、同じノードに成長しようとする複数のパスがある場合、バックトラックしなければならない場合があると思います。 (もちろん、これは大きなマップではかなり問題になるかもしれませんが、上記のような小さなマップでは大した問題ではないかもしれません。)
上記のアルゴリズムを開始する前に、主に必要なバックトラックの数を減らすために、いくつかの簡単なパスを解決することもできます。具体的には、ボードのエッジに沿ってポイント間をトレースできる場合、それらの2つのポイントをそのように接続すると他のパスに干渉しないことを保証できるため、単純にそれらを埋めてそれらから外します。方程式。次に、ボードの境界または既存のパスの境界に沿ってトレースすることにより、これらすべての「迅速で簡単な」パスが見つかるまで、これをさらに繰り返すことができます。そのアルゴリズムは実際には上記の例のボードを完全に解決しますが、間違いなく他の場所で失敗します..それでも、実行するのは非常に安く、前のアルゴリズムの検索時間を短縮します。
または
ポイントの各セット間で実際のダイクストラのアルゴリズムを実行し、最も近いポイントを最初にパスする(またはランダムな順序で数回試行する)ことができます。これはおそらくかなりの数のケースで機能し、失敗した場合は単にマップを破棄して新しいマップを生成します。
ステップ1を解決したら、ステップ2の方が簡単ですが、必ずしも簡単ではありません。パスを成長させるには、パスを外側に成長させたいと思います(最初に壁に最も近いパス、壁に向かって成長し、次に他の内側のパスを外側に成長させるなど)。成長するには、2つの基本的な操作があると思います。つまり、コーナーを反転し、空の正方形の隣接するペアに拡張します。つまり、次のような行がある場合
.v<<.
v<...
v....
v....
まず、コーナーを裏返してエッジスペースを埋めます。
v<<<.
v....
v....
v....
次に、隣接するオープンスペースのペアに拡張します。
v<<v.
v.^<.
v....
v....
v<<v.
>v^<.
v<...
v....
等..
私が概説したものは、存在する場合の解決策を保証するものではありませんが、存在する場合はほとんどの場合それを見つけることができるはずであり、マップが解決策を持たない場合、またはアルゴリズムが失敗する場合に注意してください地図を捨てて別の地図を試してみてください:)
次の2つの選択肢があります。
オプション(2)を使用してBoggleタイプのボードを生成しましたが、非常に成功しています。オプション(2)を使用する場合は、次のようにします。
必要なツール:
解決するには:
10x10のA *は100分の1秒で実行されます。おそらく1k +ボード/秒を解決できます。したがって、10秒の実行で、いくつかの「使用可能な」ボードが得られます。
ボーナスポイント: