web-dev-qa-db-fra.com

Quel algorithme pour un jeu de tic-tac-toe puis-je utiliser pour déterminer le "meilleur coup" pour l'IA?

Dans une implémentation rapide, je suppose que la partie la plus difficile est de déterminer le meilleur coup à jouer pour la machine.

Quels sont les algorithmes que l'on peut poursuivre? Je cherche des implémentations du plus simple au plus complexe. Comment pourrais-je m'attaquer à cette partie du problème?

58
praveen chettypally

La stratégie de Wikipedia pour jouer un jeu parfait (gagner ou égaliser à chaque fois) semble être un pseudo-code simple:

Citation de Wikipedia (Stratégie de Tic Tac Toe #)

Un joueur peut jouer un jeu parfait de tic-tac-toe (gagner ou au moins dessiner) s’il choisit le premier coup disponible de la liste suivante, à chaque tour, comme utilisé dans le tic-tac-toe de 1972 programme [6]

  1. Victoire: Si vous en avez deux de suite, jouez le troisième pour en avoir trois de suite.

  2. Bloquer: Si l'adversaire en a deux de suite, jouez le troisième pour les bloquer.

  3. Fork: Créez une opportunité où vous pouvez gagner de deux manières.

  4. Bloquer la fourchette de l'adversaire:

    Option 1: créez-en deux à la suite pour forcer l'adversaire à défendre, aussi longtemps car cela ne leur permet pas de créer une fourchette ou gagnant. Par exemple, si "X" a un coin, "O" a le centre, et "X" a aussi le coin opposé, "O" ne doit pas jouer un corner pour gagner. (Jouer un coin dans ce scénario Crée un fork pour "X" à Win.)

    Option 2: S'il y a une configuration où l'adversaire peut bifurquer, bloquer cette fourchette.

  5. Centre: Joue au centre.

  6. Coin opposé: Si l'adversaire est dans le coin, jouez le contraire coin.

  7. Coin vide: Joue un coin vide.

  8. Côté vide: Joue un côté vide.

Reconnaître à quoi ressemble une situation de "fourchette" pourrait se faire de manière brutale, comme suggéré. 

Note: Un adversaire "parfait" est un exercice agréable mais ne vaut finalement pas la peine de "jouer" contre. Vous pouvez toutefois modifier les priorités ci-dessus pour donner des faiblesses caractéristiques aux personnalités adverses.

54
bkane

Ce dont vous avez besoin (pour le tic-tac-toe ou un jeu beaucoup plus difficile comme les échecs), c’est l’algorithme minimax , ou sa variante légèrement plus compliquée, la taille alpha-bêta . Un minimax naïf ordinaire fera l'affaire pour un jeu avec un espace de recherche aussi petit que le tic-tac-toe, cependant.

En un mot, ce que vous voulez faire n’est pas de rechercher le déménagement qui donne le meilleur résultat possible pour vous, mais plutôt le déménagement où le pire résultat possible est aussi bon que possible. Si vous supposez que votre adversaire joue de manière optimale, vous devez assumer qu'il prendra le coup le plus mauvais pour vous, et donc le coup qui minimise son gain MAXimum.

37
Nick Johnson

La méthode par force brute consistant à générer chaque carte possible et à la marquer en fonction des cartes qu’elle produira plus tard dans l’arbre ne nécessite pas beaucoup de mémoire, en particulier lorsque vous reconnaissez que les rotations de carte à 90 degrés sont redondantes, tout comme les retournements à la verticale axe horizontal et diagonal.

Une fois que vous en êtes arrivé à ce point, il y a environ un kilo de données dans un arbre pour décrire le résultat, et donc le meilleur choix pour votre ordinateur.

-Adam

14
Adam Davis

Un algo typique pour le tic-tac-toe devrait ressembler à ceci:

Tableau: Un vecteur à neuf éléments représentant le tableau. Nous stockons 2 (indiquant Blanc), 3 (indiquant X) ou 5 (indiquant O) . Tour: Un entier indiquant le coup du jeu sur le point d'être joué. Le 1er coup sera indiqué par 1, le dernier par 9.

L'algorithme

L'algorithme principal utilise trois fonctions.

Make2: retourne 5 si le carré du centre est vide, c'est-à-dire si board [5] = 2. Sinon, cette fonction renvoie tout carré non-corner (2,4,6 ou 8).

Posswin (p): renvoie 0 si le joueur p ne peut pas gagner son prochain coup; sinon, il retourne le nombre de carrés constituant un coup gagnant. Cette fonction permettra au programme de gagner et d’empêcher les adversaires de gagner. Cette fonction fonctionne en vérifiant chacune des lignes, des colonnes et des diagonales. En multipliant les valeurs de son carré ensemble pour une ligne entière (ou une colonne ou une diagonale), la situation possible de victoire peut être vérifiée. Si le produit a 18 ans (3 x 3 x 2), alors X peut gagner. Si le produit est de 50 (5 x 5 x 2), alors O peut gagner. Si une ligne gagnante (colonne ou diagonale) est trouvée, son carré vide peut être déterminé et le numéro de ce carré est renvoyé par cette fonction.

Go (n): fait un mouvement dans la case n. Cette procédure règle la carte [n] sur 3 si Turn est impair ou sur 5 si Turn est pair. Il augmente également tour par tour.

L'algorithme a une stratégie intégrée pour chaque mouvement. Il fait le coup impair Si il joue X, le coup pair s'il joue O.

Turn =1 Go(1)   (upper left corner).
Turn =2 If Board[5] is blank, Go(5), else Go(1).
Turn =3 If Board[9] is blank, Go(9), else Go(3).
Turn =4 If Posswin(X) is not 0, then Go(Posswin(X)) i.e. [ block opponent’s win], else Go(Make2).
Turn =5 if Posswin(X) is not 0 then Go(Posswin(X)) [i.e. win], else if Posswin(O) is not 0, then Go(Posswin(O)) [i.e. block win], else if Board[7] is blank, then Go(7), else Go(3). [to explore other possibility if there be any ].
Turn =6 If Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else Go(Make2).
Turn =7 If Posswin(X) is not 0 then Go(Posswin(X)), else if Posswin(X) is not 0, then Go(Posswin(O)) else go anywhere that is blank.
Turn =8 if Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else go anywhere that is blank.
Turn =9 Same as Turn=7.

Je l'ai utilisé. Dites-moi comment vous vous sentez.

7
Kaushik

Comme vous ne travaillez qu'avec une matrice 3x3 d'emplacements possibles, il serait assez facile d'écrire une recherche parmi toutes les possibilités sans vous imposer de puissance informatique. Pour chaque espace ouvert, calculez tous les résultats possibles après avoir marqué cet espace (de manière récursive, dirais-je), puis utilisez le mouvement avec le plus de possibilités de gagner.

Optimiser cela serait une perte de temps, vraiment. Bien que certaines faciles pourraient être:

  • Vérifiez d’abord pour les victoires possibles pour L’autre équipe, bloquez la première vous trouvez (s’il ya 2 parties .__ de toute façon). 
  • Prenez toujours le centre s'il est ouvert (Et la règle précédente n'a aucun candidat ).
  • Prenez les coins devant les côtés (encore une fois, Si les règles précédentes sont vides)
