Крестики-нолики — логическая игра между двумя противниками на квадратном поле 3 на 3 клетки или большего размера. Цель игры, занять три  клетки в ряд, включая диагонали. Пишем программу игры крестики-нолики на Python с библиотекой tkinter.

Мы публикуем методическую разработку для занятий с детьми старше 12-ти лет в кружке программирование на Python. Цель - научить детей строить логические выражения и пользоваться условным оператором if (elif, else), использовать цикл for, вложенный цикл и операторы continue и break, а также, создавать собственные функции и возвращать из них результат. В конце статьи приведён пример использования алгоритма minimax с рекурсией.

Курс рассчитан на 12 занятий по 2 академических часа с учащимися 5-х классов, при условии наличия у учащихся предварительной подготовки. В качестве предварительного курса, для учащихся не знакомых с программированием, рекомендуем курс "Игра Быки и коровы на Python".

Создание графического интерфейса (GUI)

Выведем в окне приложения несколько кнопок.

from tkinter import *

Button().pack()
Button().pack()
Button().pack()
 
mainloop()

Прог. 1. Создаём три кнопки в окне приложения. 

Рис. 1. Три кнопки в окне приложения. 

from tkinter import *

Button().pack(side=LEFT, expand=YES, fill=BOTH)
Button().pack(side=LEFT, expand=YES, fill=BOTH)
Button().pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 2. Создаём три кнопки прижатые влево в окне приложения. 

Рис. 2. Три кнопки прижаты влево и растягиваются на всё окно приложения. 

from tkinter import *

frm = Frame()
frm.pack(expand=YES, fill=BOTH)
Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 3. Размещаем кнопки во фрейме прижатом вверх в окне приложения. 

from tkinter import *

frm = Frame()
frm.pack(expand=YES, fill=BOTH)
for j in  range(0, 3):
    Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 4. Для создания кнопок используем цикл for из трех повторений. 

from tkinter import *

for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 5. Для создания 9-ти кнопок, используем 2 вложенных цикла из трёх повторов каждый. 

Рис. 3. 9 кнопок, составляющих графический интерфейс игры крестики-нолики. 

Ходилка

from tkinter import *

def play(n):                                # Ход
    n.config(text = 'X')                    # Вывод на кнопку

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn = Button(frm)
        btn.pack(side=LEFT, expand=YES, fill=BOTH)
        btn.config(command = lambda n=btn: play(n))  # Обработчик события
 
mainloop()

Прог. 6. Создаём функцию play() и регистрируем её в качестве обработчика события "нажатие на кнопку". 

Рис. 4. Клик по кнопке выводит крестик на кнопку. 

from tkinter import *

def play(n):                                # Ход
    n.config(text = 'X')                    # Вывод на кнопку

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn.pack(side=LEFT, expand=YES, fill=BOTH)
        btn.config(command = lambda n=btn: play(n))
 
mainloop()

Прог. 7. Добавлены свойства text и font для кнопок.

Рис. 5. Вывод символов на кнопку отформатирован.

from tkinter import *

who = True                                  # Чей ход? True - ход крестиков

def play(n):                                # Ход крестика / нолика
    global who
    n.config(text = 'X' if who else 'O')    # Ход
    who = not(who)                          # Меняем игрока (с Х на О ...)

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn.pack(side=LEFT, expand=YES, fill=BOTH)
        btn.config(command = lambda n=btn: play(n))
 
mainloop()

Прог. 8. Реализация функционала игры крестики-нолики для двух игроков. 

Рис. 6. В игре обнаружен недостаток — возможность смены символов противника на кнопках. 

from tkinter import *

who = True                                  # Чей ход? True - ход крестиков
btn = [0]*9                                 # Для кнопок

def play(n):                                # Ход крестика / нолика
    global who
    if btn[n].cget('text') != ' ': return   # Если клетка не свободна
    btn[n].config(text = 'X' if who else 'O')       # Ход
    who = not(who)                          # Меняем игрока (с Х на О ...)

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 9. Усовершенствована функция play(), кнопки теперь в списке.

Рис. 7.

Думалка

hh

from tkinter import *

who = True                                  # Чей ход? True - ход крестиков
btn = [0]*9                                 # Для кнопок

