既知のキューを持つマッチゲームを最適化するアルゴリズム


10

Flowerzと呼ばれるゲームのソルバーをC#.NETで記述しようとしています。参考までに、MSNで再生できます。ここ:http : //zone.msn.com/gameplayer/gameplayer.aspx?game=flowerz。私は楽しみのためにそれを書いているのであり、どんな種類の割り当てや仕事に関連するもののためでもありません。このため、唯一の制限は私のコンピューター(8 GBのRAMを搭載したIntel i7コア)です。私に関する限り、他の場所で実行する必要はありません。

つまり、そのルールは次のようになります。

  • 色とりどりの花でいっぱいの列があります。その長さは任意です
    • キューは影響を受けません
    • キューはレベルの開始時に生成されます
  • 花は1つまたは2つの色を持っています。
    • 2つの色がある場合、外側の色と内側の色があります。2色の場合、外側の色がマッチングに使用されます。
    • 一致する場合、外側の色は消え、花は内側の花と同じ色の単一色の花になります。
  • ゲームの目標は、同じ色の3つ(またはそれ以上)のマッチを作成することです
    • 単色の花がマッチの一部である場合、それは競技場から削除され、空のスペースが作成されます
    • 単色の花と2色の花の外側の色を一致させることができます。この場合、単色の花は消え、2色の花の外側の色は消え、内側の色は残ります
  • キューが空であるときは、ラウンドを獲得し、少なくとも1つの空のスペースが余っています
  • カスケード一致が可能です。カスケードは、3つ(またはそれ以上)の外側の花が消え、その内側の色が3(またはそれ以上)の花の別のチェーンを形成するときです。
  • 競技場は常に7x7です
  • フィールドのいくつかのスペースは岩で覆われています
    • あなたは岩の上に花を置くことはできません
  • キューにはスペードを含めることもできます。スペードを使用して、配置した花を空いているスペースに移動できます。
    • スペードを使用する必要がありますが、実際には花を動かす必要はありません。花を元の場所から直接戻すことは完全に合法です。
  • キューには、色付きの蝶を含めることもできます。花にこの蝶を使うと、花は蝶の色になります
    • 2つの色を持つ花に蝶を適用すると、花は1つの色、つまり蝶の色のみを取得します。
    • あなたは空のスペースやすでにこの色を持っている花に蝶を無駄にすることができます
  • フィールドをクリアしてもゲームに勝つことはできません

ソルバーの目標は単純です。できるだけ多くの余ったスペースをフィールドに残して、キューを空にする方法を見つけます。基本的に、AIがゲームをプレイします。ソルバーの出力は、見つかった移動のリストです。私はスコアには興味がありませんが、できるだけ長く生き残るために、可能な限り多くのオープンスペースを残す動きに興味があります。

言うまでもなく、キューが大きくなるほど検索スペースは急速に大きくなるため、ブルートフォースは問題外です。キューは15から始まり、2、3レベルごとに5ずつ増えます。そしてもちろん、最初の花を(0,0)に配置し、2番目の花を(0,1)に配置することは、特に最初の花を(1,0)に配置し、2番目の花を(0,0)に配置することとは異なります。フィールドには、以前のラウンドからの花がすでに生息しています。そのような単純な決定は、それを作るかどうかの違いを作ることができます。

私が持っている質問は次のとおりです。

  • これはどのような問題ですか?(巡回セールスマン、ナップザック、またはその他の組み合わせの問題を考えてください)。これを知っていると、私のGoogle風味が少し良くなる可能性があります。
  • どのようなアルゴリズムで良い結果をすばやく得ることができますか?

後者について:最初に、独自のヒューリスティックアルゴリズムを記述しようとしました(基本的に:キューを知っている場合、どうすれば解決できますか?)。

私は遺伝的アルゴリズムを使用することを考えていました(少なくともそれを使用する方法を知っているため...)が、ボードのバイナリ表現を決定する際にいくつかの問題があります。次に、クロスオーバーの問題がありますが、これは、順序付けされたクロスオーバー演算子または同様のタイプの操作で解決できます。

