Kapitel 6 · Tic Tac Toe

Tic Tac Toe in Python – mit unschlagbarem Computer

Du bist der grüne Kreis, der Computer das rote Kreuz. Gewinnen kannst du nicht – bestenfalls ein Unentschieden. Der Grund ist ein Algorithmus, der jeden möglichen Spielverlauf bis zum Ende durchrechnet: Minimax, hier in der Variante NegaMax.

Aus dem Buch · Kapitel 6 Fortgeschritten KI & AlgorithmenSpiele Python 3 · läuft im Browser
Bereit · Python startet beim ersten Klick
# Tic Tac Toe mit NegaMax – Kapitel 6 »Tic Tac Toe«
# aus »Coding for Fun mit Python«, portiert auf Python 3 und c4f.
# ticTacToe.py, negamax.py und playingfield.py sind hier vereint.
from c4f import Screen, sleep, MOUSEDOWN, KEYDOWN


class TicTacToe(object):
    def __init__(self):
        self.field = [[0, 0, 0],
                      [0, 0, 0],
                      [0, 0, 0]]
        self.newGame()

    def newGame(self):
        self.player = 1
        # has to be reset this way otherwise
        # it would be a new list which is not
        # referenced by playing field :-)
        for y in range(3):
            for x in range(3):
                self.field[y][x] = 0

    def evaluate(self):
        # little improvement to chose fastest
        # win just count the open moves
        moveCount = 0
        for y in range(3):
            for x in range(3):
                if self.field[y][x] == 0:
                    moveCount += 1

        for i in range(3):
            if self.field[0][i] == self.field[1][i] == self.field[2][i]:
                if self.field[0][i] == 1:
                    return + 10 + moveCount
                if self.field[0][i] == 2:
                    return - 10 - moveCount

            if self.field[i][0] == self.field[i][1] == self.field[i][2]:
                if self.field[i][0] == 1:
                    return + 10 + moveCount
                if self.field[i][0] == 2:
                    return - 10 - moveCount

        if (self.field[0][0] == self.field[1][1] == self.field[2][2]) or \
           (self.field[0][2] == self.field[1][1] == self.field[2][0]):
            if self.field[1][1] == 1:
                return + 10 + moveCount
            if self.field[1][1] == 2:
                return - 10 - moveCount

        return 0

    def getSide(self):
        count = 0
        for y in range(3):
            for x in range(3):
                if self.field[y][x] != 0:
                    count += 1

        if count % 2 == 0:
            player = 1
        else:
            player = 2

        return player

    def getMoves(self):
        moves = []
        if self.evaluate() == 0:
            for y in range(3):
                for x in range(3):
                    if self.field[y][x] == 0:
                        move = (x, y)
                        moves.append(move)

        return moves

    def makeMove(self, move):
        x, y = move
        self.field[y][x] = self.getSide()

    def undoMove(self, move):
        x, y = move
        self.field[y][x] = 0

    def checkGameState(self):
        # unbekannter Zustand als Initialwert
        state = -1
        # aktuelle Positionsbewertung
        eval = self.evaluate()
        # sind noch Züge möglich?
        if not self.getMoves():
            # keine Züge mehr möglich
            state = 0
        if eval > 0:
            # Spieler Nr. 1 hat gewonnen
            state = 1
        if eval < 0:
            # Spieler Nr. 2 hat gewonnen
            state = 2

        return state


class NegaMax(object):
    # the evaluation function has to be smaller
    # than the defined infinity value
    infinity = 200

    def __init__(self, gameObject):
        self.gameObject = gameObject

    def getEvaluatedMoves(self, depth):
        possibleMoves  = self.gameObject.getMoves()
        evaluatedMoves = []
        for move in possibleMoves:
            self.gameObject.makeMove(move)
            self.treeNodeCounter = 0
            evaluation = self.explore(depth - 1)
            self.gameObject.undoMove(move)
            evaluatedMoves.append((evaluation, move, self.treeNodeCounter))

        evaluatedMoves.sort()
        return evaluatedMoves

    def getBestMove(self, depth):
        moves = self.getEvaluatedMoves(depth)
        print('\nEvaluated Moves:')
        for move in moves:
            print(move)
        print('----------------')
        bestMove = moves[0][1]
        print('best move = ', bestMove)
        return bestMove

    def explore(self, depth):
        alpha = -NegaMax.infinity
        moves = self.gameObject.getMoves()

        if depth <= 0 or not moves:
            if self.gameObject.getSide() == 1:
                return +self.gameObject.evaluate()
            else:
                return -self.gameObject.evaluate()

        self.treeNodeCounter += 1
        for move in moves:
            self.gameObject.makeMove(move)
            value = -self.explore(depth - 1)
            self.gameObject.undoMove(move)
            if value > alpha:
                alpha = value

        return alpha


