Пег солитер (peg solitare) — это настольная игра-головоломка для одного игрока, в которой переставляются шарики на доске. Эта игра известна с 1697 года.

С особой любовью к этой игре относился французский король Людовик XIV.

В США игра имеет название Peg Solitaire (колышковый пасьянс). В Великобритании —  Solitaire . В Индии — Brainvita. В СССР — Йога.

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

В игре используется доска с клетками для размещения шариков. Существует английская и европейская доска. Мы выбираем английскую доску:

Рис. 1. Английская доска с шариками для игры в Peg. 

Для написания программы игры Peg нам понадобятся изображения шариков в png формате:

Рис. 2. Изображения шариков в png формате.

Правила игры

Разрешён ход только по вертикали или по горизонтали и только с прыжком через соседний шарик на пустое место. После хода, шарик через который перепрыгнули снимается с доски.

Игра заканчивается, если у Вас не осталось разрешённых ходов.

В классическом «Солитере Пег» игрок побеждает, когда на поле остаётся только одна фишка. Победа считается виртуозной, если последняя фишка оказывается в центре. Настоящие профи способны продумать ходы так, что в конце игры в центр попадает тот шарик, которым была начата игра.

Цель игры — освободить всю доску от шариков, оставив последний шарик в центре доски.

Игровое поле представляет из себя квадрат, поделённый на клетки. Семь клеток по горизонтали на семь клеток по вертикали. 

from tkinter import *

colores = ['#aa9', '#776', '#ccd']              # Цвета поля
SIDE=7; SIZE=60; R=25                           # Размер поля в клетках, клетки в пикселях, радиус шарика

cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) # Объект класса Canvas с именем cnv
cnv.pack()
for y in range(SIDE):                           # Отрисовка поля
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                    fill=colores[0], outline=colores[2], width=1)
mainloop()

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

В программе 1 для создания игрового поля мы используем виджет Canvas из библиотеки tkinter. Цвет фона клеток, цвет клеток не доступных в игре и цвет линий  разделяющих клетки, определяем в списке colores. Константы SIDE и SIZE определяют количество клеток на стороне поля и размер каждой клетки. Константа R имеет значение равное радиусу шарика.

В  двух циклах for методом create_rectangle() класса Canvas мы рисуем игровое поле. Цикл for с переменной цикла x вложен в цикл for с переменной y. Параметры x и y являются порядковыми номерами создаваемого квадрата по горизонтали и вертикали, из которых легко вычислить физические координаты квадрата в пикселях. 

Параметрами метода create_rectangle являются координаты верхнего левого угла и правого нижнего угла квадрата (4 параметра). Так же, цвет квадрата, цвет границы и толщина границы квадрата (3) параметра. 

Рис. 3. Игровое поле.

from tkinter import *

colores = ['#aa9', '#776', '#ccd']              # Цвета поля
SIDE=7; SIZE=60; R=25                           # Размер поля в клетках, клетки в пикселях, радиус шарика
playArea = [0]*SIDE*SIDE                        # Игровое поле
for i in [0, 1, 5, 6, 7, 8, 12, 13, 35, 36, 40, 41, 42, 43, 47, 48]: playArea[i]=-1

cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) # Объект класса Canvas с именем cnv
cnv.pack()
for y in range(SIDE):                           # Отрисовка поля
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                    fill=colores[0 if playArea[x+y*SIDE] == 0 else 1], outline=colores[2], width=1)
mainloop()

Программа 2. Создание игрового поля.

В программу 2 мы добавили список playArea. Список playArea — это виртуальное игровое поле, каждый элемент этого списка содержит информацию о состоянии одной клетки на игровом поле (доске для игры peg). Количество элементов в списке playArea соответствует колличеству клеток на доске для игры peg.

Если элемент списка playArea имеет значение:

-1, клетка, соответствующая этому элемету находится за пределами игрового поля и ставить на неё шарики запрещено.

