マッチ3パズルゲームアルゴリズム


7

私は「コールオブアトランティス」のようなマッチ3パズルゲームを自分で書こうとしています。最も重要なアルゴリズムは、可能な3つすべてのマッチ3の可能性を見つけることです。参照できるオープンソースプロジェクトはありますか?またはアルゴリズムにキーワードがありますか?すべての可能性を計算するためのより高速なアルゴリズムを探しています。

ルール:

  • 対角線は考慮されません。
  • サイズは8x8です

ありがとう。


タイルの8x8グリッドがそれぞれ異なることを意味し、何かをするために3を一列に並べますか?宝石をちりばめたような?編集:リンクを見つけました。はい、それはビジュエルドに似ています。
共産主義者のダック

もう1つの問題は、3つのオブジェクトを続けて照合してそれらを排除した後、すべてのグリッドを再スキャンする必要があるか、それとも部分的にスキャンする必要があるかです。
スタン

1
十分な時間があるので(マッチを削除すると、ある種のアニメーションが再生されます)、グリッド全体を再スキャンする十分な時間があります。(私は知っています、私は以下に書いた方法でそれを実装しました)。
Kaj 2010

回答:


17

これらのゲームの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つです。 XoXoXX、または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バイトがゼロになり、可能なシーケンスがそこから始まることを示します。startOfXoXSequence0x00 00 00 00 aa bb cc dd

遅くなっているので、ここから離れ、後で戻ってくる可能性があります。xorsと「最初のゼロバイトを検出する」トリックでこの流れを続けるか、整数のSIMD拡張機能を調べることができます。


ちょっとした数学の実装が大好き(私はパフォーマンスフリークです):o)ただし、8x8グリッドの場合は、実際には必要ありません。上記の露骨な素朴な実装を60fpsで楽しく実行しながら、同時に実際の入力を読み取るために実行中のゲームのグラフィックスウィンドウをこすりました。
Kaj 2010

@Kaj:ええ、私は反対しません=)私はおそらくそれについて前もってもっと明確にすべきだったでしょう。私が言及したコードで時々再帰的に呼び出される4つにネストされたforループを持つ巨大な関数でさえ、移植した限られたプラットフォームでうまく動作しました。これはめったに呼び出されず、走査する小さなデータセットを持っています。
2010

素晴らしい詳細と適切なクレジット表示のための+1:op
Kaj

私はこの答えが最初は実用的であるのが大好きですが、それはそこで止まらず、後でアカデミックに
飛び込み

パフォーマンスを向上させるためには、事前計算することができleftMatchesrightMatchesmissingTopOrBottomStone行/列のすべての組み合わせのために。計算を保存するために必要なビットはですcolumns * 3 * (2 ^ columns)2 ^ columns可能性でありcolumns * 3、3つの計算すべてに必要なビットです。
Veehmot 2014

9

あなたは問題を考えすぎています。1行が8行の場合、match-3には6つの可能な位置があるため、8x8ボード全体では、96の可能なmatch-3しかありません。96の可能性をチェックすると、CPU時間がわずかに使用されます。おそらく、1つのフレームを描画するだけで数千倍のクロックサイクルを使用しています。

マッチ3を行うことができるスワップ2隣接の動きを探している場合...可能な位置ごとに、すでに2ピースがマッチする場合、マッチを行うことができる最大3つの可能なスワップがあります。ラインでは、考慮すべき動きが最大で23あり、ボード全体で考慮すべき動きは112あります。112の動きをチェックすることも、CPU時間のわずかな量です。


7

一致できる組み合わせはほんのわずかです。XXが中心の下または上にある(および90度回転した)、およびxx。「。」の右側または上部に1つ (そして両方の軸のバリエーションで回転およびミラーリングされます。そのため、(最大4x3)グリッドを8x8グリッドと照合する必要がある14の異なる検索パターンになります。パターンをブルートフォースして8x8グリッドに沿ってスライドし、チェックします。 xは同じですがあります場所であれば。もしそれがトリプルを作成することができるセットです。ですので、
だから、基本的には、スライド(例えば)

。。。。
xx.xの
。。。。
または(anotheher例)
。X。
X。 。
。X。

軒並み-すべての3つのx位置が同じアイコンを持っている場合、それは可能な動きだ。
そして、すべての可能な移動のための14倍という。


2
これは、既存の3つの実行を探すのではなく、実際の有効な移動を見つけるように見える唯一の答えです。
JasonD 2010

1
ああ、その質問を誤解した。最近そうしがちなので。:-\
Ricket

助けようとすることに自分自身に苦労しないでください!この質問は、どちらの解釈にもいくらか開かれていました。
Kaj 2010

2

速度が本当に問題である場合、私は次のようなことを試すように言うでしょう:

  • 左上から始めて、グリッド全体を繰り返します。

  • 6列目までのグリッド上のすべてのポイントで、タイルを1つ右と下にチェックします(これを行ったので、左と上をチェックする必要はありません)。

  • 7番目の列に到達したら、チェックダウンするだけです。(列が2つしかない場合、3つのスペースはありません)

  • それらが一致する場合は、タイルを1つずつチェックしてみてください。そうでない場合は、次のタイルに移動します。

  • 8列目が終わったら、1行下に移動します。

ただし、8x8グリッドの場合、最適化はそれほど必要ありません。より簡単なフォームは(同様)です。

  • 左上から始めて、グリッド全体を繰り返します。

    • 6列目までのグリッド上のすべてのポイントで、タイルを1つ右と下にチェックします(これを行ったので、左と上をチェックする必要はありません)。

    • それらが一致する場合は、タイルを1つずつチェックしてみてください。そうでない場合は、次のタイルに移動します。ボード外エラーを「同じではない」ものとして処理します。

    • 8列目が終わったら、1行下に移動します。

これにより、一方の側にコードがあり、その下にあるコードを少し調整したif構造体がないため、forループではるかに単純なコードが可能になります。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.