2012-09-30 11 views
6

Chcę użyć wyszukiwania minimax (z przycinaniem alfa-beta), lub raczej wyszukiwania negamax, aby program komputerowy zagrał w grę karcianą.Używanie wyszukiwania minimax dla gier karcianych z niedoskonałymi informacjami

Gra karciana składa się w rzeczywistości z 4 graczy. Aby móc korzystać z minimax itp., Upraszczam grę "mnie" przeciw "innym". Po każdym "ruchu" możesz obiektywnie odczytać ocenę aktualnego stanu z samej gry. Kiedy wszyscy 4 gracze umieścili kartę, najwyższy wygrywa je wszystkie - a wartości kart liczą się.

Ponieważ nie wiesz, w jaki sposób dzieli się karty pomiędzy pozostałymi 3 graczami, pomyślałem, że musisz symulować wszystkie możliwe dystrybucje ("światy") z kartami, które nie są twoje. Masz 12 kart, pozostałe 3 graczy ma łącznie 36 kart.

Tak więc moim podejściem jest ten algorytm, w którym player jest liczbą od 1 do 3 symbolizującą trzy odtwarzacze komputerowe, których program może potrzebować, aby znaleźć ruchy. I -player oznacza przeciwników, a mianowicie wszystkich pozostałych trzech graczy razem.

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; 
     } 
    } 
} 

To wydaje się działać prawidłowo, ale na głębokości 1 (depthLeft=1), program już musi obliczyć 50.000 ruchów (umieszczone cards) na średniej. To oczywiście za dużo!

Więc moje pytania to:

  1. jest realizacja poprawne w ogóle? Czy możesz symulować taką grę? Co do niedoskonałych informacji, szczególnie?
  2. Jak ulepszyć algorytm prędkości i obciążenia pracą?
  3. Czy mogę na przykład zmniejszyć zestaw możliwych ruchów do losowego zestawu 50%, aby poprawić prędkość, zachowując przy tym dobre wyniki?
  4. Znalazłem UCT algorithm jako dobre rozwiązanie (może). Czy znasz ten algorytm? Czy możesz pomóc mi go wdrożyć?

Odpowiedz

3

Wyszukiwanie minimax, ponieważ zostało to zaimplementowane, jest niewłaściwe w przypadku gier, w których występuje tak wiele niepewności. Ponieważ nie znasz rozkładu kart między innymi graczami, twoje poszukiwania spędzą wykładniczy czas eksplorując gry, które nie mogłyby się wydarzyć, biorąc pod uwagę faktyczny rozkład kart.

Myślę, że lepszym podejściem byłoby zacząć od dobrych zasad gry, gdy masz mało lub nie ma informacji o rękach innych graczy. Rzeczy takie jak:

  1. Jeśli grasz jako pierwszy w rundzie, zagraj swoją najniższą kartę, ponieważ masz małą szansę na wygranie rundy.
  2. Jeśli grasz ostatni w rundzie, zagraj swoją najniższą kartę, która wygra rundę. Jeśli nie możesz wygrać rundy, zagraj swoją najniższą kartę.

Twój program początkowo nie zawraca sobie głowy wyszukiwaniem i po prostu graj zgodnie z tymi regułami i zakładaj, że wszyscy inni gracze również będą używać tej heurystyki. Gdy program obserwuje, które karty są pierwszymi i ostatnimi graczami w każdej rundzie, może utworzyć tabelę z informacjami o kartach, które każdy gracz prawdopodobnie posiada. Na przykład. 9 wygrałoby tę rundę, ale gracz 3 nie zagrał jej, więc nie może mieć żadnych kart o wartości 9 lub wyższej.W miarę gromadzenia informacji o ręce każdego gracza przestrzeń poszukiwań zostanie ostatecznie ograniczona do punktu, w którym wyszukiwanie minimax możliwych gier może dostarczyć użytecznych informacji o następnej karcie do gry.

+0

Hmm, w odniesieniu do minimaxingu pod koniec gry. W tym momencie wiesz, że potrzebujesz x trików, aby wygrać. Każdy świat, w którym nie możesz (nie powinien) wygrać, możesz zignorować. Bo jeśli ten świat ma rację, i tak przegrałeś. Jeśli opierasz swoje prawdopodobieństwa na światach, które prowadzą do wygranej (zasadniczo używając myślenia życzeniowego), możesz prawdopodobnie przyciąć wyszukiwanie jeszcze bardziej – Cruncher

4

Chcę wyjaśnić szczegóły, których nie dotyczy zaakceptowana odpowiedź.

W wielu grach karcianych można próbować nieznanych kart, które może mieć przeciwnik, zamiast generować je wszystkie. Możesz wziąć pod uwagę informacje, takie jak krótkie kombinacje oraz prawdopodobieństwo, że niektóre karty zostaną zagrane do tej pory, podczas robienia tego próbkowania, aby zważyć prawdopodobieństwo każdej możliwej ręki (każda ręka jest możliwym światem, który rozwiążemy niezależnie). Następnie rozwiązujesz każde rozdanie za pomocą doskonałego wyszukiwania informacji. Najlepszym ruchem na wszystkich tych światach jest często najlepszy ruch - z pewnym zastrzeżeniem.

W grach takich jak Poker to nie działa zbyt dobrze - w grze chodzi o ukryte informacje. Musisz dokładnie zbalansować swoje działania, aby ukryć informacje o swojej ręce.

Jednak w takich grach jak gry karciane oparte na trickach działa to całkiem dobrze - szczególnie, że nowe informacje są ujawniane cały czas. Naprawdę dobrzy gracze dobrze wiedzą, co każdy z nich ma. Więc rozsądnie silne programy Skat i Bridge zostały oparte na tych pomysłach.

Jeśli możesz całkowicie rozwiązać ukryty świat, to jest najlepsze, ale jeśli nie możesz, możesz użyć minimax lub UCT, aby wybrać najlepszy ruch w każdym świecie. Istnieją również algorytmy hybrydowe (ISMCTS), które próbują łączyć ten proces ze sobą. Uważaj tutaj na roszczenia. Proste metody próbkowania są łatwiejsze do kodowania - powinieneś wypróbować prostsze podejście przed bardziej złożonym.

Oto kilka prac naukowych, które dadzą trochę więcej informacji na temat kiedy podejście do pobierania próbek z niedoskonałością informacji pracował dobrze:

Understanding the Success of Perfect Information Monte Carlo Sampling in Game Tree Search (Niniejszy dokument analizuje gdy podejście do pobierania próbek może pracować).

Improving State Evaluation, Inference, and Search in Trick-Based Card Games (Dokument ten opisuje zastosowanie próbkowania w Skat)

Imperfect information in a computationally challenging game (Niniejszy dokument opisuje próbkowanie w mostek)

Information Set Monte Carlo Tree Search (Ten dokument łączy próbkowanie z wyszukiwaniem UCT/Monte Carlo, aby uniknąć problemów z pierwszym numerem referencyjnym).

Problem z podejściami opartymi na regułach w zaakceptowanej odpowiedzi polega na tym, że nie mogą wykorzystać zasobów obliczeniowych poza wymagane do utworzenia początkowych reguł. Ponadto podejścia oparte na regułach będą ograniczone mocą reguł, które możesz napisać. Metody oparte na wyszukiwaniu mogą wykorzystywać siłę wyszukiwania kombinatorycznego do tworzenia znacznie silniejszej gry niż autor programu.

Powiązane problemy