0 — клетка не занята. 

>0 — номер (ID) установленного на клетку шарика.

В программе 2, в свойстве fill  при создании объкта класса cnv.create_rectangle происходит выбор цвета клетки игрового поля в зависимости от значения, записанного в список playArea. Цвет выбирается из списка colores. 

Рис. 4. На игровом поле выделены клетки, которые не участвуют в игре.

from tkinter import *
from random import *
file = ['red.png', 'yellow.png', 'gold.png', 'green.png', 'emerald.png', 'cyan.png', 'blue.png', 'pink.png',
        'azure.png', 'bronze.png', 'purple.png', 'scarlet.png', 'steel.png', 'silver.png']
colores = ['#aa9', '#776', '#ccd']              # Цвета поля
SIDE=7; SIZE=60; R=25                           # Размер поля в клетках, клетки в пикселях, радиус шарика
playArea = [0]*SIDE*SIDE                        # Игровое поле
for i in [0, 1, 5, 6, 7, 8, 12, 13, 35, 36, 40, 41, 42, 43, 47, 48]: playArea[i]=-1

def newGame(A=[10, 16, 17, 18, 24, 31]):
    global playArea, file, img
    img = [PhotoImage(file=i) for i in file]
    playArea = list(map(lambda x: x if x<0 else 0, playArea))   # Очистка поля
    cnv.delete('allBalls')                                      # Удаление шариков
    for i in A:                                                 # Размещение шариков
        x = (i % SIDE) * SIZE + SIZE / 2
        y = (i // SIDE) * SIZE + SIZE / 2
        playArea[i] = cnv.create_image(x, y, image=choice(img), anchor=CENTER, tag='allBalls')

cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) # Объект класса Canvas с именем cnv
cnv.pack()
for y in range(SIDE):                                           # Отрисовка поля
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                    fill=colores[0 if playArea[x+y*SIDE] == 0 else 1], outline=colores[2], width=1)
newGame()
mainloop()

Программа 3. Размещение фигур на игровом поле.

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

В функции newGame(), для очистки списка playArea мы использовали функцию map().

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

Функцию очищающую список playArea мы написали с помощью lambda функции. Наша lambda функция в функции map() возвращает 0 для тех элементов списка playArea, которые содержали номера шариков (номера объектов canvas), то есть, обнуляет элементы содержащие значения больше 0.

Объект map мы опять превращаем в список с помощью функции list(). 

Рис. 5. На игровом поле расставлены шарики.

from tkinter import *
from random import *
file = ['red.png', 'yellow.png', 'gold.png', 'green.png', 'emerald.png', 'cyan.png', 'blue.png', 'pink.png',
        'azure.png', 'bronze.png', 'purple.png', 'scarlet.png', 'steel.png', 'silver.png']
colores = ['#aa9', '#776', '#ccd']              # Цвета поля
SIDE=7; SIZE=60; R=25                           # Размер поля в клетках, клетки в пикселях, радиус шарика
playArea = [0]*SIDE*SIDE                        # Игровое поле
for i in [0, 1, 5, 6, 7, 8, 12, 13, 35, 36, 40, 41, 42, 43, 47, 48]: playArea[i]=-1

def ifplay(event):                                              # Разрешение движения
    global num, dx, dy
    num = event.x // SIZE + event.y // SIZE * SIDE              # Исходная клетка
    if playArea[num] > 0:                                       # Если клетка с шариком
        dx = int(event.x - cnv.coords(playArea[num])[0])        # Смещение курсора
        dy = int(event.y - cnv.coords(playArea[num])[1])        # относительно шарика
        cnv.tkraise(playArea[num])                              # Поднять шарик на верхний слой
        cnv.bind("<B1-Motion>", play)                           # Начать движение
        cnv.bind("<B1-ButtonRelease>", moveEnd)                 # Конец движения