class PlayingField(object):
    def __init__(self, size, gameObject, compObject):
        self.gameObject   = gameObject
        self.compObject   = compObject
        self.computerSide = 2
        sizeX, sizeY      = size
        self.field        = self.gameObject.field
        self.fieldsY      = len(self.field)
        self.fieldsX      = len(self.field[0])
        self.fieldWidth   = sizeX // self.fieldsX
        self.fieldHeight  = sizeY // self.fieldsY
        self.margin       = 50
        self.padding      = 30
        self.seperator    = 5
        self.size         = sizeX + self.margin * 2, sizeY + self.margin * 2
        self.screen       = Screen(self.size[0], self.size[1], background="black",
                                   title="Tic Tac Toe")
        self.screen.controls([("c", "Computer zieht"), ("n", "Neues Spiel")])
        self.running      = True

    def drawRect(self, position, color, size=(10, 10)):
        x, y = position
        width, height = size
        self.screen.rect(x, y, width, height, color)

    def fieldColor(self, x, y):
        if (y * 3 + x) % 2 == 0:
            return 0, 0, 140
        return 0, 0, 220

    def drawField(self):
        for y in range(self.fieldsY):
            for x in range(self.fieldsX):
                position = (self.margin + x * self.fieldWidth,
                            self.margin + y * self.fieldHeight)
                size     = (self.fieldWidth - self.seperator,
                            self.fieldHeight - self.seperator)
                self.drawRect(position, self.fieldColor(x, y), size)
                if self.field[y][x] != 0:
                    self.drawPlayerSign(position, self.field[y][x], self.fieldColor(x, y))

    def drawPlayerSign(self, startPosition, player, backColor):
        startX, startY = startPosition
        startPosition  = (startX + self.padding,
                          startY + self.padding)
        endPosition    = (startX + self.fieldWidth - self.padding,
                          startY + self.fieldHeight - self.padding)
        if player == 1:
            self.drawPlayerOne(startPosition, endPosition, backColor)
        else:
            self.drawPlayerTwo(startPosition, endPosition)

    def drawPlayerOne(self, startPosition, endPosition, backColor):
        x1, y1 = startPosition
        x2, y2 = endPosition
        x2, y2 = x2 - x1, y2 - y1
        color  = 0, 180, 0
        self.screen.ellipse(x1, y1, x2, y2, color)
        self.screen.ellipse(x1 + 15, y1 + 15, x2 - 30, y2 - 30, backColor)

    def drawPlayerTwo(self, startPosition, endPosition):
        color     = 180, 0, 0
        width     = 20
        adjustion = 10
        startX, startY = startPosition
        endX, endY     = endPosition
        startPosition  = startX + adjustion, startY + adjustion
        endPosition    = endX - adjustion, endY - adjustion
        self.screen.line(startPosition, endPosition, color, width)
        startX, startY = startPosition
        startX, startY = (startX - self.padding - adjustion,
                          startY - self.padding - adjustion)
        startPosition  = (startX + self.padding + adjustion,
                          startY + self.fieldHeight - self.padding - adjustion)
        endPosition    = (startX + self.fieldWidth - self.padding - adjustion,
                          startY + self.padding + adjustion)
        self.screen.line(startPosition, endPosition, color, width)

    async def makeHumanMove(self, position, button):
        x, y = position
        if x > self.margin and y > self.margin:
            x, y = (x - self.margin) // self.fieldWidth, (y - self.margin) // self.fieldHeight
            if len(self.field) > y and len(self.field[0]) > x:
                move = x, y
                if button == 3:
                    # switch sides and let the computer move
                    await self.letComputerMove()
                elif move in self.gameObject.getMoves():
                    # the human makes his move alone
                    # so we first check if its a possible move ;-)
                    self.gameObject.makeMove(move)
                    await self.checkForResult()

    async def letComputerMove(self):
        self.computerSide = self.gameObject.getSide()
        await self.makeCompMove()

    async def makeCompMove(self):
        self.refresh()
        self.screen.text("Computer denkt nach …", (self.size[0] / 2, self.margin / 2),
                         (255, 255, 255), size=20, align="center", baseline="middle")
        await self.screen.frame()  # erst den letzten Zug zeigen, dann rechnen
        bestMove = self.compObject.getBestMove(9)
        self.gameObject.makeMove(bestMove)
        await self.checkForResult()

    async def checkForResult(self):
        state = self.gameObject.checkGameState()
        if state != -1:
            # show current position for 1 seconds
            self.refresh()
            await sleep(1)
            # the computer should not start
            self.computerSide = 2
            self.gameObject.newGame()
            # show the winner of the game
            self.screen.fill((0, 0, 0))
            if state == 1:
                msg = 'Player 1 won!'
            elif state == 2:
                msg = 'Player 2 won!'
            elif state == 0:
                msg = 'The Game is drawn'

            centerX, centerY = self.size[0] / 2, self.size[1] / 2
            self.screen.rect(0, centerY - 45, self.size[0], 90, (159, 182, 205))
            self.screen.text(msg, (centerX, centerY), (255, 255, 255), size=56,
                             align="center", baseline="middle", bold=True)
            await sleep(2)

    def refresh(self):
        self.screen.fill((0, 0, 0))
        self.drawField()

    async def start(self):
        while self.running:
            # draw current playing field
            self.refresh()
            # does the computer has to play?
            if self.gameObject.getSide() == self.computerSide:
                await self.makeCompMove()
            else:
                # the human has to play!
                for event in self.screen.events():
                    if event.type == MOUSEDOWN:
                        await self.makeHumanMove(event.pos, event.button)
                    elif event.type == KEYDOWN and event.key == "c":
                        await self.letComputerMove()
                    elif event.type == KEYDOWN and event.key == "n":
                        self.computerSide = 2
                        self.gameObject.newGame()
            await self.screen.frame(30)

    def stop(self):
        self.running = False