6
billjamesdev

Vous pouvez faire en sorte que l'IA se reproduise dans certains exemples de jeux. Utilisez un algorithme d'apprentissage supervisé pour l'aider.

3
J.J.

Une tentative sans utiliser un terrain de jeu.

  1. gagner (votre double) 
  2. sinon, ne pas perdre (double de l'adversaire) 
  3. si non, avez-vous déjà une fourchette (un double double) 
  4. sinon, si l'adversaire a une fourche
    1. rechercher dans les points de blocage d'éventuels doubles et fork (gain ultime) 
    2. sinon chercher des fourches dans les points de blocage (ce qui donne à l'adversaire les possibilités les plus perdantes) 
    3. sinon seulement des points bloquants (à ne pas perdre) 
  5. sinon recherche double et fork (victoire ultime) 
  6. sinon, rechercher uniquement les fourches qui offrent à l'adversaire le plus de possibilités 
  7. sinon cherchez seulement un double 
  8. sinon impasse, cravate, au hasard. 
  9. si non (cela signifie votre premier mouvement)
    1. si c'est le premier coup du jeu;
      1. donne à l'adversaire la possibilité la plus perdante (l'algorithme génère uniquement des corners ce qui donne une possibilité de perdre 7 points à l'adversaire) 
      2. ou pour briser l'ennui juste au hasard. 
    2. si c'est le deuxième coup du jeu;
      1. ne trouver que les points qui ne perdent pas (donne un peu plus d'options) 
      2. ou trouvez les points de cette liste qui ont la meilleure chance de gagner (cela peut être ennuyeux, car il en résulte que tous les coins ou les coins adjacents ou le centre)

Remarque: lorsque vous avez double et fourches, vérifiez si votre double donne à l'adversaire un double.Si cela vous le donne, vérifiez si votre nouveau point obligatoire est inclus dans votre liste des fourchettes.

3
Mesut Ergul

Classez chacun des carrés avec des scores numériques. Si un carré est pris, passez au choix suivant (trié par ordre décroissant par rang). Vous allez devoir choisir une stratégie (il y en a deux principales pour aller en premier et trois (je pense) pour le second). Techniquement, vous pouvez simplement programmer toutes les stratégies, puis en choisir une au hasard. Cela ferait un adversaire moins prévisible.

0
Daniel Spiewak

Cette réponse suppose que vous comprenez comment implémenter l’algorithme parfait pour P1 et explique comment atteindre une situation gagnante face à des joueurs humains ordinaires, qui commettront des erreurs plus souvent que d’autres.

Le jeu doit bien sûr se terminer par un match nul si les deux joueurs jouent de manière optimale. Au niveau humain, jouer dans un coin produit beaucoup plus souvent. Pour quelque raison psychologique que ce soit, P2 est persuadé que jouer au centre n’est pas si important, ce qui est regrettable pour eux, car c’est la seule réponse qui ne crée pas un jeu gagnant pour P1.

Si P2 bloque correctement au centre, P1 doit jouer le coin opposé, car encore une fois, pour quelque raison psychologique que ce soit, P2 préférera la symétrie consistant à jouer un corner, ce qui produit encore une perte de tableau.

Quel que soit le coup que P1 puisse faire pour le coup de départ, il existe un coup que P2 peut faire qui créera une victoire pour P1 si les deux joueurs jouent de manière optimale par la suite. En ce sens, P1 peut jouer n'importe où. Les mouvements Edge sont les plus faibles en ce sens que la plus grande partie des réponses possibles à ce mouvement produisent un tirage au sort, mais il y a toujours des réponses qui créeront une victoire pour P1.

De façon empirique (plus précisément, de façon anecdotique), les meilleurs coups de départ du P1 semblent être le premier virage, le deuxième centre et le dernier bord.

Le prochain défi que vous pouvez ajouter, en personne ou via une interface graphique, consiste à ne pas afficher le tableau. Un humain peut certainement se souvenir de tout l'état, mais le défi ajouté conduit à une préférence pour les planches symétriques, qui nécessitent moins d'effort pour s'en souvenir, ce qui conduit à l'erreur que j'ai décrite dans la première branche.

Je suis très amusant aux soirées, je sais.

0
djechlin

Voici une solution qui prend en compte tous les mouvements possibles et les conséquences de chaque mouvement pour déterminer le meilleur mouvement possible. 

nous aurons besoin d'une structure de données qui représente le conseil. Nous allons représenter le conseil avec un tableau à deux dimensions. Le tableau extérieur représente le tableau entier et un tableau intérieur représente une ligne. Voici l'état d'un plateau vide. 

_gameBoard: [
    [“”, “”, “”],
    [“”, “”, “”],
    [“”, “”, “”]
]

Nous allons peupler le tableau avec des caractères 'x' et 'o'. 

Ensuite, nous aurons besoin d’une fonction permettant de vérifier le résultat. La fonction recherchera une succession de caractères. Quel que soit l'état du tableau, le résultat est l'une des 4 options suivantes: soit Incomplet, joueur X gagné, joueur O gagné ou match nul. La fonction devrait retourner qui est l'état du tableau. 

const SYMBOLS = {
  x:'X',
  o:'O'
}
const RESULT = {
  incomplete: 0,
  playerXWon: SYMBOLS.x,
  playerOWon: SYMBOLS.o,
  tie: 3
}
  function getResult(board){
      // returns an object with the result

      let result = RESULT.incomplete
      if (moveCount(board)<5){
        {result}
      }

      function succession (line){
        return (line === symbol.repeat(3))
      }

      let line

      //first we check row, then column, then diagonal
      for (var i = 0 ; i<3 ; i++){
        line = board[i].join('')
        if(succession(line)){
          result = symbol;
          return {result};
        }
      }

      for (var j=0 ; j<3; j++){
        let column = [board[0][j],board[1][j],board[2][j]]
        line = column.join('')
        if(succession(line)){
          result = symbol
          return {result};
        }
      }

      let diag1 = [board[0][0],board[1][1],board[2][2]]
      line = diag1.join('')
      if(succession(line)){
        result = symbol
        return {result};
      }

      let diag2 = [board[0][2],board[1][1],board[2][0]]
      line = diag2.join('')
      if(succession(line)){
        result = symbol
        return {result};
      }

      //Check for tie
      if (moveCount(board)==9){
        result=RESULT.tie
        return {result}
      }

      return {result}
    }

Nous pouvons maintenant ajouter la fonction getBestMove, nous fournissons n’importe quel tableau, et le symbole suivant, la fonction exécutera la vérification de tous les déplacements possibles avec la fonction getResult. Si c'est une victoire, il lui donnera un score de 1. Si c'est un lâche, il obtiendra un score de -1, une égalité obtiendra un score de 0. S'il est indéterminé, la fonction getBestMove sera récursive score du prochain coup. Comme le prochain coup est de l'oponent, sa victoire est la perte du joueur actuel et le score sera annulé. À la fin du mouvement possible reçoit un score de 1,0 ou -1, nous pouvons trier les mouvements et renvoyer le mouvement avec le score le plus élevé.

  function getBestMove (board, symbol){

    function copyBoard(board) {
      let copy = []
       for (let row = 0 ; row<3 ; row++){
        copy.Push([])
        for (let column = 0 ; column<3 ; column++){
          copy[row][column] = board[row][column]
        }
      }
      return copy
    }

    function getAvailableMoves (board) {
      let availableMoves = []
      for (let row = 0 ; row<3 ; row++){
        for (let column = 0 ; column<3 ; column++){
          if (board[row][column]===""){
            availableMoves.Push({row, column})
          }
        }
      }
      return availableMoves
    }

    let availableMoves = getAvailableMoves(board)

    let availableMovesAndScores = []

    for (var i=0 ; i<availableMoves.length ; i++){
      let move = availableMoves[i]
      let newBoard = copyBoard(board)
      newBoard = applyMove(newBoard,move, symbol)
      result = getResult(newBoard,symbol).result
      let score
      if (result == RESULT.tie) {score = 0}
      else if (result == symbol) {
        score = 1
      }
      else {
        let otherSymbol = (symbol==SYMBOLS.x)? SYMBOLS.o : SYMBOLS.x
        nextMove = getBestMove(newBoard, otherSymbol)
        score = - (nextMove.score)
      }
      if(score === 1)
        return {move, score}
      availableMovesAndScores.Push({move, score})
    }

    availableMovesAndScores.sort((moveA, moveB )=>{
        return moveB.score - moveA.score
      })
    return availableMovesAndScores[0]
  }

Algorithme en action , Github , Expliquer le processus plus en détail

0
Ben Carp