2012-09-30 20 views
6

Quiero utilizar la búsqueda de minimax (con poda alfa-beta), o más bien buscar negamax, para hacer que un programa de computadora juegue un juego de cartas.Uso de búsqueda de minimax para juegos de cartas con información imperfecta

El juego de cartas en realidad consiste en 4 jugadores. Entonces, para poder usar minimax, etc., simplifico el juego a "mí" contra los "otros". Después de cada "movimiento", puede leer objetivamente la evaluación del estado actual del juego en sí. Cuando los 4 jugadores hayan colocado la carta, la más alta los ganará a todos, y los valores de las cartas cuentan.

Como no sabes cómo es exactamente la distribución de cartas entre los otros 3 jugadores, pensé que debes simular todas las distribuciones posibles ("mundos") con las cartas que no son tuyas. Tienes 12 cartas, los otros 3 jugadores tienen 36 cartas en total.

Así que mi enfoque es este algoritmo, donde player es un número entre 1 y 3 que simboliza los tres jugadores de la computadora para los que el programa podría necesitar movimientos. Y -player representa a los oponentes, es decir, a los otros tres jugadores juntos.

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

Esto parece funcionar bien, pero para una profundidad de 1 (depthLeft=1), el programa ya tiene que calcular movimientos (50.000) tarjetas colocadas en promedio. Esto es demasiado, por supuesto!

Así que mis preguntas son:

  1. es la implementación correcta en absoluto? ¿Puedes simular un juego como este? En cuanto a la información imperfecta, especialmente?
  2. ¿Cómo se puede mejorar el algoritmo de velocidad y carga de trabajo?
  3. ¿Puedo, por ejemplo, reducir el conjunto de posibles movimientos a un conjunto aleatorio del 50% para mejorar la velocidad, manteniendo buenos resultados?
  4. Encontré UCT algorithm para ser una buena solución (tal vez). ¿Conoces este algoritmo? ¿Me puedes ayudar a implementarlo?

Respuesta

3

La búsqueda de Minimax, como ha implementado, es un enfoque incorrecto para los juegos en los que hay tanta incertidumbre. Como no conoces la distribución de cartas entre los demás jugadores, tu búsqueda pasará una cantidad exponencial de tiempo explorando juegos que no podrían darse dada la distribución real de las cartas.

Creo que un mejor enfoque sería comenzar con buenas reglas para el juego cuando tienes poca o ninguna información sobre las manos de los otros jugadores. Cosas como:

  1. Si juegas primero en una ronda, juega tu carta más baja ya que tienes pocas posibilidades de ganar la ronda.
  2. Si juegas último en una ronda, juega tu carta más baja que gane la ronda. Si no puedes ganar la ronda, juega tu carta más baja.

Haga que su programa inicialmente no se moleste con la búsqueda y simplemente siga estas reglas y suponga que todos los demás jugadores usarán estas heurísticas también. A medida que el programa observa qué cartas juegan el primer y el último jugador de cada ronda, puede crear una tabla de información sobre las cartas que cada jugador probablemente tenga. P.ej. un 9 habría ganado esta ronda, pero el jugador 3 no jugó, por lo que no debe tener ninguna carta de 9 o más.A medida que se recopile información sobre la mano de cada jugador, el espacio de búsqueda eventualmente estará limitado al punto donde una búsqueda minimax de posibles juegos podría producir información útil sobre la siguiente carta para jugar.

+0

Hmm, con respecto al minimaxing cerca del final del juego. En ese punto, sabes que necesitas x trucos para ganar. Cualquier mundo en el que no puedas (no debería) ganar puedes ignorarlo. Porque si ese mundo está bien, entonces has perdido de todos modos. Si basa sus probabilidades en los mundos que conducen a ganar (esencialmente utilizando ilusiones), entonces probablemente pueda reducir aún más la búsqueda. – Cruncher

4

Quiero aclarar detalles de que la respuesta aceptada realmente no entra.

En muchos juegos de cartas puedes probar las cartas desconocidas que tu oponente podría tener en lugar de generarlas todas. Puede tomar en cuenta información como los trajes cortos y la probabilidad de mantener determinadas cartas que se han jugado hasta ahora al hacer este muestreo para ponderar la probabilidad de cada mano posible (cada mano es un mundo posible que resolveremos independientemente). Luego, resuelve cada mano usando la búsqueda de información perfecta. El mejor movimiento en todos estos mundos suele ser el mejor movimiento en general, con algunas advertencias.

En juegos como el póker, esto no funcionará muy bien: el juego es todo sobre la información oculta. Debes equilibrar tus acciones de forma precisa para mantener oculta la información sobre tu mano.

Pero, en juegos como los juegos de cartas basados ​​en trucos, esto funciona bastante bien, especialmente dado que la información nueva se revela todo el tiempo. Los jugadores realmente buenos tienen una buena idea de lo que todos tienen. Por lo tanto, los programas razonablemente fuertes de Skat y Bridge se han basado en estas ideas.

Si puede resolver completamente el mundo subyacente, eso es lo mejor, pero si no puede, puede usar minimax o UCT para elegir el mejor movimiento en cada mundo. También hay algoritmos híbridos (ISMCTS) que intentan combinar este proceso. Tenga cuidado con los reclamos aquí. Los métodos sencillos de muestreo son más fáciles de codificar: debe intentar el enfoque más simple antes que uno más complejo.

Éstos son algunos trabajos de investigación que darán algo más de información sobre cuando el método de muestreo a la información imperfecta ha funcionado bien:

Understanding the Success of Perfect Information Monte Carlo Sampling in Game Tree Search (este trabajo se analiza cuando el método de muestreo es probable que funcione.)

Improving State Evaluation, Inference, and Search in Trick-Based Card Games (Este artículo describe el uso de muestras con Skat)

Imperfect information in a computationally challenging game (Este documento describe el muestreo en puente)

Information Set Monte Carlo Tree Search (Este documento combina el muestreo y UCT/Monte Carlo Tree Search para evitar los problemas en la primera referencia.)

El problema con los enfoques basados ​​en reglas en la respuesta aceptada es que no pueden aprovechar los recursos computacionales más allá eso se requiere para crear las reglas iniciales. Además, los enfoques basados ​​en reglas estarán limitados por el poder de las reglas que usted puede escribir. Los enfoques basados ​​en búsquedas pueden usar el poder de la búsqueda combinatoria para producir juegos mucho más fuertes que el autor del programa.

Cuestiones relacionadas