size      = 500, 500
ticTacToe = TicTacToe()
negamax   = NegaMax(ticTacToe)
fieldGUI  = PlayingField(size, ticTacToe, negamax)
await fieldGUI.start()
Tic-Tac-Toe-Brett mit blauen Feldern, grünen Kreisen und roten Kreuzen

Klick oder tipp auf ein Feld. »Computer zieht« lässt den Computer den nächsten Zug machen – auch als Eröffnung.

Konsole

    

Was du hier lernst

  • Spielbaum und Rekursion
  • Minimax und NegaMax
  • Bewertungsfunktion
  • Züge ausprobieren und zurücknehmen
  • Alpha-Beta-Suche
  • Klassen für Spiel, Suche und Oberfläche

So denkt der Computer

Tic Tac Toe hat so wenige Möglichkeiten, dass ein Computer jede einzelne Partie im Voraus durchspielen kann. Genau das tut dieses Programm vor jedem seiner Züge: Es probiert alle Züge aus, dann alle Antworten darauf, dann alle Antworten auf die Antworten – bis das Spiel entschieden ist. Aus diesem riesigen Spielbaum wählt es den Zug, der am Ende am besten ausgeht.

1. Das Spielfeld und wer am Zug ist

Das Brett ist eine Liste von drei Zeilen mit je drei Feldern: 0 heißt leer, 1 ist Spieler 1 (der grüne Kreis, du) und 2 ist Spieler 2 (das rote Kreuz, der Computer). Wer am Zug ist, muss sich das Programm nicht merken – es zählt einfach die belegten Felder:

if count % 2 == 0:
    player = 1
else:
    player = 2

Bei einer geraden Anzahl Steine ist Spieler 1 dran, sonst Spieler 2. Diese Funktion getSide() taucht später bei der Suche an einer entscheidenden Stelle wieder auf.

2. Eine Zahl für jede Stellung

Die Suche braucht ein Urteil: Wie gut ist eine Stellung? evaluate() prüft alle Reihen, Spalten und Diagonalen. Hat Spieler 1 drei in einer Reihe, gibt es einen positiven Wert, bei Spieler 2 einen negativen, sonst 0:

if self.field[0][i] == 1:
    return + 10 + moveCount
if self.field[0][i] == 2:
    return - 10 - moveCount

