it-swarm.com.de

Welchen Algorithmus für ein Tic-Tac-Toe-Spiel kann ich verwenden, um den "besten Zug" für die KI zu bestimmen?

Bei einer Tic-Tac-Toe-Implementierung denke ich, dass die Herausforderung darin besteht, den besten Zug zu bestimmen, den die Maschine spielen soll.

Welche Algorithmen können verfolgt werden? Ich untersuche Implementierungen von einfach bis komplex. Wie würde ich diesen Teil des Problems angehen?

58

Die Strategie von Wikipedia, ein perfektes Spiel zu spielen (jedes Mal gewinnen oder unentschieden), scheint wie ein einfacher Pseudo-Code zu sein:

Zitat aus Wikipedia (Tic Tac Toe # Strategie)

Ein Spieler kann ein perfektes Tic-Tac-Toe-Spiel spielen (um zu gewinnen oder zumindest zu unentschieden), wenn er in jeder Runde den ersten verfügbaren Zug aus der folgenden Liste wählt, wie er in Newell und Simons 1972-Tic-Tac-Toe verwendet wird Programm. [6]

  1. Win: Wenn Sie zwei in einer Reihe haben, spielen Sie die dritte, um drei in Folge zu erhalten.

  2. Blockieren: Wenn der Gegner zwei in einer Reihe hat, spiele die dritte, um sie zu blockieren.

  3. Gabel: Erstellen Sie eine Gelegenheit, bei der Sie auf zwei Arten gewinnen können.

  4. Gegners Gabel blockieren:

    Option 1: Erstellen Sie zwei hintereinander, um der Gegner in die Verteidigung, solange da dies nicht dazu führt, dass sie .__ erstellen. eine Gabel oder ein Gewinn. Zum Beispiel, wenn "X" hat eine Ecke, "O" hat die Mitte und "X" hat auch die gegenüberliegende Ecke, "O" darf keine Ecke spielen, um Sieg. (Durch Spielen einer Ecke in diesem Szenario Wird eine Verzweigung für "X" in Win. Erstellt.)

    Option 2: Wenn eine Konfiguration vorhanden ist wo der Gegner sich verzweigen kann, blockieren Sie diese Gabel.

  5. Center: Spielen Sie die Mitte.

  6. Gegenseite: Wenn sich der Gegner in der Ecke befindet, spielen Sie das Gegenteil Ecke.

  7. Leere Ecke: Spiele eine leere Ecke.

  8. Leere Seite: Spielen Sie eine leere Seite.

Das Erkennen, wie eine "Gabel" -Situation aussieht, könnte auf rohe Art und Weise erfolgen, wie vorgeschlagen. 

Hinweis: Ein "perfekter" Gegner ist eine schöne Übung, die sich letztendlich jedoch nicht lohnt. Sie können jedoch die oben genannten Prioritäten ändern, um den gegnerischen Persönlichkeiten charakteristische Schwächen zu verleihen.

54
bkane

Was Sie (für Tic-Tac-Toe oder ein weitaus schwierigeres Spiel wie Chess) benötigen, ist der Minimax-Algorithmus oder seine etwas kompliziertere Variante - Alpha-Beta-Beschneidung . Eine normale, naive Minimax eignet sich jedoch gut für ein Spiel mit einem so kleinen Suchraum wie Tic-Tac-Toe.

Kurz gesagt, Sie möchten nicht nach dem Zug suchen, der das bestmögliche Ergebnis für Sie hat, sondern eher nach dem Zug, bei dem das schlechteste Ergebnis so gut wie möglich ist. Wenn Sie davon ausgehen, dass Ihr Gegner optimal spielt, müssen Sie davon ausgehen, dass er die für Sie schlechteste Bewegung ausführt. Daher müssen Sie die Bewegung ausführen, die den maximalen Gewinn minimiert.

37
Nick Johnson

Die Brute-Force-Methode, jedes einzelne Board zu generieren und basierend auf den Boards, die später im Baum erzeugt werden, zu bewerten, erfordert nicht viel Speicherplatz. Dies gilt insbesondere, wenn Sie erkennen, dass 90-Grad-Board-Rotationen überflüssig sind. horizontale und diagonale Achse.

Sobald Sie an diesem Punkt angelangt sind, gibt es in einem Baumdiagramm so etwas wie weniger als 1 KB Daten, um das Ergebnis und damit die beste Bewegung für den Computer zu beschreiben.

-Adam

14
Adam Davis

Ein typischer Algorithmus für Tic-Tac-Toe sollte folgendermaßen aussehen:

Board: Ein Vektor mit neun Elementen, der das Board darstellt. Wir speichern 2 (Anzeigen Blank), 3 (Anzeigen X) oder 5 (Anzeigen O) . Turn: Eine Ganzzahl, die angibt, welche Bewegung des Spiels gerade gespielt wird. Der erste Zug wird mit 1 und der letzte mit 9 angezeigt.

Der Algorithmus

Der Hauptalgorithmus verwendet drei Funktionen.

Make2: gibt 5 zurück, wenn das mittlere Quadrat der Platine leer ist, d. H. Wenn Platine [5] = 2 ist. Andernfalls gibt diese Funktion ein beliebiges nicht eckiges Quadrat aus (2,4,6 oder 8).

Posswin (p): Gibt 0 zurück, wenn Spieler p beim nächsten Zug nicht gewinnen kann. Andernfalls wird die Anzahl der Felder zurückgegeben, die einen Gewinnzug darstellen. Mit dieser Funktion kann das Programm sowohl gewinnen als auch blockieren. Diese Funktion überprüft die Zeilen, Spalten und Diagonalen. Durch Multiplizieren der Werte seines Quadrats für eine ganze Reihe (oder Spalte oder Diagonale) kann die mögliche Situation des Gewinns überprüft werden. Wenn das Produkt 18 (3 x 3 x 2) ist, kann X gewinnen. Wenn das Produkt 50 ist (5 x 5 x 2), kann O gewinnen. Wenn eine Gewinnzeile (Spalte oder Diagonale) gefunden wird, kann das leere Quadrat bestimmt werden und die Nummer dieses Quadrats wird von dieser Funktion zurückgegeben.

Go (n): bewegt sich in Quadrat n. Bei diesem Verfahren wird die Karte [n] auf 3 gesetzt, wenn die Kurve ungerade ist, oder 5, wenn die Kurve gerade ist. Es erhöht auch den Zug um eins.

Der Algorithmus hat für jede Bewegung eine integrierte Strategie. Es bewirkt, dass die ungeradzahlige Bewegung erfolgt, wenn es X spielt, die geradzahlige Bewegung, wenn es O spielt.

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.

Ich habe es benutzt Lass mich wissen, wie es euch geht.

7
Kaushik

Da es sich nur um eine 3x3-Matrix möglicher Standorte handelt, ist es ziemlich einfach, einfach eine Suche durch alle Möglichkeiten zu schreiben, ohne die Rechenleistung zu belasten. Berechnen Sie für jeden offenen Raum alle möglichen Ergebnisse, nachdem Sie diesen Bereich markiert haben (rekursiv, würde ich sagen), und verwenden Sie dann den Zug mit den meisten Gewinnmöglichkeiten.

Dies zu optimieren, wäre wirklich eine Verschwendung von Aufwand. Obwohl es einige einfache sein könnte:

  • Überprüfen Sie zuerst auf mögliche Gewinne für Das andere Team, blockieren Sie das erste Sie finden (wenn 2 die Spiele Trotzdem vorhanden sind). 
  • Nimm immer die Mitte, wenn es geöffnet ist .__ (und die vorherige Regel hat keine Kandidaten).
  • Nehmen Sie Ecken vor den Seiten (wieder, Wenn die vorherigen Regeln leer sind)
6
billjamesdev

Sie können die KI selbst in einigen Beispielspielen spielen lassen, um daraus zu lernen. Verwenden Sie einen beaufsichtigten Lernalgorithmus, um ihn zu unterstützen.

3
J.J.

Ein Versuch ohne Spielfeld.

  1. zu gewinnen (dein Doppel) 
  2. wenn nicht, nicht verlieren (gegnerisches Doppel) 
  3. wenn nicht, haben Sie bereits eine Gabel (doppelt) 
  4. wenn nicht, wenn der Gegner eine Gabel hat
    1. suche in Blockpunkten nach möglichen Doppel- und Gabelungen (ultimativer Gewinn) 
    2. wenn nicht nach Gabeln in Sperrpunkten suchen (was dem Gegner die meisten Verlustmöglichkeiten gibt) 
    3. wenn nicht nur Sperrpunkte (nicht zu verlieren) 
  5. wenn nicht nach Doppel und Gabel suchen (ultimativer Gewinn) 
  6. wenn nicht, nur nach Gabeln suchen, die dem Gegner die meisten Verlustmöglichkeiten bieten 
  7. wenn nicht, suche nur nach einem Doppel 
  8. wenn nicht Sackgasse, Krawatte, zufällig. 
  9. wenn nicht (es bedeutet dein erster Zug)
    1. wenn es der erste Zug des Spiels ist;
      1. geben Sie dem Gegner die meisten Verlustmöglichkeiten (der Algorithmus führt nur zu Ecken, was dem Gegner 7 Punkte-Möglichkeit gibt) 
      2. oder zum brechen der langeweile einfach zufällig. 
    2. wenn es der zweite Zug des Spiels ist;
      1. finde nur die nicht verlierenden Punkte (gibt ein wenig mehr Optionen) 
      2. oder finden Sie die Punkte in dieser Liste, die die beste Gewinnchance haben (es kann langweilig sein, da nur alle Ecken oder angrenzende Ecken oder das Zentrum dazu führen)

Hinweis: Wenn Sie über Doppel und Gabeln verfügen, prüfen Sie, ob Ihr Gegner dem Gegner ein Doppel gibt. Wenn dies der Fall ist, prüfen Sie, ob Ihr neuer obligatorischer Punkt in Ihrer Gabelliste enthalten ist.

3
Mesut Ergul

Rangiere jedes der Quadrate mit numerischen Werten. Wenn ein Quadrat genommen wird, fahren Sie mit der nächsten Auswahl fort (sortiert in absteigender Reihenfolge nach Rang). Sie müssen eine Strategie wählen (es gibt zwei Hauptstrategien, um zuerst zu gehen und drei (denke ich) für die zweite). Technisch könnten Sie einfach alle Strategien programmieren und dann zufällig eine auswählen. Das würde zu einem weniger vorhersehbaren Gegner führen.

0
Daniel Spiewak

Diese Antwort setzt voraus, dass Sie die Implementierung des perfekten Algorithmus für P1 verstehen und erörtern, wie Sie unter Bedingungen gegen normale menschliche Spieler gewinnen können, die häufiger Fehler als andere machen.

Das Spiel sollte natürlich unentschieden enden, wenn beide Spieler optimal spielen. Auf menschlicher Ebene bringt P1 in einer Ecke viel häufiger Gewinne. Aus irgendeinem psychologischen Grund wird P2 zu der Annahme verleitet, dass das Spielen im Zentrum nicht so wichtig ist, was für sie unglücklich ist, da es die einzige Antwort ist, die kein Gewinnspiel für P1 schafft.

Wenn P2 richtig in der Mitte korrekt blockiert, sollte P1 die gegenüberliegende Ecke spielen, da P2 wiederum aus irgendeinem psychologischen Grund die Symmetrie des Eckenspiels bevorzugen wird, was wiederum ein Verlustbrett für sie erzeugt.

Für jede Bewegung, die P1 für die Startbewegung ausführen kann, gibt es eine Bewegung, die P2 ausführen kann, die einen Gewinn für P1 generiert, wenn beide Spieler danach optimal spielen. In diesem Sinne kann P1 überall spielen. Die Randbewegungen sind am schwächsten in dem Sinne, dass der größte Teil der möglichen Reaktionen auf diese Bewegung ein Unentschieden erzeugt, aber es gibt immer noch Antworten, die einen Gewinn für P1 erzeugen.

Empirisch (genauer gesagt, anekdotisch) scheinen die besten P1-Startbewegungen die erste Ecke, die zweite Mitte und die letzte Kante zu sein.

Die nächste Herausforderung, die Sie persönlich oder über eine grafische Benutzeroberfläche hinzufügen können, besteht darin, die Tafel nicht anzuzeigen. Ein Mensch kann sich definitiv an alle Zustände erinnern, aber die zusätzliche Herausforderung führt zu einer Bevorzugung von symmetrischen Platinen, an die man sich weniger erinnern muss, was zu dem Fehler führt, den ich im ersten Zweig skizziert habe.

Ich bin auf Partys sehr lustig, ich weiß.

0
djechlin

Hier ist eine Lösung, die alle möglichen Züge und die Konsequenzen jedes Zugs berücksichtigt, um den bestmöglichen Zug zu bestimmen. 

wir werden eine Datenstruktur benötigen, die das Board repräsentiert. Wir werden die Tafel mit einem zweidimensionalen Array darstellen. Das äußere Array repräsentiert die gesamte Platine und ein inneres Array repräsentiert eine Zeile. Hier ist der Zustand eines leeren Boards. 

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

Wir werden das Board mit 'x' und 'o' Zeichen füllen. 

Als nächstes benötigen wir eine Funktion, die das Ergebnis überprüfen kann. Die Funktion sucht nach einer Folge von Zeichen. Was auch immer der Zustand des Brettes ist, das Ergebnis ist eine von 4 Optionen: entweder Unvollständig, Spieler X gewonnen, Spieler O gewonnen oder ein Unentschieden. Die Funktion sollte den Status der Karte zurückgeben. 

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

Jetzt können wir die getBestMove-Funktion hinzufügen, eine beliebige Karte bereitstellen, und das nächste Symbol: Die Funktion überprüft alle möglichen Bewegungen mit der getResult-Funktion. Wenn es sich um einen Gewinn handelt, erhält es eine Punktzahl von 1. Wenn es ein Los ist, erhält es eine Punktzahl von -1, ein Unentschieden wird mit einer Punktzahl von 0 bewertet Punktzahl des nächsten Zuges. Da der nächste Zug der Gegner ist, ist sein Sieg der Verlust des aktuellen Spielers, und die Punktzahl wird negiert. Am Ende eines möglichen Zuges erhält der Spieler entweder eine Punktzahl von 1,0 oder -1, wir können die Züge sortieren und den Zug mit der höchsten Punktzahl zurückgeben.

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

Algorithmus in Aktion , Github , Weitere Informationen zum Prozess

0
Ben Carp