私の推測では、ソルバーは常にボード構成とそれが空にしようとしているキューを知っている必要があります。

ニューラルネットワークやファジーロジックシステムなどの他のヒューリスティックアルゴリズムをいくつか知っていますが、どれが最も適切であるか、または当面のタスクにより適した他のものがあるかどうかを知る経験がありません。


自分が取り組んでいるいくつかの複雑なゲームの検索スペースが32Gbになることを一度確認しました。当時(私は20Mbのディスクドライブを持っていました)、それは実現不可能でしたが、最近では、一部のコンピューターではRAMでほぼ実行可能です。
ジョナサン

一致すると、1色のみの花は完全に消えますか?また、2色の花は、1色の花の単一色に対して外層を一致させることができますか 私は両方の点でそうだと思いますが、これらは問題の説明で明示的に指定されていません...
Steven Stadnicki

@StevenStadnickiありがとう!その情報を元の質問に追加しました。
user849924 2013年

1
ちなみに、ちなみに、この問題の「ブール」バージョン(最後に花をキューに入れて、ボードを完全に空にする方法はありますか?)はNP完全である可能性が非常に高いです。これはNP完全なClickomania問題(erikdemaine.org/clickomania)と明らかに類似しており、問題はNPよりも難しくありません。これは、(多項式の長さの)解決策が与えられているため、シミュレーションを実行するだけで簡単に検証できるためです。これは、最適化の問題がおそらくFP ^ NPにあることを意味します。
Steven Stadnicki 2013年

回答:


9

一見すると、これは単一エージェントの検索問題のようです。つまり、エージェントは1人(AI「プレーヤー」)です。ゲームボードとキューの状態を表すゲーム状態があり、特定の状態から新しい状態を生成できる後続関数があります。

状態が「解決済み」の状態であるときに通知する目標基準もあります。そして、パスコスト -与えられた状態(この場合は、常に「1人の移動」)への進出のコスト。

この種の典型的なパズルの1つが15パズルです。そしてそれを解決する典型的な方法は、情報に基づいた検索です -例えば、古典ヒューリスティック検索A *とその変形。


ただし、この一目でわかるアプローチには問題があります。A *のようなアルゴリズムは、目標までの最短経路を提供するように設計されています(たとえば、最小の移動数)。あなたのケースでは、動きの数が常に固定されている-何の最短経路がありません-ヒューリスティック検索があなたに与えるようにパスを完了したゲーム。

あなたが望むのは、あなたに最高の完成したゲーム状態を与える一連の動きです。

だからあなたがしなければならないことは少し問題を変えることです。ゲームボードが「状態」である代わりに、移動シーケンスが「状態」になります。(つまり、アイテムをキューの「D2、A5、C7、B3、A3、...」の位置に配置します)

これは、これらの状態がどのように生成されるかを実際には気にしないことを意味します。理事会自体は付随的であり、特定の状態の品質を評価するためにのみ必要です。

これにより、問題が最適化問題に変わり、ローカル検索アルゴリズムで解決できます(これは、基本的に、特定の状態の周りに状態を作成し、状態間のパスを気にすることなく、最適な状態を選択することを意味します)。

この種のプロトタイプパズルはEight Queens Puzzleです。

このクラスの問題では、状態空間を検索して適切な解を見つけます。この場合、「良い」は目的関数評価関数、または遺伝的アルゴリズムの場合はフィットネス関数とも呼ばれます)によって評価されます。

問題の場合、目的の関数は、障害状態に達する前に使用されたキュー内の項目の数(Nはキューの長さ)に対して、0からNの間の値を返す可能性があります。それ以外の場合は、N + Mの値です。ここで、Mは、キューが空になった後にボードに残っている空白スペースの数です。したがって、値が大きいほど、ソリューションは「客観的に優れている」ことになります。

(この時点で、ゲームを実行するコードのがらくたを最適化する必要があることに注意する必要があります。これにより、状態が目的関数に使用できる完成したボードに変わります。)


