/
Автор: Рубанцев В.
Теги: программирование головоломки компьютерные игры язык программирования python
Год: 2022
Текст
Рубанцев Валерий
Программирование для всех
Компьютерные игры и головоломки на Питоне
О PegSolitaire
Ходов: 4 Солитер
о
о
Processing, ру
Валерий Рубанцев
Компьютерные игры и
головоломки на Питоне
Бесплатное издание
Все права защищены. Никакая часть этой книги не может быть воспроизведена
в любой форме без письменного разрешения правообладателей.
Автор книги не несёт ответственности за возможный вред от использования
информации, составляющей содержание книги и приложений.
Copyright 2022 Валерий Рубанцев
Лилия Рубанцева
От автора
Продолжаем программировать компьютерные игры и головоломки! На этот раз
более сложные, чем в первой книге, которая называется Простые компьютер¬
ные игры на Питоне. Поэтому программ мы напишем меньше, но почти все они
«многострочные».
Мы будем использовать все современные парадигмы программирования - объ¬
ектно-ориентированную, функциональную, императивную. Предпочтение отда¬
ётся, естественно, ООП, без которого трудно себе представить современную
компьютерную игру.
Кроме разработки игровой логики, мы изучим программирование анимации, ко¬
торая оживляет игру и добавляет ей реализма, а также метод минимакса, кото¬
рый добавляет программе достаточно интеллекта, чтобы сражаться с сильными
игроками.
Солитер, Прыгающие лягушки и Крестики-нолики хорошо известны всем с дет¬
ства. Менее известны, но не менее интересны математическая Игра Ярбро и го¬
ловоломка Eliminator. Для последней головоломки мы разработаем решалку, ко¬
торая не только легко справится со всеми уровнями, но и поможет составлять
новые.
Не останавливаясь на достигнутом, мы придумаем и запрограммируем голово¬
ломку Местор. Программировать популярные игры и головоломки приятно и
полезно, но всегда нужно стремиться развить идеи и привнести в уже известные
игры что-нибудь своё.
Во всех программах мы традиционно используем Процессинг, существенно об¬
легчающий разработку программ с графическим интерфейсом.
В конце некоторых глав даны задания для самостоятельного решения. Попро¬
буйте с ними справиться. Только так вы научитесь писать собственные програм¬
мы.
Цель книги: научиться программировать современные компьютерные игры и го¬
ловоломки на Питоне в объектно-ориентированном стиле в среде разработки
Процессинг.
Книга адресуется: школьникам, учителям информатики и всем любителям про¬
граммирования.
Валерий Рубанцев
Условные обозначения, принятые в книге:
| Дополнение или замечание
| Требование или указание
Исходный код:
# ЗАГРУЖАЕМ МЕДИАФАЙЛЫ
def pneload():
# загружаем картинки -->
global imgBack
# загружаем фоновую картинку:
imgBack = loadImage("data/imgBack.png")
Media.load(Minim(this))
Задание для самостоятельного решения
I
Заголовок проекта:
Проект
I Исходные коды всех проектов находятся в папке _Projects
Оглавление
Компьютерные игры и 2
головоломки на Питоне 2
От автора 4
Оглавление 7
Солитер 10
Правила игры 13
Солитерные задачи 23
Пора браться за программу! 32
Главный файл программы 33
Класс игры 47
Класс клетки 48
Класс игры (продолжение) 50
Класс фишки 55
Рисуем сцену 56
Ход игрока 64
Окончание игры 77
Новая игра 84
Как решить задачу 84
Задачи для самостоятельного решения 85
Прыгающие лягушки (Jumping Frogs) 87
Правила игры 88
Лягушачьи задачи 88
К программе! 92
Главный файл программы 93
Класс игры 102
Создаём фишку 107
Рисуем сцену 108
Ход игрока 116
Как решить задачу 129
Задачи для самостоятельного решения 131
Метод пуговиц и нитей 153
Игра Ярбро 162
Фоновый калькулятор 164
Кнопки и всё остальное 166
Класс игры 168
Подготовка 171
Схватка умов 174
Развязка интеллектуальной драмы 179
Учим компьютер уму 182
Класс таймера 183
Eliminator Puzzle 184
Правила игры 185
Интерфейс программы 186
За дело! 188
Игра как таковая 192
Полевые работы 194
Переходим на клеточный уровень 198
Классные шарики 200
На сцену! 203
Первоход 207
Элиминация 219
Со щитом или на щите? 225
Повышаем уровень игры 228
У нас все ходы записаны! 230
Конклюзион 230
Eliminator Solver 231
Подготовительные операции 232
Позиционный класс 234
Игровой класс 235
Пора решаться! 237
Eliminator Creator 249
Творческая мастерская 253
Mestor 260
Мастерская по изготовлению уровней 266
Крестики-нолики 273
Займёмся интерфейсом 273
Куда пойти? 283
Однако, Пётр Сергеевич, партия! 286
Ваш ход, Компьютер! 290
Крестики-нолики с минимаксом 299
Литература 310
Серия Программирование для детей 313
Солитер
Солитер - это головоломка с фишками. Она украшает обложку книги Мартина
Гарднера Математические досуги (Рис. 1).
Рис. 1
Эта головоломка известна уже не одно столетие. В начале шестнадцатой главы
Игра в солитер Гарднер приводит цитату из письма немецкого математика Гот¬
фрида Лейбница, датированного 1716 годом! Он играл в солитер наоборот -
начинал с одной фишки, а затем выставлял другие так, чтобы они образовали
какие-либо фигуры.
Происхождение игры доподлинно неизвестно, но существует красивая легенда,
что её будто бы придумал узник Бастилии, который за этой игрой коротал своё
время. Отсюда и название: le solitaire значит по-французски одинокий.
Точно так же называется известный карточный пасьянс (Рис. 2).
Рис. 2
Чтобы избежать путаницы, американцы называют солитер с фишками - Peg
Solitaire. Слово peg можно перевести как колышек. Некоторые доски для соли¬
тера имеют отверстия, в которые эти колышки и вставляют (Рис. 3).
Рис. 3
И
Раньше солитером называли также крупный бриллиант в оправе (Рис. 4).
Рис. 4
Часто солитер курьёзно называют солитёром. А солитёр - это ленточный червь.
Не перепутайте!
Вместо колышков используют фишки и другие небольшие предметы (например,
монеты или леденцы), которые кладут в лунки на доске. Можно обойтись и во¬
обще без доски, а играть на листке бумаги, расчерченном на квадратики.
Наиболее известны две доски для игры в солитер - классическая (английская) и
французская (европейская). У классической доски 33 клетки, расположенные
крестом. Средняя клетка - пустая, на остальных стоят фишки (Рис. 5).
На рисунке фишки окрашены в разные цвета. Это сделано для удобства игры.
В настольной игре все фишки одного цвета.
Средняя клетка выделена зелёным квадратиком. Он обозначает клетку, в ко¬
торой должна закончиться игра.
Французы добавили ещё 4 клетки в углы креста (Рис. 6).
Рис. 5
Рис. 6
Менее распространены доски других форм. Немецкая доска, как и английская,
имеет крестообразную форму, но стороны длиннее (Рис. 7).
Если слегка укоротить немецкий вариант с двух сторон, то получится несиммет¬
ричная доска (Рис. 8).
И последняя из известных досок называется алмазом (Рис. 9).
Правила игры
Правила игры в солитер просты и немногочисленны.
Цель игры заключается в том, чтобы за наименьшее число ходов убрать все
фишки с доски, кроме одной.
Ход состоит из одного или более прыжков.
Рис. 7
Прыжок выполняется любой фишкой влево, вправо, вверх или вниз, если в со¬
седней клетке по этим направлениям стоит другая фишка, а сразу за ней нахо¬
дится пустая клетка.
Перепрыгнутая фишка снимается с доски. Из этого следует, что, если в началь¬
ной позиции имеется N фишек, то нужно совершить N - 1 прыжок, чтобы на дос¬
ке осталась единственная фишка.
Рис. 8
Если прыгнувшая фишка может выполнить ещё один прыжок, то он также счита¬
ется за 1 ход. Значит, ходов в игре не больше, чем прыжков, но, возможно, и
меньше.
Обычно последняя фишка должна занять центральную клетку доски. В более
простом варианте игры - любую.
Если на доске осталось больше одной фишки, а ходов у игрока нет, то игра за¬
вершается его проигрышем.
Рис. 9
В нашей программе мы будем пользоваться не колышками, а фишками двух цве¬
тов. Красным цветом мы обозначим фишки, которые не имеют ходов в текущей
позиции. Зелёным - фишки, которые могут выполнить хотя бы 1 прыжок (Рис.
10).
Рис. 10
В начальной позиции на классическом доске центральное поле пустое и оно же
является конечным для последней фишки (Рис. 11).
Рис. 11
Так как доска симметрична, то первый прыжок могут выполнить 4 фишки.
Левая зелёная фишка может прыгнуть вправо. Правая - влево. Верхняя - вниз.
Нижняя - вверх (Рис. 12).
Легко заметить, что других ходов в этой позиции нет.
Вследствие симметрии позиции первый ход можно сделать любой из этих фи¬
шек.
Пусть это будет прыжок левой фишкой (Рис. 13).
Рис. 12
Она перепрыгивает через красную фишку (раскраска условная, поэтому при
прыжках цвет не учитывается), которая снимается с доски, и приземляется в пу¬
стую центральную клетку. Прыгнуть дальше она уже не может, поэтому мы пе¬
рекрашиваем её в красный цвет. Ещё 3 фишки в результате первого хода стано¬
вятся зелёными. Второй ход можно сделать любой из них (Рис. 14).
Пусть прыгнет правая фишка (Рис. 15).
Две фишки могут выполнить каскад из двух прыжков. Для примера, возьмём
верхнюю (Рис. 17).
Ситуация на поле изменится. Теперь мы имеем на выбор 5 зелёных фишек (Рис.
16).
Рис. 14
Рис. 15
Рис. 16
Рис. 17
Эти 2 последовательных прыжка одной фишкой составляют 1 ход.
Фишка не обязана делать все допустимые прыжки. Она может остановиться
после любого прыжка и тем самым закончить ход. Не всегда самый длинный
каскад ведёт к решению головоломки.
После этого каскада на доске стало на 2 фишки меньше (Рис. 18).
Рис. 18
Если нужно закончить прыжки в центральной клетке, то наилучший результат -
18 ходов.
Об игре солитер можно прочитать в журнале Наука и жизнь, №7 за 1966 год. На
странице 135 описаны правила игры и приводится пример партии на классиче¬
ской доске. Для записи ходов клетки поля пронумерованы вот так (Рис. 19).
Рис. 19
Если вам не удастся самостоятельно справиться с головоломкой, вы можете по¬
тихоньку заглянуть в ответ.
Там же указывается, что можно расставить фишки на все клетки доски, а потом
убрать любую из них, а не обязательно центральную. Игра должна закончиться в
той же самой клетке.
В программе мы реализуем только классический вариант игры. Напишите
программу, в которой игрок может освободить любое поле доски перед
началом игры. Либо удаляйте случайным образом одну из полного набора
фишек.
Гораздо больше внимания этой головоломке журнал Наука и жизнь уделил спу¬
стя 8 лет. В номере 9 за 1974 год, на страницах 124-125 описана история и пра¬
вила игры в солитер. Для записи ходов здесь используется другая нумерация
клеток (Рис. 20).
В своё время солитер был популярен в России, где его называли также Пустын¬
ником и Одинокой фишкой. В советское время солитер выпускался под назва¬
нием Йога (Рис. 21). Это странное название вызвано тогдашним интересом к ин¬
дийским йогам и йоге. Такая головоломка была и у меня. Это пластмассовая
доска с отверстиями, в которые вставлялись фишки-колышки. Компьютерный
вариант игры, конечно, и красочнее, и удобнее.
Рис. 20
Рис. 21
В журнале Занимательные головоломки №3 напечатана интересная статья о со¬
литере, который почему-то называется мадагаскарскими шашками (Рис. 22).
Рис. 22
Солитерные задачи
Как раз в то время, когда в России солитер был популярен, в журналах печатали
задачи. Для них использовали классическую и французскую доски.
А вот Задача 3 из журнала Наука и жизнь. Она отличается от классической голо¬
воломки только тем, что часть фишек уже убрана (Рис. 23).
I Ы\ II II и и
дппихя
ЕДППППСЕа
■ипппии
Рис. 23
Нужно закончить прыжки в центральной клетке 44. Наилучший результат - 18
ходов.
Задача 4 похитрее. Нужно сначала удалить с доски одну из фишек, а уже затем
решать головоломку (Рис. 24).
Рис. 24
Закончить решение нужно также в центральной клетке 44.
Задача 2 решается на французской доске. В исходной позиции свободна клетка
15, а последняя фишка должна оказаться в клетке 73 (Рис. 25).
Рис. 25
Французская доска отличается от классической тем, что при свободной цен¬
тральной клетке в начальной позиции, невозможно оставить на доске един¬
ственную фишку.
Несколько задачек предложил и Мартин Гарднер. Во всех задачках последняя
фишка должна оказаться в центральной клетке.
Латинский крест (Рис. 26).
Рис. 26
Греческий крест (Рис. 27).
Камин (Рис. 28).
Пирамида (Рис. 29). Настольная лампа (Рис. 30).
в
о
О
о
в
о
о
в
о
Рис. 27
в
в
о
о
о
в
о
о
о
о
□
в
в
О
О
о
о
О
о
в
в
о
в
О
в
О
о
в
О
в
в
о
в
в
о
в
О
Рис. 30
Рис. 29
Ромб (Рис. 31).
Рис. 31
Как вы знаете, Лейбниц решал солитерные задачи наоборот. Этот способ реше¬
ния задач называется ретроградным анализом.
Такие задачи более редкие, но парочку можно найти в журнале Наука и жизнь.
Задача 5 Глобус. Последний ход закончился в центральной клетке. Нужно вос¬
становить исходную позицию на французской доске (Рис. 32).
Задача 6 Вертушка. Условия те же самые, но играть нужно на классической дос¬
ке (Рис. 33).
Рис. 32
Рис. 33
Ответы на задачи из журнала Наука и жизнь №9 за 1974 год напечатаны в жур¬
нале №3 уже в следующем году, на страницах 127-128.
Задача 1 (классическая) решается за 18 ходов (Рис. 34).
На решение второй задачи нужно затратить 26 ходов (Рис. 35, слева). В журнале
№9 за 1975 год, на странице 149 напечатан более короткий вариант- 23 хода
(Рис. 35, справа):
0. (44). t. 64—44 (54). 2. 56—
54 (55). 3. 75—55 (65). 4. 45—
65 (55). 5. 25—45 (35). 6.
37—35 (36). 7. 34—36 (35).
8. 57—37—35 (47, 36). 9. 53—
55 (54). 10. 51—53 (52). 11.
32—34—36—56—54—52 (33,
35, 46, 55, 53). 12. 73—75—
55—35 (74. 65, 45). 13. 13—33
(32). 14. 43—23 (33). 15. 15—
13—33 (14, 23). 16. 31—51 —
53 (41, 52). 17. 63—43—23—
25—45—43 (53, 33, 24, 35, 44).
18. 42—44 (43).
Рис. 34
0. (15). 1. 35—15 (25). 2. 37—
35 (36). 3. 56—36 (46). 4. 26—
46 (36). 5. 57—37 (47). 6.
45—25 (35). 7. 15—35 (25).
8. 34—36 (35). 9. 37—35 (36).
10. 32—34 (33). 11. 52—32
(42). 12. 54—52 (53). 13. 74—
54 (64). 14. 13—33—53 (23,
43). 15. 31—33 (32). 16. 34—
32 (33). 17. 22—42 (32). 18.
41—43 (42). 19. 53—33 (43).
20. 51—53 (52). 21. 54—52
(53). 22. 62—42 (52). 23. 73—
53 (63). 24. 66—64 (65). 25.
14 — 34 — 32 — 52 — 54 —
56 — 36 — 34 — 54 — 74
(24, 33. 42, 53, 55, 46, 35, 44,
64). 26. 75—73 (74). ,
1. 35—15(25). 2. 37—35(36).
3. 34—36(35). 4. 57—37—35
(47, 36). 5. 56—36(46). 6.
26—46(36). 7. 45—25(35). 8.
15—35(25). 9. 32—34(331. 10.
52—32(42). 11. 54—52(53). 12.
13— 33—53(23,43). 13. 22—42
(32). 14. 52—32(42). 15. 31—33
(32). 16. 34—32(33). 17. 51 —
31—33(41,32). 18. 74—54—52
(64. 53). 19. 62—42(52). 20.
73—53(63). 21. 66—64(65). 22.
14— 34—32—52—54 — 56 —
36—34—54—74(24, 33, 42, 53,
55, 46, 35, 44, 64). 23. 75—73
(74).
Рис. 35
Третья задача решается за 9 ходов (Рис. 36).
Решение четвёртой задачи (Рис. 37).
Если последний прыжок выполняется на клетку 74, то решение на 1 ход короче.
Решения ретроградных задач более сложные. Читайте о них в журнале.
0. (13, 14, 15, 31, 41, 51, 73,
74, 75, 37, 47, 57, 44). 1. 43—
41 (42). 2. 23—43 (33), 3. 24—
44—42 (34, 43). 4. 63—43 (53).
5. 45—47 (46). 6. 25—45 (35).
Далее следует великолеп¬
ный каскад из 9 прыжков.
7. 55 — 35 — 37 — 57 —
55 _ 53 _ 51 — 31 — 33 —
53 (45, 36, 47, 56, 54, 52, 41,
32, 43). 8. 65—63—45 (64, 54).
9. 32—44 (43).
Рис. 36
Удаляем шашку 74. После
этого возможно решение в
8 ходов. 1. 15—35 (25). 2.
14—34—36 (24, 35). 3. 57—
37—35 (47, 36). 4. 73—53
(63). 5. 56—36—34—56—52
(46, 35, 44, 53). 6. 31—51—53
(41, 52). 7. 32—52—54 (42.
53). 8. 64—44 (54).
Рис. 37
Задача 8 решается по классическим правилам за 9 ходов (Рис. 38).
□□Е
ilGDDDI
□□□□□□□
SOQCDE3SI
■ган
Пг1Г л нгз
Рис. 38
Задача 7 - на ретроградный анализ (Рис. 39).
Рис. 39
Нужно восстановить классическую позицию с пустой центральной клеткой. В
журнале приводится решение в 24 хода.
И в книге Гарднера, и в журнале Наука и жизнь №4 за 1975 год, страница 73
приводится задача Карлсона (Рис. 40).
Рис. 40
Последняя фишка должна занять изначально пустую клетку в левом верхнем уг¬
лу доски. Существует решение в 16 ходов, которое заканчивается каскадом из 8
или 9 прыжков.
Её придумал инженер Нобл Карлсон в далёком 1960 году. Он поставил более
общую проблему: найти наименьшую квадратную доску с пустой клеткой в
верхнем левом углу, с которой можно удалить все фишки, кроме последней. До¬
казано, что длина сторон квадрата (в клетках) должна быть кратна трём. На дос¬
ке 3 х 3 клетки задача решений не имеет. Сам Карлсон решил задачу на доске 6 х
6 клеток за 29 ходов, но последняя фишка оказалась в клетке 22, если считать
клетки сверху вниз и слева направо, начиная с 1.
Последний раз солитер упоминается в первом номере журнала Наука и жизнь
за 1976 год. На страницах 126-127 рассказывается об игре Лиса и гуси, которая
играется на доске, похожей на классическую солитерную, но это уже другая ис¬
тория.
Пора браться за программу!
Теперь вы знаете достаточно (и даже больше), чтобы приступить к программи¬
рованию головоломки.
Во всех игровых программах мы будем использовать библиотеки:
• controlP5 - отвечает за элементы управления
• minim - за звуки
Очень часто в программах нужны названия цветов. В первой книге мы передава¬
ли в цветовые функции наборы чисел - цветовых составляющих. Это не очень
удобно, поскольку по числам трудно представить цвет на экране. В папке libs вы
найдёте файл colors.py, который нужно копировать в папку с программой. При
запуске программы он превратится в файл colors$py.class, поэтому вы можете
сразу копировать в папку с программой именно этот файл.
А начало цветового файла такое:
# Red Colors
IndianRed ='#CD5C5C'
LightCoral ='#F08080'
Salmon ='#FA8072'
DarkSalmon ='#E9967A'
LightSalmon ='#FFA07A'
Crimson ='#DC143C'
Red ='#FF0000'
FireBrick ='#B22222'
DarkRed ='#8B0000'
То есть числовое значение цветов присваивается переменным, названия кото¬
рых общеприняты в компьютерной графике. К сожалению, названия английские.
Обычно в программе используется небольшое число цветов, поэтому вы можете
либо сократить список, либо скопировать в программу только нужные цвета. Но
это лишняя работа, которую делать совсем необязательно.
В начало каждого файла программы, в котором используются цвета из списка,
добавьте строку:
from colons import *
Теперь вы можете пользоваться в своих программах названиями цветов.
Главный файл программы
Опыт первой книги - Простые компьютерные игры на Питоне - показывает, что
даже сравнительно несложные игры состоят из нескольких сотен строк кода. В
длинном коде трудно ориентироваться и ещё труднее его отлаживать, поэтому
лучше поделить всю программу на отдельные, законченные по смыслу части, ко¬
торые хранятся в отдельных файлах.
Обычно длинные и/или сложные программы пишут в объектно¬
ориентированном стиле, то есть всю программу представляют как совокупность
отдельных, но взаимодействующих между собой объектов, которые описывают¬
ся классами.
Так красиво у нас не получится, потому что функции библиотеки Процессинг
нельзя поместить в объекты, они должны быть глобальными. Отсюда вытекает
необходимость главного файла программы, который вполне естественно
назвать PegSolitaire.pyde.
Понятно, что нам не обойтись без картинок с фишками. Сами картинки вы уже
видели. И почему они такого цвета, уже знаете. Музыкальное сопровождение
убивает любую компьютерную игру, а вот отдельные звуки, комментирующие
действия игрока и события в игре, - совсем другое дело. У нас их будет только
четыре. Они исполняются:
• sndMove - когда фишка перепрыгивает в новую клетку
• sndPressed - когда игрок берёт фишку
• sndError - когда игрок выполняет ошибочный ход
• sndWin - когда игрок выполняет задание
И наконец, нам нужна фоновая картинка, о которой речь пойдёт дальше.
Обычно игровая программа начинается с функции preload, в которой загружают¬
ся все файлы с диска:
# ЗАГРУЖАЕМ МЕДИАФАЙЛЫ
def preload():
# загружаем картинки -->
global imgBack, imgGreen, imgRed, imgButton
# фоновая картинка:
imgBack = loadImage("data/imgBack.png")
# загружаем картинки с фишками:
imgGreen = loadImage("data/green.png")
imgRed = loadImage("data/red.png")
# прозрачная кнопка:
imgButton = loadImage("data/imgButton.png")
# загружаем звуки -->
global sndMove, sndPressed, sndError, sndWin
minim = Minim(this)
sndMove = minim.loadSample("data/buljk.wav")
sndPressed = minim.loadSample("data/pressed.wav")
sndError = minim.loadSample("data/error.wav")
sndWin = minim.loadSample("data/win.wav")
Как только функция preload закончит свою работу, вызывается функция setup, в
которой мы сразу создаём окно:
# ГОТОВИМСЯ К ПЕРВОЙ ИГРЕ
def setup():
# загружаем медиафайлы:
preload()
# окно:
size(WIDTH, HEIGHT)
Размеры окна я задал константами WIDTH и HEIGHT. В программе будет ещё
много других констант. Многие из них используются в нескольких файлах про¬
граммы. И в каждый файл необходимо копировать нужные константы. Это не
очень удобно, а что ещё хуже - при изменении значения констант исправлять их
придётся в нескольких местах. Лучше по ходу написания программы все кон¬
станты помещать в отдельный файл constants.py. В готовом виде он выглядит
так:
# This Python file uses the following encoding: utf-8
# размеры окна в пикселях:
WIDTH = 750
HEIGHT = 559
# размеры доски:
FIELD_WIDTH = 9
FIELD HEIGHT = 9
# зелёная фишка:
GREEN = 10
# красная фишка:
RED = 11
# нет клетки:
NONE = 0
# пустая клетка:
EMPTY = 1
# фишка:
PEG = 2
# размер клеток в пикселях:
B_WIDTH = 62
B HEIGHT = 62
А назначение констант мы обсудим дальше.
Как и в случае с названиями цветов, в начало всех файлов, в которых использу¬
ются константы, добавьте строку:
from constants import *
Естественно, вы не сможете сразу вычислить размеры канвы. Но нужно учесть,
что размеры картинок с фишками равны 62 на 62 пикселя. Самая большая доска
для игры в солитер - немецкая - имеет размеры 9 на 9 клеток:
# размер клеток в пикселях:
B_WIDTH = 62
B HEIGHT = 62
# размеры доски:
FIELD_WIDTH = 9
FIELD_HEIGHT = 9
# размеры окна в пикселях:
WIDTH = 750
HEIGHT = 559
Также нам потребуются элементы управления, которым также нужно предоста¬
вить место в окне.
Если бы мы ограничились только классической доской с классической же пози¬
цией, то было бы достаточно одной кнопки, чтобы начинать новую игру. Но, как
вы знаете, есть и другие доски, а также - задания с меньшим числом фишек.
Проще всего выбирать доску или задание, нажав на кнопку. Но когда в програм¬
ме много кнопок, в них легко запутаться. Поэтому нужно выбрать не очень много
заданий. Я остановился на дюжине. Это все варианты досок и задачи из книги
Гарднера. Итого нам предстоит создать 12 кнопок.
| Вы можете написать программу и для других заданий.
Картинки с заданиями вы уже видели раньше. Вопрос в том, как наглядно пока¬
зать назначение каждой кнопки. Проще всего сделать надписи. Но в некоторых
случаях они получаются очень длинными. Ещё хуже то, что они не дают никакого
представления о самом задании. Например, задача Карлсона - что в ней нужно
сделать? Хорошо было бы показать на самой кнопке небольшую картинку с за¬
данием, тогда делать выбор было бы гораздо удобнее. Можно прикрепить к
кнопке картинку, но есть более хитрый вариант. Мы создадим кнопку без
надписи и прозрачную. Если подложить под кнопку картинку, то она создаст
полное впечатление, что находится на самой кнопке. Если подумать ещё раз, то
легко прийти к выводу, что можно не впечатывать в окно 12 отдельных картинок,
а сразу нарисовать их на фоновой картинке, что я и сделал (Рис. 1).
12 кнопок я разместил ровными рядами вдоль правой стороны фоновой картин¬
ки, а на оставшейся её части мы напечатаем доску с заданием.
Конечно, такую фоновую картинку невозможно нарисовать сразу. Сначала нужно
научиться рисовать доску, надписи и другие атрибуты игры.
Размеры и координаты картинок легко узнать в графическом редакторе типа
Фотошопа.
Теперь в функции setup мы создаём все кнопки по одному сценарию:
# создаём кнопки -->
cp5 = ControlP5(this)
global btnClassic, btnFrench, btnGerman, btnAsym, btnAlmaz, \
btnLathe, btnGreece, btnKamin, btnPyramid, btnLamp, \
btnRhomb, btnCarlson
x = 0
y = 0
xb = 590 - 1
yb = 70 - 1
dx = 80
dy = 80
Ф8 I
global imgButton
global btnClassic
btnClassic = cp5.addButton("1") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btnFrench
btnFrench = cp5.addButton("2") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btnGerman
btnGerman = cp5.addButton("3") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btnAsym
btnAsym = cp5.addButton("4") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btnAlmaz
btnAlmaz = cp5.addButton("5") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btnLathe
btnLathe = cp5.addButton("6") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btnGreece
btnGreece = cp5.addButton("7") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton);
global btnKamin
btnKamin = cp5.addButton("8") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btnPyramid
btnPyramid = cp5.addButton("9") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btnLamp
btnLamp = cp5.addButton("10") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btnRhomb
btnRhomb = cp5.addButton("11") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btnCarlson
btnCarlson = cp5.addButton("12") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
Картинку imgButton мы загрузили в функции preload. В книге её не покажешь,
потому что это полностью прозрачный квадрат.
За каждой кнопкой мы закрепляем функцию, которая вызывается при нажатии
на неё мышкой:
# НАЖИМАЕМ КНОПКУ МЫШКИ
def mousePnessed():
# если нажата кнопка...
if btnClassic.isMousePressed():
playClassic()
elif btnFrench.isMousePressed():
playFnench()
elif btnGerman.isMousePressed():
playGenman()
elif btnAsym.isMousePnessed():
playAsym()
elif btnAlmaz.isMousePnessed():
playAlmaz()
elif btnLathe.isMousePressed():
playLathe()
elif btnGneece.isMousePnessed():
playGneece()
elif btnKamin.isMousePressed():
playKamin()
elif btnPynamid.isMousePnessed():
playPyramid()
elif btnLamp.isMousePnessed():
playLamp()
elif btnRhomb.isMousePressed():
playRhomb()
elif btnCanlson.isMousePnessed():
playCanlson()
Теперь самое время подумать о том, как представить условие задачи в про¬
грамме.
Доску для игры в солитер вполне разумно представить двумерным массивом.
Наибольшие размеры массива 9 х 9 клеток. Однако большинство досок имеют
размеры меньше, поэтому часть клеток в массиве не используется. Тем более
что в любом случае из-за формы доски в массиве останутся «лишние» клетки.
Остальные клетки либо пустые, либо с фишками. Обычно такого рода задачи
«шифруются» символами и собираются в строки. Каждая строка - это горизон¬
тальный ряд в списке или на доске.
Для обозначения клеток можно использовать любые символы. Пустые клетки
логично показать пробелами. Для отсутствующих на доске клеток я выбрал точ¬
ки, а для клеток с фишками - плюсик:
# классическая
доска:
puzzleClassic =
\
['
1
• ,
1
• ,
1
• ,
.+++++++
1
• ,
.+++ +++
1
• ,
.+++++++
1
• ,
1
• ,
1
• ,
1
{
"col":
4/ "now": 4 }]
Также в задачах присутствует и клетка, в которой должна закончиться игра. Её
трудно показать символом в строке, поскольку в ней может оказаться фишка.
Конечно, можно придумать особый символ для конечной клетки, в которой сто¬
ит фишка, но проще дописать её координаты после строк с символами.
|Мы пронумеруем клетки их индексами в массиве, а не так, как это делается в
журнале Наука и жизнь и в книге Гарднера.
Для сравнения - так выглядит классическая задача в нашей программе (Рис. 2).
Рис. 2
И
Точно так же, поглядывая на задания в книге, вы зашифруете и все остальные
задачки.
Когда заданий много, а так обычно и бывает в программах-головоломках, то
лучше и правильнее поместить их в отдельный файл. Я назвал его puzzles.py:
# This Python file uses the following encoding: utf-8
# классическая доска:
puzzleClassic = \
[' '
'...+++...'
'...+++...'
'.+++++++.'
'.+++ +++.'
'.+++++++.'
'...+++...'
i i
{ "col": 4,
*"row": 4 }]
# французская доска:
puzzleFrench = \
[' '
'...+++...'
..+++++..
'.+++++++.'
'.+++ +++.'
'.+++++++.'
..+++++..
i i
{ "col": 4,
'"row": 5 }]
# немецкая доска:
puzzleGerman = \
[ ...+++...
'...+++...'
'...+++...'
'+++++++++'
'++++ ++++',
+++++++++
{ "col": 4, "now": 4 }]
# асимметричная доска:
puzzleAsym = \
[
++++++++
+++ ++++
++++++++
{ "col": 4, "row": 4 }]
# ромбическая доска:
puzzleAlmaz = \
[ ....+.
..+++++.
.+++++++.
++++ ++++'
.+++++++.
..+++++.
{ "col": 4/ "row": 4 }]
# латинский крест:
puzzleLathe = \
{ "col": 4, "now": 4 }]
# греческий крест:
puzzleGreece =
\
['
i
• ,
i
+ ..
• ,
i
• ,
+
i
• ,
'. +++++
i
• ,
'. +
i
• ,
i
i
• ,
i
i
• >
i
• ,
{ "col":
4, "now":
4
}]
# камин:
puzzleKamin = \
['
i
• ,
i
• ,
i
• ,
'. +++
i
• ,
'. + +
i
• ,
i
• ,
i
• ,
i
• ,
i
• ,
{ "col":
4, "now":
4
}]
# пирамида:
puzzlePyramid =
\
['
i
i
• ,
i
'... + ..
• ,
i
• ,
'. +++
i
• ,
'. +++++
i
• ,
'.+++++++
i
i
• ,
i
• ,
i
• ,
i
• ,
{ "col":
4, "now":
4
}]
# лампа:
puzzleLamp = \
['
1
• ,
+ ..
1
• ,
1
• ,
'. +++++
1
• ,
. +
1
• ,
. +
1
• ,
1
• ,
1
1
• ,
1
{ "col":
4/ "row": 4 }]
# ромб:
puzzleRhomb = \
['
1
• ,
'... + ..
1
• ,
1
• ,
'. +++++
1
• ,
'.+++ +++
1
• ,
'. +++++
1
• ,
1
• ,
• •• + ••
1
• ,
• ,
{ "col":
4, "row": 4 }]
# задача Карлсона:
puzzleCarlson =
\
['
1
• ,
'. +++++.
1
• ,
'.++++++.
1
• ,
'.++++++.
1
• ,
'.++++++.
1
• ,
'.++++++.
1
• ,
'.++++++.
i
1
• ,
1
• ,
i
{ "col":
1
1/ "row": 1 }]
В главном файле программы импортируем все задания:
from puzzles import
*
Итак, все элементы управления со своими функциями, звуки и картинки загру¬
жаются и рисуются в главном файле программы PegSolitaire.pyde. А все особен¬
ности конкретной игры учитываются в классе игры, который мы поместим в от¬
дельный файл Game.py. Такой файл должен быть в каждой игровой программе.
А в главном файле мы создаём экземпляр класса Game, то есть собственно игру:
# создаём игру:
global game, imgGreen, imgRed
game = Game(imgGreen, imgRed)
Класс игры
Экземпляр класса создаётся (инициализируется) конструктором класса.
Совершенно очевидно, что нам нужна доска для игры, которую мы представим в
памяти компьютера списком списков board. Для его создания вызываем функ¬
цию createArray:
# КЛАСС ИГРЫ
class Game:
def init (self, imgGneen, imgRed):
self.imgGreen = imgGneen
self.imgRed = imgRed
# создаём доску:
self.boand = self.createArray(FIELD_WIDTH, FIELD_HEIGHT)
# СОЗДАЁМ ДВУМЕРНЫЙ СПИСОК
def createArray(self, cols, nows):
# игровое поле:
return [[None for now in range(rows)] for col in range(cols)]
Но такой список не хранит никакой информации о клетках поля, поэтому запол¬
няем его клетками:
self.createBoard()
Во вложенных циклах for мы перебираем все клетки из списка board и в каждую
из них помещаем клетку - экземпляр класса Cell:
# СОЗДАЁМ ДОСКУ
def createBoard(self):
for row in range(FIELD_HEIGHT):
for col in range(FIELD_WIDTH):
x = col * B_WIDTH
y = row * B_HEIGHT
self.board[col][row] = Cell(x, y, B_WIDTH, B_HEIGHT)
Класс клетки
Если доска (и вообще любое игровое поле) состоит из отдельных клеток, то их
также можно описать классом. Тогда манипулировать состоянием клеток гораз¬
до удобнее.
Состояние, или статус клетки обозначим константами:
# нет клетки:
NONE = 0
# пустая клетка:
EMPTY = 1
# фишка:
PEG = 2
Как вы помните, часть клеток списка board не принимает участия в игре, поэтому
мы должны полностью исключить их из дальнейшего рассмотрения. В эти клетки
мы запишем константу NONE. Пустые клетки игрового поля обозначим констан¬
той EMPTY, а клетку поля с фишкой - константой PEG.
Все поля клетки создаются и инициализируются в конструкторе:
# This Python file uses the following encoding: utf-8
from colors import *
from constants import *
# КЛАСС КЛЕТКИ
class Cell:
# КОНСТРУКТОР
def init (self, x, y, w, h):
# состояние клетки:
self.status = NONE
Он получает координаты верхнего левого угла и размеры клетки в пикселях:
# коорд. лев.верхнего угла:
self.x = x
self.y = y
# размеры картинки:
self.width = w
self.height = h
Эти данные необходимы клетке, чтобы начертить себя на экране.
Также клетка может быть целевой:
# целевая клетка?
self.target = False
И «зелёной», то есть такой, что в неё можно выполнить прыжок зелёной фиш¬
кой:
# готова принять фишку?
self.green = False
Класс игры (продолжение)
В функции setup мы вызываем функцию playClassic для начала игры на классиче¬
ской доске:
# начинаем игру:
playClassic()
Все «кнопочные» функции вызывают функцию и метод newGame:
# КЛАССИЧЕСКАЯ ДОСКА
def playClassic():
newGame(u'КЛАССИЧЕСКАЯ ДОСКА')
game.newGame(puzzleClassic)
Функция печатает название задачи в Консольном окне и присваивает перемен¬
ной lastPeg значение None:
# НОВАЯ ИГРА
def newGame(s):
print('')
pnint(s)
# перетаскиваемая фишка:
global lastPeg
lastPeg = None
Чтобы перемещать фишку по окну программы, мы запоминаем её в глобальной
переменной dragPeg:
# перетаскиваемая фишка:
dragPeg = None
Эта фишка делает прыжок. Но ход может на этом и не закончиться, если та же
самая фишка сделает ещё один прыжок. Это значит, что мы должны запомнить,
какая фишка прыгала последней. Её мы и запоминаем в переменной lastPeg:
# последняя перетаскиваемая фишка:
lastPeg = None
Естественно, в начале решения задачи такой фишки ещё не было.
Затем вызывается одноимённый метод игры newGame, которому мы передаём
список с информацией о текущем задании.
Чтобы не возвращаться, рассмотрим остальные кнопочные функции, которые
мало чем отличаются от классического варианта:
# ФРАНЦУЗСКАЯ ДОСКА
def playFrench():
newGame(u'ФРАНЦУЗСКАЯ ДОСКА')
game.newGame(puzzleFrench)
# НЕМЕЦКАЯ ДОСКА
def playGerman():
newGame(u'НЕМЕЦКАЯ ДОСКА')
game.newGame(puzzleGerman)
# НЕСИММЕТРИНАЯ ДОСКА
def playAsym():
newGame(u'НЕСИММЕТРИНАЯ ДОСКА')
game.newGame(puzzleAsym)
# АЛМАЗ
def playAlmaz():
newGame(u'АЛМАЗ')
game.newGame(puzzleAlmaz)
# ЛАТИНСКИЙ КРЕСТ'
def playLathe():
newGame(u^A^HC^ КРЕСТ')
game.newGame(puzzleLathe)
# ГРЕЧЕСКИЙ КРЕСТ
def playGreece():
newGame(u'ГРЕЧЕСКИЙ КРЕСТ')
game.newGame(puzzleGreece)
# КАМИН
def playKamin():
newGame(u'КАМИН')
game.newGame(puzzleKamin)
# ПИРАМИДА
def playPyramid():
newGame(u'ПИРАМИДА')
game.newGame(puzzlePyramid)
# НАСТОЛЬНАЯ ЛАМПА
def playLamp():
newGame(u'НАСТОЛЬНАЯ ЛАМПА')
game.newGame(puzzleLamp)
# РОМБ
def playRhomb():
newGame(u'РОМБ')
game.newGame(puzzleRhomb)
# ЗАДАЧА КАРЛСОНА
def playCarlson():
newGame(u'ЗАДАЧА КАРЛСОНА')
game.newGame(puzzleCarlson)
В методе newGame мы выполняем обязательные действия для каждого задания
- обнуляем число ходов и прерываем игру:
# НОВАЯ ИГРА
def newGame(self, puzzle):
self.createPuzzle(puzzle)
# номер хода:
self.nMove = 0
self.flgGameOven = False
Мы передали этому методу список с информацией о задании, и он вызывает ме¬
тод createPuzzle для создания игрового поля.
Как вы помните, все списки состоят из строк с символами, обозначающими со¬
стояние клеток доски. Кроме того, свойства всех фишек мы запишем в список
pegs:
# СОЗДАЁМ ЗАДАНИЕ
def cneatePuzzle(self, puzzle):
# список фишек:
self.pegs = []
Всего в массиве FIELD HEIGHT строк:
stn = puzzle[now]
Для классического задания и большинства других первая строка целиком состо¬
ит из точек:
Во всех строках по FIELD_WIDTH символов, которые мы просматриваем во вло¬
женном цикле for:
for col in range(FIELD_WIDTH):
Так как только одна клетка может быть целевой, то мы предварительно присва¬
иваем всем полям target значение False:
self.board[col][row].target = False
|-Ч w w
В переменную с мы записали очередной символ текущей строки:
c = str[col]
Если это точка, то на доске такой клетки нет:
if (c == "."):
# клетки нет:
self.board[col][row].status = NONE
Если пробел, то это пустая клетка доски:
elif (c == " "):
# пустая клетка:
self.board[col][row].status = EMPTY
Если плюс, то это клетка доски, на которой стоит фишка:
elif (c == "+"):
# клетка с фишкой:
self.board[col][row].status = PEG
И вот тут мы должны создать новую фишку и поместить её в список pegs:
self.pegs.append(Peg(col, now, B_WIDTH / 2, self.imgGreen, self.imgRed))
Все строки в списке puzzle просмотрены, доска и фишки созданы. После строк в
списке puzzle мы записали координаты целевой (конечной) клетки. Извлекаем их
в переменную targetCell:
targetCell = puzzle[FIELD_HEIGHT]
И отмечаем в списке board эту клетку:
self.board[targetCell["col"]][targetCell["row"]].target = True
Класс фишки
Фишка - это главное действующее лицо нашей программы. Можно сказать, её
объект и субъект. В реальной игре она представляет собой фишку, штырёк, ка¬
мешек или монетку, то есть настоящие предметы, которые можно взять в руку и
переложить на другую клетку поля или вообще убрать с доски. Вполне разумно
наши виртуальные фишки представлять объектами класса Peg, который описы¬
вает все немногочисленные свойства и поведение наших фишек.
Обычно фишки спокойно лежат в клетке, координаты которой мы передаём кон¬
структору:
# This Python file uses the following encoding: utf-8
from constants import *
# КЛАСС ФИШКИ
class Peg:
# КОНСТРУКТОР
def init (self, col, now, radius, imgGreen, imgRed):
# текущая клетка:
self.col = col
self.row = row
Радиус фишки необходимо знать, чтобы определить, ухватилась ли за неё мыш¬
ка:
# радиус фишки:
self.radius = radius
Фишка может быть зелёной или красной в зависимости от того, есть у неё ходы в
текущей позиции или нет. В конструкторе ей можно присвоить любой цвет, по¬
тому что дальше мы будем анализировать каждую новую позицию на доске:
self.status = GREEN
И последние 3 поля фишки отвечают за перемещение фишки мышкой и хране¬
ние картинок:
self.dragged = False
self.imgGreen = imgGreen
self.imgRed = imgRed
Рисуем сцену
На этом длительные приготовления к игре закончены, и начинает свою работу
функция draw:
# ОБНОВЛЯЕМ СЦЕНУ
def draw():
# игра закончилась:
if (game.flgGameOver):
return
# рисуем новый кадр:
drawSzene()
Для удобства и пользы дела всё рисование мы перенесли в функцию drawSzene.
Как обычно, сначала мы обновляем фон или фоновую картинку, как в этом слу¬
чае:
# ОБНОВЛЯЕМ СЦЕНУ
def drawSzene():
# очищаем окно:
imageMode(CORNER)
background(imgBack)
imageMode(CENTER)
На канве появится картинка с кнопками и свободное пространство для будущей
доски (Рис. 3).
Рисуем на канве все клетки доски, вызывая для каждой её метод draw:
# толщина и цвет их контуров:
strokeWeight(1)
stroke(Black)
rectMode(CORNER)
# рисуем клетки:
for row in range(FIELD_HEIGHT):
for col in range(FIELD_WIDTH):
game.board[col][row].draw()
Все клетки имеют чёрный контур толщиной в 1 пиксель, поэтому эти параметры
можно установить разом для всех клеток доски.
Метод draw рисует только клетки доски, а все остальные, имеющие статус NONE,
на экране даже не появятся:
# РИСУЕМ КЛЕТКУ
def dnaw(self):
if (self.status != NONE):
Все клетки в «нормальном» состоянии - белые, но при переносе фишки некото¬
рые из них мы зальём зелёным цветом, чтобы игрок сразу мог видеть клетки, в
которые можно поставить фишку:
# цвет заливки клетки:
fill(White)
# в эту клетку можно ходить:
if (self.green):
fill(PaleGreen)
С точки зрения геометрии, клетка - это простой прямоугольник. А в нашем слу¬
чае частный вид прямоугольника - квадрат:
# рисуем клетку:
nect(self.x, self.y, self.width, self.height)
Одна из клеток поля - конечная. Обозначаем её зелёным квадратиком в центре:
# целевая клетка:
if (self.target):
fill(Green)
rect(self.x + 20, self.y + 20, self.width - 40, self.height -
40)
Для классического варианта игры доска на экране выглядит так (Рис. 4).
Теперь можно нарисовать фишки. В «натуральной» игре все фишки одного цве¬
та, поэтому игрок должен самостоятельно найти фишки, которыми можно хо¬
дить. В компьютерной игре всё проще! Мы можем перебрать все фишки на дос¬
ке и определить, какие из них могут сделать прыжок. «Прыжковые» фишки от¬
мечаем зелёным цветом, а «непрыжковые» - красным.
Для этого в функции drawSzene вызываем метод игры fill Pegs:
# рисуем все фишки
# без перетаскиваемой:
game.fillPegs()
А он, в свою очередь, вызывает метод testMove с координатами проверяемой
клетки:
# РАСКРАШИВАЕМ ФИШКИ
def fillPegs(self):
for p in self.pegs:
p.status = GREEN if self.testMove(p.col, p.now) else RED
Каждая фишка может прыгнуть вверх, вниз, влево или вправо, оставаясь при
этом на доске. По правилам игры, она должна перепрыгнуть через фишку в со¬
седней клетке и приземлиться на пустой клетке после этой фишки:
# ПРОВЕРЯЕМ, ЕСТЬ ЛИ ХОД У ФИШКИ
def testMove(self, col, now):
# вверх:
if (now - 2 >= 0 and \
self.board[col][row - 1].status
self.board[col][row - 2].status
return True
# вниз:
if (row + 2 < FIELD_HEIGHT and \
self.board[col][row + 1].status
self.board[col][row + 2].status
return True
PEG and \
EMPTY):
PEG and \
EMPTY):
# влево:
if (col - 2 >= 0 and \
self.board[col - 1][row].status ==
self.board[col - 2][row].status ==
return True
# направо:
if (col + 2 < FIELD_WIDTH and \
self.board[col + 1][row].status ==
self.board[col + 2][row].status ==
return True
# ходов нет:
return False
PEG and \
EMPTY):
PEG and \
EMPTY):
После раскрашивания фишек мы рисуем их на канве, вызывая метод draw для
каждой фишки в списке pegs:
for p in game.pegs:
p.draw()
Он рисует фишки, которые стоят на своих местах:
# РИСУЕМ ФИШКУ
def draw(self):
# перетаскиваемая фишка
# рисуется в другом методе:
if (self.dragged):
return
# вычисляем координаты картинки:
xc = self.col * B_WIDTH + self.radius
yc = self.row * B_HEIGHT + self.radius
# учитываем цвет фишки:
if (self.status == GREEN):
image(self.imgGreen, xc, yc)
else:
image(self.imgRed, xc, yc)
Все фишки заняли свои законные места на доске и окрашены в 2 цвета (Рис. 5).
Рис. 5
Ф2 I
Хорошо видно, что 4 фишки (они зелёного цвета) могут выполнить прыжок.
И завершает функцию drawSzene вызов функции drawInfo:
# печатаем информацию:
dnawInfo()
Эта игра не очень сложная, поэтому информации на экране будет совсем немно¬
го - только число оставшихся на доске фишек и число выполненных в игре хо¬
дов:
# ПЕЧАТАЕМ ИНФОРМАЦИЮ
def dnawInfo():
# свойства шрифта:
textSize(24)
textAlign(LEFT)
stnokeWeight(O)
# цвет текста:
fill(Yellow)
# число ходов:
text(u"Ходов: " + stn(game.nMove), 440, 40)
# число оставшихся фишек:
fill(Gneen);
text(u"Фишек: " + stn(len(game.pegs)), 20, 40)
Число ходов хранится в переменной nMove, а число фишек равно длине списка
pegs.
Игра приобрела полностью законченный вид (Рис. 6).
Ход игрока
Мы сделали свою работу, и теперь игрок должен взяться за дело, то есть за
фишку. А для этого он должен нажать кнопку мышки на фишке. На это действие
игрока отзывается функция mousePressed из библиотеки Процессинга. В ней
сразу проверяем, не нажата ли какая-нибудь кнопка:
# НАЖИМАЕМ КНОПКУ МЫШКИ
def mousePnessed():
# если нажата кнопка...
if btnClassic.isMousePnessed():
playClassic()
elif btnFrench.isMousePressed():
playFrench()
elif btnGenman.isMousePnessed():
playGerman()
elif btnAsym.isMousePnessed():
playAsym()
elif btnAlmaz.isMousePnessed():
playAlmaz()
elif btnLathe.isMousePnessed():
playLathe()
elif btnGneece.isMousePnessed():
playGneece()
elif btnKamin.isMousePnessed():
playKamin()
elif btnPynamid.isMousePnessed():
playPynamid()
elif btnLamp.isMousePnessed():
playLamp()
elif btnRhomb.isMousePnessed():
playRhomb()
elif btnCanlson.isMousePnessed():
playCanlson()
Затем мы проверяем, началась ли уже игра и нажата ли кнопка в области доски
или за её пределами (например, игрок нажал кнопку):
# игра уже закончилась:
if (game.flgGameOven):
netunn
# мышка не на доске:
if (mouseX > 570):
netunn
Прыжок может выполнить только зелёная фишка. Но не любая, а только та, на
которой нажата мышка:
# ищем в массиве pegs зелёную фишку
# на которой нажата мышка:
cand = list(filter(lambda p : p.status == GREEN and \
p.over(mouseX, mouseY), game.pegs))
У каждой фишки есть метод over, который умеет определять, находится ли кур¬
сор на фишке. Сначала мы вычисляем координаты центра фишки на доске, затем
находим расстояние по горизонтали и по вертикали между центром фишки и
курсором. И наконец, по теореме Пифагора определяем, находится ли курсор на
фишке:
# ПРОВЕРЯЕМ, НАХОДИТСЯ ЛИ ТОЧКА
# (px,py) ВНУТРИ ФИШКИ
def over(self, px, py):
xc = self.col * B_WIDTH + self.radius
yc = self.row * B_HEIGHT + self.radius
dx = xc - px
dy = yc - py
return (dx * dx + dy * dy < self.radius * self.radius)
Если в списке cand не оказалось ни одного элемента, значит, фишка нажата не
на зелёной кнопке. Прыжок выполнить нельзя:
# нажата не зелёная фишка:
if len(cand) == 0:
return
Если же всё нормально, то мы включаем звук, подтверждающий, что игрок ухва¬
тил фишку:
# звук:
sndPressed.trigger()
После «фильтрования» списка фишек самый первый элемент (и единственный)
списка cand - это и есть нажатая фишка. Запоминаем её в переменной dragPeg и
отмечаем, что она готова к переносу:
# перетаскиваемая фишка:
global dnagPeg
dnagPeg = cand[0]
dnagPeg.dnagged = True
При перемещении мышки с нажатой кнопкой фишка следует за ней. Если просто
устанавливать центр фишки в горячую точку курсора, то она резко прыгнет в эту
точку, что нехорошо. Для плавного начала движения следует запомнить раз¬
ность координат горячей точки курсора и центра фишки в глобальной перемен¬
ной:
# разность координат при перемещении фишки:
offset = { "dx" : 0, "dy" : 0 }
# запоминаем разность координат
# центра фишки и курсора:
global offset
offset["dx"] = dnagPeg.getXYQ["x"] - mouseX
offset["dy"] = dnagPeg.getXYQ["y"] - mouseY
Так как в переменных col и row мы храним колонку и строку, в которой находит¬
ся фишка, то нам нужно дополнительно вычислить координаты её центра:
# ВОЗВРАЩАЕМ КООРДИНАТЫ ЦЕНТРА ФИШКИ
def getXY(self):
return { "x" : self.col * B_WIDTH + self.nadius,
"y" : self.now * B_HEIGHT + self.nadius }
Когда игрок взял фишку, мы снова помогаем ему и ищем клетки, в которые эта
фишка может прыгнуть. Поскольку она зелёная, то хотя бы одна такая клетка на
доске имеется. Но их может оказаться и больше. Чтобы найти все клетки-
кандидатки, мы вызываем метод getGreens с координатами клетки, в которой
находится фишка:
# клетки, в которые можно перепрыгнуть:
gneens = game.getGneens(dnagPeg.col, dnagPeg.now)
Этот метод возвращает список координат всех доступных клеток на доске:
# ВОЗВРАЩАЕМ КООРДИНАТЫ ПУСТЫХ КЛЕТОК,
# В КОТОРЫЕ ФИШКА МОЖЕТ ПРЫГНУТЬ ИЗ
# КЛЕТКИ (col, now)
def getGreens(self, col, now):
gneens = []
# вверх:
if (now - 2 >= 0 and \
self.board[col][row - 1].status == PEG and \
self.boand[col][now - 2].status == EMPTY):
gneens.append({ "col": col, "now": now - 2 })
# вниз:
if (now + 2 < FIELD_HEIGHT and \
self.boand[col][now + 1].status == PEG and
self.boand[col][now + 2].status == EMPTY):
gneens.append({ "col": col, "now": now + 2 })
# влево:
if (col - 2 >= 0 and \
self.boand[col - 1][now].status == PEG and
self.boand[col - 2][now].status == EMPTY):
gneens.append({ "col": col - 2, "now": now })
# направо:
if (col + 2 < FIELD_WIDTH and \
self.boand[col + 1][now].status == PEG and
self.boand[col + 2][now].status == EMPTY):
gneens.append({ "col": col + 2, "now": now })
netunn gneens
И
Клетки, оказавшиеся в списке greens, отмечаем как зелёные:
# их мы закрасим зелёным цветом:
for g in greens:
game.board[g["col"]][g["row"]].green = True
При обновлении сцены в функции drawSzene для каждой клетки вызывается её
метод draw. Если переменная green имеет значение True, то клетка закрашива¬
ется зелёным цветом:
# в эту клетку можно ходить:
if (self.green):
fill(PaleGreen)
В начальной позиции все зелёные фишки могут прыгнуть только в центральную
клетку, что хорошо видно на Рис. 7.
Рис. 7
При переносе фишки изменяются координаты мышки mouseX и mouseY. Если
мы прибавим к ним смещение центра фишки, то получим координаты центра
фишки. Передаём их в метод drawXY перетаскиваемой фишки:
# рисуем перетаскиваемую фишку:
global gragPeg
if (dnagPeg):
dragPeg.drawXY(mouseX + offset["dx"],
mouseY + offset["dy"])
Так как перетаскиваемая фишка всегда зелёная, то, зная её координаты, мы лег¬
ко нарисуем фишку в нужном положении:
# РИСУЕМ ПЕРЕТАСКИВАЕМУЮ ФИШКУ
def drawXY(self, xc, yc):
image(self.imgGreen, xc, yc)
Вы можете перемещать мышку в пределах окна, и фишка послушно будет сле¬
довать за ней (Рис. 8).
Рис. 8
Перетащив фишку в нужную клетку, игрок отпускает кнопку мышки. Это событие
вызывает функцию mouseReleased библиотеки Процессинг. Вообще говоря, иг¬
рок мог нажать, а затем отпустить кнопку мышки в любом месте экрана, а не
только на зелёной фишке. Поэтому, прежде всего, нужно убедиться, что игрок
вообще перетаскивал фишку:
# ОТПУСКАЕМ КНОПКУ МЫШКИ
def mouseReleased():
global dragPeg, lastPeg
# фишка не перетаскивалась:
if not dragPeg:
return
Если мышка держит фишку, то мы должны найти клетку, в которую игрок пере¬
нёс фишку. Обратите внимание, что в этой клетке должен оказаться курсор, а не
просто любая часть фишки:
# новая клетка:
newCol = - 1
newRow = -1
# ищем клетку для фишки:
for row in range(FIELD_HEIGHT):
for col in range(FIELD_WIDTH):
if game.board[col][row].over(mouseX, mouseY):
newCol = col
newRow = row
break
Для проверки клеток вызываем их метод over:
# ПРОВЕРЯЕМ, НАХОДИТСЯ ЛИ ТОЧКА
# (px,py) ВНУТРИ КЛЕТКИ
def over(self, px, py):
return (px >= self.x and px <= self.x + self.width and \
py >= self.y and py <= self.y + self.height)
Так как клетка имеет простую квадратную форму, то проверка совсем простая.
Если игрок плохо усвоил правила игры или у него вдруг дёрнулась рука, то он
мог отпустить мышку вообще не на клетке. А, может быть, и на клетке, но не на
зелёной. В этом случае прыжок не засчитывается, все клетки доски снова стано¬
вятся белыми, а фишка возвращается на прежнее место под обидный звук:
# фишка не на поле или клетка не зелёная:
if newCol < 0 on \
not game.board[newCol][newRow].green:
# все клетки - белые:
for r in game.board:
for c in r:
c.green = False
# фишка не перетаскивается:
dragPeg.dragged = False
dragPeg = None
# звук ошибочного хода:
sndError.trigger()
return
Если посадочная клетка для фишки выбрана верно, то мы опять же окрашиваем
все клетки доски в белый цвет, поскольку прыжок уже выполнен и подсказки не
нужны:
# все клетки - белые:
for r in game.board:
for c in r:
c.green = False
Мы должны убрать прыгнувшую фишку из старой клетки. Теперь она пустая:
# старые координаты фишки:
oldCol = dragPeg.col
oldRow = dragPeg.row
# освобождаем старую клетку:
game.board[oldCol][oldRow].status = EMPTY
И прописать её в новой клетке:
# занимаем новую клетку:
game.board[newCol][newRow].status = PEG
Это просто. А вот перепрыгнутая фишка доставляет нам гораздо больше хлопот!
Сначала нужно найти клетку, в которой она стоит:
# удаляем перепрыгнутую фишку:
dc = newCol - oldCol
dn = newRow - oldRow
# её координаты:
c = oldCol + dc // 2
n = oldRow + dn // 2
Затем - её индекс в списке pegs:
id = 0
while id < len(game.pegs):
if (game.pegs[id].col == c and game.pegs[id].row == n):
break
id += 1
Он нужен нам, чтобы удалить фишку их списка:
del(game.pegs[id])
Можно просто отмечать удалённые фишки в списке, но радикальный способ в
целом более удобен.
И напоследок мы освобождаем клетку, на которой стояла перепрыгнутая фишка:
game.boand[c][n].status = EMPTY
При подсчёте ходов мы должны учесть, что последовательные прыжки одной и
той же фишкой считаются одним ходом. Если же прыжок выполнен другой фиш¬
кой, то мы добавляем игроку ход, запоминаем последнюю перетаскиваемую
фишку и печатаем номер хода в Консольном окне:
# новые координаты фишки:
dnagPeg.col = newCol
dnagPeg.now = newRow
# прыгала новая фишка:
if (dnagPeg != lastPeg):
# добавляем ход:
game.nMove += 1
# запоминаем последнюю
# перетаскиваемую фишку:
lastPeg = dnagPeg
# печатаем номер хода в консольном окне:
рп^^и'Ход: ' + str(game.nMove))
Эта информация не обязательная, но очень важная, если вы хотите сохранить
все ходы - а вдруг вы решите задачу за меньшее число ходов, чем другие игроки
и установите мировой рекорд! Поэтому мы записываем в Консольном окне и ко¬
ординаты клеток - начала и конца прыжка:
# печатаем координаты прыжка в консольном окне:
print(str(oldCol) + " " + str(oldRow) + " --> " +
stn(newCol) + " " + str(newRow))
Покончив с формальностями, мы звучно плюхаем фишку на новую клетку и от¬
мечаем её как неперетаскиваемую:
# звук приземления фишки в клетке:
sndMove.trigger()
# фишка не перетаскивается:
dragPeg.dragged = False
dragPeg = None
Так как каждый ход может оказаться победным, то мы вызываем функцию
testGameOver, которая умеет проверять, не закончена ли игра на очередном хо¬
ду:
# проверяем, не закончилась ли игра:
testGameOver()
Но до этого радостного (или не очень) момента ещё далеко, поэтому посмот¬
рим, как изменилась ситуация на доске после первого хода игрока (Рис. 9).
Рис. 9
Всё верно: фишка перепрыгнула в центр доски. Перепрыгнутая фишка исчезла,
поэтому число фишек на доске уменьшилось. Число ходов увеличилось на 1. А
затем программа самостоятельно, без лишних просьб и напоминаний перекра¬
сила фишки на доске. Теперь у игрока осталось только 3 зелёные фишки для
следующего хода. Легко проверить, что любая из них может прыгнуть в един¬
ственную клетку (Рис. 10).
Рис. 10
Однако давайте посмотрим на протокол игры в Консольном окне. В нём мы ви¬
дим, что играется классическая партия, выполнен 1 ход из клетки (2 4) в клетку (4
4):
КЛАССИЧЕСКАЯ ДОСКА
Ход: 1
2 4 --> 4 4
Если вам нужна более подробная информация, например число оставшихся
фишек или номер клетки с перепрыгнутой фишкой, то поработайте над ис¬
ходным кодом самостоятельно. Возможно, вы захотите изменить нумерацию
клеток при разборе ходов в журнале Наука и жизнь или в книге Мартина
Гарднера.
Окончание игры
Любая игра когда-нибудь заканчивается. Закончиться она может либо победой,
либо поражением игрока.
Игрок побеждает, если на доске осталась единственная фишка в конечной клет¬
ке.
|В нашей игре фишка может закончить свои прыжки в любой клетке, в про¬
тивном случае некоторые задачи будут слишком сложны.
Игрок проигрывает, если на доске осталось больше одной фишки, которые не
могут выполнить ни одного прыжка.
В журнале Наука и жизнь, №3 за 1975 год, на странице 127 показано, как за 6
ходов получить матовую позицию, в которой нет ни одного хода (Рис. 11).
BniuB
□□□□□□□
ИЕЗПЕЕПЕЭП
□□□□□□□
ВгюВВ
Рис. 11
В начале игры пустая клетка имеет номер 47, но можно прийти к той же позиции
и с центральной пустой клеткой. На доске осталось 26 фишек, у которых нет ни
одного хода.
После каждого хода вызывается функция testGameOver. Если длина списка фи¬
шек равна 1, значит, игра закончена, о чём мы сообщаем функции gameOver с
аргументом True:
# ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ
# ЛИ ИГРА
def testGameOver():
# обновляем сцену:
drawSzene()
# если осталась последняя фишка,
# то игрок выиграл:
if (len(game.pegs) == 1):
gameOver(True)
Если на доске больше одной фишки, то нужно проверить, есть ли у них ходы. Ес¬
ли ходов нет, то число зелёных фишек равно 0:
# если ходов нет, то
# игрок проиграл:
if game.calcPegs()["green"] == 0:
gameOver(False)
Считать фишки умеет метод calcPegs:
# СЧИТАЕМ ОСТАВШИЕСЯ ФИШКИ
def calcPegs(self):
restGreen = 0
restRed = 0
for p in self.pegs:
if p.status == GREEN:
restGreen += 1
else:
restRed += 1
return { "all": restGreen + restRed, "green": restGreen, "red": restRed }
Он считает зелёные и красные фишки, а возвращает и общее число фишек, и
число фишек каждого цвета в отдельности.
В случае проигрыша вызывается функция gameOver с аргументом False.
Функции gameOver остаётся только напечатать нужное сообщение и закончить
игру:
# ИГРА ЗАКОНЧЕНА
def gameOver(win):
if (game.flgGameOver):
return
# рисуем табличку:
strokeWeight(2)
stroke(Black)
fill(255, 0, 0, 160)
rectMode(CORNER)
rect(30, -45 + 290, 500, 70)
strokeWeight(0)
# звук окончания игры:
sndWin.trigger()
# печатаем сообщение:
fill(255, 255, 0)
textAlign(CORNER)
textSize(48)
if (win):
print(u'ПОБЕДА!')
text^'^l ЭТО СДЕЛАЛИ!", 60, 300)
else:
print(u'БОЛЬШЕ НЕТ ХОДОВ!')
text^'^ ВАС НЕТ ХОДОВ!", 50, 300)
# игра закончена:
game.flgGameOver = True
Рассмотрим, как можно заматовать себя в простейшей задаче Латинский крест.
Ходим центральной фишкой вниз (Рис. 12).
Рис. 12
Выполняем ход любой зелёной фишкой. Игра закончена (Рис. 13).
Фишек 4
Ходов 2
о
о
о
У ВАС НЕТ ХОДОВ!
Рис. 13
А вот так нужно играть, чтобы победить за 5 ходов (Рис. 14)
Как указывает Мартин Гарднер, это минимальное число ходов для решения за¬
дачи.
Сохранить свои достижения вы можете, скопировав протокол игры из Консоль¬
ного окна в какой-нибудь текстовый редактор:
ЛАТИНСКИЙ КРЕСТ
Ход: 1
4 3 --> 2 3
Ход: 2
4 5 --> 4 3
Ход: 3
5 3 --> 3 3
Ход: 4
2 3 --> 4 3
Ход: 5
4 2 --> 4 4
ПОБЕДА!
Новая игра
Чтобы начать новую игру, нажмите нужную кнопку на нашей «клавиатуре» (Рис.
15).
Как решить задачу
Начните с тренировочных задач. Латинский крест мы уже решили. На решение
Греческого креста потребуется 6 ходов. Но если заканчивать игру в произволь¬
ной клетке, то и за 5.
Если какие-то задачи вам не дадутся, то поищите решения в журнале Наука и
жизнь и книге Гарднера.
На Ютубе есть познавательный ролик How To Solve The Peg Solitaire Puzzle, пока¬
зывающий, как решить классическую задачу за 18 ходов.
Для более серьёзного изучения игры почитайте четвёртый том книги Winning
Ways for your Mathematical Plays (Berlekamp, Conway, Guy, 2004 год).
Задачи для самостоятельного решения
Игровое поле имеет произвольную форму и по периметру обнесено стенами, на
которые и за которые прыгнуть нельзя.
Ходы выполняются, как в солитере. Задание считается выполненным, если на
поле осталась только одна фишка.
К сожалению, головоломка не очень популярна, и мне удалось найти всего не¬
сколько картинок с уровнями. Остальные вам придётся составлять вручную.
Напишите программу для игры Siji.
Рисование игрового поля подробно рассматривается дальше, в программе Elimi¬
nator Puzze.
Прыгающие лягушки (Jumping Frogs)
Прыгающие лягушки - одна из самых известных комбинаторных головоломок. В
Интернете вы без труда найдёте множество программ для игры с лягушками
(Рис. 1 и 2).
Рис. 1
Рис. 2
У этой головоломки есть и другие названия: Sheep and Goats, Hares and
Tortoises и Toads and Frogs.
Правила игры
Вспомним правила игры:
На листьях (или на камнях) сидят 6 лягушек - по 3 лягушки слева и справа.
Между ними имеется 1 свободный лист (или камень). Нужно за минимальное
число ходов поменять лягушек местами.
Ход выполняется так:
• Можно передвинуть левую лягушку вправо на соседнее свободное место.
• Можно передвинуть правую лягушку влево на соседнее свободное место.
• Можно перепрыгнуть левой лягушкой вправо через правую лягушку на сво¬
бодное место.
• Можно перепрыгнуть правой лягушкой влево через левую лягушку на сво¬
бодное место.
Лягушачьи задачи
В книге Болла и Коксетера Математические эссе и развлечения (Мир, 1986), на
страницах 135-136 рассматривается эта же задача, но в ней лягушек заменили
белые и чёрные пешки, обозначенные на рисунке буквами a и b (Рис. 3).
Рис. 3
В журнале Наука и жизнь №7 за 1962 год, на странице 93 была напечатана зада
ча с фишками (Рис. 4).
ф Нарисуйте семь квад-
ратинов. Вырежьте из карто¬
на шесть фишек и на каждой
напишите цифру (от 1 до 6).
Расставьте фишки так, как
показано на рисунке.
Конечная цель — поме¬
нять фишки местами, чтобы
положение их в квадратах
было следующее:
Разрешается передвигать
фишки на свободную клет¬
ку, а также перешагивать
через одну фишку, если
впереди есть свободная
клетка. Сколько ходов вам
потребуется для решения
задачи?
Рис. 4
В этой задаче все фишки пронумерованы, но ни белые, ни чёрные фишки не пе¬
репрыгивают друг через друга, поэтому их порядок не изменяется.
Также отсутствует запрет на одностороннее перемещение фишек, но, если речь
идёт о самом коротком решении, то он и не нужен.
Если задачу перенести на плоскость, то лягушек можно рассадить в прямоуголь¬
ной матрице (Рис. 5).
а
а
а
а
ь
ь
ь
а
а
а
а
ь
ъ
ь
а
а
а
а
ь
ь
ь
а
а
а
ь
ь
ъ
а
а
а
Ь
ъ
ь
ь
а
а
.а
Ь
ь
ь
ь
а
а
а
ъ
ъ
ъ
ь
Рис. 5
Задача из книги Болла и Коксетера Математические эссе и развлечения, с.
136-137.
Задача остаётся прежней - поменять лягушек местами за минимальное чис¬
ло ходов. При этом лягушки а могут перемещаться только вправо и вниз, а
лягушки b - влево и вверх.
Для квадратной доски из (2n+1)2 клеток понадобится 2n(n+1)(n+2) ходов, из
которых 4n(n+1) - переползания, а остальные 2n2(n+1) ходов - прыжки.
Вторая задача из книги Болла и Коксетера Математические эссе и развлечения,
с. 137-138 решается на квадратной доске с «выгрызами», в которые ходить нель¬
зя. «Левые» лягушки обозначены буквами a..h, а «правые» - буквами A..H. Звёз¬
дочка показывает свободное поле (Рис. 6).
Генри Дьюдени решил задачу за 46 ходов, но это решение не единственное.
Эта задача была напечатана и в журнале Наука и жизнь, №1 за 1991 год, на
странице 62:
6<и <и <и <и
этой старинной английской головоломке, известной ещё под названием
”Игра в 16”, требуется поменять местами белые и чёрные шашки за
наименьшее число ходов. (Когда играют вдвоём, то задача такая же, как при
игре в "уголки”: обыграть противника, поставив свои шашки на место рань¬
ше).
Рис. 6
Шашки можно перемещать на соседнюю пустую клетку по горизонтали или
вертикали (в направлениях север, запад, юг, восток) или перепрыгивать через
рядом стоящую шашку независимо от её цвета в тех же направлениях.
Сэм Лойд, который публиковал свои головоломки в начале XX века, полагал,
что задача решается в 52 хода. Но есть решение и короче (Рис. 7).
Присланы решения в 50, 49
и 48 ходов. Последнее, пожа¬
луй, самое короткое из всех
возможных.
Приводим решение В. Бо-
ровлева (г. Чехов Московской
обл.). 11-6-3-11-8, 9-10-в-7-1,
11-2-9-10-7, 14-16-8-13-6, 3¬
2-14-13-7, 12-3-2-13-1, 4-13¬
5-10-12, 1-16-15-1-16, 15-2-5¬
15-4, 12-16-4. На рисунке —
Рис. 7
К программе!
Если внимательно, а не как-то иначе присмотреться к головоломке, то легко
увидеть почти прямую аналогию с солитером. Особенно она похожа на соли¬
терные задачи, которые решаются не в одну строку, а на плоскости.
Но сразу отметим и различия:
• фишки могут не только перепрыгивать друг через друга, то и переходить
на пустое соседнее поле
• перепрыгнутая фишка с доски не снимается, поэтому в игре принимают
участие все фишки
• фишки делятся на 2 вида: левые фишки ходят только вправо (и вниз, если
игра ведётся на двухмерной доске), а правые - только влево (и вверх). Хо¬
ды в обратную сторону не допускаются
• игра заканчивается победой игрока, если все фишки поменялись местами
• если часть фишек ещё не заняла свои места, а ходов больше нет, то игрок
проигрывает партию
• каждый прыжок - это отдельный ход; каскады прыжков не учитываются
• фишки символизируют не колышки (Pegs), а лягушек (Frogs). Соответ¬
ственно класс Peg переименовался во Frog, а файл Pegs.py во Frogs.py. Из¬
менения непринципиальные, но порядок есть порядок.
Максимальные размеры досок не превышают 9 х 9 клеток, поэтому мы запросто
переделаем фоновую картинку из предыдущего проекта (Рис. 8).
Название программы состоит из двух слов, поэтому все кнопки пришлось опу¬
стить на 10 пикселей. Чтобы сохранить все 12 кнопок, я придумал ещё несколько
задач, которые отличаются от классических только размерами.
Главный файл программы
Названия кнопок я изменил, чтобы они соответствовали задачам. Из уважения к
самой первой задаче я оставил её «классическое» название. Так как в этой про¬
грамме у нас фишки двух разных видов, то вполне естественно обозначить их в
условии буквами L(eft) и R(ight). Целевой клетки на поле больше нет, и послед¬
нюю строку можно исключить из списка:
# This Python file uses the following encoding: utf-8
# классическая доска:
puzzleClassic = \
LLL RRR.
# доска 5 x 1:
puzzle5x1 = \
LL RR..
# доска 9 x 1:
puzzle9x1 = \
LLLL RRRR
# доска 3 x 3:
puzzle3x3 = \
[
;
;
;
[
LR.
L R.
LRR.
# доска 5 x 3:
puzzle5x3 = \
];
# доска 7 x 3:
puzzle7x3 = \
];
# доска 9 x 3:
puzzle9x3 = \
];
LLLLLRRRR
LLLL RRRR
LLLLRRRRR
.LLLLRRR.
.LLL RRR.
.LLLRRRR.
..LLLRR..
. .LL RR..
. .LLRRR..
# доска 5 x 5:
puzzle5x5 = \
[
У
1
'..LLLRR..
'..LLLRR..
'..LL RR..
'..LLRRR..
'..LLRRR..
i
i
i
i
i
i
i
i
i
'];
# доска 7 x 5:
puzzle7x5 = \
['
i
'.LLLLRRR.
i
'.LLLLRRR.
i
'.LLL RRR.
i
'.LLLRRRR.
i
'.LLLRRRR.
i
i
i
i
'];
# доска 9 x 5:
puzzle9x5 = \
['
i
'LLLLLRRRR
i
'LLLLLRRRR
i
'LLLL RRRR
i
'LLLLRRRRR
i
'LLLLRRRRR
i
i
i
i
'];
# доска 7 x 7:
puzzle7x7 = \
['
i
'.LLLLRRR.
i
'.LLLLRRR.
i
'.LLLLRRR.
i
'.LLL RRR.
i
'.LLLRRRR.
i
'.LLLRRRR.
i
'.LLLRRRR.'
# игра в 16:
puzzle16 = \
]
• 1
'..LLL...
'..LLL...
'..LL RR.
' RRR.
' RRR.
• 1
Задания «шифруются» весьма просто, и вы можете напридумывать множество
собственных головоломок.
В программе мы не станем докучать игроку лягушками, а заменим их фишками.
Теми самыми, что и в предыдущем проекте. Пусть левые фишки будут зелёного
цвета, а правые - красного.
В книжно-журнальных задачах никаких подсказок нет, но мы сделаем себе по¬
блажку и нарисуем на фишках стрелки, показывающие направления возможного
их перемещения на текущем ходу. Зелёным фишкам дозволяется ходить и пры¬
гать вправо и вниз, а красным - влево и вверх. Мы не станем рисовать множе¬
ство фишек с разными стрелками, а удовольствуемся всего четырьмя картинка¬
ми со стрелками, которые напечатаем поверх фишек (Рис. 9).
Рис. 9
Названия картинок простые и понятные. Звуки оставим в неприкосновенности,
поскольку они вполне годятся и для лягушачьих задачек. Как всегда, начинает
программу функция preload, в которой мы загружаем все файлы с диска:
# ЗАГРУЖАЕМ МЕДИАФАЙЛЫ
def preload():
# загружаем картинки -->
global imgBack, imgGreen, imgRed, \
imgAnnowRight, imgAnnowDown, \
imgAnnowLeft, imgArrowUp, imgButton
# фоновая картинка:
imgBack = loadImage("data/imgBack.png")
# загружаем картинки с фишками:
imgGreen = loadImage("data/green.png")
imgRed = loadImage("data/ned.png")
# и со стрелками:
imgArrowRight = loadImage("data/arrow-right.png")
imgAnnowDown = loadImage("data/annow-down.png")
imgAnnowLeft = loadImage("data/arrow-left.png")
imgAnnowUp = loadImage("data/annow-up.png")
# прозрачная кнопка:
imgButton = loadImage("data/imgButton.png")
# загружаем звуки -->
global sndMove, sndPressed, sndEnnon, sndWin
minim = Minim(this)
sndMove = minim.loadSample("data/buljk.wav")
sndPnessed = minim.loadSample("data/pnessed.wav")
sndEnnon = minim.loadSample("data/error.wav")
sndWin = minim.loadSample("data/win.wav")
Благодаря нашей хитрой находчивости функция setup потребовала только не¬
большой переделки. В ней мы создаём окно, дюжину кнопок, игру и начинаем
решать классическую задачку:
# НАЧИНАЕМ ПЕРВУЮ ИГРУ
def setup():
# создаём окно:
size(WIDTH, HEIGHT)
# загружаем файлы:
pneload()
# координатные режимы:
imageMode(CENTER)
nectMode(CORNER)
И
# создаём кнопки -->
cp5 = ControlP5(this)
x = 0
y = 0
xb = 590 - 1
yb = 70 - 1
dx = 80
dy = 80
global imgButton
global btnClassic
btnClassic = cp5.addButton("1") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btn5x1
btn5x1 = cp5.addButton("2") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btn9x1
btn9x1 = cp5.addButton("3") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btn3x3
btn3x3 = cp5.addButton("4") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btn5x3
btn5x3 = cp5.addButton("5") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btn7x3
btn7x3 = cp5.addButton("6") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setlmage(imgButton)
yb += dy
global btn9x3
btn9x3 = cp5.addButton("7") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btn5x5
btn5x5 = cp5.addButton("8") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btn7x5
btn7x5 = cp5.addButton("9") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btn9x5
btn9x5 = cp5.addButton("10") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
yb += dy
global btn7x7
btn7x7 = cp5.addButton("11") \
.setPosition(x + xb, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
global btn16
btn16 = cp5.addButton("12") \
.setPosition(x + xb + dx, y + yb) \
.setSize(67,67) \
.setImage(imgButton)
# создаём игру:
global game, imgGreen, imgRed, \
imgArrowDown, imgArrowRight, imgArrowLeft, imgArrowUp
game = Game(imgGreen, imgRed, \
imgArrowDown, imgArrowRight, imgAnnowLeft, imgAnnowUp)
# начинаем игру:
playClassic()
Не сильно пострадали кнопочные функции:
# КЛАССИЧЕСКАЯ ДОСКА
def playClassic():
newGame(u'КЛАССИЧЕСКАЯ ДОСКА')
game.newGame(puzzleClassic)
# ДОСКА 5 x 1
def play5x1():
newGame(u'ДОСКА 5 x 1');
game.newGame(puzzle5x1);
# ДОСКА 9 x 1
def play9x1():
newGame(u'ДОСКА 9 x 1')
game.newGame(puzzle9x1)
# ДОСКА 3 x 3
def play3x3():
newGame(u'ДОСКА 3 x 3')
game.newGame(puzzle3x3)
# ДОСКА 5 x 3
def play5x3():
newGame(u'ДОСКА 5 x 3')
game.newGame(puzzle5x3)
# ДОСКА 7 x 3
def play7x3():
newGame(u'ДОСКА 7 x 3')
game.newGame(puzzle7x3)
# ДОСКА 9 x 3
def play9x3():
newGame(u'ДОСКА 9 x 3')
game.newGame(puzzle9x3)
# ДОСКА 5 x 5
def play5x5():
newGame(u'ДОСКА 5 x 5')
game.newGame(puzzle5x5)
# ДОСКА 7 x 5
def play7x5():
newGame(u'ДОСКА 7 x 5')
game.newGame(puzzle7x5)
# ДОСКА 9 x 5
def play9x5():
newGame(u'ДОСКА 9 x 5')
game.newGame(puzzle9x5)
# ДОСКА 7 x 7
def play7x7():
newGame(u'ДОСКА 7 x 7')
game.newGame(puzzle7x7)
# ИГРА В 16
def play16():
newGame(u'ИГРА В 16')
game.newGame(puzzle16)
# НОВАЯ ИГРА
def newGame(s):
print('')
print(s)
Класс игры
Игровая доска в этой программе осталась прежней, но место списка колышков
pegs занял список frogs:
# This Python file uses the following encoding: utf-8
from colors import *
from constants import *
from Cell import Cell
from Frog import Frog
# КЛАСС ИГРЫ
class Game:
def init (self, imgGreen, imgRed, imgArrowDown, imgArrowRight, im-
gArrowLeft, imgArrowUp):
# картинки:
self.imgGreen = imgGreen
self.imgRed = imgRed
self.imgArrowDown = imgArrowDown
self.imgArrowRight = imgArrowRight
self.imgArrowLeft = imgArrowLeft
self.imgArrowUp = imgArrowUp
# создаём доску:
self.board = self.createArray(FIELD_WIDTH, FIELD_HEIGHT)
self.createBoard()
# фишки:
self.frogs = []
# номер хода:
self.nMove = 0
# флаг игры:
self.flgGameOver = True
Процесс создания доски сохранился:
# СОЗДАЁМ ДВУМЕРНЫЙ СПИСОК
def createArray(self, cols, rows):
# игровое поле:
return [[None for row in range(rows)] for col in range(cols)]
# СОЗДАЁМ ДОСКУ
def createBoard(self):
for row in range(FIELD_HEIGHT):
for col in range(FIELD_WIDTH):
x = col * B_WIDTH
y = row * B_HEIGHT
self.board[col][row] = Cell(x, y, B_WIDTH, B_HEIGHT)
Но теперь мы должны учесть, что число конечных клеток увеличилось, ведь нам
нужно поменять местами левые (зелёные) и правые (красные) фишки, то есть
все левые фишки должны занять места правых, и наоборот. Чтобы не создавать
ещё один список для конечной позиции, дополним клетки ещё одной перемен¬
ной - color:
# This Python file uses the following encoding: utf-8
from colors import *
from constants import *
# КЛАСС КЛЕТКИ
class Cell:
# КОНСТРУКТОР
def init (self, x, y, w, h):
# состояние клетки:
self.status = NONE
# коорд. лев.верхнего угла:
self.x = x
self.y = y
# размеры картинки:
self.width = w
self.height = h
# целевая клетка:
self.color = None # GREEN или RED
# готова принять фишку?
self.green = False
Она может принимать значения GREEN или RED в зависимости от того, какого
цвета фишка должна занять клетку в конце решения задачи.
Тут нужно и пора вспомнить добрым словом файл с константами constants.py:
# This Python file uses the following encoding: utf-8
# размеры доски:
FIELD_WIDTH = 9
FIELD_HEIGHT = 9
# размеры канвы в пикселях:
WIDTH = 750
HEIGHT = 559
# размер клеток в пикселях:
B_WIDTH = 62
B_HEIGHT = 62
# нет клетки:
NONE = 0
# пустая клетка:
EMPTY = 1
# фишка:
FROG = 2
# зелёная фишка:
GREEN = 10
# красная фишка:
RED = 11
# зелёная фишка:
CAN_MOVED = 100
# красная фишка:
NOT CAN MOVED = 101
Функция playClassic отсылает нас в метод игры createPuzzle:
# НОВАЯ ИГРА
def newGame(self, puzzle):
self.createPuzzle(puzzle)
# номер хода:
self.nMove = 0
self.flgGameOver = False
В нём мы создаём головоломку по данным из списка строк puzzle:
# СОЗДАЁМ ЗАДАНИЕ
def createPuzzle(self, puzzle):
# список фишек:
self.frogs = []
for row in range(FIELD_HEIGHT):
s = puzzle[row]
for col in range(FIELD_WIDTH):
c = s[col]
В строках появились буквы L и R, обозначающие тип лягушки. Встретив такую
букву, метод createPuzzle создаёт новую фишку типа GREEN для левой лягушки и
фишку типа RED - для правой. А вот полю color клеток в массиве board он при¬
сваивает противоположное значение, потому что в конце игры на них должны
стоять фишки другого цвета:
# клетки нет:
if c == '. ':
self.board[col][row].status = NONE
self.board[col][row].color = None
# пустая клетка:
elif c == ' ':
self.board[col][row].status = EMPTY
self.board[col][row].color = None
# клетка с левой фишкой:
elif c == 'L':
self.board[col][row].status = FROG
self.board[col][row].color = RED
self.frogs.append(Frog(col, row, B_WIDTH / 2, GREEN,
self.imgGreen, self.imgRed, \
self.imgArrowDown, self.imgArrowRight, \
self.imgArrowLeft, self.imgArrowUp))
# клетка с правой фишкой:
elif c == 'R':
self.board[col][row].status = FROG
self.board[col][row].color = GREEN
self.frogs.append(Frog(col, row, B_WIDTH / 2, RED, self.imgGreen, self.imgRed, \
self.imgArrowDown, self.imgArrowRight, \
self.imgArrowLeft, self.imgArrowUp))
Создаём фишку
Фишка-лягушка почти такая же, как и фишка-колышек, но имеет дополнительное
поле color, которое хранит цвет, или тип фишки:
# This Python file uses the following encoding: utf-8
from constants import *
# КЛАСС ФИШКИ
class Frog:
# КОНСТРУКТОР
def init (self, col, row, radius, clr, \
imgGreen, imgRed, imgArrowDown, imgArrowRight, \
imgArrowLeft, imgArrowUp):
# текущая клетка:
self.col = col
self.row = row
# радиус фишки:
self.radius = radius
# можно ли фишку перемещать:
self.status = can_moved
# тип фишки:
self.color = clr # GREEN или RED
# картинки:
self.imgGreen = imgGreen
self.imgRed = imgRed
self.imgArrowDown = imgArrowDown
self.imgArrowRight = imgArrowRight
self.imgArrowLeft = imgArrowLeft
self.imgArrowUp = imgArrowUp
# перемещвется:
self.dragged = False
# возможные направления хода:
self.moveDirs = { "right": False, "down": False, \
"left": False, "up": False }
А в поле moveDirs мы запишем направления возможных ходов фишки. Но это
потом.
Рисуем сцену
Пора, пора главным героям нашей программы появиться на сцене! И они появ¬
ляются в функции draw:
# ОБНОВЛЯЕМ СЦЕНУ
def draw():
# игра закончилась:
if (game.flgGameOver):
return
# рисуем новый кадр:
drawSzene()
Которая, как и полагается главным функциям, делегирует свои полномочия
функции drawSzene:
# ОБНОВЛЯЕМ СЦЕНУ
def drawSzene():
# очищаем окно:
imageMode(CORNER)
background(imgBack)
imageMode(CENTER)
# толщина и цвет контуров:
strokeWeight(1)
stroke(Black)
rectMode(CORNER)
# рисуем клетки:
for row in game.board:
for c in row:
c.draw()
Для каждой клетки доски вызывается её метод draw.
Клетка на экране - это квадрат белого, зелёного или красного цвета. Поле color
получило значение при создании новой задачки, поэтому цвет клетки выбрать
несложно:
# РИСУЕМ КЛЕТКУ
def draw(self):
# если это клетка текущей доски:
if (self.status != NONE):
# цвет заливки клетки:
if (self.color == RED): # красная
fill(255, 230, 230)
elif (self.color == GREEN): # зелёная
fill(230, 255, 230)
else: fill(White) # белая
# рисуем клетку:
rect(self.x, self.y, self.width, self.height)
Клетки из списка board, которые не принимают участия в данной игре, мы на
экране не показываем, чтобы не сбивать игрока с толку. Цветные клетки помо¬
гают игроку ориентироваться в текущей позиции. Сразу видно, где в конце игры
должны быть зелёные фишки, а где - красные. Цвета я выбрал бледные, чтобы
они не портили общую картину (Рис. 10).
Клетку, в которую можно ходить, мы отметим жёлтым квадратиком. Но это слу¬
чится только тогда, когда игрок возьмёт фишку в лапку мышки:
# в эту клетку можно ходить:
if (self.green):
fill(Yellow)
rect(self.x + 20, self.y + 20, self.width - 40, self.height - 40)
Расставляем фишки в клетки, но предварительно вызываем метод getAllDirs,
чтобы записать в поле moveDirs все направления, по которым фишка может хо¬
дить:
# рисуем все фишки
# без перетаскиваемой:
game.getAllDirs()
Рис. 10
В методе getAllDirs мы учитываем, что левые лягушки могут ходить и прыгать
только вправо и вниз, а правые - только влево и вверх:
# НАХОДИМ ВОЗМОЖНЫЕ НАПРАВЛЕНИЯ
# ПЕРЕМЕШЕНИЯ ФИШЕК
def getAllDins(self):
for i in range(len(self.frogs)):
col = self.frogs[i].col
now = self.frogs[i].row
clr = self.frogs[i].color
self.frogs[i].moveDirs = { "night": False, "down": False, #
"left": False, "up": False }
# левая лягушка ходит
# вправо и вниз:
if (clr == GREEN):
# вправо:
if (col + 1 < FIELD_WIDTH and \
self.board[col + 1][row].status == EMPTY):
self.frogs[i].moveDirs["right"] = True
if (col + 2 < FIELD_WIDTH and \
self.board[col + 1][row].status == FROG and \
self.board[col + 2][row].status == EMPTY):
self.fnogs[i].moveDins["night"] = True
# вниз:
if (row + 1 < FIELD_HEIGHT and \
self.board[col][row + 1].status == EMPTY):
self.frogs[i].moveDirs["down"] = True
if (row + 2 < FIELD_HEIGHT and \
self.board[col][row + 1].status == FROG and \
self.board[col][row + 2].status == EMPTY):
self.frogs[i].moveDirs["down"] = True
# правая лягушка ходит
# влево и вверх:
else:
# вверх:
if (row - 1 >= 0 and \
self.board[col][row - 1].status == EMPTY):
self.frogs[i].moveDirs["up"] = True
if (row - 2 >= 0 and \
self.board[col][row - 1].status == FROG and \
self.board[col][row - 2].status == EMPTY):
self.frogs[i].moveDirs["up"] = True
# влево:
if (col - 1 >= 0 and \
self.board[col - 1][row].status == EMPTY):
self.frogs[i].moveDirs["left"] = True
if (col - 2 >= 0 and \
self.board[col - 1][row].status == FROG and \
self.board[col - 2][row].status == EMPTY):
self.frogs[i].moveDirs["left"] = True
Теперь вызываем метод draw для каждой фишки:
for p in game.frogs:
p.draw()
Перетаскиваемую фишку рисуем отдельно, чтобы не усложнять метод draw:
# РИСУЕМ ФИШКУ
def draw(self):
# перетаскиваемая фишка
# рисуется в другом методе:
if (self.dragged):
return
Сейчас мы уже знаем возможные перемещения фишки, а потому рисуем соот¬
ветствующие стрелки поверх тех фишек, которые могут ходить:
# вычисляем координаты картинки:
xc = self.col * B_WIDTH + self.radius
yc = self.row * B_HEIGHT + self.radius
# учитываем цвет фишки:
if (self.color == GREEN):
image(self.imgGreen, xc, yc)
if (self.moveDirs["down"]):
image(self.imgArrowDown, xc, yc)
if (self.moveDirs["right"]):
image(self.imgArrowRight, xc, yc)
else:
image(self.imgRed, xc, yc)
if (self.moveDirs["up"]):
image(self.imgArrowUp, xc, yc)
if (self.moveDirs["left"]):
image(self.imgArrowLeft, xc, yc)
После «стоячих» фишек рисуем перетаскиваемую:
# перетаскиваемая фишка:
dnagFnog = None
# разность координат при перемещении фишки:
offset = { "dx" : 0, "dy" : 0 }
# рисуем перетаскиваемую фишку:
if (dnagFnog):
dnagFnog.dnawXY(mouseX + offset["dx"],
mouseY + offset["dy"])
Метод для её рисования почти такой же, но он дополнительно получает коор¬
динаты фишки:
# РИСУЕМ ПЕРЕТАСКИВАЕМУЮ ФИШКУ
def dnawXY(self, xc, yc):
# учитываем цвет фишки:
if (self.colon == GREEN):
image(self.imgGneen, xc, yc)
if (self.moveDins["down"]):
image(self.imgAnnowDown, xc, yc)
if (self.moveDins["night"]):
image(self.imgAnnowRight, xc, yc)
else:
image(self.imgRed, xc, yc)
if (self.moveDins["up"]):
image(self.imgAnnowUp, xc, yc);
if (self.moveDins["left"]):
image(self.imgAnnowLeft, xc, yc)
Получилась замечательная картинка, на которой хорошо видно, какие фишки и
куда могут прыгать и ходить (Рис. 11).
И завершает функцию drawSzene вызов функции drawInfo:
# печатаем информацию:
dnawInfo()
Рис. 11
Она печатает число ходов и фишек, которые ещё не добрались до своего закон¬
ного места на доске:
# ПЕЧАТАЕМ ИНФОРМАЦИЮ
def dnawInfo():
# свойства шрифта:
textSize(24)
textAlign(LEFT)
stnokeWeight(O)
# цвет текста:
fill(Yellow)
# число ходов:
text(u"Ходов: " + stn(game.nMove), 440, 40)
# число оставшихся фишек:
fill(Red)
text^'^ на месте: " + stn(game.getRestFnogsQ), 20, 40)
Общее число фишек не меняется на протяжении всей игры, поэтому мы считаем
только те фишки, которые игрок ещё должен перенести. Для этого в методе
getRestFrogs считаем фишки, цвет которых не совпадает с цветом клетки, на ко¬
торой она стоит:
# СЧИТАЕМ ФИШКИ, КОТОРЫЕ НЕ НА МЕСТЕ
def getRestFnogs(self):
nest = 0
for f in self.fnogs:
if f.colon != self.board[f.col][f.row].color:
nest += 1
return nest
Вся скудная информация об игре - на экране (Рис. 12).
Рис. 12
Ход игрока
Вовсю пользуясь нашими подсказками, игрок выбирает одну из фишек со стрел¬
ками и нажимает на ней кнопку мышки. Программа тут же отправляется в функ¬
цию mousePressed:
# НАЖИМАЕМ КНОПКУ МЫШКИ
def mousePnessed():
# если нажата кнопка...
if btnClassic.isMousePnessed():
playClassic()
elif btn5x1.isMousePnessed():
play5x1()
elif btn9x1.isMousePnessed():
play9x1()
elif btn3x3.isMousePnessed():
play3x3()
elif btn5x3.isMousePnessed():
play5x3()
elif btn7x3.isMousePnessed():
play7x3()
elif btn9x3.isMousePnessed():
play9x3()
elif btn5x5.isMousePnessed():
play5x5()
elif btn7x5.isMousePnessed():
play7x5()
elif btn9x5.isMousePnessed():
play9x5()
elif btn7x7.isMousePnessed():
play7x7()
elif btn16.isMousePnessed():
play16()
# игра уже закончилась:
if (game.flgGameOven):
netunn
# мышка не на доске:
if (mouseX > 570):
netunn
Игрок может ухватиться за любую фишку, но мы разрешим ему передвигать
только такую, у которой имеются ходы в данной позиции:
# ищем в списке frogs фишку,
# которую можно перетащить и
# на которой нажата мышка:
game.colorFrogs()
cand = list(filter(lambda p : p.status == CAN_MOVED and \
p.over(mouseX, mouseY), game.frogs))
Метод colorFrogs записывает в поле status каждой фишки значение CAN_MOVED,
если у фишки имеются ходы, и NOT_CAN_MOVED в противном случае:
# ОТМЕЧАЕМ ФИШКИ, КОТОРЫЕ МОЖНО ПЕРЕДВИГАТЬ
def colorFrogs(self):
for f in self.frogs:
f.status = CAN_MOVED if self.testMove(f.col, f.row, f.color) \
else NOT CAN MOVED
# ПРОВЕРЯЕМ, ЕСТЬ ЛИ ХОД У ФИШКИ
def testMove(self, col, row, clr):
# левая лягушка ходит
# вправо и вниз:
if (clr == GREEN):
# вправо:
if (col + 1 < FIELD_WIDTH and \
self.board[col + 1][row].status
return True
if (col + 2 < FIELD_WIDTH and \
self.board[col + 1][row].status
self.board[col + 2][row].status
return True
# вниз:
if (row + 1 < FIELD_HEIGHT and \
self.board[col][row + 1].status
return True
if (row + 2 < FIELD_HEIGHT and \
self.board[col][row + 1].status
self.board[col][row + 2].status
== empty):
FROG and \
empty):
EMPTY)
FROG and \
EMPTY):
return True
# правая лягушка ходит
# влево и вверх:
else:
# вверх:
if (row - 1 >= 0 and \
self.board[col][row
return True
if (row - 2 >= 0 and \
self.board[col][row
self.board[col][row
return True
1].status == EMPTY):
1] . status
2] . status
FROG and \
empty):
# влево:
if (col - 1 >= 0 and \
self.board[col - 1][row].status == EMPTY):
return True
if (col - 2 >= 0 and \
self.board[col
self.board[col
return True
1] [row].status
2] [row].status
FROG and \
EMPTY):
# ходов нет:
return False
Если игрок выбрал «мобильную» фишку, то мы объявляем её перетаскиваемой:
# нажата фишка,
# которую нельзя перетащить:
if (len(cand) == 0):
return
# звук:
sndPressed.trigger()
# перетаскиваемая фишка:
global dragFrog
dragFrog = cand[0]
dragFrog.dragged = True
# запоминаем разность координат
# центра фишки и курсора:
global offset
offset["dx"] = dragFrog.getXY()["x"] - mouseX
offset["dy"] = dragFrog.getXY()["y"] - mouseY
Теперь мы должны позаботиться о следующей подсказке и показать игроку
клетки, в которые он может перетащить взятую фишку:
# клетки, в которые можно перейти:
greens = game.getGreens(dragFrog.col, dragFrog.row, dragFrog.color)
for g in greens:
game.board[g["col"]][g["row"]].green = True
# ВОЗВРАЩАЕМ КООРДИНАТЫ ПУСТЫХ КЛЕТОК,
# В КОТОРЫЕ ФИШКА МОЖЕТ ПРЫГНУТЬ ИЗ
# КЛЕТКИ (col, row)
def getGreens(self, col, row, clr):
greens = []
# левая лягушка ходит
# вправо и вниз:
if (clr == GREEN):
# вправо:
if (col + 1 < FIELD_WIDTH and \
self.board[col + 1][row].status == EMPTY):
greens.append({ "col": col + 1, "row": row })
if (col + 2 < FIELD_WIDTH and \
self.board[col + 1][row].status == FROG and
self.board[col + 2][row].status == EMPTY):
greens.append({ "col": col + 2, "row": row })
# вниз:
if (row + 1 < FIELD_HEIGHT and \
self.board[col][row + 1].status == EMPTY):
greens.append({ "col": col, "row": row + 1 })
if (row + 2 < FIELD_HEIGHT and \
self.board[col][row + 1].status == FROG and \
self.board[col][row + 2].status == EMPTY):
greens.append({ "col": col, "row": row + 2 })
# правая лягушка ходит
# влево и вверх:
else:
# вверх:
if (now - 1 >= 0 and \
self.board[col][row - 1].status == EMPTY):
greens.append({ "col": col, "row": row - 1 })
if (row - 2 >= 0 and \
self.board[col][row - 1].status == FROG and \
self.board[col][row - 2].status == EMPTY):
greens.append({ "col": col, "row": row - 2 })
# влево:
if (col - 1 >= 0 and \
self.board[col - 1][row].status == EMPTY):
greens.append({ "col": col - 1, "row": row })
if (col - 2 >= 0 and \
self.board[col - 1][row].status == FROG and \
self.board[col - 2][row].status == EMPTY):
greens.append({ "col": col - 2, "row": row })
return greens
I В данной игре только одна свободная клетка, поэтому процесс можно и
упростить.
В пустой клетке появляется жёлтый квадратик, сигнализирующим игроку, что в
неё можно поставить фишку (Рис. 13).
Перемещаемую фишку в функции drawSzene рисует её метод метод drawXY:
# рисуем перетаскиваемую фишку:
if (dragFrog):
dragFrog.drawXY(mouseX + offset["dx"],
mouseY + offset["dy"])
Подхватив фишку-лягушку, игрок переносит её в клетку с жёлтым квадратиком
(Рис. 14).
Рис. 13
Рис. 14
Когда курсор окажется внутри этой клетки, он отпускает мышку, и программа
начинает выполнять функцию mouseReleased.
Если фишка не переносилась, то ничего и делать не нужно:
# ОТПУСКАЕМ КНОПКУ МЫШКИ
def mouseReleased():
# фишка не перетаскивалась:
global dragFrog
if not dnagFnog:
return
Если же мышка держала фишку, то нужно найти клетку, в которую она бросила
фишку:
# новая клетка:
newCol = -1
newRow = -1
# ищем клетку для фишки:
for row in range(FIELD_HEIGHT):
for col in range(FIELD_WIDTH):
if (game.board[colnrow].over(mouseX, mouseY)):
newCol = col
newRow = row
break
По своему опыту знаю, что не всегда и не каждый раз фишка попадает в нужную
клетку. В этом случае ход не засчитывается, фишка возвращается в исходную по¬
зицию, а незадачливый игрок получает звуковое предупреждение:
# фишка не на поле или клетка не зелёная:
if newCol < 0 or \
not game.board[newCol][newRow].green:
# все клетки - белые:
for r in game.board:
for c in r:
c.green = False
# фишка не перетаскивается:
dragFrog.dragged = False
dragFrog = None
122
J
# звук ошибочного хода:
sndError.trigger()
return
Но так бывает не всегда и не часто. Обычно фишка попадает точно в цель, и то¬
гда мы удаляем её из старой клетки и записываем в новую:
# все клетки - белые:
for r in game.board:
for c in r:
c.green = False
# старые координаты фишки:
oldCol = dragFrog.col
oldRow = dragFrog.row
# освобождаем старую клетку:
game.board[oldCol][oldRow].status = EMPTY
# занимаем новую клетку:
game.board[newCol][newRow].status = FROG
Ход считается выполненным, а его номер и координаты начальной и конечной
клеток мы вносим в протокол, который исправно печатается в Консольном окне:
# новые координаты фишки:
dragFrog.col = newCol
dragFrog.row = newRow
# добавляем ход:
game.nMove += 1
# печатаем номер хода в консольном окне:
print(u'Ход: ' + str(game.nMove))
# печатаем координаты прыжка в консольном окне:
print(str(oldCol) + " " + str(oldRow) + " --> " +
str(newCol) + " " + str(newRow))
# звук приземления фишки в клетке:
sndMove.trigger()
# фишка не перетаскивается:
dragFrog.dragged = False
dragFrog = None
Ход выполнен успешно и надёжно запротоколирован (Рис. 15).
Ходов: 1
С первого хода, конечно, не выиграешь, но всё равно после каждого хода мы
проверяем, не закончилась ли игра:
# проверяем, не закончилась ли игра:
testGameOver()
Игра может закончиться либо поражением, либо победой игрока.
Функция testGameOver проверяет, все ли фишки оказались на своих местах. Если
не все, то необходимо посмотреть, а есть ли у игрока ещё ходы, чтобы продол¬
жить игру. Если ходов не окажется, он проиграл:
# ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ
# ЛИ ИГРА
def testGameOver():
# обновляем сцену:
dnawSzene()
# не все фишки на месте:
if (game.getRestFnogs() > 0):
# если ходов нет, то
# игрок проиграл:
if (game.calcFnogs()["gneen"] == 0):
gameOver(False)
Если фишки ещё можно двигать, то игра продолжается:
else: netunn
Если же все фишки оказались в своих клетках, то игрок победил:
gameOver(True)
Когда игра закончится, игрок получит сообщение о поражении или победе:
# ИГРА ЗАКОНЧЕНА
def gameOver(win):
if (game.flgGameOver):
netunn
# рисуем табличку:
strokeWeight(2)
stroke(Black)
fill(255, 0, 0, 160)
nectMode(CORNER)
rect(30, -45 + 290, 500, 70)
strokeWeight(0)
# звук окончания игры:
sndWin.trigger()
# печатаем сообщение:
fill(255, 255, 0)
textAlign(CORNER)
textSize(48)
if (win):
р^п^и'ПОБЕДА!')
text(u"Bbl ЭТО СДЕЛАЛИ!", 60, 300)
else:
р^п^и'БОЛЬШЕ НЕТ ХОДОВ!')
text(u"y ВАС НЕТ ХОДОВ!", 50, 300)
# игра закончена:
game.flgGameOver = True
Классическая головоломка решается за 15 ходов. Как правило, с первого раза её
никому не удаётся решить. И мне тоже не удалось (Рис. 16).
Рис. 16
Подсчётом «ходячих» фишек занимается метод calcFrogs:
# СЧИТАЕМ ФИШКИ,
# КОТОРЫЕ МОГУТ ХОДИТЬ
def calcFrogs(self):
self.colorFrogs()
restGreen = 0
restRed = 0
for f in self.frogs:
if f.status == CAN_MOVED:
restGreen += 1
else: restRed += 1
return { "all": restGreen + restRed, "green": restGreen, \
"red": restRed }
Но если поднатужить мозги, то задачу всё-таки можно решить (Рис. 17).
Рис. 17
Всё по-честному! Как сказал одноглазый шахматист-любитель гроссмейстеру
Остапу Бендеру, «У меня все ходы записаны!»:
КЛАССИЧЕСКАЯ ДОСКА
Ход:
1
3 4
-->
4
4
Ход:
2
5 4
-->
3
4
Ход:
3
6 4
-->
5
4
Ход:
4
4 4
-->
6
4
Ход:
5
2 4
-->
4
4
Ход:
6
1 4
-->
2
4
Ход:
7
3 4
-->
1
4
Ход:
8
5 4
-->
3
4
Ход:
9
7 4
-->
5
4
Ход:
10
6 4
-->
7
4
Ход:
11
4 4
-->
6
4
Ход:
12
2 4
-->
4
4
Ход:
13
3 4
-->
2
4
Ход:
14
5 4
-->
3
4
Ход:
15
4 4
-->
5
4
ПОБЕДА!
Как решить задачу
Одномерные лягушачьи головоломки решаются единообразно и просто.
В книге Анания и Марии Левитиных Алгоритмические головоломки (Anany Le¬
vitin, Maria Levitin - Algorithmic Puzzles) задача 88. Toads and Frogs посвящена ре¬
шению этой головоломки (страницы 53, 77, 157) для произвольного числа n ля¬
гушек с каждой стороны. Для решения задачи необходимо выполнить n2 + 2n
ходов. Причём 2n ходов - это переползания на соседнее свободное место, а n2 -
прыжки через лягушек.
Для двух лягушек слева и справа мы получаем 22 + 2*2 = 8 ходов. Для трёх лягу¬
шек с обеих сторон - 32 + 2*3 = 15 ходов.
В общем случае, для m и n лягушек слева и справа необходимо выполнить
mn+m+n ходов: m+n переползаний и mn прыжков.
|В некоторых вариантах игры используют больше одной свободной клетки,
что приводит к огромному числу решений.
| Отдельные задачи могут вообще не иметь решений.
В папку ПРОГРАММЫ я скопировал программу Frogs, которая умеет решать за¬
дачи с разным числом лягушек с двух сторон (Рис. 18).
Она написана на Си-шарпе. Если вы хотите узнать, как решается эта и многие
другие головоломки на компьютере, то читайте книгу Программирование на
языке C#5: Комбинаторные игры и головоломки (Рис. 19).
В той же папке вы найдёте программу Frogs2 с улучшенным графическим интер¬
фейсом (Рис. 20).
Прыгающие лягушки
□
X
5.
L.RLRLR
6.
.LRLRLR
7.
RL.LRLR
в.
RLRL.LR
9.
RLRLRL.
10.
RLRLR.L
11.
RLR.RLL
12.
R.RLRLL
13.
RR.LRLL
14.
RRRL.LL
15.
RRR.LLL
вариант 2 /15
О. LLL.RRR
1.
LLLR.RR
2.
LL.RLRR
3.
L.LRLRR
4.
LRL.LRR
5.
LRLRL.R
6.
LRLRLR.
7.
LRLR.RL
в.
LR.RLRL
9.
.RLRLRL
10.
R.LRLRL
11.
RRL.LRL
12.
RRLRL.L
13.
RRLR.LL
14.
RR.RLLL
15.
RRR.LLL
найдены все решения 2
V
Записать!
Стереть!
Исходная позиция:
LLL.RRR
Конечная позиция:
Рис. 18
Рис. 19
Рис. 20
Решения двумерных задач ищите в писчебумажных и диджитальных источниках
информации.
Наглядные анимированные решения линейных головоломок можно найти на
Ютубе.
Теоретические изыскания по лягушкам читайте в первом томе книги Winning
Ways for your Mathematical Plays (Berlekamp, Conway, Guy, 2001 год, страницы
63-66, 75-76).
Задачи для самостоятельного решения
Первая задача с фишками
< Болл и Коксетер Математические эссе и развлечения, с. 132-133 >
Многим читателям наверняка знакома следующая задача. Десять фишек (или
монет) уложены в ряд. Любую из них можно перенести над двумя ближайши¬
ми к ней фишками и положить сверху на третью фишку от исходной. Требу¬
ется, следуя этому правилу, переложить фишки так, чтобы они образовали
пять пар, расположенных на равных расстояниях друг от друга.
Решение:
Пронумеруем фишки в начальном положении числами 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
Перенесём фишку 7 на 10, 5 на 2, 3 на 8, 1 на 4 и, наконец, 9 на 6. В результате
получим пары, расположенные на местах, первоначально занятых фишками 2,
4, 6, 8, 10.
Если же перенести фишку 4 на 1, 6 на 9, 8 на 3, 10 на 7 и, наконец, 2 на 5, то
пары образуются на местах 1, 3, 5, 7, 9.
Если при «перепрыгивании» фишек две лежащие друг на друге фишки считать
за одну, то можем найти ещё два решения, аналогичные предыдущим:
1. переносим фишку 7 на фишку 10, 5 на 2, 3 на 8, 1 на 6, 9 на 4;
2. кладём фишку 4 на 1, 6 на 9, 8 на 3, 10 на 5 и, наконец, 2 на 7.
В подобную игру можно играть и с восемью фишками, если не требовать,
чтобы полученные четыре пары лежали на равных расстояниях друг от друга.
Цель будет достигнута, если переложить фишку 5 на 2, 3 на 7, 4 на 1 и, нако¬
нец, 6 на 8. Такая форма задачи применима для любого большего 8 чётного
числа 8 +2n фишек. В самом деле, переложив фишку 4 на 1, мы получим по одну
сторону от этой пары ряд из 8 + 2п - 2 фишек, который затем аналогичным
способом можно свести к ряду из 8 + 2п - 4 фишек, - в конце концов при таком
способе действий у нас останется восемь фишек, которые мы сможем уло¬
жить так, как указано выше.
Более содержательным обобщением мог бы считаться случай п фишек - при
условии, что каждую фишку можно перенести над т (где т < п) соседними с
ней, последовательно расположенными фишками и опустить на следующую
за ними. Например, если уложить в ряд 12 фишек и разрешить перенос фишки
над тремя ближайшими фишками, то можно получить четыре стопки по
три фишки в каждой. Вот одно из решений этой задачи (фишки пронумерова¬
ны последовательно): фишку 7 кладём на фишку 3, 5 на 10, 9 на 7, 12 на 8, 4 на
5, 11 на 12, 2 на 6 и 1 на 2. Если уложить в ряд 16 фишек и разрешить перенос
каждой фишки над четырьмя примыкающими к ней, то можно получить че¬
тыре стопки по четыре фишки в каждой. Вот одно из решений (фишки прону¬
мерованы последовательно): кладём фишку 8 на фишку 3, 9 на 14, 1 на 5, 16 на
12, 7 на 8, 10 на 7, 6 на 9, 15 на 16, 13 на 1, 4 на 15, 2 на 13 и 11 на 6.
Напишите программу для решения таких задач.
Вторая задача с фишками
< Болл и Коксетер Математические эссе и развлечения, с. 134-135 >
Эта задача имеет японское происхождение. Положите четыре серебряные
монеты (или белые фишки) и четыре медные монеты (или чёрные фишки) в
ряд через одну вплотную друг к другу. Требуется за четыре хода (ход состоит
в том, что пара лежащих рядом монет переносится на свободное место без
изменения относительного расположения монет в паре) добиться того,
чтобы за четырьмя лежащими подряд медными монетами следовали четыре
серебряные, причем между монетами не должно быть пробелов.
Задачу можно решить следующим образом. Обозначим серебряную монету
буквой а, медную - буквой b, и пусть хх обозначает два соседних пустых ме¬
ста. Тогда последовательные положения монет можно изобразить так:
Старт
xxabababab
После
первого хода
baababaxxb
После
второго хода
baabxxaabb
После
третьего хода
bxxbaaaabb
После
четвертого хода
bbbbaaaaxx
При выборе хода нужно руководствоваться следующим правилом. Допустим,
что места монет (где пустые места также учитываются) «циклически упо¬
рядочены», т. е. условимся считать, что за последней буквой нашей записи
следует первая буква. Тогда на каждом ходе нужно переносить на свободное
место ту пару, которая лежит через одну монету от свободного места по
заранее выбранную сторону от него (т. е. переносить следует либо всё время
по часовой стрелке, либо все время против неё).
Сразу приходит на ум аналогичная задача с 2п фишками, n белыми и n чёрны¬
ми. При п > 4 эта задача решается за п ходов, однако я не нашёл простого
общего правила, которое годилось бы во всех случаях.
Деланой описал решение этой задачи, рассматривая отдельно четыре случая:
п имеет вид 4m; 4m + 2; 4m + 1 и 4m + 3. В первых двух случаях начальные п/2
ходов делают парами из разноцветных фишек, а последующие п/2 ходов - па¬
рами одноцветных фишек. В третьем и четвёртом случаях первый ход дела¬
ется по предыдущему правилу (т. е. предпоследняя и стоящая перед ней фиш¬
ки отправляются в начало ряда), в следующих (п -1)/2 ходах участвуют пары
из разноцветных фишек, а в последних (п -1)/2 ходах - пары одноцветных фи¬
шек.
Допускает решение и видоизменённая задача, которая получается, если усло¬
вие отсутствия пробелов между монетами в окончательном расположении
заменить требованием того, чтобы при каждом ходе пара фишек ставилась
либо в начало, либо в конец ряда.
Ещё один вариант этой задачи принадлежит Тэйту: он предложил ввести
условие, чтобы две монеты, делающие ход, менялись местами. Тогда для ре¬
шения задачи в случае 8 фишек требуется, по-видимому, пять ходов, а в об¬
щем случае 2п фишек требуется п + 1 ход.
См. дальше задачу Парные прыжки.
Напишите программу для решения таких задач.
Guarini's problem
В книге Джона Уоткинса (John J. Watkins - Across the Board: The Mathematics of
Chessboard Problems), на страницах 2-4 напечатана головоломка, известная как
Задача Гуарини. Впервые она была опубликована в 1512 году, то есть более 500
лет назад, но до сих пор в добром здравии шагает по многочисленным печат¬
ным изданиям.
На шахматной доске размером 3 х 3 клетки стоят 4 коня - два белых и 2 чёр¬
ных:
1
□
ш
2
Задача состоит в том, чтобы за минимальное число ходов поменять коней
местами.
Решение задачи существенно облегчается, если представить шахматную
доску в виде графа:
Напишите программу, решающую эту задачу.
Guarini’s problem 2
< John J. Watkins - Across the Board: The Mathematics of Chessboard Problems, с. 4¬
5, 20-21 >
В декабрьском выпуске журнала Scientific American за 1979 год задача Гуарини
получила интересное продолжение.
На шахматной доске размером 3 х 4 клетки на противоположных горизонта¬
лях стоят по 3 чёрных и белых коня:
Задача прежняя: поменять коней местами.
Напишите программу, решающую эту задачу.
И в этом случае шахматную доску выгодно представить в виде графа:
В журнале Наука и жизнь, №6 за 1968 год, страница 133 утверждается, что эту
задачу можно решить за 22 хода - чёрные и белые кони должны совершить по
11 прыжков:
Также должно быть выполнено условие: ни один чёрный конь не должен угро¬
жать белым коням, и наоборот.
А ещё ранее - в №2 за 1967 год, на странице 65 была напечатана и сама задача:
ПЕРЕГОНИТЕ КОНЕЙ
Ходом шахматного коня поменяйте местами белых и чёрных коней так, что¬
бы в процессе перестановки ни один чёрный конь не оказался под боем белого
и, наоборот, ни один белый конь не оказался под боем чёрного.
Какое минимальное количество перестановок потребуется вам для решения
задачи?
Ответ:
Минимальное количество перестановок,
которое потребуется для решения задачи.—
22. 11 —дли белых коней и 11 —для чер¬
ных.
Приводим один из вариантов решения:
lo¬
б.
1 —
Я
ll—
С.
2
я
12
7.
3 —
4
5 —
12.
8 —
3
0 —
1.
9 —
10
i
в.
4 —
9
12 —
7,
3 —
1
L
8.
10
5
(3 —
1.
9 —
10
7 —
2.
4 —
11
8 —
о
5 —
12
Добавление из №12 за 19876 год, страница 112:
В задаче о перемещении трёх белых и трёх чёр¬
ных коней на доске 3 х 4 решение в 22 хода отно¬
сится к тому случаю, когда белые и чёрные кони в
процессе перестановки не угрожают друг другу.
Если это условие выполнять не надо, то возмож¬
но решение в 16 ходов. Например:
12 - 7, 7 - 6, 2 - 7, 7 -12, 10 - 9, 9 - 2, 3 - 4, 4 - 9, 9 -10, 11- 4, 4 - 3, 6 - 7, 1- 6, 6 -11,
7- 6, 6 -1.
Если поставить условием в процессе перестановки чередовать ходы белых и
чёрных коней, то кратчайшее решение возможно осуществить за 18 ходов.
Перегоните коней
В журнале Наука и жизнь, №8 за 1976 год, страница 152 появилась более слож¬
ная задача:
Известна задача, в которой требуется на доске 3 х 4 поменять местами
трёх белых и трёх чёрных коней за возможно меньшее число ходов. Задача
решается минимум в 22 хода.
Усложним условия: поставим на эту же доску еще четырёх коней - двух белых
и двух чёрных. Сможете ли вы теперь справиться с поставленной задачей!
2*
4
7
,0f)
Простая (слева) и сложная (справа) задачи
Ответ из этого номера журнала:
Головоломка решается минимум в 44 хода:
3 - 4, 12 - 7, 5 -12, 10 - 5, 9 -10,
4 -9, 8 - 3, 3 -1, 1 - 8, 8 - 3,
6 -1, 1 - 8, 7 - 6, 6 -1, 12 - 7,
7 - 6, 5 -12, 12 - 7, 10 - 5,
5 -12, 9 -10, 10 - 5, 4 - 9,
9 -10, 3 - 4, 4 - 9, 8 - 3, 3 - 4,
I - 8, 8 -3, 6 -1, 1- 8, 7- 6,
6 -1, 12 - 7, 7 - 6, 5 -12, 10 - 5, 9 -10, 2 - 9, 6 - 7, 7 - 2,
II - 6, 4 -11.
В номере 12 за тот же год были подведены итоги решения этой задачи читате¬
лями.
Симметричное решение, когда чёрные зеркально повторяют ходы белых (42
хода):
12
- 7, 3
- 4, 5 -
12, 8
- 3, 10 - 5, 1-8, 9 -
10, 6 -
-1,
7 -
6,
4 -
9, 12
- 7, 3 -
4, 5 -
12, 8 - 3, 10 - 5, 1
- 8, 6 -
■ 1,
9 -
10,
7 -
- 6, 4 -
9, 12 -
- 7, 3 -
- 4, 5 -12, 8 - 3, 1-
8, 10 -
5,
6 -
1,
9 -
10, 7
- 6, 4 -
9, 12
- 7, 3 - 4, 8 - 3, 5 -
■ 12, 1-
■ 8,
10
- 5,
6 -
1, 9 -
10, 11
-6, 2 -
- 9, 7 -2, 4 -11.
Одно из лучших решений (38 ходов):
11- 4, 2 - 7, 6 -11, 9 - 2,
10 - 9, 5 -10, 12 - 5, 7 -12,
1 - 6, 6 - 7, 8 -1, 1-6, 3 - 8,
8 -1, 4 - 3, 3 - 8, 9 - 4, 4 - 3,
10 - 9, 9 - 4, 5 -10, 10 - 9, 12 - 5,
5 -10, 7 -12, 6 - 7, 1- 6,
12 - 5, 7 -12, 2 - 7, 9 - 2, 8 -1,
3 - 8, 4 - 3, 11-4, 4 - 9,
6 -11, 7 - 6.
А. Стальное (г. Москва) посчитал, что ему удалось решить задачу в 34 хода.
Однако он не заметил, что пригнал коней не в те клетки и тем самым не ре¬
шил предложенной задачи, но получилась новая задача:
Позиция 1 Позиция 2
перегнать коней из позиции 1 в позицию 2.
Сколько ходов потребуется вам?
В. Дымский (г. Казань) предлагает свой вариант задачи: требуется поменять
местами белых и чёрных коней, расставленных в Позиции 3:
Ему удалось проделать это всего за 16 ходов.
Из позиции 2 в позицию 4 кони перегоняются всего за 4 хода. Сколько ходов
потребуется вам для того, чтобы поменять местами коней, расположенных,
как показано на рисунке?
Knight permutation puzzle
В книге Шахматы и математика (Наука, 1983) Евгений Гик рассматривает ре¬
шение сложной перестановочной задачи с конями:
Самое короткое решение состоит из 24 ходов.
Напишите программу для решения этой задачи.
Эта задача упоминается и в статье Е.Гика Методом пуговиц и нитей в журнале
Наука и жизнь, №12 за 1976 год, страница 113.
Переход через Дунай
Далее в той же книге Евгений Гик обращается к перестановочной задаче Сэмюэ¬
ля Лойда с конями:
Необходимо переправить белых и чёрных коней через «Дунай» (вертикаль е),
то есть белых коней с левого берега на правый, а чёрных - с правого берега на
левый. При этом на одной вертикали не должно быть более одного коня.
Самое короткое решение включает 19 ходов.
Напишите программу для решения этой задачи.
Эта задача упоминается и в статье Е.Гика Методом пуговиц и нитей в журнале
Наука и жизнь, №12 за 1976 год, страница 113.
Решение задачи опубликовано в журнале Наука и жизнь, №4 за 1977 год, стра¬
ница 157.
Задача о перестановке слонов (Bishop permutation puzzle)
А ещё дальше в этой книге на поле нецензурной брани появляются боевые сло¬
ны, которых также следует поменять местами:
Слоны должны ходить по очереди и при этом не угрожать слонам противо¬
положного цвета.
Для решения задачи необходимо выполнить по 18 ходов слонами каждого
цвета.
Напишите программу для решения этой задачи.
Ответ на задачу:
].
19 —
14
2
— 7
2.
18-
15
3
— 6
3.
14-
8
7
— 13
4.
15-
12
6
-9
5.
20-
5
1
— 16
6,
5 —
2
16
— 19
И
1 .
8 —
11
13
— 10
8
12-
18
9
-3
9
11 —
1
10
-20
10.
17 —
11
4
— 10
1 1.
2 —
12
19
— 9
12.
11 —
16
10
-5
13.
12 —
7
9
-14
14.
18 —
13
3
— 8
15.
16 —
6
5-
-15
16.
7-
2
14
-19
17.
13 —
4
8
-17
18.
6 —
3
15-
-18
Задача о перестановке ферзей
А потом появились и ферзи:
Четырёх белых ферзей нужно перевести на ферзевый фланг, а чёрных - на ко¬
ролевский. При этом ни один ферзь не должен угрожать другим ферзям.
Для решения задачи необходимо выполнить по 13 ходов. При 8 ферзях задача
решения не имеет.
Напишите программу для решения этой задачи.
Задача с монетами
В журнале Наука и жизнь, №5 за 1968 год, на странице 27 была предложена та¬
кая задача:
На столе расположены в ряд 8 монет:
Их необходимо четырьмя ходами переместить в четыре колонки по две мо¬
неты в каждой. При этом перемещаемая монета должна «перепрыгивать»
через две другие монеты (лежащие в один ряд или одна на другой) и «призем¬
ляться» на третью.
Решите её.
Ответ показан на рисунке:
Парные прыжки
В журнале Наука и жизнь, №11 за 1968 год, на странице 83 в рамках Всесоюзной
заочной олимпиады была предложена такая задача:
10. (7-10). Четыре чёрные и четыре белые фишки лежат в ряд так, что цве¬
та чередуются; рядом оставлено место ещё для двух фишек. За один ход раз¬
решается любые две соседние фишки, не меняя их порядка, переставить на
свободные два места. Требуется расположить фишки так, чтобы четыре бе¬
лые стояли подряд и четыре чёрные - тоже, причём между фишками не
должно быть промежутка. За какое наименьшее число ходов это можно сде¬
лать? (На рисунке показано, как можно сделать это за пять ходов.)
Решите ту же задачу для n пар фишек, где n = 5, 6, 7,...
Решение (Наука и жизнь, №2 за 1969 год, страница 79).
При любом п наименьшее число ходов равно п. Решения для n = 4, 5, 6 и 7 (пар
фишек) показаны на рисунках:
Докажем следующее утверждение (для n > 4): если n пар фишек расположены
так, что крайняя слева - чёрная, цвета чередуются, и два места справа сво¬
бодны, то за n ходов можно добиться того, что два крайних места слева бу¬
дут свободны, затем подряд будут лежать n белых фишек и n чёрных фишек.
Мы знаем, что это утверждение верно для n = 4,5,6 и 7. Докажем, что если
оно верно для n пар фишек, то оно верно и для n + 4 пар фишек.
Среди n пар фишек возьмём две самые левые и две самые правые пары и будем
перекладывать их так же, как при n = 4, не обращая внимания на остальные
фишки:
Переход от л к г\+4
к
После первого хода
После второго хода.
После п+2 хода.
После п+д> хода.
После п+4 хода.
:ж1см]
□
о
Q
О
Q
О
,V
о
WI
Г'
у
Q
□
о
□
□
П
Q
О
0
О
□п
СП
0
0
м
•
III —
[I]
о
о
о
о
о
а
•
□
□□
Q
о
о
о
Q
•
ПППППЛП
Сделаем два хода. Затем за n ходов переложим остальные n пар фишек так,
как это требуется (что можно сделать по предположению). Осталось сде¬
лать ещё два хода - снова таких же, как при n = 4, чтобы получить требуе¬
мое расположение.
По индукции наше предположение доказано для всех п. (Из справедливости
этого утверждения для n = 4 следует его справедливость для n = 8, из спра¬
ведливости утверждения для n = 5 следует его справедливость для n = 9 и
так далее.)
Теперь докажем, что меньше чем за п ходов переставить фишки нельзя.
Нарисуем справа от начального расположения фишек ещё две фишки, кото¬
рые могут оказаться там после первого хода (один из двух вариантов а или б
на рисунке.
Ниже нарисуем одно из возможных конечных расположений (в, г, д, е на рисун¬
ке). Теперь, если в первоначальном расположении отметить крестиками те
фишки, на места которых должны попасть фишки другого цвета, то среди
этих крестиков можно выбрать п таких, никакие два из которых не стоят
рядом. Ясно, что все отмеченные крестиками фишки, должны быть пере
ставлены и что на это уйдёт не меньше n ходов.
Эта задача представляет собой прекрасный пример перехода от игры к ма¬
тематике. Случаи n = 4, 5, 6 и 7 - это пасьянс, очень похожий на те, которые
нередко печатаются в журнале «Наука и жизнь» (его удобно раскладывать с n
медными и n серебряными монетами). Стремление обобщить результат на
любое п, доказать, что за меньшее число ходов решить задачу нельзя, - это
уже подход, типичный для математики.
«Волки» и «овцы»
Оригинальная перестановочная задача из журнала Наука и жизнь, №11 за 1989
год, страница 27.
Необходимо поменять местами «волков» (фишки 4, 8, 11, 15, 18) и «овец»
(фишки 7,10,14,17, 21).
За один ход перемещать можно одну фишку на свободное поле вдоль линии, не
перепрыгивая, на любое число полей с одним условием: «волки» и «овцы» при
этом не должны находиться на одной линии.
(44 хода).
Решите задачу.
1) 8—12
2) 13— 5
4— 8
24—16
17— 1
16— 7
21—17
12—16
10—13
16—22
7—10
17—20
12— 3
20—24
8—24
5—21
15—12
14—20
11—15
20— 4
10— 2
2— 8
3) 15—23
4) 23—17
18— 6
8—11
6—14
12— 8
7—19
3—15
3— 6
24—12
6—10
17—21
1— 9
13—17
9—12
10— 7
21— 9
22—10
9— 3
15—18
19—13
12—15
С одной стороны на другую
Четыре перестановочные задачи с шашками были опубликованы в журнале
Наука и жизнь, №1 за 1979 год, на странице 80:
Задача 1. На шахматно-шашечной доске 8 х 8, поля которой обозначены чис¬
лами от 1 до 16 и от 1* до 16*, на полях 1-12 расставлены шашки. Задача со¬
стоит в том, чтобы переставить их на противоположную сторону доски на
поля 1*-12* за возможно меньшее количество ходов.
Известны решения в 20 ходов. Например:
1.
9-13
20. 13*-9*
2.
2-9-15*
19. 15-9*-2*
3. 5-14-12*
4. 1-5
5. 5-14
6. 10-16*-7*
7. 13-11*-4*
8. 3-10-14*
9. 6-15-11*
10. 8-15
18. 12-14*-5*
17. 5*-1*
16. 14*-5*
15. 7-16-10*
14. 4-11-13*
13. 14-10*-3*
12. 11-15*-6*
11. 15*-8*
Приведённое решение симметрично. В табличке ходов при нумерации клеток
доски, принятой нами, это очень хорошо видно. Есть и другие варианты ко¬
ротких симметричных решений.
Найдите кратчайшее несимметричное решение.
Сколько ходов вам потребуется для этого?
Задача 2. Сколько ходов потребуется вам для того, чтобы решить подобную
задачу на стоклеточной шашечной доске - перевести 15 шашек с одной сто¬
роны её на другую. Нумерацию клеток примем такую же, как и для доски 8 х 8 -
половина полей со звёздочками:
И, наконец, ещё две, не менее сложные, задачи на поиск минимума ходов.
Задача 3. На доске 8 х 8 расставлены 24 шашки: 12 белых на полях 1-12 и 12
чёрных на полях 1 *-12 *.
За сколько ходов вам удастся поменять местами белые и чёрные шашки?
Задача 4. Та же проблема, что и в предыдущей задаче, но для доски 10 х 10.
Ответов в журнале не оказалось...
Метод пуговиц и нитей
Многие перестановочные задачи решаются проще, если игровое поле предста¬
вить в виде графа, вершинами которого служат клетки, а рёбрами - допустимые
переходы между ними. Например, на рисунке мы видим задачу Гуарини в обыч¬
ном, матричном воплощении (Рис. 1, слева) и в виде графа (Рис. 1, справа):
Рис. 1
У. Сойер в книге Прелюдия к математике, на страницах 32-34 показывает,
как можно решить задачу об обходе доски 5 х 5 шахматным конём с помощью
наглядного, графического представления шахматной доски.
В журнале Наука и жизнь №12, 1976 год, на страницах 112-113 напечатана ста¬
тья Е.Гика Методом пуговиц и нитей, а ещё раньше рассказывается, как приме¬
нить этот метод для решения задачи Перегоните коней (см. выше):
Заменим все поля нашей доски 12 пуговицами, а все возможные ходы коня от¬
метим прямыми, соединяющими начальное и конечное поля, иначе говоря, со¬
единим пуговицы нитями и попытаемся упростить и упорядочить схему, не
нарушая связей.
Метод пуговиц и нитей
В соответствующих местах поместим наших коней и будем их перемещать с
пуговицы на пуговицу до тех пор, пока задача не будет решена, что сделать
теперь гораздо проще.
Скорее всего, связь перестановочных задач с графами была обнаружена после
того, как Дьюдени предложил свой метод решения таких задач. Мартин Гарднер
в книге Математические головоломки и развлечения, на страницах 156-157
рассказывает об этом изобретении Генри Дьюдени. С его помощью можно ре-
шить задачу Гуарини, в которой требуется поменять местами чёрных и белых
коней (Рис. 2, слева):
Рис. 2. Старинная задача Гуарини и её граф
Поскольку ни один конь не может попасть в среднюю клетку, то обозначим
остальные клетки кружочками (пуговицы) и пронумеруем их. Соседние, с точки
зрения шахматного коня, кружочки соединим прямыми линиями (нити). В итоге
мы получим граф, изображённый на рисунке в центре. Произведём самое слож¬
ное действие в этом методе - распутаем нити так, чтобы получился плоский
граф, в котором рёбра не пересекаются (Рис. 2, справа).
Теперь решение задачи видно как на ладони: нужно переставлять чёрных и
белых коней по часовой стрелке или против хода часовой стрелки. «Запутан¬
ная» задача распутана (Рис. 3)!
Рис. 3
В упомянутой выше статье Евгений Гик предлагает решить методом пуговиц и
нитей ещё несколько задач.
Причудливая доска
В следующей задаче снова требуется поменять местами белых и чёрных ко¬
ней. Доска здесь имеет довольно причудливую форму, но для метода пуговиц и
нитей это не является препятствием (Рис. 4).
Рис. 4
Постройте граф и определите минимальное число ходов для решения задачи.
Решение (журнал Наука и жизнь, №4 за 1977 год, страница 157) (Рис. 5).
Граф задачи для доски, имеющей причудливую форму, изображён на Рис. 18.43,
справа. Как видим, поле сЗ является как бы «транзитным» - связь между вет¬
вями а4 - d3 и Ь1 - ЬЗ возможна только через него.
Анализ показывает, что прежде всего нужно перевести через «транзит» на
сЗ всех трёх коней левой ветви (а4, Ь2, d3) на правую, и для экономии времени
расположить их на полях Ь1, аЗ, с2.
Теперь чёрному коню а2 надо перебраться на d3, а белым коням следует вер¬
нуться на левую ветвь (поля а4, Ь2). После этого второй чёрный конь с2 вре¬
менно располагается на а2 и пропускает белых коней на правую ветвь (по¬
ля Ь1, аЗ).
Наконец, этому коню надо с а2 перейти на Ь2, а белым занять поля а4, а2.
Хотя указанный план не очень сложен, но для его выполнения требуется 40
ходов.
Рис. 5. Задача на непрямоугольном поле и её граф
Перевозчику нужно переправить через реку волка, козу и мешок с капустой.
Лодка слишком мала, и поэтому перевозчик может взять с собой либо волка,
либо козу, либо мешок. Разумеется, капусту нельзя оставлять на берегу с ко¬
зой, а козу - с волком.
Как переправить всех на другой берег?
Давайте посмотрим, как с этой задачей справился Евгений Гик.
Обозначим действующих лиц через П, В, К и М. Запишем все возможные ситу¬
ации задачи на пуговицах, (сверху над чертой указано, кто находится на этом
берегу реки, а снизу - кто переехал на другой берег). Если некоторая ситуация
получается из другой за одну перевозку, то соответствующие пуговицы свя¬
жем нитью.
Распутанный клубок показан на рисунке 6, из которого следует решение зада¬
чи, причём две самые быстрые переправы состоят из семи перевозок.
Ханойская башня
На колышке А находятся три диска убывающего вверх диаметра. Требуется,
перекладывая их по одному, расположить в том же порядке на колышке В,
пользуясь вспомогательным колышком С, причём за один ход можно перено-
сить только один диск и на каждом колышке меньший диск должен лежать
выше большего (Рис. 7) (попробуйте обобщить эту задачу на тот случай, ко¬
гда на колышке А находится п дисков).
Рис. 7. Головоломка Ханойские башни
Ответа на задачу в журнале нет.
Ревнивые мужья
Три ревнивых мужа и их жёны должны переправиться через реку. Как это сде¬
лать, если лодка выдерживает только двоих, и никакой муж не допустит,
чтобы его жена осталась без него в компании, где присутствует хотя бы
один из остальных мужей.
Ещё одна задача на переправу.
Ответа на задачу в журнале нет.
Задача о девяти числах
В заключение предлагаем числовую задачу, которая легко решается с помо¬
щью метода пуговиц и нитей.
Можно ли выписать по кругу девять чисел 1, 2, ..., 9 в таком порядке, чтобы
никакая сумма двух соседних чисел не делилась ни на 3, ни на 5, ни на 7?
Решение.
Возьмём девять пуговиц-кружков и впишем девять наших чисел. Свяжем те
пуговицы, числа в которых, по условию задачи, могут стоять рядом. Рас¬
смотрим, например, пуговицу с числом 4. Имеем:
4 + 1 = 5
4 + 2 = 6
4 + 3 = 7
4 + 5 = 9
4 + 6 = 10
4 + 7 = 11
4 + 8 = 12
4 + 9 = 13
Таким образом, рядом с 4 может стоять 7 или 9, в остальных случаях сумма
делится на 3, на 5 или на 7. Точно так же, анализируя остальные восемь чисел,
получаем комбинацию пуговиц и нитей (Рис. 8, слева).
Рис. 8. Граф задачи
Выбросим нити, которые нас не устраивают. Мы видим, что 4 связано с 7 и 9,
поэтому 7 и 9 - соседи 4 по кругу. Аналогично 2 соседствует с 6 и 9.
Итак, 2 и 4 - соседи 9, а значит, 7 и 8 не являются соседями 9, и нити, связы¬
вающие 9 с 7 и 8, можно выбросить (на рисунке они зачёркнуты). Так как 1 свя¬
зан нитью с 3 и 7, то нить между 7 и 6 также можно выбросить.
Наконец, поскольку 8 связано с 3 и 5, а 5 ещё и с 6, то удаляется и нить меж¬
ду 3 и 5. Распутывая оставшийся клубок, получаем единственное решение за¬
дачи (Рис. 8, справа).
Игра Ярбро
Давайте сделаем небольшой перерыв в погрузочно-разгрузочных работах и по¬
играем с компьютером в лёгкую игру с числами.
Игру с клавиатурой (The Keyboard Game) придумал Л. Ярбро (L.D. Yarbrough), по¬
этому её называют также по имени автора игрой Ярбро. Она достаточно по¬
дробно описана в известной книге Мартина Гарднера Путешествие во времени
[ГМ90], на страницах 303-304 (в одном случае Ярбро ошибочно назван Ярдро).
Оригинальная статья была напечатана в январском номере журнала Creative
Computing. Журнал давно стал библиографической редкостью, но, к счастью, са¬
ма статья попала в сборник The Best of Creative Computing Volume 2, который
вышел в 1977 году. Эту книгу нетрудно найти в Интернете (Рис. 1).
Рис. 1
Как и в игре Баше, сначала один игрок выбирает случайное число, а затем игро¬
ки по очереди вычитают из него однозначные числа от 1 до 9. Они выбраны не
случайно: игра предназначалась для парной игры на микрокалькуляторе, где
однозначные числа вводить очень удобно (Рис. 2).
Принципиальное отличие игры Ярбро от игры Баше состоит в том, что выбор
всех ходов, начиная со второго, определяется предыдущим ходом: можно
нажимать только ту клавишу, которая соседствует по горизонтали, вертикали
или диагонали с последней нажатой. Так, после хода пятёркой разрешается
нажать любые клавиши, кроме самой пятёрки. После хода семёркой - только
клавиши 4, 5 и 8. Эта особенность игры Ярбро хорошо видна на рисунке выше.
О программировании игры Баше читайте в книге Простые компьютерные
игры на Питоне:
Рубанцев Валерий
Программирование для всех
Простые компьютерные игры на Питоне
веввв
□ОПвв
евейвов
еев
ееовве
оовв ]вввв
» V ■- « • - '» <
ввееееевв
Processing.
Число на клавише вычитается из текущего числа, которое можно видеть в окош¬
ке микрокалькулятора. Новое значение становится текущим, и следующий игрок
вычитает число на своей клавише уже из него. Игра заканчивается проигрышем
того игрока, который в результате своего хода получит отрицательное число (то
есть 0 не проигрывает!).
Довольно странно, но эта игра, в отличие от игры Баше, практически неизвестна
любителям интеллектуальных развлечений. Поэтому наша задача (или даже
миссия!) заключается в написании компьютерного варианта игры Ярбро, чтобы
посильно популяризовать её в широкой массе общественности.
Фоновый калькулятор
В калькуляторную игру вполне естественно играть на калькуляторе. Калькулято¬
ры бывают разные, но выглядят примерно одинаково - как на картинке выше.
Чтобы не метаться по безграничным просторам Интернета в поисках подходя¬
щей картинки, мы её и используем. Кроме собственно калькулятора нам потре¬
буется листочек бумаги для написания важных сообщений.
Совместим оба игровых атрибута и получим вот такую замечательную фоновую
картинку для нашей игры (Рис. 3).
Рис. 3
Поскольку калькулятор будет лежать не на простом столе, а на Рабочем, то я
тщательно замерил размеры листочка в пикселях. Именно таким должно быть и
окно:
# размеры окна в пикселях:
WIDTH = 800
HEIGHT = 600
В функции preload мы заодно загрузим и звуки, которые только мешают думать:
# ЗАГРУЖАЕМ МЕДИАФАЙЛЫ
def preload():
# загружаем картинки -->
global imgBack
# фоновая картинка:
imgBack = loadImage("data/imgBack.png")
# загружаем звуки -->
global sndChose, sndError, sndWin
minim = Minim(this)
sndChose = minim.loadSample("data/buljk.wav")
sndError = minim.loadSample("data/error.wav")
sndWin = minim.loadSample("data/win.wav")
Кнопки и всё остальное
Хорошая идея - использовать в игре картинку с калькулятором, но нарисован¬
ный калькулятор отличается от настоящего тем, что кнопки нажимать бесполез¬
но. Значит, нам нужно самостоятельно изготовить 9 кнопок и удачно разместить
их на картинке - так, чтобы они совпали с настоящими.
Все созидательные работы обычно проводятся в функции setup:
# ГОТОВИМСЯ К ПЕРВОЙ ИГРЕ
def setup():
# создаём окно:
size(WIDTH, HEIGHT)
# загружаем файлы:
preload()
rectMode(CORNER)
Окно создано, и можно приниматься за кнопки.
Их неровным счётом - 9, а ровным - 10, если мы добавим и кнопку для начина¬
ния новой игры. Мы объединим все цифровые кнопки в один список button, что
добавит нам новых хлопот, но зато код программы станет короче и яснее.
Одиночную кнопку создаём по старым рецептам:
# создаём кнопку Новая игра:
cp5 = ControlP5(this)
global btnNewGame
imgs = [loadlmage("button_a.png"),loadlmage("button_b.png"),\
loadlmage("button_c.png")]
btnNewGame = cp5.addButton("Новая игра ") \
.setPosition(560, 508) \
.setSize(l80,33) \
.setImages(imgs)
А цифровые - по новой технологии, в цикле:
# создаём цифровые кнопки 1..9:
global button
imgButton50x47 = loadImage("data/imgButton50x47.png")
button = []
for id in nange(1, 9+1):
btn = cp5.addButton(str(id)) \
.setSize(50, 47) \
.setValue(id) \
.setImage(imgButton50x47)
button.append(btn)
Все кнопки имеют прозрачный фон, чтобы через них просвечивала фоновая кар¬
тинка.
Нажатие на все цифровые кнопки обрабатывается в одной и той же функции
movePlayer, и при этом мы должны отличать одну кнопку от другой. Для этого
методом setValue записываем число, которое написано на кнопке (точнее - под
ней).
Создать кнопки - не проблема. Гораздо труднее аккуратно расставить их на кан¬
ве, чтобы нажимные кнопки совпали с нарисованными. Для этого нужны зоркий
глаз, верная рука и твёрдый арифметический расчёт с точность до одного пиксе¬
ля:
# устанавливаем цифровые кнопки:
x = у = 0
dx = 60
dy = 55
xb = 166
yb = 454
for id in range(9):
if (id + 1 == 4 or id + 1 == 7):
xb = 166
yb -= dy
button[id].setPosition(x + xb, y + yb)
xb += dx
И последние действие в функции setup направлены на создание игры и выклю¬
чение таймера:
global timer
timer = None
# создаём игру:
global game
game = Game()
Класс игры
Сначала и прежде всего объявим в файле constants.py глобальные константы,
которые видны отовсюду:
# This Python file uses the following encoding: utf-8
# диапазон выбора числа:
MIN_NUMBER = 33
MAX_NUMBER = 99
# игроки:
PLAYER = 1
COMPUTER = 2
Набор полей в классе игры очевиден. Мы должны запомнить задуманное число:
# This Python file uses the following encoding: utf-8
from constants import *
from random import randint
# КЛАСС ИГРЫ
class Game:
def init (self):
# задуманное число:
self.number = 0
А также последнюю нажатую кнопку, от которой зависит выбор следующего хо¬
да:
# последняя нажатая цифра:
self.lastNumber = 0
Так как в игре принимают участие 2 соперника, то мы должны сохранять теку¬
щий счёт по партиям, текущего игрока и победителя:
# счёт игры:
self.result = [0] * 3
self.result[PLAYER] = 0
self.result[COMPUTER] = 0
# игрок, делающий ход:
self.player = PLAYER
# победитель:
self.winner = None
И можно приступать к игре:
# задержка хода Компьютера:
self.pause = 2400
# состояние игры:
self.flgGameOver = True
# начинаем новую игру:
self.newGame()
В методе newGame мы задумываем случайное число из заданного диапазона
MIN_NUMBER.. MAX_NUMBER:
# НАЧИНАЕМ НОВУЮ ИГРУ
def newGame(self):
# загадываем число:
self.number = randint(MIN_NUMBER, MAX_NUMBER)
Считаем, что последней была нажата цифра 0:
# последняя нажатая цифра:
self.lastNumber = 0
Такая цифра в игре не участвует, поэтому на первом ходу игрок сможет нажать
любую кнопку:
# ход Игрока:
self.player = PLAYER
self.flgGameOver = False
Подготовка
И тут на экране нашими заботами появляется визуальное воплощение нашего
же кода:
# РИСУЕМ СЦЕНУ
def dnaw():
# игра закончилась:
if (game.flgGameOven):
return
# рисуем сцену:
drawSzene()
global timer
if timer:
timer.sleep()
В функции drawSzene впечатываем в канву фоновую картинку:
def drawSzene():
# очищаем канву:
background(imgBack)
А затем печатаем информацию об игре:
# пишем название игры:
textSize(40)
textAlign(LEFT)
#textStyle(BOLD)
strokeWeight(0)
fill(138, 43, 226, 100)
text^'^^ Ярбро", 530, 50)
# печатаем текущего игрока:
textAlign(LEFT)
textSize(30)
fill(0, 255, 0, 100)
stnp = и'Ход '
if (game.playen == PLAYER):
stnp += uWpoKa'
else:
stnp += и'Компьютера'
text(strp, 530, 185)
# счёт игры:
textSize(40)
textAlign(LEFT)
fill(255, 0, 255, 100)
stnn = и"Счёт " + stn(game.nesult[PLAYER]) + " : " + \
stn(game.nesult[COMPUTER])
text(strr, 530, 140);
# сообщение компьютера:
textAlign(LEFT)
textSize(27)
if (message):
fill(Penu)
text(message, 530, 288)
Загаданное число печатаем в окошечке калькулятора, где ему и надлежит быть:
# число на калькуляторе:
textAlign(RIGHT)
textSize(75)
fill(0, 0, 0)
text(stn(game.numben), 440, 174)
Правда, шрифт мы используем обычный, чем нарушаем гармонию в природе, но
с натуральным шрифтом труднее: нужно вырезать цифры из картинки и печатать
их вместо шрифтовых. Это хлопотно, но вы это сможете!
Для игроков мы делаем визуальные подсказки: закрашиваем красным цветом те
кнопки, нажимать которые запрещено правилами игры:
# показываем подсказки для игроков:
if (game.player):
# допустимые ходы:
mstr = MOVES[game.lastNumber]
rectMode(CORNER)
dx = 0
dy = 0
for i in range(9):
id = i + 1
if (not str(id) in mstr):
# недопустимый ход:
fill(255, 0, 0, 100)
#print button[i].getPosition()[0]
x = button[i].getPosition()[0] + 9 - dx
y = button[i].getPosition()[1] + 7 - dy
rect(x, y, 32, 32)
Чтобы не усугублять программу поисками допустимых кнопок и сделать её бо¬
лее универсальной, мы запишем все возможные комбинации кнопок в строко¬
вый список MOVES:
# возможные ходы:
MOVES = ["123456789",
#0
"245",
#1
"13456",
#2
"256",
#3
"12578",
#4
"12346789",
#5
"23589",
#6
"458",
#7
"45679",
#8
"568"
#9
]
Его устройство абсолютно прозрачно: в каждой строке мы перечислили кнопки,
которые на клавиатуре располагаются в непосредственной близости друг от дру¬
га.
Последняя нажатая кнопка хранится в поле game.lastNumber. Ей соответствует
строка mstr из массива MOVES с индексом game.lastNumber. В этой строке запи¬
саны все цифровые кнопки, которые можно нажать. В цикле for мы перебираем
все кнопки и проверяем, содержатся ли они в строке mstr. Если нет, значит, та¬
кую кнопку нажимать нельзя. Мы рисуем на ней красный полупрозрачный квад¬
ратик. Нажать такую кнопку всё-таки можно, но ход всё равно засчитан не будет.
Запускаем программу - всё на месте и даже выглядит вполне натурально (Рис.
4).
Рис. 4
Схватка умов
Можно и пора нажимать кнопки (а первый ход всегда за нами, что очень выгод¬
но и полезно).
Если нажата цифровая кнопка (в любое время можно начать и новую игру, если
вам грозит неминуемое поражение, но мы будем играть до конца и честно), то
программа переходит в функцию movePlayer. Параметр btn - это объект (в
нашем случае - цифровая кнопка), который вызвал событие. От метода getValue
нажатой кнопки мы узнаём, какое число выбрал игрок для хода:
# ХОД ИГРОКА
def movePlayen(btn):
if (game.playen != PLAYER):
netunn
# число игрока:
num = int(btn.getValue())
# звук нажатия на кнопку:
sndChose.trigger()
На первом ходе число игрока можно и не проверять, но мы не будем делать ис¬
ключение, чтобы не усложнять код.
Как вы помните, для первого хода последнее нажатое число равно 0, поэтому
число игрока мы будем искать в строке MOVES[0], в котором записаны все циф¬
ры. Выполняя следующие ходы, игрок может и ошибиться, поэтому такая про¬
верка необходима.
В случае ошибки ход не засчитывается, и дальнейшая работа функции
movePlayer на этом заканчивается:
# проверяем число игрока -->
# оно должно содержаться в строке
# из массива MOVES для индекса,
# равного последнему числу:
if (str(num) not in MOVES[game.lastNumber]):
# ошибка:
sndError.trigger()
netunn
Если ход выполнен верно, то число игрока num вычитается из текущего числа
number, после чего обновляется информация о ходе игрока:
# запоминаем последнее число:
game.lastNumben = num
# вычитаем число игрока:
game.numben -= num
# печатаем сообщение:
global message
message = u'Игрок взял: ' + str(num)
Ход передаётся компьютеру, но мы не забываем предварительно проверить, не
победил ли игрок:
# передаём ход Компьютеру:
game.playen = COMPUTER
# проверяем, не закончилась ли игра:
isGameOven()
global timen
timen = Timer(1000, moveComputen)
После первого хода картинка на экране будет выглядеть так (Рис. 5).
Рис. 5
Поскольку у нас всё по-честному, то мы передаём право хода компьютеру.
Компьютер случайно выбирает число из допустимых для его хода. Выбор у него
всегда есть, а ошибиться в выборе он не может:
# ХОД КОМПЬЮТЕРА
def moveComputen():
# ход Компьютера:
game.playen = COMPUTER
# допустимые ходы:
stnm = MOVES[game.lastNumben]
# число возможных кнопок:
lenm = len(stnm)
# число компьютера:
num = 0
# победный ход:
fon i in nange(lenm):
n = int(strm[i])
if (game.numben - n == 0):
num = n
break
# победного хода нет,
# выбираем первый, который
# не ведёт к поражению:
if (num == 0):
fon i in nange(lenm):
n = int(strm[i])
if (game.numben - n > 0):
num = n
break
# хороших ходов нет -->
if (num == 0):
# выбираем случайный ход:
id = randint(0, lenm - 1)
# число компьютера:
num = int(strm[id])
# вычитаем его:
game.numben -= num
# и запоминаем:
game.lastNumber = num
# сообщение о ходе:
global message
message = ^'Компьютер взял: " + str(num)
# ход переходит к Игроку:
game.player = PLAYER
isGameOver()
# задержка:
delay(game.pause)
# звук нажатия на кнопку:
sndChose.trigger()
| Подробности выбора хода компьютером читайте дальше.
Задержка в игре связана не с медлительностью мыслительных процессов у ком¬
пьютера, а с необходимостью дать игроку некоторое время, чтобы проследить
за действиями компьютера. Поэтому он успеет прочитать на экране, что компь¬
ютер нажал клавишу 5, и от числа 28 осталось 23 (Рис. 6).
Рис. 6
Кнопка 5, которая была «нажата» компьютером помечена красным цветом, ко¬
торый предупреждает игрока, что её лучше не трогать. Если бы компьютер вы¬
брал другое число, то красным цветом были бы дополнительно отмечены и дру¬
гие кнопки.
Развязка интеллектуальной драмы
Вполне вероятно и может быть, что игра на этом и закончится, поэтому мы
должны обратиться к функции isGameOver за «вердиктом»:
# ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ ЛИ ИГРА
def isGameOven():
if (game.flgGameOven):
netunn
Проверка в данном случае очень простая и необременительная для нас: проиг¬
рывает тот игрок player, который получил отрицательное число на табло:
if (game.number >= 0):
netunn
# победитель игры:
game.winner = game.playen
Дальнейшие ритуальные действия в функции isGameOver совершенно понятны и
очевидны:
# рисуем табличку:
strokeWeight(2)
stroke(Black)
fill(Red)
rectMode(CENTER)
rect(width / 2, -16 + 270, 600, 70)
strokeWeight(0)
rectMode(CORNER);
# печатаем сообщение:
fill(255, 255, 0)
textAlign(CENTER)
textSize(48)
sndWin.trigger()
s = u'ПОБЕДИЛ '
if (game.winner == PLAYER):
s += u'ИГРОК'
else:
s += u'КОМПЬЮТЕР'
text(s, width / 2, 270)
# игра закончена:
game.flgGameOver = True
Если нажимать кнопки, не думая о последствиях, то игра проходит живо, инте¬
ресно и с переменным успехом (Рис. 7 и 8).
Тут самое время вспомнить про десятую кнопку, которая начинает новую ариф¬
метическую баталию:
# НАЖИМАЯЕМ КНОПКУ "НОВАЯ ИГРА""
def newGame():
# стираем сообщение:
global message
message = ''
game.newGame()
О VaitaroughGame — X
Игра Ярбро.
Счёт 0:1
ПОБЕДИЛ КОМПЬЮТЕР
00-0
мс
MR
м-
М+
ми
Г
7
8
9
%
+/_
АС
4
5
6
X
•
•
Щ AI 1 2 3
о оо ооо •
Рис. 7
О YartoroughGame
X
Uniel uF-e
раЯ|
— «MtfUft
-3
ПОБЕДИЛ ИГРОК
00-0
МС MR
м-
м+
ми SS55!*5*SS5SS!
Г
7 8
9
%
АС)
4 5
6
X
■
1 2
3
0
00 000
•
+
L
■V
Рис. 8
Учим компьютер уму
Стоит игроку на секунду задуматься над выбором хода, и он тут же понимает,
что соперник играет чрезвычайно слабо, особенно в конце партии, когда ошиб¬
ки более заметны. Наделение программы искусственным интеллектом требует
недюжинного интеллекта и от самого программиста. Впрочем, в статье Ярбро
описана выигрышная стратегия, которая позволяет особенно не думать при вы¬
боре очередного хода, но тогда игра становится слишком детерминированной, а
потому совершенно неинтересной. Мы сделаем вид, что ничего не слышали о
стратегии Ярбро, и будем играть по собственному разумению.
Избежать откровенно глупых ходов очень просто. Перебираем в цикле for все
возможные числа n. Если число даёт разность 0, то игрок на своём ходе немину¬
емо проиграет:
# число компьютера:
num = 0
# победный ход:
for i in range(lenm):
n = int(strm[i])
if (game.number - n == 0):
num = n
break
Таким образом, компьютер точно не упустит свою победу.
Если выигрышного хода нет, то выбираем первый из возможных, который не ве¬
дёт к поражению:
# победного хода нет,
# выбираем первый, который
# не ведёт к поражению:
if (num == 0):
for i in range(lenm):
n = int(strm[i])
if (game.number - n > 0):
num = n
break
Если таких ходов не окажется вовсе, то выбираем произвольную кнопку:
# хороших ходов нет -->
if (num == 0):
# выбираем случайный ход:
id = randint(0, lenm - 1)
# число компьютера:
num = int(strm[id])
Класс таймера
Для задержки хода компьютера мы воспользовались классом Timer, который
разработали в игре Баше (см. книгу Простые компьютерные игры на Питоне).
Поскольку он может пригодиться вам и в следующих играх, я вынес его в от¬
дельный файл Timer.py:
# КЛАСС ТАЙМЕРА
class Timer:
def init (self, ms_sleep, func_sleep):
self.stop = False
self.sleep_stop = millis() + ms_sleep
self.func_sleep = func_sleep
# ЗАДЕРЖКА
def sleep(self):
if self.stop:
return
if millis() < self.sleep_stop:
return
else:
self.stop = True
self.func_sleep()
Eliminator Puzzle
Eliminator - прекрасная, но мало кому известная головоломка. Я натолкнулся на
неё на сайте Отто Янко (Рис. 1).
Г1 Г 1 1 Г 1
ггггггг
гггггггггггггг
IF
м ГГ
гг
гг
ГГ А
ГГГГ
гг
1 г Г г
л
гг 1
С. гг
гг
ГГ г г
гг
ГГГГ
ГГГГГ (
г г гг
гггггг
ГГГГ
Г ГГГГ Г
гГГГГГ
Рис. 1
К сожалению, кроме правил игры и фамилии автора, там больше ничего нет.
При известном старании и везении в Интернете можно найти одноимённую
флеш-игру (Рис. 2).
Именно её мы и возьмём за основу этого проекта.
Всего в игре 30 уровней, которые нужно проходить последовательно, начиная с
первого. На следующий уровень игрок переходит, только решив задачу текущего
уровня. Причём за ограниченное число ходов. Большинство задач решаются
очень просто, но некоторые так просто не даются.
На Ютубе нашёлся ролик, в котором показано, как пройти первые 14 уровней
(Рис. 3).
Рис. 3
Правила игры
Игровое поле представляет собой нечто вроде простого лабиринта, внутри ко¬
торого находятся разноцветные шарики. Они могут быть четырёх цветов: красно¬
го, зелёного, синего и чёрного (Рис. 4).
Рис. 4
Цель игрока: уничтожить все цветные шарики, собирая их в связные группы.
Чёрные шарики не уничтожаются, а только мешают другим шарикам объеди¬
няться.
Игрок выполняет ход, нажимая клавиши со стрелками или соответствующие
кнопки на экране. Шарики двигаются в указанном направлении до упора - в
стенку лабиринта или в другие шарики. Если в результате хода образуются груп¬
пы не менее чем из двух шариков одного цвета, то они уничтожаются.
В оригинальной версии игры после уничтожения шариков остальные могут ещё
продвинуться в том же направлении. Такие ходы встречаются в нескольких зада¬
чах. Они только усложняют игру и код, поэтому в нашей игре их не будет. Нужно
просто нажать ещё 1 раз ту же самую кнопку или клавишу, чтобы выполнить
«оригинальный» ход. Число ходов увеличится на единицу, но вы можете считать
2 хода в одном направлении за 1 ход.
Из правил игры следует, что в начальной позиции должно быть не меньше двух
шариков одного цвета (исключая чёрный), иначе их невозможно уничтожить. Ес¬
ли же в процессе игры останется одинокий шарик, то игра на этом закончится
поражением игрока.
Интерфейс программы
С шариками мы разобрались. Для стен я выбрал незамысловатый каменный
блок, а для проходов - серые плиты двух оттенков (Рис. 5).
Рис. 5
Понятно, что нам потребуется информация о номере текущего уровня и о числе
ходов. Мы дальновидно позаботимся о себе и установим пару кнопок для выбо¬
ра любого уровня из имеющихся. Число ходов мы считать будем и даже укажем
рекордный результат, но предоставим игроку полную свободу решать голово¬
ломку столько ходов, сколько он пожелает.
Неизменяемые элементы интерфейса можно сразу нарисовать, чтобы не обре¬
менять себя лишними заботами в программе. Как я уже отмечал выше, такую
картинку нужно рисовать в конце работы над программой, когда размеры и по¬
зиции всех элементов интерфейса уже определятся (Рис. 6).
Рис. 6
Задания и числа нам придётся составлять и писать отдельно для каждого уровня.
В итоге первый уровень игры в нашем исполнении должен выглядеть так (Рис.
7).
Рис. 7
За дело!
Размеры окна должны быть в точности равны размерам фоновой картинки:
# размеры окна в пикселях:
WIDTH = 772
HEIGHT = 528
I Все константы мы запишем в отдельный файл constants.py.
Максимальные размеры поля - 10 клеток по обоим измерениям, но нам удоб¬
нее накинуть ещё единицу, чтобы между полем и границами канвы оставалось
расстояние:
# макс. размеры поля:
FIELD_COLS = 11
FIELD ROWS = 11
Размеры клеток нужно выбирать такими, чтобы любое поле гармонично вписа¬
лось в канву:
# размеры клеток в пикселях:
CELL_WIDTH = 48
CELL HEIGHT = 48
Как всегда, все дисковые операции дружно выполняем в функции preload:
# Eliminator Puzzle
# добавляем библиотеки:
add_library('controlP5') # ЭУ
add_library('minim') # звук
from constants import *
from colors import *
from Game import Game
from Media import *
# ЗАГРУЖАЕМ МЕДИАФАЙЛЫ
def preload():
# загружаем картинки -->
global imgBack
# загружаем фоновую картинку:
imgBack = loadImage("data/imgBack.png")
Media.load(Minim(this))
Так как картинки и звуки нам понадобятся в разных файлах, то мы загрузим их в
методе load класса Media. Тогда доступ к ним существенно упростится:
# This Python file uses the following encoding: utf-8
# МЕДИАФАЙЛЫ
class Media():
@classmethod
def load(cls, m):
print "Media.load"
# загружаем картинку со стеновым блоком:
Media.imgWall1 = loadImage("data/mauer2.png")
# загружаем картинки с проходными клетками:
Media.imgCell1 = loadImage("data/cell1.png")
Media.imgCell2 = loadImage("data/cell2.png")
# загружаем картинки с фишками:
Media.imgGreen = loadImage("data/green.png")
Media.imgRed = loadImage("data/red.png")
Media.imgBlue = loadImage("data/blue.png")
Media.imgGray = loadImage("data/black.png")
# загружаем звуки:
minim = m
Media.sndError = minim.loadSample("data/error.wav")
Media.sndUpal = minim.loadSample("data/ball_upal.wav")
Media.sndWin = minim.loadSample("data/win.wav")
Media.sndElim = minim.loadSample("data/elimination.mp3")
Дальше приходит черёд долгой и кропотливой работы по созданию и расстанов¬
ке кнопок на канве:
# ГОТОВИМСЯ К ПЕРВОЙ ИГРЕ
def setup():
# создаём окно:
size(WIDTH, HEIGHT)
# загружаем файлы:
preload()
# координатные режимы:
imageMode(CORNER)
rectMode(CORNER)
# создаём кнопки -->
cp5 = ControlP5(this)
# загружаем прозрачные картинки:
imgButton39x38 = loadImage("data/btn39x38.png")
imgButton39x36 = loadImage("data/btn39x36.png")
imgButton34x34 = loadImage("data/btn34x34.png")
global btnPlay, btnLevelUp, btnLevelDown, \
btnUp, btnDown, btnLeft, btnRight
btnPlay = cp5.addButton("1") \
.setPosition(619, 377) \
.setSize(39,36) \
.setImage(imgButton39x36)
btnLevelUp = cp5.addButton("2") \
.setPosition(716, 139) \
.setSize(34,34) \
.setImage(imgButton34x34)
btnLevelDown = cp5.addButton("3") \
.setPosition(716, 171) \
.setSize(34,34) \
.setImage(imgButton34x34)
btnUp = cp5.addButton("4") \
.setPosition(619, 339) \
.setSize(39,38) \
.setImage(imgButton39x38)
btnDown = cp5.addButton("5") \
.setPosition(619, 414) \
.setSize(39,38) \
.setImage(imgButton39x38)
btnLeft = cp5.addButton("6") \
.setPosition(580, 376) \
.setSize(39,38) \
.setImage(imgButton39x38)
btnRight = cp5.addButton("7") \
.setPosition(659, 376) \
.setSize(39,38) \
.setImage(imgButton39x38)
# создаём игру:
global game
game = Game()
Игра как таковая
Набор игровых полей нетрудно предугадать.
Для игры потребуются:
• список клеток игрового поля
• список всех шариков
• размеры текущего поля
• максимально допустимое число ходов в игре (у нас превышение ходов не
наказывается)
• текущий уровень (номер головоломки)
• число выполненных ходов
• состояние (статус) игры
• отступ поля от границ окна для его центрирования:
# состояние игры:
STOP = 0 # остановка игры
PLAY = 1 # игра
ELIMINATION = 2 # уничтожение шариков
# This Python file uses the following encoding: utf-8
from constants import *
from Maps import *
from Ball import Ball
from Cell import Cell
# КЛАСС ИГРЫ
class Game:
def init (self):
# игровое поле:
self.field = None
# размеры поля в клетках:
self.cols = 0
self.rows = 0
# отступ поля от границ канвы:
self.offsetCols = 0
self.offsetRows = 0
# текущий уровень:
self.level = 1
# макс. число ходов в игре:
self.max_moves = 0
# список шариков:
self.balls = []
# номер хода:
self.nMove = 0
# статус игры:
self.status = STOP
# начинаем игру:
self.newGame()
Каждая новая партия начинается с метода newGame:
# НОВАЯ ИГРА
def newGame(self):
# создаём задание
# для текущего уровня level:
self.createPuzzle()
# обнуляем число ходов:
self.nMove = 0
# начинаем игру:
self.status = PLAY
Полевые работы
А вот создать на экране головоломку - задача более трудная!
Картинки в начале главы показали, как она выглядит на экране в готовом виде. В
исходном коде условие задачи нужно зашифровать.
Шарики можно обозначить первыми буквами названия цветов. Серый шарик (у
нас он будет чёрным, что больше подходит для его роли в игре) второй раз бук¬
вой G обозначить нельзя, а вот буквой Х - это пожалуйста:
# число уровней в игре:
MAX_LEVEL = 30
# This Python file uses the following encoding: utf-8
from constants import *
# R - красный шарик
# G - зелёный шарик
# В - синий шарик
# X - серый шарик
Всего в игре 30 уровней, которые нужно подробно описать, поэтому я выделил
для них отдельный файл Maps.py. Так они не будут мешать нам в более важном
коде.
Все уровни мы поместим в список maps. Чтобы не запутаться с нумерацией
уровней, используем и нулевую ячейку, хотя такого задания у нас нет:
# список уровней:
maps = [None] * (MAX_LEVEL + 1)
В первую строку списка каждого уровня запишем размеры поля (их можно вы¬
числить по остальным элементам массива, но эта строка нам всё равно нужна,
поэтому мы облегчим себе последующую работу), максимально допустимое
число ходов moves (из оригинальной игры) и минимально возможное число хо¬
дов (из личного опыта):
maps[1] = [{ "cols": 8, "nows": 7, "moves": 4, "minmoves": 2 },
Само игровое поле вместе с артефактами можно зашифровать строками. Плюсик
обозначает стену, а точка - проходную клетку. Вот и первое задание:
++++++++ ,
'+RGRGRG+',
' + + ',
+.+.+.++ ,
' + ++',
'+++++++ ']
После строк уровня я приписал решение - на тот случай, если у вас возникнут за¬
труднения при решении задачи:
#Ход 1: ВНИЗ
#Ход 2: ВЛЕВО или ВПРАВО
Все остальные 29 уровней шифруются точно так же, поэтому посмотрите в ис¬
ходном коде.
Теперь можно браться и за метод createPuzzle, который по описанной выше
шифровке создаёт текущий уровень:
# СОЗДАЁМ ЗАДАНИЕ
def createPuzzle(self):
puzzle = maps[self.level]
# список шариков:
self.balls = []
Переменная puzzle получает список с условиями текущего уровня level. В первой
(по счёту - нулевой) строке записана дополнительная информация об уровне:
# в первой строке задания
# записано число колонок и строк в
# игровом поле:
sRow = puzzle[0]
self.cols = sRow["cols"]
self.nows = sRow["nows"]
print self.cols, self.nows
# по этим данным вычисляем
# отступы поля от границ канвы:
self.offsetCols = (FIELD_COLS - self.cols) / 2.0 * CELL_WIDTH
self.offsetRows = (field_ROWS - self.nows) / 2.0 * CELL_HEIGHT
# минимально возможное
# или максимально допустимое
# число ходов:
if sRow["minmoves"]:
self.max_moves = sRow["minmoves"]
else:
self.max_moves = sRow["moves"]
А дальше друг за другом следуют строки с символами, которые обозначают со¬
держимое клеток:
# создаём двумерный массив для игрового поля:
self.field = self.createArray(self.cols, self.nows)
# СОЗДАЁМ ДВУМЕРНЫЙ СПИСОК
def createArray(self, cols, nows):
# игровое поле:
return [[None for now in range(rows)] for col in range(cols)]
# по всем строкам списка:
for now in nange(self.nows):
# текущая строка:
sRow = puzzle[row + 1]
196
Переменная sRow получает текущую строку, из которой в цикле for мы последо¬
вательно извлекаем все символы:
# по всем символам в строке:
for col in range(self.cols):
#print('col ' + str(col))
# создаём клетку поля:
self.field[col][row] = Cell(col, row, self.offsetCols,
self.offsetRows)
# символ для неё:
c = sRow[col]
Так как не все игровые поля прямоугольные, то часть клеток в игре участия не
принимает. Такие клетки мы помечаем значением NONE в списке клеток. На
экране они не появятся:
# клетки на поле нет:
if c == ' ':
self.field[col][row].status = NONE
Большинство клеток внутри лабиринта - проходимые, что мы отмечаем значе¬
нием EMPTY, в отличие от непроходимых клеток со стенами и значением WALL.
Проходимые клетки могут быть пустыми, и тогда в поле item мы записываем
NONE, и с цветными шариками. Так как шарики могут находиться только на пу¬
стых клетках, то в поле item можно записать BALL, чтобы показать, что в ней
находится шарик. Но для шариков мы подготовили и отдельный список balls, в
который записываем всю информацию о шариках:
# пустая проходная клетка:
elif c == '.':
self.field[col][row].status = EMPTY
self.field[col][row].item = NONE
# проходная клетка с красным шариком:
elif c == 'R':
self.balls.append(Ball(col, row, CELL_WIDTH // 2, RED,
self.offsetCols, self.offsetRows, self))
self.field[col][row].status = EMPTY
self.field[col][row].item = BALL
# проходная клетка с зелёным шариком:
elif c == 'G':
self.balls.append(Ball(col, row, CELL_WIDTH / 2, GREEN,
self.offsetCols, self.offsetRows, self))
self.field[col][row].status = EMPTY
self.field[col][row].item = BALL
# проходная клетка с синим шариком:
elif c == 'B':
self.balls.append(Ball(col, row, cell_width // 2, blue,
self.offsetCols, self.offsetRows, self))
self.field[col][row].status = EMPTY
self.field[col][row].item = BALL
# проходная клетка с серым шариком:
elif c == 'X':
self.balls.append(Ball(col, row, CELL_WIDTH // 2, GRAY,
self.offsetCols, self.offsetRows, self))
self.field[col][row].status = EMPTY
self.field[col][row].item = BALL
# стена:
elif c == '+':
self.field[col][row].status = WALL
Переходим на клеточный уровень
Всё игровое поле состоит из отдельных клеток, каждая из которых представляет
собой экземпляр класса Cell. Новая клетка получает в конструкторе свои коор¬
динаты на поле, а также смещение игрового поля от границ канвы:
# This Python file uses the following encoding: utf-8
from constants import *
from Media import *
# КЛАСС КЛЕТКИ
class Cell:
# КОНСТРУКТОР
def init (self, col, row, offX, offY):
# координаты клетки:
self.col = col
self.row = row
По этим данным легко вычислить координаты клетки на канве в пикселях. Так
как положение клетки не изменяется в течение всей игры, то эти вычисления
проводим непосредственно в конструкторе клетки:
# коорд. лев.верхнего угла:
self.x = self.col * CELL_WIDTH + offX
self.y = self.row * CELL_HEIGHT + offY
Клетка может отсутствовать на поле, тогда её состояние (статус) равно NONE,
она может быть проходимой (EMPTY) или непроходимой (WALL):
# состояние клетки:
self.status = NONE # EMPTY WALL
Проходимая клетка может быть пустой. Значение её поля item равно NONE:
# содержимое клетки:
self.item = NONE
Если в клетке находится шарик, то значение изменяется на BALL.
Геометрические размеры клетки задаются константами, поэтому мы вводим по¬
ля width и height исключительно для удобства пользования:
# размеры клетки:
self.width = CELL_WIDTH
self.height = CELL_HEIGHT
Опять же исключительно для удобства записываем картинки в поля клетки:
self.imgWall1 = Media.imgWall1
self.imgCell1 = Media.imgCell1
self.imgCell2 = Media.imgCell2
И последние 2 клеточных поля - цвет шарика в клетке и число шагов при подсчё¬
те шариков пригодятся нам при обнаружении групп одноцветных шариков на
поле:
# цвет шарика в клетке:
self.color = 0
# шаг при подсчёте групп:
self.n = 0
Классные шарики
Шарики - главные герои игры. Согласно правилам, они перемещаются по четы¬
рём направлениям до ближайшего препятствия, которым может быть стена или
другой шарик. При этом одновременно двигаются все шарики, у которых имеет¬
ся для этого свобода перемещений. Но проходимое ими расстояние может быть
различным. Тут всё зависит от конкретной ситуации. На картинках этого не вид¬
но, но шарики не перепрыгивают из одной клетки в другую, а плавно переме¬
щаются. В компьютерных играх такое поведение объектов называют анимацией.
В отличие от Солитера и Прыгающих лягушек, где объекты перемещал игрок,
здесь шарики двигаются самостоятельно в заданном направлении. После оста¬
новки шариков нужно проверить, не образовались ли на поле группы шариков,
которые самостоятельно уничтожаются. И в этом случае мы прибегнем к анима¬
ции. Анимация - это красиво и эффектно, но она существенно усложняет про¬
грамму.
Если вы ещё не забыли, как выглядят шарики, то параметры конструктора будут
для вас понятны и даже очевидны: каждый шарик получает прописку в клетке
поля с координатами (col, row), радиус и цвет:
# красный шарик:
RED = 10
# зелёный шарик:
GREEN = 11
# синий шарик:
BLUE = 12
# серый шарик:
GRAY = 13
# This Python file uses the following encoding: utf-8
from constants import *
from Media import *
# КЛАСС ШАРИКА
class Ball:
# КОНСТРУКТОР
def init (self, col, row, radius, clr, offsetCols, offsetRows,
game):
# текущая клетка:
self.col = col
self.row = row
# радиус шарика:
self.radius = radius
# цвет щарика:
self.color = clr
self.offsetCols = offsetCols
self.offsetRows = offsetRows
self.game = game
self.imgGreen = Media.imgGneen
self.imgRed = Media.imgRed
self.imgBlue = Media.imgBlue
self.imgGnay = Media.imgGray
self.sndElim = Media.sndElim
Этого вполне достаточно для спокойной жизни в своей клетке, но наш шарик
склонен к перемене мест, поэтому должен знать «адрес» новой клетки:
# конечная клетка:
self.tanget = { "col": self.col, "now": self.row }
Величину перемещения (иначе говоря, скорость):
# величина перемещения шарика:
self.dx = 0
self.dy = 0
Свои текущие и конечные координаты на поле в пикселях:
# текущие координаты шарика:
self.curXCYC = { "xc": 0, "yc": 0 }
# конечные координаты шарика:
self.targetXCYC = { "xc": 0, "yc": 0 }
Если шарик попадёт в группу риска, то мы пометим его флажком elimination для
последующего коллапсирующего уничтожения:
# флаг уничтожения:
self.elimination = False
Но схлопнется он не сразу, а плавно и красиво, как и подобает настоящему ге
рою любой компьютерной игры:
# текущий размер (диаметр) шарика:
self.size = self.radius * 2
На сцену!
На этом предварительное знакомство с объектами и субъектами наших предсто¬
ящих интеллектуальных баталий закончено, и они могут предстать перед нами
во всей своей спрайтовой красе.
За визуализацию мизансцены у нас отвечает всегдашняя функция draw:
# ИГРОВОЙ ЦИКЛ
def draw():
# игра закончилась:
if (game.status == STOP):
return
Непосредственно художественные работы выполняет подшефная функция
drawSzene:
# рисуем новый кадр:
drawSzene()
А вышестоящая функция draw на каждой итерации проверяет, не нужно ли уни¬
чтожить группу шариков, и не закончилась ли игра:
# уничтожаем группу шариков:
if (game.status == ELIMINATION):
game.status = PLAY
elimination()
# удаляем уничтоженные шарики
# из списка:
game.deleteBalls()
# проверяем, не закончилась ли игра:
isGameOver()
Функция drawSzene также делегирует свои полномочия нижестоящим инстанци¬
ям, так что работы у неё совсем немного. Первым делом она впечатывает фоно¬
вую картинку в канву:
# ОБНОВЛЯЕМ СЦЕНУ
def dnawSzene():
# очищаем канву:
imageMode(CORNER)
backgnound(imgBack)
Затем вызывает метод draw каждой клетки в списке field:
# рисуем клетки:
for now in game.field:
for cell in now:
cell.draw()
Поверх клеток рисует шарики. Опять же вызывая метод draw для каждого шари¬
ка:
# рисуем шарики:
for p in game.balls:
p.draw()
И наконец, печатает на экране свежую информацию о текущем состоянии игры:
# печатаем информацию:
drawInfo()
Имея под рукой красивые картинки, нарисовать игровое поле проще простого!
Если в клетке стена, то печатаем картинку с каменным блоком:
# РИСУЕМ КЛЕТКУ
def dnaw(self):
imageMode(CORNER)
# рисуем клетку -->
# стена:
if (self.status == WALL):
image(self.imgWall1, self.x, self.y)
Проходимые клетки узорно раскрашиваем в шахматном порядке:
# проходимая клетка:
elif (self.status == EMPTY):
if ((self.col + self.now) % 2):
image(self.imgCell1, self.x, self.y)
else:
image(self.imgCell2, self.x, self.y)
Делать это совсем не обязательно, но, если хочется, то можно.
Нарисовать шарик значительно труднее из-за его неспокойного нрава. Он ведь
может передвигаться по полю и уничтожаться. Рассмотрим пока простейший ва¬
риант метода draw, который рисует шарик, покоящийся в своей клетке.
Зная координаты родительской клетки, её размеры и прочие геометрические
параметры, вычисляем координаты картинки в пикселях:
# РИСУЕМ ШАРИК
def dnaw(self):
imageMode(CENTER)
# вычисляем координаты картинки:
xc = self.col * CELL_WIDTH + self.offsetCols + self.radius
yc = self.row * CELL_HEIGHT + self.offsetRows + self.nadius
Шарики различаются цветом, согласно которому мы и выбираем нужную кар¬
тинку:
# учитываем цвет шарика:
if (self.colon == GREEN):
image(self.imgGneen, xc, yc, self.size, self.size)
elif (self.colon == RED):
image(self.imgRed, xc, yc, self.size, self.size)
elif (self.colon == BLUE):
image(self.imgBlue, xc, yc, self.size, self.size)
else:
image(self.imgGray, xc, yc)
Осталось напечатать игровую статистику:
# ПЕЧАТАЕМ ИНФОРМАЦИЮ
def drawInfo():
# свойства шрифта:
#textStyle(BOLD)
textSize(32)
textAlign(LEFT)
strokeWeight(0)
Хоту мы и не ограничиваем игрока в числе ходов, но после превышения лимита,
выделяем число ходов красным цветом:
# цвет текста:
if (game.nMove <= game.max_moves):
fill(0, 255, 0)
else:
fill(Red)
# число ходов:
text(stn(game.nMove) + '/' + stn(game.max_moves), 630, 240)
Знать номер текущего уровня важнее, чем число ходов, поэтому он всегда будет
красным:
# уровень:
fill(Red)
text(game.level, 660, 179)
И только теперь вы можете запустить программу, чтобы увидеть на экране ту
самую картинку, что я показал вам в начале главы.
Первоход
В оригинальной игре ходы выполняются клавишами со стрелками. И мы так сде¬
лаем, но добавим ещё и кнопки - для тех, кто предпочитает мышку клавиатуре.
Начнём с клавиш.
Нажатие на клавишу обрабатывается в функции keyPressed. В переменной
keyCode хранится код последней нажатой клавиши. Если это одна из клавиш со
стрелками, то программа переходит в функцию doMove с соответствующим зна¬
чением параметра:
# коды клавиш:
RIGHT_ARROW = 39
LEFT_ARROW = 37
UP_ARROW = 38
DOWN_ARROW = 40
# НАЖИМАЕМ КЛАВИШУ
def keyPnessed():
print keyCode
# стрелка вправо:
if (keyCode == RIGHT_ARROW):
doMove(MOVE_RIGHT);
# стрелка влево:
elif (keyCode == LEFT_ARROW):
doMove(MOVE_LEFT)
# стрелка вверх:
elif (keyCode == UP_ARROW):
doMove(MOVE_UP)
# стрелка вниз:
elif (keyCode == DOWN_ARROW):
doMove(MOVE_DOWN)
# нажата другая клавиша:
else: return
Кнопки со стрелками посылают программу точно туда же:
# НАЖИМАЕМ КНОПКИ СО СТРЕЛКАМИ
def moveDown():
print("moveDown")
doMove(MOVE_DOWN)
def moveUp():
print("movenUp")
doMove(MOVE_UP)
def moveLeft():
print("moveLeft")
doMove(MOVE_LEFT)
def moveRight():
print("moveRight")
doMove(MOVE_RIGHT)
Функция doMove начинается с проверок. Нельзя выполнить ход, если игра за¬
кончилась:
# ВЫПОЛНЯЕМ ХОД
def doMove(dir):
#print "din" + str(dir)
if (game.status == STOP):
return
Анимация требует времени, поэтому, когда шарики двигаются или уничтожают¬
ся, также нельзя сделать следующий ход:
if (game.isMoved()):
return
if (game.isElimination()):
return
Мы легко узнаем, двигаются ли шарики, просмотрев список шариков. Если хотя
бы у одного из них значение поля dx или dy не нулевое, значит, шарики ещё
находятся в движении:
# ПРОВЕРЯЕМ, ДВИГАЮТСЯ ЛИ ШАРИКИ
def isMoved(self):
for b in self.balls:
if (b.dx != 0 or b.dy != 0):
return True
return False
Аналогично выполняется проверка на уничтожение:
# ПРОВЕРЯЕМ, УНИЧТОЖАЮТСЯ ЛИ ШАРИКИ
def isElimination(self):
for b in self.balls:
if (b.elimination):
return True
return False
Пока мы не знаем, можно ли передвинуть какие-нибудь шарики, поэтому счита
ем, что они уже находятся на своих местах:
# ни один шарик не двигается,
# все шарики на своих местах:
for b in game.balls:
b.target = { "col": b.col, "now": b.now }
Например, в первой головоломке шарики сначала могут двигаться только вниз.
Результат каждого хода мы будем записывать в консоль, чтобы по окончании иг¬
ры вы могли сохранить все свои рекордные ходы:
message = ''
Флаг flgMove показывает, нашёлся ли хотя бы 1 шарик, который можно переме¬
стить. Пока таких шариков мы не нашли:
flgMove = False
Чтобы не усложнять код, мы напишем для каждого направления перемещения
шариков отдельную функцию:
# пробуем выполнить ход
# в заданном направлении:
if dir == MOVE_RIGHT:
flgMove = testRight()
message = u'ВПРАВО'
elif dir == MOVE_LEFT:
flgMove = testLeft()
message = u'ВЛЕВО'
elif dir == MOVE_UP:
flgMove = testUp()
message = u'ВВЕРХ'
elif dir == MOVE_DOWN:
flgMove = testDown()
message = и'ВНИЗ'
Если одна из этих функций вернула True, значит, шарики могут передвигаться.
Ход выполнен, о чём мы и делаем запись в протоколе:
# ход выполнен:
if (flgMove):
game.nMove += 1
print(u'Ход ' + str(game.nMove) + ': ' + message)
Media.sndUpal.triggerQ'
В противном случае ход не засчитывается, шарики остаются на месте, а игрок
получает звуковое замечание:
else:
Media.sndError.trigger()
Чтобы получить его, нажмите на первом уровне клавишу вверх.
Все «тестовые» функции похожи друг на друга, поэтому рассмотрим только одну
- движение шариков вниз, потому что только этот ход возможен в исходной по¬
зиции на первом уровне.
Флажок res - показывает окончательный результат проверки: если удалось пе¬
редвинуть хотя бы 1 шарик, то его значение равно True. Флажок flgMove показы¬
вает результат перемещения шариков на 1 клетку вниз.
# ШАРИКИ ДВИГАЮТСЯ ВНИЗ
def testDown():
res = False
flgMove = True
В зависимости от позиции на поле шарик может вообще не передвинуться, пе¬
редвинуться на 1 клетку, на 2 клетки и так далее. Самый простой способ пере¬
мещения шариков - попытаться поочерёдно передвигать их на 1 клетку вниз.
Если хотя бы 1 шарик передвинулся, мы повторяем попытку.
Например, шарики стоят так (Рис. 8).
Рис. 8
Внизу находится пустая клетка.
Верхний и средний шарик не могут двигаться вниз, потому что там находится
другой шарик. И тогда мы передвинем только нижний шарик (Рис. 9).
Рис. 9
Но логика и физика подсказывают нам, что вниз должна переместиться вся груп¬
па шариков. Это значит, что теперь мы должны повторить попытку, и тогда упа¬
дёт зелёный шарик, а за ним и красный (Рис. 10).
а
а
а
а
а
а
Рис. 10
Ход выполнен верно, а мы обошлись без лишних сложностей и премудростей.
Итак, пока можно сдвинуть хотя бы 1 шарик, мы это делаем:
# передвигаем шарики,
# пока можно:
while (flgMove):
flgMove = False
# сдвигаем все шарики вниз на 1 клетку:
for b in game.balls:
# текущая клетка шарика:
col = b.target["col"]
row = b.target["row"]
# шарик может перейти на соседнюю клетку вниз,
# если она пустая и в ней нет другого шарика:
if (game.field[col][row + 1].status == EMPTY and \
game.field[col][row + 1].item == NONE):
# убираем шарик из текущей клетки:
game.field[col][row].item = NONE
# новая колонка клетки с шариком:
b.target["row"] += 1
# теперь шарик в этой клетке:
game.field[col][b.target["row"]].item = BALL
# величина перемещения шарика:
b.dx = 0
b.dy = 7
# текущие координаты шарика в пикселях:
b.curXCYC = b.getXCYC(b.col, b.row)
# конечные координаты шарика в пикселях:
b.targetXCYC = b.getXCYC(col, b.target["row"])
Метод шарика getXCYC возвращает его координаты в пикселях, если он находит¬
ся в клетке (col, row):
# ВОЗВРАЩАЕТ КООРДИНАТЫ ЦЕНТРА ШАРИКА
# В КЛЕТКЕ (col, now)
def getXCYC(self, col, row):
xc = col * CELL_WIDTH + self.offsetCols + self.radius
yc = row * CELL_HEIGHT + self.offsetRows + self.radius
return { "xc": xc, "yc": yc }
В итоге все перемещаемые шарики получают величину сдвига в пикселях на
каждой итерации:
b.dx = 0
b.dy = 7
В данном случае шарики падают со скоростью 7 пикселей за итерацию. А также
они получают начальные и конечные координаты на поле curXCYC и targetXCYC.
# шарик передвинулся:
flgMove = True
res = True
Функция testDown возвращает True, если хотя бы 1 шарик перешёл в другую
клетку:
# True, если хотя бы 1 шарик
# перешёл в другую клетку:
return res
Если вы сейчас нажмёте кнопку ВНИЗ, то шарики так и останутся на своих местах.
Действительно, они получили начальные и конечные координаты, а также ско¬
рость и направления перемещения, и теперь должны пройти свой путь. Так как
мы задали величину перемещения на каждой итерации, то удобнее всего дви¬
гать шарики в функции draw, которая вызывается каждую итерацию. Но шарики
обновляются на экране в функции drawSzene, которая вызывает метод draw
каждого шарика. Из этого следует, что перемещать шарик можно в его методе
draw.
Однако не все шарики двигаются одновременно. Мы отличим подвижные шари¬
ки от неподвижных по значению их полей dx и dy. Если одно из них не равно ну¬
лю, значит, шарик находится в движении:
# РИСУЕМ ШАРИК
def draw(self):
imageMode(CENTER)
# вычисляем координаты картинки:
xc = self.col * CELL_WIDTH + self.offsetCols + self.radius
yc = self.row * CELL_HEIGHT + self.offsetRows + self.radius
# шарик двигается:
if (self.dx != 0 or self.dy != 0):
Новые координаты шарика мы получим, прибавив к текущим величину смеще¬
ния:
self.curXCYC["xc"] += self.dx
self.curXCYC["yc"] += self.dy
newx = self.curXCYC["xc"]
newy = self.curXCYC["yc"]
Так как шарики двигаются плавно, но дискретно, то мы не можем быть уверены,
что они окажутся точно в центре конечной клетки. Поэтому, когда расстояние
между центром шарика и центром клетки станет меньше величины перемеще¬
ния, мы считаем, что шарик достиг конечной точки:
# шарик на месте:
if (self.dy != 0 and abs(self.targetXCYC["yc"] - newy) < 7):
Тогда он получает координаты центра клетки:
self.row = self.target["row"]
yc = self.getXCYC(self.col, self.row)["yc"]
Величина перемещения обнуляется, показывая программе, что шарик больше не
перемещается:
self.dy = 0
Для большего реализма при падении шарика мы воспроизводим звуковой эф¬
фект:
Media.sndUpal.trigger()
Если шарики двигались, а затем все остановились, но нужно проверить, не обра¬
зовались ли группы шариков для уничтожения. Мы сообщаем об этом програм¬
ме, присваивая полю status значение ELIMINATION:
if (not self.game.isMoved()):
self.game.status = ELIMINATION
Мы рассмотрели движение шариков по вертикали. То же самое - по горизонта¬
ли:
elif (self.dx != 0 and abs(self.targetXCYC["xc"] - newx) < 7):
self.col = self.target["col"]
xc = self.getXCYC(self.col, self.row)["xc"]
self.dx = 0
Media.sndUpal.trigger()
if (not self.game.isMoved()):
self.game.status = ELIMINATION
Если шарик продолжает движение, то метод draw нарисует его в новом положе¬
нии:
# новые координаты картинки:
else:
xc = newx
yc = newy
Теперь можно посмотреть и мультики из жизни шариков!
Нажимаем кнопку ВНИЗ. Все шарики одновременно и с одинаковой скоростью
начинают падать (Рис. 11).
Рис. 11
В следующей клетке зелёные шарики останавливаются, поскольку под ними ока¬
зывается стена, а красные шарики продолжают движение (Рис. 12).
Рис. 12
Пока не упадут на самое дно самого глубокого лабиринта (Рис. 13).
Рис. 13
Первый ход удачно выполнен!
Элиминация
После первого хода шарики разобрались по цветам, но так и остались в одино¬
честве. Зато сейчас хорошо видно, что ход вправо или влево позволяет сгрудить
все шарики в 2 плотные группы (Рис. 14).
Рис. 14
Как вы помните, в методе draw каждого шарика мы устанавливаем поле status в
ELIMINATION, если шарики двигались, а потом остановились:
if (not self.game.isMoved()):
self.game.status = ELIMINATION
Это значит, что требуется проверка на уничтожение.
Функция draw в главном файле получает сигнал и вызывает функцию elimination:
# уничтожаем группу шариков:
if (game.status == ELIMINATION):
game.status = PLAY
elimination()
В функции elimination мы проверяем все шарики в списке balls:
# УНИЧТОЖАЕМ ГРУППЫ ШАРИКОВ
def elimination():
# пока ни один шарик не уничтожается:
for b in game.balls:
b.elimination = False
# проверяем все шарики:
for b in game.balls:
Серые шарики (у нас они чёрные) мы пропускаем, так как они не уничтожаются:
# серые шарики не уничтожаются:
if (b.color == GRAY):
continue
Для остальных шариков вызываем функцию calcFlood для подсчёта шариков в
группе:
# клетка с шариком:
col = b.col
row = b.row
# цвет шарика:
clr = b.color
#print('elimination: ' + str(col) + ' ' + str(row))
# считаем шарики в группе:
n = calcFlood(col, row, clr)
Если проверяемый шарик входит в группу, то общее число шариков больше еди¬
ницы, и тогда мы помечаем все шарики в группе для последующего уничтоже¬
ния:
# группа:
if (n > 1):
# помечаем шарики в группе:
calcFlood(col, now, clr, True)
Если при вызове функции calcFlood параметр flg равен False, то она только под¬
считывает шарики в группе. Если параметр flg равен True, то все шарики в группе
отмечаются:
# ПОДСЧИТЫВАЕМ ЧИСЛО ШАРИКОВ В ГРУППЕ
def calcFlood(col, row, clr, flg = False):
# обнуляем поля n и color во всём списке field:
for r in game.field:
for cell in r:
cell.n = 0
cell.color = 0
# отмечаем цветом клетки с шариками:
for b in game.balls:
game.field[b.col][b.row].color = b.color
# ставим 1 в клетку с шариком::
print col, row
game.field[col][row].n = 1
# номер очередного шага:
nStep = 1
# число клеток в области:
nCells = 1
# число закрашенных на очередном шаге клеток:
nStepFlood = 0
# проверяем поле:
while True:
nStepFlood = 0
# ищем соседей для клеток с очередным числом:
for row in range(game.rows):
for col in range(game.cols):
# нашли:
if (game.field[col][row].n == nStep):
# справа:
nStepFlood += flood(col + 1, row, clr, nStep, flg)
# ниже:
nStepFlood += flood(col, row + 1, clr, nStep, flg)
# слева:
nStepFlood += flood(col - 1, row, clr, nStep, flg)
# выше:
nStepFlood += flood(col, row - 1, clr, nStep, flg)
# добавляем новые клетки к общему результату:
nCells += nStepFlood
# следующий шаг:
nStep += 1
# пока удаётся найти хотя бы одну новую клетку:
if nStepFlood == 0:
break
return nCells
Функция calcFlood для каждой клетки поля 4 раза вызывает функцию flood для
поиска (или «регистрации») своих ортогональных соседок:
# СЧИТАЕМ ИЛИ ОТМЕЧАЕМ КЛЕТКУ
def flood(col, row, clr, n, flg):
# проверяем координаты клетки:
if (col < 0 or col >= game.cols or row < 0 or row >= game.rows):
return 0
# проверяем цвет клетки и число в ней:
if (game.field[col][row].color != clr or game.field[col][row].n != 0):
return 0
# всё нормально - помечаем клетку:
if (flg):
for b in game.balls:
if (col == b.col and row == b.row):
b.elimination = True
break
# и ставим очередной номер:
game.field[col][row].n = n + 1
return 1
Подробно этот волновой процесс описан в книге Простые компьютерные игры
на Питоне.
Поскольку шарики уничтожаются не разом, а по чуть-чуть, то с этой анимацией
мы поступаем точно так же, как и с перемещениями шариков. В методе draw
каждого шарика, который помечен к уничтожению, уменьшаем его размер и не¬
прозрачность. В начале уничтожения шариков включаем звуковой файл:
# шарики уничтожаются:
noTint()
if (self.elimination):
tint(255, 255. * self.size / 2 / self.radius)
#nb = self.game.calcBalls()
if (self.size == self.radius):
self.sndElim.stop()
self.sndElim.trigger()
if (self.size > 1):
self.size -= 1
# учитываем цвет шарика:
if (self.color == GREEN):
image(self.imgGreen, xc, yc, self.size, self.size)
elif (self.color == RED):
image(self.imgRed, xc, yc, self.size, self.size)
elif (self.color == BLUE):
image(self.imgBlue, xc, yc, self.size, self.size)
else:
image(self.imgGray, xc, yc)
noTint()
С каждой итерацией шарик становится всё более прозрачным и мелким (Рис. 15).
Рис. 15
В итоге шарик превратится в пиксель.
В функции draw главного файла на каждой итерации мы вызываем метод
deleteBalls:
# удаляем уничтоженные шарики
# из списка:
game.deleteBalls()
Если он находит в списке balls шарик с однопиксельным размером, то удаляет
его из списка, а полю item клетки этого шарика присваивает значение NONE - в
ней шарика нет:
# УДАЛЯЕМ ЩАРИКИ
def deleteBalls(self):
# удаляем уничтоженные шарики
# из списка:
for b in self.balls:
if (b.size <= 1):
self.field[b.col][b.row].item = NONE
self.balls.nemove(b)
Со щитом или на щите?
В конце функции draw мы проверяем, не закончилась ли игра, вызывая функцию
isGameOver:
# проверяем, не закончилась ли игра:
isGameOven()
Игра официально заканчивается, если:
• на поле не осталось шариков - уровень пройден
• на поле остался 1 цветной шарик без пары - уровень не пройден:
# ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ ЛИ ИГРА
def isGameOven():
if (game.status == STOP):
netunn
if (game.isMoved()):
netunn
if (game.isElimination()):
netunn
# считаем оставшиеся шарики:
nb = game.calcBalls()
#pnint(nb)
# если не осталось шариков,
# значит игрок выиграл:
if (nb["all"] == 0):
# переходим на следующий уровень:
game.level += 1
if (game.level > MAX LEVEL)
)
225
A
game.level = 1
win(u'yPOBEHb ПРОЙДЕН!')
print(u'yPOBEHb ПРОЙДЕН!')
# если остался только 1 цветной шарик,
# то игрок проиграл:
if (nb["green"] == 1 on nb["red"] == 1 or nb["blue"] == 1):
win(u'УРОВЕНЬ НЕ ПРОЙДЕН!');
pnint(u'УРОВЕНЬ НЕ ПРОЙДЕН!')
Для подсчёта шариков на поле вызываем простой метод calcBalls:
# СЧИТАЕМ ОСТАВШИЕСЯ ШАРИКИ
def calcBalls(self):
restGreen = 0
restRed = 0
restBlue = 0
for b in self.balls:
if (b.color == GREEN):
restGreen += 1
elif (b.color == RED):
restRed += 1
elif (b.color == BLUE):
restBlue += 1
return { "all": restGreen + restRed + restBlue, \
"green": restGreen, "red": restRed, "blue": restBlue }
При любом исходе игры вызывается функция win, которая печатает радостное
или грустное сообщение (Рис. 16 и 17):
rectMode(CORNER)
# печатаем сообщение:
fill(255, 255, 0)
textAlign(CENTER)
textSize(48)
text(message, width / 2, 270)
# игра закончена:
game.status = STOP
Media.sndWin.trigger()
Eliminator
УРОВЕНЬ HE ПРОЙДЕН!
Рис. 16
Рис. 17
Повышаем уровень игры
Если текущий уровень пройден, игрок переходит на следующий. Когда все уров¬
ни закончатся, он вернётся на первый уровень.
Если уровень не пройден, то игроку предоставляется следующая попытка.
Для старта новой игры нужно нажать кнопку play
имённую функцию:
чтобы попасть в одно
# НОВАЯ ИГРА
def play():
print("play")
pnint(u’yPOBEHb: ' + stn(game.level))
game.newGame()
Игрок получит задание: либо следующего уровня, либо текущего - в зависимо¬
сти от результата игры.
Впрочем, игрок волен самостоятельно выбирать нужный уровень, нажимая
кнопки levelUp и levelDown (Рис. 18).
Рис. 18
Функции-обработчики для них очень простые и комментированию не подлежат:
# СЛЕДУЮЩИЙ УРОВЕНЬ
def levelUp():
pnint("levelUp")
game.level += 1
if (game.level > MAX_LEVEL):
game.level = 1
pnint(u'yPOBEHb: ' + stn(game.level))
game.newGame()
# ПРЕДЫДУЩИЙ УРОВЕНЬ
def levelDown():
pnint("levelDown")
game.level -= 1
if (game.level < 1):
game.level = MAX_LEVEL
pnint(u'УРОВЕНЬ: ' + stn(game.level))
game.newGame()
У нас все ходы записаны!
Если вы паче чаяния, то есть вдруг и неожиданно для себя решили головоломку,
то посмотрите консоль. В ней все ходы записаны (Рис. 19).
УРОВЕНЬ 1
Ход 1: ВНИЗ
Ход 2: ВЛЕВО
УРОВЕНЬ ПРОЙДЕН!
Рис. 19
Сохраните их на долгую память, чтобы не ломать голову второй раз над одним и
тем же.
Конклюзион
Анимация и прочие эффекты делают игру более привлекательной, но требуют
немалых дополнительных усилий.
Всегда начинайте разработку игры с самой простой версии. Возможно, даже с
простейшей графикой, без спрайтов и звуков. Тем более без анимации. И только
когда механизм игры будет готов, постепенно добавляйте к нему «красивости»,
контролируя каждое изменение в программе. Если заведомо известно, что про¬
грамма работала правильно, найти новые ошибки гораздо проще.
Наши программы чисто учебные, поэтому не блещут графикой и дополнитель¬
ными «опциями»: таблицей рекордов, меню, музыкой, файлами помо-
щи/справки, информацией об авторе, визуальными эффектами - и так до беско¬
нечности. Сама идея игры лучше от этого не станет, зато вы потратите десятки и
сотни часов своего свободного времени. Сначала напишите работоспособную
версию программы, предложите проверить её всем, кто не сможет отказаться.
Убедитесь, что она вызывает неподдельный интерес у окружающих, и только по¬
том подумайте о дальнейшем плодотворном развитии игры.
Eliminator Solver
Если вы пробовали решать «элиминаторные» головоломки, то убедились, что с
некоторыми из них справиться сложно. В этом случае следует поручить решение
самому компьютеру.
Все головоломки решаются не более чем за 8 ходов, так что общее число про¬
смотренных позиций невелико. Однако игра - это одно, а решатель - совсем
другое. Ему не нужна анимация, звуки, надписи и прочие красивости, поэтому в
первую очередь мы должны упростить программу так, чтобы в ней осталась
только суть игры.
В любой позиции у игрока имеется 4 хода: вправо, вниз, влево и вверх. Результа¬
ты этих ходов могут быть различны.
Рассмотрим исходную позицию первого уровня (Рис. 1).
Рис. 1
Ход вправо выполнить нельзя. Ход вниз можно. Ходы влево и вверх тоже беспо
лезны. Остался единственный ход - вниз (Рис. 2).
Рис. 2
И в этой позиции можно сделать 4 хода. Ход вниз ничего не меняет на поле. Хо¬
ды влево и вправо ведут к победе. Ход вверх возвращает игру в исходное состо¬
яние.
Подготовительные операции
Оставим в программе единственный звуковой файл sndError, сигнализирующий
об ошибке. Без него бесполезные ходы игрока могут быть восприняты как отсут¬
ствие реакции программы на его действия.
Зато добавим ещё одну кнопку, которая и решит за нас все задачи. Она обяза¬
тельно должна быть красной, как и вообще все серьёзные кнопки. Мы не
предусмотрели заранее место для этой кнопки, поэтому придётся притулить её в
самом низу канвы:
font = createFont("arial bold", 16)
btnSolve = cp5.addButton("SOLVE!") \
.setPosition(598, 460) \
.setSize(80, 39) \
.setColorBackground(color(255,0,0)) \
.setFont(font)
Вот она, эта кнопка (Рис. 3).
Рис. 3
К сожалению, даже многократное дружеское нажатие на «решительную» кнопку
делу не поможет. А поможет нам функция solve. Но сначала нам нужно решить
вопрос с игровыми позициями.
Позиционный класс
Результатом хода может быть проигрышная позиция, выигрышная позиция и не¬
ясная позиция. Обозначим их константами:
UNKNOWN = 1
LOSE = 0
WIN = 2
Уже сейчас понятно, что для каждого хода мы должны запомнить не только соб¬
ственно позицию, но и некоторые её свойства. С полем res мы уже разобрались.
Поле level хранит уровень, на котором возникла данная позиция. Не путайте с
уровнем игры, который обозначает всего лишь порядковый номер задачи. Также
нам понадобятся: ход, приведший к конкретной позиции, и предыдущая пози¬
ция:
# This Python file uses the following encoding: utf-8
from constants import *
import copy
class Position:
def init (self, position, level = 0, res = UNKNOWN):
self.res = res
self.level = level
self.pos = Position.getCopyPosition(position)
self.predpos = None
self.move = None
Последние поля нужны для того, чтобы мы могли потом восстановить все ходы,
которые привели к решению задачи.
Единственный метод класса Position возвращает копию позиции position:
@classmethod
def getCopyPosition(cls, position):
return copy.deepcopy(position)
Велик соблазн записывать все позиции в двумерный массив. Но мы вполне
обойдёмся и обычным списком:
# список позиций:
self.pos = None
В списке все позиции следуют друг за другом, поэтому позиции какого-либо
уровня нельзя определить по индексу. Однако задачки не очень сложные, так
что позиций в списке окажется совсем немного, и мы отсортируем их по значе¬
нию поля level.
Игровой класс
Так как шарики в этом варианте игры не склонны к анимации, то класс шариков
и клеток мы упраздним, а двумерный список поля упростим так, что он будет
хранить только символы из задания:
# СОЗДАЁМ ЗАДАНИЕ
def createPuzzle(self):
puzzle = maps[self.level]
# в первой строке задания
# записано число колонок и строк в
# игровом поле:
sRow = puzzle[0]
self.cols = sRow["cols"]
self.rows = sRow["rows"]
# по этим данным вычисляем
# отступы поля от границ канвы:
self.offsetCols = (FIELD_COLS - self.cols) / 2.0 * CELL_WIDTH
self.offsetRows = (field_ROWS - self.rows) / 2.0 * CELL_HEIGHT
# минимально возможное
# или максимально допустимое
# число ходов:
if sRow["minmoves"]:
self.max_moves = sRow["minmoves"]
else:
self.max_moves = sRow["moves"]
#print('max_moves ' + str(self.max_moves))
# создаём двумерный список для игрового поля:
self.field = self.createArray(self.cols, self.rows)
# по всем строкам списка:
for row in range(self.rows):
# текущая строка:
# по всем символам в строке:
for col in range(self.cols):
#print('col ' + str(col))
c = puzzle[row + 1][col]
self.field[col][row] = c
# создаём список позиций:
self.pos = []
# и запоминаем исходную позицию:
self.pos.append(Position(self.field))
По сути, мы получили тот же самый список строк, что и в самом задании, но раз¬
били строки на отдельные символы.
Теперь все клетки на игровом поле - это всего лишь картинки. Пустые клетки -
это просто серые квадратики двух оттенков:
# РИСУЕМ ПУСТУЮ КЛЕТКУ
def drawEmptyCell(self, col, row, x, y):
if ((col + row) % 2):
fill(240)
else:
fill(200)
rect(x, y, CELL_WIDTH, CELL_HEIGHT)
Для других клеток я оставил картинки, но можно заменить их цветными квадра¬
тиками:
# РИСУЕМ ИГРОВОЕ ПОЛЕ
def dnawField(self):
imageMode(CORNER)
nectMode(CORNER)
fon now in range(self.rows):
for col in range(self.cols):
# коорд. лев.верхнего угла:
x = col * CELL_WIDTH + self.offsetCols
y = now * CELL_HEIGHT + self.offsetRows
status = self.field[col][now]
# рисуем клетку -->
if status == ' ': pass
elif status == '+':
image(Media.imgWall1, x
, y)
elif status == '.':
self.dnawEmptyCell(col,
now,
x,
y)
elif status == 'R':
self.dnawEmptyCell(col,
now,
x,
y)
image(Media.imgRed, x,
y)
elif status == 'G':
self.dnawEmptyCell(col,
now,
x,
y)
image(Media.imgGneen, x
, y)
elif status == 'B':
self.dnawEmptyCell(col,
now,
x,
y)
image(Media.imgBlue, x,
y)
elif status == 'X':
self.dnawEmptyCell(col,
now,
x,
y)
image(Media.imgGnay, x,
y)
Получив значение символа из списка поля field, мы печатаем картинку со стеной
или цветным шариком. Под шариком нужно сначала нарисовать пустую клетку.
Пора решаться!
Нажимаем заветную красную кнопку и отправляемся в функцию solve.
Как вы помните, мы сохранили исходную позицию в списке pos. Она находится
на нулевом уровне. Это вполне естественно, поскольку ни один ход ещё не сде¬
лан.
# РЕШАЕМ ТЕКУЩУЮ ЗАДАЧУ
def solve():
# текущий уровень:
level = 0
Если мы не позаботимся об информации о результативности ходов, то любая за¬
дача будет решаться вечно (точнее - пока не исчерпаются системные возможно¬
сти компьютера, а это произойдёт довольно быстро). В переменную res мы за¬
пишем True, если на следующем уровне удалось сделать хотя бы 1 ход, то есть
изменить позицию. Выполнив все ходы на текущем уровне, мы проверяем зна¬
чение этой переменной. Если решение ещё не найдено, то переходим на сле¬
дующий уровень:
# результат ходов на уровне:
res = False
while True:
nes = False
# следующмй уровень:
level += 1
if not res: break
Сейчас мы находимся на нулевом уровне, и в списке pos хранится единственная,
начальная позиция. Но с каждым уровнем число позиций в списке будет увели¬
чиваться, поэтому мы должны перебрать весь список и извлечь из него позиции
текущего уровня level. Остальные позиции пропускаем:
# проверяем все позиции
# текущего уровня:
for p in game.pos:
if p.level != level:
continue
р - это экземпляр класса Position, а непосредственно позиция хранится в списке
p.pos. Из этой позиции мы должны выполнить все возможные ходы, то есть
вниз, вверх, вправо и влево. При этом мы изменим список p.pos, поэтому пра¬
вильным будет только первый ход. Все остальные ходы получат позицию после
первого хода, а вовсе не исходную. Из этого следует, что мы должны для каждо¬
го направления хода получить твёрдую копию текущей позиции, для чего мы
уже приготовили метод getCopyPosition:
# делаем все возможные ходы из начальной позиции
# вниз
# текущая позиция:
position = Position.getCopyPosition(p.pos)
Теперь в переменной position мы имеем позицию на текущем уровне. В нашем
случае - исходную позицию головоломки. Мы можем делать с ней всё что угод¬
но, и при этом список p.pos ничуть не пострадает.
Последовательность ходов может быть любой, но для первой задачи выгоднее
пойти вниз, поэтому давайте с него и начнём:
# пробуем сдвинуть шарики вниз:
if (game.testDown(position)):
Упрощённую проверку я перенёс в класс Game:
# ШАРИКИ ДВИГАЮТСЯ ВНИЗ
def testDown(self, position):
# ни один шарик не сдвинулся с места:
flgMove = False
Теперь у нас нет списка шариков, поэтому ищем их по всему полю:
# ищем шарики на поле:
for now in range(self.rows - 2, 0, -1):
for col in range(1, self.cols):
# в клетке не шарик:
if position[col][row] not in STR_BALLS:
continue
Обратите внимание, что мы ищем шарики снизу вверх. Если ниже шарика нет
других шариков, то он свободно может двигаться вниз, освобождая место тем
шарикам, которые лежат выше. Так мы можем передвинуть все шарики за 1 про¬
ход:
# текущий гор. ряд шарика:
r = row
Шарик может двигаться только в одном случае - если ниже него находится пу¬
стая клетка:
# передвигаем шарик вниз,
# пока возможно:
while (position[col][r + 1] == '.'):
r += 1
Падающий шарик переходит в новую клетку:
# шарик передвинулся:
if (r != row):
# переносим его в новую клетку:
position[col][r] = position[col][row]
# освобождаем старую:
position[col][row] = '.'
# ход сделан:
flgMove = True
# True, если хотя бы 1 шарик
# перешёл в другую клетку:
return flgMove
Если хотя бы 1 шарик переместился, мы уничтожаем (если это возможно) все
группы шариков:
# уничтожаем шарики:
game.elimination(position)
Метод elimination получает позицию на поле после падения шариков:
# УНИЧТОЖАЕМ ГРУППЫ ШАРИКОВ
def elimination(self, position):
res = False
Для координат тех шариков, которые подлежат уничтожению, создаём список
for_elimination:
# список шариков для уничтожения:
for_elimination = []
Ищем на поле цветные шарики:
for row in range(1, self.rows):
for col in range(1, self.cols):
# в клетке не цветной шарик:
if position[col][row] not in STR_COLORBALLS:
continue
Так как в массиве поля мы обозначили шарики буквами, то легко проверить,
находится ли в заданной клетке шарик. Для этого достаточно проверить, имеет¬
ся ли «шариковая» буква в строке STR BALLS:
STR BALLS = 'RGBX'
А если нам нужен цветной шарик (серые/чёрные шарики не уничтожаются), то в
строке STR_COLORBALLS:
STR COLORBALLS = 'RGB'
Для уничтожения шарика необходимо и достаточно, чтобы он имел одного орто¬
гонального соседа того же цвета. А это очень легко проверить:
# цвет шарика:
cln = position[col][now]
#pnint('elimination: ' + col + ' ' + now)
# ищем соседей того же цвета-->
# справа:
if (position[col + l][row] == cln on \
# ниже:
position[col][row + 1] == cln on \
# слева:
position[col - 1][row] == cln on \
# выше:
position[col][now - 1] == cln):
Координаты такого шарика помещаем в список for_elimination:
# это шарик можно уничтожить:
fon_elimination.append({ "col": col, "now": now })
Если список шариков после проверки поля оказался не пустым, то мы уничтожа¬
ем шарики, просто освобождая их клетки:
# нашли шарики для уничтожения:
if len(for_elimination) > 0:
# уничтожаем весь список:
for coord in for_elimination:
position[coord["col"]][coord["row"]] =
res = True
return res
Вполне вероятно, что на поле вообще не останется шариков, поэтому мы прове¬
ряем позицию после падения шариков и уничтожения групп:
# проверяем состояние игры:
resm = game.isGameOver(position)
Для прояснения обстановки считаем оставшиеся на поле шарики:
# ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ ЛИ ИГРА
def isGameOver(self, position):
res = False
# считаем оставшиеся шарики:
nb = self.calcBalls(position)
Эта операция вам известна по предыдущей версии игры, но метод calcBalls те¬
перь получает позицию для проверки:
# СЧИТАЕМ ОСТАВШИЕСЯ ШАРИКИ
def calcBalls(self, position):
restGreen = 0
restRed = 0
restBlue = 0
for row in range(1, self.rows):
for col in range(1, self.cols):
clr = position[col][row]
if (clr == 'G'):
restGreen += 1
elif (clr == 'R'):
restRed += 1
elif (clr == 'B'):
restBlue += 1
return { "all": restGreen + restRed + restBlue, \
"green": restGreen, "red": restRed, "blue": restBlue }
Если шариков не осталось, то решение задачи закончено:
# если не осталось шариков,
# значит игрок выиграл:
if (nb["all"] == 0):
return WIN
Но на поле может возникнуть такая ситуация, что продолжение решения из за¬
данной позиции невозможно:
# если остался только 1 цветной шарик,
# то игрок проиграл:
if (nb["green"] == 1 or nb["red"] == 1 or nb["blue"] == 1):
return LOSE
Если же позиция требует дальнейшего решения, то метод isGameOver возвра¬
щает значение UNKNOWN:
# неясная позиция:
return UNKNOWN
В методе solve текущая позиция получает сообщение от метода isGameOver. Так
как проигрышная позиция не требует дальнейшего решения, то мы её игнориру-
ем. Выигрышную и неясную позицию сохраняем в списке game.pos. Кроме того,
записываем ход и предыдущую позицию:
if (resm != LOSE):
#print('DOWN resm ' + resm)
newpos = Position(position, level + 1, resm)
newpos.move = u'ВНИЗ'
newpos.predpos = p
game.pos.append(newpos)
res = True
Остальные методы действуют так же, поэтому мы не будем рассматривать их
дополнительно.
После выполнения всех ходов на текущем уровне необходимо проверить, не
решена ли задача.
А задача решена, если в списке game.pos оказалась позиция (или несколько по¬
зиций), значение поля res которой равно WIN:
# следующмй уровень:
level += 1
# проверяем, не решена ли задача:
for p in game.pos:
if (p.level != level):
continue
status = p.res
if (status == WIN):
На этом решение заканчивается:
res = False
print(u'yPOBEHb ПРОЙДЕН = ' + str(level))
Но нам ещё нужно для каждой выигрышной позиции напечатать победные хо¬
ды:
pnintMoves(p, level)
В функции printMoves нужно собрать строки с записанными ходами на каждой
позиции, начиная с position:
# ПЕЧАТАЕМ РЕШЕНИЕ
def pnintMoves(position, level):
s = []
p = position
n = level
Если у неё была предыдущая позиция, то мы должны вернуться к ней и посмот¬
реть, какой ход привёл к этой позиции. И так мы продолжаем попятное движе¬
ние до тех пор, пока не достигнем исходной позиции, у которой предыдущей не
было:
while (p.pnedpos):
s.append(u,Ход ' + stn(n) + ' + p.move)
p = p.pnedpos
n -= 1
Теперь в списке s записаны все ходы, приведшие к решению задачи. Но они за¬
писаны в обратном порядке, то есть от последнего к первому. Но нам нужно
напечатать их в прямом порядке, поэтому мы переворачиваем массив:
for string in nevensed(s):
pnint(stning)
print('')
Если вы ещё не забыли, мы рассматривали ходы из начальной позиции для пер¬
вой задачи. После первого хода мы записали в список game.pos всего одну по¬
зицию, которая получилась после хода вниз. Переменная res получила значение
True, поэтому мы возвращается в начало цикла while и выполняем все возмож¬
ные ходы из позиции первого уровня:
if not res: break
После двух ходов - вправо и влево мы получим выигрышную позицию, и реше¬
ние задачи на этом закончится.
Нажимаем красную кнопку. Картинка на экране не изменяется, но в консоли
можно прочитать, что задача решена за 2 хода, причём она имеет 2 решения
(Рис. 4).
УРОВЕНЬ 1
УРОВЕНЬ ПРОЙДЕН = 2
Ход 1. ВНИЗ
УРОВЕНЬ ПРОЙДЕН = 2
Ход 1. ВНИЗ
Ход 2. ВЛЕВО
Рис. 4
Так как наша программа гибридная, то вы можете не только решать задачи с по¬
мощью компьютера, но и самостоятельно - нажимая клавиши или кнопки со
стрелками (Рис. 5).
За несколько минут вы успешно пройдёте все 30 уровней, приложенные к про¬
грамме. Все они решаются очень быстро, поскольку перебор позиций невелик.
Именно поэтому мы не проверяем, не повторяются ли позиции после выполне¬
ния очередного хода. Например, в первой задаче ходы ВНИЗ-ВВЕРХ приведут к
исходной позиции, и дальше мы будем решать ту же самую задачу, но решение
удлинится на 2 хода. Однако через 2 хода мы уже найдём самые короткие ре¬
шения, поэтому эта ветвь решения дальше и не вырастет. В более сложных про¬
граммах, конечно, позиции необходимо проверять, а это приведёт к заметному
усложнению программы.
Рис. 5
Eliminator Creator
Вмиг пройдя все 30 уровней, не захотелось ли вам наваять собственных? - Про¬
верять задания мы умеем. Осталось только их придумать. Но как только начнёшь
думать, так сразу же хочется, чтобы за тебя думали другие. Проще всего догово¬
риться с компьютером. Он совершенно безотказный, но требует обстоятельных
разъяснений в виде исходного кода. Мы это сделаем.
Опять начнём с установки новой кнопки. На этот раз зелёной (Рис. 1).
Рис. 1
Она будет составлять за нас новые задачки:
global btnPlay, btnLevelUp, btnLevelDown, \
btnUp, btnDown, btnLeft, btnRight, \
btnSolve, btnCreate
btnCreate = cp5.addButton("CREATE!") \
.setPosition(598, 294) \
.setSize(80, 39) \
.setColorBackground(color(0,155,51)) \
.setFont(font)
Я нашёл для неё подходящее место - чуть выше кнопок со стрелками.
Функция create, закреплённая за этой кнопкой, отправляет нас в одноимённый
игровой метод:
# НАЖИМАЕМ КНОПКУ МЫШКИ
def mousePressed():
elif btnCreate.isMousePressed():
create()
# СОЗДАЁМ НОВУЮ ГОЛОВОЛОМКУ
def create():
game.create()
Значение константы MAX_LEVEL нужно изменять для каждой новой задачки. Ес¬
ли вы сохранили предыдущие уровни, то следующим должен стать 31-й:
# СОЗДАЁМ НОВУЮ ГОЛОВОЛОМКУ
def create(self):
level = MAX LEVEL
Составление новой задачи начинается с возведения наружных стен. Я остано¬
вился на самом простом варианте, когда стены образуют прямоугольник. Шири¬
на и высота прямоугольника выбираются случайно из допустимого диапазона
6..10 клеток:
nows = randint(6, 10)
cols = randint(6, 10)
field = self.createArray(cols, nows)
print(cols)
pnint(nows)
Если вы хотите создавать более фигурные стены, то подумайте, как это можно
сделать.
Сразу после создания списка field в нём ничего нет. Но природа не терпит пусто¬
ты, поэтому мы заполняем его пустыми клетками:
# расставляем пустые клетки:
for r in nange(nows):
for c in nange(cols):
field[c][r] =
Теперь возводим стены по периметру поля:
# стены по периметру поля:
for c in nange(cols):
field[c][0] = '+'
field[c][rows - 1] = '+'
for r in range(1, rows - 1):
field[0][r] = '+'
field[cols - 1][r] = '+'
Внутри поля могут быть и другие, отдельные стены, а также шарики четырёх
разных цветов. Число стен и шариков выбираем случайно из экспериментально
выбранных диапазонов:
# число внутренних стен:
wall = randint(3, 7)
self.place(field, cols, rows, '+', wall)
# число красных шариков:
red_balls = randint(2, 4)
self.place(field, cols, nows, 'R', red_balls)
# число зелёных шариков:
green_balls = randint(2, 4)
self.place(field, cols, rows, 'G', green_balls)
# число синих шариков:
blue_balls = randint(0, 3)
if (blue_balls == 1):
blue_balls += 1
self.place(field, cols, rows, 'B', blue_balls)
# число серых шариков:
gray_balls = randint(0, 3)
self.place(field, cols, rows, 'X', gray_balls)
Операции по их размещению одинаковы, и мы перенесём их в метод place. Для
каждого шарика (стены) он выбирает случайную клетку на поле. Если она сво¬
бодна, то помещает туда заданный артефакт. Если уже занята, то ищет другую
клетку:
# РАЗМЕЩАЕМ СТЕНЫ И ШАРИКИ
def place(self, field, cols, rows, clr, nball):
n = 0
while (n < nball):
# координаты:
c = 0
r = 0
while (field[c][r] != '.'):
c = randint(1, cols - 1)
r = randint(1, rows - 1)
field[c][r] = clr
n += 1
Создаём карту уровня:
maps[level] = []
maps[level].append({ "cols": cols, "rows": rows, \
"moves": 4, "minmoves": 2 })
В первую «строку» записываем размеры поля.
За ней следуют строки с символами, обозначающими стены и шарики:
for r in range(rows):
s = ''
for c in range(cols):
s += field[c][r]
print( + s + "',")
maps[level].append(s)
self.level = level
print(u'yPOBEHb: ' + str(self.level))
self.newGame()
Творческая мастерская
Следующим будет 31-й уровень:
# число уровней в игре:
MAX LEVEL = 31
Запустите программу и нажмите кнопку СОЗДАТЬ! Появится новый уровень (Рис.
2).
Вы можете принять его или отвергнуть.
Наша программа очень простая, поэтому создаёт и явно неприемлемые зада¬
ния. Например, на Рис. 2 хорошо видно, что два красных шарика стоят вместе,
образуя группу. Это нехорошо! При расстановке шариков на поле нужно прове¬
рять, чтобы не было ортогональных соседей того же цвета. Но гораздо проще
визуально оценить качество предложенной компьютером задачки. Если она не
годится, то снова нажмите кнопку СОЗДАТЬ!
Я несколько раз нажал на творческую кнопку и получил, на первый взгляд,
вполне качественную головоломку (Рис. 3).
Рис. 2
Теперь нужно проверить решение. Нажимаю кнопку РЕШИТЬ! и получаю ответ
(Рис. 4).
Если вам нужны простые задачки, то сохраните информацию о ней в файле
Maps.py. Но, по-моему, 2 хода - это слишком мало.
Рис. 3
УРОВЕНЬ: 31
УРОВЕНЬ ПРОЙДЕН = 2
Ход 1. ВЛЕВО
Ход 2. ВНИЗ
УРОВЕНЬ ПРОЙДЕН = 2
Ход 1. ВЛЕВО
Ход 2. ВВЕРХ
Рис. 4
А вот следующая задачка решается уже за 8 ходов (Рис. 5 и 6).
Рис. 5
УРОВЕНЬ
31
УРОВЕНЬ
. ПРОЙДЕН = 3
Ход 1.
ВНИЗ
Ход 2.
ВЛЕВО
Ход В.
ВВЕРХ
Ход 4.
ВЛЕВО
Ход 5.
ВНИЗ
Ход 6.
ВПРАВО
Ход 7.
ВНИЗ
Ход 8.
ВЛЕВО
УРОВЕНЬ ПРОЙДЕН = 31
ХОД 1.
ВНИЗ
Ход 2.
ВЛЕВО
Ход 3.
ВЛЕВО
Ход 4.
ВНИЗ
Ход 5.
ВПРАВО
Ход б.
ВВЕРХ
Ход 7.
ВНИЗ
Ход 3.
ВЛЕВО
Рис. 6
Копирую решение в файл Maps.py:
#Ход 1. ВНИЗ
#Ход 2. ВЛЕВО
#Ход 3. ВВЕРХ
#Ход 4. ВЛЕВО
#Ход 5. ВНИЗ
#Ход 6. ВПРАВО
#Ход 7. ВНИЗ
#Ход 8. ВЛЕВО
#Ход 1. ВНИЗ
#Ход 2. ВЛЕВО
#Ход 3. ВЛЕВО
#Ход 4. ВНИЗ
#Ход 5. ВПРАВО
#Ход 6. ВВЕРХ
#Ход 7. ВНИЗ
#Ход 8. ВЛЕВО
Добавляю первую строку:
maps[31] = [{'minmoves': 2, 'rows': 10, 'moves': 4
I-, 'cols': 8},
Копирую строки с заданием из Консольного окна в программу (Рис. 7).
8
10
TTT J
' + .G.R
'+G.R.
в. + ' ,
" + + R. .
■ +\
■ +\
' + . . . .
■ ■ + ' J
1.
. G+'
...... - J
+B+++R++ j
■ ■
TTTTTTTT J
Рис. 7
Новый уровень готов:
^57)
maps[31] = [{'minmoves': 2, 'nows': 10, 'moves': 4, 'cols': 8},
'++++++++',
G.R..+',
'+G
.R...+ ,
' + .
.+.B.+ ,
++R....+ ,
' + .
' + .
+ ',
' + .
....G+ ,
+B+++R++ ,
'++++++++']
#Ход 1.
ВНИЗ
#Ход 2.
ВЛЕВО
#Ход 3.
ВВЕРХ
#Ход 4.
ВЛЕВО
#Ход 5.
ВНИЗ
#Ход 6.
ВПРАВО
#Ход 7.
ВНИЗ
#Ход 8.
ВЛЕВО
#Ход 1.
ВНИЗ
#Ход 2.
ВЛЕВО
#Ход 3.
ВЛЕВО
#Ход 4.
ВНИЗ
#Ход 5.
ВПРАВО
#Ход 6.
ВВЕРХ
#Ход 7.
ВНИЗ
#Ход 8.
ВЛЕВО
Вы можете самостоятельно пройти этот уровень или воспользоваться подсказ¬
ками и убедиться, что программа работает верно.
Основная работа при создании уровней заключается в копировании данных в
файл Maps.py. Но за сравнительно короткое время я наделал 20 уровней для
продолжения оригинальной игры. Вы найдёте их в папке Eliminator_20.
Питон не шибко быстр, поэтому в конце функции solve я добавил ограничение
на глубину поиска решения:
if not res:
break
elif level > 10:
ргШ(и'ЗАДАЧА РЕШЕНИЯ НЕ ИМЕЕТ! ' + str(level))
bneak
Конечно, можно искать решения и более глубоко, но для «человеческого» ре¬
шения они всё равно будут слишком сложны, так что 11 ходов будет вполне до¬
статочно.
Придумывайте свои уровни!
Mestor
Придумывать можно не только уровни для известной игры, но и свою игру. Дело
это непростое, поэтому мы обопрёмся на Элиминатор и добавим к нему
немножко Сокобана. Шарики по-прежнему падают и валятся в одну из четырёх
сторон, но при этом не гибнут, даже сбившись в плотную группу. Это значит, что
проиграть в нашу игру невозможно, но играть можно до бесконечности.
От славной головоломки Сокобан мы возьмём самую суть: все шарики нужно за¬
гнать на заранее отведённые места, обозначенные цветными точками. Слегка
подлатав интерфейс предыдущей игры, мы получим вот такое первое задание
(Рис. 1).
Рис. 1
Название Местор произошло с моей помощью от слова место. Стены я увесе¬
лил цветом, чтобы они не смотрелись столь мрачно, как в Элиминаторе.
Задание стало хитрее, потому что мы должны дополнительно показать и места
для шариков в конце игры. В одном списке это сделать трудно, ведь шарики мо¬
гут сразу оказаться в конечных клетках, причём не на своих местах. Можно, ко¬
нечно, сложным образом всё это зашифровать, но мы поступим проще и про¬
должим строковый список ещё на rows строчек, чтобы записать в него и конеч¬
ную позицию в игре. Все шарики одного цвета считаются одинаковыми, так что
безразлично, куда какой шарик попадёт. Важно только, чтобы цвет шарика сов¬
пал с цветом клетки.
Задание для первого уровня выглядит так:
Условие стало заметно длиннее, но зато выглядит просто и понятно, что важнее
хитроумной краткости.
В игровой класс нужно добавить список для хранения конечной позиции:
# целевая позиция:
self.targetPos = None
В методе createPuzzle заполняем его данными:
# СОЗДАЁМ ЗАДАНИЕ
def createPuzzle(self):
puzzle = maps[self.level]
# в первой строке задания
# записано число колонок и строк в
# игровом поле:
sRow = puzzle[0]
self.cols = sRow["cols"]
self.rows = sRow["rows"]
#print self.cols, self.rows
# по этим данным вычисляем
# отступы поля от границ канвы:
self.offsetCols = (FIELD_COLS - self.cols) / 2.0 * CELL_WIDTH
self.offsetRows = (field_ROWS - self.rows) / 2.0 * CELL_HEIGHT
# минимально возможное
# или максимально допустимое
# число ходов:
if sRow["minmoves"]:
self.max_moves = sRow["minmoves"]
else:
self.max_moves = sRow["moves"]
#print('max_moves ' + str(self.max_moves))
# создаём двумерный список для игрового поля:
self.field = self.createArray(self.cols, self.rows)
# по всем строкам списка:
for row in range(self.rows):
# по всем символам в строке:
for col in range(self.cols):
c = puzzle[row + 1][col]
self.field[col][row] = c
# создаём двумерный список для целевой позиции:
self.targetPos = self.createArray(self.cols, self.rows)
for row in range(self.rows):
# по всем символам в строке:
for col in range(self.cols):
c = puzzle[row + self.rows + 1][col]
self.targetPos[col][row] = c
В методе draw_field мы сначала рисуем, как обычно, игровое поле со стенами и
фишками, а затем вызываем метод draw_target, чтобы нарисовать поверх пустых
клеток и фишек цветные точки:
# РИСУЕМ ИГРОВОЕ ПОЛЕ
def draw_field(self):
imageMode(CORNER)
rectMode(CORNER)
for row in range(self.rows):
for col in range(self.cols):
# коорд. лев.верхнего угла:
x = col * CELL_WIDTH + self.offsetCols
y = row * CELL_HEIGHT + self.offsetRows
status = self.field[col][row]
# рисуем клетку -->
if status == ' ': pass
elif status == '+':
image(Media.imgWall1, x, y)
elif status == '.':
self.drawEmptyCell(col, row, x, y)
elif status == 'R':
self.drawEmptyCell(col, row, x, y)
image(Media.imgRed, x, y)
elif status == 'G':
self.drawEmptyCell(col, row, x, y)
image(Media.imgGreen, x,
> y)
elif status == 'B':
self.drawEmptyCell(col,
row,
^ y)
image(Media.imgBlue, x,
y)
elif status == 'X':
self.drawEmptyCell(col,
row,
^ y)
image(Media.imgGray, x,
y)
self.draw_target()
Сделать это несложно, ведь мы сохранили в списке targetPos цвета этих точек:
# ОТМЕЧАЕМ ЦЕЛЕВЫЕ КЛЕТКИ
def draw_target(self):
ellipseMode(CENTER)
strokeWeight(l)
stroke(255)
for row in range(self.rows):
for col in range(self.cols):
# коорд. центра:
x = col * cell_width + self.offsetCols + cell_width // 2
y = row * CELL_HEIGHT + self.offsetRows + CELL_HEIGHT // 2
status = self.targetPos[col][row]
# рисуем кружки -->
r = 16
if status == 'R':
fill(Red)
ellipse(x, y, r, r)
elif status == 'G':
fill(Green)
ellipse(x, y, r, r)
elif status == 'B':
fill(Blue)
ellipse(x, y, r, r)
elif status == 'X':
fill(Black)
ellipse(x, y, r, r)
Как выглядит игра на экране, вы уже видели. Выглядит она неплохо. И тут игрок
начинает осознанно мыслить и выбирать ходы, ведущие к победе. В самих ходах
ничего нового нет, потому что они выполняются по тем же правилам, что и в
Элиминаторе. Например, ход вниз работает так:
# ШАРИКИ ДВИГАЮТСЯ ВНИЗ
def testDown():
# ни один шарик не сдвинулся с места:
flgMove = False
# ищем шарики на поле:
for now in range(game.rows - 2, 0, -1):
for col in range(1, game.cols - 1):
# в клетке не шарик:
if game.field[col][row] not in STR_BALLS:
continue
#print('BALL')
# текущий гор. ряд шарика:
r = row
# передвигаем шарик вниз,
# пока возможно:
while game.field[col][r + 1] == '.':
r += 1
# шарик передвинулся:
if (r != row):
# переносим его в новую клетку:
game.field[col][r] = game.field[col][row]
# освобождаем старую:
game.field[col][row] = '.'
# ход сделан:
flgMove = True
# True, если хотя бы 1 шарик
# перешёл в другую клетку:
return flgMove
После каждого результативного хода игрока в функции doMove мы вызываем
функцию isGameOver, чтобы проверить, не закончилась ли игра:
# ход выполнен:
if (flgMove):
game.nMove += 1
р^п^и'Ход ' + str(game.nMove) + ': ' + message)
Media.sndUpal.trigger()
# проверяем, не закончилась ли игра:
isGameOver()
else:
Media.sndError.trigger()
Проверка очень простая. Игра заканчивается только в том случае, если все цвет¬
ные шарики стоят в клетках своего цвета:
# ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ ЛИ ИГРА
def isGameOver():
for now in range(1, game.rows - 1):
for col in range(1, game.cols - 1):
colorField = game.field[col][row]
colorTarget = game.targetPos[col][row]
if (colorField != colorTarget):
return
drawSzene()
# игрок выиграл:
game.status = STOP
# переходим на следующий уровень:
game.level += 1
if (game.level > MAX_LEVEL):
game.level = 1
win(u'УРОВЕНЬ ПРОЙДЕН!')
print(u'УРОВЕНЬ ПРОЙДЕН!')
print("")
Мастерская по изготовлению уровней
Игра новая, поэтому уровни придётся составлять самим. Для этого я написал
программу MestorCreator, которая берёт на себя часть наших работ и забот (Рис.
2).
Рис. 2
Для нового уровня нужно назначить порядковый номер:
# число уровней в игре:
MAX LEVEL = 16
Нажимаем кнопку СОЗДАТЬ! и получаем случайное задание (Рис. 3).
Рис. 3
Это начальная позиция. Её можно скопировать из Консольного окна (Рис. 4).
Из начальной позиции нужно получить конечную. Перемещайте шарики по все¬
му полю так, чтобы как можно сильнее запутать игрока, но при этом старайтесь
создать какой-нибудь интересный узор из цветных шариков.
За 21 ход я собрал все шарики в одну линию (Рис. 5).
8
10
' ++++++++
J
+
1
'+.
...R.+
J
.+. . .+
J
'++
J
4G
R+
J
+
J
.++.++
J
G+
J
' ++++++++
J
' ++++++++
J
+
J
+
J
+
+
J
'++
J
+
J
+
J
'+.
.++.++
J
. RGGR+
J
' ++++++++
'I
Рис. 4
Получилось неплохо. Пусть это будет конечная позиция.
Для конечной позиции в консоли имеется только пустая заготовка, так что шари¬
ки вы должны расставить самостоятельно. Задание в файле Maps.py готово:
maps[16] =[{ "cols": 8, "nows": 10, "moves": 21, "minmoves": 7 },
'++++++++',
' + + ',
' + R.+ ',
+..+...+ ,
++....++ ,
+G....R+ ,
' + + ',
+..++.++ ,
' + G+',
'++++++++',
'++++++++'
+ ',
+ ',
' + .
'++
....++ ,
' + .
+ ',
' + .
+ ',
' + .
.++.++',
' + .
. RGGR+',
'++++++++']
Рис. 5
Перезапустите программу и перейдите к последнему заданию (Рис. 6).
Рис. 6
Проверьте, что все фишки и точки на месте. Нажмите кнопку РЕШИТЬ! В боль¬
шинстве случаев вас ждёт разочарование: так как в этой версии игры шарики не
уничтожаются, то число позиций значительно больше, чем в Элиминаторе, и
Питон не может просмотреть их дальше 11 ходов. Но решение многих задач
длиннее. Это значит, что не для всех задач мы сможем найти самые короткие
решения. То, что решение имеется, мы уверены, поскольку по правилам игры
перешли от одного положения к другому.
Но эта задача благополучно решается за 7 ходов (Рис. 7).
УРОВЕНЬ: 16
УРОВЕНЬ ПРОЙДЕН = 7
Ход 1.
ВЛЕВО
Ход 2.
ВНИЗ
Ход В.
ВПРАВО
Ход 4.
ВНИЗ
Ход 5.
ВПРАВО
Ход 6.
ВНИЗ
Ход 7.
ВПРАВО
Рис. 7
Первые 10 заданий я составил этим способом.
Следующую пятёрку заданий я составлял наоборот. Получал случайное поле и в
текстовом файле расставлял шарики в конечную позицию, чтобы она заведомо
была красивой. Затем делал ходы, чтобы запутать ситуацию и получить началь¬
ную позицию.
Крестики-нолики
Мальчик с нашей улицы
С девочкой играл
На асфальте крестики
Мелом рисовал
Шла над южным городом
Летняя пора
Крестики - нолики
Детская игра.
Детская песня
Крестики-нолики - игра, знакомая каждому с детства! На поле 3 х 3 клетки 2 иг¬
рока поочерёдно выставляют свой значок: первый игрок - крестик, второй - но¬
лик. Выигрывает тот, кто первым составит из трёх своих значков непрерывный
ряд - по горизонтали, по вертикали или по диагонали.
Давно известно, что при правильной игре каждая партия в Крестики-нолики за¬
канчивается вничью, но игра до сих пор привлекает внимание детей, взрослых и
программистов...
| По-английски игра называется Cross and Naught, Cross & Zero, Tic-Tac-Toe.
Займёмся интерфейсом
Игровое поле мы нарисуем на листочке бумаги в клеточку, которая для этого так
и разлинована (Рис. 1).
Крестики и нолики обычно невзрачные, но мы их сделаем красивее (Рис. 2).
Измерения листочка бумаги показывают, что его размеры в пикселях составляют
450 по ширине и 600 по высоте. Точно такие же размеры должно иметь окно:
# This Python file uses the following encoding: utf-8
# размеры окна в пикселях:
WIDTH = 450
HEIGHT = 600
Размеры картинок большие, чтобы они достойно представляли крестики и ноли¬
ки на экране:
# размеры картинок:
IMAGE_WIDTH = 96
IMAGE HEIGHT = 96
Рис. 2
Звуки в серьёзных играх необязательны, но у нас их немного и они прозвучат ис¬
ключительно по делу. Все загрузочные работы выполняем в функции preload:
# ЗАГРУЖАЕМ МЕДИАФАЙЛЫ
def preload():
# загружаем картинки -->
global imgBack
# загружаем фоновую картинку:
imgBack = loadImage("data/imgBack.png")
Media.load(Minim(this))
И в методе load класса Media:
# This Python file uses the following encoding: utf-8
# МЕДИАФАЙЛЫ
class Media():
@classmethod
def load(cls, m):
#print "Media.load"
# крестик и нолик:
Media.imgX = loadImage("data/x.png");
Media.imgO = loadImage("data/o.png");
# загружаем звуки:
minim = m
# загружаем звуки -->
# нажимаем кнопку:
Media.sndChose = minim.loadSample("data/chose.wav");
# ошибка:
Media.sndError = minim.loadSample("data/error.wav");
# победа:
Media.sndWin = minim.loadSample("data/win.wav")
Функцию setup традиционно начинаем с создания окна:
# ГОТОВИМСЯ К ПЕРВОЙ ИГРЕ
def setup():
# создаём окно:
size(WIDTH, HEIGHT)
# загружаем файлы:
preload()
Тут же вспоминаем, что нам не обойтись без кнопок.
Первая кнопка начинает новую игру, поскольку партия в Крестики-нолики за¬
канчивается очень быстро, и с первого раза не наиграешься.
Ещё 9 кнопок мы аккуратно положим на клетки игрового поля для удобного
нажимания мышкой.
Чтобы под кнопками просвечивали картинки, мы делаем их полностью прозрач¬
ными:
# создаём кнопки -->
# создаём кнопку Новая игра:
cp5 = ControlP5(this)
global btnNewGame
imgs = [loadImage("button_a.png"),loadImage("button_b.png"),\
loadImage("button_c.png")]
btnNewGame = cp5.addButton("Новая игра ") \
.setPosition(160, 555) \
.setSize(180,33) \
.setlmages(imgs)
# создаём цифровые кнопки 1..9:
global button
# загружаем прозрачную картинку:
imgButton95x96 = loadImage("data/imgButton96x96.png")
button = []
for id in range(9+1):
btn = cp5.addButton(str(id)) \
.setSize(IMAGE_WIDTH, IMAGE_HEIGHT) \
.setValue(id) \
.setImage(imgButton95x96)
button.append(btn)
# устанавливаем цифровые кнопки:
x = y = 0
dx = 100
dy = 100
xb = 103
yb = 203
for id in range(9+1):
if (id + 1 == 4 or id + 1 == 7):
xb = 103
yb += dy
button[id].setPosition(x + xb, y + yb)
xb += dx
И хотя кнопки не видны невооружённым глазом, они исправно реагируют на
мышиные нажатия и вызывают функцию movePlayer. Функция одна на все кноп¬
ки, поэтому мы записываем в поле value номер кнопки id, по которому будем их
различать. Позиционирование элементов управления на странице - требует
внимания и скрупулёзных измерений координат.
В самом конце функции setup создаём игру и тут же начинаем её:
global timer
timer = None
# создаём игру:
global game
game = Game()
# начинаем игру:
newGame()
Таймер нам нужен, как и в других играх против компьютера, чтобы сделать не¬
большую задержку после хода.
Размеры доски постоянны, поэтому их следует сохранить в константе:
# размеры доски:
SIZE = 3
Поля класса Game отражают правила игры в крестики и нолики:
# This Python file uses the following encoding: utf-8
fnom constants impont *
fnom Media impont *
fnom random impont nandint
# КЛАСС ИГРЫ
class Game:
def init (self):
# размеры доски в клетках:
self.cols = SIZE
self.nows = SIZE
Игровое поле (доска) квадратное, что диктует нам хранение текущей позиции в
двумерном списке:
# игровое поле:
self.boand = None
Сначала все клетки поля пустые - IEMPTY, затем на них появляются числа игрока
(IPLAYER) и компьютера - (ICOMPUTER), соответственно:
# номера игроков:
PLAYER = 1
COMPUTER = 2
# числа в клетках:
IPLAYER = 1
ICOMPUTER = 10
IEMPTY = 0
Здесь требует пояснения только выбор значения для числа компьютера в списке
board. Казалось бы, это могла быть двойка, но при подсчёте крестиков и ноликов
на доске очень важно, чтобы значения числа игрока и числа компьютера суще¬
ственно отличались (см. дальше).
Назначение остальных полей понятно без лишних слов:
# счёт игры:
self.result = [0] * 3
# число ходов:
self.nMove = 0
# игрок, делающий ход:
self.player = PLAYER
# победитель:
self.winner = None
# состояние игры:
self.flgGameOver = True
Поскольку выбор параметров игры отсутствует, то в конструкторе мы только об¬
нуляем счёт.
Для почина новой игры вызываем функцию newGame, а та, в свою очередь, од¬
ноимённый игровой метод:
# НАЖИМАЕМ КНОПКУ МЫШКИ
def mousePressed():
# если нажата кнопка...
if btnNewGame.isMousePressed():
newGame()
fon b in button:
if b.isMousePressed():
movePlayen(b)
break
# НАЖИМАЯЕМ КНОПКУ "НОВАЯ ИГРА"
def newGame():
print "newGame"
game.newGame()
if (game.playen == COMPUTER):
moveComputer()
Функция createArray возвращает двумерный массив, заполненный нулями, что
соответствует пустым клеткам IEMPTY:
# НОВАЯ ИГРА
def newGame(self):
# готовимся к новой игре -->
# создаём пустое поле:
self.board = self.cneateAnnay(self.nows, self.cols)
# обнуляем число ходов:
self.nMove = 0
# СОЗДАЁМ ДВУМЕРНЫЙ СПИСОК
def cneateAnnay(self, cols, nows):
# игровое поле:
return [[IEMPTY fon now in range(rows)] fon col in range(cols)]
Игроки выполняют ходы по очереди, но кто-то должен начать игру. Чтобы нико¬
му не было обидно, мы совершенно случайно выбираем первоходца:
# кто делает первый ход:
self.player = randint(PLAYER, COMPUTER)
self.flgGameOver = False
И здесь в свои права вступает функция draw:
# ИГРОВОЙ ЦИКЛ
def draw():
# игра закончилась:
if (game.flgGameOver):
return
# рисуем новый кадр:
drawSzene()
global timer
if timer:
timer.sleep()
Которая только и делает, что вызывает очень полезную функцию drawSzene. В
ней мы во всей красе показываем свои интерфейсные заготовки и добавляем
скромные, но изящные надписи о состоянии игрового процесса, который уже
пошёл:
# ОБНОВЛЯЕМ СЦЕНУ
def drawSzene():
# рисуем фоновую картинку:
background(imgBack)
# рисуем доску:
game.drawBoard()
# пишем название игры:
textSize(42)
textAlign(LEFT)
#textStyle(BOLD)
strokeWeight(0)
fill(138, 43, 226, 100)
text(u"Крестики-нолики", 65, 50)
# печатаем текущего игрока:
textAlign(LEFT)
textSize(30)
fill(0, 255, 0, 100)
stnp = u'Ход '
if (game.playen == PLAYER):
stnp += и'Игрока'
else:
stnp += u'Компьютера'
text(strp, 100, 185)
# счёт игры:
textSize(40)
textAlign(LEFT)
fill(255, 0, 255, 100)
stnn = и"Счёт " + stn(game.nesult[PLAYER]) + " : " \
+ stn(game.nesult[COMPUTER])
text(stnn, 100, 140)
За прорисовку игрового поля с его клетками, крестиками и ноликами отвечает
метод drawBoard, самое сложное в котором - правильно и точно вычислить ко¬
ординаты картинок:
# РИСУЕМ ИГРОВОЕ ПОЛЕ
def drawBoard(self):
for now in nange(self.nows):
for col in nange(self.cols):
item = self.board[col][row]
x = 103 + 100 * col
y = 203 + 100 * now
if item == IEMPTY: pass
elif item == IPLAYER: image(Media.imgX, x, y)
elif item == ICOMPUTER: image(Media.imgO, x, y)
Запускаем программу и убеждаемся, что интерфейс хорош до потери дара речи
(Рис. 3).
Охо - х
1 Kl
оестики-н
юлики
и рт
нтлп 9бп»4
Рис. 3
Куда пойти?
Совсем не обязательно честь сделать первый ход выпадет Игроку, но нам удоб¬
нее начать с него, поскольку он уже от природы награждён интеллектом, и нам
не нужно заботиться о наделении его интеллектом искусственным.
Если ход первый, то Игрок может выбрать любую клетку по своему вкусу. Если
на доске уже есть крестики-нолики, то ему придётся выбирать клетку тщатель¬
нее, чтобы не угодить в уже занятую клетку, что категорически запрещено пра¬
вилами игры. Короче говоря, любой игрок имеет право ходить только в пустую
клетку. Вопрос, в какую именно? Игрок-человек должен ответить на него само¬
стоятельно, а затем нажать выбранную клетку.
Всякое действие Игрока следует проверить. Например, нельзя позволять ему
делать ход не в свою очередь или когда игра уже закончилась:
# ХОД ИГРОКА
def movePlayen(btn):
# игра закончилась:
if (game.flgGameOven):
netunn
# ход Компьютера:
if (game.playen != PLAYER):
netunn
Но такие ошибки случаются редко - если только игрок попался нетерпеливый
или невнимательный.
Если ход выполнен с соблюдением этих правил, то по номеру нажатой кнопки
мы определяем строку и столбец клетки в массиве игрового поля:
# звук нажатия на кнопку:
Media.sndChose.tniggen()
# номер нажатой клетки 0..8:
num = int(btn.getValue())
# координаты на доске:
now = num // game.nows
col = num % game.cols
И тут снова и опять нужно проконтролировать выбор клетки. Если она уже заня¬
та, то вторично ходить в неё нельзя. Крестики и нолики стопками не складывают:
# проверяем ход игрока -->
# эта клетка уже занята?
if (game.board[col][row] != IEMPTY):
# ошибка:
Media.sndError.trigger()
return
Если и это правило соблюдено, то ход засчитывается, а в пустой клетке появля¬
ется значок Игрока, а это всегда крестик, чтобы он не запутался:
# ход сделан:
game.nMove += 1
# ставим крестик:
game.board[col][row] = IPLAYER
Первый крестик можно поставить ку¬
да угодно, но лучше всего - в самое
яблочко (Рис. 4).
После каждого хода необходимо про¬
верить, не закончилась ли игра:
# проверяем, не закончилась ли игра:
isGameOver()
После первого хода она закончиться не может, и ход передаётся Компьютеру:
# передаём ход Компьютеру:
game.player = COMPUTER
global timer
timer = Timer(1000, moveComputer)
Рис. 4
Однако, Пётр Сергеевич, партия!
Если вы не знаете, кто такой Пётр Сергеевич, то поищите его в Интернете.
Игра заканчивается, если:
• Игрок выстроил ряд из трёх крестиков - победа Игрока
• Компьютер выстроил ряд из трёх ноликов - победа Компьютера
• На доске не осталось пустых клеток - ничья
В функции isGameOver мы сначала проверяем символы текущего игрока, ведь
только он может победить после своего хода. Причём игрок может занять по¬
следнюю клетку поля, но это не будет ничейная позиция.
Константа IPLAYER равна 1, а константа ICOMPUTER - 10. Значит, сумма трёх
значков в любом направлении для Игрока равна 3, а для Компьютера - 30. Если
мы получим другую сумму в каком-либо ряду, то в нём не может быть трёх сим¬
волов одного игрока:
# ПРОВЕРЯЕМ, НЕ ЗАКОНЧИЛАСЬ ЛИ ИГРА
def isGameOven():
# сумма трёх значков заданного игрока:
summa = 0
if (game.playen == PLAYER):
summa = IPLAYER * SIZE
else:
summa = ICOMPUTER * SIZE
# ищем ряд из трёх значков игрока:
sum = 0
Все горизонтальные и вертикальные ряды, а также диагонали легко проверяются
в циклах:
# проверяем горизонтали:
fon y in nange(SIZE):
sum = 0
fon x in nange(SIZE):
sum += game.board[x][y]
if (sum == summa):
return gameOver(game.player)
# проверяем вертикали:
for x in range(SIZE):
sum = 0
for y in range(SIZE):
sum += game.board[x][y]
if (sum == summa):
return gameOver(game.player)
# проверяем диагонали:
sum = 0
for y in range(SIZE):
sum += game.board[y][y]
if (sum == summa):
return gameOver(game.player)
sum = 0
for y in range(SIZE - 1, -1, -1):
sum += game.board[SIZE - y - 1][y]
if (sum == summa):
return gameOver(game.player)
Если сумма значков в ряду равна сумме значков текущего игрока, то он победил
(Рис. 5), и программа отправляется в функцию gameOver для торжественного
объявления победителя.
Если этого не случилось, то самое время проверить, не закончилась ли игра по¬
бедой дружбы, то есть вничью (Рис. 6):
# ничья?
if (game.nMove == game.cols * game.rows):
return gameOver(DRAW)
Рис. 5
Рис. 6
Функция gameOver очень простая. Она получает информацию о проверке от
функции isGameOver. Это либо ничья (DRAW), либо победитель (PLAYER или
COMPUTER). Этого вполне достаточно, чтобы показать табличку с результатом
игры на экране:
# ИГРА ЗАКОНЧЕНА
def gameOven(nes):
#pnint(nes)
# ничья:
s = и'НИЧЬЯ'
if (res != DRAW):
game.winner = res
# изменяем счёт:
game.result[game.winner] += 1
s = и'ПОБЕДИЛ '
if (game.winner == PLAYER):
s += и'ИГРОК';
else:
s += u'КОМПЬЮТЕР';
# обновляем сцену:
drawSzene()
# игра закончена:
game.flgGameOver = True
# рисуем табличку:
strokeWeight(2);
stroke(Black)
fill(255, 0, 0, 160)
rectMode(CENTER)
rect(width / 2, -16 + 370, 440, 70)
strokeWeight(0)
rectMode(CORNER)
# печатаем сообщение:
fill(255, 255, 0)
textAlign(CENTER)
textSize(32)
text(s, width / 2, 364)
Media.sndWin.trigger()
Ваш ход, Компьютер!
Но до окончания игры ещё далеко, ведь Компьютер не сделал ни одного хода.
В отличие от Игрока, Компьютер ходит не сам по себе, а вызывает для этого
функцию moveComputer.
Проверки соблюдения правил игры были бы излишни, поскольку Компьютер не
может их нарушить. Поэтому ход засчитывается, функция getMoveCoords под¬
сказывает ему, куда ходить, а Компьютер ставит свой нолик в указанную клетку:
# ХОД КОМПЬЮТЕРА
def moveComputen():
if (game.flgGameOven):
return
Media.sndChose.tniggen()
# ход выполнен:
game.nMove += 1
# выбираем клетку для хода:
res = getMoveCoords()
# ставим нолик:
game.board[res["x"]][res["y"]] = ICOMPUTER
Дальше следует те же самые процедуры, что и в функции Игрока:
# проверяем, не закончилась ли игра:
isGameOver()
# ход переходит к Игроку:
game.player = PLAYER
Цель каждого игрока - добиться победы. Если на по¬
ле возникнет одна из ситуаций, показанных на рисун¬
ке, то Компьютер должен поставить нолик в свобод¬
ную клетку, чтобы он дополнил ряд до трёх ноликов.
Этот ход принесёт Компьютеру победу (Рис. 7).
Рис. 7
Конечно, такая позиция может возникнуть только в
эндшпиле, но мы позаботимся о победе Компьютера
уже в дебюте - в функции getMoveCoords:
# ИЩЕМ КЛЕТКУ ДЛЯ ХОДА КОМПЬЮТЕРА
def getMoveCoords():
#print(str(game.nMove))
# координаты клетки:
col = 0
now = 0
# если есть 2 нолика и пустая клетка
# в каком-либо ряду,
# то ставим нолик:
res = test00XX(ICOMPUTER)
if (res):
return res
Распознать выигрышную позицию призвана функция testOOXX, которая получает
«имя» игрока, для которого нужно найти выигрышный код.
В ней мы последовательно проверяем все горизонтальные и вертикальные ря¬
ды, а затем обе диагонали. Проверки практически такие же, как в функции
isGameOver, только теперь нам нужно найти не три, а два заданных символа:
# ВЫБИРАЕМ ХОД
def test00XX(player):
# сумма двух значков заданного игрока:
summa = player * 2
# ищем ряд из двух значков игрока:
sum = 0
emptycell = 0
# проверяем горизонтали:
for y in range(game.rows):
sum = 0
for x in range(game.cols):
if (game.board[x][y] == IEMPTY):
emptycell = x
sum += game.board[x][y]
if (sum == summa):
return { "x": emptycell, "y": y }
# проверяем вертикали:
for x in range(game.cols):
sum = 0
for y in range(game.rows):
if (game.board[x][y] == IEMPTY):
emptycell = y
sum += game.board[x][y]
if (sum == summa):
return { "x": x, "y": emptycell }
# проверяем диагонали -->
# нисходящая:
sum = 0
for y in range(game.rows):
if (game.board[y][y] == IEMPTY):
emptycell = y
sum += game.board[y][y]
if (sum == summa):
return { "x": emptycell, "y": emptycell }
# восходящая:
sum = 0
for y in range(game.rows - 1, -1, -1):
if (game.board[game.rows - y - 1][y] == IEMPTY):
emptycell = y
sum += game.board[game.rows - y - 1][y]
if (sum == summa):
return { "x": game.rows - emptycell - 1, "y": emptycell };
# двух символов нет:
return None
Если победная клетка найдена, она возвращается в функцию moveComputer, и
Компьютер ставит в неё свой нолик:
# выбираем клетку для хода:
res = getMoveCoords()
# ставим нолик:
game.board[res["x"]][res["y"]] = ICOMPUTER
Не найдя выигрышного продолжения, Компьютер поставит нолик в случайную
пустую клетку. Понятно, что Игрок легко победит такого соперника, поэтому иг¬
райте в поддавки. По крайней мере, Компьютер победы не упустит (Рис. 8).
Рис. 8
Вторая эндшпильная ситуация грозит Компьютеру поражением (Рис. 9).
Рис. 9
Игрок сумел поставить 2 крестика в одном ряду, и следующим ходом добавит к
ним третий.
Если Компьютер не может выиграть на своём ходе, то он должен пресечь по¬
бедную попытку Игрока.
С помощью той же функции testOOXX мы найдём ряд с двумя крестиками и со¬
общим Компьютеру о грозящей ему опасности:
# если есть 2 крестика и пустая клетка
# в каком-либо ряду,
# то ставим нолик:
res = test00XX(IPLAYER)
if (res):
return res
Проверка показывает, что Компьютер успешно отражает угрозы со стороны Иг¬
рока (Рис. 10).
Рис. 10
Но Игрок легко построит вилку, и Компьютер не сможет защититься (Рис. 11).
Совершенно случайные ходы не годятся, они часто приводят к быстрому проиг¬
рышу. Однако легко заметить, что ценность клеток игрового поля далеко не
одинакова. Например, через центральную клетку проходит 4 трёхклеточных ря¬
да, через угловые клетки - по три, через остальные - только по два (Рис. 12).
Рис. 12
Будем считать, что ценность клеток равна числу таких рядов и запишем эти дан¬
ные в массив VALUE:
# ценность клеток:
VALUE = [[3, 2, 3],
[2, 4, 2],
[3, 2, 3]]
Теперь будем выбирать из оставшихся свободных клеток такие, ценность кото¬
рых наибольшая. Их координаты запишем в массив cand:
# список клеток для хода:
cand = []
# максимальное значение клеток:
maxvalue = 0
for y in range(game.rows):
for x in range(game.cols):
# проверяем только пустые клетки:
if (game.board[x][y] == IEMPTY):
(297 )
# если значение в клетке
# больше максимального,
# то запоминаем клетку:
if (VALUE[x][y] > maxvalue):
maxvalue = VALUE[x][y]
cand = []
# клетки с маленьким значением
# пропускаем:
elif (VALUE[x][y] < maxvalue):
continue
# если такое значение клетки
# уже было, то добавояем её в список:
cand.append({ "coords": { "x": x, "y": y } })
Если Компьютер получил ход, значит, на поле ещё остались пустые клетки, и
список cand не пустой. В нём может оказаться единственная клетка, если других
клеток не осталось или остальные клетки имеют меньшее значение. Но в списке
может оказаться и несколько клеток с одинаковым значением.
В любом случае мы выбираем из этого списка случайную клетку и возвращаем
её координаты Компьютеру:
# выбираем случайную клетку из списка:
id = randint(0, len(cand) - 1)
cell = cand[id];
col = cell["coords"]["x"]
row = cell["coords"]["y"]
return { "x": col, "y": row }
К сожалению, такой жадный алгоритм приводит к однообразию дебютов, по¬
скольку первым ходом Компьютер всегда занимает центральную клетку. Затем
пустую угловую (не обязательно лучшую). И наконец, клетку, примыкающую к
стороне поля. Но игра настолько проста, что от неё трудно ждать разнообразия.
Алгоритм выбора хода получился незамысловатым, но и его вполне достаточно,
чтобы Компьютер никогда не проигрывал.
Крестики-нолики с минимаксом
Крестики-нолики - очень простая игра, но именно на ней программисты любят
демонстрировать метод (или алгоритм) минимакс. Именно здесь он не очень
нужен, но сильно пригодится при программировании более сложных игр.
Игрок продолжает руководствоваться при выборе хода своим умом, а Компью¬
тер получает координаты клетки от нового метода getMoveCoords:
# ХОД КОМПЬЮТЕРА
def moveComputen():
if (game.flgGameOven):
return
Media.sndChose.tniggen()
# ход выполнен:
game.nMove += 1
# выбираем клетку для хода:
#res = getMoveCoords()
# ставим нолик:
#game.board[res["x"]][res["y"]] = ICOMPUTER
# выбираем клетку для хода:
coords = game.getMoveCoords()
# ставим нолик:
game.board[coords["col"]][coords["row"]] = ICOMPUTER;
#pnint('coonds ' + str(coords["col"]) + ' ' + str(coords["row"]))
# проверяем, не закончилась ли игра:
isGameOver()
# ход переходит к Игроку:
game.player = PLAYER
Объявим константу WIN_SCORE, значение для которой выбирается произволь¬
ное, но большое:
# выигрышные очки:
WIN SCORE = 1000
Она будет сигнализировать о победе одного из игроков.
Добавим в класс игр ещё одно - очень важное! - поле depth:
# глубина поиска:
self.depth = 2
Для успешной игры в крестики-нолики вполне достаточно просмотреть текущую
позицию на 2 хода вперёд! Например, из начальной позиции Компьютер делает
ход, затем Игрок делает ход, после чего оценивается возникшая на доске пози¬
ция.
Для выбора хода Компьютер вызывает метод getMoveCoords. Тот, в свою оче¬
редь, передаёт методу maximizePlay текущую позицию на доске и глубину поис¬
ка:
# ВОЗВРАЩАЕМ КООРДИНАТЫ ХОДА КОМПЬЮТЕРА
def getMoveCoonds(self):
# получаем ход:
comp_move = self.maximizePlay(self.boand, self.depth)
# координаты клетки:
coonds = { "col": comp_move[0], "now": comp_move[1] }
return coonds
А получает от него координаты клетки для хода.
Чтобы выбрать лучший ход в данной позиции, нужно её как-то оценить. Для это¬
го служит оценочная функция. В данном случае - метод calcScore:
# ХОД КОМПЬЮТЕРА
def maximizePlay(self, board, depth):
# оцениваем позицию на доске:
scone = self.calcScore(board)
Он получает текущую позицию, а возвращает её числовую оценку. Важно, чтобы
оценочная функция работала быстро, поскольку позиций на доске придётся про¬
смотреть немало.
Метод calcScore подсчитывает сумму оценок для всех горизонтальных и верти¬
кальных рядов и двух диагоналей:
# ОЦЕНОЧНАЯ ФУНКЦИЯ
def calcScore(self, board):
# общая оценка:
points = 0
# считаем фишки в вертикальных рядах:
score = 0
for col in range(self.cols):
score = self.scorePosition(board, col, 0, 0, 1)
# выиграл Компьютер:
if (score == WIN_SCORE):
return WIN_SCORE
# выиграл Игрок:
if (score == -WIN_SCORE):
return -WIN_SCORE;
# промежуточная позиция -->
# добавляем очки:
points += score
# считаем фишки в горизонтальных рядах:
for row in range(self.rows):
score = self.scorePosition(board, 0, row, 1, 0)
if (score == WIN_SCORE):
return WIN_SCORE
if (score == -WIN_SCORE):
return -WIN_SCORE
points += score
# считаем фишки в нисходящей диагонали:
score = self.scorePosition(board, 0, 0, 1, 1)
if (scone == WIN_SCORE):
return WIN_SCORE
if (score == -WIN_SCORE):
return -WIN_SCORE;
points += score
# считаем фишки в восходящей диагонали:
score = self.scorePosition(board, 0, 2, 1, -1)
if (score == WIN_SCORE):
return WIN_SCORE
if (score == -WIN_SCORE):
return -WIN_SCORE
points += score
return points
Функция scorePosition очень простая. Она подсчитывает фишки каждого игрока в
заданном ряду:
# ПОДСЧИТЫВАЕМ ФИШКИ В ЗАДАННОМ РЯДУ
def scorePosition(self, board, col, row, dcol, drow):
# очки Игрока:
playerPoints = 0
# очки Компьютерa:
computerPoints = 0
for i in range(SIZE):
if (board[col][row] == IPLAYER):
playerPoints += 1
elif (board[col][row] == ICOMPUTER):
computerPoints += 1
# координаты след. клетки ряда:
col += dcol
row += drow
И возвращает число фишек Компьютера в заданном ряду. Но это только в том
случае, если ни один игрок в текущей позиции не закончил игру в свою пользу. В
таком случае функция scorePosition возвращает значение WIN_SCORE. Если по-
бедил Компьютер, то положительное, а если Игрок - такое же по абсолютной
величине, но отрицательное:
if (playenPoints == SIZE):
netunn -WIN_SCORE
# выиграл Компьютер:
elif (computenPoints == SIZE):
netunn WIN_SCORE
else:
# игра продолжается:
netunn computenPoints
Метод calcScore получает число фишек в заданном ряду и находит сумму фишек
Компьютера во всех рядах. Одна и та же фишка будет посчитана несколько раз,
поэтому ценность фишки зависит от её положения на доске. Больше всего очков
принесёт центральная фишка. Это вы уже знаете.
Но метод calcScore сразу же закончит свою работу, если получит от метода
scorePosition сообщение о победе одного из игроков в данной позиции.
Если в процессе перебора будет достигнута позиция, из которой невозможно
продолжить игру, или достигнута максимальная глубина перебора, то функция
maximizePlay на этом свою работу заканчивает:
# терминальная позиция:
if (self.isTenminated(boand, depth, scone)):
netunn [None, None, scone]
Такие позиции называются терминальными. Они обрывают дальнейшие поиски
лучшего хода.
Метод isTerminated получает текущую позицию board, текущую глубину перебо¬
ра depth и оценку позиции score. Если значение переменной depth равно нулю,
то дальнейший просмотр позиций заканчивается. То же самое в случае победы
одного из игроков или когда на доске не осталось свободных клеток:
# ВОЗВРАШАЕМ true,
# ЕСЛИ ДОСТИГНУТВ ТЕРМИНАЛЬНАЯ ПОЗИЦИЯ
def isTerminated(self, board, depth, score):
if (depth == 0 or \
score == WIN_SCORE or \
score == -WIN_SCORE or \
self.isFull(board)):
return True
return False
Здесь вам уже всё известно за исключением метода isFull, который последова¬
тельно перебирает все клетки доски. Если обнаруживается пустая клетка, он тут
же возвращает False. Если же свободных клеток не окажется, то метод возвра¬
щает True:
# ПРОВЕРЯЕМ, НЕ ЗАПОЛНЕНА
# ЛИ ДОСКА ФИШКАМИ
def isFull(self, board):
for row in range(self.rows):
for col in range(self.cols):
if (board[col][row] == IEMPTY):
return False
return True
Опять возвращаемся в функцию maximizePlay. Как вы помните, она должна вер¬
нуть Компьютеру координаты клетки с наибольшей оценкой позиции. Это и
естественно, ведь программа играет за своих.
Координаты клетки и оценка позиции для хода Компьютера хранятся в пере¬
менной max_score. Нулевой элемент списка - это номер колонки, первый - но¬
мер строки, а второй - оценка. Пока координат клетки мы не знаем (Nonel), а
оценку берём любую, но очень маленькую, чтобы реальная оценка позиции ока¬
залась заведомо больше:
# максимальная оценка:
# col, row, score
max_score = [None, None, -WIN_SCORE + 1]
Из позиции на доске board (на первом ходе Компьютера из начальной позиции)
мы должны сделать все возможные ходы Компьютера. Для этого ищем на доске
свободные клетки:
# перебираем все клетки доски:
for row in range(self.rows):
for col in range(self.cols):
# не пустая клетка:
if (board[col][row] != IEMPTY):
continue
В свободную клетку мы должны поставить фишку Компьютера. Но сейчас мы не
делаем реальный ход на текущей доске, а только пробуем поставить фишку
Компьютера, чтобы оценить новую позицию. Портить текущую доску нельзя,
поэтому мы создаём её точную копию:
# создаём копию доски:
new_board = self.boardCopy(board)
Для этого пишем новый метод boardCopy:
# ВОЗВРАЩАЕМ КОПИЮ ДОСКИ board
def boardCopy(self, board):
return copy.deepcopy(board)
И в пустую клетку уже на новой доске ставим фишку Компьютера:
# ставим на свободную клетку
# фишку Компьютера:
new_board[col][row] = ICOMPUTER
Если бы мы искали лучший ход Компьютера на 1 ход вперёд, то нашли бы клет¬
ку, для которой значение оценочной функции score оказалось бы наибольшим, и
Компьютер поставил бы на неё свою фишку. Нетрудно посчитать, что он выбе¬
рет для первого хода центральную клетку. Но не всегда Компьютер ходит пер¬
вым, поэтому такая недальновидная игра быстро приведёт к поражению. Это
значит, что мы должны учесть и ход Игрока, то есть просмотреть все позиции
после хода Компьютера и уже из них сделать все возможные ходы Игрока.
При этом мы полагаем, что Игрок оценивает позицию точно так же, как и мы, то
есть с помощью метода calcScore. Скорее всего, это не так, но у нас нет другого
выбора, поскольку мы не знаем, как именно Игрок выбирает ходы. Следуя
нашему предположению, мы приходим к выводу, что Игрок на своём ходе вы¬
берет позицию с наибольшей оценкой для себя. Но если это так, то после этих
ходов Игрока мы должны выбрать для Компьютера минимальную оценку. В
итоге мы получим после ходов Игрока минимальные оценки всех позиций после
первого хода Компьютера. Но из них он должен выбрать максимальную, чтобы
сделать удачный ход. При глубине перебора, равной двум, на этом весь перебор
и закончится. При более глубоком переборе уже из каждой позиции Игрока
нужно сделать все возможные ходы за Компьютер. А из них - снова за Игрока.
И так, пока не исчерпается глубина перебора. Если последний ход за Игроком,
то для Компьютера выбираются позиции с минимальными значениями позиций
Игрока, а из них - максимальная. И так далее, пока мы не доберёмся до задан¬
ной позиции board.
В нашем примере после первого хода Компьютера вызывается метод
minimizePlay для Игрока:
# ход Игрока:
playerMove = self.minimizePlay(new_board, depth - 1)
Если это первый ход Игрока, то он запоминается в списке max_score. Если ход не
первый, то мы выбираем лучший для Компьютера после хода Игрока:
# запоминаем лучший ход:
if (max_score[0] == None or playerMove[2] >
max_score[2]):
max_score[0] = col
max_score[1] = now
max_score[2] = playenMove[2]
После выполнения всех ходов на заданную глубину, метод maximizePlay возвра¬
щает координаты лучшего хода:
return max score
Ходы Игрока выполняются так же, но он стремится минимизировать оценки для
позиций Компьютера:
# ХОД ИГРОКА
def minimizePlay(self, board, depth):
score = self.calcScore(board)
if (self.isTerminated(board, depth, score)):
return [None, None, score]
# минимальная оценка:
# col, row, score
min_score = [None, None, WIN_SCORE - 1]
for row in range(self.rows):
for col in range(self.cols):
# не пустая клетка:
if (board[col][row] != IEMPTY):
continue
new_board = self.boardCopy(board)
new_board[col][row] = IPLAYER
computerMove = self.maximizePlay(new_board, depth - 1)
if (min_score[0] == None or computerMove[2] <
min_score[2]):
min_score[0] = col
min_score[1] = row
min_scone[2] = computenMove[2]
return min scone
Несмотря на «неглубокую глубину» просмотра, победить Компьютер невоз¬
можно, поэтому закономерный исход всех игр - ничья (Рис. 1).
Рис. 1
А вот проиграть Компьютеру вполне возможно (Рис. 2).
Рис. 2
Но только по недосмотру или из жалости, ведь компьютер -
наш железный друг.
Литература
[JavaScript]
РубанцевВалерий
Программирование на ЯваСкрипте
Занимательная графика на
ЯваСкрипте
2017. - 430 с.
В книге подробно описываются воз¬
можности графической библиотеки
p5.js для простой и эффективной раз¬
работки графических приложений на
языке ЯваСкрипт.
Все функции проиллюстрированы
многочисленными примерами.
Рубанцев Валерий
Программирование на ЯваСкрипте
Занимательная графика на ЯваСкрипте
®р5*И^^
[ГМ90]
Гарднер Мартин
Путешествие во времени
М.:Мир, 1990. - 341 с.
ISBN: 5-03-001166-8
Рубаицев Валерий
Тотальный тренинг по ЯваСкрипту
Массивы и функциональное программирование
keysreduceRighl
findlndex лот
every indexOf
\\indudes find
lastlndexOf
toString join
«for Each
copyWithin
ciiift reduce
sim concaty
/fill
entries
isArray
Максим Мозговой
САМОУЧИТЕЛЬ
Занимательное
программирование
[JavaScript]
РубанцевВалерий
Тотальный тренинг по ЯваСкрипту
Массивы и функциональное про¬
граммирование
2017. - 260 с.
Книга о массивах, методах и функциональ¬
ном программировании на ЯваСкрипте.
Все методы проиллюстрированы демон¬
страционными проектами.
На занимательных примерах показано, как
решать практические задачи
на ЯваСкрипте в функциональном стиле.
Для учащихся, учителей информатики, лю¬
бителей программирования и начинающих
программистов, имеющих небольшой опыт
в программировании на ЯваСкрипте.
[ММ04]
Мозговой Максим
Занимательное программирование
Питер, 2004. - 208 с.
ISBN: 5-94723-853-5
Серия: Самоучитель
[JavaScript]
' . в ППП
Рубанцев Валерий
Простые компьютерные игры
РубанцевВалерий
Тотальный тренинг по ЯваСкрипту
Простые компьютерные игры
2017. - 220 с.
В книге подробно описывается разработка
10 простых компьютерных игр. Среди них
есть и очень известные игры - Игра Баше,
Угадай число, Закраска - и не очень, и со¬
всем новые - Пузыри, Блиц-Клик, Охота на
Скалоеда и Скалоедов, две программы про
Незнайку и великолепная головоломка
Ножки вверх!
Цель книги: научиться писать простые ком¬
пьютерные игры на языке ЯваСкрипт с ис¬
пользованием графической библиотеки
p5.js.
Для учащихся, учителей информатики, лю¬
бителей программирования и начинающих
программистов, имеющих небольшой опыт
в программировании на ЯваСкрипте.
Серия Программирование для детей
до eo&pwi н
Рубанцев Валерий
Развивающее программирование
Практикум по решению задач на языке Питон 3
Базовый уровень
Рубанцев Валерии
КОМПЬЮТЕР, НАУКА И ЖИЗНЬ
тт
От древности /
Рубанцев Валерий
Развивающее программирование
Практикум по решению задач на языке Пи¬
тон 3. Базовый уровень
2016. - 500 с.
В книге подробно рассматривается реше¬
ние более 100 задач: математических, сло¬
весных, комбинаторных, вероятностных,
игровых.
Лучшие упражнения для отработки навыков
программирования на языке Питон.
Рубанцев Валерий
Компьютер, наука и жизнь
Занимательные математические задачи: От
древности до современности
2016. - 500 с.
Около 150 проектов на языке Си-шарп, по¬
казывающих, как можно решать разнооб¬
разные занимательные математические за¬
дачи на компьютере.