def play(event):                                                # Начало движения
    x = int(event.x-cnv.coords(playArea[num])[0]-dx)
    y = int(event.y-cnv.coords(playArea[num])[1]-dy)
    cnv.move(playArea[num], x, y)    

def moveEnd(event):                                             # Конец движения
    cnv.unbind("<B1-Motion>")
    cnv.unbind("<B1-ButtonRelease>")

def newGame(A=[10, 16, 17, 18, 24, 31]):
    global playArea, file, img
    img = [PhotoImage(file=i) for i in file]
    playArea = list(map(lambda x: x if x<0 else 0, playArea))   # Очистка поля
    cnv.delete('allBalls')                                      # Удаление шариков
    for i in A:                                                 # Размещение шариков
        x = (i % SIDE) * SIZE + SIZE / 2
        y = (i // SIDE) * SIZE + SIZE / 2
        playArea[i] = cnv.create_image(x, y, image=choice(img), anchor=CENTER, tag='allBalls')

cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) # Объект класса Canvas с именем cnv
cnv.bind('<Button-1>', ifplay)
cnv.pack()
for y in range(SIDE):                                           # Отрисовка поля
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                    fill=colores[0 if playArea[x+y*SIDE] == 0 else 1], outline=colores[2], width=1)
newGame()
mainloop()

Программа 4. Реализация перемещения шариков по игровому полю.

В программу 4 мы добавили функции ifplay(), play(), moveEnd(). Функцию ifplay() мы зарегистрировали в качестве обработчика события <Button-1> объекта cnv класса Canvas. В функции ifplay() происходит проверка наличия шарика в клетке над которой прижали левую кнопку мыши. В случае, если шарик в районе указателя мыши имеется, регистрируется функция play() в качестве обработчика события <B1-Motion> объекта cnv и функция moveEnd() в качестве обработчика события <B1-ButtonRelease> того же объекта cnv.

В функции play() происходит движение шарика, а в функции moveEnd() движение прекращается. Заметим, метод bind() регистрирует функцию как обработчик события, а метод unbind() отменяет регистрацию.

Рис. 6. Перемещение шариков по игровому полю.

from tkinter import *
from random import *
file = ['red.png', 'yellow.png', 'gold.png', 'green.png', 'emerald.png', 'cyan.png', 'blue.png', 'pink.png',
        'azure.png', 'bronze.png', 'purple.png', 'scarlet.png', 'steel.png', 'silver.png']
colores = ['#aa9', '#776', '#ccd']              # Цвета поля
SIDE=7; SIZE=60; R=25                           # Размер поля в клетках, клетки в пикселях, радиус шарика
playArea = [0]*SIDE*SIDE                        # Игровое поле
for i in [0, 1, 5, 6, 7, 8, 12, 13, 35, 36, 40, 41, 42, 43, 47, 48]: playArea[i]=-1

def ifplay(event):                                              # Разрешение движения
    global num, dx, dy
    num = event.x // SIZE + event.y // SIZE * SIDE              # Исходная клетка
    if playArea[num] > 0:                                       # Если клетка с шариком
        dx = int(event.x - cnv.coords(playArea[num])[0])        # Смещение курсора
        dy = int(event.y - cnv.coords(playArea[num])[1])        # относительно шарика
        cnv.tkraise(playArea[num])                              # Поднять шарик на верхний слой
        cnv.bind("<B1-Motion>", play)                           # Начать движение
        cnv.bind("<B1-ButtonRelease>", moveEnd)                 # Конец движения

def play(event):                                                # Начало движения
    x = int(event.x-cnv.coords(playArea[num])[0]-dx)
    y = int(event.y-cnv.coords(playArea[num])[1]-dy)
    cnv.move(playArea[num], x, y)    