def play(n):                                # Ход крестика / нолика
    global who
    if btn[n].cget('text') != ' ': return   # Если клетка не свободна
    btn[n].config(text = 'X' if who else 'O')       # Ход
    who = not(who)                          # Меняем игрока (с Х на О ...)
    print(n)

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 10. В функцию play() добавили функцию print(). 

============ RESTART: ~/X0v10.py ============
0
1
2
3
4
5
6
7
8
>>> 

Лист. 1.

Рис. 10.

from tkinter import *

who = True                                  # Чей ход? True - ход крестиков
btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле

def play(n):                                # Ход крестика / нолика
    global who
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X' if who else 'O')       # Ход
    playArea[n] = 1 if who else -1          # Ход на виртуальном поле
    who = not(who)                          # Меняем игрока (с Х на О ...)
    print(playArea)

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 11. Создан список playArea(). 

      

Рис. 11.

=========== RESTART: /home/dior/X0v11.py ===========
[0, 0, 0, 0, 0, 0, 0, 1, 0]
[0, 0, 0, 0, -1, 0, 0, 1, 0]
[0, 0, 0, 0, -1, 1, 0, 1, 0]
[-1, 0, 0, 0, -1, 1, 0, 1, 0]
[-1, 0, 0, 0, -1, 1, 0, 1, 1]
[-1, 0, -1, 0, -1, 1, 0, 1, 1]
[-1, 0, -1, 0, -1, 1, 1, 1, 1]

Лист. 2.

from tkinter import *

who = True                                  # Чей ход? True - ход крестиков
btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def play(n):                                # Ход крестика / нолика
    global who
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X' if who else 'O')       # Ход
    playArea[n] = 1 if who else -1          # Ход на виртуальном поле
    who = not(who)                          # Меняем игрока (с Х на О ...)
    print(won(playArea), standings)

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 12. Создан список standings и функция won(). 

=========== RESTART: /home/dior/X0v12.py ===========
False [0, 1, 0, 0, 0, 1, 0, 0]
False [0, 0, 0, 0, -1, 1, -1, -1]
False [0, 0, 1, 0, 0, 1, -1, -1]
False [-1, 0, 1, -1, 0, 1, -2, -1]
False [-1, 0, 2, -1, 0, 2, -1, -1]
False [-2, 0, 2, -1, 0, 1, -1, -2]
True [-2, 0, 3, 0, 0, 1, -1, -1]

Лист. 3.

Рис. 12.

from tkinter import *

who = True                                  # Чей ход? True - ход крестиков
btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def play(n):                                # Ход крестика / нолика
    global who
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X' if who else 'O')       # Ход
    playArea[n] = 1 if who else -1          # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win' if who else 'O')
    who = not(who)                          # Меняем игрока (с Х на О ...)

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 14. В функцию play() добавили вывод победителя в заголовок окна. 

Рис. 14.

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def play(n):                                # Ход крестика, поиск хода нолика
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win')
        return
    if 0 not in playArea:                   # Если ничья
        tk.title('No won')
        return
    i = playArea.index(0)                   # Логика болвана
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if won(playArea):                       # Если победа
        tk.title('O win')

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 15. Игра с болваном

Рис. 15.

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                         # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def theBest():
    return playArea.index(0)                # Логика болвана

def play(n):                                # Ход крестика, поиск хода нолика
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win')
        return
    if 0 not in playArea:                   # Если ничья
        tk.title('No won')
        return
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if won(playArea):                       # Если победа
        tk.title('O win')

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 16. Добавлена функция theBest(). 

Рис. 16.

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def theBest():
    if playArea[4] == 0: return 4           # Первый ход
    return playArea.index(0)                # Логика болвана

def play(n):                                # Ход крестика, поиск хода нолика
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win')
        return
    if 0 not in playArea:                   # Если ничья
        tk.title('No won')
        return
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if won(playArea):                       # Если победа
        tk.title('O win')

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 17. В функцию theBest() добавлен вариант первого хода ноликов. 