ローカル検索アルゴリズムの例については:基本パターンは、特定の状態を取り、それを変化させ、より良い結果をもたらす次の状態に向かって移動する山登り検索です。

明らかに、これは極大値(など)で行き詰まる可能性があります。この形式では、貪欲なローカル検索と呼ばれます。この問題やその他の問題に対処するためのバリエーションはたくさんあります(Wikipediaがカバーしています)。その一部(例:ローカルビームサーチ)は、複数の状態を一度に追跡します。

これに対する1つの特定のバリエーションは、遺伝的アルゴリズムWikipedia)です。遺伝的アルゴリズムの基本的な手順は次のとおりです。

  1. 状態をある種の文字列に変換する方法を決定します。あなたの場合、これは1から49までのキューの長さの数字の文字列である可能性があります(7x7ボードで可能なすべての配置を表し、おそらくそれぞれ1バイトが格納されています)。(「スペード」ピースは、移動の各フェーズについて、2つの後続のキューエントリで表すことができます。)
  2. 繁殖個体群をランダムに選択し、より良い適応度を持つ州に高い確率を与えます。繁殖個体数は元の個体数と同じサイズである必要があります。元の個体群から複数回州を選択できます。
  3. 繁殖個体群の状態をペアにする(最初は2番目に、3番目は4番目に行く、など)
  4. 各ペア(文字列内の位置)の交差点をランダムに選択します。
  5. クロスオーバーポイントの後の文字列の部分を交換して、ペアごとに2つの子孫を作成します。
  6. 子孫の各状態をランダムに変異させます。たとえば、文字列内のランダムな位置をランダムな値に変更することをランダムに選択します。
  7. 母集団が1つ以上の解に収束するまで(または所定の世代数になるか、十分に良い解が見つかるまで)、新しい母集団でプロセスを繰り返します。

それはのような遺伝的アルゴリズムソリューションは感じているかもしれない、あなたの問題に適している-いくつかの調整と。私が目にする最大の問題は、上記の文字列表現では、非常に異なる前半分で状態のテール半分を切り替えると、「デッド」状態になる可能性が高いことです(2つの半分の間の競合する動きのため、低いフィットネススコアで)。

おそらく、この問題を克服することは可能です。頭に浮かぶアイデアの1つは、前半が似ている州が繁殖ペアになる可能性を高めることです。これは、ペアリングする前に、繁殖している州の人口を並べ替えるのと同じくらい簡単かもしれません。また、世代数が増えるにつれて、クロスオーバーの可能性のある位置を、ストリングの最初から最後まで徐々に移動することもできます。

「四角がいっぱい」の障害状態に遭遇することに対してより耐性がある(おそらく完全に免疫がある)状態での動きの表現を考え出すことも可能かもしれません。おそらく、前の動きからの相対座標として動きを表します。または、特定の位置に最も近い空のスペースを選択して移動します。

このような重要なAIの問題と同様に、重要な調整が必要になります。

そして、前述したように、他の主要な課題は、単に目的関数を最適化することです。これを高速化すると、大量のスペースを検索し、キューが長いゲームの解決策を検索できます。


この答えについては、特にすべての用語を正しく理解するために、大学のAI教科書「人工知能:現代のアプローチ」をRussellとNorvigが詳しく調べなければなりませんでした。「良い」かどうかはわかりませんが(他に比較するAIテキストはありません)、悪いことではありません。少なくともそれはかなり大きいです;)