Der Zusatz moveCount zählt die noch freien Felder. Ein Sieg nach fünf Zügen ist damit mehr wert als einer nach neun – der Computer gewinnt so schnell wie möglich und verliert, wenn überhaupt, so spät wie möglich.

Ein Nullsummenspiel

Was der eine gewinnt, verliert der andere. Deshalb genügt eine einzige Zahl: Positiv ist gut für Spieler 1 und im selben Maß schlecht für Spieler 2. Auf dieser Eigenschaft baut der ganze Algorithmus auf.

3. Ausprobieren und zurücknehmen

Um in die Zukunft zu schauen, setzt die Suche einen Stein, bewertet, was passiert – und nimmt ihn wieder weg:

self.gameObject.makeMove(move)
evaluation = self.explore(depth - 1)
self.gameObject.undoMove(move)

Es gibt nur ein einziges Brett, die Suche spielt darauf vor und zurück. Das spart Speicher und ist schneller, als bei jedem Schritt ein neues Brett anzulegen.

4. Minimax: das Beste für mich, das Schlechteste für dich

Die Idee von Minimax: Wenn ich am Zug bin, wähle ich den Zug mit der besten Bewertung für mich (Maximum). Mein Gegner wählt danach den Zug, der für mich am schlechtesten ist (Minimum). Beide Schritte wechseln sich ab, bis das Spiel vorbei ist. So entsteht für jeden Zug eine Bewertung, die davon ausgeht, dass der Gegner perfekt spielt.

5. NegaMax: eine Funktion statt zwei

Das Buch verwendet eine elegante Abwandlung. Statt einer Funktion für Maximum und einer für Minimum gibt es nur explore: Sie bewertet jede Stellung aus Sicht dessen, der am Zug ist. Was für mich +10 ist, ist für den Gegner −10 – also dreht ein Minuszeichen die Sicht bei jeder Ebene um:

for move in moves:
    self.gameObject.makeMove(move)
    value = -self.explore(depth - 1)
    self.gameObject.undoMove(move)
    if value > alpha:
        alpha = value

Am Ende des Baums (keine Züge mehr oder Tiefe erreicht) liefert evaluate() die Zahl aus Sicht von Spieler 1. Ist Spieler 2 am Zug, dreht explore das Vorzeichen – dafür braucht es getSide().

6. Der beste Zug

getEvaluatedMoves bewertet jeden möglichen Zug und sortiert die Liste. Warum nimmt getBestMove dann den kleinsten Wert? Nach dem eigenen Zug ist der Gegner dran, und die Zahl sagt, wie gut es für ihn aussieht. Der kleinste Wert ist also der Zug, der dem Gegner am wenigsten Chancen lässt.

In der Konsole erscheinen alle bewerteten Züge als Tupel: Bewertung, Feld (x, y) und die Zahl der untersuchten Stellungen. Eine 0 bedeutet: Bei perfektem Spiel beider Seiten endet die Partie unentschieden.

7. Die Oberfläche

PlayingField zeichnet das Brett und nimmt Klicks entgegen. Aus der Pixelposition wird mit ganzzahliger Division das Feld:

x, y = (x - self.margin) // self.fieldWidth, (y - self.margin) // self.fieldHeight

Ein Rechtsklick – oder die Taste »Computer zieht« – tauscht die Rollen: Dann macht der Computer den nächsten Zug, auch den ersten einer Partie.

Warum der Computer nicht verliert

Tic Tac Toe ist bei fehlerfreiem Spiel beider Seiten immer unentschieden. Der Computer rechnet alles bis zum Ende durch und macht deshalb nie einen Fehler. Du kannst also höchstens ein Remis erreichen. Unter »Probier mal« lässt sich der Computer kurzsichtig machen – dann hast du eine Chance.

Vom Tic Tac Toe zum Schach

Schachprogramme arbeiten nach derselben Grundidee: Sie durchsuchen den Spielbaum mit Minimax und Alpha-Beta-Suche – nur viel tiefer und mit einer weit feineren Bewertung, denn Schach lässt sich nicht bis zum Ende durchrechnen. Wie sich so ein Gegner anfühlt, kannst du bei BobbyChess ausprobieren: Schach gegen eine KI im Stil von Bobby Fischer.

