9

コンピュータープログラムにカードゲームをプレイさせるために、ミニマックス検索(アルファベータプルーニングを使用)、またはむしろネガマックス検索を使用したいと思います。

カードゲームは実際には4人のプレイヤーで構成されています。そこで、ミニマックス法などを使えるようにするために、ゲームを「他人」に対して「私」に単純化しています。それぞれの「移動」の後、ゲーム自体から現在の状態の評価を客観的に読み取ることができます。4人のプレイヤー全員がカードを置いたとき、最も高いプレイヤーがすべてのプレイヤーに勝ちます-そしてカードの値がカウントされます。

他の3人のプレイヤー間のカードの分配が正確にどのようになっているのかわからないので、自分のものではないカードですべての可能な分配(「世界」)をシミュレートする必要があると思いました。あなたは12枚のカードを持っています、他の3人のプレーヤーは合計で36枚のカードを持っています。

したがって、私のアプローチはこのアルゴリズムです。ここで、playerはプログラムが動きを見つける必要があるかもしれない3人のコンピュータープレーヤーを象徴する1から3の間の数字です。そして-player、対戦相手、つまり他の3人のプレイヤー全員を表します。

private Card computerPickCard(GameState state, ArrayList<Card> cards) {
    int bestScore = Integer.MIN_VALUE;
    Card bestMove = null;
    int nCards = cards.size();
    for (int i = 0; i < nCards; i++) {
        if (state.moveIsLegal(cards.get(i))) { // if you are allowed to place this card
            int score;
            GameState futureState = state.testMove(cards.get(i)); // a move is the placing of a card (which returns a new game state)
            score = negamaxSearch(-state.getPlayersTurn(), futureState, 1, Integer.MIN_VALUE, Integer.MAX_VALUE);
            if (score > bestScore) {
                bestScore = score;
                bestMove = cards.get(i);
            }
        }
    }
    // now bestMove is the card to place
}