私はクロスオーバーの問題も特定しました:子供がキューにあるアイテムよりも多くのアイテムを配置している可能性は十分にあります(TSPのGAの欠如のようなもの:彼は後に2回以上(またはまったく!) 。クロスオーバーたぶん命じたクロスオーバー(permutationcity.co.uk/projects/mutants/tsp.htmlは)仕事ができるあなたが移動し、一連の状態を作るとき、これは特に適用される。。
user849924

よくわからない-私の考えでは、失敗状態とは、すでに占有されている位置に駒が置かれていることです(したがって、そのゲームは早期に終了し、結果としてフィットネススコアが低くなります)。したがって、キューの長さは遺伝的文字列の長さと一致します-それは決して間違った長さではありません。それでも、あなたはスワッピングと注文のアイデアを持っているかもしれません。特定の順序でゲームが完了し、2つの手を入れ替えた場合、単純に1つ(または2つ)の移動の位置をランダムに設定した場合よりも、変化した状態が完了したゲームになる可能性がはるかに高いと思います。
Andrew Russell

失敗状態は、移動を配置するオプションがなくなったとき、つまり、空のスペースがなくなり、その移動で一致が発生しなくなったときです。あなたが言っているのと同じように、あなたはそれをすでに占有されている位置に配置しなければなりません(しかし、それは開始する場所がこれ以上ない場合にのみ当てはまります)。私が投稿したクロスオーバーは面白いかもしれません。A染色体はA1、B1、...、G1、A2、B2、C2に配置され、B染色体はG7 ... A7、G6、F6、E6に配置されています。Aからいくつかのランダムを選択し、それらのインデックスを保持します。BからAの補数を選択し、それらのインデックスを保持して子のためにマージします。
user849924 2013年

このクロスオーバーの「問題」は、同じ場所での複数の移動が許可されていることです。しかし、これはStefan KのソリューションのSimulateAutomaticChangesに似たもので簡単に解決できるはずです。子のムーブセット/状態を、プレイフィールドの基本状態に適用します(単純にすべての動きを1つずつ適用します)。 )を達成することはできません(占有された場所に花を配置する必要があるため)、その場合、子は無効であり、再度繁殖する必要があります。ここで、障害状態がポップアップします。私は今それを手に入れました、へえ。:D
user849924 2013年

2つの理由で、これを答えとして受け入れます。まず、GAにこの問題を解決させるために必要なアイデアを教えてくれました。第二に:あなたが最初でした。; p
user849924 2013年

2

分類

答えは簡単ではありません。ゲーム理論にはゲームの分類がいくつかありますが、そのゲームと特別な理論との明確な1対1の一致はないようです。これは、組み合わせ問題の特別な形式です。

これは巡回セールスマンではありません。これは、「ノード」にアクセスして、最後のノードから次のノードに到達するためにある程度のコストがかかる注文を決定することになります。キューを並べ替えることも、マップ上のすべてのフィールドを使用する必要もありません。

一部の項目を「ナップザック」に入れると一部のフィールドが空になるため、ナップザックは一致しません。したがって、これは拡張された形式の可能性がありますが、おそらくアルゴリズムが適用されない可能性があります。

ウィキペディアはここに分類に関するいくつかのヒントを与えます:http : //en.wikipedia.org/wiki/Game_theory#Types_of_games

