← Каталог
Pygame — мини-игры на Python — 4.3. Крестики-нолики с ИИ
Фрагмент из «Pygame — мини-игры на Python»: крестики-нолики 3×3, клик мыши и эвристический ИИ.
#!/usr/bin/env python3
"""Крестики-нолики 3×3 на Pygame: игрок (X) против эвристического ИИ (O)."""
from __future__ import annotations
import pygame
import sys
from typing import List, Optional, Tuple
pygame.init()
WIDTH, HEIGHT = 480, 560
BOARD_SIZE = 3
CELL = 120
OFFSET_X = (WIDTH - CELL * BOARD_SIZE) // 2
OFFSET_Y = 100
FPS = 60
BG = (18, 22, 36)
GRID = (90, 100, 140)
X_COLOR = (255, 120, 120)
O_COLOR = (120, 200, 255)
TEXT = (230, 230, 240)
WIN_LINE = (255, 220, 80)
PlayerMark = str # "X" | "O"
Cell = Optional[PlayerMark]
class Board:
def __init__(self, size: int = BOARD_SIZE) -> None:
self.size = size
self.cells: List[List[Cell]] = [[None] * size for _ in range(size)]
def reset(self) -> None:
self.cells = [[None] * self.size for _ in range(self.size)]
def empty_cells(self) -> List[Tuple[int, int]]:
return [
(row, col)
for row in range(self.size)
for col in range(self.size)
if self.cells[row][col] is None
]
def place(self, row: int, col: int, mark: PlayerMark) -> bool:
if self.cells[row][col] is not None:
return False
self.cells[row][col] = mark
return True
def winner(self) -> Optional[PlayerMark]:
n = self.size
lines = []
lines.extend(self.cells)
lines.extend([[self.cells[r][c] for r in range(n)] for c in range(n)])
lines.append([self.cells[i][i] for i in range(n)])
lines.append([self.cells[i][n - i - 1] for i in range(n)])
for line in lines:
if line[0] is not None and all(cell == line[0] for cell in line):
return line[0]
return None
def is_full(self) -> bool:
return not self.empty_cells()
def would_win(board: Board, row: int, col: int, mark: PlayerMark) -> bool:
board.place(row, col, mark)
won = board.winner() == mark
board.cells[row][col] = None
return won
def choose_ai_move(board: Board, ai: PlayerMark = "O", human: PlayerMark = "X") -> Tuple[int, int]:
empty = board.empty_cells()
if not empty:
raise ValueError("no empty cells")
# 1. Выиграть, если можем
for row, col in empty:
if would_win(board, row, col, ai):
return row, col
# 2. Заблокировать победу соперника
for row, col in empty:
if would_win(board, row, col, human):
return row, col
center = board.size // 2
if (center, center) in empty:
return center, center
corners = [
(0, 0),
(0, board.size - 1),
(board.size - 1, 0),
(board.size - 1, board.size - 1),
]
for cell in corners:
if cell in empty:
return cell
return empty[0]
def cell_from_mouse(mx: int, my: int) -> Optional[Tuple[int, int]]:
col = (mx - OFFSET_X) // CELL
row = (my - OFFSET_Y) // CELL
if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE:
return row, col
return None
def draw_mark(surface: pygame.Surface, row: int, col: int, mark: PlayerMark) -> None:
x = OFFSET_X + col * CELL
y = OFFSET_Y + row * CELL
pad = 22
if mark == "X":
pygame.draw.line(surface, X_COLOR, (x + pad, y + pad), (x + CELL - pad, y + CELL - pad), 6)
pygame.draw.line(surface, X_COLOR, (x + CELL - pad, y + pad), (x + pad, y + CELL - pad), 6)
else:
pygame.draw.circle(
surface,
O_COLOR,
(x + CELL // 2, y + CELL // 2),
CELL // 2 - pad,
6,
)
def draw_board(surface: pygame.Surface, board: Board, status: str) -> None:
surface.fill(BG)
font = pygame.font.SysFont("consolas", 28)
surface.blit(font.render(status, True, TEXT), (24, 24))
for i in range(1, BOARD_SIZE):
pygame.draw.line(
surface,
GRID,
(OFFSET_X, OFFSET_Y + i * CELL),
(OFFSET_X + BOARD_SIZE * CELL, OFFSET_Y + i * CELL),
3,
)
pygame.draw.line(
surface,
GRID,
(OFFSET_X + i * CELL, OFFSET_Y),
(OFFSET_X + i * CELL, OFFSET_Y + BOARD_SIZE * CELL),
3,
)
pygame.draw.rect(
surface,
GRID,
(OFFSET_X, OFFSET_Y, BOARD_SIZE * CELL, BOARD_SIZE * CELL),
3,
)
for row in range(BOARD_SIZE):
for col in range(BOARD_SIZE):
mark = board.cells[row][col]
if mark:
draw_mark(surface, row, col, mark)
hint = pygame.font.SysFont("consolas", 20).render("ЛКМ — ход · R — заново · Esc — выход", True, TEXT)
surface.blit(hint, (24, HEIGHT - 36))
def main() -> None:
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Крестики-нолики")
clock = pygame.time.Clock()
board = Board()
human_turn = True
status = "Ваш ход (X)"
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
if event.key == pygame.K_r:
board.reset()
human_turn = True
status = "Ваш ход (X)"
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and human_turn:
if board.winner() or board.is_full():
continue
cell = cell_from_mouse(*event.pos)
if cell is None:
continue
row, col = cell
if not board.place(row, col, "X"):
continue
human_turn = False
winner = board.winner()
if winner:
status = "Победа X!"
elif board.is_full():
status = "Ничья"
else:
status = "Ход компьютера (O)…"
if not human_turn and not board.winner() and not board.is_full():
row, col = choose_ai_move(board)
board.place(row, col, "O")
human_turn = True
winner = board.winner()
if winner:
status = "Победа O — попробуйте ещё (R)"
elif board.is_full():
status = "Ничья — R для реванша"
else:
status = "Ваш ход (X)"
draw_board(screen, board, status)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main() #!/usr/bin/env python3
"""Крестики-нолики 3×3 на Pygame: игрок (X) против эвристического ИИ (O)."""
from __future__ import annotations
import pygame
import sys
from typing import List, Optional, Tuple
pygame.init()
WIDTH, HEIGHT = 480, 560
BOARD_SIZE = 3
CELL = 120
OFFSET_X = (WIDTH - CELL * BOARD_SIZE) // 2
OFFSET_Y = 100
FPS = 60
BG = (18, 22, 36)
GRID = (90, 100, 140)
X_COLOR = (255, 120, 120)
O_COLOR = (120, 200, 255)
TEXT = (230, 230, 240)
WIN_LINE = (255, 220, 80)
PlayerMark = str # "X" | "O"
Cell = Optional[PlayerMark]
class Board:
def __init__(self, size: int = BOARD_SIZE) -> None:
self.size = size
self.cells: List[List[Cell]] = [[None] * size for _ in range(size)]
def reset(self) -> None:
self.cells = [[None] * self.size for _ in range(self.size)]
def empty_cells(self) -> List[Tuple[int, int]]:
return [
(row, col)
for row in range(self.size)
for col in range(self.size)
if self.cells[row][col] is None
]
def place(self, row: int, col: int, mark: PlayerMark) -> bool:
if self.cells[row][col] is not None:
return False
self.cells[row][col] = mark
return True
def winner(self) -> Optional[PlayerMark]:
n = self.size
lines = []
lines.extend(self.cells)
lines.extend([[self.cells[r][c] for r in range(n)] for c in range(n)])
lines.append([self.cells[i][i] for i in range(n)])
lines.append([self.cells[i][n - i - 1] for i in range(n)])
for line in lines:
if line[0] is not None and all(cell == line[0] for cell in line):
return line[0]
return None
def is_full(self) -> bool:
return not self.empty_cells()
def would_win(board: Board, row: int, col: int, mark: PlayerMark) -> bool:
board.place(row, col, mark)
won = board.winner() == mark
board.cells[row][col] = None
return won
def choose_ai_move(board: Board, ai: PlayerMark = "O", human: PlayerMark = "X") -> Tuple[int, int]:
empty = board.empty_cells()
if not empty:
raise ValueError("no empty cells")
# 1. Выиграть, если можем
for row, col in empty:
if would_win(board, row, col, ai):
return row, col
# 2. Заблокировать победу соперника
for row, col in empty:
if would_win(board, row, col, human):
return row, col
center = board.size // 2
if (center, center) in empty:
return center, center
corners = [
(0, 0),
(0, board.size - 1),
(board.size - 1, 0),
(board.size - 1, board.size - 1),
]
for cell in corners:
if cell in empty:
return cell
return empty[0]
def cell_from_mouse(mx: int, my: int) -> Optional[Tuple[int, int]]:
col = (mx - OFFSET_X) // CELL
row = (my - OFFSET_Y) // CELL
if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE:
return row, col
return None
def draw_mark(surface: pygame.Surface, row: int, col: int, mark: PlayerMark) -> None:
x = OFFSET_X + col * CELL
y = OFFSET_Y + row * CELL
pad = 22
if mark == "X":
pygame.draw.line(surface, X_COLOR, (x + pad, y + pad), (x + CELL - pad, y + CELL - pad), 6)
pygame.draw.line(surface, X_COLOR, (x + CELL - pad, y + pad), (x + pad, y + CELL - pad), 6)
else:
pygame.draw.circle(
surface,
O_COLOR,
(x + CELL // 2, y + CELL // 2),
CELL // 2 - pad,
6,
)
def draw_board(surface: pygame.Surface, board: Board, status: str) -> None:
surface.fill(BG)
font = pygame.font.SysFont("consolas", 28)
surface.blit(font.render(status, True, TEXT), (24, 24))
for i in range(1, BOARD_SIZE):
pygame.draw.line(
surface,
GRID,
(OFFSET_X, OFFSET_Y + i * CELL),
(OFFSET_X + BOARD_SIZE * CELL, OFFSET_Y + i * CELL),
3,
)
pygame.draw.line(
surface,
GRID,
(OFFSET_X + i * CELL, OFFSET_Y),
(OFFSET_X + i * CELL, OFFSET_Y + BOARD_SIZE * CELL),
3,
)
pygame.draw.rect(
surface,
GRID,
(OFFSET_X, OFFSET_Y, BOARD_SIZE * CELL, BOARD_SIZE * CELL),
3,
)
for row in range(BOARD_SIZE):
for col in range(BOARD_SIZE):
mark = board.cells[row][col]
if mark:
draw_mark(surface, row, col, mark)
hint = pygame.font.SysFont("consolas", 20).render("ЛКМ — ход · R — заново · Esc — выход", True, TEXT)
surface.blit(hint, (24, HEIGHT - 36))
def main() -> None:
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Крестики-нолики")
clock = pygame.time.Clock()
board = Board()
human_turn = True
status = "Ваш ход (X)"
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
if event.key == pygame.K_r:
board.reset()
human_turn = True
status = "Ваш ход (X)"
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and human_turn:
if board.winner() or board.is_full():
continue
cell = cell_from_mouse(*event.pos)
if cell is None:
continue
row, col = cell
if not board.place(row, col, "X"):
continue
human_turn = False
winner = board.winner()
if winner:
status = "Победа X!"
elif board.is_full():
status = "Ничья"
else:
status = "Ход компьютера (O)…"
if not human_turn and not board.winner() and not board.is_full():
row, col = choose_ai_move(board)
board.place(row, col, "O")
human_turn = True
winner = board.winner()
if winner:
status = "Победа O — попробуйте ещё (R)"
elif board.is_full():
status = "Ничья — R для реванша"
else:
status = "Ваш ход (X)"
draw_board(screen, board, status)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()