Я попытался реализовать простую игру Гомоку (подключение 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 ответа
Я могу предложить небольшое улучшение, когда вы проверяете выигрыш с помощью кода:
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):
# ...