Dieses Projekt ist eine Python-Implementierung des klassischen Vier-Gewinnt-Spiels, bei dem ein Spieler gegen eine KI antreten kann. Es nutzt die Pygame-Bibliothek für die Spieloberfläche und -steuerung. Die Entscheidungsfindung der KI basiert auf dem Monte-Carlo-Baumsuchalgorithmus (Monte Carlo tree search algorithm), und der Schwierigkeitsgrad ist einstellbar, sodass Spieler sich mit schlaueren KI-Gegnern herausfordern können.
Wichtige Konzepte:
Verwendung von Pygame für die Spieleentwicklung.
Implementierung des Monte-Carlo-Baumsuchalgorithmus für die Entscheidungsfindung der KI.
👀 Vorschau
🎯 Aufgaben
In diesem Projekt lernst du:
Wie man ein Spiel mit Pygame baut
Wie man den Monte-Carlo-Baumsuchalgorithmus für die Entscheidungsfindung der KI implementiert
Wie man den Schwierigkeitsgrad der KI anpasst und verbessert
Wie man ein lustiges und interaktives Vier-Gewinnt-Spiel für Mensch gegen KI-Kämpfe erstellt
🏆 Errungenschaften
Nach Abschluss dieses Projekts kannst du:
Spiele mit Python und Pygame entwickeln
Die Prinzipien des Monte-Carlo-Baumsuchalgorithmus verstehen
Den Schwierigkeitsgrad eines KI-Gegners einstellen, um ein herausforderndes Spielerlebnis zu schaffen
Benutzerinterfaces verbessern, um das Spielerlebnis noch interessanter zu gestalten
Mehr als nur ein Lerntext – erlebe es selbst.
Entwicklungsvorbereitung
Das Vier-Gewinnt-Spiel wird auf einem Raster der Größe 7*6 gespielt. Die Spieler setzen abwechselnd ihre Spielsteine von oben in eine Spalte. Der Stein fällt in die unterste leere Position in dieser Spalte. Der Spieler, der vier Steine in einer Reihe (horizontal, vertikal oder diagonal) verbindet, gewinnt das Spiel.
Erstelle eine Datei namens fourinrow.py im Verzeichnis ~/project, um den Code für dieses Projekt zu speichern. Darüber hinaus müssen wir die Pygame-Bibliothek installieren, um die Spieloberfläche zu implementieren und die Operationen zu unterstützen.
cd ~/project
touch fourinrow.py
sudo pip install pygame
Die erforderlichen Bildressourcen für dieses Projekt findest du im Verzeichnis ~/project/images.
Um den Code in diesem Projekt besser zu verstehen, wird empfohlen, ihn parallel zum Code der vollständigen Lösung zu studieren.
Die verwendeten Variablen umfassen die Breite und Höhe des Schachbretts (können geändert werden, um Schachbretter unterschiedlicher Größe zu entwerfen), den Schwierigkeitsgrad, die Größe der Spielsteine und die Einstellung einiger Koordinatenvariablen.
In der Datei fourinrow.py gib den folgenden Code ein:
import random, copy, sys, pygame
from pygame.locals import *
BOARDWIDTH = 7 ## Anzahl der Spalten auf dem Spielbrett
BOARDHEIGHT = 6 ## Anzahl der Reihen auf dem Spielbrett
assert BOARDWIDTH >= 4 and BOARDHEIGHT >= 4, 'Board must be at least 4x4.'
## Die Python-Assert-Anweisung wird verwendet, um zu deklarieren, dass der gegebene boolesche Ausdruck wahr sein muss.
## Wenn der Ausdruck falsch ist, wird eine Ausnahme ausgelöst.
DIFFICULTY = 2 ## Schwierigkeitsgrad, Anzahl der Züge, die der Computer betrachten kann
## Hier bedeutet 2, dass 7 mögliche Züge des Gegners und wie darauf reagiert werden soll, berücksichtigt werden
SPACESIZE = 50 ## Größe der Spielsteine
FPS = 30 ## Bildwiederholrate des Bildschirms, 30/s
WINDOWWIDTH = 640 ## Breite des Spielfensters in Pixeln
WINDOWHEIGHT = 480 ## Höhe des Spielfensters in Pixeln
XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * SPACESIZE) / 2) ## X-Koordinate der linken Kante des Rasters
YMARGIN = int((WINDOWHEIGHT - BOARDHEIGHT * SPACESIZE) / 2) ## Y-Koordinate der oberen Kante des Rasters
BRIGHTBLUE = (0, 50, 255) ## Blaue Farbe
WHITE = (255, 255, 255) ## Weiße Farbe
BGCOLOR = BRIGHTBLUE
TEXTCOLOR = WHITE
RED = 'red'
BLACK = 'black'
EMPTY = None
HUMAN = 'human'
COMPUTER = 'computer'
Zusätzlich müssen wir auch einige globale Pygame-Variablen definieren. Diese globalen Variablen werden später in verschiedenen Modulen mehrmals aufgerufen. Viele von ihnen sind Variablen, die geladene Bilder speichern, daher ist die Vorbereitungsarbeit etwas langwierig, bitte habe Geduld.
## Initialisiere Pygame-Module
pygame.init()
## Erstelle ein Clock-Objekt
FPSCLOCK = pygame.time.Clock()
## Erstelle das Spielfenster
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
## Setze den Titel des Spielfensters
pygame.display.set_caption(u'four in row')
## Rect(left, top, width, height) wird verwendet, um Position und Größe zu definieren
REDPILERECT = pygame.Rect(int(SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)
## Erstelle die unteren linken und rechten Spielsteine im Fenster
BLACKPILERECT = pygame.Rect(WINDOWWIDTH - int(3 * SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)
## Lade das Bild des roten Spielsteins
REDTOKENIMG = pygame.image.load('images/4rowred.png')
## Skaliere das Bild des roten Spielsteins auf SPACESIZE
REDTOKENIMG = pygame.transform.smoothscale(REDTOKENIMG, (SPACESIZE, SPACESIZE))
## Lade das Bild des schwarzen Spielsteins
BLACKTOKENIMG = pygame.image.load('images/4rowblack.png')
## Skaliere das Bild des schwarzen Spielsteins auf SPACESIZE
BLACKTOKENIMG = pygame.transform.smoothscale(BLACKTOKENIMG, (SPACESIZE, SPACESIZE))
## Lade das Bild des Schachbretts
BOARDIMG = pygame.image.load('images/4rowboard.png')
## Skaliere das Bild des Schachbretts auf SPACESIZE
BOARDIMG = pygame.transform.smoothscale(BOARDIMG, (SPACESIZE, SPACESIZE))
## Lade das Bild des menschlichen Siegers
HUMANWINNERIMG = pygame.image.load('images/4rowhumanwinner.png')
## Lade das Bild des KI-Siegers
COMPUTERWINNERIMG = pygame.image.load('images/4rowcomputerwinner.png')
## Lade das Bild eines Unentschiedens
TIEWINNERIMG = pygame.image.load('images/4rowtie.png')
## Gib ein Rect-Objekt zurück
WINNERRECT = HUMANWINNERIMG.get_rect()
## Zentriere das Siegerbild im Spielfenster
WINNERRECT.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
## Lade das Pfeilbild für Benutzeranweisungen
ARROWIMG = pygame.image.load('images/4rowarrow.png')
## Gib ein Rect-Objekt zurück
ARROWRECT = ARROWIMG.get_rect()
## Setze die linke Position des Pfeilbilds
ARROWRECT.left = REDPILERECT.right + 10
## Ausrichte das Pfeilbild vertikal mit dem darunter liegenden roten Spielstein
ARROWRECT.centery = REDPILERECT.centery
Um den Code in diesem Projekt besser zu verstehen, wird empfohlen, ihn parallel zum Code der vollständigen Lösung zu studieren.
Zunächst wird die zweidimensionale Liste, die das Brett repräsentiert, geleert. Anschließend werden die entsprechenden Positionen auf dem Brett basierend auf den Zügen des Spielers und der KI mit Farben versehen.
def drawBoard(board, extraToken=None):
## DISPLAYSURF ist unsere Schnittstelle, die im Variablen-Initialisierungsmodul definiert ist.
DISPLAYSURF.fill(BGCOLOR) ## Fülle den Hintergrund des Spielfensters mit blauer Farbe.
spaceRect = pygame.Rect(0, 0, SPACESIZE, SPACESIZE) ## Erstelle eine Rect-Instanz.
for x in range(BOARDWIDTH):
## Bestimme die Koordinaten der oberen linken Ecke jeder Zelle in jeder Reihe jeder Spalte.
for y in range(BOARDHEIGHT):
spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))
## Wenn x = 0 und y = 0, handelt es sich um die erste Zelle in der ersten Reihe der ersten Spalte.
if board[x][y] == RED: ## Wenn der Zellwert rot ist,
## zeichne einen roten Spielstein im Spielfenster innerhalb von spaceRect.
DISPLAYSURF.blit(REDTOKENIMG, spaceRect)
elif board[x][y] == BLACK: ## Andernfalls zeichne einen schwarzen Spielstein.
DISPLAYSURF.blit(BLACKTOKENIMG, spaceRect)
## extraToken ist eine Variable, die Positions- und Farbinformationen enthält.
## Sie wird verwendet, um einen bestimmten Spielstein anzuzeigen.
if extraToken!= None:
if extraToken['color'] == RED:
DISPLAYSURF.blit(REDTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))
elif extraToken['color'] == BLACK:
DISPLAYSURF.blit(BLACKTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))
## Zeichne die Spielsteinfelder.
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))
DISPLAYSURF.blit(BOARDIMG, spaceRect)
## Zeichne die Spielsteine in der unteren linken und rechten Ecke des Spielfensters.
DISPLAYSURF.blit(REDTOKENIMG, REDPILERECT) ## Linker roter Spielstein.
DISPLAYSURF.blit(BLACKTOKENIMG, BLACKPILERECT) ## Rechter schwarzer Spielstein.
def getNewBoard():
board = []
for x in range(BOARDWIDTH):
board.append([EMPTY] * BOARDHEIGHT)
return board ## Gib die Brettliste mit BOARDHEIGHT Anzahl von None-Werten zurück.
Im obigen Code zeichnet die Funktion drawBoard() das Brett und die Spielsteine darauf. Die Funktion getNewBoard() gibt eine neue Brett-Datenstruktur zurück.
Kurze Erklärung der Idee der Monte-Carlo-Baumsuche (Monte Carlo tree search):
Verwende die eindimensionale Monte-Carlo-Methode, um das Go-Spielbrett zu bewerten. Genauer gesagt, wenn eine bestimmte Brettsituation gegeben ist, wählt das Programm zufällig einen Punkt aus allen verfügbaren Punkten in der aktuellen Situation aus und setzt einen Spielstein darauf. Dieser Prozess der zufälligen Auswahl verfügbarer Punkte (Rollpunkt) wird wiederholt, bis keine Seite mehr einen verfügbaren Punkt hat (das Spiel endet). Anschließend wird der resultierende Gewinn oder Verlust dieses Endzustands als Grundlage für die Bewertung der aktuellen Situation zurückgeführt.
In diesem Projekt wählt die KI kontinuierlich verschiedene Spalten aus und bewertet die Ergebnisse des Sieges beider Seiten. Die KI wird letztendlich eine Strategie mit einer höheren Bewertung wählen.
Bevor du dir die folgenden Bilder und Texte ansiehst, schau dir zuerst den Code am Ende an und beziehe dich dann auf die entsprechenden Erklärungen.
Beobachte die Konfrontation zwischen KI und Spieler in der folgenden Abbildung:
Einige Variablen im Projekt können den Prozess der Spielsteinoperationen der KI intuitiv widerspiegeln:
PotentialMoves: Gibt eine Liste zurück, die die Wahrscheinlichkeit des Sieges der KI darstellt, wenn ein Spielstein in eine beliebige Spalte in der Liste gesetzt wird. Die Werte sind Zufallszahlen zwischen -1 und 1. Wenn der Wert negativ ist, bedeutet dies, dass der Spieler in den nächsten zwei Zügen gewinnen könnte, und je kleiner der Wert, desto größer die Wahrscheinlichkeit, dass der Spieler gewinnt. Wenn der Wert 0 ist, bedeutet dies, dass der Spieler nicht gewinnen wird und die KI auch nicht gewinnen wird. Wenn der Wert 1 ist, bedeutet dies, dass die KI gewinnen kann.
bestMoveFitness: Die Fitness ist der maximale Wert, der aus PotentialMoves ausgewählt wird.
bestMoves: Wenn es mehrere maximale Werte in PotentialMoves gibt, bedeutet dies, dass die Chancen des Spielers, zu gewinnen, am geringsten sind, wenn die KI den Spielstein in die Spalten setzt, in denen sich diese Werte befinden. Daher werden diese Spalten der Liste bestMoves hinzugefügt.
column: Wenn es mehrere Werte in bestMoves gibt, wird zufällig eine Spalte aus bestMoves als Zug der KI ausgewählt. Wenn es nur einen Wert gibt, ist column dieser eindeutige Wert.
In diesem Projekt können wir durch das Ausgeben dieser bestMoveFitness, bestMoves, column und potentialMoves die Parameter jedes Schritts der KI in der obigen Abbildung ableiten.
Durch die Untersuchung der Auswahl der KI im dritten Schritt können wir das Algorithmus besser verstehen:
Die folgende Abbildung zeigt einige Züge der KI, die möglichen Entscheidungen des Spielers, wenn die KI einen Stein in die erste Spalte setzt, und die Auswirkungen des nächsten Zuges der KI auf die Gewinnchancen des Spielers. Durch diesen Such- und Iterationsprozess kann die KI die Gewinnsituationen des Gegners und ihrer eigenen in den nächsten zwei Schritten bestimmen und entsprechend Entscheidungen treffen.
Die folgende Abbildung ist ein Flussdiagramm zur Berechnung des Fitnesswerts für die KI. In diesem Projekt beträgt der Schwierigkeitskoeffizient 2, und wir müssen 7^4 = 2041 Fälle berücksichtigen:
Aus dem obigen Flussdiagramm lässt sich leicht erkennen, dass, wenn die KI ihren ersten Stein in die Spalte 0, 1, 2, 4, 5 oder 6 setzt, der Spieler immer die verbleibenden zwei Steine in die Spalte 3 setzen kann und gewinnt. Zur einfacheren Darstellung verwenden wir eine Sequenz, um verschiedene Kombinationen darzustellen, wobei das erste Element den ersten Zug der KI, die zweite Zahl die Reaktion des Spielers und die dritte Zahl die Reaktion der KI darstellt. "X" steht für jeden gültigen Zug. Daher ist [0,0,x] = 0, und man kann ableiten, dass, wenn die Sequenz [0,x<>3,x] ist, der Spieler nicht gewinnen kann. Nur wenn der zweite Stein des Spielers in der Spalte 3 ist und der zweite Zug der KI nicht in der Spalte 3 ist, kann die KI gewinnen. Daher ist [0,x=3,x<>3] = -1, und es gibt 6 solche Fälle. Das Endergebnis ist (0+0+...(43 mal)-1*6)/7/7 = -0.12.
Nach demselben Prinzip sind die Ergebnisse für die anderen vier Fälle alle -0.12. Wenn der erste Zug der KI in der Spalte 3 ist, kann der Spieler nicht gewinnen, und die KI kann auch nicht gewinnen, also ist der Wert 0. Die KI wählt den Zug mit dem höchsten Fitnesswert, was bedeutet, dass sie ihren Stein in die Spalte 3 setzen wird.
Die gleiche Analyse kann auf die nachfolgenden Züge der KI angewendet werden. Zusammenfassend lässt sich sagen, dass je höher die Wahrscheinlichkeit ist, dass der Spieler nach dem Zug der KI gewinnt, desto niedriger der Fitnesswert für die KI ist, und die KI wird den Zug mit einem höheren Fitnesswert wählen, um zu verhindern, dass der Spieler gewinnt. Natürlich wird die KI, wenn sie selbst gewinnen kann, den Zug priorisieren, der zu ihrem eigenen Sieg führt.
def getPotentialMoves(board, tile, lookAhead):
if lookAhead == 0 or isBoardFull(board):
'''
Wenn der Schwierigkeitskoeffizient 0 ist oder das Brett voll ist,
gib eine Liste zurück, bei der alle Werte auf 0 gesetzt sind. Dies bedeutet, dass
der Fitnesswert gleich den potenziellen Zügen für jede Spalte ist.
In diesem Fall wird die KI den Stein zufällig fallen lassen und ihre Intelligenz verlieren.
'''
return [0] * BOARDWIDTH
## Bestimme die Farbe des Gegnersteins
if tile == RED:
enemyTile = BLACK
else:
enemyTile = RED
potentialMoves = [0] * BOARDWIDTH
## Initialisiere eine Liste potenzieller Züge, bei der alle Werte auf 0 gesetzt sind
for firstMove in range(BOARDWIDTH):
## Iteriere über jede Spalte und betrachte jeden Zug von beiden Seiten als firstMove
## Der Zug der anderen Seite wird dann als counterMove betrachtet
## Hier bezieht sich unser firstMove auf den Zug der KI, und der Zug des Gegners wird als counterMove betrachtet
## Mache eine tiefe Kopie des Bretts, um gegenseitige Einflüsse zwischen board und dupeBoard zu vermeiden
dupeBoard = copy.deepcopy(board)
if not isValidMove(dupeBoard, firstMove):
## Wenn der Zug, einen schwarzen Stein in die von firstMove angegebene Spalte zu setzen, in dupeBoard ungültig ist
continue
## Gehe zum nächsten firstMove über
makeMove(dupeBoard, tile, firstMove)
## Wenn es ein gültiger Zug ist, setze die entsprechende Rasterfarbe
if isWinner(dupeBoard, tile):
## Wenn die KI gewinnt
potentialMoves[firstMove] = 1
## Der gewinnende Stein erhält automatisch einen hohen Wert, um seine Gewinnchancen anzuzeigen
## Je größer der Wert, desto höher die Gewinnchancen und desto niedriger die Gewinnchancen des Gegners
break
## Störe nicht die Berechnung anderer Züge
else:
if isBoardFull(dupeBoard):
## Wenn es keine leeren Raster in dupeBoard gibt
potentialMoves[firstMove] = 0
## Es ist nicht möglich, einen Zug zu machen
else:
for counterMove in range(BOARDWIDTH):
## Betrachte den Zug des Gegners
dupeBoard2 = copy.deepcopy(dupeBoard)
if not isValidMove(dupeBoard2, counterMove):
continue
makeMove(dupeBoard2, enemyTile, counterMove)
if isWinner(dupeBoard2, enemyTile):
potentialMoves[firstMove] = -1
## Wenn der Spieler gewinnt, ist der Fitnesswert für die KI in dieser Spalte am niedrigsten
break
else:
## Rufe getPotentialMoves rekursiv auf
results = getPotentialMoves(dupeBoard2, tile, lookAhead - 1)
## Verwende hier eine Fließkommadarstellung für genauere Ergebnisse
## Dies stellt sicher, dass die Werte in potentialMoves im Bereich [-1, 1] liegen
potentialMoves[firstMove] += (sum(results)*1.0 / BOARDWIDTH) / BOARDWIDTH
return potentialMoves
Ziehe den Spielstein, bestimme das Feld, in dem sich der Spielstein befindet, validiere den Spielstein, rufe die Funktion zum Fallenlassen des Spielsteins auf und beende die Aktion.
def getHumanMove(board, isFirstMove):
draggingToken = False
tokenx, tokeny = None, None
while True:
## Verwende pygame.event.get(), um alle Ereignisse zu verarbeiten
for event in pygame.event.get():
if event.type == QUIT: ## Stoppe und beende
pygame.quit()
sys.exit()
elif event.type == MOUSEBUTTONDOWN and not draggingToken and REDPILERECT.collidepoint(event.pos):
## Wenn der Ereignistyp Mausklick ist, draggingToken ist True und die Mausklickposition innerhalb von REDPILERECT liegt
draggingToken = True
tokenx, tokeny = event.pos
elif event.type == MOUSEMOTION and draggingToken: ## Wenn der rote Spielstein gezogen wird
tokenx, tokeny = event.pos ## Aktualisiere die Position des gezogenen Spielsteins
elif event.type == MOUSEBUTTONUP and draggingToken:
## Wenn die Maustaste losgelassen wird und der Spielstein gezogen wird
## Wenn der Spielstein direkt über dem Brett gezogen wird
if tokeny < YMARGIN and tokenx > XMARGIN and tokenx < WINDOWWIDTH - XMARGIN:
column = int((tokenx - XMARGIN) / SPACESIZE) ## Bestimme die Spalte, in die der Spielstein fallen wird, basierend auf der x-Koordinate des Spielsteins (0,1...6)
if isValidMove(board, column): ## Wenn der Spielsteinzug gültig ist
"""
Lasse ihn in das entsprechende leere Feld fallen,
Diese Funktion zeigt nur den Fall-Effekt an
Das Füllen des Felds mit dem Spielstein kann auch ohne diese Funktion mit folgendem Code erreicht werden
"""
animateDroppingToken(board, column, RED)
## Setze das unterste Feld in der leeren Spalte auf rot
board[column][getLowestEmptySpace(board, column)] = RED
drawBoard(board) ## Zeichne den roten Spielstein in das Feld, in das er gefallen ist
pygame.display.update() ## Fensteraktualisierung
return
tokenx, tokeny = None, None
draggingToken = False
if tokenx!= None and tokeny!= None: ## Wenn ein Spielstein gezogen wird, zeige den gezogenen Spielstein an
drawBoard(board, {'x':tokenx - int(SPACESIZE / 2), 'y':tokeny - int(SPACESIZE / 2), 'color':RED})
## Passe die x-, y-Koordinaten an, damit die Maus während des Ziehens immer in der Mittelposition des Spielsteins ist
else:
drawBoard(board) ## Wenn es ein ungültiger Zug ist, nachdem die Maustaste losgelassen wurde, da alle Werte im Brett none sind
## Beim Aufruf von drawBoard werden die Operationen ausgeführt, um die beiden unteren Spielsteine anzuzeigen, was gleichbedeutend damit ist, den Spielstein an den Ort zurückzusetzen, von dem aus er gezogen wurde
if isFirstMove:
DISPLAYSURF.blit(ARROWIMG, ARROWRECT) ## Die KI beginnt, zeige das Hinweisbild für die Aktion an
pygame.display.update()
FPSCLOCK.tick()
Im obigen Code behandelt die Funktion getHumanMove() den Zug des Spielers. Die Funktion animateDroppingToken() animiert das Fallenlassen des Spielsteins. Die Funktion getLowestEmptySpace() gibt den untersten leeren Platz in einer Spalte zurück.
Implementiere die Funktion, um die Bewegung des Computers und das Platzieren der KI-Spielsteine an den jeweiligen Positionen zu animieren.
def animateComputerMoving(board, column):
x = BLACKPILERECT.left ## Die linke Koordinate des schwarzen Spielsteins unten
y = BLACKPILERECT.top ## Die obere Koordinate des schwarzen Spielsteins unten
speed = 1.0
while y > (YMARGIN - SPACESIZE): ## Wenn y einen größeren Wert hat, was bedeutet, dass der Spielstein unterhalb des Fensters ist
y -= int(speed) ## Verringere y kontinuierlich, was bedeutet, dass der Spielstein nach oben bewegt wird
speed += 0.5 ## Erhöhe die Geschwindigkeit, mit der y verringert wird
drawBoard(board, {'x':x, 'y':y, 'color':BLACK})
## y ändert sich ständig, zeichne kontinuierlich den schwarzen Spielstein, um einen Effekt des kontinuierlichen Aufstiegs zu erzeugen
pygame.display.update()
FPSCLOCK.tick()
## Wenn der Spielstein an die Spitze des Bretts aufgestiegen ist
y = YMARGIN - SPACESIZE ## Setze y zurück, damit die Unterkante des Spielsteins mit der Oberkante des Bretts fluchtet
speed = 1.0
while x > (XMARGIN + column * SPACESIZE): ## Wenn x größer als die x-Koordinate der gewünschten Spalte ist
x -= int(speed) ## Verringere x kontinuierlich, was bedeutet, dass der Spielstein nach links bewegt wird
speed += 0.5
drawBoard(board, {'x':x, 'y':y, 'color':BLACK})
## An diesem Punkt bleibt die y-Koordinate unverändert, was bedeutet, dass der Spielstein horizontal zur Spalte bewegt wird
pygame.display.update()
FPSCLOCK.tick()
## Der schwarze Spielstein landet auf dem berechneten leeren Platz
animateDroppingToken(board, column, BLACK)
Wähle die höchste Zahl aus der Liste der zurückgegebenen potentialMoves als Fitnesswert aus und wähle zufällig aus den Spalten mit hohen Fitnesswerten als endgültiges Ziel für den Zug aus.
def getComputerMove(board):
potentialMoves = getPotentialMoves(board, BLACK, DIFFICULTY) ## Potenzielle Züge, eine Liste mit BOARDWIDTH Werten
## Die Werte in der Liste hängen von der eingestellten Schwierigkeitsstufe ab
bestMoves = [] ## Erstelle eine leere bestMoves-Liste
bestMoveFitness = -1 ## Da der Mindestwert in potentialMoves -1 ist, dient er als untere Grenze
print(bestMoveFitness)
for i in range(len(potentialMoves)):
if potentialMoves[i] > bestMoveFitness and isValidMove(board, i):
bestMoveFitness = potentialMoves[i] ## Aktualisiere bestMoves kontinuierlich, damit jeder Wert in bestMoves der größte ist
## und gleichzeitig sicherstellen, dass der Zug gültig ist.
for i in range(len(potentialMoves)):
if potentialMoves[i] == bestMoveFitness and isValidMove(board, i):
bestMoves.append(i) ## Liste alle Spalten auf, in die der Spielstein bewegt werden kann. Diese Liste kann leer sein,
## nur einen Wert oder mehrere Werte enthalten.
print(bestMoves)
return random.choice(bestMoves) ## Wähle zufällig eine der Spalten aus, in die der Spielstein bewegt werden kann, als Zielzug aus.
Durch kontinuierliche Änderung der entsprechenden Koordinaten der Spielsteine wird der Animations-Effekt des Fallens erreicht.
def getLowestEmptySpace(board, column):
## Gib den untersten leeren Platz in einer Spalte zurück
for y in range(BOARDHEIGHT-1, -1, -1):
if board[column][y] == EMPTY:
return y
return -1
def makeMove(board, player, column):
lowest = getLowestEmptySpace(board, column)
if lowest!= -1:
board[column][lowest] = player
'''
Weise den Spieler (rot/schwarz) dem untersten leeren Platz in der Spalte zu.
Da der Spielstein in den untersten leeren Platz einer Spalte fallen gelassen wird,
wird dieser als die Farbe dieses Platzes betrachtet.
'''
def animateDroppingToken(board, column, color):
x = XMARGIN + column * SPACESIZE
y = YMARGIN - SPACESIZE
dropSpeed = 1.0
lowestEmptySpace = getLowestEmptySpace(board, column)
while True:
y += int(dropSpeed)
dropSpeed += 0.5
if int((y - YMARGIN) / SPACESIZE) >= lowestEmptySpace:
return
drawBoard(board, {'x':x, 'y':y, 'color':color})
pygame.display.update()
FPSCLOCK.tick()
Im obigen Code führt die Funktion makeMove() einen Zug auf dem Brett aus. Die Funktion animateDroppingToken() animiert das Fallenlassen des Spielsteins. Die Funktion getLowestEmptySpace() gibt den untersten leeren Platz in einer Spalte zurück.
Prüfe die Gültigkeit eines Spielsteinzugs und ob es noch leere Felder auf dem Schachbrett gibt.
def isValidMove(board, column):
## Prüfe die Gültigkeit eines Spielsteinzugs
if column < 0 or column >= (BOARDWIDTH) or board[column][0]!= EMPTY:
## Wenn die Spalte kleiner als 0 oder größer als BOARDWIDTH ist oder es kein leeres Feld in der Spalte gibt
return False
## Dann ist es ein ungültiger Zug, sonst ist er gültig
return True
def isBoardFull(board):
## Wenn es keine leeren Felder im Raster gibt, gib True zurück
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT):
if board[x][y] == EMPTY:
return False
return True
Im obigen Code gibt die Funktion isValidMove()True zurück, wenn der Zug gültig ist. Die Funktion isBoardFull() gibt True zurück, wenn das Brett voll ist.
Es werden mehrere Diagramme bereitgestellt, um die vier Gewinnbedingungen leicht verständlich zu machen. Die in den Diagrammen gezeigten Positionen entsprechen den Extremwerten von x und y.
def isWinner(board, tile):
## Überprüfe die horizontale Anordnung der Spielsteine
for x in range(BOARDWIDTH - 3): ## x nimmt die Werte 0, 1, 2, 3 an
for y in range(BOARDHEIGHT): ## iteriere über alle Zeilen
## Wenn x = 0, überprüfe, ob die ersten vier Spielsteine in der y-ten Zeile alle der gleichen Art sind. Dies kann verwendet werden, um alle horizontalen Anordnungen von vier in einer Reihe zu durchlaufen. Wenn irgendein x, y wahr ist, kann dies als Gewinn festgestellt werden
if board[x][y] == tile and board[x+1][y] == tile and board[x+2][y] == tile and board[x+3][y] == tile:
return True
## Überprüfe die vertikale Anordnung der Spielsteine, ähnlich wie bei der horizontalen Anordnung
for x in range(BOARDWIDTH):
for y in range(BOARDHEIGHT - 3):
if board[x][y] == tile and board[x][y+1] == tile and board[x][y+2] == tile and board[x][y+3] == tile:
return True
## Überprüfe die links geneigte diagonale Anordnung der Spielsteine
for x in range(BOARDWIDTH - 3): ## x nimmt die Werte 0, 1, 2, 3 an
for y in range(3, BOARDHEIGHT): ## weil beim Bilden einer links geneigten Diagonalen von vier in einer Reihe der unterste Spielstein mindestens vier Felder von der Spitze entfernt sein muss, d.h. y >= 3
if board[x][y] == tile and board[x+1][y-1] == tile and board[x+2][y-2] == tile and board[x+3][y-3] == tile: ## überprüfe, ob die vier links geneigten diagonalen Spielsteine die gleiche Farbe haben
return True
## Überprüfe die rechts geneigte diagonale Anordnung der Spielsteine, ähnlich wie bei der links geneigten Diagonalen
for x in range(BOARDWIDTH - 3):
for y in range(BOARDHEIGHT - 3):
if board[x][y] == tile and board[x+1][y+1] == tile and board[x+2][y+2] == tile and board[x+3][y+3] == tile:
return True
return False
Schließlich erstellen wir die Spielhauptschleife, um das Spiel kontinuierlich laufen zu lassen.
def main():
## Bestehender Code ausgelassen
isFirstGame = True ## Initialisiere isFirstGame
while True: ## Lasse das Spiel kontinuierlich laufen
runGame(isFirstGame)
isFirstGame = False
def runGame(isFirstGame):
if isFirstGame:
## Zu Beginn des ersten Spiels
## Lasse die KI den ersten Zug machen, damit die Spieler sehen können, wie das Spiel gespielt wird
turn = COMPUTER
showHelp = True
else:
## Ab dem zweiten Spiel werden die Züge zufällig zugewiesen
if random.randint(0, 1) == 0:
turn = COMPUTER
else:
turn = HUMAN
showHelp = False
mainBoard = getNewBoard() ## Setze die anfängliche leere Brettstruktur auf
while True: ## Spielhauptschleife
if turn == HUMAN: ## Wenn es der Zug des Spielers ist
getHumanMove(mainBoard, showHelp) ## Rufe die Methode für den Zug des Spielers auf, Details siehe Methode getHumanMove
if showHelp:
## Wenn es ein Hinweissymbol gibt, schalte den Hinweis aus, nachdem die KI den ersten Zug gemacht hat
showHelp = False
if isWinner(mainBoard, RED): ## Wenn der rote Spielstein (Spieler) gewinnt
winnerImg = HUMANWINNERIMG ## Lade das Bild für den Sieger des Spielers
break ## Verlasse die Schleife
turn = COMPUTER ## Übergebe den ersten Zug an die KI
else:
## Wenn es der Zug der KI ist
column = getComputerMove(mainBoard) ## Rufe die Methode für den Zug der KI auf, Details siehe Methode getComputerMove
print(column)
animateComputerMoving(mainBoard, column) ## Bewege den schwarzen Spielstein
makeMove(mainBoard, BLACK, column) ## Setze den untersten leeren Platz in der Spalte auf schwarz
if isWinner(mainBoard, BLACK):
winnerImg = COMPUTERWINNERIMG
break
turn = HUMAN ## Wechsle zum Zug des Spielers
if isBoardFull(mainBoard):
## Wenn das Brett voll ist, ist es ein Unentschieden
winnerImg = TIEWINNERIMG
break
Basierend auf dem Monte-Carlo-Algorithmus wurde in diesem Projekt ein Mensch gegen KI-Schachspiel mit Python und dem Pygame-Modul implementiert. Das Projekt ermöglichte es uns, uns mit den Grundlagen der Instanzierung und Bewegung von Objekten in Pygame vertraut zu machen und gab uns einen ersten Einblick in die konkrete Anwendung des Monte-Carlo-Algorithmus.