Реализация игры Gomoku (Connect 5) в pygame

Я попытался реализовать простую игру Гомоку (подключение 5) в pygame и хотел получить некоторую помощь. Пока что основные функции работают нормально, когда 2 игрока сражаются друг с другом.

Весь код

import numpy as np
import pygame
import sys
import math

# initialize the pygame program
pygame.init()

# static variables
ROW_COUNT = 15
COL_COUNT = 15

# define screen size
BLOCKSIZE = 50 # individual grid
S_WIDTH = COL_COUNT * BLOCKSIZE # screen width
S_HEIGHT = ROW_COUNT * BLOCKSIZE # screen height
PADDING_RIGHT = 200 # for game menu
SCREENSIZE = (S_WIDTH + PADDING_RIGHT,S_HEIGHT)
RADIUS = 20 # game piece radius

# colors
BLACK = (0,0,0)
WHITE = (255,255,255)
BROWN = (205,128,0)

# create a board array
def create_board(row, col):
    board = np.zeros((row,col))
    return board

# draw a board in pygame window
def draw_board(screen):
    for x in range(0,S_WIDTH,BLOCKSIZE):
        for y in range(0,S_HEIGHT,BLOCKSIZE):
            rect = pygame.Rect(x, y, BLOCKSIZE, BLOCKSIZE)
            pygame.draw.rect(screen,BROWN,rect)

    # draw inner grid lines
    # draw vertical lines
    for x in range(BLOCKSIZE // 2, S_WIDTH - BLOCKSIZE // 2 + BLOCKSIZE, BLOCKSIZE):
        line_start = (x, BLOCKSIZE // 2)
        line_end = (x,S_HEIGHT-BLOCKSIZE // 2)
        pygame.draw.line(screen, BLACK, line_start,line_end,2)

    # draw horizontal lines
    for y in range(BLOCKSIZE // 2, S_HEIGHT - BLOCKSIZE // 2 + BLOCKSIZE, BLOCKSIZE):
        line_start = (BLOCKSIZE // 2,y)
        line_end = (S_WIDTH-BLOCKSIZE // 2,y)
        pygame.draw.line(screen, BLACK, line_start,line_end,2)
    pygame.display.update()

# drop a piece
def drop_piece(board, row, col, piece):
    board[row][col] = piece

# draw a piece on board
def draw_piece(screen,board):
    # draw game pieces at mouse location
    for x in range(COL_COUNT):
        for y in range(ROW_COUNT):
            circle_pos = (x * BLOCKSIZE + BLOCKSIZE//2, y * BLOCKSIZE + BLOCKSIZE//2)
            if board[y][x] == 1:
                pygame.draw.circle(screen, BLACK, circle_pos, RADIUS)
            elif board[y][x] == 2:
                pygame.draw.circle(screen, WHITE, circle_pos, RADIUS)
    pygame.display.update()

# check if it is a valid location
def is_valid_loc(board, row, col):
    return board[row][col] == 0

# victory decision
def who_wins(board, piece):
    # check for horizontal win
    for c in range(COL_COUNT - 4):
        for r in range(ROW_COUNT):
            if board[r][c] == piece and board[r][c+1] == piece and board[r][c+2] == piece and board[r][c+3] == piece
                and board[r][c+4] == piece:
                return True

    # check for vertical win
    for c in range(COL_COUNT):
        for r in range(ROW_COUNT-4):
            if board[r][c] == piece and board[r+1][c] == piece and board[r+2][c] == piece and board[r+3][c] == piece
                and board[r+4][c] == piece:
                return True

    # check for positively sloped diagonal wih
    for c in range(COL_COUNT-4):
        for r in range(4,ROW_COUNT):
            if board[r][c] == piece and board[r-1][c+1] == piece and board[r-2][c+2] == piece and board[r-3][c+3] == piece
                and board[r-4][c+4] == piece:
                return True

    # check for negatively sloped diagonal win
    for c in range(COL_COUNT-4):
        for r in range(ROW_COUNT-4):
            if board[r][c] == piece and board[r+1][c+1] == piece and board[r+2][c+2] == piece and board[r+3][c+3] == piece
                and board[r+4][c+4] == piece:
                return True

def main():
    # game variables
    game_over = False
    turn = 0 # turn == 0 for player 1, turn == 1 for player 2
    piece_1 = 1 # black
    piece_2 = 2 # white

    # FPS
    FPS = 60
    frames_per_sec = pygame.time.Clock()

    # board 2D array
    board = create_board(ROW_COUNT,COL_COUNT)
    print(board)

    # game screen
    SCREEN = pygame.display.set_mode(SCREENSIZE)
    SCREEN.fill(WHITE)
    pygame.display.set_caption('Gomoku (Connet 5)')
    # icon = pygame.image.load('icon.png')
    # pygame.display.set_icon(icon)

    # font
    my_font = pygame.font.Font('freesansbold.ttf', 32)

    # text message
    label_1 = my_font.render('Black wins!', True, WHITE, BLACK)
    label_2 = my_font.render('White wins!', True, WHITE, BLACK)

    # display the screen
    draw_board(SCREEN)

    # game loop
    while not game_over:

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            elif event.type == pygame.MOUSEBUTTONDOWN:
                x_pos = event.pos[0]
                y_pos = event.pos[1]

                col = int(math.floor(x_pos / BLOCKSIZE))
                row = int(math.floor(y_pos / BLOCKSIZE))

                # turn decision, if black(1)/white(2) piece already placed, go back to the previous turn
                if board[row][col] == 1:
                    turn = 0
                if board[row][col] == 2:
                    turn = 1

                # Ask for Player 1 move
                if turn == 0:
                    # check if its a valid location then drop a piece
                    if is_valid_loc(board, row, col):
                        drop_piece(board, row, col, piece_1)
                        draw_piece(SCREEN,board)

                        if who_wins(board,piece_1):
                            print('Black wins!')
                            SCREEN.blit(label_1, (280,50))
                            pygame.display.update()
                            game_over = True

                # Ask for Player 2 move
                else:
                    # check if its a valid location then drop a piece
                    if is_valid_loc(board, row, col):
                        drop_piece(board, row, col, piece_2)
                        draw_piece(SCREEN,board)

                        if who_wins(board,piece_2):
                            print('White wins!')
                            SCREEN.blit(label_2, (280,50))
                            pygame.display.update()
                            game_over = True

                print(board)

                # increment turn
                turn += 1
                turn = turn % 2

                if game_over:
                    pygame.time.wait(4000)

        frames_per_sec.tick(FPS)

if __name__ == '__main__':
    main()

Скриншоты игрового процесса:
введите описание изображения здесь

введите описание изображения здесь

Комментарий

Кажется, игра работает нормально. Я переместил часть своего кода в main() так что я могу создать функцию перезапуска для следующего шага.

Также я думаю о создании некоторых дополнительных функций:

  • функция отмены: отменяет предыдущий ход
  • функция повтора: восстанавливает фрагмент, удаленный undo() функция
  • оставить след на текущем произведении. пример:
    введите описание изображения здесь

Любая критика / помощь приветствуются!

4 ответа
4

Я могу предложить небольшое улучшение, когда вы проверяете выигрыш с помощью кода:

        if board[r][c] == piece and board[r][c+1] == piece and board[r][c+2] == piece and board[r][c+3] == piece
            and board[r][c+4] == piece:

Вы проверяете, все ли указанные позиции равны piece, чтобы вы могли написать помощника:

def all_equal(xs, y):
    return all(x == y for x in xs)

И повторно используйте эту функцию для различных проверок, уменьшая повторение этой логической концепции и делая код более чистым и читаемым.


Другой способ уменьшить повторение — в этом блоке кода:

            if turn == 0:
                # check if its a valid location then drop a piece
                if is_valid_loc(board, row, col):
                    drop_piece(board, row, col, piece_1)
                    draw_piece(SCREEN,board)

                    if who_wins(board,piece_1):
                        print('Black wins!')
                        SCREEN.blit(label_1, (280,50))
                        pygame.display.update()
                        game_over = True

            # Ask for Player 2 move
            else:
                # check if its a valid location then drop a piece
                if is_valid_loc(board, row, col):
                    drop_piece(board, row, col, piece_2)
                    draw_piece(SCREEN,board)

                    if who_wins(board,piece_2):
                        print('White wins!')
                        SCREEN.blit(label_2, (280,50))
                        pygame.display.update()
                        game_over = True

Ты мог бы сделать

if turn == 0:
    piece = piece_1
    name = "Black"
    label = label_1
else:
    piece = piece_2
    name = "White"
    label = label_2

А потом:

                if is_valid_loc(board, row, col):
                    drop_piece(board, row, col, piece)
                    draw_piece(SCREEN,board)

                    if who_wins(board,piece):
                        print(name + ' wins!')
                        SCREEN.blit(label, (280,50))
                        pygame.display.update()
                        game_over = True

Это проясняет, что к игрокам применяется одна и та же программная логика, и только некоторые переменные меняются между обработкой ходов двух игроков.


В целом код выглядит действительно чистым, хорошее разделение задач и четкие константы в начале.

    Здесь есть что улучшить, но это веселая и функциональная игра, и вы пока неплохо справились.

    • Не звони init из глобального пространства имен
    • Этот комментарий:
    # create a board array
    def create_board(row, col):
    

    и все твои другие подобные комментарии хуже, чем вообще без комментариев. Даже если бы этот комментарий был информативным, вы бы хотели переместить его в стандартный """docstring""" в первой строке внутри метода. В качестве бонуса некоторые из ваших комментариев не только лишние, но и ложь, например # draw game pieces at mouse location. Это делает нет рисовать в месте нахождения мыши; он отрисовывается в каждом месте игровой сетки.

    • В настоящее время ваш массив Numpy имеет тип с плавающей запятой, что неуместно. Вместо этого установите целочисленный тип.

    • Рассмотрите возможность использования обозначения именованных аргументов для нечетких величин, таких как ширина вашей линии.

    • СУХОЙ (не повторяйся) набери этот код:

            if board[y][x] == 1:
                pygame.draw.circle(screen, BLACK, circle_pos, RADIUS)
            elif board[y][x] == 2:
                pygame.draw.circle(screen, WHITE, circle_pos, RADIUS)
      

    так что звонок circle пишется только один раз

    • Рекомендация от @Setris — гораздо лучший способ проверить победу. Даже если вы сохраните исчерпывающий метод, используйте нарезку массива Numpy, чтобы упростить себе жизнь.
    • Наличие флага завершения цикла, например game_over обычно не лучшая идея, и это не исключение. Вы можете просто сломать или вернуться, если у вас есть достаточно конкретный метод.
    • Не все так полезно для print что-нибудь. Как только ваш код заработает, я удалю их все.
    • SCREEN.fill(WHITE) не помогает. Если цвет вашей доски коричневый, почему бы просто не залить коричневым? Измените размер окна в соответствии с размером доски (я этого не показывал). Кроме того, рисунок на доске должен полностью потерять свои первые две петли — вы можете нарисовать один прямоугольник, а не каждую плитку.
    • SCREEN должно быть в нижнем регистре.
    • Не отслеживайте время или FPS. У вас нет анимации. Не надо event.get; вместо event.wait. Также не вешайте программу на несколько секунд, а затем выйдите; позволить пользователю выйти самим, когда он захочет.
    • Распакуйте кортеж event.pos вместо того, чтобы индексировать его.
    • Ваш turn должны использовать те же определения стоимости, что и ваши изделия. Поступать иначе — бесполезно.
    • Почему вы звоните обоим is_valid_loc а также делать # turn decision, if black(1)/white(2) piece already placed, go back to the previous turn? Я не понимаю этого. Кажется, они делают то же самое.
    • Ваша логика хода для каждого игрока очень однообразна и должна быть написана только один раз.
    • Для draw_pieceвместо того, чтобы перебирать каждый элемент массива, вызовите np.argwhere
    • Нет смысла предварительно настраивать две разные метки, только чтобы отображать одну. Вы можете создать и показать этикетку на лету.
    • Добавьте подсказки типа.

    Предложенный

    Я бы пошел дальше этого, но это должно помочь вам начать:

    from typing import Tuple
    
    import numpy as np
    import math
    import pygame
    from pygame import display, draw, font, Surface, QUIT, MOUSEBUTTONDOWN
    
    # static variables
    ROW_COUNT = 15
    COL_COUNT = 15
    
    EMPTY = 0
    BLACK_PIECE = 1  # black
    WHITE_PIECE = 2  # white
    PIECES = (BLACK_PIECE, WHITE_PIECE)
    
    # define screen size
    BLOCKSIZE = 50                    # individual grid
    S_WIDTH = COL_COUNT * BLOCKSIZE   # screen width
    S_HEIGHT = ROW_COUNT * BLOCKSIZE  # screen height
    PADDING_RIGHT = 200               # for game menu
    SCREENSIZE = (S_WIDTH + PADDING_RIGHT, S_HEIGHT)
    RADIUS = 20                       # game piece radius
    
    # colors
    BLACK = (0, 0, 0)
    WHITE = (255, 255, 255)
    BROWN = (205, 128, 0)
    PIECE_COLOURS = (BLACK, WHITE)
    
    
    def create_board(row: int, col: int) -> np.ndarray:
        return np.zeros((row, col), dtype=np.int32)
    
    
    def draw_board(screen: Surface) -> None:
        screen.fill(BROWN)
    
        # draw vertical inner grid lines
        for x in range(BLOCKSIZE // 2, S_WIDTH + BLOCKSIZE // 2, BLOCKSIZE):
            draw.line(
                screen, BLACK,
                start_pos=(x, BLOCKSIZE // 2),
                end_pos=(x, S_HEIGHT-BLOCKSIZE // 2),
                width=2,
            )
    
        # draw horizontal inner lines
        for y in range(BLOCKSIZE // 2, S_HEIGHT + BLOCKSIZE // 2, BLOCKSIZE):
            draw.line(
                screen, BLACK,
                start_pos=(BLOCKSIZE // 2, y),
                end_pos=(S_WIDTH - BLOCKSIZE // 2, y),
                width=2,
            )
    
    
    def drop_piece(board: np.ndarray, row: int, col: int, piece: int) -> None:
        board[row][col] = piece
    
    
    def pixel_from_grid(x: int, y: int) -> Tuple[int, int]:
        return (
            x * BLOCKSIZE + BLOCKSIZE // 2,
            y * BLOCKSIZE + BLOCKSIZE // 2,
        )
    
    
    def draw_piece(screen: Surface, board: np.ndarray) -> None:
        for piece, colour in zip(PIECES, PIECE_COLOURS):
            for y, x in np.argwhere(board == piece):
                draw.circle(
                    screen, PIECE_COLOURS[board[y][x] - 1],
                    pixel_from_grid(x, y), RADIUS,
                )
        display.update()
    
    
    def is_valid_loc(board: np.ndarray, row: int, col: int) -> bool:
        return board[row][col] == EMPTY
    
    
    def who_wins(board: np.ndarray, piece: int) -> bool:
        # check for horizontal win
        for c in range(COL_COUNT - 4):
            for r in range(ROW_COUNT):
                if np.all(board[r, c:c+5] == piece):
                    return True
    
        # check for vertical win
        for c in range(COL_COUNT):
            for r in range(ROW_COUNT - 4):
                if np.all(board[r:r+5, c] == piece):
                    return True
    
        # check for positively sloped diagonal wih
        for c in range(COL_COUNT - 4):
            for r in range(4, ROW_COUNT):
                if (
                    board[r, c] == piece
                    and board[r-1, c+1] == piece
                    and board[r-2, c+2] == piece
                    and board[r-3, c+3] == piece
                    and board[r-4, c+4] == piece
                ):
                    return True
    
        # check for negatively sloped diagonal win
        for c in range(COL_COUNT - 4):
            for r in range(ROW_COUNT - 4):
                if (
                    board[r, c] == piece
                    and board[r+1, c+1] == piece
                    and board[r+2, c+2] == piece
                    and board[r+3, c+3] == piece
                    and board[r+4, c+4] == piece
                ):
                    return True
    
        return False
    
    
    def setup_gui() -> Surface:
        screen = display.set_mode(SCREENSIZE)
        display.set_caption('Gomoku (Connet 5)')
        # icon = pygame.image.load('icon.png')
        # pygame.display.set_icon(icon)
    
        draw_board(screen)
        display.update()
        return screen
    
    
    def end_banner(screen: Surface, piece: str) -> None:
        my_font = font.Font('freesansbold.ttf', 32)
        label = my_font.render(f'{piece} wins!', True, WHITE, BLACK)
        screen.blit(label, (280, 50))
        display.update()
    
    
    def game(screen: Surface) -> None:
        turn = BLACK_PIECE
    
        # board 2D array
        board = create_board(ROW_COUNT, COL_COUNT)
    
        # game loop
        while True:
            event = pygame.event.wait()
            if event.type == QUIT:
                return
    
            elif event.type == MOUSEBUTTONDOWN:
                x_pos, y_pos = event.pos
                col = math.floor(x_pos / BLOCKSIZE)
                row = math.floor(y_pos / BLOCKSIZE)
    
                if not is_valid_loc(board, row, col):
                    continue
    
                drop_piece(board, row, col, turn)
                draw_piece(screen, board)
    
                if who_wins(board, turn):
                    name="Black" if turn == BLACK_PIECE else 'White'
                    end_banner(screen, name)
                    return
    
                turn = 3 - turn
    
    
    def main() -> None:
        # initialize the pygame program
        pygame.init()
        try:
            screen = setup_gui()
            game(screen)
            while pygame.event.wait().type != QUIT:
                pass
        finally:
            pygame.quit()
    
    
    if __name__ == '__main__':
        main()
    

      В целом, игра работает хорошо, и дизайнерские решения соответствуют размеру и назначению программы. Имена переменных понятны, есть некоторое разделение между графическим интерфейсом и игровой логикой, и вы приложили усилия для использования функций. Некоторые из моих предложений могут быть преждевременными, но если вы планируете расширить программу, добавив такую ​​функцию, как ИИ или отмена / повтор ходов, я бы предложил редизайн.

      Крушение

      На моем экране в правой части игры отображается белая полоса. Если щелкнуть по нему, drop_a_piece поднимает непойманный IndexError потому что у него нет проверки границ.

      Избегайте лишних комментариев

      Многие комментарии излишни:

      # drop a piece
      def drop_piece(board, row, col, piece):
          board[row][col] = piece
      

      По названию функции я могу сказать, что это бросает кусок. Подобные комментарии оскорбительны для интеллекта читателя. Если вы хотите задокументировать свои функции, способ Python — использовать строки документации и (необязательно) доктесты для обеспечения соблюдения контрактов.

      Случайный # комментарии допустимы, если они действительно предлагают глубокое понимание кода, которое в противном случае не обязательно очевидно. У вас есть несколько таких, например

      # turn decision, if black(1)/white(2) piece already placed, go back to the previous turn
      

      … но убедитесь, что эти комментарии не являются костылями для излишне запутанного кода.

      Избегайте комментариев как замены правильных абстракций кода

      Другие комментарии используются вместо пространств имен или функций, реальных программных структур, которые организуют логику.

      Например, что-то очень простое:

      # colors
      BLACK = (0,0,0)
      WHITE = (255,255,255)
      BROWN = (205,128,0)
      

      может быть:

      class Colors:
          BLACK = 0, 0, 0
          WHITE = 255, 255, 255
          BROWN = 205, 128, 0
      

      Пример комментария, который, кажется, пытается обозначить функцию, находится в who_wins

      # check for vertical win
      for c in range(COL_COUNT):
      # ... more code ...
      

      который с таким же успехом можно разбить на функцию с именем check_vertical_win. После этого до его заключения, who_wins будет выглядеть так:

      def who_wins(board, piece):
          return (
              check_left_diagonal_win(board, piece) or
              check_right_diagonal_win(board, piece) or
              check_vertical_win(board, piece) or
              check_horizontal_win(board, piece)
          )
      

      Теперь проблема в том, что board а также piece должны проходить через несколько уровней этих функций в стиле C, не относящихся к ООП, о которых я расскажу позже.

      Сделайте все возможное с NumPy

      Если вы используете NumPy, вы можете использовать его в полной мере! Единственный вызов NumPy во всем коде — board = np.zeros((row,col)). Это похоже на упущенную возможность — вы должны иметь возможность векторизовать многие свои операции и написать идиоматический код NumPy. Это может показаться преждевременным, но если вы добавите ИИ, который должен перемещаться по дереву игры, NumPy может предложить потенциально значительное повышение эффективности. Надеемся, что читаемость улучшится.

      Если вы не собираетесь использовать NumPy как нечто большее, чем прославленный форматтер печати 2d-списков, я бы отказался от зависимости и добавил симпатичный помощник печати из нескольких строк или удалил print полностью, поскольку это игра с графическим интерфейсом.

      Сделайте все возможное, чтобы сделать его модулем

      Код имеет типичный код «драйвера», типичный для модулей, которые предполагается импортировать как отдельный пакет:

      if __name__ == '__main__':
          main()
      

      но из-за всех переменных и функций scattershot за пределами main, это не очень удобный модуль, который можно было бы использовать в случае импорта, поэтому это кажется ложным обещанием.

      main должно быть очень простым — ваш клиент использует код как библиотеку черного ящика (подумайте об этом, как о том, как вы используете NumPy), поэтому вы просите их реализовать 80 строк, чтобы использовать ваши функции для реализации игры Gomoku. Ясно, что слишком много игровой логики было сброшено в main. В идеале вы должны свести это к паре строк или около того, которые работают как черный ящик, возможно, с некоторыми ручками настройки, например:

      if __name__ == '__main__':
          game = Gomoku(some_config)
          game.play()
      

      Создание класса для работы с состоянием платы или, по крайней мере, с использованием модулей и пространством имен ваших функций и данных может помочь решить проблему необходимости передачи board к вашим функциям и постоянно нарушайте инкапсуляцию, чтобы читать глобальные файлы.

      Я бы предпочел Gomoku класс, который запускает графический интерфейс и игровой цикл, который является клиентом GomokuPosition класс, который включает доску, слой и методы, которые работают с состоянием игры.

      Снижение цикломатической сложности

      Вы довольно хорошо справляетесь с использованием довольно небольших и однозначных функций ответственности до тех пор, пока main, когда сложность взрывается. Всего имеется 13 ветвей и петель, которые вмещаются до 6 глубиной:

      while not game_over:
         for event in pygame.event.get():
             elif event.type == pygame.MOUSEBUTTONDOWN:
                 if turn == 0:
                     if is_valid_loc(board, row, col):
                         if who_wins(board,piece_1):
                             # you may ask yourself, "well... how did I get here?"
      

      Об этом сложно рассуждать, и это должно быть несколько одноцелевых функций, сохраняющих пользовательский интерфейс и игровую логику как можно более раздельными (полезно, если вы планируете добавить ИИ).

      Не превышайте глубину вложения 2-3 блоков и держите if/else цепочки длиной не более 2–3 ветвей, иначе функция будет слишком сложной.

      Избегайте длинных очередей

      Мне нужно прокрутить по горизонтали, чтобы прочитать такие строки, как:

      if board[r][c] == piece and board[r+1][c+1] == piece and board[r+2][c+2] == piece and board[r+3][c+3] == piece
          and board[r+4][c+4] == piece:
      

      Пожалуйста, придерживайтесь ширины 80 символов. Подобные проверки можно разбить на функции или на несколько строк с помощью круглых скобок (почти всегда избегайте обратной косой черты для продолжения строки):

      if (
          board[r][c] == piece and 
          board[r+1][c+1] == piece and  
          board[r+2][c+2] == piece and 
          board[r+3][c+3] == piece and
          board[r+4][c+4] == piece
      ):
      

      (Здесь используется стиль «грустного лица», пропагандируемый Чернить).

      А еще лучше, здесь есть шаблон, который вы можете извлечь и использовать с NumPy:

      if np.all(board.diagonal(r)[c:c+5] == piece):
      

      Это не просто вопрос элегантности, итерации на более высоком уровне абстракции вместо сильной зависимости от низкоуровневого индексирования /range() должно уменьшить количество ошибок. Кодовая база с тоннами подробного ручного индексирования имеет тенденцию скрывать тонкие одиночные ошибки, ошибки копирования-вставки и опечатки, и их трудно доверять и проверять.

      Если вы так же неумелы в NumPy, как и я, я обычно могу найти правильный вектор, выполнив поиск таких вещей, как «numpy diagonal slice». В 99% случаев встроенный или однострочный, который помещает мой императивный Python for код цикла к стыду. Если вы решите не использовать NumPy, вы все равно можете использовать срезы (и часто itertools), чтобы попытаться свести к минимуму подверженные ошибкам и неидиоматические range звонки и индексы.

      СУХОЙ похожий код

      Куски кода # Ask for Player 1 move а также # Ask for Player 2 move почти такие же с очевидными параметрами: piece_num а также on_win_message. Такой код готов к работе. После этого рефакторинга они по-прежнему объединяют игровую логику и представление / пользовательский интерфейс, так что я бы разделил их.

      Используйте промежуточные переменные для упрощения кода

      Рассмотреть возможность:

      for x in range(BLOCKSIZE // 2, S_WIDTH - BLOCKSIZE // 2 + BLOCKSIZE, BLOCKSIZE):
          line_start = (x, BLOCKSIZE // 2)
          line_end = (x,S_HEIGHT-BLOCKSIZE // 2)
          pygame.draw.line(screen, BLACK, line_start,line_end,2)
      

      Предполагая, что здесь мы не будем использовать NumPy, BLOCKSIZE // 2 выполняется неоднократно, и код обычно кричащий / COBOL-y и неприятен для глаз. Даже использование двух локальных переменных дает небольшое облегчение, обеспечивая удобочитаемость по горизонтали для нескольких дополнительных вертикальных линий:

      half = BLOCKSIZE // 2
      end = S_WIDTH - half + BLOCKSIZE
      
      for x in range(half, end, BLOCKSIZE):
          line_start = x, half
          line_end = x, S_HEIGHT - half
          pygame.draw.line(screen, BLACK, line_start, line_end, width=2)
      

      Я пробрался width=2, потому что иначе трудно сказать, что означает этот параметр. Как правило, использование именованных параметров — отличная идея.

      Избегайте магических переменных / литералов

      Код

      piece_1 = 1 # black
      piece_2 = 2 # white
      

      похоже на попытку перечисления, но почему бы и нет BLACK = 0 а также WHITE = 1? Пустой квадрат равен 0, поэтому при дальнейшем рассмотрении это имеет смысл, но я не уверен, что эти части должны существовать; единственные места в коде, которые они используются:

      drop_piece(board, row, col, piece_1)
      draw_piece(SCREEN,board)
      
      if who_wins(board,piece_1):
      

      и другое место, где они заменены на магические константы:

      if board[row][col] == 1: # <-- really piece_1
          turn = 0
      if board[row][col] == 2: # <-- really piece_2
          turn = 1
      

      Все это кажется немного спонтанным.

      Точно так же жестко запрограммированный литерал 4 появляется много раз внутри who_wins. Непонятно, почему это не было сделано константой, как это было сделано для других номеров конфигурации платы.

      Упростите проверку выигрыша

      Нет необходимости сканировать всю доску, чтобы проверить выигрыш. Вы можете отсчитывать в 4 направлениях от последнего хода, считая совпадающие ячейки наружу, пока не достигнете желаемой суммы в одном из направлений.

      Вы можете использовать подсчет количества сделанных на данный момент ходов, чтобы пропустить всю проверку выигрыша, если на доске просто недостаточно фигур для того, чтобы выигрыш был возможен. Практически без затрат вы получите значительный прирост производительности, если добавите ИИ, который будет искать в дереве позже.

      Мелкие придирки

      • В col = int(math.floor(x_pos / BLOCKSIZE)), math.floor уже возвращает int так что вы можете пропустить дополнительный звонок.

      • Распаковка может быть шикарной:

        x_pos = event.pos[0]
        y_pos = event.pos[1]
        

        идет в x_pos, y_pos = event.pos

      • who_wins немного странно сформулировано, потому что, возможно, никто не выиграл. я бы предпочел is_won, check_win или же just_won если он проверяет только выигрыш на последнем ходу, как предложено выше.

      • game_over не нужно; вы можете разорвать цикл (или вернуться, если это была функция) и запустить задержку после игры отдельно.

      • Пошаговые стратегии для 2 игроков обычно используют ply вместо turn. Вы можете продолжать увеличивать ply и использовать его четность (ply & 1 или же ply % 2 == 0), чтобы определить сторону. Если вы используете turn как отдельный бит, который переворачивается вперед и назад, я бы предпочел turn ^= 1 над

        turn += 1
        turn = turn % 2
        
      • Соблюдайте интервалы:

        pygame.draw.line(screen, BLACK, line_start,line_end,2)
        

        должно быть

        pygame.draw.line(screen, BLACK, line_start, line_end, 2)
        
      • Поскольку ничьи возможны, когда доска заполняется, и ни одна из сторон не делает 5 подряд, вы можете решить эту проблему в пользовательском интерфейсе и игровой логике.

      • Множественное число: draw_piece действительно рисует все части, поэтому его следует называть draw_pieces.

      • Опечатка в подписи: 'Gomoku (Connet 5)'. Подключить 4 это игра с разрывом соединения, поэтому Гомоку больше похожа на обобщенный mxnxk крестики-нолики но я просто педантичен.

      • Упорядочить импорт по алфавиту

      Возможна перезапись

      Вот GomokuPosition модуль:

      class GomokuPosition:
          dirs = (
              ((0, -1), (0, 1)), 
              ((1, 0), (-1, 0)),
              ((1, 1), (-1, -1)),
              ((1, -1), (-1, 1)),
          )
      
          def __init__(self, rows, cols, n_to_win, players="wb", blank="."):
              self.ply = 0
              self.rows = rows
              self.cols = cols
              self.last_move = None
              self.n_to_win = n_to_win
              self.boards = [[[0] * cols for _ in range(rows)] for i in range(2)]
              self.players = players
              self.blank = blank
      
          def board(self, row=None, col=None):
              if row is None and col is None:
                  return self.boards[self.ply&1]
              elif col is None:
                  return self.boards[self.ply&1][row]
      
              return self.boards[self.ply&1][row][col]
      
          def move(self, row, col):
              if self.in_bounds(row, col) and self.is_empty(row, col):
                  self.board(row)[col] = 1
                  self.ply += 1
                  self.last_move = row, col
                  return True
      
              return False
      
          def is_empty(self, row, col):
              return not any(board[row][col] for board in self.boards)
      
          def in_bounds(self, y, x):
              return y >= 0 and y < self.rows and x >= 0 and x < self.cols
      
          def count_from_last_move(self, dy, dx):
              if not self.last_move:
                  return 0
      
              last_board = self.boards[(self.ply-1)&1]
              y, x = self.last_move
              run = 0
      
              while self.in_bounds(y, x) and last_board[y][x]:
                  run += 1
                  x += dx
                  y += dy
              
              return run
      
          def just_won(self):
              return self.ply >= self.n_to_win * 2 - 1 and any(
                  (self.count_from_last_move(*x) + 
                   self.count_from_last_move(*y) - 1 >= self.n_to_win)
                  for x, y in self.dirs
              )
              
          def is_draw(self):
              return self.ply >= self.rows * self.cols and not self.just_won()
      
          def last_player(self):
              if self.ply < 1:
                  raise IndexError("no moves have been made")
      
              return self.players[(self.ply-1)&1]
      
          def char_for_cell(self, row, col):
              for i, char in enumerate(self.players):
                  if self.boards[i][row][col]:
                      return char
              
              return self.blank
      
          def to_grid(self):
              return [
                  [self.char_for_cell(row, col) for col in range(self.cols)]
                  for row in range(self.rows)
              ]
      
          def __repr__(self):
              return "n".join([" ".join(row) for row in self.to_grid()])
      
      
      if __name__ == "__main__":
          pos = GomokuPosition(rows=4, cols=4, n_to_win=3)
      
          while not pos.just_won() and not pos.is_draw():
              print(pos, "n")
      
              try:
                  if not pos.move(*map(int, input("[row col] :: ").split())):
                      print("try again")
              except (ValueError, IndexError):
                  print("try again")
      
          print(pos, "n")
              
          if pos.just_won():
              print(pos.last_player(), "won")
          else:
              print("draw")
      

      Теперь Gomoku Модуль графического интерфейса пользователя может импортировать позицию и использовать ее в качестве бэкэнда для игровой логики, как показано ниже. По общему признанию, мне немного наскучил графический интерфейс, поэтому в качестве упражнения для читателя осталось множество шероховатостей и сомнительных решений UX.

      import itertools
      import math
      import pygame
      
      from gomoku_position import GomokuPosition
      
      class Colors:
          BLACK = 0, 0, 0
          WHITE = 255, 255, 255
          BROWN = 205, 128, 0
      
      
      class Gomoku:
          def __init__(
              self,
              size=60,
              piece_size=20,
              rows=15,
              cols=15,
              n_to_win=5,
              caption="Gomoku"
          ):
              self.rows = rows
              self.cols = cols
              self.w = rows * size
              self.h = cols * size
              self.size = size
              self.piece_size = piece_size
              self.half_size = size // 2
              pygame.init()
              pygame.display.set_caption(caption)
              self.screen = pygame.display.set_mode((self.w, self.h))
              self.screen.fill(Colors.WHITE)
              self.player_colors = {"w": Colors.WHITE, "b": Colors.BLACK}
              self.player_names = {"w": "White", "b": "Black"}
              self.board = GomokuPosition(rows, cols, n_to_win)
      
          def row_lines(self):
              half = self.half_size
      
              for y in range(half, self.h - half + self.size, self.size):
                  yield (half, y), (self.w - half, y)
      
          def col_lines(self):
              half = self.half_size
      
              for x in range(half, self.w - half + self.size, self.size):
                  yield (x, half), (x, self.h - half)
              
          def draw_background(self):
              rect = pygame.Rect(0, 0, self.w, self.h)
              pygame.draw.rect(self.screen, Colors.BROWN, rect)
      
          def draw_lines(self):
              lines = itertools.chain(self.col_lines(), self.row_lines())
      
              for start, end in lines:
                  pygame.draw.line(
                      self.screen, 
                      Colors.BLACK, 
                      start, 
                      end, 
                      width=2
                  )
      
          def draw_board(self):
              self.draw_background()
              self.draw_lines()
              
          def draw_piece(self, row, col):
              player = self.board.last_player()
              circle_pos = (
                 col * self.size + self.half_size, 
                 row * self.size + self.half_size,
              )
              pygame.draw.circle(
                 self.screen, 
                 self.player_colors[player], 
                 circle_pos, 
                 self.piece_size
              )
      
          def show_outcome(self):
              player = self.player_names[self.board.last_player()]
              msg = "draw!" if self.board.is_draw() else f"{player} wins!"
              font_size = self.w // 10
              font = pygame.font.Font("freesansbold.ttf", font_size)
              label = font.render(msg, True, Colors.WHITE, Colors.BLACK)
              x = self.w // 2 - label.get_width() // 2
              y = self.h // 2 - label.get_height() // 2
              self.screen.blit(label, (x, y))
      
          def exit_on_click(self):
              while True:
                  for event in pygame.event.get():
                      if (event.type == pygame.QUIT or 
                              event.type == pygame.MOUSEBUTTONDOWN):
                          pygame.quit()
                          return
      
          def make_move(self, x, y):
              col = math.floor(x / self.size)
              row = math.floor(y / self.size)
              
              if self.board.move(row, col):
                  self.draw_piece(row, col)
              
          def play(self):
              pygame.time.Clock().tick(10)
              self.draw_board()
              pygame.display.update()
      
              while not self.board.just_won() and not self.board.is_draw():
                  for event in pygame.event.get():
                      if event.type == pygame.QUIT:
                          pygame.quit()
                          return
                      elif event.type == pygame.MOUSEBUTTONDOWN:
                          self.make_move(*event.pos)
                          pygame.display.update()
              
              self.show_outcome()
              pygame.display.update()
              self.exit_on_click()
      
      if __name__ == "__main__":
          game = Gomoku(rows=5, cols=5, n_to_win=4)
          game.play()
      

      Чтобы проверить, выиграл ли игрок, вам не нужно каждый раз сканировать всю доску. Достаточно сканирования только вокруг части, которая только что была размещена во всех направлениях (по горизонтали, вертикали, диагонали с положительным уклоном и диагонали с отрицательным уклоном).

      Вот пример реализации этой идеи:

      from enum import Enum, auto
      
      
      class Direction(Enum):
          N = auto()
          NE = auto()
          E = auto()
          SE = auto()
          S = auto()
          SW = auto()
          W = auto()
          NW = auto()
      
          @property
          def vector(self):
              return {
                  Direction.N: (-1, 0),
                  Direction.NE: (-1, 1),
                  Direction.E: (0, 1),
                  Direction.SE: (1, 1),
                  Direction.S: (1, 0),
                  Direction.SW: (1, -1),
                  Direction.W: (0, -1),
                  Direction.NW: (-1, -1),
              }[self]
      
      
      def count(board, row, col, direction):
          """
          Return the number of consecutive pieces matching the starting piece's
          color, starting from (but not including) the starting piece, and going
          in the given direction.
      
          row: starting piece's row
          col: starting piece's column
          """
      
          def on_board(row, col):
              return 0 <= row < ROW_COUNT and 0 <= col < COL_COUNT
      
          piece_color = board[row][col]
          row_delta, col_delta = direction.vector
      
          count = 0
          row, col = row + row_delta, col + col_delta
          while on_board(row, col) and board[row][col] == piece_color:
              count += 1
              row, col = row + row_delta, col + col_delta
      
          return count
      
      
      def is_win(board, row, col):
          """
          Returns True if the piece played on (row, col) wins the game.
          """
      
          def is_win_helper(board, row, col, d1, d2):
              return count(board, row, col, d1) + 1 + count(board, row, col, d2) >= 5
      
          return (
              # horizontal win
              is_win_helper(board, row, col, Direction.W, Direction.E)
              # vertical win
              or is_win_helper(board, row, col, Direction.N, Direction.S)
              # positively sloped diagonal win
              or is_win_helper(board, row, col, Direction.SW, Direction.NE)
              # negatively sloped diagonal win
              or is_win_helper(board, row, col, Direction.NW, Direction.SE)
          )
      

      Затем вы можете проверить, есть ли у игрока, который только что играл в (row, col) выиграл, сделав:

      if is_win(board, row, col):
          # ...
      

        Добавить комментарий

        Ваш адрес email не будет опубликован. Обязательные поля помечены *