Probier mal

Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.

  1. 01Alpha-Beta: derselbe Zug, viel schneller

    Drück zuerst im Original auf »Computer zieht«, solange das Brett leer ist: Der Computer braucht mehrere Sekunden, weil er rund eine halbe Million Stellungen durchrechnet. Mit Alpha-Beta-Suche bricht er Zweige ab, sobald klar ist, dass der Gegner sie nie zulassen würde. Das Ergebnis ist exakt dasselbe – nach einem Bruchteil der Arbeit. Die dritte Zahl in der Konsole zeigt die untersuchten Stellungen pro Zug.

    -     def explore(self, depth):
    -         alpha = -NegaMax.infinity
    -         moves = self.gameObject.getMoves()
    +     def explore(self, depth, alpha=-infinity, beta=+infinity):
    +         moves = self.gameObject.getMoves()
    -             value = -self.explore(depth - 1)
    -             self.gameObject.undoMove(move)
    -             if value > alpha:
    +             value = -self.explore(depth - 1, -beta, -alpha)
    +             self.gameObject.undoMove(move)
    +             if value >= beta:
    +                 return beta  # Beta-Schnitt: das lässt der Gegner nie zu
    +             if value > alpha:
  2. 02Abwechslung bei gleich guten Zügen

    Oft sind mehrere Züge gleich gut – der Computer nimmt dann immer den ersten, und jede Partie verläuft gleich. Wie in einer späteren Fassung aus dem Buch wählt er jetzt zufällig unter allen besten Zügen.

    - from c4f import Screen, sleep, MOUSEDOWN, KEYDOWN
    + import random
    + from c4f import Screen, sleep, MOUSEDOWN, KEYDOWN
    -         bestMove = moves[0][1]
    +         bestMoves = [m[1] for m in moves if m[0] == moves[0][0]]
    +         bestMove = random.choice(bestMoves)
  3. 03Ein kurzsichtiger Gegner

    getBestMove(9) lässt den Computer neun Halbzüge weit denken – also bis zum Spielende. Mit nur zwei Halbzügen sieht er noch, wo er gewinnen oder einen Sieg verhindern kann, aber keine Fallen. Jetzt ist er schlagbar: Baue eine Zwickmühle mit zwei offenen Reihen.

    - bestMove = self.compObject.getBestMove(9)
    + bestMove = self.compObject.getBestMove(2)
  4. 04Mitzählen, wie viel gerechnet wird

    Nach jedem Zug steht in der Konsole die Summe aller untersuchten Stellungen. Probiere es einmal mit dem Original und einmal zusammen mit der Alpha-Beta-Variante – der Unterschied ist gewaltig.

    -         print('best move = ', bestMove)
    +         print('best move = ', bestMove)
    +         print('Stellungen untersucht:', sum(m[2] for m in moves))

Vom Buch in den Browser

Im Buch besteht das Spiel aus drei Dateien: ticTacToe.py (Spielregeln), negamax.py (Suche) und playingfield.py (Oberfläche mit pygame). Hier stehen sie in einer Datei. TicTacToe und NegaMax sind unverändert – nur print ist jetzt eine Funktion. Angepasst wurde die Oberfläche:

Im Buch (pygame) Hier (c4f)
pygame.draw.rect(self.screen, farbe, rect) self.screen.rect(x, y, b, h, farbe)
Ring: innere Ellipse in der Farbe von self.screen.get_at(...) innere Ellipse in der Feldfarbe aus fieldColor(x, y)
pygame.font.Font(None, 80).render(...) + blit self.screen.text(msg, …, size=56)
time.sleep(1) await sleep(1)
pygame.event.poll() self.screen.events()
Rechtsklick lässt den Computer ziehen Rechtsklick oder Taste »Computer zieht«

Einige Details:

  • Ganzzahlige Division: In Python 2 lieferte sizeX / self.fieldsX eine ganze Zahl, in Python 3 eine Kommazahl. Als Listenindex wäre die unbrauchbar, deshalb steht dort jetzt //.
  • Pixel zurücklesen: pygame konnte die Farbe eines Pixels abfragen, um den Kreis innen wieder mit der Feldfarbe zu füllen. c4f zeichnet nur und liest nicht zurück, also berechnet fieldColor die Farbe direkt.
  • Warten: time.sleep würde Python anhalten, ohne dass der Browser das Brett zeigt. await sleep(...) wartet und zeigt dabei an.
  • Start: Das Original startete die Spielschleife schon im Konstruktor von PlayingField. Hier läuft sie über await fieldGUI.start().
  • psyco: Das Buch beschleunigte die Suche mit psyco, einem Just-in-time-Compiler für Python 2. Den gibt es für Python 3 nicht mehr. Die wirksamere Beschleunigung ist ohnehin algorithmisch – siehe die Alpha-Beta-Variante unter »Probier mal«.

