Методичка для создания игры на Python с библиотекой Tkinter и с виджетом Canvas. Игра называется Пикассо и Модильяни. Цель игры - захватить большую часть игрового поля.

Графический интерфейс игры.

Играют Игрок и Противник-компьютер (Пикассо и Модильяни), вместе - игроки или художники (artists). Игровое поле состоит из клеток, закрашенных случайным образом в один из 6-ти цветов. Игрок начинает игру с верхнего левого квадрата, Компьютер - с нижнего правого. Художники по очереди выбирают на игровом поле один из цветов. В этот цвет перекрашивается часть поля, захваченная художником и к этой части захваченного поля присоединяются смежные клетки, совпадающие с ним по цвету. Художникам нельзя выбирать цвет своей части поля или цвет противника.

Правила игры сформулированы, ставим задачу на создание графического интерфейса программы. Начнём с поля. Поле состоит из цветных клеток. Для реализации этой части программы нам подойдёт библиотека Tkinter и виджет Canvas. Функция choice() из библиотеки random поможет нам случайным образом выбрать цвет из списка. Пусть в нашей игре игровое поле будет размером 21 х 21 клетку и каждая клетка пусть будет со стороной 30 px (пикселей). Палитра цветов будет содержать 6 цветов.

from tkinter import *                                       # Импортируем все функции из библиотеки tkinter
from random import choice                                   # Импортируем функцию choice из библиотеки random

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

tk = Tk()                                                   # Создаём главное окно программы
tk.title('Picasso and Modigliani')                          # Заголовок главного окна программы
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)             # Создаём объект класса Canvas с именем cnv
cnv.pack(expand=YES, fill=BOTH)                             # Размещаем объект cnv в главном окне программы
for n in range(SIDE*SIDE):                                  # Случайным образом заполняем список playGround
    playGround.append(choice(range(len(COLORSCHEME))))      # значениями из диапазона индексов списка COLORSCHEME.
for y in range(SIDE):                                       # На холсте рисуем квадраты окрашенные цветом соответствующим
    for x in range(SIDE):                                   # цвету на который ссылается элемент playGround
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                             fill=COLORSCHEME[playGround[x+(y*SIDE)]],
                             width=0, tag='rctng')

mainloop()                                                  # Главный цикл программы

Лист. 1. Программа, создающая игровое поле для игры Picasso and Modigliani.

В этой программе, может быть, требует разъяснений:

COLORSCHEME (Цветовая схема) - кортеж кодов цветов, которые будут использоваться для раскрашивания игрового поля.

Код каждого цвета - строковый тип (символы заключены в кавычки), но строка содержит трёхзначное шестнадцатиричное число. Символ # - признак шестнадцатиричного числа. Первая цифра этого числа - уровень красного в итоговом цвете, вторая цифра - уровень зелёного, третья - уровень синего. Цвета смешиваются в заданных пропорциях и мы получаем оттенки. Каждая из трёх компонент цвета (RGB) может иметь до 16 уровней (0 1 2 3 4 5 6 7 8 9 a b c d f). Такая схема кодирования цвета позволяет получать до 16х16х16=4096 цветов и оттенков. Но, нам столько не надо.

playGround = [ ] Игровое поле, в начале пустой список. Далее в программе в список playGround добавляется SIDE*SIDE=21х21=441 элемент. Именно столько клеток на игровом поле.

for n in range(SIDE*SIDE):
    playGround.append(choice(range(len(COLORSCHEME))))

Каждый элемент списка принимает случайное значение из диапазона от 0 до длины кортежа COLORSCHEME. В нашей программе от 0 до 6. 0 включительно 6 исключительно. Всего 6 значений. Каждый элемент списка playGround в дальнейшем будет использоваться как индекс цвета в кортеже COLORSCHEME.

Например, далее в программе:

for y in range(SIDE):
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                             fill=COLORSCHEME[playGround[x+(y*SIDE)]],
                             width=0, tag='rctng')

Метод create_rectangle объекта класса Canvas создаёт на объекте класса Canvas квадрат, заданный координатами верхнего левого угла и координатами правого нижнего угла. Кроме координат, параметром метода create_rectangle является свойство fill. Свойству fill присваивают код цвета в описанном выше формате (уровни RGB в шестнадцатиричном формате в кавычках).

Выражение x+(y*SIDE)вычисляет номер элемента в списке playGround соответствующий клетке в x-овом столбце и y-овой строке на игровом поле.

playGround[x+(y*SIDE)] возвращает индекс цвета для клетки с координатами x, y на игровом поле, а

COLORSCHEME[playGround[x+(y*SIDE)]] возвращает код цвета этой клетки.

tk = Tk() создаёт главное окно программы.

cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) создаёт в главном окне программы объект именованный как cnv класса Canvas с заданными размерами (высота, ширина).

Как работают циклы for in range в Python, можно посмотреть в справочнике. Если кратко:

Функция range() создаёт диапазон, в нашем случае, range(SIDE) создаёт числовой диапазон от 0 до 21. В Цикле for x in range(SIDE):, переменная x принимает каждое значение из этого диапазона и с этим значением выполняется тело цикла. Тело цикла выделено отступом от начала строки.

Рис. 1 Игровое поле для игры Пикассо и Модильяни.

Создание функций.

