私は「コールオブアトランティス」のようなマッチ3パズルゲームを自分で書こうとしています。最も重要なアルゴリズムは、可能な3つすべてのマッチ3の可能性を見つけることです。参照できるオープンソースプロジェクトはありますか?またはアルゴリズムにキーワードがありますか?すべての可能性を計算するためのより高速なアルゴリズムを探しています。
ルール:
- 対角線は考慮されません。
- サイズは8x8です
ありがとう。
私は「コールオブアトランティス」のようなマッチ3パズルゲームを自分で書こうとしています。最も重要なアルゴリズムは、可能な3つすべてのマッチ3の可能性を見つけることです。参照できるオープンソースプロジェクトはありますか?またはアルゴリズムにキーワードがありますか?すべての可能性を計算するためのより高速なアルゴリズムを探しています。
ルール:
ありがとう。
回答:
これらのゲームの1つをハンドヘルドプラットフォームに移植するのを手伝いました。可能性を見つけるためにAIコードを振り返ると、それは複雑で残忍です(4つにネストされたループ、時々再帰的に呼び出されるなど)。
(その複雑さの一部は、状況に応じて移動の強さを評価しようとすることから生じます:より長いチェーンをより高く評価する、コンボを探すなど...)
ただし、実際に「最適」である必要はありません。移植時にコードに触れることすらしませんでした。プロファイラーには表示されませんでした。
今見てみると、セルあたり1つの32ビットワード(実際にはセルあたり1バイトを使用していると思います)でも、ボード全体が非常に小さなL1キャッシュに収まるので、キャッシュされているものに影響を与えずに多くの過剰な読み取りを実行できますフレームレートが多すぎます。 特に、ボード構成が変更されるたびに、このプロセス全体を一度だけ実行する必要があるためです。(大きなシータがホバリングしていることn^2
は、非常に低いn
することは、キャッシュされたメモリに与えられた小さな乗数は言うまでもなく、でそれほど。)
言われていること:娯楽のために、問題を並列化してみましょう。ビット単位の操作から始めます。
1つの特定のタイプ(タイプを区別するために色を使用します)の行のすべての部分(石と呼ぶ)を表すビットマスクがあるとします。赤い石から始めて、後でビットマスクを計算するコストについて心配します。
// Let's assume top right indexing.
// (The assumption is not necessary, --
// it just makes the left-shift and right-shift operators
// look like they're pointing in the correct direction.)
// this is for row 2
col index 76543210
color BRRGYRBR // blue, red, red, green, yellow, ...
"red" bits 01100101
一連の3になるために1回のスワップが必要なシリーズを探しています。提供:Kaj、これは基本的に3つの組み合わせのうちの1つです。 XoX
、oXX
、またはXXo
どこX
で一致した石とo
何か他のものです。
(このアイデアは素晴らしいハッカーの喜びの本から借りられましたものです。そのようなものがあなたを喜ばせるなら、fxtbookも参照してください。)
// using c-style bitwise operators:
// & is "and"
// ^ is "xor"
// | is "or"
// << and >> are arithmetic (non-sign-extending) shifts
redBitsThisRow = redBitsRows[2]
// find the start of an XoX sequence
startOfXoXSequence = redBitsThisRow & (redBitsThisRow << 2);
// for our example, this will be 00000100
// find any two stones together in a row
startOfXXSequence = redBitsThisRow & (redBitsThisRow << 1);
// for our example, this will be 01000000
XXまたはXoXシーケンスの開始ではなく、欠落している石の位置を知っている方が便利です。
// give us any sequences that might want a stone from the left
missingLeftStone = startOfXXSequence << 1;
// for our example, this will be 10000000
// give us any sequences that might want a stone from the right
missingRightStone = startOfXXSequence >> 2;
// for our example, this will be 00010000
// give us any sequences that might want a stone from the top or bottom
missingTopOrBottomStone = missingRightStone | missingLeftStone | (startOfXoXSequence >> 1)
// for our example, this will be 10010010
(約1ロードおよび9 ALU命令-5シフト、2 ors、2 ands-インラインシフターを含まない恐ろしいCPUを使用。多くのアーキテクチャでは、これらのシフトは無料である可能性があります。)
これらの不足している場所を埋めることはできますか?
// look to the left, current row
leftMatches = redBitsThisRow & (missingLeftStone << 1)
// look to the right, current row
rightMatches = redBitsThisRow & (missingRightStone >> 1)
// look on the row above
topMatches = redBitsRow[1] & missingTopOrBottomStone
// look on the row below
bottomMatches = redBitsRow[3] & missingTopOrBottomStone
(別の2つのロードと6つのALU命令-4と2のシフト-CPUの不良。行0と行7は問題を引き起こす可能性があることに注意してください-これらの計算の周りに分岐するか、割り当てによって分岐を回避するかを選択できます。 1つは0の前に、もう1つは7の後に2つの余分な行のスペースを確保し、それらをゼロにしてください。)
これで、石を交換して一致させることができる石の位置を示す「一致」変数がいくつかあります。
これは、「後続のゼロをカウントする」組み込みまたは非常に安価なインラインメソッドを想定しています。
swapType = RIGHT_TO_LEFT;
matches = leftMatches;
while ( (colIdx = ctz(matches)) < WORD_BITS ) {
// rowIdx is 2 in our examples above
workingSwaps.insert( SwapInfo(rowIdx, colIdx, swapType) );
// note that this SwapInfo construction could do some more advanced logic:
// run the swap on a temporary board and see how much score it accumulates
// assign some sort of value based on preferring one type of match to another, etc
matches = matches ^ (1<<colIdx); // clear the match, so we can loop to the next
}
// repeat for LEFT_TO_RIGHT with rightMatches
// repeat for TOP_TO_BOTTOM with topMatches
// repeat for BOTTOM_TO_TOP with bottomMatches
リトルエンディアン環境とビッグエンディアン環境では、このビットロジックが機能しないことに注意してください。マシンのワードサイズよりも幅の広いボードでは、はるかに扱いにくくなります。(あなたは何かを試すことができますstd::bitset
このます。)
カラムはどうですか?テーブルの2つのコピーを保持するのが最も簡単な場合があります。1つは行順に格納され、もう1つは列順に格納されます。ゲッターとセッターのラッピングボードアクセスがある場合、これは簡単なはずです。すべてが1つのセットになりrowArray[y][x] = newType; colArray[x][y] = newType;
、それが簡単になった後、私は2つのアレイを最新の状態に維持してもかまいません。
...しかし、管理rowBits[color][row]
とcolBits[color][col]
不愉快になります。
ただし、余談ですがrowBits
、and colBits
がある場合は、代わりに、rowBitsがcolBitsを指すようにして同じコードを実行できます。この場合、ボード幅=ボード高さ= 8と仮定した疑似コード...
foreach color in colors {
foreach bits in rowBits, colBits {
foreach row in 0..7 { // row is actually col the second time through
// find starts, as above but in bits[row]
// find missings, as above
// generate matches, as above but in bits[row-1], bits[row], and bits[row+1]
// loop across bits in each matches var,
// evaluate and/or collect them, again as above
}
}
}
素敵な2D配列をビットに変換する手間をかけたくない場合はどうなりますか?8x8ボード、1セルあたり8ビット、64ビット対応のプロセッサーを使用すると、8セル= 8バイト= 64ビットというように、それでうまくいく可能性があります。これでボード幅に固定されましたが、これは有望なようです。
「0」が予約されており、ストーンが1から始まり、NUM_STONE_TYPESまで含まれているとします。
startOfXXSequence = rowBytes ^ (rowBytes << (8*1))
// now all bytes that are 0x00 are the start of a XX sequence
startOfXoXSequence = rowBytes ^ (rowBytes << (8*2))
// all bytes that are 0x00 are the start of a XoX sequence
これは、色ごとに1つのパスを必要としないことに注意してください。のようなものをBRBRBRGY
取得します-上位4バイトがゼロになり、可能なシーケンスがそこから始まることを示します。startOfXoXSequence
0x00 00 00 00 aa bb cc dd
遅くなっているので、ここから離れ、後で戻ってくる可能性があります。xorsと「最初のゼロバイトを検出する」トリックでこの流れを続けるか、整数のSIMD拡張機能を調べることができます。
leftMatches
、rightMatches
、missingTopOrBottomStone
行/列のすべての組み合わせのために。計算を保存するために必要なビットはですcolumns * 3 * (2 ^ columns)
。2 ^ columns
可能性でありcolumns * 3
、3つの計算すべてに必要なビットです。
あなたは問題を考えすぎています。1行が8行の場合、match-3には6つの可能な位置があるため、8x8ボード全体では、96の可能なmatch-3しかありません。96の可能性をチェックすると、CPU時間がわずかに使用されます。おそらく、1つのフレームを描画するだけで数千倍のクロックサイクルを使用しています。
マッチ3を行うことができるスワップ2隣接の動きを探している場合...可能な位置ごとに、すでに2ピースがマッチする場合、マッチを行うことができる最大3つの可能なスワップがあります。ラインでは、考慮すべき動きが最大で23あり、ボード全体で考慮すべき動きは112あります。112の動きをチェックすることも、CPU時間のわずかな量です。
一致できる組み合わせはほんのわずかです。XXが中心の下または上にある(および90度回転した)、およびxx。「。」の右側または上部に1つ (そして両方の軸のバリエーションで回転およびミラーリングされます。そのため、(最大4x3)グリッドを8x8グリッドと照合する必要がある14の異なる検索パターンになります。パターンをブルートフォースして8x8グリッドに沿ってスライドし、チェックします。 xは同じですがあります場所であれば。もしそれがトリプルを作成することができるセットです。ですので、
だから、基本的には、スライド(例えば)
。。。。
xx.xの
。。。。
または(anotheher例)
。X。
X。 。
。X。
軒並み-すべての3つのx位置が同じアイコンを持っている場合、それは可能な動きだ。
そして、すべての可能な移動のための14倍という。
速度が本当に問題である場合、私は次のようなことを試すように言うでしょう:
左上から始めて、グリッド全体を繰り返します。
6列目までのグリッド上のすべてのポイントで、タイルを1つ右と下にチェックします(これを行ったので、左と上をチェックする必要はありません)。
7番目の列に到達したら、チェックダウンするだけです。(列が2つしかない場合、3つのスペースはありません)
それらが一致する場合は、タイルを1つずつチェックしてみてください。そうでない場合は、次のタイルに移動します。
8列目が終わったら、1行下に移動します。
ただし、8x8グリッドの場合、最適化はそれほど必要ありません。より簡単なフォームは(同様)です。
左上から始めて、グリッド全体を繰り返します。
6列目までのグリッド上のすべてのポイントで、タイルを1つ右と下にチェックします(これを行ったので、左と上をチェックする必要はありません)。
それらが一致する場合は、タイルを1つずつチェックしてみてください。そうでない場合は、次のタイルに移動します。ボード外エラーを「同じではない」ものとして処理します。
8列目が終わったら、1行下に移動します。
これにより、一方の側にコードがあり、その下にあるコードを少し調整したif構造体がないため、forループではるかに単純なコードが可能になります。
美しくもエレガントでもありませんが、マッチ3ゲームをJavaScriptで記述しました。GitHubのソースコードでは、Webブラウザーでmatch-3ロジックを表示します。https://github.com/richtaur/bombada/blob/master/htdocs/js/match3.js#L184