def moveEnd(event):                                             # Конец движения
    num2 = event.x // SIZE + event.y // SIZE * SIDE             # Целевая клетка
    if playArea[num2] == 0 and playArea[int((num+num2)/2)] > 0 and ((abs(num2-num) == 2 and num2//SIDE-num//SIDE == 0) or (abs(num2-num) == 2*SIDE)):
        x = num2 % SIDE * SIZE - cnv.coords(playArea[num])[0] + SIZE / 2
        y = num2 // SIDE * SIZE - cnv.coords(playArea[num])[1] + SIZE / 2
        cnv.move(playArea[num], x, y)
        cnv.delete(playArea[int((num+num2)/2)])
        playArea[int((num+num2)/2)] = 0
        playArea[num2], playArea[num] = playArea[num], 0
    else:
        x = num % SIDE * SIZE - cnv.coords(playArea[num])[0] + SIZE / 2
        y = num // SIDE * SIZE - cnv.coords(playArea[num])[1] + SIZE / 2
        cnv.move(playArea[num], x, y)
    cnv.unbind("<B1-Motion>")
    cnv.unbind("<B1-ButtonRelease>")

def newGame(A=[10, 16, 17, 18, 24, 31]):
    global playArea, file, img
    img = [PhotoImage(file=i) for i in file]
    playArea = list(map(lambda x: x if x<0 else 0, playArea))   # Очистка поля
    cnv.delete('allBalls')                                      # Удаление шариков
    for i in A:                                                 # Размещение шариков
        x = (i % SIDE) * SIZE + SIZE / 2
        y = (i // SIDE) * SIZE + SIZE / 2
        playArea[i] = cnv.create_image(x, y, image=choice(img), anchor=CENTER, tag='allBalls')

cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) # Объект класса Canvas с именем cnv
cnv.bind('<Button-1>', ifplay)
cnv.pack()
for y in range(SIDE):                                           # Отрисовка поля
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                    fill=colores[0 if playArea[x+y*SIDE] == 0 else 1], outline=colores[2], width=1)
newGame()
mainloop()

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

В программу 5, в функцию moveEnd() мы добавили проверку законности хода игрока. Если игрок опустил шарик в клетку, в которую ходить по правилам нельзя, шарик возвращается в исходную позицию.

Рис. 7. Игра окончена, на поле остался один шарик.

#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- 
#
# Peg.py
# Copyright (C) 2021 Aleksandr Diorditsa, see <https://adior.ru>
# I want to thank all my students for the inspiration they give me.
#
# 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.
# 
# peg.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 *
file = ['red.png', 'yellow.png', 'gold.png', 'green.png', 'emerald.png', 'cyan.png', 'blue.png', 'pink.png',
        'azure.png', 'bronze.png', 'purple.png', 'scarlet.png', 'steel.png', 'silver.png']
colores = ['#aa9', '#776', '#ccd']              # Цвета поля
SIDE=7; SIZE=60; R=25                           # Размер поля в клетках, клетки в пикселях, радиус шарика
playArea = [0]*SIDE*SIDE                        # Игровое поле
for i in [0, 1, 5, 6, 7, 8, 12, 13, 35, 36, 40, 41, 42, 43, 47, 48]: playArea[i]=-1
f = open('peg', 'r')
peg = f.read()
peg = eval('[' + peg + ']')
f.close()
fnGame = [lambda A=i: newGame(A) for i in peg]

def ifplay(event):                                              # Разрешение движения
    global num, dx, dy
    num = event.x // SIZE + event.y // SIZE * SIDE              # Исходная клетка
    if playArea[num] > 0:                                       # Если клетка с шариком
        dx = int(event.x - cnv.coords(playArea[num])[0])        # Смещение курсора
        dy = int(event.y - cnv.coords(playArea[num])[1])        # относительно шарика
        cnv.tkraise(playArea[num])                              # Поднять шарик на верхний слой
        cnv.bind("<B1-Motion>", play)                           # Начать движение
        cnv.bind("<B1-ButtonRelease>", moveEnd)                 # Конец движения

def play(event):                                                # Начало движения
    x = int(event.x-cnv.coords(playArea[num])[0]-dx)
    y = int(event.y-cnv.coords(playArea[num])[1]-dy)
    cnv.move(playArea[num], x, y)    

def moveEnd(event):                                             # Конец движения
    num2 = event.x // SIZE + event.y // SIZE * SIDE             # Целевая клетка
    if playArea[num2] == 0 and playArea[int((num+num2)/2)] > 0 and ((abs(num2-num) == 2 and num2//SIDE-num//SIDE == 0) or (abs(num2-num) == 2*SIDE)):
        x = num2 % SIDE * SIZE - cnv.coords(playArea[num])[0] + SIZE / 2
        y = num2 // SIDE * SIZE - cnv.coords(playArea[num])[1] + SIZE / 2
        cnv.move(playArea[num], x, y)
        cnv.delete(playArea[int((num+num2)/2)])
        playArea[int((num+num2)/2)] = 0
        playArea[num2], playArea[num] = playArea[num], 0
    else:
        x = num % SIDE * SIZE - cnv.coords(playArea[num])[0] + SIZE / 2
        y = num // SIDE * SIZE - cnv.coords(playArea[num])[1] + SIZE / 2
        cnv.move(playArea[num], x, y)
    cnv.unbind("<B1-Motion>")
    cnv.unbind("<B1-ButtonRelease>")

def newGame(A=[10, 16, 17, 18, 24, 31]):
    global playArea, file, img
    img = [PhotoImage(file=i) for i in file]
    playArea = list(map(lambda x: x if x<0 else 0, playArea))   # Очистка поля
    cnv.delete('allBalls')                                      # Удаление шариков
    for i in A:                                                 # Размещение шариков
        x = (i % SIDE) * SIZE + SIZE / 2
        y = (i // SIDE) * SIZE + SIZE / 2
        playArea[i] = cnv.create_image(x, y, image=choice(img), anchor=CENTER, tag='allBalls')

frm = [Frame() for i in range(len(fnGame)//12 +1)]
for i in frm: i.pack()
for i in range(1, len(fnGame)):
    Button(frm[(i//12)], text=chr(ord('A' if i<27 else 'G')+i-1), command=fnGame[i]).pack(side=LEFT)
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) # Объект класса Canvas с именем cnv
cnv.bind('<Button-1>', ifplay)
cnv.pack()
for y in range(SIDE):                                           # Отрисовка поля
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                    fill=colores[0 if playArea[x+y*SIDE] == 0 else 1], outline=colores[2], width=1)
newGame()
mainloop()

Программа 6. Игра с загрузкой уровней из файла

В программу 4 мы добавили загрузку уровней игры из файла с именем peg, и кнопки выбора уровня игры. В качестве обработчика события "Нажатие на кнопку выбора уровня игры" мы зарегистрировали элемент списка функций fnGame который, в свою очередь, вызывает функцию newGame().

Рис. 8. Вариант расстановки шариков на игровом поле.

Рис. 8. Вариант расстановки шариков на игровом поле.

Рис. 8. Вариант расстановки шариков на игровом поле.

[24],
[10, 16, 17, 18, 24, 31],
[10, 17, 22, 23, 24, 25, 26, 31, 38],
[2, 3, 4, 9, 10, 11, 16, 17, 18, 23, 25],
[3, 9, 10, 11, 15, 16, 17, 18, 19, 24, 31, 37, 38, 39, 44, 45, 46],
[10, 16, 17, 18, 22, 23, 24, 25, 26, 28, 29, 30, 31, 32, 33, 34],
[3, 9, 10, 11, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 29, 30, 31, 32, 33, 37, 38, 39, 45],
[2, 3, 4, 9, 10, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 44, 45, 46],
[10, 15, 16, 18, 19, 22, 23, 25, 26, 29, 30, 32, 33, 38],
[10, 15, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 30, 31, 32, 37, 38, 39, 44, 45, 46],
[2, 3, 4, 9, 10, 11, 16, 17, 18, 22, 23, 24, 25, 26, 30, 31, 32, 37, 38, 39, 44, 45, 46],
[15, 16, 18, 19, 22, 23, 24, 25, 26, 29, 30, 32, 33],
[10, 16, 17, 18, 22, 24, 26, 29, 30, 31, 32, 33, 37, 38, 39, 44, 46],
[10, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 33, 34, 37, 38, 39, 44, 46],
[9, 11, 14, 15, 19, 20, 22, 26, 30, 32, 37, 39],
[15, 16, 17, 18, 19, 22, 23, 25, 26, 29, 30, 31, 32, 33],
[10, 16, 17, 18, 23, 24, 25, 30, 31, 32, 38],
[10, 14, 15, 16, 18, 19, 20, 21, 22, 24, 26, 27, 29, 31, 33, 37, 39],
[10, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30, 32, 33, 38, 45],
[3, 10, 15, 16, 18, 19, 22, 23, 25, 26, 29, 30, 32, 33, 38, 45],
[2, 3, 4, 9, 10, 11, 15, 19, 22, 23, 25, 26, 29, 33, 37, 38, 39, 44, 45, 46],
[3, 9, 10, 11, 15, 16, 18, 19, 21, 22, 26, 27, 29, 30, 32, 33, 37, 38, 39, 45],
[9, 10, 11, 14, 15, 16, 18, 19, 20, 21, 22, 26, 27, 29, 30, 32, 33, 37, 38, 39],
[2, 3, 4, 9, 10, 11, 14, 15, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 33, 34, 37, 38, 39, 44, 45, 46]

Модуль 1. Уровни игры. текстовый файл "peg".

Немецкий математик Г. Лейбниц (1646—1716гг) начинал игру на поле с одним шариком и перепрыгивая через лунку, не убирал, а ставил на нее шарик. Учёный говорил, что восстанавливать намного сложнее, чем разрушать. Каждый раз он ставил себе задачу создать из появляющихся на поле шариков красивую фигуру.

#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- 
#
# Peg.py
# Copyright (C) 2021 Aleksandr Diorditsa, see <https://adior.ru>
# I want to thank all my students for the inspiration they give me.
#
# 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.
# 
# peg.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 *
file = ['red.png', 'yellow.png', 'gold.png', 'green.png', 'emerald.png', 'cyan.png', 'blue.png', 'pink.png',
        'azure.png', 'bronze.png', 'purple.png', 'scarlet.png', 'steel.png', 'silver.png']
colores = ['#aa9', '#776', '#ccd']              # Цвета поля
SIDE=7; SIZE=60; R=25                           # Размер поля в клетках, клетки в пикселях, радиус шарика
playArea = [0]*SIDE*SIDE                        # Игровое поле
for i in [0, 1, 5, 6, 7, 8, 12, 13, 35, 36, 40, 41, 42, 43, 47, 48]: playArea[i]=-1
btn = []; frm = []

def loadLevel():                                # Загрузка уровней игры
    global btn, frm
    f = open('peg', 'r')
    peg = f.read()
    peg = eval('[' + peg + ']')
    f.close()
    fnGame = [lambda A=i: newGame(A) for i in peg]
    for i in btn: i.destroy()
    for i in frm: i.destroy()
    btn.clear()
    frm.clear()
    frm = [Frame(frmT) for i in range(len(fnGame)//12 +1)]
    for i in frm: i.pack()
    btn = [Button(frm[(i//12)], text=chr(ord('A' if i<27 else 'G')+i-1), command=fnGame[i]) for i in range(len(fnGame))]
    for i in btn: i.pack(side=LEFT)

def ifplay(event):                                              # Разрешение движения
    global num, dx, dy
    num = event.x // SIZE + event.y // SIZE * SIDE              # Исходная клетка
    if playArea[num] > 0:                                       # Если клетка с шариком
        dx = int(event.x - cnv.coords(playArea[num])[0])        # Смещение курсора
        dy = int(event.y - cnv.coords(playArea[num])[1])        # относительно шарика
        cnv.tkraise(playArea[num])                              # Поднять шарик на верхний слой
        cnv.bind("<B1-Motion>", play)                           # Начать движение
        cnv.bind("<B1-ButtonRelease>", moveEnd)                 # Конец движения

def play(event):                                                # Начало движения
    x = int(event.x-cnv.coords(playArea[num])[0]-dx)
    y = int(event.y-cnv.coords(playArea[num])[1]-dy)
    cnv.move(playArea[num], x, y)    

def moveEnd(event):                                             # Конец движения
    num2 = event.x // SIZE + event.y // SIZE * SIDE             # Целевая клетка
    num3 = int((num+num2)/2)                                    # Средняя клетка
    if playArea[num2] == 0 and playArea[num3] == 0 and ((abs(num2-num) == 2 and num2//SIDE-num//SIDE == 0) or (abs(num2-num) == 2*SIDE)):
        x = num2 % SIDE * SIZE - cnv.coords(playArea[num])[0] + SIZE / 2
        y = num2 // SIDE * SIZE - cnv.coords(playArea[num])[1] + SIZE / 2
        cnv.move(playArea[num], x, y)
        cnv.delete(playArea[num3])
        playArea[num3] = cnv.create_image(num3%SIDE*SIZE+SIZE/2, num3//SIDE*SIZE+SIZE/2, image=choice(img), anchor=CENTER, tag='allBalls')
        playArea[num2], playArea[num] = playArea[num], 0
    else:
        x = num % SIDE * SIZE - cnv.coords(playArea[num])[0] + SIZE / 2
        y = num // SIDE * SIZE - cnv.coords(playArea[num])[1] + SIZE / 2
        cnv.move(playArea[num], x, y)
    cnv.unbind("<B1-Motion>")
    cnv.unbind("<B1-ButtonRelease>")

def newGame(A=[24]):
    global playArea, file, img
    img = [PhotoImage(file=i) for i in file]
    playArea = list(map(lambda x: x if x<0 else 0, playArea))   # Очистка поля
    cnv.delete('allBalls')                                      # Удаление шариков
    for i in A:                                                 # Размещение шариков
        x = (i % SIDE) * SIZE + SIZE / 2
        y = (i // SIDE) * SIZE + SIZE / 2
        playArea[i] = cnv.create_image(x, y, image=choice(img), anchor=CENTER, tag='allBalls')

def save():
    f = open('peg', 'a')
    s = []
    for i in range(SIDE**2):
        if playArea[i]>0:
            s.append(i)
    s = ',\n' + str(s)
    f.write(s)
    f.close()

def rotate():
    newArea = [0]*SIDE*SIDE
    for y in range(SIDE):
        for x in range(SIDE):
            newArea[x+y*SIDE] = playArea[(SIDE - 1 - y) + SIDE * x]
    A = []
    for i in range(SIDE**2):
        if newArea[i]>0:
            A.append(i)
    newGame(A)

frmT = Frame()
frmT.pack()
loadLevel()
cnv = Canvas(width=SIDE*SIZE, height=SIDE*SIZE) # Объект класса Canvas с именем cnv
cnv.bind('<Button-1>', ifplay)
cnv.pack()
frmB = Frame()
frmB.pack()
Button(frmB, text='Rotate', command=rotate).pack(side=LEFT)
Button(frmB, text='Save', command=save).pack(side=LEFT)
Button(frmB, text='Load', command=loadLevel).pack(side=LEFT)
for y in range(SIDE):                                           # Отрисовка поля
    for x in range(SIDE):
        cnv.create_rectangle(x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE,
                    fill=colores[0 if playArea[x+y*SIDE] == 0 else 1], outline=colores[2], width=1)
newGame()
mainloop()

Программа 7. Редактор

Рис. 9. Редактор уровней игры