В процессе игры ситуация на игровом поле будет постоянно меняться и нам потребуется после каждого хода игроков обновлять картинку на экране компьютера, точнее в объекте cnv класса canvas. В нашей программе функции отвечающие за логику игры будут производить изменения в списке playGround в соответствии с игровой ситуацией, а отображать эти изменения на экране будет метод create_rectangle(), рисующий закрашенные квадратики на объекте класса canvas. Выделим код, рисующий квадратики из программы (см листинг 1) в функцию doDraw() с помощью ключевого слова def:

from tkinter import *
from random import choice

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

def doDraw(x, y, w, h):
    '''
    Функция doDraw рисует на холсте цветные прямоугольники
    шириной w, высотой h, колличество прямоугольников x колонок * y строк
    '''
    cnv.delete('rctngl')
    for i in range(y):
        for j in range(x):
            cnv.create_rectangle(j*w, i*h, (j+1)*w, (i+1)*h,
                                 fill=COLORSCHEME[playGround[j+(i*y)]],
                                 width=0, tag='rctngl')

def newGame(x, y, randomList, sourceList):
    '''
    Функция newGame очищает список randomList и добавляет в него x*y элементов
    значения которых случайно выбраны из списка sourceList.
    '''
    randomList.clear()
    for n in range(x*y):
        randomList.append(choice(sourceList))

tk = Tk()
tk.title('Picasso and Modigliani')
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)
cnv.pack(expand=YES, fill=BOTH)
newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))
doDraw(SIDE, SIDE, SIZE, SIZE)

mainloop()

Лист. 2. Программа, создающая игровое поле для игры Picasso and Modigliani.

Функционально программа пока не изменилась, но теперь мы можем перерисовывать игровое поле на экране компьютера, вызывая функцию doDraw(), каждый раз когда это нам будет необходимо.

Обратите внимание, мы дополнили код, рисующий квадратики в программе листинг 1 ещё одной строкой с методом delete() класса canvas. Метод delete() класса canvas удаляет объекты на холсте canvas. В нашем случае (см. листинг 2) методом cnv.delete('rctngl') мы удаляем все объекты с тегом 'rctngl'. У нас в программе тег 'rctngl' был присвоен всем закрашенным квадратикам при их создании методом cnv.create_rectangle(......., tag='rctngl').

Зачем удалять квадратики в нашей программе листинг 2, до того как мы их создали? Да, пока нет в этом необходимости. Но, мы будем пользоваться функцией doDraw() во время работы программы многократно, а она, если не использовать delete() будет рисовать новые закрашенные квадратики поверх старых. На экране всё будет выглядеть правильно, а вот с памятью компьютера будут проблемы. Каждый слой квадратиков формируется в оперативной памяти компьютера и занимает там место. Поэтому, не нужные, устаревшие слои квадратиков мы удаляем методом delete() перед тем как нарисовать новые.

Сыграв одну партию, игрок может быть захочет сыграть ещё раз. Значит, нам необходимо будет запрограммировать повторное заполнение списка playGround случайными числами, индексами цветов из кортежа COLORSCHEME. Мы об этом уже позаботились. Кроме функции doDraw(), в программе листинг 2 мы создали функцию newGame().

Давайте посмотрим, как в нашей программе создаётся игровое поле playGround. В начале, playGround это пустой список. Перед игрой этот список методом append() пополняется новыми элементами со случайными значениями, индексами цветов из кортежа COLORSCHEME. Если перед следующей игрой мы вызовем тот же код, пополняющий новыми элементами игровое поле playGround, то окажется, что в списке playGround в 2 раза больше элементов чем их отображается на экране. Решается эта проблема аналогично, как и проблема расходования памяти в функции doDraw(), но немного другими средствами.

Метод clear(), применённый к объекту класса list (список), очищает список, удаляя все его элементы. В функции newGame() перед наполнением списка playGround новыми элементами, мы очищаем этот список методом clear().

Мы создали 2 функции doDraw() и newGame(). Наша программа из почти линейной, за исключением циклов for (см. листинг 1), превратилась в функциональную. Как теперь работает наша программа? См. листинг 2.

  1. Мы импортируем все функции из библиотеки tkinter и функцию choice из библиотеки random.
  2. Создаём переменные SIDE и SIZE, кортеж COLORSCHEME и пустой список playGround.
  3. Создаём функции doDraw() и newGame().
  4. Создаём главное окно программы и присваиваем ему имя tk.
  5. Создаём в окне программы холст и присваиваем ему имя cnv.
  6. Вызываем функцию newGame(), она заполняет список playGround
  7. Вызываем функцию doDraw(), она рисует на холсте цветные квадратики

Далее программа, последовательно выполняясь, переходит к последней строке программы mainloop() Но на этом выполнение программы не заканчивается. Функция mainloop() завершит свою работу только после того как мы закроем главное окно программы (tk). Функция mainloop() - главный цикл программы основанной на библиотеке Tkinter. Код функции mainloop() выполняется циклически, иногда временно отдавая ресурсы компьютера другим процессам и программам запущенным на компьютере, чтобы они не зависли. Функция mainloop() Следит за событиями, которые происходят с нашей программой, в том числе, и за нажатием на кнопки мыши в окне нашей программы. Если мы правильно настроим нашу программу, то с помощью функции mainloop() будет обнаружено нажатие кнопки мыши в окне нашей программы и управление в нашей программе будет передано функции обработчику события, которую мы ещё должны написать.

