Крестики-нолики — логическая игра между двумя противниками на квадратном поле 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). В функции происходит следующая логика:

  1. Если победа уже достигнута (функция `won()` возвращает не 0), то возвращается лучший ход до победы.
  2. Если поля не пусто (неcontains zeros), то возвращается 0, потому что игра окончена.
  3. Иначе, для каждого пустого поля (`i` от 0 до 8) создается копия текущего состояния поля (`pgcp`) и делается ход на это поле.
  4. Для полученного нового состояния поля вызывается функция `minmax()` с противоположным знаком хода (`-1*step`). Это потому что мы ищем лучший ответ для компьютера, который играет как можно хуже (min).
  5. Если новый лучший ход обнаружен (значение возвращает меньше или больше чем текущий лучший ход), то он становится новым лучшим ходом.
  6. В конце функция возвращает лучший ход и номер поля, на котором этот ход должен быть сделан.

В этом алгоритме используется идея "двойной динамизма", когда мы ищем минимальный (min) ответ для компьютера, а затем максимальный (max) ответ для игрока.

"Текст предоставлен llama3 (модель AI)"