Рис. 17.

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def theBest():
    if playArea[4] == 0: return 4           # Первый ход
    for i in range(9):                      # Если есть возможность победить
        if playArea[i] != 0: continue       # Если клетка не свободна
        A = playArea.copy()                 # Делаем копию виртуального поля
        A[i] = -1                           # Делаем пробный ход
        if won(A): return i                 # Если пробный ход принёс победу
    return playArea.index(0)                # Логика болвана

def play(n):                                # Ход крестика, поиск хода нолика
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win')
        return
    if 0 not in playArea:                   # Если ничья
        tk.title('No won')
        return
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if won(playArea):                       # Если победа
        tk.title('O win')

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 18. В функцию theBest() добавлена возможность победить. 

Рис. 18.

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def theBest():
    if playArea[4] == 0: return 4           # Первый ход
    for i in range(9):                      # Если есть возможность победить
        if playArea[i] != 0: continue       # Если клетка не свободна
        A = playArea.copy()                 # Делаем копию виртуального поля
        A[i] = -1                           # Делаем пробный ход
        if won(A): return i                 # Если пробный ход принёс победу
        best = True                         # Предположим что ход i не плохой
        for j in range(9):                  # Есть возможность проиграть?
            if A[j] != 0: continue          # Если клетка не свободна
            B = A.copy()                    # Делаем копию копии виртуального поля
            B[j] = 1                        # Делаем пробный ход
            if won(B):                      # Победа противника - наше поражение
                best = False                # Xод i оказывается плохой
                break
        if best: return i                   # Если ход i не плохой
    return playArea.index(0)                # Логика болвана

def play(n):                                # Ход крестика, поиск хода нолика
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win')
        return
    if 0 not in playArea:                   # Если ничья
        tk.title('No won')
        return
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if won(playArea):                       # Если победа
        tk.title('O win')

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 19. В функцию play() добавлена возможность не проиграть.

Вилка

В том случае, если игрок создаст на поле вилку, см. рис. 19а, программа 19, просто растеряется. Обнаружив вилку, программа 19 перестанет ходить, так как ситуации, иображённые на рис. 19а, будут обрабатываться в функции play() блоком кода условного оператора elif 2 in standings. Этот блок кода отвечает за ответный ход ноликов в ситуации, когда 2 крестика выстроились в ряд и есть угроза, что следующим ходом крестики одержат победу. Но, во всех трёх случаях с вилкой, представленных на рисунке 19а, при любом ходе ноликов, крестики выигрывают следующим ходом.

  

Рис. 19а. Три варианта вилки.

Очень просто доработать программу 19 так, чтобы её не клинило на вилке. Но, наша цель написать программу которая будет побеждать или играть вничью, а не проигрывать с достоинством. Поэтому, следующей нашей задачей будет написание программного кода, который не допустит возможности появления ситуаций представленных на рисунке 19а.

Предотвратить возможность создания вилки, нолики могут заранее. Рассмотрим варианты игры, см. рис. 19б, предшествовавшие ситуациям, показанным на рис. 19а.

  

Рис. 19б. Состояние на игровом поле перед тем как крестики поставят вилку.

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    global standings
    standings[0] = Ar[0] + Ar[1] + Ar[2]
    standings[1] = Ar[3] + Ar[4] + Ar[5]
    standings[2] = Ar[6] + Ar[7] + Ar[8]
    standings[3] = Ar[0] + Ar[3] + Ar[6]
    standings[4] = Ar[1] + Ar[4] + Ar[7]
    standings[5] = Ar[2] + Ar[5] + Ar[8]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    else: return False

def theBest():
    if playArea[4] == 0: return 4           # Первый ход
    for i in range(9):                      # Если есть возможность победить
        if playArea[i] != 0: continue       # Если клетка не свободна
        A = playArea.copy()                 # Делаем копию виртуального поля
        A[i] = -1                           # Делаем пробный ход
        if won(A): return i                 # Если пробный ход принёс победу
        best = True                         # Предположим что ход i не плохой
        for j in range(9):                  # Есть возможность проиграть?
            if A[j] != 0: continue          # Если клетка не свободна
            B = A.copy()                    # Делаем копию копии виртуального поля
            B[j] = 1                        # Делаем пробный ход
            if won(B) or (standings.count(2)>1 and -2 not in standings):
                best = False                # Xод i оказывается плохой
                break
        if best:  сandidate = i             # Если ход i не плохой
    return сandidate                        # Если следующий ход не победный