Обработчик события.

Игрок будет играть в игру 'Picasso and Modigliani' мышью. Клик мышью по цветному игровому полю (объект canvas) является выбором цвета для следующего хода игрока. Значит, нам следует настроить обработчик события "нажатие левой кнопки мыши". В программе, событие "нажатие левой кнопки мыши" должно вызывать функцию, назовём её play, которая будет анализировать игровую ситуацию, перестраивать список playGround и вызывать функцию doDraw() для отображения изменившейся игровой ситуации на экране.

Начнём реализацию наших планов с малого, настроим обработчик события "нажатие левой кнопки мыши" для объекта canvas, которое будет вызывать заготовку или другими словами, заглушку функции play().

from tkinter import *
from random import choice

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

def play(event):
    '''
    Обработчик щелчка мышью.
    '''
    print(event)

def doDraw(x, y, w, h):
    '''
    Функция doDraw рисует на холсте цветные прямоугольники
    шириной w, высотой h, колличество прямоугольников x колонок * y строк
    '''
    cnv.delete('rctngl')
    for i in range(y):
        for j in range(x):
            cnv.create_rectangle(j*w, i*h, (j+1)*w, (i+1)*h,
                                 fill=COLORSCHEME[playGround[(j)+(i*y)]],
                                 width=0, tag='rctngl')

def newGame(x, y, randomList, sourceList):
    '''
    Функция newGame очищает список randomList и добавляет в него x*y элементов
    значения которых случайно выбраны из списка sourceList.
    '''
    randomList.clear()
    for n in range(x*y):
        randomList.append(choice(sourceList))

tk = Tk()
tk.title('Picasso and Modigliani')
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)
cnv.pack(expand=YES, fill=BOTH)
cnv.bind('<Button-1>', play)                                # Настраиваем обработчик события "Щелчок мышью"
newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))
doDraw(SIDE, SIDE, SIZE, SIZE)

mainloop()

Лист. 3. Программа "Picasso and Modigliani" с заготовкой функции play().

>>> 
 RESTART: artists05.py 
<ButtonPress event state=Mod2 num=1 x=12 y=19>
<ButtonPress event state=Mod2 num=1 x=614 y=16>
<ButtonPress event state=Mod2 num=1 x=615 y=613>
<ButtonPress event state=Mod2 num=1 x=13 y=616>
<ButtonPress event state=Mod2 num=1 x=316 y=318>
>>> 

Вывод сообщений из программы листинг 3 в консоль.

Как видите (см. лист. 3),  настроить обработчик события не сложно. Метод bind объекта класса canvas настраивает обработчик события объекта. Первый параметр метода bind - строка '<Button-1>' идентификатор события "нажатие левой кнопки мыши". Идентификаторов событий в библиотеке Tkinter много. Второй параметр - имя функции, которая будет вызвана в случае возникновения события. Имя функции указывается без круглых скобок и параметров.

В случае возникновения события будет вызвана функция - обработчик события, указанная в bind. При вызове, в обработчик события нельзя передать параметр, если только не воспользоваться лямбда функцией (lambda). Но, само событие формирует набор параметров и мы в обработчике события можем их использовать. Обратите внимание, во время работы программы листинг 3, я 5 раз кликнул мышью в разных углах холста (canvas) и по середине. В результате, в функции play(), функция print(event) вывела в консоль 5 строк с атрибутами (свойствами) события, вызвавшего обработчик play. Среди этих параметров имеется два значения, координаты x и y, которые понадобятся нам для дальнейшей работы над программой "Picasso and Modigliani".

Прежде чем продолжить работу над игрой, сделаем небольшое исследование. Добавьте в функцию play() в программе  листинг 3 еще одну строчку с функцией print(dir(event)). Запустите программу и кликните на игровом поле мышью. Посмотрите вывод сообщений из программы в консоль:

>>> 
 RESTART: artists05.py 
<ButtonPress event state=Mod2 num=1 x=253 y=377>
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'char', 'delta', 'height', 'keycode', 'keysym', 'keysym_num', 'num', 'send_event', 'serial', 'state', 'time', 'type', 'widget', 'width', 'x', 'x_root', 'y', 'y_root']
>>>

Функция dir(event) передала функции print списком все атрибуты события (структуру объекта event). Меня заинтересовали атрибуты 'x', 'y'.

Ещё раз, добавьте в функцию play() в программе  листинг 3 еще одну строчку с функцией print('x=', event.x, 'y=', event.y). Запустите программу и кликните на игровом поле мышью. Посмотрите вывод сообщений из программы в консоль:

>>> 
 RESTART: artists05.py 
<ButtonPress event state=Mod2 num=1 x=168 y=197>
x= 168 y= 197
>>>

Мы нашли свойства объекта event, которые нам понадобятся в нашей программе. event.x и event.y - это координаты точки в которой кликнули мышью. Начало координат, точка (0, 0) находится в левом верхнем углу объекта класса canvas.

Программирование правил игры.

