Крестики-нолики — логическая игра между двумя противниками на квадратном поле 3 на 3 клетки или большего размера. Цель игры, занять три клетки в ряд, включая диагонали. Пишем программу игры крестики-нолики на Python с библиотекой Pygame Zero (pgzrun).
Кто решает задачи по комбинаторной геометрии, тот
впоследствии пишет короткие и сильные алгоритмы.
Алексей Савватеев.
Мы публикуем методическую разработку для занятий с детьми старше 10-ти лет в кружке программирование на Python. Цель - научить детей строить логические выражения и пользоваться условным оператором if (elif, else), использовать цикл for, вложенный цикл и операторы continue и break, а также, создавать собственные функции и возвращать из них результат. В конце статьи приведён пример использования алгоритма minimax рекурсивный алгоритм с возвратом (backtracking).
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*AVX2.*")
import pgzrun, pygame
"""
Pygame Zero (pgzrun) - это обёртка над Pygame,
которая упрощает создание игр,
автоматизируя многие рутинные задачи.
"""
# Настройки игры, глобальные константы
TITLE = "tic-tac-toe" # Заголовок окна
WIDTH = HEIGHT = 400 # Ширина и высота окна
CELL = WIDTH // 3 # Размер клетки
# Игровые переменные
playground = [0] * 9 # Виртуальное игровое поле
# Элементами списка playground могут быть числа -1, 0 и 1.
# 0 пустая клетка на игровом поле
# -1 в клетке установлен О
# 1 в клетке установлен Х
def won(pg):
"""
Подсчёт сумм линий по 3
возвращает вес хода -1, 0, 1
"""
pass
def play(n):
"""
Ход в игре
"""
pass
def n_to_xy(n):
"""
Конвертирует номер клетки в пару координат
в центре этой клетки
"""
return (CELL // 2, CELL // 2)
def xy_to_n(x, y):
"""
Конвертирует пару координат x, y
в номер клетки
"""
return 0
def draw_playground():
"""
Рисует по клеткам игровое поле в клетку и
символы в соответствии с игровой ситуацией
"""
pass
def draw():
"""
draw() особая функция, вызывается автоматически каждый кадр,
обычно, 60 FPS. Доступны объекты: screen, actors и др.
Назначение: отрисовка всего, что должно быть на экране.
"""
pygame.display.set_caption(TITLE) # Обновление заголовка
draw_playground() # Нарисовать игровое поле
def on_mouse_down(pos):
"""
on_mouse_down(pos) особая функция, вызывается автоматически при клике мыши
Через параметр pos в функцию передаётся кортеж из пары координат (x, y)
"""
play(xy_to_n(*pos)) # Сделать ход в выбранную клетку
pgzrun.go() # Запускается главный игровой цикл
Лист. 1.
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*AVX2.*")
import pgzrun, pygame
"""
Pygame Zero (pgzrun) - это обёртка над Pygame,
которая упрощает создание игр,
автоматизируя многие рутинные задачи.
"""
# Настройки игры, глобальные константы
TITLE = "tic-tac-toe" # Заголовок окна
WIDTH = HEIGHT = 400 # Ширина и высота окна
CELL = WIDTH // 3 # Размер клетки
FONT_SIZE = WIDTH // 4 # Размер цифр
FONT_COLOR = 'sienna' # Цвет букв
font_params = {'color': FONT_COLOR,'fontsize': FONT_SIZE}
BTN_COLOR = 'khaki' # Цвет кнопки
GRID_COLOR = 'dimgray' # Цвет сетки
BORDER_COLOR = 'gold' # Цвет бордюра клеток
# Игровые переменные
playground = [0] * 9 # Виртуальное игровое поле
# Элементами списка playground могут быть числа -1, 0 и 1.
# 0 пустая клетка на игровом поле
# -1 в клетке установлен О
# 1 в клетке установлен Х
def won(pg):
"""
Подсчёт сумм линий по 3
возвращает вес хода -1, 0, 1
"""
pass
def play(n):
"""
Ход в игре
"""
pass
def n_to_xy(n):
"""
Конвертирует номер клетки в пару координат
в центре этой клетки
"""
col = n % 3
row = n // 3
x = col * CELL
y = row * CELL
return (x + CELL // 2, y + CELL // 2)
def xy_to_n(x, y):
"""
Конвертирует пару координат x, y
в номер клетки
"""
col = x // CELL
row = y // CELL
return row * 3 + col
def draw_playground():
"""
Рисует по клеткам игровое поле в клетку и
символы в соответствии с игровой ситуацией
"""
for n in range(9):
x = n_to_xy(n)[0] - CELL // 2
y = n_to_xy(n)[1] - CELL // 2
screen.draw.filled_rect(Rect(x, y, CELL, CELL), GRID_COLOR)
screen.draw.filled_rect(Rect(x+1, y+1, CELL-1, CELL-1), BORDER_COLOR)
screen.draw.filled_rect(Rect(x+7, y+7, CELL-14, CELL-14), BTN_COLOR)
txt = "X" if playground[n] == 1 else "O"
if playground[n] == 0: continue
screen.draw.text(txt, center = n_to_xy(n), **font_params)
def draw():
"""
draw() особая функция, вызывается автоматически каждый кадр,
обычно, 60 FPS. Доступны объекты: screen, actors и др.
Назначение: отрисовка всего, что должно быть на экране.
"""
pygame.display.set_caption(TITLE) # Обновление заголовка
draw_playground() # Нарисовать игровое поле
def on_mouse_down(pos):
"""
on_mouse_down(pos) особая функция, вызывается автоматически при клике мыши
Через параметр pos в функцию передаётся кортеж из пары координат (x, y)
"""
play(xy_to_n(*pos)) # Сделать ход в выбранную клетку
pgzrun.go() # Запускается главный игровой цикл
Лист. 2.
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*AVX2.*")
import pgzrun, pygame
"""
Pygame Zero (pgzrun) - это обёртка над Pygame,
которая упрощает создание игр,
автоматизируя многие рутинные задачи.
"""
# Настройки игры, глобальные константы
TITLE = "tic-tac-toe" # Заголовок окна
WIDTH = HEIGHT = 400 # Ширина и высота окна
CELL = WIDTH // 3 # Размер клетки
FONT_SIZE = WIDTH // 4 # Размер цифр
FONT_COLOR = 'sienna' # Цвет букв
font_params = {'color': FONT_COLOR,'fontsize': FONT_SIZE}
BTN_COLOR = 'khaki' # Цвет кнопки
GRID_COLOR = 'dimgray' # Цвет сетки
BORDER_COLOR = 'gold' # Цвет бордюра клеток
# Игровые переменные
playground = [0] * 9 # Виртуальное игровое поле
# Элементами списка playground могут быть числа -1, 0 и 1.
# 0 пустая клетка на игровом поле
# -1 в клетке установлен О
# 1 в клетке установлен Х
who = 1 # Чей ход
def won(pg):
"""
Подсчёт сумм линий по 3
возвращает вес хода -1, 0, 1
"""
pass
def play(n):
"""
Ход в игре
"""
global who
playground[n] = who
who *= -1
def n_to_xy(n):
"""
Конвертирует номер клетки в пару координат
в центре этой клетки
"""
col = n % 3
row = n // 3
x = col * CELL
y = row * CELL
return (x + CELL // 2, y + CELL // 2)
def xy_to_n(x, y):
"""
Конвертирует пару координат x, y
в номер клетки
"""
col = x // CELL
row = y // CELL
return row * 3 + col
def draw_playground():
"""
Рисует по клеткам игровое поле в клетку и
символы в соответствии с игровой ситуацией
"""
for n in range(9):
x = n_to_xy(n)[0] - CELL // 2
y = n_to_xy(n)[1] - CELL // 2
screen.draw.filled_rect(Rect(x, y, CELL, CELL), GRID_COLOR)
screen.draw.filled_rect(Rect(x+1, y+1, CELL-1, CELL-1), BORDER_COLOR)
screen.draw.filled_rect(Rect(x+7, y+7, CELL-14, CELL-14), BTN_COLOR)
txt = "X" if playground[n] == 1 else "O"
if playground[n] == 0: continue
screen.draw.text(txt, center = n_to_xy(n), **font_params)
def draw():
"""
draw() особая функция, вызывается автоматически каждый кадр,
обычно, 60 FPS. Доступны объекты: screen, actors и др.
Назначение: отрисовка всего, что должно быть на экране.
"""
pygame.display.set_caption(TITLE) # Обновление заголовка
draw_playground() # Нарисовать игровое поле
def on_mouse_down(pos):
"""
on_mouse_down(pos) особая функция, вызывается автоматически при клике мыши
Через параметр pos в функцию передаётся кортеж из пары координат (x, y)
"""
play(xy_to_n(*pos)) # Сделать ход в выбранную клетку
pgzrun.go() # Запускается главный игровой цикл
Лист. 3.
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*AVX2.*")
import pgzrun, pygame
"""
Pygame Zero (pgzrun) - это обёртка над Pygame,
которая упрощает создание игр,
автоматизируя многие рутинные задачи.
"""
# Настройки игры, глобальные константы
TITLE = "tic-tac-toe" # Заголовок окна
WIDTH = HEIGHT = 400 # Ширина и высота окна
CELL = WIDTH // 3 # Размер клетки
FONT_SIZE = WIDTH // 4 # Размер цифр
FONT_COLOR = 'sienna' # Цвет букв
font_params = {'color': FONT_COLOR,'fontsize': FONT_SIZE}
BTN_COLOR = 'khaki' # Цвет кнопки
GRID_COLOR = 'dimgray' # Цвет сетки
BORDER_COLOR = 'gold' # Цвет бордюра клеток
# Игровые переменные
playground = [0] * 9 # Виртуальное игровое поле
# Элементами списка playground могут быть числа -1, 0 и 1.
# 0 пустая клетка на игровом поле
# -1 в клетке установлен О
# 1 в клетке установлен Х
#playground[4] = -1 # Первый ход О
def won(pg):
"""
Подсчёт сумм линий по 3
возвращает вес хода -1, 0, 1
"""
standings = [0]*8
for i in range(3):
standings[i] = pg[i*3] + pg[i*3+1] + pg[i*3+2]
standings[i+3] = pg[i] + pg[i+3] + pg[i+6]
standings[6] = pg[0] + pg[4] + pg[8]
standings[7] = pg[2] + pg[4] + pg[6]
if 3 in standings : return 1 # Победа Х
if -3 in standings : return -1 # Победа О
return 0 # Вес нейтральный
def play(n):
"""
Ход в игре
"""
global TITLE
if playground[n] != 0: return # Если выбрана не пустая клетка
if won(playground) != 0: return # Прекратить игру в случае победы
for i in (1, -1):
playground[n] = i # Ход на виртуальном поле
if won(playground) == -1: # Победа O
TITLE = 'Победа O'
elif not 0 in playground: # Если не осталось пустых клеток
TITLE = 'Ничья'
break
n = minmax(playground, -1)[1]
def minmax(pg, step):
"""
Поиск лучшего хода. pg список игровое поле из 9 элементов.
step = 1 ход крестика, step = -1 ход О.
Возвращает списсок [оценка позиции, индекс лучшего хода]
"""
best = [-2 * step, None]
if won(pg) != 0: return [won(pg), None]
if 0 not in pg: return [0, None]
for i in range(9):
if pg[i] != 0: continue
pgcp = pg.copy()
pgcp[i] = step
x = minmax(pgcp, -1 * step)[0]
if (step == 1 and x > best[0]) or (step == -1 and x < best[0]):
best = [x, i]
return best
def n_to_xy(n):
"""
Конвертирует номер клетки в пару координат
в центре этой клетки
"""
col = n % 3
row = n // 3
x = col * CELL
y = row * CELL
return (x + CELL // 2, y + CELL // 2)
def xy_to_n(x, y):
"""
Конвертирует пару координат x, y
в номер клетки
"""
col = x // CELL
row = y // CELL
return row * 3 + col
def draw_playground():
"""
Рисует по клеткам игровое поле в клетку и
символы в соответствии с игровой ситуацией
"""
for n in range(9):
x = n_to_xy(n)[0] - CELL // 2
y = n_to_xy(n)[1] - CELL // 2
screen.draw.filled_rect(Rect(x, y, CELL, CELL), GRID_COLOR)
screen.draw.filled_rect(Rect(x+1, y+1, CELL-1, CELL-1), BORDER_COLOR)
screen.draw.filled_rect(Rect(x+7, y+7, CELL-14, CELL-14), BTN_COLOR)
txt = "X" if playground[n] == 1 else "O"
if playground[n] == 0: continue
screen.draw.text(txt, center = n_to_xy(n), **font_params)
def draw():
"""
draw() особая функция, вызывается автоматически каждый кадр,
обычно, 60 FPS. Доступны объекты: screen, actors и др.
Назначение: отрисовка всего, что должно быть на экране.
"""
pygame.display.set_caption(TITLE) # Обновление заголовка
draw_playground() # Нарисовать игровое поле
def on_mouse_down(pos):
"""
on_mouse_down(pos) особая функция, вызывается автоматически при клике мыши
Через параметр pos в функцию передаётся кортеж из пары координат (x, y)
"""
play(xy_to_n(*pos)) # Сделать ход в выбранную клетку
pgzrun.go() # Запускается главный игровой цикл
Лист. 10.
Детальное описание функции minmax()
Теоретическая основа
Функция реализует алгоритм минимакс для игры в крестики-нолики. Этот алгоритм используется для нахождения оптимального хода в играх с нулевой суммой (что выигрывает один игрок, то проигрывает другой).
Основные концепции:
-
Дерево игры - все возможные последовательности ходов
-
Эвристическая оценка - числовая оценка позиции
-
Рекурсивный перебор - исследование возможных вариантов
-
Альфа-бета отсечения (в данном коде нет, но принцип похож)
Подробный разбор кода с комментариями
def minmax(pg, step): """ Алгоритм минимакс для поиска оптимального хода pg: playground - текущее состояние поля (список из 9 элементов) step: 1 (крестик/X) или -1 (нолик/O) - чей сейчас ход оценивается Возвращает: [оценка_позиции, индекс_лучшего_хода] """ # Инициализируем лучший результат # Ключевой момент: для крестика (step=1) начинаем с -2, для нолика с 2 # Это гарантирует, что первый найденный вариант будет "лучшим" # -2 и 2 выбраны как значения за пределами возможных оценок [-1, 0, 1] best = [-2 * step, None] # Базовый случай 1: игра уже завершена # Если кто-то уже выиграл, возвращаем результат игры game_result = won(pg) if game_result != 0: return [game_result, None] # 1 - победа X, -1 - победа O # Базовый случай 2: ничья (нет пустых клеток) if 0 not in pg: return [0, None] # 0 означает ничью # Рекурсивный случай: перебираем все возможные ходы for i in range(9): # Пропускаем занятые клетки if pg[i] != 0: continue # Создаем копию поля для моделирования хода pgcp = pg.copy() # Моделируем ход текущего игрока в клетку i pgcp[i] = step # Рекурсивно вызываем minmax для противника # Ключевой момент: передаем -step для смены игрока # Получаем оценку позиции после этого хода x = minmax(pgcp, -1 * step)[0] # Обновляем лучший результат в зависимости от игрока: # 1. Для крестика (step=1): ищем МАКСИМАЛЬНУЮ оценку (x > best[0]) # Крестик хочет максимизировать свой выигрыш # 2. Для нолика (step=-1): ищем МИНИМАЛЬНУЮ оценку (x < best[0]) # Нолик хочет минимизировать выигрыш крестика (максимизировать свой) if (step == 1 and x > best[0]) or (step == -1 and x < best[0]): best = [x, i] # Сохраняем новую лучшую оценку и ход # Возвращаем лучшую найденную оценку и соответствующий ход return best
Как работает оценка позиции
Функция won(pg) возвращает:
-
1- победа крестика -
-1- победа нолика -
0- игра продолжается или ничья
Логика выбора хода:
Для крестика (step = 1):
Дерево решений: Крестик смотрит: "Если я пойду сюда, то нолик ответит туда..." Он выбирает ход, который дает максимальную оценку при условии, что нолик будет играть оптимально
Для нолика (step = -1):
Нолик смотрит: "Если крестик пойдет сюда, то я должен ответить туда..." Он выбирает ход, который минимизирует оценку для крестика (максимизирует свои шансы)
Пример работы на практике
Рассмотрим упрощенный пример с 2 возможными ходами:
# Текущее поле: [0, 0, 0, 0, -1, 0, 0, 0, 0] (нолик в центре) # Ход крестика (step=1) # Вариант 1: Ход в клетку 0 (левый верх) # minmax() моделирует: X в 0 → O в ... → и т.д. # Предположим, оценка = 0 (ничья) # Вариант 2: Ход в клетку 8 (правый низ) # minmax() моделирует: X в 8 → O в 0 → X в 2 → ... # Предположим, оценка = 1 (победа X) # Крестик выберет вариант 2 (оценка 1 > 0)
Рекурсивная структура вызовов
minmax(поле, игрок)
├── Проверяем: игра окончена? → возвращаем результат
├── Для каждой свободной клетки:
│ ├── Копируем поле
│ ├── Делаем ход
│ └── Рекурсивно вызываем minmax() для противника
│
└── Выбираем лучший ход:
- Для X: максимальная оценка
- Для O: минимальная оценка
Почему такой подход оптимален?
-
Полный перебор - алгоритм рассматривает ВСЕ возможные варианты до конца игры
-
Оптимальность - гарантирует лучший возможный результат при оптимальной игре противника
-
Предвидение - учитывает, что противник тоже будет играть оптимально
Визуализация процесса
Текущая позиция (X ходит)
│
├── Ход в A → оценка -1 (O выигрывает)
├── Ход в B → оценка 0 (ничья)
└── Ход в C → оценка 1 (X выигрывает)
│
└── X выбирает ход C
Ключевые моменты реализации
-
Инициализация best:
[-2 * step, None]гарантирует, что первый найденный ход станет лучшим -
Рекурсивный вызов:
minmax(pgcp, -1 * step)меняет игрока на каждом уровне -
Условие выбора: разная логика для X и O из-за противоположных целей
-
Отсутствие отсечений: простейший минимакс без оптимизаций (альфа-бета)
Сложность алгоритма
Для крестиков-ноликов:
-
Максимальная глубина рекурсии: 9 ходов
-
Количество листьев в дереве: до 9! = 362880
-
На практике меньше, так как игра заканчивается раньше при победе
Как это используется в игре
def play(n): # ... # Компьютер (нолик) ищет лучший ход: n = minmax(playground, -1)[1] # -1 означает "ход нолика" # Получаем индекс лучшей клетки и ходим туда
Этот алгоритм гарантирует, что компьютер будет играть в крестики-нолики безупречно - либо выиграет, либо сведет игру к ничьей.