Gioco tris con Pygame

Aug 22 2020

Ho programmato un tris usando pygame. È il primo gioco che realizzo. Apprezzerei davvero qualsiasi commento tu abbia sullo stile di codifica e qualsiasi miglioramento che potrebbe essere fatto.

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()

Ho separato il codice in diversi moduli. Il graphicsmodulo include le classi per creare i diversi sprite usati dal gioco (il tabellone, le celle del tabellone e i messaggi di testo). Il logicmodulo è utilizzato per il computer AI.

Condivido solo questa "parte del gioco" del codice perché è qui che apprezzerei i tuoi commenti. Se desideri controllare gli altri moduli o ottenerli per poter giocare, puoi farlo qui .

La ringrazio molto per il vostro tempo.

Risposte

2 MartinThoma Aug 23 2020 at 22:31

Analisi statica del codice

Lascia che ti guidi attraverso alcuni dei miei strumenti preferiti di analisi del codice statico :

  • Scrivi annotazioni : non ne usi nessuna. Sono fantastici e tutti dovrebbero usarli per Python 3.6+. Poiché nessuno dovrebbe usare le versioni di Python precedenti alla 3.6, tutti dovrebbero usarle ^^
  • black and isort : per la formattazione generale, mi piace usare l'autoformatter nero. Il tuo codice è già buono quando si tratta di questo / PEP8
  • Flake8 : controlli di stile di maggio. Ad esempio, il parametro da a create_cellsdovrebbe essere minuscolo ( cell, non CELL).
  • McCabe : Per verificare se ci sono parti difficili da capire, utilizzo python -m mccabe --min 5 yourscript.py. Si lamenta solo del loop di gioco.

Altri commenti

funzione principale

Di solito mi piace mettere tutto in funzioni. Puoi inserire il loop di gioco in una mainfunzione e quindi aggiungere:

if __name__ == "__main__":
    main()

Questo ha il vantaggio che puoi importare parti di questo file senza eseguirlo. Questo rende i test a volte molto più facili.

Proprietà e YAGNI

Python ha il decoratore di proprietà . Nel tuo caso, tuttavia, rimuoverei get_cellse accedo .cellsdirettamente. Rende il codice più facile da leggere e se ne hai bisogno, puoi comunque introdurre la proprietà in un secondo momento.

C'è YAGNI - non ne avrai bisogno. Non creare astrazioni se non ne hai bisogno. Il metodo to_resetsembra essere un buon esempio.

Stile Docstring

Il tuo stile docstring è molto simile a numpydoc ; forse puoi cambiarlo per essere coerente con quello. Ciò significa in particolare che non è necessario documentare tutti i metodi all'interno della classe docstring. Invece, si documenta il metodo all'interno del metodo. Molti editor possono quindi utilizzarlo anche per mostrare aiuto sul metodo.

Denominazione

I buoni nomi sono difficili da trovare. Vogliamo nomi chiari nel contesto, ma anche nomi non troppo lunghi in stile Java.

Ecco alcuni esempi che trovo difficile da capire:

  • timer2: Di solito quando devi aggiungere numeri, questo è un segno che non è un buon nome
  • player_play: dovrebbe essere chiamato is_players_turn, ma questo è già un metodo
  • is_player_turn (): Non sono sicuro di cosa self.board_logic.check_winnerstia facendo, quindi non sono sicuro di quale sarebbe un buon nome. Ma poiché questo metodo viene chiamato solo una volta, mi chiedo se il metodo stesso sia davvero necessario.

Enumerazioni

Non so cosa self.board_logic.current_player() == "o"stia facendo, ma forse il metodo potrebbe restituire un enum e potresti confrontare con un enum? I confronti tra stringhe sono soggetti a errori di battitura