import random, sys import pygame #from pygame.locals import * # "Konstanten" (UNVERAENDERT) FIELD_EMPTY = " " # das Zeichen für ein noch nicht gesetztes Feld FIELD_SYMBOL = ("X", "O") # die Zeichen für die beiden Spieler # "Konstanten" für das Spielfenster (NEU) SIZE_FIELD = 100 # Größe eines Feldes (X, O, oder leer) in Pixeln LINE_THICKNESS = 10 # Breite der Linien zwischen den Feldern DEFAULT_SIZE = (400, 150) # Größe des Standardfensters FONT_SIZE = 30 # Standard-Schriftgröße für normalen Text # Farben COLOR_FOREGROUND = (0, 0, 0) COLOR_BACKGROUND = (255, 255, 255) COLOR_GREEN = (0, 255, 0) COLOR_RED = (255, 0, 0) # DIESE IMPLEMENTIERUNG SPEICHERT LEDIGLICH DIE LISTE DER X/O WIE IN DER KONSOLENFASSUNG. EBENFALLS MOEGLICH WAERE BEISPIELSWEISE, # EINE ZUSAETZLICHE LISTE VON RECHTECKEN ZU VERWALTEN, UM SO DEN KLICK AUF EINE POSITIONS ANDERS AUSFINDIG ZU MACHEN. def createEmptyBoard(size:int) -> list: """Erzeugt ein leeres Spielfeld quadratischer Größe int und gibt es zurück.""" # UNVERAENDERT (aus Konsolenfassung) return [[FIELD_EMPTY]*size for n in range(size)] # size Zeilen à size Spalten (anfangs leer) def displayField(windowSurface:pygame.Surface, board:list, coord:tuple): """Zeigt das Symbol an den Koordinaten coord (Tupel der Form (Reihe, Spalte) als Indizes) des Spielfeldes board auf der GUI/Oberfläche an. Die Oberfläche wird entsprechend aktualisiert.""" # NEU (nicht in Konsolenfassung vorhanden) symbol = board[coord[0]][coord[1]] # das anzuzeigende Symbol font = pygame.font.SysFont(None, SIZE_FIELD) # Schrift label = font.render(symbol, True, COLOR_FOREGROUND, COLOR_BACKGROUND) # Schriftzug mit dem Symbol # Beginn-Koordinaten errechnen (oben links im Eck von dem jeweiligen Feld) xPos = coord[1]*SIZE_FIELD+(coord[1]+1)*LINE_THICKNESS yPos = coord[0]*SIZE_FIELD+(coord[0]+1)*LINE_THICKNESS # Hintergrund setzen (Das muss gemacht werden, da dafür auf das komplizierte Zeichnen der Trennlinien verzichtet wird) pygame.draw.rect(windowSurface, COLOR_BACKGROUND, (xPos, yPos, SIZE_FIELD, SIZE_FIELD)) # Koordinaten für zentrierten Text ermitteln (da die Methode blit() den Text zentrieren wird) xPos += (SIZE_FIELD - label.get_width()) / 2 yPos += (SIZE_FIELD - label.get_height()) / 2 windowSurface.blit(label, (xPos, yPos)) pygame.display.update() def displayBoard(board:list): """Zeigt das gesamte Spielfeld board auf der GUI/Oberfläche windowSurface an.""" # VERAENDERT (aber eigentlich nicht benötigt in dieser Fassung) windowSurface = pygame.display.get_surface() # In dieser Implementierung wird auf das Zeichnen der Trennlinien verzichtet, indem stattdessen die einzelnen Felder # inkl. Hintergrund (!) gezeichnet werden und die Hintergrundfarbe des Fensters stattdessen auf die Vordergrundfarbe # gesetzt wird. Es werden die Felder mit Vorder- und Hintergrundfarbe also so gezeichnet, dass der Zwischenraum # genau den Trennlinien entspricht (Vorteil: Man tut sich leichter beim Überschreiben eines einzelnen Feldes, da # anderenfalls evtl. noch "Reste" des vorherigen Zeichens angezeigt werden (beim Erneuern des Spielfeldes). # Geht man anders vor, so müssten die Linien wie folgt gezeichnet werden (bspw.): #size_board = len(board) * (SIZE_FIELD + LINE_THICKNESS) + LINE_THICKNESS # Größe des gesamten Spielfeldes inkl. Trennlinien #for i in range(len(board)+1): # startPos = i*(SIZE_FIELD+LINE_THICKNESS) # pygame.draw.rect(windowSurface, COLOR_FOREGROUND, (startPos, 0, LINE_THICKNESS, size_board)) # pygame.draw.rect(windowSurface, COLOR_FOREGROUND, (0, startPos, size_board, LINE_THICKNESS)) # Einzelne Felder anzeigen for row in range(len(board)): for column in range(len(board[row])): displayField(windowSurface, board, (row, column)) def setSymbol(windowSurface:pygame.Surface, board:list, coord:tuple, symbol:str) -> bool: """Diese Funktion setzt das Symbol an den übergebenen Koordinaten coord im Spielfeld board auf symbol und macht die Änderung für den Benutzer über die Oberfläche/GUI sichtbar. Die Koordinaten müssen als Tupel der Form (Zeile, Spalte) übergeben werden. Überprüft dann, ob der Spieler dadurch gewonnen hat und gibt entsprechend True/False zurück, d. h. True, falls der Spieler durch setzen des Feldes gewonnen hat.""" # ERWEITERT (um einen Funktionsaufruf nach der Zuweisung an board[y][x]) y = coord[0] # Zeile x = coord[1] # Spalte board[y][x] = symbol # setzt das Symbol im Feld displayField(windowSurface, board, (y, x)) # Sieg über (aktuelle) Spalte prüfen: won = True # Annahme for row in board: # gehe durch jede Zeile if row[x] != symbol: # überprüfe (aktuelle) Spalte in jeder Zeile won = False # nicht gewonnen über aktuelle Spalte, da mind. eine Zeile nicht passt if won: return True # Sieg über (aktuelle) Zeile prüfen: won = True # Annahme for col in board[y]: # gehe durch jede Spalte der aktuellen Zeile if col != symbol: won = False # nicht gewonnen über aktuelle Zeile, da mind. eine Spalte nicht passt if won: return True # Sieg über Diagonale 1 (von oben links nach unten rechts) prüfen: if (x == y): # nur dann muss Diagonale 1 überprüft werden! won = True # Annahme for i in range(len(board)): # Beispiel: Feld der Größe 3: gehe über 0, 1 und 2 if board[i][i] != symbol: won = False # nicht gewonnen über Diagonale 1 if won: return True # Sieg über Diagonale 2 (von unten links nach oben rechts) prüfen: if (x+y == len(board)-1): # nur dann muss Diagonale 2 überprüft werden! won = True # Annahme for i in range(len(board)): if board[i][len(board)-i-1] != symbol: won = False # nicht gewonnen über Diagonale 2 if won: return True return False # falls nie ein Sieg festgestellt wurde, dann logischerweise nicht gewonnen def isFinished(board:list) -> bool: """Gibt True zurück, falls alle Felder getippt wurden und das Spiel folglich unentschieden endete.""" # Hätte das Spiel nicht unentschieden geendet, so hätte die Funktion setSymbol entsprechend Rückmeldung gegeben. # UNVERAENDERT (aus Konsolenfassung) for i in board: # jede Zeile if FIELD_EMPTY in i: # -> noch unbelegtes Feld gefunden -> nicht fertig return False # else (nicht notwendig) return True # def inputInt(description:str, minimumValue:int, maximumValue:int) -> int: # ENTFERNT (da nicht benötigt) def inputCoordinates(windowSurface:pygame.Surface, board:list) -> tuple: """Diese Funktion wartet darauf, dass der Nutzer auf ein Feld klickt und gibt entsprechend ein Tupel zurück, was die zugehörigen Indizes (Koordinaten) im Spielfeld board enthält, sofern diese Position noch nicht belegt ist. Die Rückgabe erfolgt in der Form (Zeile, Spalte)""" # VERAENDERT while True: for event in pygame.event.get(): eventType = event.type if eventType == pygame.QUIT: pygame.quit() sys.exit() # Beenden elif eventType == pygame.MOUSEBUTTONDOWN: # Maustaste wurde gedrückt # Jetzt holen wir uns die Koordinaten, die geklickt wurde. Dazu greifen wir auf das Attribut (Variable) # dict in einem event zu. Jedes event hat ein solches Attribut, welches ein Dictionary ist. Ein Event # vom Typ MouseButtonDown besitzt dabei immer den Eintrag pos, das heißt: info = event.dict # info ist ein Dictionary von Informationen (bei MouseButtonDown gibt es button und pos) pos = info["pos"] # pos enthält nun die geklickte Position als TUPEL (x, y) # Berechnung des Indizes aus den Koordinaten: column = pos[0] // (SIZE_FIELD+LINE_THICKNESS) # Spaltenindex mit Ganzzahliger Division von x-Koord. berechnen if pos[0] % (SIZE_FIELD+LINE_THICKNESS) <= LINE_THICKNESS: # Der Modulo-Operator ist das Gegenstück zu column. D. h. die Divison von Position durch SIZE_FIELD+LINE... # liefert column Rest "Modulo-Ergebnis". Wenn das Ergebnis der Modulo-Rechnung nun kleiner ist als die # Breite einer Linie, dann wurde auf eine Linie geklickt und es wird entsprechend wieder gewartet. continue row = pos[1] // (SIZE_FIELD+LINE_THICKNESS) # Zeilenindex mit Ganzzahliger Division von y-Koord. berechnen if pos[1] % (SIZE_FIELD+LINE_THICKNESS) <= LINE_THICKNESS: continue if row >= len(board) or column >= len(board[row]) or \ board[row][column] != FIELD_EMPTY: # Index ungütltig (bei zu großem Spielfeld) oder bereits belegt continue return (row, column) def waitForKeyPressed(windowSurface:pygame.Surface): """Wartet, bis der Nutzer klickt oder eine Taste drückt.""" # NEU (nicht in Konsolenfassung vorhanden) while True: for event in pygame.event.get(): eventType = event.type if eventType == pygame.QUIT: pygame.quit() sys.exit() # Beenden elif eventType == pygame.MOUSEBUTTONDOWN or eventType == pygame.KEYDOWN: return def displayCenteredMultilineString(windowSurface:pygame.Surface, font:pygame.font.Font, lines:list): """Diese Funktion zeichnet mehrere Zeichenketten so im Abstand von distance untereinander auf die Oberfläche windowSurface, dass das Gesamtbild zentriert ist. Die Oberfläche wird entsprechend aktualisiert.""" # NEU (nicht in Konsolenfassung vorhanden) windowSurface.fill(COLOR_BACKGROUND) # Oberfläche leeren centerx = windowSurface.get_rect().centerx # Horizontale Mitte currenty = windowSurface.get_rect().centery - font.get_height()*(len(lines)-1)/2 # Vertikaler Anfang (ganz oben) for i in range(len(lines)): label = font.render(lines[i], True, COLOR_FOREGROUND, COLOR_BACKGROUND) # Schriftzug labelRect = label.get_rect() # umgebendes Rechteck zwischenspeichern, um Text zu zentrieren labelRect.centerx = centerx # Text horizontal zentrieren labelRect.centery = currenty currenty += font.get_height() windowSurface.blit(label, labelRect) # Text anzeigen pygame.display.update() def wantsRematch(windowSurface:pygame.Surface) -> bool: """Fragt nach, ob eine weitere Spielrunde erwünscht wird und gibt in diesem Fall True zurück, sonst False.""" # Oberfläche vorbereiten windowSurface.convert(pygame.display.set_mode(DEFAULT_SIZE)) # Größe ändern windowSurface.fill(COLOR_BACKGROUND) font = pygame.font.SysFont(None, FONT_SIZE) # Schrift windowCenterX = windowSurface.get_rect().centerx windowCenterY = windowSurface.get_rect().centery # Frage question = font.render("Neue Runde, neues Glück?", True, COLOR_FOREGROUND, COLOR_BACKGROUND) # Schriftzug questionRect = question.get_rect() # umgebendes Rechteck zwischenspeichern, um Text zu zentrieren questionRect.center = (windowCenterX, windowCenterY-font.get_height()) windowSurface.blit(question, questionRect) # Text anzeigen # "Nochmal"-Button buttonRematchText = font.render("Nochmal", True, COLOR_BACKGROUND) # Schriftzug buttonRematchTextRect = buttonRematchText.get_rect() # umgebendes Rechteck zwischenspeichern, um Text zu zentrieren buttonRematchTextRect.center = (windowCenterX*0.6, windowCenterY+font.get_height()) buttonRematch = pygame.draw.rect(windowSurface, COLOR_GREEN, (buttonRematchTextRect.left-10, buttonRematchTextRect.top-10, buttonRematchTextRect.width+20, buttonRematchTextRect.height+20)) windowSurface.blit(buttonRematchText, buttonRematchTextRect) # Text anzeigen # "Beenden"-Button buttonExitText = font.render("Beenden", True, COLOR_BACKGROUND) # Schriftzug buttonExitTextRect = buttonExitText.get_rect() # umgebendes Rechteck zwischenspeichern, um Text zu zentrieren buttonExitTextRect.center = (windowCenterX*1.4, windowCenterY+font.get_height()) buttonExit = pygame.draw.rect(windowSurface, COLOR_RED, (buttonExitTextRect.left-10, buttonExitTextRect.top-10, buttonExitTextRect.width+20, buttonExitTextRect.height+20)) windowSurface.blit(buttonExitText, buttonExitTextRect) # Text anzeigen pygame.display.update() # Sichtbar machen while True: # Auf Events warten for event in pygame.event.get(): eventType = event.type if eventType == pygame.QUIT: pygame.quit() sys.exit() # Beenden elif eventType == pygame.MOUSEBUTTONDOWN: # Wir holen uns den geklickten Punkt info = event.dict # info ist ein Dictionary von Informationen (bei MouseButtonDown gibt es button und pos) pos = info["pos"] # pos enthält nun die geklickte Position als TUPEL (x, y) if buttonRematch.collidepoint(pos): # -> Nochmal Button gedrückt return True elif buttonExit.collidepoint(pos): # -> Beenden Button gedrückt return False def playGame(windowSurface:pygame.Surface): """Führt ein (einziges) TicTacToe-Spiel durch (-> Hauptfunktion).""" # VERAENDERT size = 3 # hier konstant (könnte geändert werden) board = createEmptyBoard(size) # erstellt leeres Spielfeld der Größe size currentPlayer = random.randint(0, 1) # legt den ersten Spieler fest (0 und 1 möglich) # Startspieler-Benachrichtigung für 3s anzeigen displayCenteredMultilineString(windowSurface, pygame.font.SysFont(None, FONT_SIZE), ["Es beginnt Spieler " + str(currentPlayer+1), "mit dem Symbol " + str(FIELD_SYMBOL[currentPlayer] + "."), "", "Taste drücken, um zu starten."]) waitForKeyPressed(windowSurface) # Warten, bis Spieler Taste drückt/klickt # Nachdem die Größe des Fensters zuvor nicht bekannt war (da size nicht bekannt war) muss die Größe geändert werden, # indem ein neues Fenster erstellt wird. windowSurface.convert(pygame.display.set_mode((len(board) * (SIZE_FIELD + LINE_THICKNESS) + LINE_THICKNESS, len(board) * (SIZE_FIELD + LINE_THICKNESS) + LINE_THICKNESS))) windowSurface.fill(COLOR_FOREGROUND) # Warum FOREGROUND? Siehe Kommentar in displayBoard() displayBoard(board) # gibt das Spielfeld erstmalig aus (leer; Fencepost-Problem) while not isFinished(board): # solange noch eine Position frei ist (kein Unentschieden) coord = inputCoordinates(windowSurface, board) if setSymbol(windowSurface, board, coord, FIELD_SYMBOL[currentPlayer]): # -> setSymbol hat True geliefert, d. h. der aktuelle Spieler hat durch diesen Zug gewonnen # Gewinnbenachrichtigung anzeigen displayCenteredMultilineString(windowSurface, pygame.font.SysFont(None, FONT_SIZE), ["Herzlichen Glückwunsch,", "Spieler " + str(currentPlayer+1) + "!", "", "Du hast gewonnen!", "", "Taste drücken..."]) waitForKeyPressed(windowSurface) break # -> else-Teil der while-Schleife wird nicht ausgeführt currentPlayer = (currentPlayer+1) % 2 # nächster Spieler (0->1, 1->0) else: # -> falls die while-Schleife nicht durch break verlassen wurde und die Bedingung nicht mehr wahr ist displayCenteredMultilineString(windowSurface, pygame.font.SysFont(None, FONT_SIZE), ["Unentschieden!", "", "Taste drücken..."]) waitForKeyPressed(windowSurface) def play(): """Führt so viele Spiele aus, wie gewünscht sind.""" # VERAENDERT pygame.init() # PyGame initialisieren # Fenster (Surface) erzeugen pygame.display.set_caption("TicTacToe") windowSurface = pygame.display.set_mode((400, 150)) windowSurface.fill(COLOR_BACKGROUND) # Willkommensnachricht anzeigen displayCenteredMultilineString(windowSurface, pygame.font.SysFont(None, FONT_SIZE), ["Willkommen zu TicTacToe", "Spielspaß für jedermann!", "", "Taste drücken, um fortzufahren."]) waitForKeyPressed(windowSurface) # Warten, bis Spieler Taste drückt/klickt # Spiele-Schleife (bis nicht mehr gespielt werden will) while True: # Schleife wird durch break verlassen playGame(windowSurface) # ein Spiel spielen if not wantsRematch(windowSurface): break pygame.quit() # Ende play() # ruft die Funktion play auf