Пусть за соблюдением игроком правил игры отвечает функция play(), см. листинг 3. Напомним эти правила:

  1. В свою очередь, игрок мышью выбирает на игровом поле цвет своего хода.
  2. Цвет хода не должен совпадать ни с текущим цветом игрока ни с текущим цветом противника.
  3. Текущий цвет игрока определяется по цвету клетки расположенной в левом верхнем углу игрового поля.
  4. Текущий цвет противника определяется по цвету клетки расположенной в правом нижнем углу игрового поля.
  5. В свой ход участок игрового поля, захваченный игроком, перекрашивается в выбранный цвет своего хода.
  6. Участок игрового поля, захваченного игроком начинается с клетки расположенной в левом верхнем углу игрового поля.
  7. Участком игрового поля, захваченного игроком, считаются все клетки одного цвета имеющие между собой одну или несколько смежных сторон. 

Перед тем как начать программировать, для наглядности начертим небольшое игровое поле:

y\x 0 1 2 3
0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
3 12 13 14 15

Табл. 1. Игровое поле 4х4 клетки = 16 клеток

  • Игровое поле составляют 16 клеток с числами красного цвета в диапазоне [0 ... 16[, 16 исключено.
  • Числа в верхней строке координата клетки x или номер столбца в диапазоне [0 ... 4[, 4 исключается. 
  • Числа в левой колонке координата клетки y или номер строки в диапазоне [0 ... 4[, 4 исключается.

В программе листинг 3, для игрового поля условно представленного в таблице 1 переменная SIDE = 4, а длина списка playGround SIDE*SIDE=16. Индекс элементов списка playGround в диапазоне  [0 ... 16-1] или [0 ...SIDE*SIDE[.

В математике принято заключать числовые последовательности в квадратные скобки. Квадратная скобка может быть открытая или закрытая (повёрнута к числу или от числа). Открытая квадратная скобка говорит о том, что число не входит в числовой диапазон, а закрытая, что число входит в числовой диапазон.

Принимая во внимание все что сказано о таблице 1, вычислить индекс элемента списка playGround (красное число), зная номер строки y и столбца x можно по формуле n = x + y * SIDE. Где SIDE = числу столбцов.

В программе мы будем знать координаты event.x и event.y представленные в пикселях (точки составляющие изображение на экране). Чтобы получить номер столбца или строки необходимо координаты event.x и event.y поделить нацело на размер клетки игрового поля в пикселях (на значение переменной SIZE).

Теперь мы можем вывести формулу индекса элемента списка playGround, вычисляемого по координатам точки на игровом поле, в которой игрок кликнул мышью.

n = event.x//SIZE + event.y//SIZE*SIDE

Операция // в Python - деление нацело.

from tkinter import *
from random import choice

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

def play(event):
    '''
    Обработчик щелчка мышью, высисляет для списка playGround индекс, соответствующий клетке
    по которой щёлкнул игрок.
    '''
    n = event.x//SIZE + event.y//SIZE*SIDE                  # Номер клетки в списке PLAYGROUND
    if playGround[n] != playGround[0] and playGround[n] != playGround[SIDE*SIDE-1]:
        playGround[0] = playGround[n]                       # Перекрашиваем одну клетку.
    doDraw(SIDE, SIDE, SIZE, SIZE)

def doDraw(x, y, w, h):
    '''
    Функция doDraw рисует на холсте цветные прямоугольники
    шириной w, высотой h, колличество прямоугольников x колонок * y строк
    '''
    cnv.delete('rctngl')
    for i in range(y):
        for j in range(x):
            cnv.create_rectangle(j*w, i*h, (j+1)*w, (i+1)*h,
                                 fill=COLORSCHEME[playGround[(j)+(i*y)]],
                                 width=0, tag='rctngl')

def newGame(x, y, randomList, sourceList):
    '''
    Функция newGame очищает список randomList и добавляет в него x*y элементов
    значения которых случайно выбраны из списка sourceList.
    '''
    randomList.clear()
    for n in range(x*y):
        randomList.append(choice(sourceList))

tk = Tk()
tk.title('Picasso and Modigliani')
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)
cnv.pack(expand=YES, fill=BOTH)
cnv.bind('<Button-1>', play)
newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))
doDraw(SIDE, SIDE, SIZE, SIZE)

mainloop()

Лист. 4. Программа "Picasso and Modigliani" с функцией play() для 1 игрока.

Теперь наша программа листинг 4 следит за соблюдением игроком правил выбора цвета и перекрашивает клетку игрока в верхнем левом углу в выбранный цвет.

В программе листинг 4 оператор условного выполнения if проверяет логическое условие следующее за ключевым словом if и если условие истинно, то выполняется следующий блок операторов. Срока if playGround[n] != playGround[0] and playGround[n] != playGround[SIDE*SIDE-1]: читается так - "Если playGround[n] не равноplayGround[0] и playGround[n] не равно playGround[SIDE*SIDE-1], то выполнить следующий блок операторов". Напомню, блоки выделяются в программе отступом. Если логическое условие оператора if ложно, то следующий блок операторов будет пропущен и интерпретатор Python перейдёт к выполнению операторов следующих за этим блоком.

Функция с рекурсией.

Программа листинг 4 не выполняет в полной мере все 7 пунктов правил которые мы запланировали реализовать на этапе её создания. А в частности, пункт 5 "В свой ход участок игрового поля, захваченный игроком, перекрашивается в выбранный цвет своего хода". Программа листинг 4 перекрашивает только одну клетку, а должна перекрашивать все клетки одного цвета имеющие смежные стороны с клеткой в левом верхнем углу.

Прежде чем приступить к программированию, еще раз рассмотрим таблицу:

y\x 0 1 2 3
0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
3 12 13 14 15

Табл. 2. Игровое поле 4х4 клетки = 16 клеток

В игровой ситуации, представленной в таблице 2, чтобы перекрасить все клетки жёлтого цвета, имеющие смежные стороны с клеткой 0, в программе необходимо проверить:

  1. Если клетка 1 такого же цвета что и клетка 0,
  2. если клетка 5 такого же цвета что и клетка 0,
  3. если клетка 5 такого же цвета что и клетка 0,
  4. если клетка 9 такого же цвета что и клетка 0,
  5. если клетка 10 такого же цвета что и клетка 0,
  6. перекрасить все эти клетки.
  7. А так же необходимо проверить все соседние клетки с клетками, которые уже прошли проверку на соседство и цвет. 

То есть в ситуации, представленной в таблице 2, кроме перечисленных выше, должны будут пройти проверку на совпадение цвета с клеткой 0 клетки 4, 8, 13, 14, 11, 6, 2.

Перед нами стоит задача, в общем случае, начиная с клетки 0 искать соседние клетки с таким же цветом и перекрашивать, а для каждой найденной и перекрашенной клетки опять искать всех соседей с таким же цветом, какой был вначале у клетки 0 и та далее ... Следует заметить, что соседями клетки в середине поля с координатами (x, y) являются 4 клетки. Это:

  • клетка справа с координатами (x+1, y),
  • клетка слева с координатами (x-1, y),
  • клетка сверху с координатами (x, y-1),
  • клетка снизу с координатами (x, y+1).

Так же, программа должна следить за тем, чтобы координаты проверяемой клетки не выходили за пределы игрового поля.То есть x и y может принимать значения только из числового диапазона [0 ... SIDE[.

Назовём функцию, которая будет выполнять эту задачу doPaint(n). Где параметры (x, y) координаты клетки, которую следует проверить на смежность с полем игрока и перекрасить если она пройдёт эту проверку.

from tkinter import *
from random import choice

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

def play(event):
    '''
    Обработчик щелчка мышью, высисляет для списка playGround индекс, соответствующий клетке
    по которой щёлкнул игрок.
    '''
    def doPaint(x, y, oldColor, nextColor):                                      # Author of this algorithm : Diorditsa A.
        '''
        Перекрашивает смежные клетки одного цвета.
        '''
        if x in range(0, SIDE) and y in range(0, SIDE) and playGround[x+y*SIDE] == oldColor:
            playGround[x+y*SIDE] = nextColor                # Перекрашиваем клетку если она смежна и одного цвета с полем игрока.
            doPaint(x+1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки справа
            doPaint(x-1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки слева
            doPaint(x, y+1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки снизу
            doPaint(x, y-1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки сверху

    n = event.x//SIZE + event.y//SIZE*SIDE                  # Номер клетки в списке playGround
    if playGround[n] != playGround[0] and playGround[n] != playGround[SIDE*SIDE-1]:
        doPaint(0, 0, playGround[0], playGround[n])
    doDraw(SIDE, SIDE, SIZE, SIZE)

def doDraw(x, y, w, h):
    '''
    Функция doDraw рисует на холсте цветные прямоугольники
    шириной w, высотой h, колличество прямоугольников x колонок * y строк
    '''
    cnv.delete('rctngl')
    for i in range(y):
        for j in range(x):
            cnv.create_rectangle(j*w, i*h, (j+1)*w, (i+1)*h,
                                 fill=COLORSCHEME[playGround[j+(i*y)]],
                                 width=0, tag='rctngl')

def newGame(x, y, randomList, sourceList):
    '''
    Функция newGame очищает список randomList и добавляет в него x*y элементов
    значения которых случайно выбраны из списка sourceList.
    '''
    randomList.clear()
    for n in range(x*y):
        randomList.append(choice(sourceList))

tk = Tk()
tk.title('Picasso and Modigliani')
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)
cnv.pack(expand=YES, fill=BOTH)
cnv.bind('<Button-1>', play)
newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))
doDraw(SIDE, SIDE, SIZE, SIZE)

mainloop()

Лист. 5. Программа "Picasso and Modigliani" с рекурсивной функцией doPaint() для 1 игрока.

Функция doPaint(x, y, oldColor, nextColor) работает следующим образом:

  • Условный оператор if проверяет не выходят ли координаты x и y, переданные функции при запуске за пределы игрового поля,
  • и имеет ли клетка с координатами (x, y) тот же цвет, что был у поля игрока до запуска функции doPaint(), старый цвет игрока.
  • Если условие выполняется, то:
    • Заданную координатами (x, y) клетку находят в списке playGround и перекрашивают в новый цвет,
    • Снова запускают функцию doPaint для всех клеток смежных с клеткой с координатами (x, y).
    • А если какие либо смежные клетки будут удовлетворять условиям в операторе if, то:
      • уже для всех их смежных клеток опять будет запущена функция doPaint.
  • Рекурсивный вызов функции doPaint прекращается тогда, когда условие в операторе if не выполняется (принимает значение False).

Вызов функцией самой себя называют рекурсивным вызовом функции. 

Наша программа листинг 5 ещё не умеет играть против игрока автоматически, но уже может представлять интерес. На этом этапе создания программы "Picasso and Modigliani" хотелось бы дополнить графический интерфейс программы функционалом. А именно, необходимо добавить возможность запускать программу с начала. Снабдим нашу программу "Picasso and Modigliani" кнопкой старт.

Кнопка "Start".

У объектов класса canvas из библиотеки Tkinter есть метод create_text() с помощью которого можно разместить на холсте текст. Параметрами метода create_text() являются координаты (x, y) на холсте, свойство text, anchor, tag и ещё несколько параметров.

from tkinter import *
from random import choice

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

def play(event):
    '''
    Обработчик щелчка мышью, высисляет для списка playGround индекс, соответствующий клетке
    по которой щёлкнул игрок.
    '''
    def doPaint(x, y, oldColor, nextColor):                                      # Author of this algorithm : Diorditsa A.
        '''
        Перекрашивает смежные клетки одного цвета.
        '''
        if x in range(0, SIDE) and y in range(0, SIDE) and playGround[x+y*SIDE] == oldColor:
            playGround[x+y*SIDE] = nextColor                # Перекрашиваем клетку если она смежна и одного цвета с полем игрока.
            doPaint(x+1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки справа
            doPaint(x-1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки слева
            doPaint(x, y+1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки снизу
            doPaint(x, y-1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки сверху

    n = event.x//SIZE + event.y//SIZE*SIDE                  # Номер клетки в списке playGround
    if n == 0:
        newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))                        # Новая игра
    elif playGround[n] != playGround[0] and playGround[n] != playGround[SIDE*SIDE-1]:
        doPaint(0, 0, playGround[0], playGround[n])                                     # Ход игрока
    doDraw(SIDE, SIDE, SIZE, SIZE)

def doDraw(x, y, w, h):
    '''
    Функция doDraw рисует на холсте цветные прямоугольники
    шириной w, высотой h, колличество прямоугольников x колонок * y строк
    '''
    cnv.delete('rctngl')
    for i in range(y):
        for j in range(x):
            cnv.create_rectangle(j*w, i*h, (j+1)*w, (i+1)*h,
                                 fill=COLORSCHEME[playGround[(j)+(i*y)]],
                                 width=0, tag='rctngl')
    cnv.tkraise('txtStart')                                 # Поднять текст "Start" в верхний слой холста.

def newGame(x, y, randomList, sourceList):
    '''
    Функция newGame очищает список randomList и добавляет в него x*y элементов
    значения которых случайно выбраны из списка sourceList.
    '''
    randomList.clear()
    for n in range(x*y):
        randomList.append(choice(sourceList))

tk = Tk()
tk.title('Picasso and Modigliani')
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)
cnv.pack(expand=YES, fill=BOTH)
cnv.bind('<Button-1>', play)
cnv.create_text(SIZE//2, SIZE//2, text='Start', anchor=CENTER, tag='txtStart')          # Сделать надпись "Start"
newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))
doDraw(SIDE, SIDE, SIZE, SIZE)

mainloop()

Лист. 6. Программа "Picasso and Modigliani" с кнопкой 'Start' для 1 игрока.

В программе листинг 6 методом create_text(SIZE//2, SIZE//2, text='Start', anchor=CENTER, tag='txtStart') мы добавили на холсте cnv в левый верхний угол надпись 'Start' и привязали эту надпись к тегу 'txtStart'. Чтобы эту надпись не закрывали цветные клетки, мы в функции doDraw(), в самом её конце, методом объекта canvas tkraise('txtStart') поднимаем слой с надписью на верхний уровень. Метод tkraise() объекта canvas поднимает объекты, перечисленные как аргументы метода tkraise() по именам или тегам, в самый верхний слой холста.

В функцию play() мы добавили вызов функции newGame() при условии, что игрок кликнул мышью по клетке 0, которая находится в левом верхнем углу холста. На этой же клетке имеется надпись 'Start'.

Игру "Picasso and Modigliani" для 1 игрока мы написали. Пора переходить к написанию той части программы, которая будет играть за компьютер против игрока в автоматическом режиме.

Функция autoPlay().

Напишем функцию autoPlay(), которая будет играть за компьютер против игрока в автоматическом режиме.

from tkinter import *
from random import choice

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

def play(event):
    '''
    Обработчик щелчка мышью, высисляет для списка playGround индекс, соответствующий клетке
    по которой щёлкнул игрок.
    '''
    def doPaint(x, y, oldColor, nextColor):                                      # Author of this algorithm : Diorditsa A.
        '''
        Перекрашивает смежные клетки одного цвета.
        '''
        if x in range(0, SIDE) and y in range(0, SIDE) and playGround[x+y*SIDE] == oldColor:
            playGround[x+y*SIDE] = nextColor                # Перекрашиваем клетку если она смежна и одного цвета с полем игрока.
            doPaint(x+1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки справа
            doPaint(x-1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки слева
            doPaint(x, y+1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки снизу
            doPaint(x, y-1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки сверху

    def autoPlay():
        '''
        Выбирает допустимое значение индекса для списка COLORSCHEME случайным образом.
        '''
        k = [k for k in range (len(COLORSCHEME)) if k != playGround[0] and k != playGround[SIDE*SIDE-1]]
        return choice(k)
    
    n = event.x//SIZE + event.y//SIZE*SIDE                  # Номер клетки в списке playGround
    if n == 0:
        newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))                            # Новая игра
    elif playGround[n] != playGround[0] and playGround[n] != playGround[SIDE*SIDE-1]:
        doPaint(0, 0, playGround[0], playGround[n])                                         # Ход игрока
        doPaint(SIDE-1, SIDE-1, playGround[SIDE*SIDE-1], autoPlay())                        # Ход компьютера
    doDraw(SIDE, SIDE, SIZE, SIZE)

def doDraw(x, y, w, h):
    '''
    Функция doDraw рисует на холсте цветные прямоугольники
    шириной w, высотой h, колличество прямоугольников x колонок * y строк
    '''
    cnv.delete('rctngl')
    for i in range(y):
        for j in range(x):
            cnv.create_rectangle(j*w, i*h, (j+1)*w, (i+1)*h,
                                 fill=COLORSCHEME[playGround[(j)+(i*y)]],
                                 width=0, tag='rctngl')
    cnv.tkraise('txtStart')

def newGame(x, y, randomList, sourceList):
    '''
    Функция newGame очищает список randomList и добавляет в него x*y элементов
    значения которых случайно выбраны из списка sourceList.
    '''
    randomList.clear()
    for n in range(x*y):
        randomList.append(choice(sourceList))

tk = Tk()
tk.title('Picasso and Modigliani')
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)
cnv.pack(expand=YES, fill=BOTH)
cnv.bind('<Button-1>', play)
cnv.create_text(SIZE//2, SIZE//2, text='Start', anchor=CENTER, tag='txtStart')
newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))
doDraw(SIDE, SIDE, SIZE, SIZE)

mainloop()

Лист. 7. Программа "Picasso and Modigliani" с примитивной функцией autoPlay().

Мы написали функцию autoPlay(), она выбирает цвет для следующего хода компьютера и возвращает его с помощью оператора return. Делается выбор цвета случайным образом из допустимых значений с помощью функции choice() из библиотеки random. Для временного хранения допустимых значений индексов цвета создан локальный список k.

k = [k for k in range (len(COLORSCHEME)) if k != playGround[0] and k != playGround[SIDE*SIDE-1]]

Всего индексов цветов у нас столько, сколько кодов цветов записано в кортеже COLORSCHEME. Функция len(COLORSCHEME) возвращает длину кортежа COLORSCHEME. Из диапазона range (0, len(COLORSCHEME)) исключаются индексы playGround[0] и playGround[SIDE*SIDE-1] (цвет игрока и цвет противника). Теперь, в функции autoPlay() игрок - это компьютер, а противник - это человек.

Запускаем программу листинг 7. Играем и обнаруживаем, случайный выбор цвета компьютером из допустимых значений - не лучшая стратегия. Компьютер плохо играет.

Усовершенствованная стратегия игры для компьютера.

Попробуем переписать функцию autoPlay() таким образом, чтобы компьютер перебирал все допустимые цвета для своего хода и считал сколько для каждого цвета можно присоединить новых клеток за один ход. Затем, из полученного списка, функция autoPlay() должна выбрать лучший результат и вернуть индекс цвета. С этим, выбранным в функции autoPlay() цветом должна сыграть функция doPaint(x, y, oldColor, nextColor).

Возможно, для поиска лучшего результата можно будет воспользоваться нашей функцией doPaint(), если она сможет считать сколько присоединилось к полю игрока новых клеток.

Обратите внимание, в функции autoPlay() С помощью ключевого слова global мы объявили как глобальный список playGround.  Глобальная переменная или объект может быть использована в любой части программы.

Добавим в нашу программу листинг 8 в функции play() новую переменную countRecursion в которой функция doPaint() будет подсчитывать сколько клеток удалось перекрасить за ход. С помощью ключевого слова nonlocal укажем в функции doPaint() что переменная countRecursion для этой функции не является локальной и здесь переменную countRecursion не следует создавать заново. Не локальные переменные, не являясь глобальными, не создаются заново в функции в которой они объявлены как nonlocal.

Тогда уж необходимо сказать какие переменные являются локальными. Локальные переменные создаются внутри функции и используются (видны) только в этой функции. В функции autoPlay() созданы три локальных списка rating = [ ], savePlayGround = list(playGround) и,

k = [k for k in range (len(COLORSCHEME)) if k != playGround[0] and k != playGround[SIDE*SIDE-1]]

которые будут использоваться только в функции autoPlay() и будут уничтожены по завершению работы этой функции.

Основная и единственная задача функции autoPlay() оптимально выбрать цвет для следующего хода компьютера.

#!/usr/bin/python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width:$

# Picasso and Modigliani (artists)
# This is my version of the game, known as Filler.
#
# Created on July 10, 2021.
# Author of this program code : Diorditsa A.
# I thank Sergey Polozkov for checking the code for hidden errors
#
# artists.py is distributed in the hope that it will be useful, but
# WITHOUT WARRANTY OF ANY KIND; not even an implied warranty
# MARKETABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See. See the GNU General Public License for more information.
# You can get a copy of the GNU General Public License
# by link http://www.gnu.org/licenses/

from tkinter import *
from random import choice

SIDE = 21; SIZE = 30                                        # Размер поля в клетках, размер клетки в пикселях
COLORSCHEME = ('#f00','#0f0','#00f','#ff0','#d7f','#988')   # RGB
playGround = []                                             # Цветное игровое поле, int

def play(event):
    '''
    Обработчик щелчка мышью, высисляет для списка playGround индекс, соответствующий клетке
    по которой щёлкнул игрок.
    '''
    countRecursion = 0                                      # Счётчик перекрашиваний
    def doPaint(x, y, oldColor, nextColor):                 # Author of this algorithm : Diorditsa A.
        '''
        Перекрашивает смежные клетки одного цвета.
        '''
        if x in range(0, SIDE) and y in range(0, SIDE) and playGround[x+y*SIDE] == oldColor:
            playGround[x+y*SIDE] = nextColor                # Перекрашиваем клетку если она смежна и одного цвета с полем игрока.
            nonlocal countRecursion                         # Счётчик перекрашиваний
            countRecursion += 1                             # Считаем сколько клеток удалось перекрасить
            doPaint(x+1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки справа
            doPaint(x-1, y, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки слева
            doPaint(x, y+1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки снизу
            doPaint(x, y-1, oldColor, nextColor)            # Рекурсивно запускаем функцю doPaint(x, y) для клетки сверху

    def autoPlay():
        '''
        Перебирает допустимые значения индексов для списка COLORSCHEME в поисках лучшего.
        '''
        global playGround
        savePlayGround = list(playGround)                   # Копия цветного игрового поля
        rating = []
        k = [k for k in range (len(COLORSCHEME)) if k != playGround[0] and k != playGround[SIDE*SIDE-1]]
        for nextColor in k:                                 # k список допустимых цветов
            nonlocal countRecursion                         # Счётчик перекрашиваний
            countRecursion = 0
            doPaint(SIDE-1, SIDE-1, playGround[SIDE*SIDE-1], nextColor)
            doPaint(SIDE-1, SIDE-1, nextColor, savePlayGround[SIDE*SIDE-1])
            rating.append(countRecursion)                   # Добавляем в список рейтинг i-того цвета из списка k
            playGround = list(savePlayGround)               # Восстанавливаем список playGround из копии
        return(k[rating.index(max(rating))])                # Ищем цвет с наибольшим рейтингом
    
    n = event.x//SIZE + event.y//SIZE*SIDE                  # Номер клетки в списке playGround
    if n == 0:
        newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))                            # Новая игра
    elif playGround[n] != playGround[0] and playGround[n] != playGround[SIDE*SIDE-1]:
        doPaint(0, 0, playGround[0], playGround[n])                                         # Ход игрока
        doPaint(SIDE-1, SIDE-1, playGround[SIDE*SIDE-1], autoPlay())                        # Ход компьютера
    doDraw(SIDE, SIDE, SIZE, SIZE)

def doDraw(x, y, w, h):
    '''
    Функция doDraw рисует на холсте цветные прямоугольники
    шириной w, высотой h, колличество прямоугольников x колонок * y строк
    '''
    cnv.delete('rctngl')
    for i in range(y):
        for j in range(x):
            cnv.create_rectangle(j*w, i*h, (j+1)*w, (i+1)*h,
                                 fill=COLORSCHEME[playGround[(j)+(i*y)]],
                                 width=0, tag='rctngl')
    cnv.tkraise('txtStart')

def newGame(x, y, randomList, sourceList):
    '''
    Функция newGame очищает список randomList и добавляет в него x*y элементов
    значения которых случайно выбраны из списка sourceList.
    '''
    randomList.clear()
    for n in range(x*y):
        randomList.append(choice(sourceList))

tk = Tk()
tk.title('Picasso and Modigliani')
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE)
cnv.pack(expand=YES, fill=BOTH)
cnv.bind('<Button-1>', play)
cnv.create_text(SIZE//2, SIZE//2, text='Start', anchor=CENTER, tag='txtStart')
newGame(SIDE, SIDE, playGround, range(len(COLORSCHEME)))
doDraw(SIDE, SIDE, SIZE, SIZE)
tk.resizable(False, False)

mainloop()

Лист. 8. Программа "Picasso and Modigliani", файл artists.py

В программе листинг 8 функция autoPlay() перебирает все 4 доступных для хода компьютера цвета и для каждого из этих цветов дважды запускает функцию doPaint(x, y, oldColor, nextColor). Функция  doPaint(x, y, oldColor, nextColor) вносит изменения в список playGround.

Зачем функция autoPlay() дважды вызывает функцию doPaint(x, y)? Вызывая дважды функцию doPaint(x, y) функция autoPlay() играет с четырьмя цветами только на 1 шаг вперёд. Первый вызов doPaint(x, y) перекрашивает уже захваченное поле в выбранный цвет, а второй вызов doPaint(x, y) подсчитывает в переменной countRecursion количество перекрашенных и присоединённых клеток выбранного цвета.

В функции autoPlay() после вызова функции doPaint(x, y) дважды, восстанавливается из резервной копии список playGround = list(savePlayGround), далее оператор for выбирает из списка k следующий цвет и уже для него подсчитывает в переменной countRecursion количество перекрашенных и присоединённых клеток.

Так же, в цикле for для каждого цвета из списка k в список rating добавляется элемент со значением равным количеству перекрашенных и присоединённых клеток. Из этого списка rating в конце работы функции autoPlay() оператор return(k[rating.index(max(rating))]) выбирает наибольшее число, определяет его индекс (номер в списке) и возвращает значение из списка k с этим индексом.

Теперь компьютер играет довольно сносно, но не дальновидно.

Рис. 2. Игра Picasso and Modigliani.

Эта программа написана мной под впечатлением от просмотра фильма "Модильяни" снятого Миком Дэвисом.