Крестики-нолики — логическая игра между двумя противниками на квадратном поле 3 на 3 клетки или большего размера. Цель игры, занять три клетки в ряд, включая диагонали. Пишем программу игры крестики-нолики на Python с библиотекой tkinter.
Мы публикуем методическую разработку для занятий с детьми старше 10-ти лет в кружке программирование на Python. Цель - научить детей строить логические выражения и пользоваться условным оператором if (elif, else), использовать цикл for, вложенный цикл и операторы continue и break, а также, создавать собственные функции и возвращать из них результат.
Возьмём шаблон для игр на поле в клетку. Подробнее о создании этого шаблона на странице Игровое поле 2023.
from tkinter import * # графическая библиотека
from random import shuffle # перемешать список suffle(A)
column = 3 # столбцы
row = 3 # строки
btn = []
playground = list('ЧТОЕСТЖУ ') # виртуальное игровое поле
def play(n): # функция обработчик нажатия на кнопку
btn[n].config(text=n)
for i in range(row):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(column):
n = i * column + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=3, height=2)
btn[n].config(text=playground[n])
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 1. Шаблон для игр на поле в клетку.
В программе, листинг 1 необходимо изменить виртуальное игровое поле playground, убрать вывод элементов списка playgroun на клетки игрового поля и, возможно, убрать лишние переменные row и column.
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def play(n): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
btn[n].config(text='X') # Ход крестиков
playground[n] = 1
if not 0 in playground: return
n = playground.index(0)
btn[n].config(text='O') # Ход ноликов
playground[n] = -1
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 2. Игра Крестики-Нолики с болваном.
В программе листинг 2 создан список, виртуальное игровое поле playground. Каждому элементу этого списка соответствует клетка на игровом поле. В начале игры, список playground содержит девять 0 (нолей). Элемент списка playground со значением 0 является абстрактным представлением соответствующей пустой клетки на игровом поле.
Соответствие элементов списка playground клеткам на игровом поле осуществляется по индексу в списке playground
Если, в процессе игры, на клетку игрового поля записан "X", в соответствующий этой клетке элемент списка playground должно быть записано значение 1. Если на игровом поле записан "O", в списке playground должно появиться значение - 1.
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def play(n): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
btn[n].config(text='X') # Ход крестиков
playground[n] = 1
if not 0 in playground: return
n = best(playground)
btn[n].config(text='O') # Ход ноликов
playground[n] = -1
def best(pg): # Поиск лучшего хода
if pg[4] == 0: return 4 # Ход в центр
best = pg.index(0) # Ход болвана
return best
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 3. Создана функция best() для поиска лучшего хода.
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def play(n): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = best(playground)
def best(pg): # Поиск лучшего хода
if pg[4] == 0: return 4 # Ход в центр
best = pg.index(0) # Ход болвана
return best
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 4. Усовершенствована функция play().
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def won(pg): # Подсчёт сумм линий
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 3
if -3 in standings : return -3
return 0 # Возврат веса хода
def play(n): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
if won(playground) != 0: break # Прекратить игру в случае победы
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = best(playground)
def best(pg): # Поиск лучшего хода
if pg[4] == 0: return 4 # Ход в центр
best = pg.index(0) # Ход болвана
return best
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 5. Добавлены функция won() для подсчёта сумм линий на игровом поле и её вызов в функцию play().
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def won(pg): # Подсчёт сумм линий
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 3
if -3 in standings : return -3
return 0 # Возврат веса хода
def play(n): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
if won(playground) != 0: break # Прекратить игру в случае победы
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = best(playground)
def best(pg): # Поиск лучшего хода
if pg[4] == 0: return 4 # Ход в центр
best = pg.index(0) # Ход болвана
for i in range(9): # Перебор ходов
if pg[i] != 0: continue
pgcp1 = pg.copy()
pgcp1[i] = -1
if won(pgcp1) == -3: return i # Ход приносит победу
x = -3
for j in range(9): # Ход не должен вести к поражению
if pgcp1[j] != 0: continue
pgcp2 = pgcp1.copy()
pgcp2[j] = 1
x = max(x, won(pgcp2))
if x == 3: break
if x < 3:
best = i
return best
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 6. Усовершенствована функция best().
Рекурсия
Рекурсия
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def won(pg): # Подсчёт сумм линий
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 3
if -3 in standings : return -3
return 0 # Возврат веса хода
def play(n): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
if i == 1: print('Перед ходом X: ', recur(playground))
if i == -1: print('Перед ходом O: ', recur(playground))
if won(playground) != 0: break # Прекратить игру в случае победы
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = best(playground)
def recur(pg, n=0): # Подсчёт вариантов развития игры
for i in range(9): # Перебор ходов
if pg[i] != 0: continue
n += 1
pgcp = pg.copy()
pgcp[i] = 1
n += recur(pgcp)
return n
def best(pg): # Поиск лучшего хода
if pg[4] == 0: return 4 # Ход в центр
best = pg.index(0) # Ход болвана
for i in range(9): # Перебор ходов
if pg[i] != 0: continue
pgcp1 = pg.copy()
pgcp1[i] = -1
if won(pgcp1) == -3: return i # Ход приносит победу
x = -3
for j in range(9): # Ход не должен вести к поражению
if pgcp1[j] != 0: continue
pgcp2 = pgcp1.copy()
pgcp2[j] = 1
x = max(x, won(pgcp2))
if x == 3: break
if x < 3:
best = i
return best
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 7. Добавлена функция recur().
Перед ходом X: 986409
Перед ходом O: 109600
Перед ходом X: 13699
Перед ходом O: 1956
Перед ходом X: 325
Перед ходом O: 64
Перед ходом X: 15
Перед ходом O: 4
Перед ходом X: 1
Терм. 1. Вывод из программы листинг 7.
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def won(pg): # Подсчёт сумм линий
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 3
if -3 in standings : return -3
return 0 # Возврат веса хода
def play(n): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
if i == 1: print('Перед ходом X: ', recur(playground, 1))
if i == -1: print('Перед ходом O: ', recur(playground, -1))
if won(playground) != 0: break # Прекратить игру в случае победы
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = best(playground)
def recur(pg, step, n=0): # Подсчёт вариантов развития игры
for i in range(9): # Перебор ходов
if pg[i] != 0: continue
n += 1
pgcp = pg.copy()
pgcp[i] = step
if won(pgcp) != 0: break
n += recur(pgcp, -1*step)
return n
def best(pg): # Поиск лучшего хода
if pg[4] == 0: return 4 # Ход в центр
best = pg.index(0) # Ход болвана
for i in range(9): # Перебор ходов
if pg[i] != 0: continue
pgcp1 = pg.copy()
pgcp1[i] = -1
if won(pgcp1) == -3: return i # Ход приносит победу
x = -3
for j in range(9): # Ход не должен вести к поражению
if pgcp1[j] != 0: continue
pgcp2 = pgcp1.copy()
pgcp2[j] = 1
x = max(x, won(pgcp2))
if x == 3: break
if x < 3:
best = i
return best
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 8. Усовершенствована функция recur().
Перед ходом X: 269173
Перед ходом O: 22850
Перед ходом X: 2983
Перед ходом O: 630
Перед ходом X: 101
Перед ходом O: 19
Перед ходом X: 1
Перед ходом O: 4
Перед ходом X: 1
Перед ходом X: 269173
Перед ходом O: 26852
Перед ходом X: 2424
Перед ходом O: 130
Перед ходом X: 119
Перед ходом O: 52
Перед ходом X: 15
Перед ходом O: 4
Перед ходом X: 1
Терм. 1. Вывод из программы листинг 8.
sddsdf
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = []
playground = [0] * 9 # виртуальное игровое поле
def won(pg): # Подсчёт сумм линий
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): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
if won(playground) != 0: break # Прекратить игру в случае победы
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = best(playground, -1)
if playground[n] != 0: n = playground.index(0) # Ход болвана
def best(pg, step, k=0):
for i in range(9):
if pg[i] != 0: continue
pgcp = pg.copy()
pgcp[i] = step
if won(pgcp) != 0:
return i
k = best(pgcp, -1*step)
return k
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)]
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 7. Функция best() переписана с использованием рекурсии.
В программе листинг 7, функция best() сначала ищет клетку ход в которую принесёт победу ноликам. Если такая клетка не найдена, функция best(), запущенная рекурсивно, ищет клетку, ход в которую помешает победить крестикам. Если же вариантов с победой ноликов или крестиков функция best() не находит, рекурсия продолжается до заполнения игрового поля полностью. При этом, функция best() возвращает 0, а в функции play() на этот случай предусмотрен ход болвана.
Алгоритм Минимакс (из теории игр) рекомендует: уменьшайте выигрыш для противника и увеличивайте выигрыш для себя.
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = [] # Список кнопок
playground = [0] * 9 # виртуальное игровое поле
def won(pg): # Подсчёт сумм линий
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): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
if won(playground) != 0: break # Прекратить игру в случае победы
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = minmax(playground, -1)[1]
def minmax(pg, step): # Поиск лучшего хода
best = [step, None]
if won(pg) != 0 : return best
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
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)] # Размещение кнопки на фрейме
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
Лист. 8. Ровно 50 строк, в том числе 6 пустых строк.
#!/usr/bin/env python3
from tkinter import * # графическая библиотека
btn = [] # Список кнопок
playground = [0] * 9 # виртуальное игровое поле
def won(pg): # Подсчёт сумм линий
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): # функция обработчик нажатия на кнопку
if playground[n] != 0: return
for i in (1, -1):
if won(playground) != 0: break # Прекратить игру в случае победы
btn[n].config(text='X' if i == 1 else 'O') # Ход
playground[n] = i # Ход на виртуальном поле
if not 0 in playground: break
n = minmax(playground, -1)[1]
def minmax(pg, step): # Поиск лучшего хода
best = [step, None]
if won(pg) != 0 : return best
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
for i in range(3):
f = Frame() # Фрейм
f.pack(expand=YES, fill=BOTH) # Вывод фрейма на экран
for j in range(3):
n = i * 3 + j
btn += [Button(f)] # Размещение кнопки на фрейме
btn[n].pack(expand=YES, fill=BOTH, side=LEFT)
btn[n].config(width=4, height=2, font=('times', 36, 'italic'))
btn[n].config(command=lambda n=n: play(n))
mainloop() # главный цикл программы
В функции `minmax()` реализована алгоритм Минimax для поиска лучшего хода в игре. Алгоритм работает рекурсивно, то есть он вызывает себя сам для каждого возможного хода. Функция принимает два параметра: `pg` - текущее состояние виртуального поля (`playground`) и `step` - знак хода (1 или -1). В функции происходит следующая логика:
- Если победа уже достигнута (функция `won()` возвращает не 0), то возвращается лучший ход до победы.
- Если поля не пусто (неcontains zeros), то возвращается 0, потому что игра окончена.
- Иначе, для каждого пустого поля (`i` от 0 до 8) создается копия текущего состояния поля (`pgcp`) и делается ход на это поле.
- Для полученного нового состояния поля вызывается функция `minmax()` с противоположным знаком хода (`-1*step`). Это потому что мы ищем лучший ответ для компьютера, который играет как можно хуже (min).
- Если новый лучший ход обнаружен (значение возвращает меньше или больше чем текущий лучший ход), то он становится новым лучшим ходом.
- В конце функция возвращает лучший ход и номер поля, на котором этот ход должен быть сделан.
В этом алгоритме используется идея "двойной динамизма", когда мы ищем минимальный (min) ответ для компьютера, а затем максимальный (max) ответ для игрока.
"Текст предоставлен llama3 (модель AI)"