def play(n):                                # Ход крестика, поиск хода нолика
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win')
        return
    if 0 not in playArea:                   # Если ничья
        tk.title('No won')
        return
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if won(playArea):                       # Если победа
        tk.title('O win')

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 20. В функцию theBest() добавлена логика игры в случае угрозы вилки и удалена логика Болвана. 

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    for i in range(3):
        standings[i] = Ar[i*3] + Ar[i*3+1] + Ar[i*3+2]
        standings[i+3] = Ar[i] + Ar[i+3] + Ar[i+6]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    return False

def theBest():
    if playArea[4] == 0: return 4           # Первый ход
    for i in range(9):                      # Если есть возможность победить
        if playArea[i] != 0: continue       # Если клетка не свободна
        A = playArea.copy()                 # Делаем копию виртуального поля
        A[i] = -1                           # Делаем пробный ход
        if won(A): return i                 # Если пробный ход принёс победу
        best = True                         # Предположим что ход i не плохой
        for j in range(9):                  # Есть возможность проиграть?
            if A[j] != 0: continue          # Если клетка не свободна
            B = A.copy()                    # Делаем копию копии виртуального поля
            B[j] = 1                        # Делаем пробный ход
            if won(B) or (standings.count(2)>1 and -2 not in standings):
                best = False                # Xод i оказывается плохой
                break
        if best: сandidate = i              # Если ход i не плохой
    return сandidate                        # Если следующий ход не победный

def play(n):                                # Ход крестика, поиск хода нолика
    if won(playArea): return                # Если победа
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if won(playArea):                       # Если победа
        tk.title('X win')
        return
    if 0 not in playArea:                   # Если ничья
        tk.title('No won')
        return
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if won(playArea):                       # Если победа
        tk.title('O win')

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 21. 

Рис. 21.

from tkinter import *

btn = [0]*9                                 # Для кнопок
playArea = [0]*9                            # Виртуальное игровое поле
standings = [0]*8                           # Суммы линий

def won(Ar):                                # Подсчёт сумм линий
    for i in range(3):
        standings[i] = Ar[i*3] + Ar[i*3+1] + Ar[i*3+2]
        standings[i+3] = Ar[i] + Ar[i+3] + Ar[i+6]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    return False

def theBest():
    if playArea[4] == 0: return 4           # Первый ход
    for i in range(9):                      # Если есть возможность победить
        if playArea[i] != 0: continue       # Если клетка не свободна
        A = playArea.copy()                 # Делаем копию виртуального поля
        A[i] = -1                           # Делаем пробный ход
        if won(A): return i                 # Если пробный ход принёс победу
        best = True                         # Предположим что ход i не плохой
        for j in range(9):                  # Есть возможность проиграть?
            if A[j] != 0: continue          # Если клетка не свободна
            B = A.copy()                    # Делаем копию копии виртуального поля
            B[j] = 1                        # Делаем пробный ход
            if won(B) or (standings.count(2)>1 and -2 not in standings):
                best = False                # Xод i оказывается плохой
                break
        if best: сandidate = i              # Если ход i не плохой
    return сandidate                        # Если следующий ход не победный

def test():                                 # Нолик победил или ничья
    if won(playArea) or 0 not in playArea:
        tk.title('O win' if -3 in standings else 'No won')
        return True
    return False

def play(n):                                # Ход крестика, поиск хода нолика
    if test(): return                       # Если конец игры
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if test(): return                       # Если конец игры
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    test()                                  # Если конец игры

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, text=' ', font=('mono', 20, 'bold'))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
        btn[i*3+j].config(command = lambda n=i*3+j: play(n))
 
mainloop()

Прог. 22. 

#!/usr/bin/env python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- 
#
# t-t-t.py (tic tac toe)
# Copyright (C) 2021 Aleksandr Diorditsa, see <https://adior.ru>
# I want to thank all my students for the inspiration they give me.
# I thank Sergey Polozkov and Timur Sharafutdinov, Yana Romanova, Gleb Shalimov
# for checking the code for hidden errors. 
#
# brownian-motion.py is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# t-t-t.py is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