Zu den Buch-Quellen gehören auch getunte Fassungen. Die Datei alphbetaprunning.py hätte sich so nicht importieren lassen: Der Standardwert -AlphaBetaPrunning.infinity verweist auf die Klasse, bevor sie existiert, und beim rekursiven Aufruf sind die Grenzen des Suchfensters vertauscht. Eingebunden war in der getunten Fassung NegaScout, eine verwandte Suche, die beides richtig macht. Die Alpha-Beta-Variante auf dieser Seite ist korrigiert.

Original aus dem Buch ansehen ticTacToe.py · Python 2
#coding: latin1

'''
Created on 14.04.2009

@author: Lars Heppert
'''

import psyco
psyco.log()
psyco.profile()

from playingfield import PlayingField
from negamax import NegaMax

class TicTacToe(object):
    def __init__(self):
        self.field = [[0, 0, 0],
                      [0, 0, 0],
                      [0, 0, 0]]
        self.newGame()

    def newGame(self):
        self.player = 1
#       has to be reset this way otherwise
#       it would be a new list which is not
#       referenced by playing field :-) 
        for y in range(3):
            for x in range(3):
                self.field[y][x] = 0
            
    def evaluate(self):
#       little improvement to chose fastest
#       win just count the open moves 
        moveCount = 0
        for y in range(3):
            for x in range(3):
                if self.field[y][x] == 0:
                    moveCount += 1
                    
        for i in range(3):
            if self.field[0][i] == self.field[1][i] == self.field[2][i]:
                if self.field[0][i] == 1:
                    return + 10 + moveCount
                if self.field[0][i] == 2:
                    return - 10 - moveCount
                
            if self.field[i][0] == self.field[i][1] == self.field[i][2]:
                if self.field[i][0] == 1:
                    return + 10 + moveCount
                if self.field[i][0] == 2:
                    return - 10 - moveCount
                
        if  (self.field[0][0] == self.field[1][1] == self.field[2][2]) or (self.field[0][2] == self.field[1][1] == self.field[2][0]):
            if self.field[1][1] == 1:
                return + 10 + moveCount
            if self.field[1][1] == 2:
                return - 10 - moveCount
            
        return 0
    
    def getSide(self):
        count = 0
        for y in range(3):
            for x in range(3):
                if self.field[y][x] != 0:
                    count += 1
                    
        if count % 2 == 0:
            player = 1
        else:
            player = 2
            
        return player
    
    def getMoves(self):
        moves = []
        if self.evaluate() == 0:
            for y in range(3):
                for x in range(3):
                    if self.field[y][x] == 0:
                        move = (x, y)
                        moves.append(move)
        
        return moves
    
    def makeMove(self, move):
        x, y = move
        self.field[y][x] = self.getSide()
        
    def undoMove(self, move):
        x, y = move
        self.field[y][x] = 0
        
    def checkGameState(self):
        # unbekannte Zustand als Initialwert
        state   = -1
        # aktuelle Postionsbewertung
        eval = self.evaluate()
        # sind noch Züge möglich?
        if not self.getMoves():
            # keine Züge mehr möglich
            state   = 0
        if eval > 0:
            # Spieler Nr. 1 hat gewonnen
            state   = 1
        if eval < 0:
            # Spieler Nr. 2 hat gewonnen
            state   = 2
            
        return state

if __name__ == '__main__':
    size        = 500, 500
    ticTacToe   = TicTacToe()
    negamax     = NegaMax(ticTacToe)
    fieldGUI    = PlayingField(size, ticTacToe, negamax)
Original aus dem Buch ansehen negamax.py · Python 2
'''
Created on 14.04.2009

@author: Lars Heppert
'''