private int negamaxSearch(int player, GameState state, int depthLeft, int alpha, int beta) {
    ArrayList<Card> cards;
    if (player >= 1 && player <= 3) {
        cards = state.getCards(player);
    }
    else {
        if (player == -1) {
            cards = state.getCards(0);
            cards.addAll(state.getCards(2));
            cards.addAll(state.getCards(3));
        }
        else if (player == -2) {
            cards = state.getCards(0);
            cards.addAll(state.getCards(1));
            cards.addAll(state.getCards(3));
        }
        else {
            cards = state.getCards(0);
            cards.addAll(state.getCards(1));
            cards.addAll(state.getCards(2));
        }
    }
    if (depthLeft <= 0 || state.isEnd()) { // end of recursion as the game is finished or max depth is reached
        if (player >= 1 && player <= 3) {
            return state.getCurrentPoints(player); // player's points as a positive value (for self)
        }
        else {
            return -state.getCurrentPoints(-player); // player's points as a negative value (for others)
        }
    }
    else {
        int score;
        int nCards = cards.size();
        if (player > 0) { // make one move (it's player's turn)
            for (int i = 0; i < nCards; i++) {
                GameState futureState = state.testMove(cards.get(i));
                if (futureState != null) { // wenn Zug gültig ist
                    score = negamaxSuche(-player, futureState, depthLeft-1, -beta, -alpha);
                    if (score >= beta) {
                        return score;
                    }
                    if (score > alpha) {
                        alpha = score; // alpha acts like max
                    }
                }
            }
            return alpha;
        }
        else { // make three moves (it's the others' turn)
            for (int i = 0; i < nCards; i++) {
                GameState futureState = state.testMove(cards.get(i));
                if (futureState != null) { // if move is valid
                    for (int k = 0; k < nCards; k++) {
                        if (k != i) {
                            GameState futureStateLevel2 = futureState.testMove(cards.get(k));
                            if (futureStateLevel2 != null) { // if move is valid
                                for (int m = 0; m < nCards; m++) {
                                    if (m != i && m != k) {
                                        GameState futureStateLevel3 = futureStateLevel2.testMove(cards.get(m));
                                        if (futureStateLevel3 != null) { // if move is valid
                                            score = negamaxSuche(-player, futureStateLevel3, depthLeft-1, -beta, -alpha);
                                            if (score >= beta) {
                                                return score;
                                            }
                                            if (score > alpha) {
                                                alpha = score; // alpha acts like max
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            return alpha;
        }
    }
}

これはうまくいくようですが、深さが1(depthLeft=1)の場合、プログラムはすでに平均50,000ムーブ(配置されたカード)を計算する必要があります。もちろん、これは多すぎます!

だから私の質問は:

  1. 実装はまったく正しいですか?このようなゲームをシミュレートできますか?特に不完全情報については?
  2. 速度と作業負荷のアルゴリズムをどのように改善できますか?
  3. たとえば、良い結果を維持しながら、可能な移動のセットを50%のランダムなセットに減らして、速度を向上させることはできますか?
  4. 私はUCTアルゴリズムが良い解決策であることに気づきました(多分)。このアルゴリズムを知っていますか?実装を手伝ってもらえますか?
4

2 に答える 2

10

受け入れられた答えが実際には入らない詳細を明確にしたいと思います。

多くのカードゲームでは、すべてを生成する代わりに、対戦相手が持つ可能性のある未知のカードをサンプリングできます。このサンプリングを行う際に、ショートスーツや特定のカードを持っている確率などの情報を考慮して、それぞれの可能なハンドの可能性を重み付けすることができます(各ハンドは、個別に解決する可能性のある世界です)。次に、完全情報検索を使用して各手を解決します。これらすべての世界での最良の動きは、多くの場合、全体として最良の動きですが、いくつかの注意点があります。

ポーカーのようなゲームでは、これはあまりうまく機能しません-ゲームはすべて隠された情報に関するものです。手に関する情報を隠しておくには、アクションのバランスを正確にとる必要があります。

しかし、トリックベースのカードゲームのようなゲームでは、これはかなりうまく機能します-特に新しい情報が常に明らかにされているためです。本当に良いプレーヤーは、とにかく誰もが何を持っているかについて良い考えを持っています。したがって、かなり強力なSkatおよびBridgeプログラムは、これらのアイデアに基づいています。

根底にある世界を完全に解決できる場合はそれが最善ですが、解決できない場合は、ミニマックスまたはUCTを使用して、各世界で最適な動きを選択できます。このプロセスを混合しようとするハイブリッドアルゴリズム(ISMCTS)もあります。ここでの主張に注意してください。単純なサンプリングアプローチはコーディングが簡単です。より複雑なアプローチの前に、より単純なアプローチを試す必要があります。

不完全情報へのサンプリングアプローチがうまく機能した場合について、さらに詳しい情報を提供するいくつかの研究論文を次に示します。

ゲームツリー検索における完全情報モンテカルロサンプリングの成功を理解する(このペーパーでは、サンプリングアプローチが機能する可能性が高い場合を分析します)。

トリックベースのカードゲームにおける状態評価、推論、および検索の改善(このペーパーでは、Skatでのサンプリングの使用について説明します)

計算が難しいゲームの不完全情報(このペーパーでは、Bridgeでのサンプリングについて説明します)

情報セットモンテカルロ木探索(このペーパーでは、最初の参照の問題を回避するために、サンプリングとUCT /モンテカルロ木探索を統合します。)

受け入れられた回答におけるルールベースのアプローチの問題は、初期ルールの作成に必要な以上の計算リソースを利用できないことです。さらに、ルールベースのアプローチは、作成できるルールの能力によって制限されます。検索ベースのアプローチでは、組み合わせ検索の力を利用して、プログラムの作成者よりもはるかに強力なプレイを生み出すことができます。

于 2015-05-04T18:09:19.390 に答える
5

実装したミニマックス検索は、不確実性が非常に高いゲームでは間違ったアプローチです。他のプレイヤー間のカードの配布がわからないため、実際のカードの配布では発生し得なかったゲームの探索に、検索に指数関数的な時間が費やされます。

他のプレーヤーのハンドに関する情報がほとんどまたはまったくない場合は、プレーの適切なルールから始めるのがより良いアプローチだと思います。のようなもの:

  1. ラウンドで最初にプレイする場合は、ラウンドに勝つ可能性がほとんどないため、最も低いカードをプレイします。
  2. ラウンドの最後にプレイする場合は、ラウンドに勝つ最も低いカードをプレイします。ラウンドに勝てない場合は、一番下のカードをプレイします。

プログラムで最初は検索に煩わされることなく、これらのルールに従ってプレイし、他のすべてのプレーヤーもこれらのヒューリスティックを使用することを想定します。 プログラムは、各ラウンドの最初と最後のプレーヤーがどのカードをプレイするかを観察するときに、各プレーヤーが保持している可能性のあるカードに関する情報のテーブルを作成できます。たとえば、9はこのラウンドで勝ったはずですが、プレーヤー3はそれをプレイしなかったので、9以上のカードを持っていてはなりません。各プレイヤーの手に関する情報が収集されると、検索スペースは最終的に、可能なゲームのミニマックス検索が次にプレイするカードに関する有用な情報を生成できるポイントに制限されます。

于 2012-10-02T04:15:58.490 に答える