from tkinter import *
from random import *

btn = [0]*9                                 # Для кнопок
first = True                                # Первым ходит нолик

def startGame():
    global first, btn, playArea, standings
    playArea = [0]*9                        # Виртуальное игровое поле
    standings = [0]*8                       # Суммы линий
    for i in btn: i.config(text=' ', font=('mono', 20, 'bold'), width=5, heigh=3)
    if first:
        n = choice(range(9))
        btn[n].config(text = 'O')
        playArea[n] = -1
    first = not first
    tk.title('Tic Tac Toe')

def won(Ar):                                # Подсчёт сумм линий
    for i in range(3):
        standings[i] = Ar[i*3] + Ar[i*3+1] + Ar[i*3+2]
        standings[i+3] = Ar[i] + Ar[i+3] + Ar[i+6]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    return False

def theBest():
    if playArea[4] == 0: return 4           # Первый ход
    for i in range(9):                      # Если есть возможность победить
        if playArea[i] != 0: continue       # Если клетка не свободна
        A = playArea.copy()                 # Делаем копию виртуального поля
        A[i] = -1                           # Делаем пробный ход
        if won(A): return i                 # Если пробный ход принёс победу
        best = True                         # Предположим что ход i не плохой
        for j in range(9):                  # Есть возможность проиграть?
            if A[j] != 0: continue          # Если клетка не свободна
            B = A.copy()                    # Делаем копию копии виртуального поля
            B[j] = 1                        # Делаем пробный ход
            if won(B) or (standings.count(2)>1 and -2 not in standings):
                best = False                # Xод i оказывается плохой
                break
        if best: сandidate = i              # Если ход i не плохой
    return сandidate                        # Если следующий ход не победный

def test():                                 # Нолик победил или ничья
    if won(playArea) or 0 not in playArea:
        tk.title('O win' if -3 in standings else 'No won')
        return True
    return False

def play(n):                                # Ход крестика, поиск хода нолика
    if test():                              # Если конец игры
        startGame()
        return
    if playArea[n] != 0: return             # Если клетка не свободна
    btn[n].config(text = 'X')               # Ход крестика
    playArea[n] = 1                         # Ход на виртуальном поле
    if test(): return                       # Если конец игры
    i = theBest()                           # Оптимальный ход
    btn[i].config(text = 'O')               # Делаем ход
    playArea[i] = -1
    if test(): return                       # Если конец игры

tk = Tk()                                   # Окно программы
for i in  range(0, 3):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 3):
        btn[i*3+j] = Button(frm, command = lambda n=i*3+j: play(n))
        btn[i*3+j].pack(side=LEFT, expand=YES, fill=BOTH)
 
startGame()
mainloop()

Прог. 23 Программа

В программе 23

Алгоритм минимакс

апа

#!/usr/bin/env python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- 
#
# t-t-t.py (tic tac toe)
# Copyright (C) 2021 Aleksandr Diorditsa, see <https://adior.ru>
# I want to thank all my students for the inspiration they give me.
# I thank Sergey Polozkov and Timur Sharafutdinov
# for checking the code for hidden errors. 
#
# brownian-motion.py is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# t-t-t.py is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

from tkinter import *
from random import *

first = True                            # Первым ходит нолик

def won(Ar):                            # Проверка состояния победы
    standings = [0]*8
    for i in range(3):
        standings[i] = Ar[i*3] + Ar[i*3+1] + Ar[i*3+2]
        standings[i+3] = Ar[i] + Ar[i+3] + Ar[i+6]
    standings[6] = Ar[0] + Ar[4] + Ar[8]
    standings[7] = Ar[2] + Ar[4] + Ar[6]
    if 3 in standings or -3 in standings: return True
    return False

def move(n, Ar):                        # Ход в клетку n
    Ar[n] = Ar[9]
    Ar[9] *= -1
    return Ar

def minimax(Ar):
    if won(Ar): return [Ar[9], None]    
    elif 0 not in Ar: return [0, None]  
    best = [Ar[9]*2, None]              
    for i in range(9):                  
        if Ar[i] == 0:                  
            value = minimax(move(i, Ar.copy()))[0]
            if Ar[9] == 1 and value <= best[0]: best = [value, i]
            elif Ar[9] == -1 and value >= best[0]: best = [value, i]
    return best                        