class NegaMax(object):
#   the evaluation function has to be smaller
#   than the defined infinity value
    infinity = 200
    
    def __init__(self, gameObject):
        self.gameObject = gameObject
    
    def getEvaluatedMoves(self, depth):
        possibleMoves   = self.gameObject.getMoves()
        evaluatedMoves  = []
        for move in possibleMoves:
            self.gameObject.makeMove(move)
            self.treeNodeCounter = 0
            evaluation = self.explore(depth-1)
            self.gameObject.undoMove(move)
            evaluatedMoves.append((evaluation, move, self.treeNodeCounter))
        
        evaluatedMoves.sort()
        return evaluatedMoves
        
    def getBestMove(self, depth):
        moves = self.getEvaluatedMoves(depth)
        print '\nEvaluated Moves:'
        for move in moves:
            print move
        print '----------------'
        bestMove = moves[0][1]
        print 'best move = ', bestMove
        return bestMove        
    
    def explore(self, depth):
        alpha = -NegaMax.infinity        
        moves = self.gameObject.getMoves()

        if depth <= 0 or not moves:
            if self.gameObject.getSide() == 1:
                return +self.gameObject.evaluate()
            else:
                return -self.gameObject.evaluate()
        
        self.treeNodeCounter += 1
        for move in moves:
            self.gameObject.makeMove(move)
            value = -self.explore(depth-1)
            self.gameObject.undoMove(move)
            if value > alpha:
                alpha = value
                
        return alpha
Original aus dem Buch ansehen playingfield.py · Python 2
'''
Created on 18.04.2009

@author: Lars Heppert
'''

import pygame
import time
pygame.init()

class PlayingField(object):
    def __init__(self, size, gameObject, compObject):
        self.gameObject     = gameObject
        self.compObject     = compObject
        self.computerSide   = 2
        sizeX, sizeY        = size
        self.field          = self.gameObject.field
        self.fieldsY        = len(self.field)
        self.fieldsX        = len(self.field[0])
        self.fieldWidth     = sizeX / self.fieldsX
        self.fieldHeight    = sizeY / self.fieldsY
        self.margin         = 50
        self.padding        = 30
        self.seperator      = 5
        self.size           = sizeX+self.margin*2, sizeY+self.margin*2
        self.screen         = pygame.display.set_mode(self.size)
        self.running        = True
        self.start()
        
    def drawRect(self, position, color, size=(10, 10)):
        x, y            = position
        width, height   = size
        rect            = (x, y, width, height)
        pygame.draw.rect(self.screen, color, rect)
        
    def drawField(self):
        for y in range(self.fieldsY):
            for x in range(self.fieldsX):
                position    = (self.margin+x*self.fieldWidth,
                               self.margin+y*self.fieldHeight)
                size        = (self.fieldWidth-self.seperator,
                               self.fieldHeight-self.seperator)
                if (y*3+x) % 2 == 0:
                    color = 0, 0, 140
                else:
                    color = 0, 0, 220
                self.drawRect(position, color, size)
                if (self.field[y][x] != 0):
                    self.drawPlayerSign(position, self.field[y][x])
                    
    def drawPlayerSign(self, startPosition, player):
        startX, startY  =   startPosition
        startPosition   =  (startX+self.padding,
                            startY+self.padding)
        endPosition     =  (startX+self.fieldWidth-self.padding,
                            startY+self.fieldHeight-self.padding)
        if player == 1:
            self.drawPlayerOne(startPosition, endPosition)
        else:
            self.drawPlayerTwo(startPosition, endPosition)
            
    def drawPlayerOne(self, startPosition, endPosition):
        x1, y1      = startPosition
        x2, y2      = endPosition
        x2, y2      = x2-x1, y2-y1
        rect        = x1, y1, x2, y2
        innerRect   = x1+15, y1+15, x2-30, y2-30
        color       = 0, 180, 0
        backColor   = self.screen.get_at(startPosition)
        pygame.draw.ellipse(self.screen, color, rect, 0)
        pygame.draw.ellipse(self.screen, backColor, innerRect, 0)    
            
    def drawPlayerTwo(self, startPosition, endPosition):
        color       = 180, 0, 0
        width       = 20
        adjustion   = 10
        startX, startY  = startPosition
        endX, endY      = endPosition
        startPosition   = startX+adjustion, startY+adjustion
        endPosition     = endX-adjustion, endY-adjustion
        pygame.draw.line(self.screen, color, startPosition, endPosition, width)
        startX, startY  = startPosition
        startX, startY  = (startX-self.padding-adjustion,
                           startY-self.padding-adjustion)
        startPosition   = (startX+self.padding+adjustion,
                           startY+self.fieldHeight-self.padding-adjustion)
        endPosition     = (startX+self.fieldWidth-self.padding-adjustion,
                           startY+self.padding+adjustion)
        pygame.draw.line(self.screen, color, startPosition, endPosition, width)
        
    def makeHumanMove(self, position, button):
        x, y    = position
        if x > self.margin and y > self.margin:
            x, y    = (x-self.margin)/self.fieldWidth, (y-self.margin)/self.fieldHeight
            if len(self.field) > y and len(self.field[0]) > x:
                move = x, y
                if button == 3:
#                   switch sides and let the computer move 
                    self.computerSide = self.gameObject.getSide()
                    self.makeCompMove()
                elif move in self.gameObject.getMoves():
#                   the human makes his move alone
#                   so we first check if its a possible move ;-) 
                    self.gameObject.makeMove(move)
                    self.checkForResult()
                        
    def makeCompMove(self):
        bestMove = self.compObject.getBestMove(9)
        self.gameObject.makeMove(bestMove)
        self.checkForResult()
         
    def checkForResult(self):
        state = self.gameObject.checkGameState()
        if state != -1:
#           show current position for 1 seconds 
            self.refresh()
            time.sleep(1)
#           the computer should not start
            self.computerSide = 2
            self.gameObject.newGame()
#           show the winner of the game 
            self.screen.fill((0, 0, 0))
            if state == 1:
                msg = 'Player 1 won!'
            elif state == 2:
                msg = 'Player 2 won!'
            elif state == 0:
                msg = 'The Game is drawn'
                
            font = pygame.font.Font(None, 80)
            # Render the text
            text = font.render(msg, True, (255, 255, 255), (159, 182, 205))
            # Create a rectangle
            textRect = text.get_rect()
            # Center the rectangle
            textRect.centerx = self.screen.get_rect().centerx
            textRect.centery = self.screen.get_rect().centery
            # Blit the text
            self.screen.blit(text, textRect)                
            pygame.display.flip()    
            time.sleep(2)
                
    def refresh(self):
        self.screen.fill((0, 0, 0))
        self.drawField()                
        pygame.display.flip()
        
    def start(self):
        while self.running:
#           draw current playing field 
            self.refresh()
#           does the computer has to play? 
            if self.gameObject.getSide() == self.computerSide:
                self.makeCompMove()
            else:
#               the human has to play! 
                event = pygame.event.poll()
                if event.type == pygame.QUIT:
                    self.running = False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    self.makeHumanMove(event.pos, event.button)
                    
    def stop(self):
        self.running = False
Original aus dem Buch ansehen alphbetaprunning.py · Python 2
'''
Created on 19.04.2009

@author: Lars Heppert
'''

class AlphaBetaPrunning(object):
#   the evaluation function has to be smaller
#   than the defined infinity value
    infinity = 200
    
    def __init__(self, gameObject):
        self.gameObject = gameObject
    
    def getEvaluatedMoves(self, depth):
        possibleMoves   = self.gameObject.getMoves()
        evaluatedMoves  = []
        for move in possibleMoves:
            self.gameObject.makeMove(move)
            self.treeNodeCounter = 0
            evaluation = self.explore(depth-1)
            self.gameObject.undoMove(move)
            evaluatedMoves.append((evaluation, move, self.treeNodeCounter))
        
        evaluatedMoves.sort()
        return evaluatedMoves
        
    def getBestMove(self, depth):
        moves = self.getEvaluatedMoves(depth)
        print '\nEvaluated Moves:'
        for move in moves:
            print move
        print '----------------'
        bestMove = moves[0][1]
        print 'best move = ', bestMove
        return bestMove        
    
    def explore(self, depth,
                alpha   =   -AlphaBetaPrunning.infinity,
                beta    =   +AlphaBetaPrunning.infinity):
        
        moves = self.gameObject.getMoves()
        if depth <= 0 or not moves:
            if self.gameObject.sideToPlay == 1:
                return +self.gameObject.evaluate()
            else:
                return -self.gameObject.evaluate()
        
        self.treeNodeCounter += 1
        for move in moves:
            self.gameObject.makeMove(move)
            value = -self.explore(depth-1, -alpha, -beta)
            self.gameObject.undoMove(move)
            if value >= beta:
#               beta cut off 
                return beta
            if value > alpha:
                alpha = value
                
        return alpha