私はそれを「離散時間最適制御問題」(http://en.wikipedia.org/wiki/Optimal_control)として分類しますが、これが役立つとは思いません。

アルゴリズム

完全なキューが本当にわかっている場合は、ツリー検索アルゴリズムを適用できます。あなたが言ったように、問題の複雑さはキューの長さとともに非常に速く成長します。メモリをあまり必要としない「深さ優先検索(DFS)」のようなアルゴリズムを使用することをお勧めします。スコアは重要ではないので、最初の解決策を見つけた後に停止することができます。最初に検索するサブブランチを決定するには、順序付けにヒューリスティックを適用する必要があります。つまり、評価関数(たとえば、空のフィールドの数。これはより高度なものであるほど優れています)を記述し、次の移動のどれが最も有望かを比較するスコアを与える必要があることを意味します。

次に必要なのは次の部分のみです。

  1. ゲームのすべての情報を格納するゲーム状態のモデル(例:ボードステータス/マップ、キュー、移動番号/キュー内の位置)
  2. 与えられたゲーム状態のすべての有効な動きを提供する動きジェネレーター
  3. 「移動」および「移動を元に戻す」機能。与えられた(有効な)動きをゲーム状態に適用/元に戻します。一方、「移動」機能は「元に戻す」機能の「元に戻す」情報を保存する必要があります。ゲームの状態をコピーして、反復ごとに変更すると、検索が大幅に遅くなります。少なくともスタックに状態を保存するようにしてください(=ローカル変数、 "new"を使用した動的割り当てなし)。
  4. 各ゲームの状態に匹敵するスコアを与える評価関数
  5. 検索機能

以下は、深さ優先検索の不完全な参照実装です。

public class Item
{
    // TODO... represents queue items (FLOWER, SHOVEL, BUTTERFLY)
}

public class Field
{
    // TODO... represents field on the board (EMPTY or FLOWER)
}

public class Modification {
    int x, y;
    Field originalValue, newValue;

    public Modification(int x, int y, Field originalValue, newValue) {
        this.x = x;
        this.y = y;
        this.originalValue = originalValue;
        this.newValue = newValue;
    }

    public void Do(GameState state) {
        state.board[x,y] = newValue;
    }

    public void Undo(GameState state) {
        state.board[x,y] = originalValue;
    }
}

class Move : ICompareable {

    // score; from evaluation function
    public int score; 

    // List of modifications to do/undo to execute the move or to undo it
    Modification[] modifications;

    // Information for later knowing, what "control" action has been chosen
    public int x, y;   // target field chosen
    public int x2, y2; // secondary target field chosen (e.g. if moving a field)


    public Move(GameState state, Modification[] modifications, int score, int x, int y, int x2 = -1, int y2 = -1) {
        this.modifications = modifications;
        this.score = score;
        this.x = x;
        this.y = y;
        this.x2 = x2;
        this.y2 = y2;
    }

    public int CompareTo(Move other)
    {
        return other.score - this.score; // less than 0, if "this" precededs "other"...
    }

    public virtual void Do(GameState state)
    {
        foreach(Modification m in modifications) m.Do(state);
        state.queueindex++;
    }

    public virtual void Undo(GameState state)
    {
        --state.queueindex;
        for (int i = m.length - 1; i >= 0; --i) m.Undo(state); // undo modification in reversed order
    }
}

class GameState {
    public Item[] queue;
    public Field[][] board;
    public int queueindex;

    public GameState(Field[][] board, Item[] queue) {
        this.board = board;
        this.queue = queue;
        this.queueindex = 0;
    }

    private int Evaluate()
    {
        int value = 0;
        // TODO: Calculate some reasonable value for the game state...

        return value;
    }

    private List<Modification> SimulateAutomaticChanges(ref int score) {
        List<Modification> modifications = new List<Modification>();
        // TODO: estimate all "remove" flowers or recoler them according to game rules 
        // and store all changes into modifications...
        if (modifications.Count() > 0) {
            foreach(Modification modification in modifications) modification.Do(this);

            // Recursively call this function, for cases of chain reactions...
            List<Modification> moreModifications = SimulateAutomaticChanges();

            foreach(Modification modification in modifications) modification.Undo(this);

            // Add recursively generated moves...
            modifications.AddRange(moreModifications);
        } else {
            score = Evaluate();
        }

        return modifications;
    }

    // Helper function for move generator...
    private void MoveListAdd(List<Move> movelist, List<Modifications> modifications, int x, int y, int x2 = -1, int y2 = -1) {
        foreach(Modification modification in modifications) modification.Do(this);

        int score;
        List<Modification> autoChanges = SimulateAutomaticChanges(score);

        foreach(Modification modification in modifications) modification.Undo(this);

        modifications.AddRange(autoChanges);

        movelist.Add(new Move(this, modifications, score, x, y, x2, y2));
    }


    private List<Move> getValidMoves() {
        List<Move> movelist = new List<Move>();
        Item nextItem = queue[queueindex];
        const int MAX = board.length * board[0].length + 2;

        if (nextItem.ItemType == Item.SHOVEL)
        {

            for (int x = 0; x < board.length; ++x)
            {
                for (int y = 0; y < board[x].length; ++y)
                {
                    // TODO: Check if valid, else "continue;"

                    for (int x2 = 0; x2 < board.length; ++x2)
                    {
                        for(int y2 = 0; y2 < board[x].length; ++y2) {
                            List<Modifications> modifications = new List<Modifications>();

                            Item fromItem = board[x][y];
                            Item toItem = board[x2][y2];
                            modifications.Add(new Modification(x, y, fromItem, Item.NONE));
                            modifications.Add(new Modification(x2, y2, toItem, fromItem));

                            MoveListAdd(movelist, modifications, x, y, x2, y2);
                        }
                    }
                }
            }

        } else {

            for (int x = 0; x < board.length; ++x)
            {
                for (int y = 0; y < board[x].length; ++y)
                {
                    // TODO: check if nextItem may be applied here... if not "continue;"

                    List<Modifications> modifications = new List<Modifications>();
                    if (nextItem.ItemType == Item.FLOWER) {
                        // TODO: generate modifications for putting flower at x,y
                    } else {
                        // TODO: generate modifications for putting butterfly "nextItem" at x,y
                    }

                    MoveListAdd(movelist, modifications, x, y);
                }
            }
        }

        // Sort movelist...
        movelist.Sort();

        return movelist;
    }


    public List<Move> Search()
    {
        List<Move> validmoves = getValidMoves();

        foreach(Move move in validmoves) {
            move.Do(this);
            List<Move> solution = Search();
            if (solution != null)
            {
                solution.Prepend(move);
                return solution;
            }
            move.Undo(this);
        }

        // return "null" as no solution was found in this branch...
        // this will also happen if validmoves == empty (e.g. lost game)
        return null;
    }
}

このコードは動作することが確認されておらず、コンパイル可能でも完全でもありません。しかし、それはあなたにそれを行う方法のアイデアを与えるはずです。最も重要な作業は評価関数です。より高度なものであるほど、後でアルゴリズムが試行する(そして元に戻す必要がある)間違った「試行」が行われます。これにより、複雑さが大幅に軽減されます。

これが遅すぎる場合は、2人ゲームのいくつかのメソッドをHashTablesとして適用することもできます。そのためには、評価する各ゲーム状態の(反復)ハッシュキーを計算し、ソリューションにつながらない状態にマークを付ける必要があります。たとえば、Search()メソッドが「null」を返す前にHashTableエントリを作成する必要があり、Search()に入るときに、この状態がすでに肯定的な結果なしに到達しているかどうかを確認し、そうである場合は「null」を返します。さらなる調査。これには巨大なハッシュテーブルが必要であり、「ハッシュ衝突」を受け入れる必要があります。これにより、おそらく既存のソリューションが見つからない可能性がありますが、ハッシュ関数が十分であり、テーブルが十分に大きい(計算可能なリスクのリスク)。

あなたの評価関数が最適であると仮定して、この問題を(あなたが説明したように)より効率的に解決する他のアルゴリズムはないと思います...


はい、完全なキューを知ることができます。評価関数の実装でも、有効だが潜在的に悪い配置を考慮しますか?同じ色がすでにフィールド上にある場合、別の色の花の隣に配置するような動きになる可能性はありますか?または、スペースが不足しているため、まったく異なるブロックが一致する場所に花を配置しますか?
user849924 2013年

この回答から、モデルとゲームルールの操作方法についてのアイデアが得られたので、賛成します。ご協力ありがとうございます。
user849924 2013年

@ user849924:はい、もちろん、評価関数はそのための評価「値」を計算する必要があります。現在のゲームの状態が悪化する(失うに近い)ほど、返される評価値は悪化します。最も簡単な評価は、空のフィールドの数を返すことです。同じような色の花の隣に配置された花ごとに0.1を追加することで、これを改善できます。関数がいくつかのランダムなゲーム状態を選択していることを確認するには、それらの値を計算して比較します。あなたは、状態Aがより良い状態Bよりもあると考えられる場合は、スコアのフォアAはBのものよりも良いはずです
SDwarfs
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.