def startGame():
    global playArea, first
    tk.title('tic tac toe')
    for i in range(9): btn[i].config(text = ' ')
    playArea = [0]*9+[1]                # Игровое поле и игрок
    if first:
        n = choice(range(9))
        btn[n].config(text = 'O')
        move(n, playArea)
    first = not first

def play(n):
    if  won(playArea) or 0 not in playArea:
        startGame()
        return
    elif playArea[n] != 0: return
    btn[n].config(text = 'X')
    move(n, playArea)
    if 0 not in playArea or won(playArea): return
    m = minimax(playArea)[1]            # Получить оптимальный ход
    btn[m].config(text = 'O')
    move(m, playArea)
    if won(playArea): tk.title('0 WIN')

tk = Tk()
btn = []
frm = [Frame() for i in range(3)]
for i in frm: i.pack(expand=YES, fill=BOTH)
for i in range(9):
    btn += [Button(frm[i//3], text=' ', font=('mono', 20, 'bold'), width=4, height=3)]
    btn[i].pack(side=LEFT, expand=YES, fill=BOTH)
    btn[i].config(command = lambda n=i: play(n))

startGame()
mainloop()

Прог. 24

Описание функции minimax():

  1. Пусть самый первый ход сделали крестики, в playArea[9] уже записан -1 (для ноликов ищем оптимальный ход).
  2. В функцию won передаём playArea (или его копию) и проверяем была ли после предыдущего хода победа, а затем проверяем, сыграна ли игра в ничью.
    1. Если была победа крестиков или ноликов, функция won возвращает True. Функция minimax возвращает список [-1, None], где -1 лучшая оценка хода крестиков, None — номер клетки не учитывается.
    2. Если предыдущий ход был сделан ноликами и они победили, функция minimax возвращает список [1, None], где 1 лучшая оценка хода ноликов.
    3. Если на предыдущем ходе не было победы и если не осталось свободных клеток, значит это ничья. Функция minimax возвращает список [0, None], где 0 лучшая оценка предыдущего хода, в случае ничьей.
  3. Если игра не закончилась, необходимо выбрать оптимальный ход, для этого:
    1. Присваиваем переменной best значение [-2, None], если следующий ход ноликов и [2, None], если следующий ход крестиков. Это худшая оценка игроков.
    2. Делаем в каждую доступную клетку, в цикле for, пробный ход значением текущего игрока. Пробный ход делается на копии игрового поля playArea. Переменная игрока playArea[9], в функции move, в это же время, инвертируется.
    3. После каждого пробного хода рекурсивно вызывается функция minimax.
    4. Рекурсивные вызовы функции minimax прекращаются по достижении конца игры с победой или вничью.
  4. Только когда все рекурсивные вызовы функции minimax для клетки с номером i запущены, начинается завершение работы последней из этих функций. Функция была последней запущена, первой начинает завершать свою работу, как в стеке.
    1. Функция minimax возвращает оценку последнего хода в дереве ходов для клетки с номером i. Оценка сохраняется в переменной value.
    2. Если формируется оценка хода нолика (playArea[9]) = -1, то лучшей оценкой хода будет большее значение из ранее сохранённого значения в переменной best[0] и текущей оценки сохранённой в переменной value.
    3. Если формируется оценка хода крестика (playArea[9]) = 1, то лучшей оценкой хода будет меньшее значение из ранее сохранённого значения в переменной best[0] и текущей оценки сохранённой в переменной value.
    4. Если на предыдущем этапе (4.2 или 4.3) значение best[0] изменяется, то в best[1] сохраняется значение i — номер клетки для лучшего хода.
  5. Функция завершаемая последней, из функций minimax, вызванных в цикле for и рекурсивно, вернёт в переменной best[1] искомый лучший ход.

Рекомендую материалы: https://gitlab.com/sunilghimire/tic-tac-toe.git

Стивенс Род. Алгоритмы. Теория и практическое применение.

Евгений Корнилов. Программирование шахмат и других логических игр.