Tic-Tac-Toe-Spiel mit Pygame

Aug 22 2020

Ich habe ein Tic-Tac-Toe-Spiel mit Pygame programmiert. Es ist das erste Spiel, das ich mache. Ich würde mich sehr über Ihre Kommentare zum Codierungsstil und zu möglichen Verbesserungen freuen.

import pygame
import graphics # External module graphics.py
import logic    # External module logic.py

# Setting up window
HEIGHT = 600
WIDTH = 480
FPS = 30
TITLE = "Tic-tac-toe"

# Defining colors
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)

# Initializing pygame and create window
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption(TITLE)
clock = pygame.time.Clock()

# Game settings
TOP = 100
SIZE = 450
CELL = SIZE // 3
CPU_SPEED = FPS // 2


class TicTacToe():
    """Main tic-tac-toe game object.

    Attributes
    ----------
    board : graphics.Board
        pygame.Sprite object. Consists mainly of the lines that make
        up the grid.
    board_logic : logic.Board
        Object that defines the game logic and AI.
    cells : 3 x 3 nested list.
        List containing individual graphics.Cell objects.
        Each `Cell` object is a cell sprite.
    player_score : graphics.Text
        Sprite object that shows the player score.
    cpu_score : graphics.Text
        Sprite object that shows the cpu score.
    end_message : graphics.Text
        Sprite object that shows a message when the game ends:
        win/lose/tie.
    player_play : bool
        Determine when it's the player's turn to play.
    check : bool
        Determine when the check the end of the game.
        Set to `True` after each player or cpu move.
    reset : bool
        Determines when to reset the board after the end of game.
    timer : int
        Counter to limit cpu moves speed.
    timer2 : int
        Counter to limit end game reactions speed.

    Methods
    -------
    add_to_group(sprites_group)
        Add all of the sprite objects of the `TicTacToe` object
        to `sprites_group`.
    create_cells(CELL)
        Create 9 `graphics.Cell` objects of size `SIZE` x `SIZE`
        and append them to `self.cells`.
    update()
        Check if the game has ended or a player has won. Calls
        methods accordingly. Verify when to check with `self.check`
        and limit frequency using `self.timer2`.
        Verifies it's the computer's turn to play by checking
        `self.player_turn`. Calls `computer_turn` method when
        corresponds. Limits computer's play speed using
        `self.timer`.
    computer_turn()
        Use `graphics.board_logic.computer_turn()` method to determine
        next case to fill.
        Fill cell accordingly and update to check board end game
        conditions and player's turn.
    reset_game()
        Reset `graphics.Board` to delete extra lines on the board.
        Reset all `graphics.Cell` sprites to a blank surface.
        Reset `self.end_message` to show an empty string.
    update_score()
        Add one point to the corresponding current player score.
        Either `self.cpu_score` or `self.player_score`.
    win_message()
        Modify `self.end_message` to show a win or lose message.
    tie_message()
        Modify `self.end_message` to show a tied game message.
    get_cells():
        Return `self.cells` containing all the `graphics.Cell`
        sprite objects.
    is_player_turn():
        Return `True` when player's turn conditions are met.
        `False` otherwise.
    to_reset():
        Return `self.reset` attribute. Use to determine when
        to reset the game board.

    """

    def __init__(self):
        self.board = graphics.Board()
        self.board_logic = logic.Board()
        self.cells = self.create_cells(CELL)
        self.player_score = graphics.Text('P1 : 0',
                                          (WIDTH * 4 // 5, TOP * 1 // 3))
        self.cpu_score = graphics.Text('CPU : 0',
                                       (WIDTH * 4 // 5, TOP * 2 // 3))
        self.end_message = graphics.Text('',
                                         (WIDTH * 2 // 5, TOP // 2))
        self.player_play = False
        self.check = False
        self.reset = False
        self.timer = 0
        self.timer2 = 0

    def add_to_group(self, sprites_group):
        sprites_group.add(self.player_score)
        sprites_group.add(self.cpu_score)
        sprites_group.add(self.end_message)
        for cell in self.cells:
            sprites_group.add(cell)
        sprites_group.add(self.board)

    def create_cells(self, CELL=CELL):
        cells = []
        for i in range(3):
            row = []
            for j in range(3):
                pos = (self.board.rect.left + j * CELL,
                       self.board.rect.top + i * CELL)
                row.append(graphics.Cell(pos))
            cells.append(row)
        return cells

    def update(self):
        if self.check:
            if self.timer2 > CPU_SPEED // 2:
                self.timer2 = 0
                self.check = False
                if self.board_logic.check_winner():
                    self.board.draw_triple(self.board_logic.win_line_pos())
                    self.win_message()
                    self.update_score()
                    self.reset = True
                elif self.board_logic.endgame():
                    self.tie_message()
                    self.reset = True
            else:
                self.timer2 += 1

        if self.timer < CPU_SPEED:
            self.timer += 1
        else:
            self.timer = 0
            if not self.is_player_turn() and not self.to_reset():
                self.computer_turn()

    def computer_turn(self):
        i, j = self.board_logic.computer_turn()
        # print(self.board_logic)
        self.cells[i][j].computer_fill()
        self.check = True
        self.player_play = True

    def player_turn(self, ij):
        self.timer = 0
        self.board_logic.user_turn(ij)
        i, j = ij
        self.cells[i][j].player_fill()
        self.player_play = False
        self.check = True

    def reset_game(self):
        self.board.new_board()
        self.board_logic.reset()
        self.end_message.write('')
        for i in range(3):
            for j in range(3):
                self.cells[i][j].reset()
        self.reset = False

    def update_score(self):
        if self.board_logic.current_player() == 'o':
            self.player_score.add1()
        else:
            self.cpu_score.add1()

    def win_message(self):
        if self.board_logic.current_player() == 'o':
            self.end_message.write('You win!')
        else:
            self.end_message.write('You lose!')

    def tie_message(self):
        self.end_message.write('     Tie!')

    def get_cells(self):
        return self.cells

    def is_player_turn(self):
        return self.player_play and not self.board_logic.check_winner()

    def to_reset(self):
        return self.reset


all_sprites = pygame.sprite.Group()
game = TicTacToe()
game.add_to_group(all_sprites)

# Game loop
running = True
while running:
    clock.tick(FPS)

    # Process input
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.MOUSEBUTTONUP:
            if game.to_reset():
                game.reset_game()
            elif game.is_player_turn():
                pos = pygame.mouse.get_pos()
                cells = game.get_cells()
                for i in range(3):
                    for j in range(3):
                        if cells[i][j].hits(pos):
                            game.player_turn((i, j))

    game.update()

    # Draw / render
    screen.fill(BLACK)
    all_sprites.draw(screen)
    pygame.display.flip()

pygame.quit()

Ich habe den Code in verschiedene Module aufgeteilt. Das graphicsModul enthält die Klassen zum Erstellen der verschiedenen Sprites, die vom Spiel verwendet werden (das Spielbrett, die Brettzellen und die Textnachrichten). Das logicModul wird für die Computer-KI verwendet.

Ich teile nur diesen "Spielteil" des Codes, da ich mich hier über Ihre Kommentare freuen würde. Wenn Sie die anderen Module überprüfen oder erhalten möchten, um das Spiel zu spielen, können Sie dies hier tun .

Vielen Dank für Ihre Zeit.

Antworten

2 MartinThoma Aug 23 2020 at 22:31

Statische Code-Analyse

Lassen Sie sich von mir durch einige meiner bevorzugten Tools zur Analyse statischen Codes führen :

  • Typanmerkungen : Sie verwenden keine. Sie sind fantastisch und jeder sollte sie für Python 3.6+ verwenden. Da niemand Python-Versionen unter 3.6 verwenden sollte, sollte nur jeder sie verwenden ^^
  • schwarz und isort : Für die allgemeine Formatierung verwende ich gerne den Autoformatter schwarz. Ihr Code ist bereits gut, wenn es um dieses / PEP8 geht
  • Flake8 : Kann Stilprüfungen durchführen. Beispielsweise sollte der Parameter to create_cellsKleinbuchstaben sein ( cellnicht CELL).
  • McCabe : Um zu überprüfen, ob es Teile gibt, die schwer zu verstehen sind, benutze ich python -m mccabe --min 5 yourscript.py. Es beschwert sich nur über die Spielschleife.

Andere Kommentare

Hauptfunktion

Normalerweise stelle ich gerne alles in Funktionen. Sie können die Spielschleife in eine mainFunktion einfügen und dann hinzufügen:

if __name__ == "__main__":
    main()

Dies hat den Vorteil, dass Sie Teile dieser Datei importieren können, ohne sie auszuführen. Dies erleichtert das Testen manchmal erheblich.

Eigenschaften und YAGNI

Python hat den Eigenschaftsdekorateur . In Ihrem Fall würde ich jedoch einfach entfernen get_cellsund .cellsdirekt darauf zugreifen . Es erleichtert das Lesen des Codes und wenn Sie ihn benötigen, können Sie die Eigenschaft später noch einführen.

Da ist YAGNI - du wirst es nicht brauchen. Erstellen Sie keine Abstraktionen, wenn Sie sie nicht benötigen. Die Methode to_resetscheint ein gutes Beispiel zu sein.

Docstring Style

Ihr docstring Stil ist sehr ähnlich zu numpydoc ; Vielleicht können Sie es ändern, um damit übereinzustimmen. Dies bedeutet insbesondere, dass Sie nicht alle Methoden innerhalb der Klasse docstring dokumentieren müssen. Stattdessen dokumentieren Sie die Methode innerhalb der Methode. Viele Editoren können dies dann auch verwenden, um Hilfe zur Methode anzuzeigen.

Benennung

Gute Namen sind schwer zu finden. Wir wollen Namen, die im Kontext klar sind, aber auch keine zu langen Namen im Java-Stil.

Hier sind einige Beispiele, die ich schwer zu verstehen finde:

  • timer2: Wenn Sie normalerweise Zahlen hinzufügen müssen, ist dies normalerweise ein Zeichen dafür, dass es kein guter Name ist
  • player_play: Sollte aufgerufen werden is_players_turn, aber das ist schon eine Methode
  • is_player_turn (): Ich bin mir nicht sicher, was self.board_logic.check_winnerich tue, daher bin ich mir nicht sicher , was ein guter Name wäre. Da diese Methode jedoch nur einmal aufgerufen wird, frage ich mich, ob die Methode selbst wirklich notwendig ist.

Aufzählungen

Ich weiß nicht, was self.board_logic.current_player() == "o"tut, aber vielleicht könnte die Methode eine Aufzählung zurückgeben und Sie könnten sie mit einer Aufzählung vergleichen? Zeichenfolgenvergleiche sind anfällig für Tippfehler