КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 712476 томов
Объем библиотеки - 1400 Гб.
Всего авторов - 274472
Пользователей - 125061

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Владимиров: Ирландец 2 (Альтернативная история)

Написано хорошо. Но сама тема не моя. Становление мафиози! Не люблю ворьё. Вор на воре сидит и вором погоняет и о ворах книжки сочиняет! Любой вор всегда себя считает жертвой обстоятельств, мол не сам, а жизнь такая! А жизнь кругом такая, потому, что сам ты такой! С арифметикой у автора тоже всё печально, как и у ГГ. Простая задачка. Есть игроки, сдающие определённую сумму для участия в игре и получающие определённое количество фишек. Если в

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Дамиров: Курсант: Назад в СССР (Детективная фантастика)

Месяца 3-4 назад прочел (а вернее прослушал в аудиоверсии) данную книгу - а руки (прокомментировать ее) все никак не доходили)) Ну а вот на выходных, появилось время - за сим, я наконец-таки сподобился это сделать))

С одной стороны - казалось бы вполне «знакомая и местами изьезженная» тема (чуть не сказал - пластинка)) С другой же, именно нюансы порой позволяют отличить очередной «шаблон», от действительно интересной вещи...

В начале

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Стариков: Геополитика: Как это делается (Политика и дипломатия)

Вообще-то если честно, то я даже не собирался брать эту книгу... Однако - отсутствие иного выбора и низкая цена (после 3 или 4-го захода в книжный) все таки "сделали свое черное дело" и книга была куплена))

Не собирался же ее брать изначально поскольку (давным давно до этого) после прочтения одной "явно неудавшейся" книги автора, навсегда зарекся это делать... Но потом до меня все-таки дошло что (это все же) не "очередная злободневная" (читай

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Москаленко: Малой. Книга 3 (Боевая фантастика)

Третья часть делает еще более явный уклон в экзотерику и несмотря на все стсндартные шаблоны Eve-вселенной (базы знаний, нейросети и прочие девайсы) все сводится к очередной "ступени самосознания" и общения "в Астралях")) А уж почти каждодневные "глюки-подключения-беседы" с "проснувшейся планетой" (в виде галлюцинации - в образе симпатичной девчонки) так и вообще...))

В общем герою (лишь формально вникающему в разные железки и нейросети)

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
Влад и мир про Черепанов: Собиратель 4 (Боевая фантастика)

В принципе хорошая РПГ. Читается хорошо.Есть много нелогичности в механике условий, заданных самим же автором. Ну например: Зачем наделять мечи с поглощением душ и забыть об этом. Как у игрока вообще можно отнять душу, если после перерождении он снова с душой в своём теле игрока. Я так и не понял как ГГ не набирал опыта занимаясь ремеслом, особенно когда служба якобы только за репутацию закончилась и групповое перераспределение опыта

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).

Большая книга проектов Python [Эл Свейгарт] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

БОЛЬШАЯ КНИГА
ПРОЕКТОВ
PYTHON

ЭЛ СВЕЙГАРТ

2022

ББК 32.973.2-018.1
УДК 004.43
С24

Свейгарт Эл
С24 Большая книга проектов Python. — СПб.: Питер, 2022. — 432 с.: ил. — (Серия
«Библиотека программиста»).
ISBN 978-5-4461-1907-3

16+

Вы уже освоили основы синтаксиса Python и готовы программировать? Отточите свои навыки
на самых интересных задачах — графике, играх, анимации, расчетах и многом другом. Вы можете
экспериментировать, добавляя к готовым проектам собственные детали.
В 256 строк кода поместится всё — «винтажная» экранная заставка, забег улиток на скорость,
рекламный заголовок-приманка, вращающаяся спираль ДНК и так далее. Добавьте к этому пару строк
своего кода, и вы сможете делиться собственными уникальными проектами в интернете.
(В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)

ББК 32.973.2-018.1
УДК 004.43

Права на издание получены по соглашению с No Starch Press. Все права защищены. Никакая часть данной книги
не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских
прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может
гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные
ошибки, связанные с использованием книги. Издательство не несет ответственности за доступность материалов,
ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию все ссылки на интернетресурсы были действующими.

ISBN 978-1718501249 англ.

ISBN 978-5-4461-1907-3

© 2021 by Al Sweigart. The Big Book of Small Python Projects: 81 Easy
Practice Programs, ISBN 9781718501249,
published by No Starch Press Inc. 245 8th Street, San Francisco,
California United States 94103
© Перевод на русский язык ООО Издательство «Питер», 2022
© Издание на русском языке, оформление ООО Издательство
«Питер», 2022
© Серия «Библиотека программиста», 2022

Оглавление

Об авторе..................................................................................................................................... 9
О научном редакторе........................................................................................................... 10
Введение..................................................................................................................................... 11
Проект 1. Бейглз................................................................................................................... 25
Проект 2. Парадокс дней рождения............................................................................. 30
Проект 3. Сообщение в виде битовой карты............................................................ 35
Проект 4. Блек-джек........................................................................................................... 39
Проект 5. Отскакивающий от краев логотип DVD.................................................. 47
Проект 6. Шифр Цезаря.................................................................................................... 53
Проект 7. Взлом шифра Цезаря.................................................................................... 57
Проект 8. Генерация календарей................................................................................... 60
Проект 9. Морковка в коробке...................................................................................... 65
Проект 10. Чо-хан................................................................................................................ 71
Проект 11. Генератор заголовков-приманок.............................................................. 75
Проект 12. Гипотеза Коллатца........................................................................................ 80
Проект 13. Игра «Жизнь» Конвея.................................................................................. 83
Проект 14. Обратный отсчет........................................................................................... 87
Проект 15. Глубокая пещера............................................................................................ 90
Проект 16. Ромбы................................................................................................................. 93

6   Оглавление
Проект 17. Арифметика с игральными костями...................................................... 97
Проект 18. Выбрасыватель игральных костей......................................................... 104
Проект 19. Цифровые часы............................................................................................ 108
Проект 20. Цифровой поток.......................................................................................... 111
Проект 21. Визуализация ДНК...................................................................................... 114
Проект 22. Утята................................................................................................................ 118
Проект 23. Гравировщик.................................................................................................. 124
Проект 24. Разложение на множители..................................................................... 130
Проект 25. Быстрый стрелок......................................................................................... 134
Проект 26. Фибоначчи.................................................................................................... 137
Проект 27. Аквариум....................................................................................................... 141
Проект 28. Заливка........................................................................................................... 150
Проект 29. Моделирование лесного пожара.......................................................... 157
Проект 30. Четыре в ряд................................................................................................ 162
Проект 31. Угадай число.................................................................................................. 168
Проект 32. Простак.......................................................................................................... 171
Проект 33. Мини-игра со взломом............................................................................. 173
Проект 34. «Виселица» и «Гильотина»..................................................................... 179
Проект 35. Гексагональная сетка................................................................................ 185
Проект 36. Песочные часы............................................................................................. 188
Проект 37. Голодные роботы........................................................................................ 194
Проект 38. «Я обвиняю!»................................................................................................ 202
Проект 39. Муравей Лэнгтона...................................................................................... 211
Проект 40. П0г0в0рим (leetspeak).............................................................................. 217
Проект 41. Под счастливой звездой........................................................................... 220

Оглавление    7

Проект 42. Магический хрустальный шар................................................................ 228
Проект 43. Манкала......................................................................................................... 231
Проект 44. Бегущий в лабиринте 2D......................................................................... 238
Проект 45. Бегущий в лабиринте 3D......................................................................... 244
Проект 46. М
 оделирование статистики за миллион бросков
игральных костей........................................................................................ 253
Проект 47. Генератор картин в стиле Мондриана............................................... 256
Проект 48. Парадокс Монти Холла............................................................................ 263
Проект 49. Таблица умножения.................................................................................. 270
Проект 50. Девяносто девять бутылок...................................................................... 272
Проект 51. ДевяНосто деевяять буутылок................................................................ 275
Проект 52. Счет в различных системах счисления.............................................. 279
Проект 53. Периодическая таблица элементов..................................................... 283
Проект 54. Поросячья латынь....................................................................................... 287
Проект 55. Лотерея Powerball....................................................................................... 290
Проект 56. Простые числа.............................................................................................. 295
Проект 57. Индикатор хода выполнения.................................................................. 298
Проект 58. Радуга.............................................................................................................. 302
Проект 59. Камень, ножницы, бумага...................................................................... 306
Проект 60. Камень, ножницы, бумага (беспроигрышная версия).................. 310
Проект 61. Шифр ROT13................................................................................................... 314
Проект 62. Вращающийся куб....................................................................................... 317
Проект 63. Царская игра Ура....................................................................................... 324
Проект 64. Семисегментный модуль индикации.................................................. 333
Проект 65. Ковер из «Сияния»..................................................................................... 337
Проект 66. Простой шифр подстановки.................................................................... 340

8   Оглавление
Проект 67. Синусовидное сообщение........................................................................ 345
Проект 68. Игра в 15........................................................................................................ 349
Проект 69. Бега улиток................................................................................................... 355
Проект 70. Соробан — японский абак..................................................................... 359
Проект 71. Повторение музыки.................................................................................... 365
Проект 72. Губкорегистр................................................................................................. 369
Проект 73. Головоломка судоку.................................................................................. 372
Проект 74. Преобразование текста в речь............................................................. 379
Проект 75. Три карты Монте........................................................................................ 381
Проект 76. Крестики-нолики......................................................................................... 387
Проект 77. Ханойская башня........................................................................................ 391
Проект 78. Вопросы с подвохом.................................................................................. 396
Проект 79. Игра «2048»................................................................................................ 403
Проект 80. Шифр Виженера.......................................................................................... 411
Проект 81. Головоломка с ведрами воды................................................................. 416
Приложение A. Указатель тегов................................................................................. 422
Приложение Б. Таблица кодов символов................................................................ 426

Об авторе

Эл Свейгарт (Al Sweigart) — разработчик программного обеспечения, автор и участник Python Software
Foundation. Ранее работал руководителем по вопросам
образования в Музее искусств и цифровых развлечений —
музее компьютерных игр Окленда, Калифорния. Эл написал
несколько книг по программированию, включая Automate the
Boring Stuff with Python1 и Invent Your Own Computer Games with
Python2. Книги Эла свободно доступны под лицензией Creative Commons на его сайте https://inventwithpython.com/. Его кошка Зофи обожает снеки с водорослями нори.

1

Свейгарт Э. Автоматизация рутинных задач с помощью Python. — М.: Вильямс, 2017.

2

Свейгарт Э. Учим Python, делая крутые игры. — М.: Эксмо, 2021.

О научном редакторе

Сара Кучински (Sarah Kuchinsky) — магистр естественных наук, корпоративный инструктор и консультант.
Она использует Python для множества целей, включая
моделирование систем здравоохранения, разработку игр
и автоматизацию задач. Сара — один из основателей конференции North Bay Python, председатель комиссии по обучающим пособиям конференции PyCon US и ведущий организатор
группы PyLadies Silicon Valley. Защитила дипломы по теории управления, инженерии и математике.

Введение

Программировать легко, когда можно просто скопировать print('Hello, world!'). Вероятно, вам случалось
читать хорошо структурированную книгу или проходить
онлайн-курс для начинающих, прорабатывая упражнения
и поддакивая жаргонным словечкам, которые вы (более или
менее) понимали. Однако когда пришло время покидать гнездо
и писать собственные программы, возможно, летать самостоятельно
оказалось не так просто. Вы обнаружили, что пялитесь в пустое окно редактора и не
знаете, как начать писать свои программы на Python.
Проблема в том, что следовать учебнику очень полезно для усвоения теории, но
это далеко не всегда то же самое, что учиться писать новые программы с нуля. На
данном этапе часто рекомендуют изучать программное обеспечение с открытым
исходным кодом или работать над собственными проектами, но проекты с открытым исходным кодом далеко не всегда хорошо документированы или доступны для
начинающих. И хотя работа над собственными проектами очень стимулирует, вы
остаетесь совершенно без руководства.
В книге вы найдете практические примеры применения различных идей программирования в виде коллекции более чем 80 игр, имитационных моделей и объектов цифрового искусства. Они представляют собой не просто фрагменты кода,
а полноценные работающие программы на Python. Вы можете скопировать их
код, чтобы лучше познакомиться с тем, как они работают, поэкспериментировать,
а затем попытаться воссоздать их самостоятельно в качестве практики. Вскоре вы
найдете идеи для собственных программ и, главное, будете знать, как приступить
к их реализации.

Проектирование маленьких программ
Программирование зарекомендовало себя как навык, открывающий большие
возможности, в том числе по созданию технологических компаний стоимостью

12   Введение
миллиарды долларов и потрясающих технических достижений. Стремиться к большему при создании своего ПО легко, но когда переоцениваешь свои силы — в конце
концов получаешь только незаконченные программы и разочарование. Однако вовсе
не нужно быть компьютерным гением, чтобы создавать интересные и креативные
программы.
Проекты на Python в этой книге отвечают нескольким основным принципам проектирования, чтобы упростить для начинающих понимание исходного кода.
Они маленькие — большинство приведенных в книге программ не превышает
256 строк кода, а часто они намного короче. Благодаря таким ограничениям
размера читателю проще понять эти программы. Число 256 выбрано случайно, но 256 = 28, а степени двойки — счастливые для программистов числа.
Приводятся в виде текста — текст проще, чем графика. Когда и исходный
код, и выводимые программой результаты представляют собой текст, можно
легко отследить, скажем, причинно-следственную связь между оператором
print('Thanks for playing!') в коде и выводимой на экран надписью Thanks
for playing!.
Не требуют установки — все программы заключены в отдельные, самодостаточные файлы исходного кода Python с расширением .py, например
tictactoe.py . Не нужно запускать программу установки и можно легко
разместить такую программу в интернете, чтобы поделиться ею с другими.
Их много — в книге приведена 81 программа. Вы обязательно найдете программы себе по вкусу среди настольных игр, карточных игр, художественных
цифровых изображений, имитационных моделей, математических загадок,
лабиринтов и развлекательных программ.
Они простые — эти программы были написаны так, чтобы быть понятными
даже для начинающих. Выбирая между кодом на основе сложных, высокопроизводительных алгоритмов и простым и ясным кодом, я всегда в этой
книге склонялся к последнему.
Программы в текстовом формате могут показаться несколько старомодными, но
подобный стиль программирования позволяет не отвлекаться на нюансы, связанные
со скачиванием графических данных, установкой дополнительных библиотек и организацией каталогов проекта. Вместо этого можно сосредоточиться на самом коде.

Для кого эта книга
Книга написана для двух групп людей. В первую входят те, кто уже освоил основы
Python и программирования вообще, но все еще не вполне представляет, как писать собственные программы. Этим людям может казаться, что программирование
для них «не сложилось». Можно успешно решать практические упражнения из

Как научиться чему-то на программах из этой книги  

13

учебников, но с трудом представлять себе, как выглядит полная программа. Благодаря сначала копированию, а затем и воссозданию игр из данной книги эти люди
постепенно поймут, как изучаемые здесь понятия программирования компоновать
во множество настоящих программ.
Во вторую группу входят новички в сфере программирования, достаточно азартные
и настроенные на приключения, желающие погрузиться в работу с головой и сразу
же начать создавать игры, имитационные модели и программы, обрабатывающие
большие массивы числовых данных. Таких людей устраивает копирование кода
и изучение его по ходу дела. Или, возможно, они уже умеют программировать на
другом языке, но Python им внове. Эта книга, хотя и не заменяет полноценный вводный курс Python, кратко знакомит читателя с его основами и учит использовать
отладчик для исследования внутренних механизмов работы программы во время
выполнения.
Опытные программисты тоже могут развлечься с программами из этой книги, но
учтите, что она написана все-таки для новичков.

Что можно найти в издании
Хотя основная часть книги посвящена конкретным программам, в ней вы найдете
также дополнительные источники информации по общим вопросам программирования и Python.
Проекты — перечислять здесь 81 проект будет слишком долго, но каждому
из них посвящена отдельная глава, включающая название проекта, описание,
пример результатов работы программы и ее исходный код. Вдобавок приводятся рекомендации относительно изменений, которые вы можете внести
в код, чтобы адаптировать эти программы к своим требованиям.
Приложение A «Указатель тегов» — перечислены все проекты, разбитые на
категории по тегам проектов.
Приложение Б «Таблица кодов символов» — список кодов символов для
сердечек, линий, стрелок и блоков, которые могут выводить ваши программы.

Как научиться чему-то на программах
из этой книги
Эта книга не учит языку Python или каким-либо понятиям программирования, подобно обычному учебнику. В ней применяется подход «учеба на практике», при котором
читателя призывают вручную копировать программы, экспериментировать с ними
и исследовать внутренние механизмы их работы путем запуска их под отладчиком.

14   Введение
Основная идея книги состоит не в подробном пояснении синтаксиса языка программирования, а в демонстрации полноценных примеров программ, реализующих
нечто реальное: карточные ли игры, воспроизведение ли анимации, исследование
ли математической загадки. Поэтому я рекомендую придерживаться следующих
этапов.
1. Скачайте программу и запустите ее, чтобы посмотреть, что она делает.
2. Начав с пустого файла, скопируйте код игры из книги, вручную набрав его
(не используйте команды копирования/вставки!).
3. Запустите программу снова, вернитесь и исправьте все опечатки и ошибки,
которые вы могли случайно внести в код.
4. Запустите программу из-под отладчика, чтобы последовательно выполнить
код построчно и разобраться, что делает каждая строка.
5. Найдите комментарии, отмеченные (!), чтобы найти код, который можно
изменить и посмотреть, как это повлияет на программу при следующем
запуске.
6. Наконец, попробуйте воссоздать программу самостоятельно с нуля. Не обязательно точную ее копию; можете привнести в нее что-то свое.
При копировании кода из книги можете не набирать комментарии (текст в конце
строк, следующий за символом #) — это примечания для программистов, игнорируемые Python. Однако старайтесь писать свой код на Python на строках с теми же
номерами, что и программы в данном издании, чтобы упростить их сравнение. Если
у вас не получается найти опечатки в своих программах, то можете сравнить свой
код с кодом из книги с помощью онлайн-утилиты diff по адресу https://inventwithpython.com/bigbookpython/diff/.
Каждую программу описывает набор тегов, например настольная игра, имитационная
модель, художественная и для двух игроков. Пояснения ко всем этим тегам и перекрестный указатель тегов и проектов приведены в приложении A.

Скачивание и установка Python
Python — название как языка программирования, так и интерпретатора, выполняющего код на языке Python. Утилита-интерпретатор совершенно бесплатна и свободно доступна для скачивания и использования. Можете проверить, не установлен
ли уже в вашей системе Python, с помощью командной строки. В Windows откройте
командную строку и введите py --version. Если выведено примерно следующее,
значит, Python установлен:
C:\Users\Al>py --version
Python 3.9.1

Запуск редактора Mu  

15

В macOS и Linux откройте терминал и введите python3 --version. Если будет выведено примерно следующее, значит, Python установлен:
$ python3 --version
Python 3.9.1

В этой книге используется Python версии 3. При переходе от Python 2 к Python 3
было внесено несколько обратно несовместимых изменений, и для работы описанных здесь программ требуется как минимум Python версии 3.1.1 (выпущена
в 2009 году). Если вы увидите сообщение об ошибке, гласящее, что Python не найден
или версия Python — 2, можете скачать свежий установочный пакет Python для вашей операционной системы с сайта https://python.org/. В случае проблем с установкой
Python дополнительные инструкции можно найти здесь: https://installpython3.com/.

Скачивание и установка редактора Mu
Код Python вы будете вводить в текстовом редакторе или интегрированной среде
разработки (IDE) приложений. Я рекомендую использовать в качестве IDE редактор Mu, если вы новичок; он прост и не отвлекает ваше внимание множеством
расширенных опций.
Откройте сайт https://codewith.mu/ в браузере. В Windows и macOS скачайте
установочный пакет для соответствующей операционной системы и запустите
его, дважды щелкнув на файле. В macOS запуск установочного пакета приведет
к открытию окна, в котором необходимо перетащить пиктограмму Mu в каталог
Applications для продолжения установки. В Ubuntu придется установить Mu в качестве пакета Python. В этом случае откройте новое окно терминала и выполните
команду pip3 install mu-editor для установки и mu-editor — для запуска. Нажмите кнопку Instructions в разделе Python Package страницы загрузки для по­дробных
инструкций.

Запуск редактора Mu
После установки запустите Mu:
в Windows 7 или более поздней версии щелкните на пиктограмме Start
в нижнем левом углу экрана, введите mu в поле поиска и выберите Mu, когда
он появится;
в macOS откройте окно Finder, щелкните на Applications, а затем на mu-editor;
в Ubuntu нажмите Ctrl+Alt+T, чтобы открыть окно терминала, и введите команду python3 -m mu.

16   Введение
При первом запуске Mu появится окно Select Mode (Выберите режим) со следующими вариантами: Adafruit CircuitPython, BBC micro:bit, Pygame Zero и Python 3. Выберите
Python 3. При желании позднее всегда можно изменить режим, нажав кнопку Mode
вверху окна редактора.
Вы сможете вводить код в главном окне Mu, а затем сохранять его, открывать и запускать файлы с помощью кнопок вверху.

Запуск IDLE и других редакторов
Можете использовать какие угодно редакторы для написания кода на Python.
Вместе с Python устанавливается ПО IDLE (Integrated Development and Learning
Environment, интегрированная среда разработки и изучения), которое может служить альтернативным редактором, если по какой-либо причине вам не удалось
установить Mu или заставить его работать. Запустим IDLE.
В Windows 7 или более поздней версии щелкните на пиктограмме Start
в нижнем левом углу экрана, введите idle в поле поиска и выберите IDLE
(Python GUI).
В macOS откройте окно Finder, нажмите ApplicationsPython 3.9IDLE.
В Ubuntu выберите ApplicationsAccessoriesTerminal и введите idle3 (можете
также щелкнуть на Applications вверху экрана, выбрать Programming, а затем
щелкнуть на IDLE 3).
На Raspberry Pi нажмите кнопку меню Raspberry Pi в левом верхнем углу;
щелкните на Programming, а затем встав на Python 3 (IDLE). Можете также выбрать Thonny Python IDE из меню Programming.
Существует еще несколько бесплатных редакторов, с помощью которых можно
вводить и выполнять код Python, например:
Thonny, IDE Python для начинающих по адресу https://thonny.org/;
PyCharm Community Edition — IDE Python, которую используют разработчики-профессионалы, по адресу https://www.jetbrains.com/pycharm/.

Установка модулей Python
Для большинства программ из этой книги требуется только стандартная библиотека
Python, устанавливаемая автоматически вместе с Python. Однако для некоторых
программ требуются сторонние модули, например pyperclip , bext , playsound
и pyttsx3. Их все можно установить сразу, загрузив модуль bigbookpython.

Копирование кода из книги  

17

Что касается редактора Mu, то необходимо установить версию 1.1.0-alpha (или более
позднюю). По состоянию на 2020 год эту версию можно найти вверху страницы
скачивания по адресу https://codewith.mu/en/download в разделе Try the Alpha of the
Next Version of Mu (Попробуйте альфа-версию обновленного Mu). После установки
нажмите значок с шестеренкой в левом нижнем углу окна, чтобы вызвать окно
Mu Administration (Администрирование Mu). Выберите вкладку Third Party Packages (Сторонние пакеты), введите bigbookpython в текстовое поле и нажмите Ok.
В результате этого будут установлены все сторонние модули, используемые программами из книги.
При использовании Visual Studio Code или редактора IDLE откройте редактор
и выполните следующий код Python в интерактивной командной оболочке:
>>> import os, sys
>>> os.system(sys.executable + ' -m pip install --user bigbookpython')
0

Число 0, выводимое после второй инструкции, означает, что все работает должным
образом. В противном случае, если вы увидите сообщение об ошибке или другое
число, попробуйте выполнить следующие команды без опции --user:
>>> import os, sys
>>> os.system(sys.executable + ' -m pip install bigbookpython')
0

При использовании любого редактора можете попробовать выполнить команду
import pyperclip или import bext, чтобы убедиться, что установка прошла успешно.
Если эти команды импорта не возвращают сообщения об ошибке, значит, соответствующие модули установлены правильно и вы сможете запускать использующие
их проекты из книги.

Копирование кода из книги
Программирование — навык, совершенствуемый прежде всего практикой. Не
стоит просто читать код в этой книге и копировать/вставлять его в свой компьютер. Потратьте немного времени, но наберите код в редакторе вручную. Обращайте внимание на номера строк, чтобы случайно не пропустить ничего. Если
вы столкнетесь с ошибками, то воспользуйтесь онлайн-утилитой diff по адресу
https://inventwithpython.com/bigbookpython/diff/, чтобы сравнить свой код с кодом
из книги. Чтобы лучше разобраться в работе программ, попробуйте запустить
их под отладчиком.

18   Введение
После того как исходный код будет введен и запущен несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи относительно возможных
небольших изменений вы можете найти в комментариях, помеченных (!), вдобавок
в каждом проекте есть список идей более крупных изменений.
Далее попробуйте воссоздать программу с самого начала, не глядя на исходный
код в книге. Ваша программа не должна в точности воспроизводить программу из
книги, вы можете придумать собственную версию.
Проработав все программы из книги, вероятно, вы захотите начать создавать собственные. Большинство современных компьютерных игр и прикладных программ
достаточно сложны, их создание требует команды, состоящей из программистов,
архитекторов и графических дизайнеров. Однако многие настольные, карточные
игры и игры на бумаге достаточно просты, чтобы для них можно было разработать
соответствующую программу. Многие из них относятся к категории абстрактных
стратегических игр. Их список можно найти по адресу https://en.wikipedia.org/wiki/
List_of_abstract_strategy_games.

Запуск программ из терминала
Использующие модуль bext программные проекты из книги выводят разноцветный текст. Однако эти цвета не отображаются, если запускать программы из Mu,
IDLE или других редакторов, так что их необходимо запускать из окна терминала
(командной строки). В Windows для этого запустите программу Command Prompt
(Командная строка) из меню Пуск. В macOS запустите Terminal (Терминал) из
Spotlight. В Ubuntu Linux запустите Terminal (Терминал) из Ubuntu Dash или нажмите Ctrl+Alt+T.
Когда откроется окно терминала, перейдите в каталог с вашими файлами .py с помощью команды cd (change directory — «сменить каталог»). Например, если я работаю под Windows и сохранил программы Python в каталог C:\Users\Al, то должен
ввести следующую команду:
C:\>cd C:\Users\Al
C:\Users\Al>

Далее, чтобы запустить программы Python, введите команду python вашаПрограмма.py
в Windows или python3 вашаПрограмма.py в macOS или Linux, заменив вашаПрограм­
ма.py на название соответствующей программы на Python:
C:\Users\Al>python guess.py
Угадайте число, (c) Эл Свейгарт al@inventwithpython.com

Где получить помощь  

19

Я загадал число от 1 до 100.
У вас осталось 10 попыток. Угадывайте.
--сокращено--

Прервать выполнение программы можно из терминала нажатием Ctrl+C, вместо
того чтобы закрывать само окно терминала.

Запуск программ со смартфона или планшета
Работать с ноутбука или стационарного компьютера с полноценной клавиатурой
будет удобнее, поскольку набирать код на клавиатуре телефона или даже планшета
очень утомительно. И хотя не существует официальных интерпретаторов Python
для Android или iOS, есть сайты с интерактивными онлайн-оболочками, которые
можно использовать из браузера. Они подходят и для ноутбуков/стационарных
компьютеров на случай, если вы преподаватель, у которого нет прав на установку
нового программного обеспечения на компьютерах в учебном классе.
Интерпретаторы Python на сайтах https://repl.it/languages/Python3/ и https://www.pythonanywhere.com/ можно свободно использовать в браузере. Эти сайты подходят
для большинства проектов из данной книги, но не для программ, использующих
сторонние модули, например bext, pyperclip, pyttsx3 и playsound, и не для программ, читающих или записывающих файлы с помощью функции open(). Если вы
увидите эти элементы в коде программы, значит, она не будет работать в указанных
онлайн-интерпретаторах Python. Однако большинство программ из книги будет
в них прекрасно работать.

Где получить помощь
Если вы не можете нанять частного преподавателя и у вас нет друга-программиста,
который мог бы ответить на вопросы по программированию, то вам придется самостоятельно искать ответы на них. К счастью, эти вопросы практически наверняка
кто-то уже задавал. Умение найти ответы самостоятельно — важный навык для
любого программиста.
Не расстраивайтесь, если поймете, что постоянно ищете в интернете ответы на
вопросы по программированию. Пока вы учитесь, нет ничего постыдного в том,
чтобы искать что-то в интернете, вместо того чтобы запоминать все нюансы программирования с самого начала. Даже профессиональные разработчики ПО каждый
день ищут что-то в Сети. В этом разделе вы узнаете, как задавать «умные» вопросы
и искать ответы на них в интернете.

20   Введение
Когда программа пытается выполнить недопустимую инструкцию, отображается
сообщение об ошибке: трассировка, описывающая тип произошедшей ошибки и то,
на какой строке кода она произошла. Вот пример программы, в которой произошла
ошибка во время вычисления того, сколько кусков пиццы должен получить каждый:
Traceback (most recent call last):
File "pizza.py", line 5, in
print('Each person gets', (slices / people), ' slices of pizza.')
ZeroDivisionError: division by zero

Из этой трассировки не сразу ясно, что проблема вызвана переменной people,
значение которой равно 0, вследствие чего выражение slices / people приводит
к ошибке деления на ноль. Сообщения об ошибках зачастую настолько коротки,
что даже не являются предложениями. Поскольку программисты сталкиваются
с этими сообщениями постоянно, они играют роль скорее оповещений, а не полноценных пояснений. Если вы встретили какое-то сообщение об ошибке впервые,
то скопируйте его и поищите в интернете, почти наверняка вы найдете подробное
пояснение, что означает эта ошибка и чем она могла быть вызвана.
Если в интернете найти решение проблемы не удалось, то можете выставить свой
вопрос на онлайн-форуме или задать кому-либо по электронной почте. Для большей
эффективности процесса задавайте конкретные, четко сформулированные вопросы.
То есть приведите полный исходный код и полное сообщение об ошибке со всеми
подробностями, расскажите, какие решения проблемы уже пробовали, а также какую операционную систему и версию Python используете. Изложенные на форуме
ответы не только позволят решить вашу проблему, но и помогут в будущем другим
программистам с тем же вопросом, которые найдут ваше сообщение.

Набор кода
Программисту не обязательно уметь быстро печатать на клавиатуре, но лишним
это умение не будет. Многие люди печатают двумя пальцами, в то время как более
быстрый набор может значительно облегчить написание программ. По мере работы
над программами из книги вам будет удобнее смотреть на код, а не на клавиатуру.
Существуют бесплатные сайты для обучения быстрому набору текста, например
https://typingclub.com/ и https://www.typing.com/. Хорошая программа для обучения
набору текста отображает клавиатуру и прозрачные руки на экране, чтобы вы могли
практиковаться, избавляясь от вредной привычки смотреть на клавиатуру в поисках клавиш. Как и любой другой навык, набор — дело привычки, а написание кода
даст вам множество возможностей привыкнуть к набору.
Выполнять различные действия намного быстрее, не перемещая указатель мыши
к пункту меню, позволяют также сочетания горячих клавиш. Они часто записаны

Копирование и вставка  

21

в виде наподобие Ctrl+C, означающем, что нужно нажать одну из двух клавиш Ctrl
и затем, не отпуская ее, нажать клавишу C. Но не нажать клавишу Ctrl, отпустить
ее, а затем нажать клавишу C.
Выучить распространенные сочетания горячих клавиш, например Ctrl+C для копирования или Ctrl+S для сохранения, можно, открыв мышью меню вверху приложения
(в Windows и Linux) или вверху экрана (в macOS). Время, потраченное на изучение
этих сочетаний клавиш, окупится с лихвой.
Другие сочетания горячих клавиш не так очевидны. Например, Alt+Tab в Windows
и Linux и Command+Tab в macOS позволяет переключиться на окно другого приложения. Чтобы выбрать конкретное окно, можно зажать Alt или Command и последовательно нажимать Tab.

Копирование и вставка
Буфер обмена (clipboard) — элемент операционной системы, предназначенный для
временного хранения данных для вставки, которые могут представлять собой текст,
изображения, файлы и другие типы информации, хотя в этом разделе мы будем
говорить только о текстовых данных. При копировании (copying) текста копия выбранного в текущий момент времени текста попадает в буфер обмена. При вставке
(pasting) текст из буфера вставляется в то место, где сейчас находится курсор, как
если бы вы мгновенно набрали его сами. Копирование и вставка текста освобождают
вас от необходимости заново набирать уже имеющийся на вашем компьютере текст,
неважно, одну строку или сотни страниц.
Чтобы копировать и вставить текст, сначала выберите (выделите, highlight)
текст, который будете копировать. Для этого можно нажать основную кнопку
мыши (левую, если мышь предназначена для правшей) и перетащить указатель
мыши по всему выбираемому тексту. Однако зачастую быстрее и точнее будет
нажать кнопку Shift и переместить курсор с помощью сочетания горячих клавиш. Многие ­приложения позволяют мгновенно выделить целое слово путем
двойного щелчка на нем. Можно также выделить целую строку или абзац тройным щелчком.
Следующий шаг: нажать Ctrl+C в Windows или Command+C в macOS, чтобы копировать выделенный текст в буфер обмена. В нем может содержаться только один
элемент, так что копируемый текст замещает находившееся в буфере до этого.
Наконец, передвиньте курсор туда, куда нужно вставить текст, и нажмите Ctrl+V
в Windows или Command+V в macOS. Вставлять текст можно столько раз, сколько нужно; он остается в буфере обмена до тех пор, пока вы не скопируете новый
текст.

22   Введение

Поиск и замена текста
Дэн Рассел (Dan Russell), поисковый антрополог в Google, в статье 2011 года
в Atlantic пояснил: при изучении привычек использования компьютеров людьми
оказалось, что 90 % из них не знали, что можно нажать Ctrl+F (в Windows и Linux)
или Command+F (в macOS) для поиска слов в приложениях. Это исключительно
удобная возможность не только в редакторах кода, но и в текстовых редакторах,
браузерах, приложениях электронных таблиц и практически во всех прочих программах, отображающих текст. Вы можете нажать Ctrl+F — и появится окно Find
(Найти), куда можно ввести слово для поиска в программе. Нажатие клавиши F3
обычно позволяет повторить поиск и найти следующее вхождение слова. Эта возможность экономит колоссальное количество времени по сравнению с поиском
слова с помощью просмотра документа вручную.
В редакторах также есть возможность поиска и замены текста, с которой обычно
связано сочетание клавиш Ctrl+H (Command+H). Она позволяет находить вхождения
какого-либо фрагмента текста и заменять его другим. Это очень удобно, например, для переименования переменной или функции. Однако возможность поиска
и замены текста следует использовать с осторожностью, чтобы не заменить текст,
случайно совпавший с критерием поиска.

Отладчик
Отладчик — утилита, выполняющая программы построчно, с возможностью просмотра текущего состояния переменных программы. Это ценный инструмент для
поиска программных ошибок. В данном разделе я расскажу о возможностях отладчика редактора Mu. Не волнуйтесь: возможности всех отладчиков одинаковы,
даже если интерфейсы пользователя различаются.
Чтобы запустить программу в отладчике, воспользуйтесь пунктом меню Debug
(Отладка) в своей IDE вместо пункта меню Run (Запуск). Отладчик запустится
в приостановленном состоянии на первой строке программы. У всех отладчиков есть
кнопки Continue (Продолжить), Step In (Шаг с заходом), Step Over (Шаг с обходом),
Step Out (Шаг с выходом) и Stop (Останов).
При нажатии кнопки Continue (Продолжить) программа выполняется как обычно,
до тех пор пока не завершится или не достигнет точки останова (о них я расскажу
позже в этом разделе). Если вы закончили отладку и хотите, чтобы программа далее
выполнялась обычным образом, то нажмите Continue (Продолжить).
При нажатии кнопки Step In (Шаг с заходом) отладчик выполняет следующую
строку кода, после чего останавливается. Если следующая строка кода представляет

Отладчик  

23

собой вызов функции, то отладчик «заходит» в эту функцию и переходит к первой
строке ее кода.
При нажатии кнопки Step Over (Шаг с обходом) отладчик выполняет следующую
строку кода аналогично кнопке Step In (Шаг с заходом). Но если следующая строка
кода представляет собой вызов функции, то отладчик «обходит» код этой функции.
Функция выполняется с обычной скоростью, и отладчик приостанавливается после возврата из ее вызова. Кнопку Step Over (Шаг с обходом) используют чаще, чем
кнопку Step In (Шаг с заходом).
При нажатии кнопки Step Out (Шаг с выходом) отладчик выполняет строки кода
с обычной скоростью, пока не происходит возврат из текущей функции. Если вы
«зашли» в вызов функции с помощью кнопки Step In (Шаг с заходом) и просто
хотите выполнить оставшиеся инструкции, пока не выйдете обратно, то нажмите
кнопку Step Out (Шаг с выходом), чтобы отладчик «вышел» из текущего вызова
функции.
Если вы хотите полностью завершить сеанс отладки и не продолжать выполнение
остатка программы, то нажмите кнопку Stop (Останов). Она немедленно завершает
выполнение программы.
Можно установить точку останова (breakpoint) на конкретной строке, при этом
программа будет выполняться с обычной скоростью, до тех пор пока не достигнет
данной строки. Затем отладчик остановит выполнение программы, чтобы вы могли
изучить значения переменных и продолжить пошаговое выполнение отдельных
строк кода. В большинстве IDE установить точку останова можно с помощью
двойного щелчка на номере строки в левой части окна.
В любом отладчике где-нибудь в окне отладки отображаются значения, хранящиеся в текущий момент в переменных программы. Впрочем, один из распространенных методов отладки программ — отладка с выводом значений (print debugging).
Метод заключается в добавлении вызовов print() для отображения значений
переменных и в повторном запуске программы. Этот подход к отладке прост
и удобен, однако часто требует больше времени, чем использование отладчика.
При отладке с выводом значений необходимо добавлять вызовы print(), перезапускать программу, а затем удалять эти вызовы. Однако после перезапуска программы часто оказывается, что нужно добавить дополнительные вызовы print(),
чтобы узнать значения других переменных. А значит, приходится перезапускать
программу еще раз, что может означать еще один цикл добавления вызовов
print() и т. д. Кроме того, легко можно упустить какие-либо из этих вызовов,
что потребует дополнительного цикла их удаления. Отладка с выводом значений
удобна для простых ошибок, но в конечном счете использование настоящего отладчика экономит время.

24   Введение

Резюме
Программирование — интересный и творческий навык. Хотите ли вы овладеть
основами синтаксиса Python или просто посмотреть на настоящие программы на
Python — проекты в этой книге породят новые идеи о том, чего можно добиться
с помощью всего нескольких страниц кода.
Лучший способ работы с этими программами — не просто читать их код и копировать его. Потратьте немного времени, но наберите код в редакторе вручную, чтобы
выработать «мышечную память». Помимо всего прочего, это немного замедлит вас,
так что вы поневоле будете внимательнее обдумывать каждую из строк кода в ходе
их набора, а не просто пробегать по ним глазами. Старайтесь искать в интернете все
незнакомые инструкции или пробовать выполнять их в интерактивной командной
оболочке.
Наконец, не поленитесь повторить программу с нуля, а затем модифицировать
ее, добавив что-то свое. Благодаря этим упражнениям вы освоите концепции, на
основе которых создаются настоящие работающие программы, а главное, хорошо
проведете время!

От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

1
Бейглз

В дедуктивной логической игре «Бейглз» необходимо по
подсказкам угадать секретное число из трех цифр. В ответ на ваши попытки угадать игра выдает одну из трех
подсказок: Pico, если вы угадали правильную цифру на
неправильном месте, Fermi, если в вашей догадке есть правильная цифра на правильном месте, и Bagels, если в догадке
не содержится правильных цифр. На угадывание секретного
числа у вас десять попыток.

Программа в действии
Результат выполнения bagels.py выглядит следующим образом:
Bagels, a deductive logic game.
By Al Sweigart al@inventwithpython.com
I am thinking of a 3-digit number. Try to guess what it is.
Here are some clues:
When I say:
That means:
Pico
One digit is correct but in the wrong position.
Fermi
One digit is correct and in the right position.
Bagels
No digit is correct.
I have thought up a number.

26   Проект 1. Бейглз
You have 10 guesses to get it.
Guess #1:
> 123
Pico
Guess #2:
> 456
Bagels
Guess #3:
> 178
Pico Pico
--сокращено-Guess #7:
> 791
Fermi Fermi
Guess #8:
> 701
You got it!
Do you want to play again? (yes or no)
> no
Thanks for playing!

Описание работы
Обратите внимание: в данной программе используются не целочисленные значения,
а строковые, содержащие цифры. Например, '426' — вовсе не то же значение, что
и 426. Это необходимо, поскольку мы производим строковое сравнение с секретным числом, а не математическую операцию. Учтите, что '0' может быть значащей
цифрой: строковое значение '026' отличается от '26', в то время как целочисленное
значение 026 то же самое, что 26.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.

"""Бейглз, (c) Эл Свейгарт al@inventwithpython.com
Дедуктивная логическая игра на угадывание числа по подсказкам.
Код размещен на https://nostarch.com/big-book-small-python-projects
Один из вариантов этой игры приведен в книге Invent Your Own
Computer Games with Python на https://nostarch.com/inventwithpython
Теги: короткая, игра, головоломка"""
import random
NUM_DIGITS = 3 # (!) Попробуйте задать эту константу равной 1 или 10.
MAX_GUESSES = 10 # (!) Попробуйте задать эту константу равной 1 или 100.

def main():
print('''Bagels, a deductive logic game.
By Al Sweigart al@inventwithpython.com

Описаниеработы  

18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.

I am thinking of a {}-digit number with no repeated digits.
Try to guess what it is. Here are some clues:
When I say:
That means:
Pico
One digit is correct but in the wrong position.
Fermi
One digit is correct and in the right position.
Bagels
No digit is correct.
For example, if the secret number was 248 and your guess was 843, the
clues would be Fermi Pico.'''.format(NUM_DIGITS))
while True: # Основной цикл игры.
# Переменная, в которой хранится секретное число, которое
secretNum = getSecretNum() # должен угадать игрок
print('I have thought up a number.')
print(' You have {} guesses to get it.'.format(MAX_GUESSES))
numGuesses = 1
while numGuesses MAX_GUESSES:
print('You ran out of guesses.')
print('The answer was {}.'.format(secretNum))
# Спрашиваем игрока, хочет ли он сыграть еще раз.
print('Do you want to play again? (yes or no)')
if not input('> ').lower().startswith('y'):
break
print('Thanks for playing!')
def getSecretNum():
"""Возвращает строку из NUM_DIGITS уникальных случайных цифр."""
numbers = list('0123456789') # Создает список цифр от 0 до 9.
random.shuffle(numbers) # Перетасовываем их случайным образом.
# Берем первые NUM_DIGITS цифр списка для нашего секретного числа:
secretNum = ''
for i in range(NUM_DIGITS):
secretNum += str(numbers[i])

27

28   Проект 1. Бейглз
68.
return secretNum
69.
70.
71. def getClues(guess, secretNum):
72.
"""Возвращает строку с подсказками pico, fermi и bagels
73.
для полученной на входе пары из догадки и секретного числа."""
74.
if guess == secretNum:
75.
return 'You got it!'
76.
77.
clues = []
78.
79.
for i in range(len(guess)):
80.
if guess[i] == secretNum[i]:
81.
# Правильная цифра на правильном месте.
82.
clues.append('Fermi')
83.
elif guess[i] in secretNum:
84.
# Правильная цифра на неправильном месте.
85.
clues.append('Pico')
86.
if len(clues) == 0:
87.
return 'Bagels' # Правильных цифр нет вообще.
88.
else:
89.
# Сортируем подсказки в алфавитном порядке, чтобы их исходный
90.
# порядок ничего не выдавал.
91.
clues.sort()
92.
# Склеиваем список подсказок в одно строковое значение.
93.
return ' '.join(clues)
94.
95.
96. # Если программа не импортируется, а запускается, производим запуск:
97. if __name__ == '__main__':
98.
main()

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Идеи относительно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
изменить количество цифр в секретном числе, изменив константу NUM_DIGITS;
изменить количество попыток угадывания, доступных игроку, изменив константу MAX_GUESSES;
создать версию, в которой в секретном числе могут содержаться не только
цифры, но и буквы.

Исследование программы  

29

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и снова запустите программу, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если изменить константу NUM_DIGITS?
2. Что будет, если изменить константу MAX_GUESSES?
3. Что будет, если задать значение NUM_DIGITS больше 10?
4. Что будет, если secretNum = getSecretNum() в строке 30 заменить на secretNum =
'123'?
5. Какое сообщение об ошибке вы получите, если удалите или закомментируете
numGuesses = 1 в строке 34?
6. Что будет, если вы удалите или закомментируете random.shuffle(numbers)
в строке 62?
7. Что будет, если вы удалите или закомментируете if guess == secretNum:
в строке 74 и return 'You got it!' в строке 75?
8. Что будет, если вы закомментируете numGuesses += 1 в строке 44?

2
Парадокс дней рождения

Парадокс дней рождения, также известный как задача
о днях рождения, заключается в удивительно высокой
вероятности того, что у двух человек совпадает день рождения даже в относительно небольшой группе людей.
В группе из 70 человек вероятность совпадения дней рождения у двух людей составляет 99,9 %. Но даже в группе всего
лишь из 23 человек вероятность совпадения дней рождения
составляет 50 %. Приведенная программа производит несколько вероятностных
экспериментов, чтобы определить процентные соотношения для групп различного
размера. Подобные эксперименты с определением возможных исходов с помощью
множества случайных испытаний называются экспериментами Монте-Карло.
Узнать больше о парадоксе дней рождения можно в соответствующей статье «Википедии»: https://ru.wikipedia.org/wiki/Парадокс_дней_рождения.

Программа в действии
Результат выполнения birthdayparadox.py выглядит следующим образом:
Birthday Paradox, by Al Sweigart al@inventwithpython.com
--сокращено-How many birthdays shall I generate? (Max 100)
> 23

Описание работы  

31

Here are 23 birthdays:
Oct 9, Sep 1, May 28, Jul 29, Feb 17, Jan 8, Aug 18, Feb 19, Dec 1, Jan 22,
May 16, Sep 25, Oct 6, May 6, May 26, Oct 11, Dec 19, Jun 28, Jul 29, Dec 6,
Nov 26, Aug 18, Mar 18
In this simulation, multiple people have a birthday on Jul 29
Generating 23 random birthdays 100,000 times...
Press Enter to begin...
Let's run another 100,000 simulations.
0 simulations run...
10000 simulations run...
--сокращено-90000 simulations run...
100000 simulations run.
Out of 100,000 simulations of 23 people, there was a
matching birthday in that group 50955 times. This means
that 23 people have a 50.95 % chance of
having a matching birthday in their group.
That's probably more than you would think!

Описание работы
Выполнение 100 000 операций имитационного моделирования займет немало
времени, поэтому в строках 94 и 95 выводятся сообщения о каждых 10 000 произведенных операций. Такая обратная связь демонстрирует пользователю, что
программа не зависла. Обратите внимание на знаки подчеркивания в некоторых
числах, например, 10_000 в строке 95 и 100_000 в строках 93 и 103. Никакого особого
смысла у этих знаков подчеркивания нет, но Python позволяет их указывать, чтобы
программисты могли упростить чтение чисел в их программах. Другими словами,
число «сто тысяч» в форме 100_000 понятнее, чем 100000.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.

"""Имитационное моделирование парадокса дней рождения, (c) Эл Свейгарт
al@inventwithpython.com Изучаем неожиданные вероятности из "Парадокса
дней рождения". Больше информации — в статье
https://ru.wikipedia.org/wiki/Парадокс_дней_рождения
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, математическая, имитационное моделирование"""
import datetime, random
def getBirthdays(numberOfBirthdays):
""" Возвращаем список объектов дат для случайных дней рождения."""
birthdays = []
for i in range(numberOfBirthdays):
# Год в нашем имитационном моделировании роли не играет, лишь
# бы в объектах дней рождения он был одинаков.
startOfYear = datetime.date(2001, 1, 1)

32   Проект 2. Парадокс дней рождения
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.

# Получаем случайный день года:
randomNumberOfDays = datetime.timedelta(random.randint(0, 364))
birthday = startOfYear + randomNumberOfDays
birthdays.append(birthday)
return birthdays
def getMatch(birthdays):
""" Возвращаем объект даты дня рождения, встречающегося
несколько раз в списке дней рождения."""
if len(birthdays) == len(set(birthdays)):
return None # Все дни рождения различны, возвращаем None.
# Сравниваем все дни рождения друг с другом попарно:
for a, birthdayA in enumerate(birthdays):
for b, birthdayB in enumerate(birthdays[a + 1 :]):
if birthdayA == birthdayB:
return birthdayA # Возвращаем найденные соответствия.
# Отображаем вводную информацию:
print('''Birthday Paradox, by Al Sweigart al@inventwithpython.com
The Birthday Paradox shows us that in a group of N people, the odds
that two of them have matching birthdays is surprisingly large.
This program does a Monte Carlo simulation (that is, repeated random
simulations) to explore this concept.
(It's not actually a paradox, it's just a surprising result.)
''')
# Создаем кортеж названий месяцев по порядку:
MONTHS = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
while True: # Запрашиваем, пока пользователь не введет допустимое значение.
print('How many birthdays shall I generate? (Max 100)')
response = input('> ')
if response.isdecimal() and (0 < int(response) Hello!
Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!He
lo!Hello!Hello
l !He lo e
llo!Hello!Hello!Hello!Hello!He
llo!Hello!Hello!Hello He lo H l !Hello!Hello!Hello!Hello!Hello H
el
lo!Hello!Hello!He
lo!Hello!Hello!Hello!Hello!Hel
o!Hello!Hello
lo e lo!H ll !Hello!Hello!H l
!Hello!He
llo!Hel
Hello!Hello!Hell ! e
Hello!He
ello!Hello!Hello!Hello!Hell H
l
H llo! ell
ello!Hello!Hell !Hello el o
lo!H l
ello!Hello!Hell
ell !He o
!Hello
llo!Hello!Hel
el
He o
!Hello!H
lo!Hello!Hell
l !H llo
ello!Hel
Hello!He
H llo Hell
ello!Hell
ello!H l
Hell !H l o!
ello!Hell
ello!H l o
o!H l
H
lo!Hel
ello! el
o!Hel
H
lo!He
llo! e
llo!Hell
llo!H
llo!
llo!Hello
llo!
ll
lo!Hell
e
llo
l
e
ll
l
H
Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!Hello!He

Описание работы
Вместо того чтобы по отдельности вводить все символы шаблона карты мира,
вы можете скопировать его целиком из https://inventwithpython.com/bitmapworld.txt
и вставить. Строки из 68 точек вверху и внизу шаблона служат «линейками» для
упрощения правильного выравнивания. Однако программа будет работать и при
наличии каких-либо опечаток в этом шаблоне.
Вызываемый в строке 43 метод bitmap.splitlines() возвращает список отдельных
строк из многострочного строкового значения bitmap. Благодаря использованию
многострочного строкового значения можно легко заменить нашу битовую карту
любым другим шаблоном. Программа заполнит все непробельные символы в шаб­
лоне, так что нет разницы, какие это символы: звездочки, точки или любые другие
символы.
Код message[i % len(message)] повторяет текст в message. Когда i, увеличиваясь
с 0, доходит до числа len(message), выражение i % len(message) снова становится

Описание работы  

37

равно 0. В результате этого выражение message[i % len(message)] повторяет символы в message при росте i.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.

"""Сообщение в виде битовой карты, (c) Эл Свейгарт al@inventwithpython.com
Отображает текстовое сообщение в соответствии с указанной битовой картой.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, графика"""
import sys
# (!) Попробуйте заменить это многострочное строковое значение на любое
# другое изображение
# Вверху и внизу строки расположено 68 точек:
# (Можете также скопировать и вставить это строковое значение из
# из https://inventwithpython.com/bitmapworld.txt)
bitmap = """
....................................................................
**************
* *** ** *
******************************
********************* ** ** * * ****************************** *
**
*****************
******************************
*************
** * **** ** ************** *
*********
*******
**************** * *
********
*************************** *
*
* **** ***
*************** ****** ** *
**** *
***************
*** *** *
******
*************
**
** *
********
*************
* ** ***
********
********
* *** ****
*********
****** *
**** ** * **
*********
****** * *
*** *
*
******
***** **
*****
*
*****
**** *
********
*****
****
*********
****
**
*******
*
***
*
*
**
*
*
...................................................................."""
print('Bitmap Message, by Al Sweigart al@inventwithpython.com')
print('Enter the message to display with the bitmap.')
message = input('> ')
if message == '':
sys.exit()
# Проходим в цикле по всем строкам битовой карты:
for line in bitmap.splitlines():
# Проходим в цикле по всем символам строки:
for i, bit in enumerate(line):
if bit == ' ':
# Выводим пустое пространство согласно пробелу в битовой карте:
print(' ', end='')

38   Проект 3. Сообщение в виде битовой карты
49.
50.
51.
52.

else:
# Выводим символ сообщения:
print(message[i % len(message)], end='')
print() # Выводим символ новой строки.

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Можете, например, изменить
строковое значение в bitmap и создать совершенно другие шаблоны.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если игрок введет в качестве сообщения пустую строку?
2. Играет ли какую-либо роль то, какие именно не пробельные символы указаны
в переменной bitmap?
3. Каков смысл переменной i, созданной в строке 45?
4. Какая программная ошибка возникнет, если удалить или закомментировать
print() в строке 52?

4
Блек-джек

Блек-джек, известный также как «двадцать одно», —
карточная игра, в которой игроки пытаются набрать количество очков, как можно более близкое к 21, но не
больше. В данной программе используются изображения, составленные из текстовых символов, так называемая
ASCII-графика. ASCII (American Standard Code for Information
Interchange, американский стандартный код обмена информацией) представляет собой таблицу соответствий текстовых символов числовым кодам,
она применялась до того, как ее заменила кодировка Unicode1. Игральные карты
в этой программе представляют собой пример ASCII-графики:
___
___
|A | |10 |
| ♣ | | ♦ |
|__A| |_10|

Прочие правила и историю этой карточной игры вы можете найти в статье «Википедии»: https://ru.wikipedia.org/wiki/Блэкджек.

1

Вообще-то нельзя сказать, что Unicode заменила ASCII, ASCII используется и по сей
день. — Примеч. пер.

40   Проект 4. Блек-джек

Программа в действии
Результат выполнения blackjack.py выглядит следующим образом:
Blackjack, by Al Sweigart al@inventwithpython.com
Rules:
Try to get as close to 21 without going over.
Kings, Queens, and Jacks are worth 10 points.
Aces are worth 1 or 11 points.
Cards 2 through 10 are worth their face value.
(H)it to take another card.
(S)tand to stop taking cards.
On your first play, you can (D)ouble down to increase your bet
but must hit exactly one more time before standing.
In case of a tie, the bet is returned to the player.
The dealer stops hitting at 17.
Money: 5000
How much do you bet? (1-5000, or QUIT)
> 400
Bet: 400
DEALER: ???
___
___
|## | |2 |
|###| | ♥ |
|_##| |__2|
PLAYER: 17
___
___
|K | |7 |
| ♠ | | ♦ |
|__K| |__7|
(H)it, (S)tand, (D)ouble down
> h
You drew a 4 of ♦.
--сокращено-DEALER: 18
___
___
___
|K | |2 | |6 |
| ♦ | | ♥ | | ♠ |
|__K| |__2| |__6|
PLAYER: 21
___
___
___
|K | |7 | |4 |
| ♠ | | ♦ | | ♦ |
|__K| |__7| |__4|
You won $400!
--сокращено--

Описание работы  

41

Описание работы
Символов для мастей карт на клавиатуре нет, поэтому нам приходится вызывать
chr() для их отображения. Передаваемое в chr() целое число называется кодом
символа (code point) кодировки Unicode, оно представляет собой уникальное число,
идентифицирующее символ в соответствии со стандартом Unicode. Unicode часто
понимают неправильно. Прекрасное введение в Unicode — доклад Неда Бачхелдера (Ned Batchelder) Pragmatic Unicode, or How Do I Stop the Pain?, сделанный
на конференции PyCon US 2012. Вы можете найти его по адресу https://youtu.be/
sgHbC6udIqc/. В приложении Б приведен полный список символов Unicode, которые
можно использовать в программах Python.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.

"""Блек-джек, (c) Эл Свейгарт al@inventwithpython.com
Классическая карточная игра, известная также как "двадцать одно".
(В этой версии нет страхования или разбиения.)
Больше информации — в статье https://ru.wikipedia.org/wiki/Блэкджек
Код размещен на сайте https://nostarch.com/big-book-small-python-projects
Теги: большая, игра, карточная игра"""
import random, sys
# Задаем значения констант:
HEARTS
= chr(9829) # Символ 9829 — '♥'.
DIAMONDS = chr(9830) # Символ 9830 — '♦'.
SPADES
= chr(9824) # Символ 9824 — '♠'.
CLUBS
= chr(9827) # Символ 9827 — '♣'.
# (Список кодов chr можно найти в https://inventwithpython.com/charactermap)
BACKSIDE = 'backside'
def main():
print('''Blackjack, by Al Sweigart al@inventwithpython.com
Rules:
Try to get as close to 21 without going over.
Kings, Queens, and Jacks are worth 10 points.
Aces are worth 1 or 11 points.
Cards 2 through 10 are worth their face value.
(H)it to take another card.
(S)tand to stop taking cards.
On your first play, you can (D)ouble down to increase your bet
but must hit exactly one more time before standing.
In case of a tie, the bet is returned to the player.
The dealer stops hitting at 17.''')
money = 5000
while True: # Основной цикл игры.
# Проверяем, не закончились ли у игрока деньги:
if money 21:
break
# Получаем ход игрока: H, S или D:
move = getMove(playerHand, money - bet)
# Обработка действий игрока:
if move == 'D':
# Игрок удваивает, он может увеличить ставку:
additionalBet = getBet(min(bet, (money - bet)))
bet += additionalBet
print('Bet increased to {}.'.format(bet))
print('Bet:', bet)
if move in ('H', 'D'):
# "Еще" или "удваиваю": игрок берет еще одну карту.
newCard = deck.pop()
rank, suit = newCard
print('You drew a {} of {}.'.format(rank, suit))
playerHand.append(newCard)
if getHandValue(playerHand) > 21:
# Перебор у игрока:
continue
if move in ('S', 'D'):
# "Хватит" или "удваиваю": переход хода к следующему игроку
break

Описание работы  

88.
# Обработка действий дилера:
89.
if getHandValue(playerHand) 21:
97.
break # Перебор у дилера.
98.
input('Press Enter to continue...')
99.
print('\n\n')
100.
101.
# Отображает итоговые карты на руках:
102.
displayHands(playerHand, dealerHand, True)
103.
104.
playerValue = getHandValue(playerHand)
105.
dealerValue = getHandValue(dealerHand)
106.
# Проверяем, игрок выиграл, проиграл или сыграл вничью:
107.
if dealerValue > 21:
108.
print('Dealer busts! You win ${}!'.format(bet))
109.
money += bet
110.
elif (playerValue > 21) or (playerValue < dealerValue):
111.
print('You lost!')
112.
money -= bet
113.
elif playerValue > dealerValue:
114.
print('You won ${}!'.format(bet))
115.
money += bet
116.
elif playerValue == dealerValue:
117.
print('It\'s a tie, the bet is returned to you.')
118.
119.
input('Press Enter to continue...')
120.
print('\n\n')
121.
122. def getBet(maxBet):
123.
"""Спрашиваем у игрока, сколько он ставит на этот раунд."""
124.
while True: # Продолжаем спрашивать, пока не будет введено
125.
# допустимое значение.
126.
print('How much do you bet? (1-{}, or QUIT)'.format(maxBet))
127.
bet = input('> ').upper().strip()
128.
if bet == 'QUIT':
129.
print('Thanks for playing!')
130.
sys.exit()
131.
132.
if not bet.isdecimal():
133.
continue # Если игрок не ответил — спрашиваем снова.
134.
135.
bet = int(bet)
136.
if 1 e
Please enter the key (0 to 25) to use.
> 4
Enter the message to encrypt.
> Meet me by the rose bushes tonight.
QIIX QI FC XLI VSWI FYWLIW XSRMKLX.
Full encrypted text copied to clipboard.
Caesar Cipher, by Al Sweigart al@inventwithpython.com
Do you want to (e)ncrypt or (d)ecrypt?
> d
Please enter the key (0 to 26) to use.
> 4
Enter the message to decrypt.
> QIIX QI FC XLI VSWI FYWLIW XSRMKLX.
MEET ME BY THE ROSE BUSHES TONIGHT.
Full decrypted text copied to clipboard.

Описание работы
Подобно большинству программ шифрования, шифр Цезаря преобразует буквы
в числа, производит над ними определенные математические операции и преобразует их обратно в буквы текста. В контексте шифрования мы будем называть элементы
текста символами. Символы могут быть буквами, цифрами и знаками препинания,
каждому из которых соответствует уникальное целочисленное значение. В случае
программы для шифра Цезаря все символы представляют собой буквы, а целочисленные значения — их позиции в строке SYMBOLS: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.

"""Шифр Цезаря, (c) Эл Свейгарт al@inventwithpython.com
Шифр Цезаря — шифр сдвига, в котором шифрование и дешифровка букв
производятся путем сложения и вычитания соответствующих чисел.
Дополнительная информация: https://ru.wikipedia.org/wiki/Шифр_Цезаря
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, для начинающих, криптография, математическая"""
try:

import pyperclip # pyperclip копирует текст в буфер обмена.
except ImportError:
pass # Если библиотека pyperclip не установлена, ничего не делаем.
# Все возможные символы для шифрования/дешифровки:
# (!) Можете добавить также символы для цифр и знаков препинания,
# чтобы шифровать и их.

Описание работы  

16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.

55

SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
print('Caesar Cipher, by Al Sweigart al@inventwithpython.com')
print('The Caesar cipher encrypts letters by shifting them over by a')
print('key number. For example, a key of 2 means the letter A is')
print('encrypted into C, the letter B encrypted into D, and so on.')
print()
# Спрашиваем у пользователя, хочет ли он шифровать или расшифровывать:
while True: # Повторяем вопрос, пока пользователь не введет e или d.
print('Do you want to (e)ncrypt or (d)ecrypt?')
response = input('> ').lower()
if response.startswith('e'):
mode = 'encrypt'
break
elif response.startswith('d'):
mode = 'decrypt'
break
print('Please enter the letter e or d.')
# Просим пользователя ввести ключ шифрования:
while True: # Повторяем вопрос, пока пользователь не введет допустимый ключ.
maxKey = len(SYMBOLS) - 1
print('Please enter the key (0 to {}) to use.'.format(maxKey))
response = input('> ').upper()
if not response.isdecimal():
continue
if 0 = len(SYMBOLS):
num = num - len(SYMBOLS)
elif num < 0:
num = num + len(SYMBOLS)
# Добавляем соответствующий числу зашифрованный/расшифрованный символ
# в translated:
translated = translated + SYMBOLS[num]
else:
# Просто добавляем символ без шифрования/расшифровки:
translated = translated + symbol
# Выводим зашифрованную/расшифрованную строку на экран:
print(translated)
try:

pyperclip.copy(translated)
print('Full {}ed text copied to clipboard.'.format(mode))
except:
pass # Если pyperclip не установлена, ничего не делаем.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Идеи относительно возможных
небольших изменений вы найдете в комментариях, помеченных (!). При желании
можете расширить спектр шифруемых символов, добавив дополнительные в строковое значение SYMBOLS.

Исследование программы
Попробуйте ответить на следующие вопросы. Поэкспериментируйте с изменениями
кода и запустите программу снова, чтобы увидеть, как они повлияют на ее работу.
1. Что будет, если SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' в строке 16 заменить
на SYMBOLS = 'ABC'?
2. Что будет, если зашифровать сообщение с ключом 0?
3. Какое сообщение об ошибке вы получите, если удалите или закомментируете
translated = '' в строке 56?
4. Какое сообщение об ошибке вы получите, если удалите или закомментируете
key = int(response) в строке 45?
5. Что будет, если translated = translated + SYMBOLS[num] в строке 76 заменить
на translated = translated + symbol?

7
Взлом шифра Цезаря

Эта программа способна взламывать сообщения, зашифрованные с помощью шифра Цезаря из проекта 6, даже
если ключ неизвестен. Существует всего 26 возможных
ключей для шифра Цезаря, так что компьютер может легко
перебрать все возможные расшифровки и отобразить пользователю результаты. В криптографии подобная методика называется атакой методом подбора (brute-force attack). Если вам
интересно узнать больше о шифрах и их взломе, то можете прочитать мою книгу
Cracking Codes with Python.

Программа в действии
Результат выполнения caesarhacker.py выглядит следующим образом:
Caesar Cipher Hacker, by Al Sweigart al@inventwithpython.com
Enter the encrypted Caesar cipher message to hack.
> QIIX QI FC XLI VSWI FYWLIW XSRMKLX.
Key #0: QIIX QI FC XLI VSWI FYWLIW XSRMKLX.
Key #1: PHHW PH EB WKH URVH EXVKHV WRQLJKW.
Key #2: OGGV OG DA VJG TQUG DWUJGU VQPKIJV.
Key #3: NFFU NF CZ UIF SPTF CVTIFT UPOJHIU.
Key #4: MEET ME BY THE ROSE BUSHES TONIGHT.
Key #5: LDDS LD AX SGD QNRD ATRGDR SNMHFGS.
Key #6: KCCR KC ZW RFC PMQC ZSQFCQ RMLGEFR.
--сокращено--

58   Проект 7. Взлом шифра Цезаря

Описание работы
Обратите внимание, что строки с 20-й по 36-ю данной программы практически
идентичны строкам с 55-й по 78-ю программы для шифра Цезаря. В программе
взлома шифра Цезаря реализован тот же код дешифрования, только в цикле for,
в целях выполнения этого кода для всех возможных ключей.
К сожалению, наша программа взлома не настолько совершенна, чтобы определить,
какой из ключей — нужный, поэтому выведенные результаты должен прочитать
человек и определить, какая из расшифровок представляет собой исходное сообщение на английском (или каком-либо другом письменном языке, который был
зашифрован). В главе 11 книги Cracking Codes with Python подробнее рассказывается,
как написать код на Python для выявления сообщений на английском.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.

"""Взлом шифра Цезаря, (c) Эл Свейгарт al@inventwithpython.com
Данная программа взламывает сообщения, зашифрованные шифром Цезаря,
путем прямого перебора всех возможных ключей.
Больше информации — по адресу
https://ru.wikipedia.org/wiki/Шифр_Цезаря#Взлом_шифра
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, криптография, математическая"""
print('Caesar Cipher Hacker, by Al Sweigart al@inventwithpython.com')
# Просим пользователя ввести сообщение для взлома:
print('Enter the encrypted Caesar cipher message to hack.')
message = input('> ')
# Все возможные символы для шифрования/дешифровки:
# (должно совпадать с набором SYMBOLS, использовавшимся при шифровании)
SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
for key in range(len(SYMBOLS)):
translated = ''

# Проходим в цикле по всем возможным ключам.

# Расшифровываем каждый символ в сообщении:
for symbol in message:
if symbol in SYMBOLS:
num = SYMBOLS.find(symbol) # Получаем числовое значение символа.
num = num - key # Расшифровываем числовое значение.
# Выполняем переход по кругу, если число меньше 0:
if num < 0:
num = num + len(SYMBOLS)
# Добавляем соответствующий числу расшифрованный символ
# в translated:
translated = translated + SYMBOLS[num]
else:
# Просто добавляем символ без расшифровки:

Исследование программы  

36.
37.
38.
39.

59

translated = translated + symbol
# Выводим проверяемый ключ вместе с расшифрованным на его основе текстом:
print('Key #{}: {}'.format(key, translated))

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Учтите, что строковое значение
в переменной SYMBOLS должно совпадать с переменной SYMBOLS в сгенерировавшей
исходный текст программе шифрования.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Какое сообщение об ошибке вы получите, если удалите или закомментируете
translated = '' в строке 20?
2. Что будет, если translated = translated + SYMBOLS[num] в строке 33 заменить
на translated = translated + symbol?
3. Что будет, если ввести в программу взлома шифра Цезаря незашифрованный
текст?

8
Генерация календарей

Эта программа генерирует подходящие для распечатки
текстовые файлы с календарями на выбранный месяц
(в выбранном году). Даты и календари — каверзный вопрос в программировании, поскольку существует очень
много различных правил для определения количества дней
в месяце, високосных годов, а также того, каким днем недели
является конкретная дата. К счастью, модуль datetime решает все
эти проблемы вместо вас. Данная программа генерирует многострочное строковое
значение, содержащее страницу календаря на месяц.

Программа в действии
Результат выполнения calendarmaker.py выглядит следующим образом:
Calendar Maker, by Al Sweigart al@inventwithpython.com
Enter the year for the calendar:
> 2029
Enter the month for the calendar, 1-12:
> 12

Описание работы  

61

December 2029
...Sunday.....Monday....Tuesday...Wednesday...Thursday....Friday....Saturday..
+----------+----------+----------+----------+----------+----------+----------+
|25
|26
|27
|28
|29
|30
| 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+----------+----------+----------+----------+----------+----------+----------+
| 2
| 3
| 4
| 5
| 6
| 7
| 8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+----------+----------+----------+----------+----------+----------+----------+
| 9
|10
|11
|12
|13
|14
|15
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+----------+----------+----------+----------+----------+----------+----------+
|16
|17
|18
|19
|20
|21
|22
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+----------+----------+----------+----------+----------+----------+----------+
|23
|24
|25
|26
|27
|28
|29
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+----------+----------+----------+----------+----------+----------+----------+
|30
|31
| 1
| 2
| 3
| 4
| 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+----------+----------+----------+----------+----------+----------+----------+
Saved to calendar_2029_12.txt

Описание работы
Обратите внимание, что функция getCalendarFor() возвращает гигантское многострочное строковое значение с календарем на заданный месяц в заданном году.
Упомянутое значение в этой функции хранится в переменной calText, в которую
постепенно добавляются линии, пробелы и даты. Чтобы можно было отслеживать даты, переменная currentDate содержит объект datetime.date(), которому
присваивается значение следующей или предыдущей даты путем сложения или
вычитания объектов datetime.timedelta(). Узнать больше о модулях Python для
работы с временем и датами вы можете из главы 17 моей книги Automate the Boring
Stuff with Python по адресу https://automatetheboringstuff.com/2e/chapter17/.

62   Проект 8. Генерация календарей
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.

"""Генерация календарей, (c) Эл Свейгарт al@inventwithpython.com
Создает календари на месяц, готовые для распечатки, и сохраняет их
в текстовом файле. Код размещен на https://nostarch.com/big-book-smallpython-projects Теги: короткая"""
import datetime
# Задаем константы:
DAYS = ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday')
MONTHS = ('January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December')
print('Calendar Maker, by Al Sweigart al@inventwithpython.com')
while True: # Цикл для запроса у пользователя года.
print('Enter the year for the calendar:')
response = input('> ')
if response.isdecimal() and int(response) > 0:
year = int(response)
break
print('Please enter a numeric year, like 2023.')
continue
while True: # Цикл для запроса у пользователя месяца.
print('Enter the month for the calendar, 1-12:')
response = input('> ')
if not response.isdecimal():
print('Please enter a numeric month, like 3 for March.')
continue
month = int(response)
if 1 ')
if pot.upper() == 'QUIT':
print('Thanks for playing!')
sys.exit()

Описание работы  

27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.

elif not pot.isdecimal():
print('Please enter a number.')
elif int(pot) > purse:
print('You do not have enough to make that bet.')
else:
# Допустимая ставка.
pot = int(pot) # Преобразуем pot в тип integer.
break # Выходим из цикла после размещения допустимой ставки.
# Бросаем кости.
dice1 = random.randint(1, 6)
dice2 = random.randint(1, 6)
print('The dealer swirls the cup and you hear the rattle of dice.')
print('The dealer slams the cup on the floor, still covering the')
print('dice and asks for your bet.')
print()
print('
CHO (even) or HAN (odd)?')
# Спрашиваем у игрока, на что он ставит: на чо или на хан:
while True:
bet = input('> ').upper()
if bet != 'CHO' and bet != 'HAN':
print('Please enter either "CHO" or "HAN".')
continue
else:
break
# Открываем результаты броска костей:
print('The dealer lifts the cup to reveal:')
print(' ', JAPANESE_NUMBERS[dice1], '-', JAPANESE_NUMBERS[dice2])
print('
', dice1, '-', dice2)
# Определяем, выиграл ли игрок:
rollIsEven = (dice1 + dice2) % 2 == 0
if rollIsEven:
correctBet = 'CHO'
else:
correctBet = 'HAN'
playerWon = bet == correctBet
# Отображаем результаты ставок:
if playerWon:
print('You won! You take', pot, 'mon.')
purse = purse + pot # Прибавляем приз к кошельку.
print('The house collects a', pot // 10, 'mon fee.')
purse = purse - (pot // 10) # Сбор игрального дома 10%.
else:
purse = purse - pot # Вычитаем ставку из кошелька.
print('You lost!')

73

74   Проект 10. Чо-хан
78.
79.
80.
81.
82.
83.

# Проверяем, не закончились ли у игрока деньги:
if purse == 0:
print('You have run out of money!')
print('Thanks for playing!')
sys.exit()

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
реализовать один из вариантов игры, описанных в вышеупомянутой статье
«Википедии», в которых несколько игроков делают ставки друг против
друга; ввести в игру управляемых компьютером игроков с их собственными
кошельками;
добавить бонусы для определенных бросков, например 7 или «змеиных глаз»1;
предоставить игрокам возможность делать ставки на определенные числа
и получать в случае успеха бонус.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Как изменить сумму денег, с которой игрок начинает игру?
2. Как программа предотвращает ставки игроков на сумму, превышающую их
наличность в кошельке?
3. Как программа определяет, четная или нечетная сумма значений на двух
костях?
4. Что будет, если random.randint(1, 6) в строке 37 заменить на random.ran­
dint(1, 1)?
5. Будет ли игральный дом получать 10%-ный сбор, если pot // 10 в строке 73
(не 74) заменить на 0?
6. Что будет, если удалить или закомментировать строки 80, 81, 82 и 83?

1

Когда на обеих костях выпадает 1. — Примеч. пер.

11
Генератор заголовков-приманок

Нашему сайту нужно как-то заманивать людей смотреть
рекламу! Но придумать интересный оригинальный контент непросто. К счастью, с помощью генератора заголовков-приманок компьютер сможет создать миллионы
шокирующих фиктивных заголовков. Конечно, их качество — очень низкое, но читателей, похоже, это не смущает.
Данная программа генерирует любое нужное количество заголовков на основе шаблона в стиле игры Mad Libs1.
В этой программе много текста для шаблонов заголовков, но сам код прост и вполне
доступен для начинающих.

Программа в действии
Результат выполнения clickbait.py выглядит следующим образом:
Clickbait Headline Generator
By Al Sweigart al@inventwithpython.com
Our website needs to trick people into looking at ads!
Enter the number of clickbait headlines to generate:
> 1000
1

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

76   Проект 11. Генератор заголовков-приманок
Big Companies Hate Him! See How This New York Cat Invented a Cheaper Robot
What Telephone Psychics Don't Want You To Know About Avocados
You Won't Believe What This North Carolina Shovel Found in Her Workplace
--сокращено-14 Reasons Why Parents Are More Interesting Than You Think (Number 1 Will Surprise
You!)
What Robots Don't Want You To Know About Cats
This Florida Telephone Psychic Didn't Think Robots Would Take Her Job. She Was Wrong.

Описание работы
Программа включает несколько функций для генерации различных видов заголовков-приманок, получающих случайные слова из STATES, NOUNS, PLACES, WHEN
и других списков. Далее эти функции вставляют полученные слова в строковое
значение шаблона с помощью строкового метода format(), а затем возвращают это
значение. Все очень похоже на книгу для игры в Mad Libs, но пробелы заполняет
компьютер, благодаря чему программа может генерировать тысячи заголовковприманок в секунду.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.

"""Генератор заголовков-приманок, (c) Эл Свейгарт al@inventwithpython.com
Генератор заголовков-приманок для сайта со скучным контентом
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, для начинающих, юмор, слова"""
import random
# Задаем константы:
OBJECT_PRONOUNS = ['Her', 'Him', 'Them']
POSSESIVE_PRONOUNS = ['Her', 'His', 'Their']
PERSONAL_PRONOUNS = ['She', 'He', 'They']
STATES = ['California', 'Texas', 'Florida', 'New York', 'Pennsylvania',
'Illinois', 'Ohio', 'Georgia', 'North Carolina', 'Michigan']
NOUNS = ['Athlete', 'Clown', 'Shovel', 'Paleo Diet', 'Doctor', 'Parent',
'Cat', 'Dog', 'Chicken', 'Robot', 'Video Game', 'Avocado',
'Plastic Straw','Serial Killer', 'Telephone Psychic']
PLACES = ['House', 'Attic', 'Bank Deposit Box', 'School', 'Basement',
'Workplace', 'Donut Shop', 'Apocalypse Bunker']
WHEN = ['Soon', 'This Year', 'Later Today', 'RIGHT NOW', 'Next Week']
def main():
print('Clickbait Headline Generator')
print('By Al Sweigart al@inventwithpython.com')
print()
print('Our website needs to trick people into looking at ads!')
while True:
print('Enter the number of clickbait headlines to generate:')

Описание работы  

30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.

77

response = input('> ')
if not response.isdecimal():
print('Please enter a number.')
else:
numberOfHeadlines = int(response)
break # Выходим из цикла, когда будет введено допустимое число.
for i in range(numberOfHeadlines):
clickbaitType = random.randint(1, 8)
if clickbaitType == 1:
headline = generateAreMillennialsKillingHeadline()
elif clickbaitType == 2:
headline = generateWhatYouDontKnowHeadline()
elif clickbaitType == 3:
headline = generateBigCompaniesHateHerHeadline()
elif clickbaitType == 4:
headline = generateYouWontBelieveHeadline()
elif clickbaitType == 5:
headline = generateDontWantYouToKnowHeadline()
elif clickbaitType == 6:
headline = generateGiftIdeaHeadline()
elif clickbaitType == 7:
headline = generateReasonsWhyHeadline()
elif clickbaitType == 8:
headline = generateJobAutomatedHeadline()
print(headline)
print()
website = random.choice(['wobsite', 'blag', 'Facebuuk', 'Googles',
'Facesbook', 'Tweedie', 'Pastagram'])
when = random.choice(WHEN).lower()
print('Post these to our', website, when, 'or you\'re fired!')
# Все эти функции возвращают заголовки различных типов:
def generateAreMillennialsKillingHeadline():
noun = random.choice(NOUNS)
return 'Are Millennials Killing the {} Industry?'.format(noun)
def generateWhatYouDontKnowHeadline():
noun = random.choice(NOUNS)
pluralNoun = random.choice(NOUNS) + 's'
when = random.choice(WHEN)
return 'Without This {}, {} Could Kill You {}'.format(noun, pluralNoun,
when)
def generateBigCompaniesHateHerHeadline():
pronoun = random.choice(OBJECT_PRONOUNS)

78   Проект 11. Генератор заголовков-приманок
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.

state = random.choice(STATES)
noun1 = random.choice(NOUNS)
noun2 = random.choice(NOUNS)
return 'Big Companies Hate {}! See How This {} {} Invented a Cheaper {}'.
format(pronoun, state, noun1, noun2)
def generateYouWontBelieveHeadline():
state = random.choice(STATES)
noun = random.choice(NOUNS)
pronoun = random.choice(POSSESIVE_PRONOUNS)
place = random.choice(PLACES)
return 'You Won\'t Believe What This {} {} Found in {} {}'.format(state,
noun, pronoun, place)
def generateDontWantYouToKnowHeadline():
pluralNoun1 = random.choice(NOUNS) + 's'
pluralNoun2 = random.choice(NOUNS) + 's'
return 'What {} Don\'t Want You To Know About {}'.format(pluralNoun1,
pluralNoun2)
def generateGiftIdeaHeadline():
number = random.randint(7, 15)
noun = random.choice(NOUNS)
state = random.choice(STATES)
return '{} Gift Ideas to Give Your {} From {}'.format(number, noun, state)
def generateReasonsWhyHeadline():
number1 = random.randint(3, 19)
pluralNoun = random.choice(NOUNS) + 's'
# number2 should be no larger than number1:
number2 = random.randint(1, number1)
return '{} Reasons Why {} Are More Interesting Than You Think (Number {}
Will Surprise You!)'.format(number1, pluralNoun, number2)
def generateJobAutomatedHeadline():
state = random.choice(STATES)
noun = random.choice(NOUNS)
i = random.randint(0, 2)
pronoun1 = POSSESIVE_PRONOUNS[i]
pronoun2 = PERSONAL_PRONOUNS[i]
if pronoun1 == 'Their':
return 'This {} {} Didn\'t Think Robots Would Take {} Job. {} Were
Wrong.'.format(state, noun, pronoun1, pronoun2)
else:
return 'This {} {} Didn\'t Think Robots Would Take {} Job. {} Was
Wrong.'.format(state, noun, pronoun1, pronoun2)

Исследование программы  

79

129. # Если программа не импортируется, а запускается, производим запуск:
130. if __name__ == '__main__':
131.
main()

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
добавить еще несколько типов заголовков-приманок;
добавить новые категории слов, помимо NOUNS, STATES и т. д.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Какое сообщение об ошибке вы получите, если удалите или закомментируете
numberOfHeadlines = int(response) в строке 34?
2. Какое сообщение об ошибке вы получите, если замените int(response) на
response в строке 34?
3. Какое сообщение об ошибке вы получите, если замените строку 19 на
WHEN = []?

12
Гипотеза Коллатца

Гипотеза Коллатца, называемая также дилеммой 3n + 1, —
простейшая из нерешенных задач математики (не волнуйтесь, сама программа достаточно проста для начинающих).
Начиная с числа n, следующие члены последовательности
формируются в соответствии с тремя правилами.
1. Если n — четное, то следующий член последовательности равен n / 2.
2. Если n — нечетное, то следующий член равен n * 3 + 1.
3. Если n равно 1, то прекращаем. В противном случае повторяем.
Существует (математически не доказанная) гипотеза, что любая такая последовательность завершается 1. Больше информации о гипотезе Коллатца можно найти
в статье «Википедии»: https://ru.wikipedia.org/wiki/Гипотеза_Коллатца.

Программа в действии
Результат выполнения collatz.py выглядит следующим образом:
Collatz Sequence, or, the 3n + 1 Problem
By Al Sweigart al@inventwithpython.com
The Collatz sequence is a sequence of numbers produced from a starting
number n, following three rules:
--сокращено--

Описание работы  

81

Enter a starting number (greater than 0) or QUIT:
> 26
26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1
Collatz Sequence, or, the 3n + 1 Problem
By Al Sweigart al@inventwithpython.com
--сокращено-Enter a starting number (greater than 0) or QUIT:
> 27
27, 82, 41, 124, 62, 31, 94, 47, 142, 71, 214, 107, 322, 161, 484, 242, 121,
364, 182, 91, 274, 137, 412, 206, 103, 310, 155, 466, 233, 700, 350, 175, 526,
263, 790, 395, 1186, 593, 1780, 890, 445, 1336, 668, 334, 167, 502, 251, 754,
377, 1132, 566, 283, 850, 425, 1276, 638, 319, 958, 479, 1438, 719, 2158,
1079, 3238, 1619, 4858, 2429, 7288, 3644, 1822, 911, 2734, 1367, 4102, 2051,
6154, 3077, 9232, 4616, 2308, 1154, 577, 1732, 866, 433, 1300, 650, 325, 976,
488, 244, 122, 61, 184, 92, 46, 23, 70, 35, 106, 53, 160, 80, 40, 20, 10, 5,
16, 8, 4, 2, 1

Описание работы
Определить, четное число или нечетное, можно с помощью оператора деления
по модулю %. Напомню, что этот оператор относится к числу операторов «взятия
остатка от деления»: 23 делится на 7 — равно 3 с остатком 2, то есть 23 по модулю 7
равно просто 2. Четные числа делятся на 2 без остатка, а нечетные — с остатком 1.
При четном n выражение в if n % 2 == 0: в строке 33 равно True. При нечетном же
оно равно False.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.

"""Гипотеза Коллатца, (c) Эл Свейгарт al@inventwithpython.com
Генерирует члены последовательности Коллатца по заданному начальному числу.
Больше информации — по адресу https://ru.wikipedia.org/wiki/Гипотеза_Коллатца
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, математическая"""
import sys, time
print('''Collatz Sequence, or, the 3n + 1 Problem
By Al Sweigart al@inventwithpython.com
The Collatz sequence is a sequence of numbers produced from a starting
number n, following three rules:
1) If n is even, the next number n is n / 2.
2) If n is odd, the next number n is n * 3 + 1.
3) If n is 1, stop. Otherwise, repeat.
It is generally thought, but so far not mathematically proven, that
every starting number eventually terminates at 1.
''')

82   Проект 12. Гипотеза Коллатца
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.

print('Enter a starting number (greater than 0) or QUIT:')
response = input('> ')
if not response.isdecimal() or response == '0':
print('You must enter an integer greater than 0.')
sys.exit()
n = int(response)
print(n, end='', flush=True)
while n != 1:
if n % 2 == 0: # Если n — четное...
n = n // 2
else: # В противном случае n — нечетное...
n = 3 * n + 1
print(', ' + str(n), end='', flush=True)
time.sleep(0.1)
print()

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Сколько членов содержит последовательность Коллатца, начинающаяся с 32?
2. Сколько членов содержит последовательность Коллатца, начинающаяся с 33?
3. Всегда ли последовательности Коллатца, начальные члены которых являются
степенями двойки (2, 4, 8, 16, 32, 64, 128 и т. д.), состоят только из четных
чисел (не считая конечного числа 1)?
4. Что будет, если ввести 0 в качестве начального числа?

13

Игра «Жизнь» Конвея

Игра «Жизнь» Конвея — клеточный автомат для имитационного моделирования, позволяющий создавать
интересные узоры на основе простых правил. Изобрел
ее математик Джон Конвей в 1970 году, а сделала известной рубрика «Математические игры» Мартина Гарднера
в журнале Scientific American. Программисты и специалисты
по вычислительной технике сегодня очень любят эту скорее интересную визуализацию, чем настоящую «игру». Двумерная доска содержит сетку
из «клеток», которые все следуют трем простым правилам:
живые клетки с двумя или тремя соседями остаются живыми на следующем
шаге моделирования;
мертвые клетки с ровно тремя соседями оживают на следующем шаге моделирования;
все прочие клетки умирают или остаются мертвыми на следующем шаге
моделирования.
Состояние клетки на следующем шаге моделирования (живая или мертвая) зависит
только от текущего состояния. Клетки не «помнят» старых состояний. Существует множество исследований, посвященных узорам, генерируемым на основе этих
простых правил. К сожалению, профессор Конвей умер от осложнений COVID-19
в апреле 2020 года. Больше информации об игре «Жизнь» вы можете найти в статье «Википедии» https://ru.wikipedia.org/wiki/Игра_«Жизнь», а больше информации
о Мартине Гарднере — в статье https://ru.wikipedia.org/wiki/Гарднер,_Мартин.

84   Проект 13. Игра «Жизнь» Конвея

Программа в действии
Результат выполнения conwaysgameoflife.py выглядит следующим образом:
O
OO
OO
OO

O
O

O

O

O
O
O

O

O
O
OO

O

OO
O
OO
O O
OO
OO

OOO

OO

O
O
O

O

OO
OOO
OO

O

O

OO OO
OO
O O
O
O
OOOO OO
O
OOO

O O
O OOOO
O

O

OO
OO
O

O O
OO
O

O

OOO

OO O
OOOO
O
O OO OO O
O
OO O
OO
O
O OOO

O OO
O O
OO

O

O O
O

O

OO
O OOO
OOOOO O
OOOO

O

Описание работы
Состояние клеток хранится в ассоциативных массивах в переменных cells
и nextCells. Роль ключей обоих массивов играют кортежи (x, y), где x и y— целочисленные, 'O' для живых клеток и ' ' для мертвых. В строках с 40-й по 44-ю
представление этих ассоциативных массивов выводится на экран. Ассоциативный
массив в переменной cells отражает текущее состояние клеток, а nextCells содержит ассоциативный массив для клеток на следующем шаге моделирования.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.

"""Игра "Жизнь" Конвея, (c) Эл Свейгарт al@inventwithpython.com
Клеточный автомат для имитационного моделирования. Нажмите Ctrl+C для останова.
Больше информации — в статье https://ru.wikipedia.org/wiki/Игра_«Жизнь»
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, графика, имитационное моделирование"""
import copy, random, sys, time
# Задаем константы:
WIDTH = 79
# Ширина сетки клеток.
HEIGHT = 20 # Высота сетки клеток.
# (!) Попробуйте заменить значение ALIVE на '#' или другой символ:
ALIVE = 'O' # Символ, соответствующий живой клетке.
# (!) Попробуйте заменить значение DEAD на '.' или другой символ:
DEAD = ' '
# Символ, соответствующий мертвой клетке.
# (!) Попробуйте заменить значение ALIVE на '|', а DEAD на '-'.
# cells и nextCells — ассоциативные массивы для хранения состояния игры.

Описание работы  

21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.

85

# Роль их ключей играют кортежи (x, y), а значения представляют собой
# одно из значений ALIVE или DEAD.
nextCells = {}
# Вставляем в nextCells случайные живые и мертвые клетки:
for x in range(WIDTH): # Проходим в цикле по всем столбцам.
for y in range(HEIGHT): # Проходим в цикле по всем строкам.
# Исходные клетки могут быть живыми или мертвыми с вероятностью 50 %.
if random.randint(0, 1) == 0:
nextCells[(x, y)] = ALIVE # Добавляем живую клетку.
else:
nextCells[(x, y)] = DEAD # Добавляем мертвую клетку.
while True: # Основной цикл программы.
# Итерации этого цикла соответствуют шагам моделирования.
print('\n' * 50) # Разделяем шаги символами новой строки.
cells = copy.deepcopy(nextCells)
# Выводим клетки на экран:
for y in range(HEIGHT):
for x in range(WIDTH):
print(cells[(x, y)], end='') # Выводим # или пробел.
print() # Выводим символ новой строки в конце строки.
print('Press Ctrl-C to quit.')
# Вычисляем клетки следующего шага исходя из клеток на текущем шаге:
for x in range(WIDTH):
for y in range(HEIGHT):
# Получаем координаты (x, y) соседних клеток, даже если они
# выходят за границы:
left = (x - 1) % WIDTH
right = (x + 1) % WIDTH
above = (y - 1) % HEIGHT
below = (y + 1) % HEIGHT
# Подсчитываем количество живых соседей:
numNeighbors = 0
if cells[(left, above)] == ALIVE:
numNeighbors += 1 # Сосед вверху слева жив.
if cells[(x, above)] == ALIVE:
numNeighbors += 1 # Сосед вверху жив.
if cells[(right, above)] == ALIVE:
numNeighbors += 1 # Сосед вверху справа жив.
if cells[(left, y)] == ALIVE:
numNeighbors += 1 # Сосед слева жив.
if cells[(right, y)] == ALIVE:
numNeighbors += 1 # Сосед справа жив.
if cells[(left, below)] == ALIVE:
numNeighbors += 1 # Сосед внизу слева жив.
if cells[(x, below)] == ALIVE:
numNeighbors += 1 # Сосед внизу жив.
if cells[(right, below)] == ALIVE:
numNeighbors += 1 # Сосед внизу справа жив.

86   Проект 13. Игра «Жизнь» Конвея
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.

# Устанавливаем состояние клеток в соответствии с правилами игры
if cells[(x, y)] == ALIVE and (numNeighbors == 2
or numNeighbors == 3):
# Живые клетки с двумя или тремя соседями остаются живыми:
nextCells[(x, y)] = ALIVE
elif cells[(x, y)] == DEAD and numNeighbors == 3:
# Мертвые клетки с тремя соседями становятся живыми:
nextCells[(x, y)] = ALIVE
else:
# Все остальные клетки умирают или остаются мертвыми:
nextCells[(x, y)] = DEAD
try:

time.sleep(1) # Добавляем паузу в 1 секунду, чтобы уменьшить мигание.
except KeyboardInterrupt:
print("Conway's Game of Life")
print('By Al Sweigart al@inventwithpython.com')
sys.exit() # Если нажато Ctrl+C — завершаем программу.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Идеи относительно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
задать вместо 50 % различный процент клеток, начинающих с «живого» состояния;
добавить возможность чтения начального состояния из текстового файла,
чтобы пользователи могли редактировать начальные состояния ячеек вручную.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если WIDTH = 79 в строке 10 заменить на WIDTH = 7?
2. Что будет, если удалить или закомментировать print('\n' * 50) в строке 36?
3. Что будет, если random.randint(0, 1) в строке 28 заменить на random.ran­
dint(0, 10)?
4. Что будет, если nextCells[(x, y)] = DEAD в строке 85 заменить на nextCells[(x,
y)] = ALIVE?

14
Обратный отсчет

Программа выводит цифровой таймер, отсчитывающий
время в обратном направлении. Вместо того чтобы непосредственно визуализировать цифры, мы генерируем их
изображения с помощью модуля sevseg.py из проекта 64.
Чтобы программа обратного отсчета работала, необходимо сначала создать этот файл. Далее вы можете установить
таймер обратного отсчета на любое количество секунд, минут
и часов. Данная программа аналогична проекту 19.

Программа в действии
Результат выполнения countdown.py выглядит следующим образом:
__
__
__
__
| | | | * | | | |
|__| |__| * |__| |__|
Press Ctrl-C to quit.

*
*

__
__
__| |__|
|__
__|

Описание работы
После выполнения оператора import sevseg можно вызвать функцию sev­
seg.getSevSegStr(), возвращающую многострочное строковое значение с семисегментными цифрами. Однако программа обратного отсчета должна отображать

88   Проект 14. Обратный отсчет
составленное из звездочек двоеточие в качестве разделителя часов, минут и секунд.
Для этого необходимо разбить три строки многострочного строкового значения
с цифрами на три отдельных строковых значения с помощью метода splitlines().
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.

"""Обратный отсчет, (c) Эл Свейгарт al@inventwithpython.com
Отображает динамическое изображение таймера обратного отсчета с помощью
семисегментного индикатора. Для останова нажмите Ctrl+C. Больше информации —
в статье https://ru.wikipedia.org/wiki/Семисегментный_индикатор
Требует наличия в том же каталоге файла sevseg.py.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, графика"""
import sys, time
import sevseg # Импорт программы sevseg.py.
# (!) Можете заменить это значение на любое количество секунд:
secondsLeft = 30
try:

while True: # Основной цикл программы.
# Очищаем экран, выводя несколько символов новой строки:
print('\n' * 60)
# Берем часы/минуты/секунды из secondsLeft:
# Например: 7265 равно 2 часам 1 минуте 5 секундам.
# 7265 // 3600 равно 2 часам:
hours = str(secondsLeft // 3600)
# 7265 % 3600 равно 65, и 65 // 60 равно 1 минуте:
minutes = str((secondsLeft % 3600) // 60)
# А 7265 % 60 равно 5 секундам:
seconds = str(secondsLeft % 60)
# Получаем из модуля sevseg строковые значения для цифр:
hDigits = sevseg.getSevSegStr(hours, 2)
hTopRow, hMiddleRow, hBottomRow = hDigits.splitlines()
mDigits = sevseg.getSevSegStr(minutes, 2)
mTopRow, mMiddleRow, mBottomRow = mDigits.splitlines()
sDigits = sevseg.getSevSegStr(seconds, 2)
sTopRow, sMiddleRow, sBottomRow = sDigits.splitlines()
# Отображаем цифры:
print(hTopRow
+ '
print(hMiddleRow + '
print(hBottomRow + '

*
*

' + mTopRow
+ '
' + mMiddleRow + '
' + mBottomRow + '

if secondsLeft == 0:
print()
print('
* * * * BOOM * * * *')

*
*

' + sTopRow)
' + sMiddleRow)
' + sBottomRow)

Исследование программы  

89

47.
break
48.
49.
print()
50.
print('Press Ctrl-C to quit.')
51.
52.
time.sleep(1) # Вставляем паузу на 1 секунду.
53.
secondsLeft -= 1
54. except KeyboardInterrupt:
55.
print('Countdown, by Al Sweigart al@inventwithpython.com')
56.
sys.exit() # Если нажато сочетание клавиш Ctrl+C — завершаем программу.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
запросить у пользователя начальное время обратного отсчета;
запросить у пользователя сообщение для отображения в конце обратного
отсчета.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если secondsLeft = 30 в строке 13 заменить на secondsLeft = 30.5?
2.
3.
4.
5.
6.

Что будет, если 2 в строках 30, 33 и 36 заменить на 1?
Что будет, если time.sleep(1) в строке 52 заменить на time.sleep(0.1)?
Что будет, если secondsLeft -= 1 в строке 53 заменить на secondsLeft -= 2?
Что будет, если вы удалите или закомментируете print('\n' * 60) в строке 18?
Какое сообщение об ошибке вы получите, если удалите или закомментируете
import sevseg в строке 10?

15
Глубокая пещера

Программа представляет собой динамическое изображение глубокой пещеры, ведущей до самого центра Земли.
Хотя и будучи короткой, программа создает интересную
бесконечную визуализацию благодаря природе экрана компьютера, основанной на прокрутке, доказывая тем самым,
что для создания интересного зрелища не нужно много кода.
Данная программа аналогична проекту 58.

Программа в действии
Результат выполнения deepcave.py выглядит следующим образом:
Deep Cave, by Al Sweigart al@inventwithpython.com
Press Ctrl-C to stop.
####################
########################################
####################
#########################################
####################
########################################
####################
########################################
#####################
#######################################
######################
######################################
#####################
#######################################
####################
########################################
###################
#########################################
--сокращено--

Описание работы  

91

Описание работы
Эта программа активно использует тот факт, что при выводе на экран новых строк
старые передвигаются вверх. Благодаря выводу на экран чуть отличающегося зазора программа создает у зрителя впечатление, что он движется вниз.
Количество символов «решетка» слева отслеживается в переменной leftWidth.
Количество пробелов посередине — в переменной gapWidth. Количество символов
«решетка» справа вычисляется по формуле WIDTH - gapWidth - leftWidth, что гарантирует одинаковую ширину всех строк.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.

"""Глубокая пещера, (c) Эл Свейгарт al@inventwithpython.com
Динамическое изображение глубокой пещеры, ведущей до самого центра Земли.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, прокрутка, графика"""
import random, sys, time
# Задаем константы:
WIDTH = 70 # (!) Попробуйте заменить на 10 или 30.
PAUSE_AMOUNT = 0.05 # (!) Попробуйте заменить на 0 или 1.0.
print('Deep Cave, by Al Sweigart al@inventwithpython.com')
print('Press Ctrl-C to stop.')
time.sleep(2)
leftWidth = 20
gapWidth = 10
while True:
# Отображает фрагмент туннеля:
rightWidth = WIDTH - gapWidth - leftWidth
print(('#' * leftWidth) + (' ' * gapWidth) + ('#' * rightWidth))
# Проверяем, не нажато ли Ctrl+C, во время короткой паузы:
try:
time.sleep(PAUSE_AMOUNT)
except KeyboardInterrupt:
sys.exit() # Если нажато сочетание клавиш Ctrl+C — завершаем программу.
# Подбираем ширину левой части:
diceRoll = random.randint(1, 6)
if diceRoll == 1 and leftWidth > 1:
leftWidth = leftWidth - 1 # Уменьшаем ширину левой части.
elif diceRoll == 2 and leftWidth + gapWidth < WIDTH - 1:
leftWidth = leftWidth + 1 # Увеличиваем ширину левой части.
else:
pass # Ничего не делаем; ширина левой части не меняется.

92   Проект 15. Глубокая пещера
40.
41.
42.
43.
44.
45.
46.
47.
48.

# Подбираем ширину зазора:
# (!) Попробуйте раскомментировать весь следующий код:
#diceRoll = random.randint(1, 6)
#if diceRoll == 1 and gapWidth > 1:
#
gapWidth = gapWidth - 1 # Уменьшаем.
#elif diceRoll == 2 and leftWidth + gapWidth < WIDTH - 1:
#
gapWidth = gapWidth + 1 # Увеличиваем ширину зазора.
#else:
#
pass # Ничего не делаем; ширина зазора не меняется.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Идеи относительно возможных
небольших изменений вы найдете в комментариях, помеченных (!).

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если (' ' * gapWidth) в строке 23 заменить на ('.' * gapWidth)?
2. Что будет, если random.randint(1, 6) в строке 32 заменить на random.ran­
dint(1, 1)?
3. Что будет, если random.randint(1, 6) в строке 32 заменить на random.ran­
dint(2, 2)?
4. Какое сообщение об ошибке вы получите, если удалите или закомментируете
leftWidth = 20 в строке 17?
5. Что будет, если WIDTH = 70 в строке 10 заменить на WIDTH = -70?
6. Какое сообщение об ошибке вы получите, если PAUSE_AMOUNT = 0.05 в строке 11 замените на PAUSE_AMOUNT = -0.05?

16
Ромбы

Данная программа иллюстрирует небольшой алгоритм
для рисования ромбов различного размера с помощью
ASCII-графики. Программа включает функции для рисования как контуров, так и заполненного ромба указанного
размера. Эти функции — хорошее упражнение для начинающих; попробуйте понять закономерности рисунков ромбов
по мере увеличения их размера.

94   Проект 16. Ромбы

Программа в действии
Результат выполнения diamonds.py выглядит следующим образом:
Diamonds, by Al Sweigart al@inventwithpython.com
/\
\/
/\
\/
/\

/
\

\
/

\/

/\
//\\
\\//
\/
/\

/

\

\

/

/
\

\
/

\/

/\
//\\
///\\\
\\\///
\\//
\/
--сокращено--

Описание работы
Для создания подобной программы самостоятельно не помешает сначала «нарисовать» ромбы различного размера в редакторе и выяснить закономерности
их построения. Такая методика поможет вам понять, что каждая строка контура
ромба состоит из четырех частей: нескольких ведущих пробелов, внешней прямой
косой черты, нескольких внутренних пробелов и внешней обратной косой черты.
Заполненные ромбы вместо внутренних пробелов включают несколько внутренних
прямых и обратных косых черт. Только разобравшись с этими закономерностями,
я смог написать diamonds.py.

Описание работы  

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.

"""Ромбы, (c) Эл Свейгарт al@inventwithpython.com
Рисует ромбы различного размера.
Код размещен на https://nostarch.com/big-book-small-python-projects
/\
/\
/ \
//\\
/\
/\
/
\
///\\\
/ \
//\\ /
\ ////\\\\
/\
/\ /
\ ///\\\ \
/ \\\\////
/ \ //\\ \
/ \\\/// \
/
\\\///
\ / \\// \ /
\\//
\ /
\\//
\/
\/
\/
\/
\/
\/
Теги: крошечная, для начинающих, графика"""
def main():
print('Diamonds, by Al Sweigart al@inventwithpython.com')
# Отображает ромбы размера с 0 по 6:
for diamondSize in range(0, 6):
displayOutlineDiamond(diamondSize)
print() # Выводит символ новой строки.
displayFilledDiamond(diamondSize)
print() # Выводит символ новой строки.
def displayOutlineDiamond(size):
# Отображает верхнюю половину ромба:
for i in range(size):
print(' ' * (size - i - 1), end='') # Пробелы слева.
print('/', end='') # Левая сторона ромба.
print(' ' * (i * 2), end='') # Внутренность ромба.
print('\\') # Правая сторона ромба.
# Отображает нижнюю половину ромба:
for i in range(size):
print(' ' * i, end='') # Пробелы слева.
print('\\', end='') # Левая сторона ромба.
print(' ' * ((size - i - 1) * 2), end='') # Внутренность ромба.
print('/') # Правая сторона ромба.
def displayFilledDiamond(size):
# Отображает верхнюю половину ромба:
for i in range(size):
print(' ' * (size - i - 1), end='') # Пробелы слева.
print('/' * (i + 1), end='') # Левая сторона ромба.
print('\\' * (i + 1)) # Правая сторона ромба.
# Отображает нижнюю половину ромба:
for i in range(size):
print(' ' * i, end='') # Пробелы слева.
print('\\' * (size - i), end='') # Левая сторона ромба.

95

96   Проект 16. Ромбы
52.
print('/' * (size - i)) # Правая сторона ромба.
53.
54.
55. # Если программа не импортируется, а запускается, производим запуск:
56. if __name__ == '__main__':
57.
main()

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
нарисовать другие фигуры: треугольники, прямоугольники и фигуры в виде
различных ромбов;
выводить нарисованные фигуры в текстовый файл, а не на экран.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если print('\\') в строке 31 заменить на print('@')?
2. Что будет, если print(' ' * (i * 2), end='') в строке 30 заменить на print('@'
* (i * 2), end='')?
3. Что будет, если range(0, 6) в строке 18 заменить на range(0, 30)?
4. Что будет, если удалить или закомментировать for i in range(size): в строке 34 или в строке 49?

17
Арифметика
с игральными костями

Эта программа — математическая головоломка — выбрасывает от двух до шести игральных костей, сумму очков
на которых вы должны вычислить так быстро, как только
можете. Но данная программа — не просто автоматизированные обучающие карточки; выпавшие верхние (лицевые)
стороны костей отображаются в случайных местах на экране.
Подобная ASCII-графика позволяет превратить упражнения
в арифметике в развлечение.

98   Проект 17. Арифметика с игральными костями

Программа в действии
Результат выполнения dicemath.py выглядит следующим образом:
Dice Math, by Al Sweigart al@inventwithpython.com
Add up the sides of all the dice displayed on the screen. You have
30 seconds to answer as many as possible. You get 4 points for each
correct answer and lose 1 point for each incorrect answer.
Press Enter to begin...

+-------+
|
O |
|
|
| O
|
+-------+
Enter the sum: 13
--сокращено--

+-------+
| O
O |
|
O
|
| O
O |
+-------+
+-------+
| O
O |
|
|
| O
O |
+-------+

+-------+
| O
|
|
|
|
O |
+-------+

Описание работы
Отображаемые на экране кости представлены ассоциативным массивом, хранящимся в переменной canvas. В Python кортежи аналогичны спискам, но их содержимое
нельзя менять. Ключи этого ассоциативного массива представляют собой кортежи
(x, y), соответствующие позиции левого верхнего угла кости, а значения — один из
«кортежей костей» в ALL_DICE. Как видно из строк с 28-й по 80-ю, каждый кортеж
кости содержит список строковых значений, графически отражающих одну из возможных лицевых сторон кости, и целого числа, отражающего количество точек на
этой лицевой стороне. Далее программа на основе данной информации отображает
кость и вычисляет общую сумму очков.
В строках с 174-й по 177-ю данные из ассоциативного массива canvas визуализируются на экране, подобно тому как в проекте 13 мы визуализировали клетки на
экране.
1. """Арифметика с игральными костями, (c) Эл Свейгарт al@inventwithpython.com
2. Игра с обучающими карточками на сложение, в которой нужно
➥ суммировать все очки на выброшенных игральных костях
3. Код размещен на https://nostarch.com/big-book-small-python-projects

Описание работы  

4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.

Теги: большая, графика, игра, математическая"""
import random, time
# Задаем константы:
DICE_WIDTH = 9
DICE_HEIGHT = 5
CANVAS_WIDTH = 79
CANVAS_HEIGHT = 24 - 3

# -3, чтобы было куда ввести сумму внизу

# Длительность в секундах:
QUIZ_DURATION = 30 # (!) Попробуйте заменить это значение на 10 или 60.
MIN_DICE = 2 # (!) Попробуйте заменить это значение на 1 или 5.
MAX_DICE = 6 # (!) Попробуйте заменить это значение на 14.
# (!) Попробуйте заменить эти значения на различные другие:
REWARD = 4 # (!) Очки, полученные за правильные ответы.
PENAltY = 1 # (!) Очки, отнятые за неправильные ответы.
# (!) Попробуйте задать отрицательное значение PENAltY, чтобы давать
# очки за неправильные ответы!
# Если все кости не помещаются на экране, программа зависает:
assert MAX_DICE +-------+ ^
#
| O
| |
#
|
O
| DICE_HEIGHT (5)
#
|
O | |
#
+-------+ v
#

#
DICE_WIDTH (9)
topLeftX = left
topLeftY = top
topRightX = left + DICE_WIDTH
topRightY = top
bottomLeftX = left
bottomLeftY = top + DICE_HEIGHT
bottomRightX = left + DICE_WIDTH
bottomRightY = top + DICE_HEIGHT
# Проверяем, не пересекается ли эта игральная кость с предыдущей.
overlaps = False
for prevDieLeft, prevDieTop in topLeftDiceCorners:
prevDieRight = prevDieLeft + DICE_WIDTH
prevDieBottom = prevDieTop + DICE_HEIGHT
# Проверяем все углы этой кости, не входят ли они
# в область, занимаемую предыдущей костью:
for cornerX, cornerY in ((topLeftX, topLeftY),
(topRightX, topRightY),
(bottomLeftX, bottomLeftY),
(bottomRightX, bottomRightY)):
if (prevDieLeft 1d10+2
9 (7, +2)
> 2d38-1
32 (20, 13, -1)
> 100d6

Описание работы  

105

364 (3, 3, 2, 4, 2, 1, 4, 2, 4, 6, 4, 5, 4, 3, 3, 3, 2, 5, 1, 5, 6, 6, 6, 4,
5, 5, 1, 5, 2, 2, 2, 5, 1, 1, 2, 1, 4, 5, 6, 2, 4, 3, 4, 3, 5, 2, 2, 1, 1, 5,
1, 3, 6, 6, 6, 6, 5, 2, 6, 5, 4, 4, 5, 1, 6, 6, 6, 4, 2, 6, 2, 6, 2, 2, 4, 3,
6, 4, 6, 4, 2, 4, 3, 3, 1, 6, 3, 3, 4, 4, 5, 5, 5, 6, 2, 3, 6, 1, 1, 1)
--сокращено--

Описание работы
Большая часть кода этой программы посвящена проверке форматирования вводимых пользователем данных. Сам же случайный бросок игральных костей представляет собой всего лишь вызов random.randint(). У этой функции отсутствует
систематическая ошибка: все целые числа из указанного диапазона возвращаются
с равной вероятностью, так что random.randint() идеально подходит для моделирования выбрасывания костей.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.

"""Выбрасыватель игральных костей, (c) Эл Свейгарт al@inventwithpython.com
Моделирует выбрасывание костей, в нотации Dungeons & Dragons
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, имитационное моделирование"""
import random, sys
print('''Dice Roller, by Al Sweigart al@inventwithpython.com
Enter what kind and how many dice to roll. The format is the number of
dice, followed by "d", followed by the number of sides the dice have.
You can also add a plus or minus adjustment.
Examples:
3d6 rolls three 6-sided dice
1d10+2 rolls one 10-sided die, and adds 2
2d38-1 rolls two 38-sided die, and subtracts 1
QUIT quits the program
''')
while True: # Основной цикл программы:
try:
diceStr = input('> ') # Приглашение ввести описание игральных костей.
if diceStr.upper() == 'QUIT':
print('Thanks for playing!')
sys.exit()
# Очищаем строку описания игральных костей:
diceStr = diceStr.lower().replace(' ', '')
# Ищем "d" в строке описания игральных костей:
dIndex = diceStr.find('d')
if dIndex == -1:

106   Проект 18. Выбрасыватель игральных костей
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.

raise Exception('Missing the "d" character.')
# Выясняем количество костей. ("3" в "3d6+1"):
numberOfDice = diceStr[:dIndex]
if not numberOfDice.isdecimal():
raise Exception('Missing the number of dice.')
numberOfDice = int(numberOfDice)
# Выясняем, присутствует ли модификатор в виде знака плюс или минус:
modIndex = diceStr.find('+')
if modIndex == -1:
modIndex = diceStr.find('-')
# Выясняем количество граней ("6" в "3d6+1"):
if modIndex == -1:
numberOfSides = diceStr[dIndex + 1 :]
else:
numberOfSides = diceStr[dIndex + 1 : modIndex]
if not numberOfSides.isdecimal():
raise Exception('Missing the number of sides.')
numberOfSides = int(numberOfSides)
# Выясняем числовое значение модификатора (The "1" in "3d6+1"):
if modIndex == -1:
modAmount = 0
else:
modAmount = int(diceStr[modIndex + 1 :])
if diceStr[modIndex] == '-':
# Меняем числовое значение модификатора на отрицательное:
modAmount = -modAmount
# Моделируем бросок игральных костей:
rolls = []
for i in range(numberOfDice):
rollResult = random.randint(1, numberOfSides)
rolls.append(rollResult)
# Отображаем итоговую сумму очков:
print('Total:', sum(rolls) + modAmount, '(Each die:', end='')
# Отображаем отдельные броски:
for i, roll in enumerate(rolls):
rolls[i] = str(roll)
print(', '.join(rolls), end='')
# Отображаем числовое значение модификатора:
if modAmount != 0:
modSign = diceStr[modIndex]
print(', {}{}'.format(modSign, abs(modAmount)), end='')
print(')')

Исследование программы  

85.
86.
87.
88.
89.

107

except Exception as exc:
# Перехватываем все исключения и отображаем сообщение пользователю:
print('Invalid input. Enter something like "3d6" or "1d10+2".')
print('Input was invalid because: ' + str(exc))
continue # Возвращаемся к приглашению ввести описание игральных костей.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
добавить модификатор знака умножения, в дополнение к модификаторам
знаков сложения и вычитания;
добавить возможность автоматически исключать бросок костей с минимальной суммой очков.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если удалить или закомментировать rolls.append(rollResult)
в строке 69?
2. Что будет, если rolls.append(rollResult) в строке 69 заменить на rolls.app­
end(-rollResult)?
3. Что будет, если удалить или закомментировать print(', '.join(rolls),
end='') в строке 77?
4. Что будет, если вместо строки описания броска костей не ввести ничего?

19
Цифровые часы

Эта программа выводит цифровые часы, показывающие
текущее время. Вместо того чтобы непосредственно визуализировать цифры, мы генерируем их изображения
с помощью модуля sevseg.py из проекта 64. Данная программа аналогична проекту 14.

Программа в действии
Результат выполнения digitalclock.py выглядит следующим образом:
__
__
__
__
| | |__| *
__| __|
|__| __| *
__| __|
Press Ctrl-C to quit.

*
*

__
__
__| |__
__| |__|

Описание работы
Наша программа для цифровых часов напоминает программу из проекта 14. Они
обе не только импортируют модуль sevseg.py , но и разбивают возвращаемые
функцией sevseg.getSevSegStr() многострочные строковые значения с помощью
метода splitlines(). Это позволяет отображать составленное из звездочек двоеточие в качестве разделителя часов, минут и секунд на часах. Сравните данный код
с кодом проекта 14 и посмотрите, в чем они схожи, а чем различаются.

Описание работы  

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.

109

"""Цифровые часы, (c) Эл Свейгарт al@inventwithpython.com
Отображает показывающие текущее время цифровые часы с семисегментным
индикатором. Нажмите Ctrl+C для останова.
Код размещен на https://nostarch.com/big-book-small-python-projects
Требует наличия в том же каталоге файла sevseg.py
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, графика"""
import sys, time
import sevseg # Импорт программы sevseg.py.
try:

while True: # Основной цикл программы.
# Очищаем экран, выводя несколько символов новой строки:
print('\n' * 60)
# Получаем текущее время из системных часов компьютера:
currentTime = time.localtime()
# % 12, поскольку мы используем 12-, а не 24-часовые часы:
hours = str(currentTime.tm_hour % 12)
if hours == '0':
hours = '12' # 12-часовые часы показывают 12:00, а не 00:00.
minutes = str(currentTime.tm_min)
seconds = str(currentTime.tm_sec)
# Получаем из модуля sevseg строковые значения для цифр:
hDigits = sevseg.getSevSegStr(hours, 2)
hTopRow, hMiddleRow, hBottomRow = hDigits.splitlines()
mDigits = sevseg.getSevSegStr(minutes, 2)
mTopRow, mMiddleRow, mBottomRow = mDigits.splitlines()
sDigits = sevseg.getSevSegStr(seconds, 2)
sTopRow, sMiddleRow, sBottomRow = sDigits.splitlines()
# Отображаем цифры:
print(hTopRow
+ '
' + mTopRow
+ '
print(hMiddleRow + ' * ' + mMiddleRow + '
print(hBottomRow + ' * ' + mBottomRow + '
print()
print('Press Ctrl-C to quit.')

*
*

' + sTopRow)
' + sMiddleRow)
' + sBottomRow)

# Продолжаем выполнение цикла до перехода на новую секунду:
while True:
time.sleep(0.01)
if time.localtime().tm_sec != currentTime.tm_sec:
break
except KeyboardInterrupt:
print('Digital Clock, by Al Sweigart al@inventwithpython.com')
sys.exit() # Если нажато сочетание клавиш Ctrl+C — завершаем программу.

110   Проект 19. Цифровые часы

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если time.sleep(0.01) в строке 45 заменить на time.sleep(2)?
2. Что будет, если 2 в строках 27, 30 и 33 заменить на 1?
3. Что будет, если удалить или закомментировать print('\n' * 60) в строке 15?
4. Какое сообщение об ошибке вы получите, если удалите или закомментируете
import sevseg в строке 10?

20
Цифровой поток

Эта программа имитирует визуализацию «цифрового
потока» из научно-фантастического фильма «Матрица».
Случайные цепочки из двоичного «дождя» поднимаются
от низа экрана, формируя красивую хакерскую визуализацию. (К сожалению, из-за того, что текст движется по мере
прокрутки экрана вниз, невозможно создать падающие вниз
потоки без модулей наподобие bext.)

Программа в действии
Результат выполнения digitalstream.py выглядит следующим образом:
Digital Stream Screensaver, by Al Sweigart al@inventwithpython.com
Press Ctrl-C to quit.
0
0
0
0
1
0
0
1
1 0
0
0
0
1
0
0 0
0
0
1
0
0
0
1 0 0
1
0
1
0
0
1
011 1
1
0
1
0
0
0
000 11
0
1
1
0 1 0
1
1
110 10 1 0
1
101 0
0
1
000 11 1 1
0
100 1
0
11 00 0 1
1 1
001 1
1
0
1 10 0
0 0
010 0
1
1 11 11
--сокращено--

1
0
0
1
0
1
0 1 1
1 0 1
11 1 1
01
10
0

0
0
0
1
0
0
0

112   Проект 20. Цифровой поток

Описание работы
Как и в проекте 15, для создания динамического изображения в этой программе используется прокрутка на основе вызовов print(). Каждому столбцу соответствует
целое число в списке columns: columns[0] — число, соответствующее крайнему слева
столбцу, columns[1] — число, соответствующее столбцу непосредственно справа от
него, и т. д. Начальные значения этих чисел программа устанавливает в 0, то есть выводит ' ' (пустое строковое значение с пробелом) вместо потока в данном столбце.
А затем меняет каждое из этих чисел случайным образом на значение в диапазоне
от MIN_STREAM_LENGTH до MAX_STREAM_LENGTH. Далее с каждой выведенной строкой
это число уменьшается на 1. И до тех пор, пока соответствующее столбцу число
превышает 0, программа выводит в данном столбце случайным образом 1 или 0.
В результате на экране создается эффект цифрового потока.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.

"""Цифровой поток, (c) Эл Свейгарт al@inventwithpython.com
Экранная заставка в стиле визуальных эффектов фильма "Матрица".
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, графика, для начинающих, прокрутка"""
import random, shutil, sys, time
# Задаем константы:
MIN_STREAM_LENGTH = 6 # (!) Попробуйте заменить это значение на 1 или 50.
MAX_STREAM_LENGTH = 14 # (!) Попробуйте заменить это значение на 100.
PAUSE = 0.1 # (!) Попробуйте заменить это значение на 0.0 или 2.0.
STREAM_CHARS = ['0', '1'] # (!) Попробуйте заменить их на другие символы.
# Плотность может варьироваться от 0.0 до 1.0:
DENSITY = 0.02 # (!) Попробуйте заменить это значение на 0.10 или 0.30.
# Получаем размер окна терминала:
WIDTH = shutil.get_terminal_size()[0]
# В Windows нельзя вывести что-либо в последнем столбце без добавления
# автоматически символа новой строки, поэтому уменьшаем ширину на 1:
WIDTH -= 1
print('Digital Stream, by Al Sweigart al@inventwithpython.com')
print('Press Ctrl-C to quit.')
time.sleep(2)
try:

# Если для столбца счетчик равен 0, поток не отображается.
# В противном случае он показывает, сколько раз должны отображаться
# в этом столбце 1 или 0.
columns = [0] * WIDTH
while True:
# Задаем счетчики для каждого из столбцов:
for i in range(WIDTH):
if columns[i] == 0:

Исследование программы  

113

36.
if random.random() 0:
43.
print(random.choice(STREAM_CHARS), end='')
44.
columns[i] -= 1
45.
else:
46.
print(' ', end='')
47.
print() # Выводим символ новой строки в конце строки столбцов.
48.
sys.stdout.flush() # Обеспечиваем появление текста на экране.
49.
time.sleep(PAUSE)
50. except KeyboardInterrupt:
51.
sys.exit() # Если нажато сочетание клавиш Ctrl+C — завершаем программу.

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
добавить отображение и других символов, кроме единиц и нулей;
добавить другие фигуры, помимо строк, включая прямоугольники, тре­
угольники и ромбы.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если print(' ', end='') в строке 46 заменить на print('.',
end='')?
2. Какое сообщение об ошибке вы получите, если PAUSE = 0.1 в строке 11 замените на PAUSE = -0.1?
3. Что будет, если columns[i] > 0 в строке 42 заменить на columns[i] < 0?
4. Что будет, если columns[i] > 0 в строке 42 заменить на columns[i] ")
^^
( >)
(^ ) (< )
( ^)
^ ^
^^
^^
("< ^^
(``^^)
(^^=
(^ )
(< )( ^)
(v ) ( "<
^^
^ ^ ^ ^
--сокращено--

Описание работы  

119

Описание работы
Утята в этой программе представлены с помощью класса Duckling . В методе
__init__() данного класса случайным образом выбираются черты каждого утенка,
а различные части тела утят возвращаются методами getHeadStr(), getBodyStr()
и getFeetStr().
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.

"""Утята, (c) Эл Свейгарт al@inventwithpython.com
Экранная заставка со множеством утят
>" )
( >)
^ ^

=^^)
( ^)
^ ^

(``=
(v )
^ ^

("=
(^ )
^^

>")
( >)
^^

("=
(v )
^^

Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, графика, объектно-ориентированная, прокрутка"""
import random, shutil, sys, time
# Задаем константы:
PAUSE = 0.2 # (!) Попробуйте заменить это значение на 1.0 или 0.0.
DENSITY = 0.10 # (!) Замените это значение на любое из диапазона от 0.0 до 1.0.
DUCKLING_WIDTH = 5
LEFT = 'left'
RIGHT = 'right'
BEADY = 'beady'
WIDE = 'wide'
HAPPY = 'happy'
ALOOF = 'aloof'
CHUBBY = 'chubby'
VERY_CHUBBY = 'very chubby'
OPEN = 'open'
CLOSED = 'closed'
OUT = 'out'
DOWN = 'down'
UP = 'up'
HEAD = 'head'
BODY = 'body'
FEET = 'feet'
# Получаем размер окна терминала:
WIDTH = shutil.get_terminal_size()[0]
# В Windows нельзя вывести что-либо в последнем столбце без добавления
# автоматически символа новой строки, поэтому уменьшаем ширину на 1:
WIDTH -= 1
def main():
print('Duckling Screensaver, by Al Sweigart')

120   Проект 22. Утята
44.
print('Press Ctrl-C to quit...')
45.
time.sleep(2)
46.
47.
ducklingLanes = [None] * (WIDTH // DUCKLING_WIDTH)
48.
49.
while True: # Основной цикл программы.
50.
for laneNum, ducklingObj in enumerate(ducklingLanes):
51.
# Проверяем, имеет ли смысл создавать утенка на этой полоске:
52.
if (ducklingObj == None and random.random() 0:
canvas[(cursorX, cursorY)].add(command)
cursorY = cursorY - 1
elif command == 'S' and cursorY < CANVAS_HEIGHT - 1:
canvas[(cursorX, cursorY)].add(command)
cursorY = cursorY + 1
elif command == 'A' and cursorX > 0:
canvas[(cursorX, cursorY)].add(command)
cursorX = cursorX - 1
elif command == 'D' and cursorX < CANVAS_WIDTH - 1:
canvas[(cursorX, cursorY)].add(command)
cursorX = cursorX + 1
else:

Исследование программы  

160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.

129

# Если курсор не двигается, чтобы не выйти за пределы холста,
# то не меняем множество в canvas[(cursorX, cursorY)]
# canvas[(cursorX, cursorY)].
continue
# Если не существует множества для (cursorX, cursorY), добавляем
# пустое множество:
if (cursorX, cursorY) not in canvas:
canvas[(cursorX, cursorY)] = set()
# Добавляем строку с
if command == 'W':
canvas[(cursorX,
elif command == 'S':
canvas[(cursorX,
elif command == 'A':
canvas[(cursorX,
elif command == 'D':
canvas[(cursorX,

направлением во множество для этой точки xy:
cursorY)].add('S')
cursorY)].add('W')
cursorY)].add('D')
cursorY)].add('A')

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если response = input('> ').upper() в строке 101 заменить на
response = input('> ')?
2. Что будет, если canvasStr += '#' в строке 61 заменить на canvasStr += '@'?
3. Что будет, если canvasStr += ' ' в строке 89 заменить на canvasStr + = '.'?
4. Что будет, если выражение moves = [] в строке 94 заменить на moves = list
('SDWDDSASDSAAWASSDSAS')?

24
Разложение на множители

Множители заданного числа — другие1 два числа, произведение которых дает это заданное число. Например,
2 × 13 = 26, так что 2 и 13 — множители 26. Кроме того,
1 × 26 = 26, так что 1 и 26 — также множители 26. Следовательно, у 26 четыре множителя: 1, 2, 13 и 26.
Число, у которого только два множителя: 1 и оно само, называется простым. В противном случае оно называется составным. Найдите
новые простые числа с помощью нашей программы разложения на множители!2
(Подсказка: простые числа всегда заканчиваются на нечетную цифру, но не 5.)
Вдобавок в проекте 56 мы покажем, как вычислить их с помощью компьютера.
Математика в этой программе не слишком сложна, так что она идеально подходит
для начинающих.

1

Обычно само число также считается своим множителем, и далее автор тоже это упоминает. — Примеч. пер.

2

Термин «факторизация», полагаю, здесь не подходит, так как под ним обычно понимают
разложение именно на простые множители. — Примеч. пер.

Описание работы  

131

Программа в действии
Результат выполнения factorfinder.py выглядит следующим образом:
Factor Finder, by Al Sweigart al@inventwithpython.com
--сокращено-Enter a number to factor (or "QUIT" to quit):
> 26
1, 2, 13, 26
Enter a number to factor (or "QUIT" to quit):
> 4352784
1, 2, 3, 4, 6, 8, 12, 16, 24, 29, 48, 53, 58, 59, 87, 106, 116, 118, 159,
174, 177, 212, 232, 236, 318, 348, 354, 424, 464, 472, 636, 696, 708, 848,
944, 1272, 1392, 1416, 1537, 1711, 2544, 2832, 3074, 3127, 3422, 4611, 5133,
6148, 6254, 6844, 9222, 9381, 10266, 12296, 12508, 13688, 18444, 18762, 20532,
24592, 25016, 27376, 36888, 37524, 41064, 50032, 73776, 75048, 82128, 90683,
150096, 181366, 272049, 362732, 544098, 725464, 1088196, 1450928, 2176392,
4352784
Enter a number to factor (or "QUIT" to quit):
> 9787
1, 9787
Enter a number to factor (or "QUIT" to quit):
> quit

Описание работы
Узнать, является ли число множителем другого числа, можно, проверив, делится
ли это второе число на первое без остатка. Например, 7 — множитель 21, поскольку
21 ÷ 7 равно 3. Кроме того, это значит, что 3 — также множитель 21. Однако 8 — не
множитель 21, поскольку 21 ÷ 8 = 2,625. Дробная часть означает наличие остатка,
так что нацело они не делятся.
Оператор деления по модулю % демонстрирует, есть ли остаток: 21 % 7 оказывается
равно 0, так что остатка нет, и 7 — множитель 21, в то время как 21 % 8 равно 1, ненулевое значение, означающее что 8 — не один из множителей 21. Эта методика
используется в нашей программе разложения на множители в строке 35 для определения того, является ли число множителем.
Функция math.sqrt() возвращает квадратный корень передаваемого в нее числа.
Например, math.sqrt(25) возвращает 5.0, поскольку 5 в квадрате равно 25, а значит, 5 является квадратным корнем из 25.
1.
2.
3.
4.
5.
6.

"""Разложение на множители, (c) Эл Свейгарт al@inventwithpython.com
Находит все множители заданного числа
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, математическая"""
import math, sys

132   Проект 24. Разложение на множители
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.

print('''Factor Finder, by Al Sweigart al@inventwithpython.com
A number's factors are two numbers that, when multiplied with each
other, produce the number. For example, 2 x 13 = 26, so 2 and 13 are
factors of 26. 1 x 26 = 26, so 1 and 26 are also factors of 26. We
say that 26 has four factors: 1, 2, 13, and 26.
If a number only has two factors (1 and itself), we call that a prime
number. Otherwise, we call it a composite number.
Can you discover some prime numbers?
''')
while True: # Основной цикл программы.
print('Enter a positive whole number to factor (or QUIT):')
response = input('> ')
if response.upper() == 'QUIT':
sys.exit()
if not (response.isdecimal() and int(response) > 0):
continue
number = int(response)
factors = []
# Поиск множителей числа:
for i in range(1, int(math.sqrt(number)) + 1):
if number % i == 0: # Если остатка нет, значит — множитель.
factors.append(i)
factors.append(number // i)
# Преобразуем во множество, чтобы избавиться от повторов:
factors = list(set(factors))
factors.sort()
# Выводим результаты:
for i, factor in enumerate(factors):
factors[i] = str(factor)
print(', '.join(factors))

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если удалить или закомментировать factors.append(i) в строке 36?

Исследование программы  

133

2. Что будет, если удалить или закомментировать factors = list(set(factors))
в строке 40? (Подсказка: введите на входе квадрат какого-нибудь числа, например 25, 36 или 49.)
3. Что будет, если удалить или закомментировать factors.sort() в строке 41?
4. Какое сообщение об ошибке вы получите, если factors = [] в строке 31 замените на factors = ''?
5. Что будет, если factors = [] в строке 31 заменить на factors = [-42]?
6. Какое сообщение об ошибке вы получите, если factors = [] в строке 31 замените на factors = ['hello']?

25
Быстрый стрелок

Эта программа проверяет быстроту вашей реакции: вы
должны нажать Enter сразу же, как только увидите слово
DRAW. Однако осторожнее: нажмете Enter до появления
DRAW — и проиграете! Самый ли вы быстрый стрелок на
Диком Западе?

Программа в действии
Результат выполнения fastdraw.py выглядит следующим образом:
Fast Draw, by Al Sweigart al@inventwithpython.com
Time to test your reflexes and see if you are the fastest
draw in the west!
When you see "DRAW", you have 0.3 seconds to press Enter.
But you lose if you press Enter before "DRAW" appears.
Press Enter to begin...
It is high noon...
DRAW!
You took 0.3485 seconds to draw. Too slow!
Enter QUIT to stop, or press Enter to play again.
> quit
Thanks for playing!

Описание работы  

135

Описание работы
Функция input() приостанавливает программу в ожидании ввода пользователем
строкового значения. Столь простое поведение означает, что создавать игры в режиме реального времени с помощью одной только этой функции не получится.
Однако программы буферизуют вводимые с клавиатуры данные, так что, если
нажать клавиши C, A и T до вызова input(), они будут сохранены и появятся сразу
же после выполнения input().
Фиксация времени непосредственно перед вызовом input() в строке 22 и сразу
же после этого вызова в строке 24 позволяет определить, сколько времени потребовалось игроку, чтобы нажать Enter. Однако если данная клавиша была нажата до
вызова input(), то буферизованное нажатие Enter приведет к мгновенному возврату
из input() (обычно примерно за 3 миллисекунды). Поэтому в строке 26 мы проверяем, не было ли время меньше 0,01 секунды (то есть 10 миллисекунд), чтобы
выяснить, не нажал ли игрок Enter слишком рано.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.

"""Быстрый стрелок, (c) Эл Свейгарт al@inventwithpython.com
Проверьте свои рефлексы и узнайте, самый ли вы быстрый стрелок на Диком Западе.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, игра"""
import random, sys, time
print('Fast Draw, by Al Sweigart al@inventwithpython.com')
print()
print('Time to test your reflexes and see if you are the fastest')
print('draw in the west!')
print('When you see "DRAW", you have 0.3 seconds to press Enter.')
print('But you lose if you press Enter before "DRAW" appears.')
print()
input('Press Enter to begin...')
while True:
print()
print('It is high noon...')
time.sleep(random.randint(20, 50) / 10.0)
print('DRAW!')
drawTime = time.time()
input() # Возврат из этой функции не происходит до нажатия Enter.
timeElapsed = time.time() - drawTime
if timeElapsed < 0.01:
# Если игрок нажал Enter до появления DRAW!, возврат из вызова
# input() происходит практически мгновенно.
print('You drew before "DRAW" appeared! You lose.')
elif timeElapsed > 0.3:
timeElapsed = round(timeElapsed, 4)

136   Проект 25. Быстрый стрелок
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.

print('You took', timeElapsed, 'seconds to draw. Too slow!')
else:
timeElapsed = round(timeElapsed, 4)
print('You took', timeElapsed, 'seconds to draw.')
print('You are the fastest draw in the west! You win!')
print('Enter QUIT to stop, or press Enter to play again.')
response = input('> ').upper()
if response == 'QUIT':
print('Thanks for playing!')
sys.exit()

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если drawTime = time.time() в строке 22 заменить на drawTime = 0?
2. Что будет, если timeElapsed > 0.3 в строке 30 заменить на timeElapsed < 0.3?
3. Что будет, если time.time() - drawTime в строке 24 заменить на time.time() +
+ drawTime?
4. Что будет, если удалить или закомментировать input('Press Enter to
begin...') в строке 15?

26
Фибоначчи

Последовательность Фибоначчи —знаменитая математическая закономерность, открытие которой приписывается итальянскому математику Фибоначчи, жившему
в XIII веке (хотя на самом деле она была открыта еще
раньше). Последовательность начинается с 0 и 1, а каждое
следующее число равно сумме двух предыдущих. Последовательность бесконечна:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 ...
Последовательность Фибоначчи применяется при написании музыки, прогнозах
курсов акций, определении закономерностей соцветий в головках подсолнечника
и многих других сферах. Наша программа позволяет рассчитать столько чисел
Фибоначчи, сколько вам нужно. Больше информации об этой последовательности
можно найти в статье «Википедии»: https://ru.wikipedia.org/wiki/Числа_Фибоначчи.

Программа в действии
Результат выполнения fibonacci.py выглядит следующим образом:
Fibonacci Sequence, by Al Sweigart al@inventwithpython.com
--сокращено-Enter the Nth Fibonacci number you wish to
calculate (such as 5, 50, 1000, 9999), or QUIT to quit:
> 50

138   Проект 26. Фибоначчи
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584,
4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229,
832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817,
39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733,
1134903170, 1836311903, 2971215073, 4807526976, 7778742049

Описание работы
Поскольку числа Фибоначчи быстро вырастают до очень больших значений, в строках с 46-й по 50-ю мы проверяем, не ввел ли пользователь число, превышающее
10 000, и выводим в этом случае предупреждение, что отображение результатов
на экране может занять некоторое время. И хотя программа может выполнять
миллионы вычислений практически мгновенно, вывод текста на экран происходит
относительно медленно и может занимать несколько секунд. Предупреждение сообщает пользователю, что он всегда может прервать выполнение программы, нажав
сочетание клавиш Ctrl+C.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.

"""Последовательность Фибоначчи, (c) Эл Свейгарт al@inventwithpython.com
Вычисляет числа из последовательности Фибоначчи: 0, 1, 1, 2, 3, 5, 8, 13...
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, математическая"""
import sys
print('''Fibonacci Sequence, by Al Sweigart al@inventwithpython.com
The Fibonacci sequence begins with 0 and 1, and the next number is the
sum of the previous two numbers. The sequence continues forever:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987...
''')
while True: # Основной цикл программы.
while True: # Спрашиваем, пока пользователь не введет допустимое число.
print('Enter the Nth Fibonacci number you wish to')
print('calculate (such as 5, 50, 1000, 9999), or QUIT to quit:')
response = input('> ').upper()
if response == 'QUIT':
print('Thanks for playing!')
sys.exit()
if response.isdecimal() and int(response) != 0:
nth = int(response)
break # Когда пользователь ввел допустимое число — выходим из цикла
print('Please enter a number greater than 0, or QUIT.')

Описание работы  

31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.

print()
# Обработка частных случаев, если пользователь ввел 1 или 2:
if nth == 1:
print('0')
print()
print('The #1 Fibonacci number is 0.')
continue
elif nth == 2:
print('0, 1')
print()
print('The #2 Fibonacci number is 1.')
continue
# Отображаем предупреждение, если пользователь ввел большое число:
if nth >= 10000:
print('WARNING: This will take a while to display on the')
print('screen. If you want to quit this program before it is')
print('done, press Ctrl-C.')
input('Press Enter to begin...')
# Вычисляем N-е число Фибоначчи:
secondToLastNumber = 0
lastNumber = 1
fibNumbersCalculated = 2
print('0, 1, ', end='') # Выводим первые два числа Фибоначчи.
# Выводим все остальные числа Фибоначчи:
while True:
nextNumber = secondToLastNumber + lastNumber
fibNumbersCalculated += 1
# Выводим следующее число последовательности:
print(nextNumber, end='')
# Проверяем, нашли ли мы требуемое пользователем N-е число:
if fibNumbersCalculated == nth:
print()
print()
print('The #', fibNumbersCalculated, ' Fibonacci ',
'number is ', nextNumber, sep='')
break
# Выводим запятую между членами последовательности:
print(', ', end='')
# Заменяем последние два числа:
secondToLastNumber = lastNumber
lastNumber = nextNumber

139

140   Проект 26. Фибоначчи
Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
использовать отличные от 0 и 1 начальные числа;
вычислять следующее число как сумму трех, а не двух предыдущих.

Исследование программы
Это очень простая программа, так что вариантов ее модификации не так уж много.
Вместо этого задумайтесь: для чего ее можно использовать? И какие еще полезные
последовательности можно вычислить программным образом?

27
Аквариум

Наблюдайте за вашими собственными виртуальными
рыбками в виртуальном аквариуме, заполненном пузырьками воздуха и крупными водорослями! При каждом
запуске программа генерирует случайным образом рыбок
различных видов и цветов. Сделайте паузу и насладитесь
успокаивающей тишью этого программного аквариума или
попробуйте запрограммировать виртуальных акул, чтобы вселить
ужас в его обитателей! Данную программу нельзя запустить из IDE или редактора,
поскольку она использует модуль bext и для правильного отображения требует
запуска из командной строки или терминала. Больше информации о модуле bext
можно найти по адресу https://pypi.org/project/bext/.

142   Проект 27. Аквариум

Программа в действии
На рис. 27.1 показано, как выглядит результат выполнения fishtank.py.

Рис. 27.1. Результат работы программы-аквариума с несколькими рыбками,
крупными водорослями и пузырьками воздуха

Описание работы
Современные графические программы для генерации динамических изображений
часто полностью очищают все окно и перерисовывают его 30 или 60 раз в секунду.
Получается частота кадров (frame rate, FPS) 30 или 60 кадров в секунду. Чем выше
FPS, тем более плавным выглядит движение.
Вывод в окно терминала происходит намного медленнее. Если очищать все окно
терминала для перерисовки его содержимого с помощью модуля bext, то получится
только около 3 или 4 FPS, а значит, мерцание изображения в окне будет довольно
заметным.
Можно ускорить этот процесс, рисуя символы лишь в тех частях экрана терминала,
которые поменялись. Большую часть вывода программы-аквариума составляет
пустое пространство, так что функции clearAquarium() для имитации движения
элементов достаточно только выводить символы пробела ' ' там, где сейчас
находятся рыбки, водоросли и пузырьки. В результате частота кадров растет,

Описание работы  

143

мерцание изображения уменьшается и смотреть на изображение аквариума гораздо приятнее.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.

"""Аквариум, (c) Эл Свейгарт al@inventwithpython.com
Безмятежное динамическое изображение аквариума. Нажмите Ctrl+C для останова.
Аналогична ASCIIQuarium и @EmojiAquarium, но моя программа основана на
более старой программе ASCII-аквариума под DOS.
https://robobunny.com/projects/asciiquarium/html/
https://twitter.com/EmojiAquarium
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: очень большая, графика, bext"""
import random, sys, time
try:

import bext
except ImportError:
print('This program requires the bext module, which you')
print('can install by following the instructions at')
print('https://pypi.org/project/Bext/')
sys.exit()
# Задаем константы:
WIDTH, HEIGHT = bext.size()
# В Windows нельзя вывести что-либо в последнем столбце без добавления
# автоматически символа новой строки, поэтому уменьшаем ширину на 1:
WIDTH -= 1
NUM_KELP = 2 # (!) Попробуйте заменить это значение на 10.
NUM_FISH = 10 # (!) Попробуйте заменить это значение на 2 или 100.
NUM_BUBBLERS = 1 # (!) Попробуйте заменить это значение на 0 или 10.
FRAMES_PER_SECOND = 4 # (!) Попробуйте заменить это число на 1 или 60.
# (!) Попробуйте изменить константы, чтобы получился аквариум с одними
# только водорослями или пузырьками.
# Примечание: все строковые значения в ассоциативном массиве рыбок должны быть
# одинаковой длины.
FISH_TYPES = [
{'right': ['>'],
'left': ['||>'],
'left': [''],
'left': ['||.'], 'left': ['o||)).'], 'left': ['o[['],
'left': [''],
'left': ['',
'-._.-._^=>', '._.-._.^=>'],
'left': [' ').upper()
171.
if response == 'QUIT':
172.
print('Thanks for playing!')
173.
sys.exit()
174.
if displayMode == COLOR_MODE and response in tuple('RGBYCP'):
175.
# Возвращаем номер типа элемента в зависимости от response:
176.
return {'R': 0, 'G': 1, 'B': 2,
177.
'Y': 3, 'C': 4, 'P': 5}[response]
178.
if displayMode == SHAPE_MODE and response in tuple('HTDBCS'):
179.
# Возвращаем номер типа элемента в зависимости от response:
180.
return {'H': 0, 'T': 1, 'D':2,
181.
'B': 3, 'C': 4, 'S': 5}[response]
182.
183.
184. def changeTile(tileType, board, x, y, charToChange=None):
185.
"""Меняем цвет/форму клетки с помощью алгоритма рекурсивной
186.
заливки."""
187.
if x == 0 and y == 0:
188.
charToChange = board[(x, y)]
189.
if tileType == charToChange:
190.
return # Простейший случай: тот же самый элемент.
191.
192.
board[(x, y)] = tileType
193.
194.
if x > 0 and board[(x - 1, y)] == charToChange:
195.
# Рекурсивно: меняем левый соседний элемент:
196.
changeTile(tileType, board, x - 1, y, charToChange)

155

156   Проект 28. Заливка
197.
if y > 0 and board[(x, y - 1)] == charToChange:
198.
# Рекурсивно: меняем верхний соседний элемент:
199.
changeTile(tileType, board, x, y - 1, charToChange)
200.
if x < BOARD_WIDTH - 1 and board[(x + 1, y)] == charToChange:
201.
# Рекурсивно: меняем правый соседний элемент:
202.
changeTile(tileType, board, x + 1, y, charToChange)
203.
if y < BOARD_HEIGHT - 1 and board[(x, y + 1)] == charToChange:
204.
# Рекурсивно: меняем нижний соседний элемент:
205.
changeTile(tileType, board, x, y + 1, charToChange)
206.
207.
208. def hasWon(board):
209.
"""Возвращает True, если вся доска — одного цвета/формы."""
210.
tile = board[(0, 0)]
211.
212.
for x in range(BOARD_WIDTH):
213.
for y in range(BOARD_HEIGHT):
214.
if board[(x, y)] != tile:
215.
return False
216.
return True
217.
218.
219. # Если программа не импортируется, а запускается, выполняем запуск:
220. if __name__ == '__main__':
221.
main()

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
добавить дополнительные формы и цвета;
создать другие формы игральной доски, помимо прямоугольной.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Какое сообщение об ошибке вы получите, если board = {} в строке 92 замените на board = []?
2. Какое сообщение об ошибке вы получите, если return board в строке 105
замените на return None?
3. Что будет, если movesLeft -= 1 в строке 76 заменить на movesLeft -= 0?

29
Моделирование
лесного пожара

В данной программе мы моделируем лес, деревья в котором непрерывно растут, а затем сгорают. На каждом этапе
моделирования существует равная 1 % вероятность того,
что на пустом месте вырастет дерево, а также 1 % вероятности того, что в дерево попадет молния и оно сгорит. Пожары
распространяются на непосредственно прилегающие к нему
деревья, поэтому более густой лес скорее пострадает от сильного
пожара, чем разреженный. Данная программа была создана под впечатлением от
программы Emoji Sim Ники Кейс (Nicky Case), размещенной по адресу http://nca­
se.me/simulating/model/.

158   Проект 29. Моделирование лесного пожара

Программа в действии
На рис. 29.1 показано, как выглядит результат выполнения forestfiresim.py.

Рис. 29.1. Моделирование лесных пожаров, зеленые A обозначают деревья,
а красные W — пожары

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

Описание работы  

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.

159

"""Моделирование лесных пожаров, (c) Эл Свейгарт al@inventwithpython.com
Имитационное моделирование распространения лесных пожаров. Нажмите Ctrl+C
для останова.
По мотивам программы Emoji Sim Ники Кейс http://ncase.me/simulating/model/
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, bext, имитационное моделирование"""
import random, sys, time
try:

import bext
except ImportError:
print('This program requires the bext module, which you')
print('can install by following the instructions at')
print('https://pypi.org/project/Bext/')
sys.exit()
# Задаем константы:
WIDTH = 79
HEIGHT = 22
TREE = 'A'
FIRE = 'W'
EMPTY = ' '
# (!) Попробуйте заменить эти параметры на что-либо в диапазоне от 0.0 до 1.0:
INITIAL_TREE_DENSITY = 0.20 # Начальное заполнение леса деревьями.
GROW_CHANCE = 0.01 # Вероятность превращения пустого места в дерево.
FIRE_CHANCE = 0.01 # Вероятность попадания в дерево молнии и его сгорания.
# (!) Попробуйте задать длительность паузы равной 1.0 или 0.0:
PAUSE_LENGTH = 0.5
def main():
forest = createNewForest()
bext.clear()
while True: # Основной цикл программы.
displayForest(forest)
# Отдельный шаг моделирования:
nextForest = {'width': forest['width'],
'height': forest['height']}
for x in range(forest['width']):
for y in range(forest['height']):
if (x, y) in nextForest:
# Если значение nextForest[(x, y)] уже было задано
# на предыдущей итерации, ничего не делаем:
continue

160   Проект 29. Моделирование лесного пожара
52.
if ((forest[(x, y)] == EMPTY)
53.
and (random.random() 4
1234567
+-------+
|.......|
|.......|
|XXX.XO.|
|OOOOXO.|
|OOOXOX.|
|OXXXOXX|
+-------+
Player O has won!

Описание работы
Структура программ в проектах настольных игр в этой книге похожа и обычно
включает ассоциативный массив или список для хранения состояния доски,
функцию getNewBoard(), возвращающую структуру данных для доски, функцию
displayBoard() для визуализации структуры данных для доски на экране и т. д.
Можете посмотреть на прочие проекты в данной книге с тегом настольная игра
и сравнить их друг с другом, особенно если планируете сами писать свои программы
для настольных игр.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.

"""Четыре в ряд, (c) Эл Свейгарт al@inventwithpython.com
Игра с отбрасыванием игровых элементов, требующая выстроить
четыре в ряд, аналогично игре Connect Four
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, игра, настольная игра, для двух игроков"""
import sys
# Константы, необходимые для отображения доски:
EMPTY_SPACE = '.' # Точки считать проще, чем пробелы.
PLAYER_X = 'X'
PLAYER_O = 'O'

164   Проект 30. Четыре в ряд
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.

# Не забудьте поменять displayBoard() & COLUMN_LABELS, если меняете BOARD_WIDTH.
BOARD_WIDTH = 7
BOARD_HEIGHT = 6
COLUMN_LABELS = ('1', '2', '3', '4', '5', '6', '7')
assert len(COLUMN_LABELS) == BOARD_WIDTH
def main():
print("""Four in a Row, by Al Sweigart al@inventwithpython.com
Two players take turns dropping tiles into one of seven columns, trying
to make four in a row horizontally, vertically, or diagonally.
""")
# Начинаем новую игру:
gameBoard = getNewBoard()
playerTurn = PLAYER_X
while True: # Очередь играть одного из игроков.
# Отображаем доску и даем игроку возможность сделать ход:
displayBoard(gameBoard)
playerMove = askForPlayerMove(playerTurn, gameBoard)
gameBoard[playerMove] = playerTurn
# Проверяем, выиграл ли игрок или не достигнута ли ничья:
if isWinner(playerTurn, gameBoard):
displayBoard(gameBoard) # Отображаем доску один последний раз.
print('Player ' + playerTurn + ' has won!')
sys.exit()
elif isFull(gameBoard):
displayBoard(gameBoard) # Отображаем доску один последний раз.
print('There is a tie!')
sys.exit()
# Переход хода к другому игроку:
if playerTurn == PLAYER_X:
playerTurn = PLAYER_O
elif playerTurn == PLAYER_O:
playerTurn = PLAYER_X
def getNewBoard():
"""Возвращает ассоциативный массив, соответствующий игральной доске
для Connect Four.
Ключами служат кортежи (columnIndex, rowIndex) из двух целых чисел,
а значениями — одно из строковых значений 'X', 'O' или '.' (пустое
место)."""
board = {}
for columnIndex in range(BOARD_WIDTH):
for rowIndex in range(BOARD_HEIGHT):
board[(columnIndex, rowIndex)] = EMPTY_SPACE

Описание работы  

165

64.
return board
65.
66. def displayBoard(board):
67.
"""Отображает доску и все клетки на экране."""
68.
69.
'''Подготовка списка для передачи в строковый метод format()
70.
для шаблона доски. В этом списке хранятся все клетки доски
71.
(и пустые участки) слева направо, сверху вниз:'''
72.
tileChars = []
73.
for rowIndex in range(BOARD_HEIGHT):
74.
for columnIndex in range(BOARD_WIDTH):
75.
tileChars.append(board[(columnIndex, rowIndex)])
76.
77.
# Отображаем доску:
78.
print("""
79.
1234567
80.
+-------+
81.
|{}{}{}{}{}{}{}|
82.
|{}{}{}{}{}{}{}|
83.
|{}{}{}{}{}{}{}|
84.
|{}{}{}{}{}{}{}|
85.
|{}{}{}{}{}{}{}|
86.
|{}{}{}{}{}{}{}|
87.
+-------+""".format(*tileChars))
88.
89.
90. def askForPlayerMove(playerTile, board):
91.
"""Даем возможность игроку выбрать столбец на доске
92.
для размещения элемента.
93.
Возвращает кортеж (столбец, строка), в который попадет элемент."""
94.
while True: # Запрашиваем игрока, пока не будет сделан допустимый ход.
95.
print('Player {}, enter a column or QUIT:'.format(playerTile))
96.
response = input('> ').upper().strip()
97.
98.
if response == 'QUIT':
99.
print('Thanks for playing!')
100.
sys.exit()
101.
102.
if response not in COLUMN_LABELS:
103.
print('Enter a number from 1 to {}.'.format(BOARD_WIDTH))
104.
continue # Снова спрашиваем у игрока ход.
105.
106.
columnIndex = int(response) - 1 # -1 for 0-based the index.
107.
108.
# Если столбец заполнен, снова запрашиваем ход:
109.
if board[(columnIndex, 0)] != EMPTY_SPACE:
110.
print('That column is full, select another one.')
111.
continue # Снова спрашиваем у игрока ход.
112.
113.
# Начиная снизу, ищем первый пустой участок.
114.
for rowIndex in range(BOARD_HEIGHT - 1, -1, -1):

166   Проект 30. Четыре в ряд
115.
if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
116.
return (columnIndex, rowIndex)
117.
118.
119. def isFull(board):
120.
"""Возвращает True, если в `board` нет пустых участков,
121.
в противном случае возвращает False."""
122.
for rowIndex in range(BOARD_HEIGHT):
123.
for columnIndex in range(BOARD_WIDTH):
124.
if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
125.
return False # Нашли пустой участок и возвращаем False.
126.
return True # Все участки заполнены.
127.
128.
129. def isWinner(playerTile, board):
130.
"""Возвращает True, если `playerTile` содержит в одной строке
131.
на `board` четыре элемента в ряд, иначе возвращает False."""
132.
133.
# Проходим по всей доске в поисках четырех в ряд:
134.
for columnIndex in range(BOARD_WIDTH - 3):
135.
for rowIndex in range(BOARD_HEIGHT):
136.
# Ищем горизонтальные четыре в ряд, двигаясь вправо:
137.
tile1 = board[(columnIndex, rowIndex)]
138.
tile2 = board[(columnIndex + 1, rowIndex)]
139.
tile3 = board[(columnIndex + 2, rowIndex)]
140.
tile4 = board[(columnIndex + 3, rowIndex)]
141.
if tile1 == tile2 == tile3 == tile4 == playerTile:
142.
return True
143.
144.
for columnIndex in range(BOARD_WIDTH):
145.
for rowIndex in range(BOARD_HEIGHT - 3):
146.
# Ищем вертикальные четыре в ряд, двигаясь вниз:
147.
tile1 = board[(columnIndex, rowIndex)]
148.
tile2 = board[(columnIndex, rowIndex + 1)]
149.
tile3 = board[(columnIndex, rowIndex + 2)]
150.
tile4 = board[(columnIndex, rowIndex + 3)]
151.
if tile1 == tile2 == tile3 == tile4 == playerTile:
152.
return True
153.
154.
for columnIndex in range(BOARD_WIDTH - 3):
155.
for rowIndex in range(BOARD_HEIGHT - 3):
156.
# Ищем четыре в ряд, двигаясь вправо вниз по диагонали:
157.
tile1 = board[(columnIndex, rowIndex)]
158.
tile2 = board[(columnIndex + 1, rowIndex + 1)]
159.
tile3 = board[(columnIndex + 2, rowIndex + 2)]
160.
tile4 = board[(columnIndex + 3, rowIndex + 3)]
161.
if tile1 == tile2 == tile3 == tile4 == playerTile:
162.
return True
163.
164.
# Ищем четыре в ряд, двигаясь влево вниз по диагонали:
165.
tile1 = board[(columnIndex + 3, rowIndex)]

Исследование программы  

167

166.
tile2 = board[(columnIndex + 2, rowIndex + 1)]
167.
tile3 = board[(columnIndex + 1, rowIndex + 2)]
168.
tile4 = board[(columnIndex, rowIndex + 3)]
169.
if tile1 == tile2 == tile3 == tile4 == playerTile:
170.
return True
171.
return False
172.
173.
174. # Если программа не импортируется, а запускается, производим запуск:
175. if __name__ == '__main__':
176.
main()

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
создать варианты «три в ряд» и «пять в ряд»;
создать вариант данной игры для трех игроков;
добавить «джокерный» элемент, случайным образом пропадающий после
ходов всех игроков, который может использовать любой из игроков;
добавить «заблокированные» элементы, не доступные для использования
никому из игроков.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если PLAYER_O = 'O' в строке 11 заменить на PLAYER_O = 'X'?
2. Что будет, если return (columnIndex, rowIndex) в строке 116 заменить на
return (columnIndex, 0)?
3. Что будет, если response == 'QUIT' в строке 98 заменить на response != 'QUIT'?
4. Какое сообщение об ошибке вы получите, если tileChars = [] в строке 72
замените на tileChars = {}?

31
Угадай число

«Угадай число» — классическая игра, на которой начинающие могут попрактиковаться в программировании.
В этой игре компьютер загадывает случайное число от 1
до 100. У игрока есть 10 попыток угадать его. После каждой
из попыток компьютер говорит игроку, больше или меньше
загаданного это число.

Программа в действии
Результат выполнения guess.py выглядит следующим образом:
Guess the Number, by Al Sweigart al@inventwithpython.com
I am thinking of a number between 1 and 100.
You have 10 guesses left. Take a guess.
> 50
Your guess is too high.
You have 9 guesses left. Take a guess.
> 25
Your guess is too low.
--snip-You have 5 guesses left. Take a guess.
> 42
Yay! You guessed my number!

Описание работы  

169

Описание работы
В программе «Угадай число» используется несколько базовых идей программирования: циклы, операторы if-else, функции, вызовы методов и случайные числа. Модуль
random языка Python позволяет генерировать псевдослучайные числа — числа, выглядящие случайными, но на самом деле таковыми не являющиеся. Генерировать
псевдослучайные числа компьютеру проще, чем действительно случайные, и они
считаются «достаточно случайными» для таких приложений, как компьютерные
игры и некоторые научные модели.
Модуль random языка Python генерирует псевдослучайные числа, исходя из заданного начального значения, и все последовательности псевдослучайных чисел,
сгенерированных на основе одного начального значения, будут одинаковы. Например, введите следующие команды в интерактивную командную оболочку:
>>> import random
>>> random.seed(42)
>>> random.randint(1, 10); random.randint(1, 10); random.randint(1, 10)
2
1
5

Если перезапустить командную оболочку и выполнить этот код снова, то будут сгенерированы те же псевдослучайные числа: 2, 1, 5. Компьютерная игра Minecraft генерирует свои псевдослучайные виртуальные миры на основе начальных значений,
поэтому различные игроки могут воссоздать один и тот же мир, воспользовавшись
одним начальным значением.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.

"""Угадай число, (c) Эл Свейгарт al@inventwithpython.com
Угадайте загаданное число по подсказкам.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, игра"""
import random
def askForGuess():
while True:
guess = input('> ')

# Введите свое предположение.

if guess.isdecimal():
return int(guess)

# Преобразуем строковое представление
# предположения в число.
print('Please enter a number between 1 and 100.')
print('Guess the Number, by Al Sweigart al@inventwithpython.com')
print()
secretNumber = random.randint(1, 100) # Выбираем случайное число.

170   Проект 31. Угадай число
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.

print('I am thinking of a number between 1 and 100.')
for i in range(10): # У игрока есть 10 попыток.
print('You have {} guesses left. Take a guess.'.format(10 - i))
guess = askForGuess()
if guess == secretNumber:
break # Если число угадано — выходим из цикла.
# Даем подсказку:
if guess < secretNumber:
print('Your guess is too low.')
if guess > secretNumber:
print('Your guess is too high.')
# Раскрываем результаты:
if guess == secretNumber:
print('Yay! You guessed my number!')
else:
print('Game over. The number I was thinking of was', secretNumber)

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
создать игру «Угадай букву», подсказки в которой основаны на алфавитном
порядке букв;
сделать так, чтобы в подсказках после каждой попытки выводилось «теплее»
или «холоднее», в зависимости от предыдущей попытки игрока.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если input('> ') в строке 11 заменить на input(secretNumber)?
2. Какое сообщение об ошибке вы получите, если return int(guess) в строке 14
замените на return guess?
3. Что будет, если random.randint(1, 100) в строке 20 заменить на random.ran­
dint(1, 1)?
4. Что будет, если format(10 - i) в строке 24 заменить на format(i)?
5. Какое сообщение об ошибке вы получите, если guess == secretNumber в строке 37 замените на guess = secretNumber?

32
Простак

В этой короткой и простой программе вы изучите тайное
тонкое искусство заинтриговать простака на многие часы.
Я не стану раскрывать здесь соль шутки. Просто скопируйте код и запустите его. Данный проект замечательно
подходит для начинающих.

Программа в действии
Результат выполнения gullible.py выглядит следующим образом:
Gullible, by Al Sweigart al@inventwithpython.com
Do you want to know how to keep a gullible person busy
> y
Do you want to know how to keep a gullible person busy
> y
Do you want to know how to keep a gullible person busy
> yes
Do you want to know how to keep a gullible person busy
> YES
Do you want to know how to keep a gullible person busy
> TELL ME HOW TO KEEP A GULLIBLE PERSON BUSY FOR HOURS
"TELL ME HOW TO KEEP A GULLIBLE PERSON BUSY FOR HOURS"
response.
Do you want to know how to keep a gullible person busy
> y

for hours? Y/N
for hours? Y/N
for hours? Y/N
for hours? Y/N
for hours? Y/N
is not a valid yes/no
for hours? Y/N

172   Проект 32. Простак
Do you want to know how to keep a gullible person busy for hours? Y/N
> y
Do you want to know how to keep a gullible person busy for hours? Y/N
> n
Thank you. Have a nice day!

Описание работы
Для удобства использования ваши программы должны стремиться интерпретировать целый спектр данных, которые может ввести пользователь. Например, данная
программа задает пользователю вопрос, предполагающий ответ типа «да/нет», но
игроку будет проще ввести y или n вместо полных слов. Программа также понимает, что хочет пользователь, если нажата клавиша Caps Lock, поскольку вызывает
строковый метод lower() для введенного игроком строкового значения. Таким
образом, 'y', 'yes', 'Y', 'Yes' и 'YES' интерпретируются программой одинаково.
То же самое относится и к отрицательному ответу пользователя.
1.
2.
3.
4.
5.
6.
7.
8.
9.

"""Простак, (c) Эл Свейгарт al@inventwithpython.com
Как заинтриговать простака на многие часы (программа-шутка).
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, юмор"""
print('Gullible, by Al Sweigart al@inventwithpython.com')
while True: # Основной цикл программы.
print('Do you want to know how to keep a gullible person busy for
hours? Y/N')
response = input('> ') # Получаем ответ пользователя.
if response.lower() == 'no' or response.lower() == 'n':
break # В случае "no" выходим из цикла.
if response.lower() == 'yes' or response.lower() == 'y':
continue # В случае "yes" продолжаем с начала цикла.
print('"{}" is not a valid yes/no response.'.format(response))

10.
11.
12.
13.
14.
15.
16.
17. print('Thank you. Have a nice day!')

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если response.lower() == 'no' в строке 11 заменить на respon­
se.lo­wer() != 'no'?
2. Что будет, если while True: в строке 8 заменить на while False:?

33
Мини-игра со взломом

В этой игре игрок должен взломать компьютер, угадав
слово из семи букв, используемое в качестве секретного
пароля. Блоки памяти компьютера выдают возможные
слова, а игрок получает подсказки относительно того, насколько близки его догадки к правильному паролю. Например, если секретный пароль — MONITOR, а игрок ввел
CONTAIN, подсказка будет содержать информацию о совпадении
двух из семи букв, поскольку и MONITOR, и CONTAIN содержат буквы O и N —
вторую и третью соответственно. Эта игра напоминает проект 1 и игру с взломом
в серии компьютерных игр Fallout.

Программа в действии
Результат выполнения hacking.py выглядит следующим образом:
Hacking Minigame, by Al Sweigart al@inventwithpython.com
Find the password in the computer's memory:
0x1150 $],>@|~~RESOLVE^
0x1250 {>+){-;/DESPITE
0x1180 %[!]{REFUGEE@?~,
0x1280 }/.}!-DISPLAY%%/
0x1190 _[^%[@}^>,:*%?_?@+{%#.
0x11a0 )?~/)+PENAltY?-=
0x12a0 >[,?*#IMPROVE@$/
--сокращено--

174   Проект 33. Мини-игра со взломом
Enter password: (4 tries remaining)
> resolve
Access Denied (2/7 correct)
Enter password: (3 tries remaining)
> improve
A C C E S S
G R A N T E D

Описание работы
В названии этой игры упоминается взлом, но никакого настоящего взлома не происходит. Если бы мы просто перечислили на экране возможные слова, то игровой
процесс остался бы таким же. Однако косметические дополнения, имитирующие
банки памяти компьютера, придают восхитительное ощущение взлома компьютера.
Внимание к нюансам и впечатлению пользователя от работы с программой превращает простую и скучную игру в по-настоящему увлекательную.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.

"""Мини-игра со взломом, (c) Эл Свейгарт al@inventwithpython.com
Мини-игра со взломом из Fallout 3. Найдите семибуквенное слово-пароль
с помощью подсказок, возвращаемых при каждой попытке угадать его.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, графика, игра, головоломка"""
# Примечание: для этой программы необходим файл sevenletterwords.txt.
# Скачать его можно на https://inventwithpython.com/sevenletterwords.txt
import random, sys
# Задаем константы:
# Заполнитель мусорными символами для "памяти компьютера".
GARBAGE_CHARS = '~!@#$%^&*()_+-={}[]|;:,.?/'
# Загружаем список WORDS из текстового файла с семибуквенными словами.
with open('sevenletterwords.txt') as wordListFile:
WORDS = wordListFile.readlines()
for i in range(len(WORDS)):
# Преобразуем каждое слово в верхний регистр и удаляем символ новой строки
# в конце:
WORDS[i] = WORDS[i].strip().upper()
def main():
"""Запуск одной игры со взломом."""
print('''Hacking Minigame, by Al Sweigart al@inventwithpython.com
Find the password in the computer's memory. You are given clues after
each guess. For example, if the secret password is MONITOR but the
player guessed CONTAIN, they are given the hint that 2 out of 7 letters
were correct, because both MONITOR and CONTAIN have the letter O and N
as their 2nd and 3rd letter. You get four guesses.\n''')
input('Press Enter to begin...')

Описание работы  

175

34.
gameWords = getWords()
35.
# "Память компьютера" — только для вида, но выглядит круто:
36.
computerMemory = getComputerMemoryString(gameWords)
37.
secretPassword = random.choice(gameWords)
38.
39.
print(computerMemory)
40.
# Начинаем с четырех оставшихся попыток и постепенно уменьшаем
41
# их количество:
42.
for triesRemaining in range(4, 0, -1):
43.
playerMove = askForPlayerGuess(gameWords, triesRemaining)
44.
if playerMove == secretPassword:
45.
print('A C C E S S
G R A N T E D')
46.
return
47.
else:
48.
numMatches = numMatchingLetters(secretPassword, playerMove)
49.
print('Access Denied ({}/7 correct)'.format(numMatches))
50.
print('Out of tries. Secret password was {}.'.format(secretPassword))
51.
52. def getWords():
53.
"""Возвращает список из 12 слов — возможных паролей.
54.
55.
Секретный пароль будет первым словом в списке.
56.
Ради честной игры мы попытаемся гарантировать наличие слов
57.
с различным количеством совпадающих с паролем букв."""
58.
secretPassword = random.choice(WORDS)
59.
words = [secretPassword]
60.
61.
# Находим еще два слова; количество совпадающих букв — 0.
62.
# "< 3" потому, что секретный пароль уже входит в список слов.
63.
while len(words) < 3:
64.
randomWord = getOneWordExcept(words)
65.
if numMatchingLetters(secretPassword, randomWord) == 0:
66.
words.append(randomWord)
67.
68.
# Находим два слова, у которых совпадает три буквы
69.
# (но прекращаем поиск после 500 попыток, если не удалось найти).
70.
for i in range(500):
71.
if len(words) == 5:
72.
break # Нашли пять слов, так что выходим из цикла.
73.
74.
randomWord = getOneWordExcept(words)
75.
if numMatchingLetters(secretPassword, randomWord) == 3:
76.
words.append(randomWord)
77.
78.
# Находим по крайней мере семь слов, у которых совпадает хотя бы одна
79.
# буква (но прекращаем поиск после 500 попыток, если не удалось найти).
80.
for i in range(500):
81.
if len(words) == 12:
82.
break # Нашли 7 или более слов, так что выходим из цикла.
83.
84.
randomWord = getOneWordExcept(words)

176   Проект 33. Мини-игра со взломом
85.
if numMatchingLetters(secretPassword, randomWord) != 0:
86.
words.append(randomWord)
87.
88.
# Добавляем любые случайные слова, чтобы всего их было 12.
89.
while len(words) < 12:
90.
randomWord = getOneWordExcept(words)
91.
words.append(randomWord)
92.
93.
assert len(words) == 12
94.
return words
95.
96.
97. def getOneWordExcept(blocklist=None):
98.
"""Возвращает случайное слово из списка WORDS, не входящее в blocklist."""
99.
if blocklist == None:
100.
blocklist = []
101.
102.
while True:
103.
randomWord = random.choice(WORDS)
104.
if randomWord not in blocklist:
105.
return randomWord
106.
107.
108. def numMatchingLetters(word1, word2):
109.
"""Возвращает число совпадающих букв в указанных двух словах."""
110.
matches = 0
111.
for i in range(len(word1)):
112.
if word1[i] == word2[i]:
113.
matches += 1
114.
return matches
115.
116.
117. def getComputerMemoryString(words):
118.
"""Возвращает строковое значение, соответствующее "памяти компьютера"."""
119.
120.
# Выбираем по одной содержащей слово строке. Всего строк 16,
121.
# но они разбиты на две половины.
122.
linesWithWords = random.sample(range(16 * 2), len(words))
123.
# Начальный адрес памяти (также только для вида).
124.
memoryAddress = 16 * random.randint(0, 4000)
125.
# Создаем строковое значение для "памяти компьютера".
126.
computerMemory = [] # Будет включать 16 строковых значений, по одному
127.
# на строку.
128.
nextWord = 0 # Индекс в WORDS слова, помещаемого в строку.
129.
for lineNum in range(16): # "Память компьютера" содержит 16 строк.
130.
# Создаем половину строки мусорных символов:
131.
leftHalf = ''
132.
rightHalf = ''
133.
for j in range(16): # Каждая половина содержит 16 символов.
134.
leftHalf += random.choice(GARBAGE_CHARS)
135.
rightHalf += random.choice(GARBAGE_CHARS)

Описание работы  

177

136.
137.
# Заполняем пароль из WORDS:
138.
if lineNum in linesWithWords:
139.
# Находим случайное место для вставки слова в половине строки:
140.
insertionIndex = random.randint(0, 9)
141.
# Вставляем слово:
142.
leftHalf = (leftHalf[:insertionIndex] + words[nextWord]
143.
+ leftHalf[insertionIndex + 7:])
144.
nextWord += 1 # Обновляем слово для вставки в половину строки.
145.
if lineNum + 16 in linesWithWords:
146.
# Находим случайное место в половине строки для вставки слова:
147.
insertionIndex = random.randint(0, 9)
148.
# Вставляем слово:
149.
rightHalf = (rightHalf[:insertionIndex] + words[nextWord]
150.
+ rightHalf[insertionIndex + 7:])
151.
nextWord += 1 # Обновляем слово для вставки в половину строки.
152.
153.
computerMemory.append('0x' + hex(memoryAddress)[2:].zfill(4)
154.
+ ' ' + leftHalf + '
'
155.
+ '0x' + hex(memoryAddress + (16*16))[2:].zfill(4)
156.
+ ' ' + rightHalf)
157.
158.
memoryAddress += 16 # Перескакиваем, скажем, с 0xe680 на 0xe690.
159.
160.
# Все строковые значения списка computerMemory для возвращения
161.
# объединяются в одно большое строковое значение:
162.
return '\n'.join(computerMemory)
163.
164.
165. def askForPlayerGuess(words, tries):
166.
"""Ввод пользователем догадки."""
167.
while True:
168.
print('Enter password: ({} tries remaining)'.format(tries))
169.
guess = input('> ').upper()
170.
if guess in words:
171.
return guess
172.
print('That is not one of the possible passwords listed above.')
173.
print('Try entering "{}" or "{}".'.format(words[0], words[1]))
174.
175.
175. # Если программа не импортируется, а запускается, производим запуск:
177. if __name__ == '__main__':
178.
try:
179.
main()
180.
except KeyboardInterrupt:
181.
sys.exit() # Если нажато Ctrl+C — завершаем программу.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:

178   Проект 33. Мини-игра со взломом
найдите список слов в интернете и создайте собственный файл
sevenletterwords.txt, возможно, с шести- или восьмибуквенными словами;
создайте другую визуализацию «памяти компьютера».

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если for j in range(16): в строке 133 заменить на for j in range(0):?
2. Что будет, если GARBAGE_CHARS = '~!@#$%^&*()_+-={}[]|;:,.?/' в строке 14
заменить на GARBAGE_CHARS = '.'?
3. Что будет, если gameWords = getWords() в строке 34 заменить на gameWords =
['MALKOVICH'] * 20?
4. Какое сообщение об ошибке вы получите, если return words в строке 94 замените на return?
5. Что будет, если randomWord = random.choice(WORDS) в строке 103 заменить на
secretPassword = 'PASSWORD'?

34

«Виселица»» и «Гильотина»

В этой классической игре со словами необходимо угадывать буквы в загаданном слове. За каждую неправильную
букву рисуется дополнительная часть виселицы. Попробуйте угадать слово целиком, прежде чем будет нарисована
вся виселица. В роли загаданных слов в этой версии —названия животных, например RABBIT и PIGEON, но вы можете
заменить их собственным набором слов.
В переменной HANGMAN_PICS содержатся строковые значения для ASCII-графики,
отражающей каждый шаг рисования виселицы:
+--+
| |
|
|
|
|
=====

+--+
| |
O |
|
|
|
=====

+--+
| |
O |
| |
|
|
=====

+--+
| |
O |
/| |
|
|
=====

+--+
| |
O |
/|\ |
|
|
=====

+--+
| |
O |
/|\ |
/
|
|
=====

+--+
| |
O |
/|\ |
/ \ |
|
=====

Чтобы придать игре французский колорит, можете заменить строковые значения
в переменной HANGMAN_PICS на следующие рисунки гильотины:
|
|
|
|
|
|
|
|===

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|===|

|===|
|
|
|
|
|
|
|
|
|
|
|
|
|===|

|===|
|
|
|
|
|
|
|
|
|
|
|\ /|
|===|

|===|
|
|
|
|
|
|
|
|
|/-\|
|\ /|
|===|

|===|
|| /|
||/ |
|
|
|
|
|/-\|
|\ /|
|===|

|===|
|| /|
||/ |
|
|
|
|
|/-\|
|\O/|
|===|

180   Проект 34. «Виселица»» и «Гильотина»

Программа в действии
Результат выполнения hangman.py выглядит следующим образом:
Hangman, by Al Sweigart al@inventwithpython.com
+--+
| |
|
|
|
|
=====
The category is: Animals
Missed letters: No missed letters yet.
_ _ _ _ _
Guess a letter.
> e
--сокращено-+--+
| |
O |
/| |
|
|
=====
The category is: Animals
Missed letters: A I S
O T T E _
Guess a letter.
> r
Yes! The secret word is: OTTER
You have won!

Описание работы
Принцип игры у «Виселицы» и «Гильотины» один, различны только внешние
представления, что позволяет с легкостью заменить ASCII-графику виселицы на
ASCII-графику гильотины без изменения основной логики программы. Подобное
разделение представления и логики в программе упрощает добавление новых
возможностей и перепроектирование. В коммерческой разработке программного
обеспечения эта стратегия служит примером одного из архитектурных паттернов
проектирования, основная задача которого — структурирование программ в целях
большей понятности и упрощения модификации. В основном эти принципы используются в крупных программных продуктах, но применимы и к более мелким.

Описание работы  

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.

"""Виселица, (c) Эл Свейгарт al@inventwithpython.com
Угадайте буквы загаданного слова, пока не будет нарисована виселица.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, игра, слова, головоломка"""
# Одна из версий этой игры приведена в книге Invent Your Own
# Computer Games with Python на https://nostarch.com/inventwithpython
import random, sys
# Задаем константы:
# (!) Попробуйте добавить в HANGMAN_PICS новые строковые значения
# или изменить существующие, чтобы рисовать гильотину вместо виселицы.
HANGMAN_PICS = [r"""
+--+
| |
|
|
|
|
=====""",
r"""
+--+
| |
O |
|
|
|
=====""",
r"""
+--+
| |
O |
| |
|
|
=====""",
r"""
+--+
| |
O |
/| |
|
|
=====""",
r"""
+--+
| |
O |
/|\ |
|

181

182   Проект 34. «Виселица»» и «Гильотина»
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.

|
=====""",
r"""
+--+
| |
O |
/|\ |
/
|
|
=====""",
r"""
+--+
| |
O |
/|\ |
/ \ |
|
====="""]
# (!) Попробуйте заменить константы CATEGORY и WORDS на другие строковые
# значения.
CATEGORY = 'Animals'
WORDS = 'ANT BABOON BADGER BAT BEAR BEAVER CAMEL CAT CLAM COBRA COUGAR
➥ COYOTE CROW DEER DOG DONKEY DUCK EAGLE FERRET FOX FROG GOAT GOOSE HAWK
➥ LION LIZARD LLAMA MOLE MONKEY MOOSE MOUSE MULE NEWT OTTER OWL PANDA PARROT
➥ PIGEON PYTHON RABBIT RAM RAT RAVEN RHINO SALMON SEAL SHARK SHEEP SKUNK
➥ SLOTH SNAKE SPIDER STORK SWAN TIGER TOAD TROUT TURKEY TURTLE WEASEL WHALE
➥ WOLF WOMBAT ZEBRA'.split()

75.
76. def main():
77.
print('Hangman, by Al Sweigart al@inventwithpython.com')
78.
79.
# Переменные для новой игры:
80.
missedLetters = [] # Список неправильных попыток угадать буквы.
81.
correctLetters = [] # Список правильных попыток угадать буквы.
82.
secretWord = random.choice(WORDS) # Загаданное слово.
83.
84.
while True: # Основной цикл игры.
85.
drawHangman(missedLetters, correctLetters, secretWord)
86.
87.
# Пусть пользователь введет свою букву:
88.
guess = getPlayerGuess(missedLetters + correctLetters)
89.
90.
if guess in secretWord:
91.
# Добавляем правильную догадку в correctLetters:
92.
correctLetters.append(guess)
93.
# Проверяем, не выиграл ли игрок:
94.
foundAllLetters = True # Начинаем с предположения,
95.
# что он выиграл.
96.
for secretWordLetter in secretWord:
97.
if secretWordLetter not in correctLetters:

Описание работы  

183

98.
# В загаданном слове есть буква, пока еще отсутствующая
99.
# в correctLetters, так что игрок пока что не выиграл:
100.
foundAllLetters = False
101.
break
102.
if foundAllLetters:
103.
print('Yes! The secret word is:', secretWord)
104.
print('You have won!')
105.
break # Выходим из основного цикла игры.
106.
else:
107.
# Игрок не угадал:
108.
missedLetters.append(guess)
109.
110.
# Проверяем, не превысил ли игрок допустимое количество попыток
111.
# и проиграл. ("- 1", поскольку пустая виселица в
112.
# HANGMAN_PICS не считается)
113.
if len(missedLetters) == len(HANGMAN_PICS) - 1:
114.
drawHangman(missedLetters, correctLetters, secretWord)
115.
print('You have run out of guesses!')
116.
print('The word was "{}"'.format(secretWord))
117.
break
118.
119.
120. def drawHangman(missedLetters, correctLetters, secretWord):
121.
"""Рисуем текущее состояние виселицы вместе с неугаданными
122.
и правильно угаданными буквами загаданного слова."""
123.
print(HANGMAN_PICS[len(missedLetters)])
124.
print('The category is:', CATEGORY)
125.
print()
126.
127.
# Отображаем неправильные попытки угадать букву:
128.
print('Missed letters: ', end='')
129.
for letter in missedLetters:
130.
print(letter, end=' ')
131.
if len(missedLetters) == 0:
132.
print('No missed letters yet.')
133.
print()
134.
135.
# Отображаем пропуски вместо загаданного слова (по одному на букву):
136.
blanks = ['_'] * len(secretWord)
137.
138.
# Заменяем пропуски на угаданные буквы:
139.
for i in range(len(secretWord)):
140.
if secretWord[i] in correctLetters:
141.
blanks[i] = secretWord[i]
142.
143.
# Отображаем загаданное слово с пропусками между буквами:
144.
print(' '.join(blanks))
145.
146.
147. def getPlayerGuess(alreadyGuessed):
148.
"""Возвращает введенную пользователем букву. Убеждается,
149.
что пользователь ввел одну букву, которую не вводил ранее."""

184   Проект 34. «Виселица»» и «Гильотина»
150.
while True: # Запрашиваем, пока пользователь не введет допустимую букву.
151.
print('Guess a letter.')
152.
guess = input('> ').upper()
153.
if len(guess) != 1:
154.
print('Please enter a single letter.')
155.
elif guess in alreadyGuessed:
156.
print('You have already guessed that letter. Choose again.')
157.
elif not guess.isalpha():
158.
print('Please enter a LETTER.')
159.
else:
160.
return guess
161.
162.
163. # Если программа не импортируется, а запускается, производим запуск:
164. if __name__ =='__main__':
165.
try:
166.
main()
167.
except KeyboardInterrupt:
168.
sys.exit() # Если нажато Ctrl+C — завершаем программу.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Идеи относительно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
добавить возможность выбора категории и позволить пользователю выбирать, слова из какой категории он хочет угадывать;
добавить пользователю возможность выбора версии программы с виселицей
или гильотиной.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если удалить или закомментировать missedLetters.append(guess)
в строке 108?
2. Что будет, если drawHangman(missedLetters, correctLetters, secretWord)
в строке 85 заменить на drawHangman(correctLetters, missedLetters,
secretWord)?
3. Что будет, если ['_'] в строке 136 заменить на ['*']?
4. Что будет, если выражение print(' '.join(blanks)) в строке 144 заменить
на print(secretWord)?

35
Гексагональная сетка

Эта короткая программа генерирует мозаичное изображение гексагональной сетки, напоминающей мелкоячеистую
проволочную сетку, и демонстрирует, что для создания
интересных изображений не нужно много кода. Несколько более сложный вариант данной программы приведен
в проекте 65.
Обратите внимание: в этой программе используются неформатированные строки, в которых открывающая кавычка предваряется буквой r в нижнем
регистре, так что обратные косые черты не интерпретируются как символы экранирования.

186   Проект 35. Гексагональная сетка

Программа в действии
На рис. 35.1 показано, как выглядит результат выполнения shiningcarpet.py.

Рис. 35.1. Выведенное мозаичное изображение гексагональной сетки

Описание работы
Эффективность программирования заключается в возможности быстро и безошибочно выполнять повторяющиеся команды. Благодаря этому с помощью
десятка строк кода можно создавать на экране сотни, тысячи или даже миллионы
шестиугольников.
В командной строке или в окне терминала можно перенаправить выводимые программой данные с экрана в текстовый файл. В Windows для создания текстового
файла с шестиугольниками выполните команду py hexgrid.py > hextiles.txt.
В Linux и macOS выполните команду python3 hexgrid.py > hextiles.txt. А поскольку накладываемое размером экрана ограничение снято, можете увеличить
значения констант X_REPEAT и Y_REPEAT и сохранить полученное в файл, который
затем можно будет легко распечатать на бумаге, отправить по электронной почте
или разместить в соцсети. То же самое относится к любому сгенерированному с помощью компьютера графическому изображению.
1.
2.
3.
4.

"""Гексагональная сетка, (c) Эл Свейгарт al@inventwithpython.com
Выводит на экран мозаику в виде гексагональной сетки
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, графика"""

Исследование программы  

5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.

187

# Задаем константы:
# (!) Попробуйте изменить эти значения:
X_REPEAT = 19 # Количество ячеек по горизонтали.
Y_REPEAT = 12 # Количество ячеек по вертикали.
for y in range(Y_REPEAT):
# Выводим верхнюю половину шестиугольника:
for x in range(X_REPEAT):
print(r'/ \_', end='')
print()
# Выводим нижнюю половину шестиугольника:
for x in range(X_REPEAT):
print(r'\_/ ', end='')
print()

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
увеличить размер шестиугольников;
выполнять замощение прямоугольниками вместо шестиугольников.
Попробуйте поэкспериментировать и переделать эту программу так, чтобы создавалась более крупная шестиугольная сетка наподобие следующего узора:
/

\

/
\

\___/
/
\

/

/

\___/
/
\

\

/

\___/
/
\

\___/
/
\

\

/

\___/
/
\

\___/
/
\

\

/

\___/
/
\

\___/
/
\

\

/

\___/
/
\

\___/
/
\

\

/

\___/
/
\

\___/
/
\

\
/

\___/
/
\

\___/
\___/
\___/
\___/
\___/
\___/
\
/
\
/
\
/
\
/
\
/
\
/
\
/
\
/
\_____/
\_____/
\_____/
\_____
\
/
\
/
\
/
\
/
\
/
\
/
\
/
\
/
\_____/
\_____/
\_____/
\_____/
/
\
/
\
/
\
/
\
/
\
/
\
/
\
/
\
/
\_____/
\_____/
\_____/
\_____
/

\

\

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

36
Песочные часы

Эта программа для визуализации включает приближенный
физический движок для моделирования падения песка
через небольшое отверстие песочных часов. Песок скапливается горкой в нижней части песочных часов; а в конце
песочные часы переворачиваются и процесс повторяется.

Программа в действии
На рис. 36.1 показано, как выглядит результат выполнения hourglass.py.

Рис. 36.1. Результаты работы программы «Песочные часы»

Описание работы  

189

Описание работы
Программа «Песочные часы» реализует примитивный физический движок (physics
engine). Это программное обеспечение для имитационного моделирования падения
физических объектов под влиянием силы тяжести, их столкновения друг с другом и вообще движения в соответствии с законами физики. Физические движки
применяются в компьютерных играх, при создании динамических изображений
и имитационном моделировании, имеющем научные цели. В строках с 91-й по 102-ю
каждая «песчинка» проверяет, есть ли внизу незанятое место, и движется вниз, если
есть. В противном случае она проверяет, есть ли пространство для движения вниз
и влево (строки с 104-й по 112-ю) или вниз и вправо (строки с 114-й по 122-ю).
Конечно, кинематика — раздел классической физики, занимающийся движением
макроскопических объектов, — намного обширнее. Однако простое моделирование
движения песка в песочных часах, на которое было бы приятно смотреть, не требует
научной степени по физике.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.

"""Песочные часы, (c) Эл Свейгарт al@inventwithpython.com
Динамическое изображение песочных часов. Нажмите Ctrl+C для останова.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, графика, bext, имитационное моделирование"""
import random, sys, time
try:

import bext
except ImportError:
print('This program requires the bext module, which you')
print('can install by following the instructions at')
print('https://pypi.org/project/Bext/')
sys.exit()
# Задаем константы:
PAUSE_LENGTH = 0.2 # (!) Попробуйте заменить это значение на 0.0 или 1.0.
# (!) Попробуйте заменить это значение на любое число от 0 до 100:
WIDE_FALL_CHANCE = 50
SCREEN_WIDTH = 79
SCREEN_HEIGHT = 25
X = 0 # Индекс значений X в кортеже (x, y) – 0.
Y = 1 # Индекс значений Y в кортеже (x, y) – 1.
SAND = chr(9617)
WALL = chr(9608)
# Описываем стенки песочных часов:
HOURGLASS = set() # Кортежи (x, y) для стенок песочных часов.
# (!) Попробуйте закомментировать часть строк HOURGLASS.add(), чтобы стереть
# стенки песочных часов:
for i in range(18, 37):
HOURGLASS.add((i, 1))
# Верхняя крышка песочных часов.

190   Проект 36. Песочные часы
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.

HOURGLASS.add((i, 23))
for i in range(1, 5):
HOURGLASS.add((18, i))
HOURGLASS.add((36, i))
HOURGLASS.add((18, i +
HOURGLASS.add((36, i +
for i in range(8):
HOURGLASS.add((19 + i,
HOURGLASS.add((35 - i,
HOURGLASS.add((25 - i,
HOURGLASS.add((29 + i,

# Нижняя крышка.
# Верхняя левая прямая стенка.
# Верхняя правая прямая стенка.
19)) # Стенка внизу слева.
19)) # Стенка внизу справа.
5 + i))
5 + i))
13 + i))
13 + i))

#
#
#
#

Верхняя левая наклонная стенка.
Верхняя правая наклонная стенка.
Нижняя левая наклонная стенка.
Нижняя правая наклонная стенка.

# Начальная горка песка в верхней половине песочных часов:
INITIAL_SAND = set()
for y in range(8):
for x in range(19 + y, 36 - y):
INITIAL_SAND.add((x, y + 4))
def main():
bext.fg('yellow')
bext.clear()
# Выводим сообщение о возможности выхода:
bext.goto(0, 0)
print('Ctrl-C to quit.', end='')
# Отображаем стенки песочных часов:
for wall in HOURGLASS:
bext.goto(wall[X], wall[Y])
print(WALL, end='')
while True: # Основной цикл программы.
allSand = list(INITIAL_SAND)
# Рисуем начальную горку песка:
for sand in allSand:
bext.goto(sand[X], sand[Y])
print(SAND, end='')
runHourglassSimulation(allSand)
def runHourglassSimulation(allSand):
"""Моделируем падение песка, пока он не прекратит двигаться.
"""
while True: # Проходим в цикле, пока песок не закончится.
random.shuffle(allSand) # Случайный порядок песчинок.
sandMovedOnThisStep = False
for i, sand in enumerate(allSand):

Описание работы  

84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.

if sand[Y] == SCREEN_HEIGHT - 1:
# Песок на дне, а значит, больше не будет двигаться:
continue
# Если под песчинкой пусто, перемещаем ее вниз:
noSandBelow = (sand[X], sand[Y] + 1) not in allSand
noWallBelow = (sand[X], sand[Y] + 1) not in HOURGLASS
canFallDown = noSandBelow and noWallBelow
if canFallDown:
# Отрисовываем песчинку ниже на одну строку:
bext.goto(sand[X], sand[Y])
print(' ', end='') # Очищаем старое место ее расположения.
bext.goto(sand[X], sand[Y] + 1)
print(SAND, end='')
# Песчинка ниже на одну строку:
allSand[i] = (sand[X], sand[Y] + 1)
sandMovedOnThisStep = True
else:
# Проверяем, может ли песчинка упасть влево:
belowLeft = (sand[X] - 1, sand[Y] + 1)
noSandBelowLeft = belowLeft not in allSand
noWallBelowLeft = belowLeft not in HOURGLASS
left = (sand[X] - 1, sand[Y])
noWallLeft = left not in HOURGLASS
notOnLeftEdge = sand[X] > 0
canFallLeft = (noSandBelowLeft and noWallBelowLeft
and noWallLeft and notOnLeftEdge)
# Проверяем, может ли песчинка упасть вправо:
belowRight = (sand[X] + 1, sand[Y] + 1)
noSandBelowRight = belowRight not in allSand
noWallBelowRight = belowRight not in HOURGLASS
right = (sand[X] + 1, sand[Y])
noWallRight = right not in HOURGLASS
notOnRightEdge = sand[X] < SCREEN_WIDTH - 1
canFallRight = (noSandBelowRight and noWallBelowRight
and noWallRight and notOnRightEdge)
# Задаем направление падения:
fallingDirection = None
if canFallLeft and not canFallRight:
fallingDirection = -1 # Задаем падение песка влево.
elif not canFallLeft and canFallRight:
fallingDirection = 1 # Задаем падение песка вправо.
elif canFallLeft and canFallRight:
# Возможны оба направления, так что выбираем случайным
# образом:
fallingDirection = random.choice((-1, 1))
# Проверяем, может ли песчинка упасть "далеко",

191

192   Проект 36. Песочные часы
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.

# на две позиции влево или вправо, вместо одной:
if random.random() * 100 1
canFallTwoLeft = (canFallLeft and noSandBelowTwoLeft
and noWallBelowTwoLeft and notOnSecondToLeftEdge)
belowTwoRight = (sand[X] + 2, sand[Y] + 1)
noSandBelowTwoRight = belowTwoRight not in allSand
noWallBelowTwoRight = belowTwoRight not in HOURGLASS
notOnSecondToRightEdge = sand[X] < SCREEN_WIDTH - 2
canFallTwoRight = (canFallRight
and noSandBelowTwoRight and noWallBelowTwoRight
and notOnSecondToRightEdge)
if canFallTwoLeft and not canFallTwoRight:
fallingDirection = -2
elif not canFallTwoLeft and canFallTwoRight:
fallingDirection = 2
elif canFallTwoLeft and canFallTwoRight:
fallingDirection = random.choice((-2, 2))
if fallingDirection == None:
# Эта песчинка никуда упасть не может,
# переходим к следующей.
continue
# Отрисовываем песчинку на новом месте:
bext.goto(sand[X], sand[Y])
print(' ', end='') # Затираем старую песчинку.
bext.goto(sand[X] + fallingDirection, sand[Y] + 1)
print(SAND, end='') # Отрисовываем новую песчинку.
# Перемещаем песчинку на новое место:
allSand[i] = (sand[X] + fallingDirection, sand[Y] + 1)
sandMovedOnThisStep = True
sys.stdout.flush() # (Необходимо для использующих bext программ.)
time.sleep(PAUSE_LENGTH) # Пауза
# Если на этом шаге песок вообще не двигался, обнуляем песочные часы:
if not sandMovedOnThisStep:
time.sleep(2)
# Вытираем весь песок:
for sand in allSand:
bext.goto(sand[X], sand[Y])
print(' ', end='')
break # Выходим из основного цикла моделирования.

Исследование программы  

193

186. # Если программа не импортируется, а запускается, производим запуск:
187. if __name__ == '__main__':
188.
try:
189.
main()
190.
except KeyboardInterrupt:
191.
sys.exit() # Если нажато Ctrl+C — завершаем программу.

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
реализовать другие формы стенок, помимо песочных часов;
создать места на экране, откуда непрерывно высыпаются новые песчинки.

Исследование программы
Попробуйте ответить на следующие вопросы. Поэкспериментируйте с изменениями
кода и запустите программу снова, чтобы увидеть, как они повлияют на ее работу.
1. Что будет, если range(18, 37) в строке 31 заменить на range(18, 30)?
2. Что будет, если range(8) в строке 39 заменить на range(0)?
3. Что будет, если sandMovedOnThisStep = False в строке 82 заменить на
sandMovedOnThisStep = True?
4. Что будет, если выражение fallingDirection = None в строке 125 заменить
на fallingDirection = 1?
5. Что будет, если random.random() * 100 0:
168.
# Телепортируем игрока на случайную новую позицию:
169.
board['teleports'] -= 1
170.
return getRandomEmptySpace(board, robots)
171.
elif move != '' and move in allMoves:
172.
# Возвращаем новую позицию игрока в соответствии со сделанным ходом:
173.
return {'Q': (playerX - 1, playerY - 1),
174.
'W': (playerX + 0, playerY - 1),
175.
'E': (playerX + 1, playerY - 1),
176.
'D': (playerX + 1, playerY + 0),
177.
'C': (playerX + 1, playerY + 1),
178.
'X': (playerX + 0, playerY + 1),
179.
'Z': (playerX - 1, playerY + 1),
180.
'A': (playerX - 1, playerY + 0),
181.
'S': (playerX, playerY)}[move]
182.
183.
184. def moveRobots(board, robotPositions, playerPosition):
185.
"""Возвращаем список кортежей (x, y) новых позиций роботов,
186.
после их попыток переместиться в направлении игрока."""
187.
playerx, playery = playerPosition
188.
nextRobotPositions = []
189.
190.
while len(robotPositions) > 0:
191.
robotx, roboty = robotPositions[0]
192.
193.
# Определяем направление движения робота.
194.
if robotx < playerx:
195.
movex = 1 # Перемещаем вправо.
196.
elif robotx > playerx:
197.
movex = -1 # Перемещаем влево.
198.
elif robotx == playerx:
199.
movex = 0 # Не перемещаем по горизонтали.
200.
201.
if roboty < playery:
202.
movey = 1 # Перемещаем вверх.
203.
elif roboty > playery:
204.
movey = -1 # Перемещаем вниз.
205.
elif roboty == playery:
206.
movey = 0 # Не перемещаем по вертикали.
207.
# Проверяем, не натыкается ли робот на стену, и корректируем

200   Проект 37. Голодные роботы
208.
# направление его движения:
209.
if board[(robotx + movex, roboty + movey)] == WALL:
210.
# Робот натолкнется на стену, так что выбираем другой ход:
211.
if board[(robotx + movex, roboty)] == EMPTY_SPACE:
212.
movey = 0 # Робот не может переместиться горизонтально.
213.
elif board[(robotx, roboty + movey)] == EMPTY_SPACE:
214.
movex = 0 # Робот не может переместиться вертикально.
215.
else:
216.
# Робот не может переместиться.
217.
movex = 0
218.
movey = 0
219.
newRobotx = robotx + movex
220.
newRoboty = roboty + movey
221.
222.
if (board[(robotx, roboty)] == DEAD_ROBOT
223.
or board[(newRobotx, newRoboty)] == DEAD_ROBOT):
224.
# Робот попал на место столкновения, удаляем его.
225.
del robotPositions[0]
226.
continue
227.
# Проверяем, не наткнулся ли он на другого робота, и уничтожаем обоих
228.
# в этом случае:
229.
if (newRobotx, newRoboty) in nextRobotPositions:
230.
board[(newRobotx, newRoboty)] = DEAD_ROBOT
231.
nextRobotPositions.remove((newRobotx, newRoboty))
232.
else:
233.
nextRobotPositions.append((newRobotx, newRoboty))
234.
235.
# Удаляем роботов из robotPositions по мере их перемещения.
236.
del robotPositions[0]
237.
return nextRobotPositions
238.
239.
240. # Если программа не импортируется, а запускается, производим запуск:
241. if __name__ == '__main__':
242.
main()

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
создайте роботов двух различных видов: способных перемещаться только по
диагонали и только в основных направлениях;
предоставьте игроку возможность оставлять после себя ограниченное количество ловушек, останавливающих любого ступившего на них робота;
предоставьте игроку возможность создать для своей защиты ограниченное
количество «мгновенных стен».

Исследование программы  

201

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если WALL = chr(9617) в строке 22 заменить на WALL = 'R'?
2. Что будет, если return nextRobotPositions в строке 237 заменить на return
robotPositions?
3. Что будет, если удалить или закомментировать displayBoard(board, robots,
playerPosition) в строке 44?
4. Что будет, если удалить или закомментировать robots = moveRobots(board,
robots, playerPosition) в строке 53?

38

«Я обвиняю!»

Вы — известный детектив Матильда Камю. Пропала
кошка Зофи, и вам нужно изучить все зацепки. Подозреваемые всегда врут или всегда говорят правду. Найдете ли
вы Зофи вовремя и обвините ли того, кого нужно?
В данной игре вы едете на такси в различные места города.
В каждом из этих мест есть подозреваемый и предмет. Вы можете спрашивать подозреваемых о других подозреваемых и предметах,
сравнивать их ответы со своими записями и определять, говорят они правду или
лгут. Кто-нибудь из них может знать, кто похитил Зофи (или где она находится,
или какой предмет можно найти в месте пребывания похитителя), но вы должны
решить, верите ли вы им. На поиск злоумышленника у вас пять минут, но если вы
три раза обвините кого-то ошибочно, то проиграете. Эта игра создана под впечатлением от игры Where’s an Egg? («Найди яйцо») с сайта Homestar Runner.

Программа в действии
Результат выполнения jaccuse.py выглядит следующим образом:
J'ACCUSE! (a mystery game)
--сокращено-Time left: 5 min, 0 sec
You are in your TAXI. Where do you want to go?
(A)LBINO ALLIGATOR PIT

Описание работы  

203

(B)OWLING ALLEY
(C)ITY HALL
(D)UCK POND
(H)IPSTER CAFE
(O)LD BARN
(U)NIVERSITY LIBRARY
(V)IDEO GAME MUSEUM
(Z)OO
> a
Time left: 4 min, 48 sec
You are at the ALBINO ALLIGATOR PIT.
ESPRESSA TOFFEEPOT with the ONE COWBOY BOOT is here.
(J) "J'ACCUSE!" (3 accusations left)
(Z) Ask if they know where ZOPHIE THE CAT is.
(T) Go back to the TAXI.
(1) Ask about ESPRESSA TOFFEEPOT
(2) Ask about ONE COWBOY BOOT
> z
They give you this clue: "DUKE HAUTDOG"
Press Enter to continue...
--сокращено--

Описание работы
Чтобы разобраться с данной игрой, необходимо внимательно изучить ассоциативный массив clues, описанный в строках с 51-й по 109-ю. Можете раскомментировать строки с 151-й по 154-ю, чтобы вывести его на экран. Роль ключей в этом
ассоциативном массиве играют строковые значения из списка SUSPECTS, а роль
значений — «ассоциативные массивы зацепок». Каждый из этих ассоциативных
массивов зацепок содержит строковые значения из списков SUSPECTS и ITEMS ,
служащие ответами одних подозреваемых относительно других подозреваемых
и предметов. Например, если clues['DUKE HAUTDOG']['CANDLESTICK'] равно 'DUCK
POND', то, когда мы спросим Дюка Отдога о подсвечнике (Candlestick), Дюк ответит,
что тот находится в Утином пруду (Duck pond). Подозреваемые, предметы, места
и преступник перетасовываются при каждой новой игре.
Эта структура данных — центральная для нашей программы, следовательно, чтобы
разобраться в программе в целом, необходимо разобраться с этой структурой.
1.
2.
3.
4.
5.
6.
7.

"""Я обвиняю!, (c) Эл Свейгарт al@inventwithpython.com
Детективная игра с обманом и пропавшей кошкой
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: очень большая, игра, юмор, головоломка"""
# Сыграть в исходную Flash-игру вы можете по адресу:
# https://homestarrunner.com/videlectrix/wheresanegg.html

204   Проект 38. «Я обвиняю!»
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.

# Больше информации — в статье http://www.hrwiki.org/wiki/Where's_an_Egg%3F
import time, random, sys
# Задаем константы:
SUSPECTS = ['DUKE HAUTDOG', 'MAXIMUM POWERS', 'BILL MONOPOLIS', 'SENATOR
SCHMEAR', 'MRS. FEATHERTOSS', 'DR. JEAN SPLICER', 'RAFFLES THE CLOWN',
'ESPRESSA TOFFEEPOT', 'CECIL EDGAR VANDERTON']
ITEMS = ['FLASHLIGHT', 'CANDLESTICK', 'RAINBOW FLAG', 'HAMSTER WHEEL', 'ANIME
VHS TAPE', 'JAR OF PICKLES', 'ONE COWBOY BOOT', 'CLEAN UNDERPANTS', '5 DOLLAR
GIFT CARD']
PLACES = ['ZOO', 'OLD BARN', 'DUCK POND', 'CITY HALL', 'HIPSTER CAFE',
'BOWLING ALLEY', 'VIDEO GAME MUSEUM', 'UNIVERSITY LIBRARY', 'ALBINO
ALLIGATOR PIT']
TIME_TO_SOLVE = 300 # Длительность игры — 300 секунд (5 минут).
# Первые буквы и максимальная длина мест действия необходимы для отображения
# меню:
PLACE_FIRST_LETTERS = {}
LONGEST_PLACE_NAME_LENGTH = 0
for place in PLACES:
PLACE_FIRST_LETTERS[place[0]] = place
if len(place) > LONGEST_PLACE_NAME_LENGTH:
LONGEST_PLACE_NAME_LENGTH = len(place)
# Основные проверки корректности констант:
assert len(SUSPECTS) == 9
assert len(ITEMS) == 9
assert len(PLACES) == 9
# Первые буквы не должны повторяться:
assert len(PLACE_FIRST_LETTERS.keys()) == len(PLACES)
knownSuspectsAndItems = []
# visitedPlaces: ключи — места действия, значения —
➥ строковые значения для находящихся там подозреваемых и предметов.
visitedPlaces = {}
currentLocation = 'TAXI' # Начинаем игру в такси.
accusedSuspects = [] # Обвиненные подозреваемые никаких зацепок не дают.
liars = random.sample(SUSPECTS, random.randint(3, 4))
accusationsLeft = 3 # Вы можете обвинить не более трех человек.
culprit = random.choice(SUSPECTS)
# Ссылки на общие индексы; например, SUSPECTS[0] и ITEMS[0] находятся
# в PLACES[0].
random.shuffle(SUSPECTS)
random.shuffle(ITEMS)
random.shuffle(PLACES)

36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48. # Создаем структуры данных для зацепок, полученных от говорящих правду
49. # о каждом из предметов и подозреваемых.
50. # clues: ключи — подозреваемые, у которых попросили зацепку,
➥ значение — "ассоциативный массив зацепок".

Описание работы  

51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.

205

clues = {}
for i, interviewee in enumerate(SUSPECTS):
if interviewee in liars:
continue # Пока пропускаем лжецов.
# Ключи в этом "ассоциативном массиве зацепок" — предметы
# и подозреваемые, значения — полученные зацепки.
clues[interviewee] = {}
clues[interviewee]['debug_liar'] = False # Удобно для отладки.
for item in ITEMS: # Выбираем зацепки относительно всех предметов.
if random.randint(0, 1) == 0: # Говорит, где находится предмет:
clues[interviewee][item] = PLACES[ITEMS.index(item)]
else: # Говорит, у кого предмет:
clues[interviewee][item] = SUSPECTS[ITEMS.index(item)]
for suspect in SUSPECTS: # Выбираем зацепки относительно всех
# подозреваемых.
if random.randint(0, 1) == 0: # Говорит, где находится подозреваемый:
clues[interviewee][suspect] = PLACES[SUSPECTS.index(suspect)]
else: # Говорит, какой предмет есть у подозреваемого:
clues[interviewee][suspect] = ITEMS[SUSPECTS.index(suspect)]
# Создаем структуры данных для получаемых от лжецов зацепок
# относительно всех предметов и подозреваемых:
for i, interviewee in enumerate(SUSPECTS):
if interviewee not in liars:
continue # Мы уже обработали тех, кто говорит правду.
# Ключи в этом "ассоциативном массиве зацепок" — предметы
# и подозреваемые, значения — полученные зацепки.
clues[interviewee] = {}
clues[interviewee]['debug_liar'] = True # Удобно для отладки.
# Этот опрашиваемый подозреваемый – лжец, и его зацепки ложны:
for item in ITEMS:
if random.randint(0, 1) == 0:
while True: # Выбираем случайное (неправильное) место.
# Лжет относительно местонахождения предмета.
clues[interviewee][item] = random.choice(PLACES)
if clues[interviewee][item] != PLACES[ITEMS.index(item)]:
# Выходим из цикла после выбора ложной зацепки.
break
else:
while True: # Выбираем случайного (неправильного) подозреваемого.
clues[interviewee][item] = random.choice(SUSPECTS)
if clues[interviewee][item] != SUSPECTS[ITEMS.index(item)]:
# Выходим из цикла после выбора ложной зацепки.
break
for suspect in SUSPECTS:
if random.randint(0, 1) == 0:
while True: # Выбираем случайное (неправильное) место.
clues[interviewee][suspect] = random.choice(PLACES)
if clues[interviewee][suspect] != PLACES[ITEMS.index(item)]:

206   Проект 38. «Я обвиняю!»
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.

# Выходим из цикла после выбора ложной зацепки.
break

else:
while True: # Выбираем случайный (неправильный) предмет.
clues[interviewee][suspect] = random.choice(ITEMS)
if clues[interviewee][suspect] !=
➥ ITEMS[SUSPECTS.index(suspect)]:
# Выходим из цикла после выбора ложной зацепки.
break
# Создаем структуры данных для ответов на вопросы о Зофи:
zophieClues = {}
for interviewee in random.sample(SUSPECTS, random.randint(3, 4)):
kindOfClue = random.randint(1, 3)
if kindOfClue == 1:
if interviewee not in liars:
# (Правдиво) отвечают, у кого Зофи.
zophieClues[interviewee] = culprit
elif interviewee in liars:
while True:
# Выбираем (неправильного) подозреваемого.
zophieClues[interviewee] = random.choice(SUSPECTS)
if zophieClues[interviewee] != culprit:
# Выходим из цикла после выбора ложной зацепки.
break
elif kindOfClue == 2:
if interviewee not in liars:
# (Правдиво) отвечают, где Зофи.
zophieClues[interviewee] = PLACES[SUSPECTS.index(culprit)]
elif interviewee in liars:
while True:
# Выбираем случайное (неправильное) место.
zophieClues[interviewee] = random.choice(PLACES)
if zophieClues[interviewee] != PLACES[SUSPECTS.index(culprit)]:
# Выходим из цикла после выбора ложной зацепки.
break
elif kindOfClue == 3:
if interviewee not in liars:
# (Правдиво) отвечают, близ какого предмета находится Зофи.
zophieClues[interviewee] = ITEMS[SUSPECTS.index(culprit)]
elif interviewee in liars:
while True:
# Выбираем случайный (неправильный) предмет.
zophieClues[interviewee] = random.choice(ITEMS)
if zophieClues[interviewee] != ITEMS[SUSPECTS.index(culprit)]:
# Выходим из цикла после выбора ложной зацепки.
break
# Эксперимент: раскомментируйте этот код, чтобы посмотреть на содержимое
# структур данных с зацепками:

Описание работы  

152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.

#import pprint
#pprint.pprint(clues)
#pprint.pprint(zophieClues)
#print('culprit =', culprit)
# НАЧАЛО ИГРЫ
print("""J'ACCUSE! (a mystery game)")
By Al Sweigart al@inventwithpython.com
Inspired by Homestar Runner\'s "Where\'s an Egg?" game
You are the world-famous detective Mathilde Camus.
ZOPHIE THE CAT has gone missing, and you must sift through the clues.
Suspects either always tell lies, or always tell the truth. Ask them
about other people, places, and items to see if the details they give are
truthful and consistent with your observations. Then you will know if
their clue about ZOPHIE THE CAT is true or not. Will you find ZOPHIE THE
CAT in time and accuse the guilty party?
""")
input('Press Enter to begin...')
startTime = time.time()
endTime = startTime + TIME_TO_SOLVE

while True: # Основной цикл игры.
if time.time() > endTime or accusationsLeft == 0:
# Обрабатываем условие "игра окончена":
if time.time() > endTime:
print('You have run out of time!')
elif accusationsLeft == 0:
print('You have accused too many innocent people!')
culpritIndex = SUSPECTS.index(culprit)
print('It was {} at the {} with the {} who catnapped her!'.
format(culprit, PLACES[culpritIndex], ITEMS[culpritIndex]))
184.
print('Better luck next time, Detective.')
185.
sys.exit()
186.
187.
print()
188.
minutesLeft = int(endTime - time.time()) // 60
189.
secondsLeft = int(endTime - time.time()) % 60
190.
print('Time left: {} min, {} sec'.format(minutesLeft, secondsLeft))
191.
192.
if currentLocation == 'TAXI':
193.
print(' You are in your TAXI. Where do you want to go?')
194.
for place in sorted(PLACES):
195.
placeInfo = ''
196.
if place in visitedPlaces:
197.
placeInfo = visitedPlaces[place]
198.
nameLabel = '(' + place[0] + ')' + place[1:]
199.
spacing = " " * (LONGEST_PLACE_NAME_LENGTH - len(place))
200.
print('{} {}{}'.format(nameLabel, spacing, placeInfo))
201.
print('(Q)UIT GAME')

207

208   Проект 38. «Я обвиняю!»
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.

while True: # Продолжаем спрашивать, пока не получим допустимый ответ.
response = input('> ').upper()
if response == '':
continue # Спрашиваем снова.
if response == 'Q':
print('Thanks for playing!')
sys.exit()
if response in PLACE_FIRST_LETTERS.keys():
break
currentLocation = PLACE_FIRST_LETTERS[response]
continue # Возвращаемся к началу основного цикла игры.
# Находимся в одном из мест; игрок может запрашивать зацепки.
print(' You are at the {}.'.format(currentLocation))
currentLocationIndex = PLACES.index(currentLocation)
thePersonHere = SUSPECTS[currentLocationIndex]
theItemHere = ITEMS[currentLocationIndex]
print(' {} with the {} is here.'.format(thePersonHere, theItemHere))
# Добавляем находящихся в этом месте подозреваемого и предмет
# в наш список известных подозреваемых и предметов:
if thePersonHere not in knownSuspectsAndItems:
knownSuspectsAndItems.append(thePersonHere)
if ITEMS[currentLocationIndex] not in knownSuspectsAndItems:
knownSuspectsAndItems.append(ITEMS[currentLocationIndex])
if currentLocation not in visitedPlaces.keys():
visitedPlaces[currentLocation] = '({}, {})'.format(
➥ thePersonHere.lower(), theItemHere.lower())
# Обвиненные ранее ошибочно подозреваемые не дают
# игроку зацепок:
if thePersonHere in accusedSuspects:
print('They are offended that you accused them,')
print('and will not help with your investigation.')
print('You go back to your TAXI.')
print()
input('Press Enter to continue...')
currentLocation = 'TAXI'
continue # Возвращаемся к началу основного цикла игры.
# Отображаем меню с известными подозреваемыми и предметами, о которых
# можно спросить:
print()
print('(J) "J\'ACCUSE!" ({} accusations left)'.format(accusationsLeft))
print('(Z) Ask if they know where ZOPHIE THE CAT is.')
print('(T) Go back to the TAXI.')
for i, suspectOrItem in enumerate(knownSuspectsAndItems):
print('({}) Ask about {}'.format(i + 1, suspectOrItem))
while True: # Продолжаем спрашивать, пока не получим допустимый ответ.
response = input('> ').upper()
if response in 'JZT' or (response.isdecimal() and 0 < int(response) LONGEST_COLUMN:
LONGEST_COLUMN = len(key)
# Помещаем все данные об элементах в структуру данных:
ELEMENTS = {} # Структура данных со всеми данными об элементах.
for line in elements:
element = {'Atomic Number': line[0],
'Symbol':
line[1],
'Element':
line[2],
'Origin of name': line[3],
'Group':
line[4],
'Period':
line[5],
'Atomic weight': line[6] + ' u', # атомная единица массы
'Density':
line[7] + ' g/cm^3', # граммов/куб. см
'Melting point': line[8] + ' K', # градусов по Кельвину
'Boiling point': line[9] + ' K', # градусов по Кельвину
'Specific heat capacity':
line[10] + ' J/(g*K)',
'Electronegativity':
line[11],
'Abundance in earth\'s crust': line[12] + ' mg/kg'}
# Часть данных включает текст в квадратных скобках из "Википедии",
# который необходимо удалить, например атомный вес бора:
# вместо "10.81[III][IV][V][VI]" должно быть "10.81"
for key, value in element.items():
# Удаляем римские цифры в квадратных скобках:
element[key] = re.sub(r'\[(I|V|X)+\]', '', value)
ELEMENTS[line[0]] = element
ELEMENTS[line[1]] = element

# Сопоставляем атомный номер и элемент.
# Сопоставляем символ и элемент.

285

286   Проект 53. Периодическая таблица элементов
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.

print('Periodic Table of Elements')
print('By Al Sweigart al@inventwithpython.com')
print()
while True: # Основной
# Выводим таблицу и
print('''
1 2 3 4 5 6
1 H
2 Li Be
3 Na Mg
4 K Ca Sc Ti V Cr
5 Rb Sr Y Zr Nb Mo
6 Cs Ba La Hf Ta W
7 Fr Ra Ac Rf Db Sg

цикл программы.
позволяем пользователю выбрать элемент:
Periodic Table of Elements
7 8 9 10 11 12 13 14 15 16 17 18
He
B C N O F Ne
Al Si P S Cl Ar
Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr
Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe
Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn
Bh Hs Mt Ds Rg Cn Nh Fl Mc Lv Ts Og

Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu
Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr''')
print('Enter a symbol or atomic number to examine, or QUIT to quit.')
response = input('> ').title()
if response == 'Quit':
sys.exit()
# Отображаем информацию о выбранном элементе:
if response in ELEMENTS:
for key in ALL_COLUMNS:
keyJustified = key.rjust(LONGEST_COLUMN)
print(keyJustified + ': ' + ELEMENTS[response][key])
input('Press Enter to continue...')

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. К какой программной ошибке приведет замена response == 'Quit' в строке 81
на response == 'quit'?
2. Что будет, если удалить или закомментировать строки 53 и 55?

54
Поросячья латынь

«Поросячья латынь» — игра, в которой слова на английском языке преобразуются в пародию на латынь. Если
слово начинается на согласную, то говорящий переставляет ее в конец слова, добавляя после нее ay. Например,
pig превращается в igpay, а latin — в atinlay. Если же слово
начинается на гласную, то говорящий просто добавляет в его
конец yay. Например, elephant превращается в elephantyay,
а umbrella — в umbrellayay.

Программа в действии
Результат выполнения piglatin.py выглядит следующим образом:
Igpay Atinlay (Pig Latin)
By Al Sweigart al@inventwithpython.com
Enter your message:
> This is a very serious message.
Isthay isyay ayay eryvay erioussay essagemay.
(Copied pig latin to clipboard.)

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

288   Проект 54. Поросячья латынь
поросячьей латыни. Функция main() вызывается только в случае, если пользователь
напрямую запускает программу. Можете также писать собственные программы на
Python, импортировать piglatin.py с помощью оператора import piglatin и вызывать функцию englishToPigLatin() с помощью piglatin.englishToPigLatin().
Такая методика повторного использования экономит время и усилия, которые
понадобились бы для воссоздания этого кода своими руками.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.

"""Поросячья латынь, (c) Эл Свейгарт al@inventwithpython.com
Переводит сообщения на английском на поросячью латынь.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, слова"""
try:

import pyperclip # pyperclip копирует текст в буфер обмена.
except ImportError:
pass # Если pyperclip не установлен, ничего не делаем. Не проблема.
VOWELS = ('a', 'e', 'i', 'o', 'u', 'y')
def main():
print('''Igpay Atinlay (Pig Latin)
By Al Sweigart al@inventwithpython.com
Enter your message:''')
pigLatin = englishToPigLatin(input('> '))
# Объединяем все слова обратно в одно строковое значение:
print(pigLatin)
try:

pyperclip.copy(pigLatin)
print('(Copied pig latin to clipboard.)')
except NameError:
pass # Если pyperclip не установлена, ничего не делаем.
def englishToPigLatin(message):
pigLatin = '' # Строковое значение с переводом на поросячью латынь.
for word in message.split():
# Отделяем небуквенные символы в начале слова:
prefixNonLetters = ''
while len(word) > 0 and not word[0].isalpha():
prefixNonLetters += word[0]
word = word[1:]
if len(word) == 0:
pigLatin = pigLatin + prefixNonLetters + ' '
continue
# Отделяем небуквенные символы в конце слова:

Исследование программы  

289

44.
suffixNonLetters = ''
45.
while not word[-1].isalpha():
46.
suffixNonLetters = word[-1] + suffixNonLetters
47.
word = word[:-1]
48.
# Запоминаем, находится ли слово полностью или только первые буквы
49.
# в верхнем регистре.
50.
wasUpper = word.isupper()
51.
wasTitle = word.istitle()
52.
53.
word = word.lower() # Переводим слово в нижний регистр для перевода.
54.
55.
# Отделяем согласные буквы в начале слова:
56.
prefixConsonants = ''
57.
while len(word) > 0 and not word[0] in VOWELS:
58.
prefixConsonants += word[0]
59.
word = word[1:]
60.
61.
# Добавляем в слово "поросячье" окончание:
62.
if prefixConsonants != '':
63.
word += prefixConsonants + 'ay'
64.
else:
65.
word += 'yay'
66.
# Переводим слово полностью или только первые буквы обратно
67.
# в верхний регистр:
68.
if wasUpper:
69.
word = word.upper()
70.
if wasTitle:
71.
word = word.title()
72.
73.
# Добавляем небуквенные символы обратно в начало слова.
74.
pigLatin += prefixNonLetters + word + suffixNonLetters + ' '
75.
return pigLatin
76.
77.
78. if __name__ == '__main__':
79.
main()

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если message.split() в строке 33 заменить на message?
2. Что будет, если ('a', 'e', 'i', 'o', 'u', 'y') в строке 11 заменить на ()?
3. Что будет, если ('a', 'e', 'i', 'o', 'u', 'y') в строке 11 заменить на ('A',
'E', 'I', 'O', 'U', 'Y')?

55
Лотерея Powerball

Лотерея Powerball — замечательный способ потерять немного денег. Купив билет за 2 доллара, вы можете выбрать
шесть номеров: пять из диапазона от 1 до 69, и шестой
номер Powerball — от 1 до 26. Порядок номеров значения
не имеет. Если выпадут выбранные вами числа — вы выиграете 1 миллиард 586 миллионов долларов! Правда, вы не
выиграете, поскольку на это есть только один шанс из 292 201 338.
Но если вы потратите 200 долларов на 100 билетов, то ваши шансы улучшатся до
1 из 2 922 013. Вы тоже не выиграете, но зато потратите в 100 раз больше денег. Чем
больше вы любите терять деньги, тем интереснее участвовать в лотерее!
Чтобы наглядно показать вам, насколько часто вы будете проигрывать в лотерее,
эта программа моделирует миллион розыгрышей Powerball, а затем сравнивает их
с выбранными вами номерами. Теперь вы сможете совершенно бесплатно получить
удовольствие от проигрыша в лотерее.
Любопытный факт: шансы выиграть у всех наборов из шести номеров одинаковы. Так что когда в следующий раз будете покупать лотерейный билет, выберите
номера 1, 2, 3, 4, 5 и 6. Вероятность их выпадения точно такая же, как и у любого
другого набора.

Описание работы  

291

Программа в действии
Результат выполнения powerballlottery.py выглядит следующим образом:
Powerball Lottery, by Al Sweigart al@inventwithpython.com
Each powerball lottery ticket costs $2. The jackpot for this game
is $1.586 billion! It doesn't matter what the jackpot is, though,
because the odds are 1 in 292,201,338, so you won't win.
This simulation gives you the thrill of playing without wasting money.
Enter 5 different numbers from 1 to 69, with spaces between
each number. (For example: 5 17 23 42 50 51)
> 1 2 3 4 5
Enter the powerball number from 1 to 26.
> 6
How many times do you want to play? (Max: 1000000)
> 1000000
It costs $2000000 to play 1000000 times, but don't
worry. I'm sure you'll win it all back.
Press Enter to start...
The winning numbers are: 12 29 48 11 4 and 13 You lost.
The winning numbers are: 54 39 3 42 16 and 12 You lost.
The winning numbers are: 56 4 63 23 38 and 24 You lost.
--сокращено-The winning numbers are: 46 29 10 62 17 and 21 You lost.
The winning numbers are: 5 20 18 65 30 and 10 You lost.
The winning numbers are: 54 30 58 10 1 and 18 You lost.
You have wasted $2000000
Thanks for playing!

Описание работы
Результаты работы этой программы выглядят распределенными вполне однообразно, поскольку код allWinningNums.ljust(21) в строке 109 дополняет числа пробелами так, чтобы они занимали 21 столбец, вне зависимости от количества цифр
в выигравших номерах. В результате надпись You lost всегда появляется в одном
месте экрана и хорошо читается, даже когда программа быстро выводит на экран
много строк.
1.
2.
3.
4.
5.
6.
7.

"""Лотерея Powerball, (c) Эл Свейгарт al@inventwithpython.com
Моделирование лотереи, позволяющее почувствовать все удовольствие от
проигрыша в лотерее, не тратя понапрасну денег.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, юмор, имитационное моделирование"""
import random

292   Проект 55. Лотерея Powerball
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.

print('''Powerball Lottery, by Al Sweigart al@inventwithpython.com
Each powerball lottery ticket costs $2. The jackpot for this game
is $1.586 billion! It doesn't matter what the jackpot is, though,
because the odds are 1 in 292,201,338, so you won't win.
This simulation gives you the thrill of playing without wasting money.
''')
# Запрашиваем у игрока первые пять номеров, от 1 до 69:
while True:
print('Enter 5 different numbers from 1 to 69, with spaces between')
print('each number. (For example: 5 17 23 42 50)')
response = input('> ')
# Убеждаемся, что игрок ввел пять номеров:
numbers = response.split()
if len(numbers) != 5:
print('Please enter 5 numbers, separated by spaces.')
continue
# Преобразуем строковые значения в числа:
try:
for i in range(5):
numbers[i] = int(numbers[i])
except ValueError:
print('Please enter numbers, like 27, 35, or 62.')
continue
# Проверяем, что номера — в диапазоне от 1 до 69:
for i in range(5):
if not (1 ').upper()
if playerMove == 'Q':
print('Thanks for playing!')
sys.exit()
if playerMove == 'R' or playerMove == 'P' or playerMove == 'S':
break
else:
print('Type one of R, P, S, or Q.')
# Отображаем на экране выбранный игроком ход:
if playerMove == 'R':

308   Проект 59. Камень, ножницы, бумага
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.

print('ROCK versus...')
playerMove = 'ROCK'
elif playerMove == 'P':
print('PAPER versus...')
playerMove = 'PAPER'
elif playerMove == 'S':
print('SCISSORS versus...')
playerMove = 'SCISSORS'
# Считаем до трех с драматическими паузами:
time.sleep(0.5)
print('1...')
time.sleep(0.25)
print('2...')
time.sleep(0.25)
print('3...')
time.sleep(0.25)
# Отображаем на экране выбранный компьютером ход:
randomNumber = random.randint(1, 3)
if randomNumber == 1:
computerMove = 'ROCK'
elif randomNumber == 2:
computerMove = 'PAPER'
elif randomNumber == 3:
computerMove = 'SCISSORS'
print(computerMove)
time.sleep(0.5)
# Отображаем и фиксируем победу/поражение/ничью:
if playerMove == computerMove:
print('It\'s a tie!')
ties = ties + 1
elif playerMove == 'ROCK' and computerMove == 'SCISSORS':
print('You win!')
wins = wins + 1
elif playerMove == 'PAPER' and computerMove == 'ROCK':
print('You win!')
wins = wins + 1
elif playerMove == 'SCISSORS' and computerMove == 'PAPER':
print('You win!')
wins = wins + 1
elif playerMove == 'ROCK' and computerMove == 'PAPER':
print('You lose!')
losses = losses + 1
elif playerMove == 'PAPER' and computerMove == 'SCISSORS':
print('You lose!')
losses = losses + 1
elif playerMove == 'SCISSORS' and computerMove == 'ROCK':
print('You lose!')
losses = losses + 1

Исследование программы  

309

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
добавьте в игру ходы «ящерица» и «Спок». Ящерица травит Спока и ест бумагу, но ее давит камень и обезглавливают ножницы. Спок ломает ножницы
и испаряет камень, но его травит ящерица, а бумага содержит улики против
него;
пусть игрок получает очко за каждую победу и теряет — за поражение. При
выигрыше игрок может также выбирать «пан или пропал» и, возможно, выиграть больше очков в 2, 4, 8, 16 и т. д. раз в последующих раундах.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Какое сообщение об ошибке вы получите, если random.randint(1, 3) в строке 54 замените на random.randint(1, 300)?
2. Что будет, если playerMove == computerMove в строке 65 заменить на True?

60
Камень, ножницы, бумага
(беспроигрышная версия)

Этот вариант игры «Камень, ножницы, бумага» идентичен
проекту 59, за исключением того что игрок всегда выигрывает. Выбирающий ход компьютера код устроен так,
чтобы всегда выбирать проигрышный. Можете предложить
сыграть в эту игру своим друзьям, наверное, они будут очень
радоваться... сначала. Посмотрим, сколько времени им понадобится, чтобы заметить жульничество игры в их пользу.

Программа в действии
Результат выполнения rockpaperscissorsalwayswin.py выглядит следующим образом:
Rock, Paper, Scissors, by Al Sweigart al@inventwithpython.com
- Rock beats scissors.
- Paper beats rocks.
- Scissors beats paper.
0 Wins, 0 Losses, 0 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
> p
PAPER versus...

Описание работы  

311

1...
2...
3...
ROCK
You win!
1 Wins, 0 Losses, 0 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
> s
SCISSORS versus...
1...
2...
3...
PAPER
You win!
2 Wins, 0 Losses, 0 Ties
--сокращено-SCISSORS versus...
1...
2...
3...
PAPER
You win!
413 Wins, 0 Losses, 0 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
--сокращено--

Описание работы
Возможно, вы обратите внимание, что эта версия программы короче, чем в проекте 59. Логично: когда не требуется выбирать ход компьютера случайным образом
и вычислять результаты игры, можно убрать довольно много кода. Вдобавок отсутствуют переменные для отслеживания количества проигрышей и ничьих, ведь
они все равно были бы всегда равны нулю.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.

""" Камень, ножницы, бумага (беспроигрышная версия)
(c) Эл Свейгарт al@inventwithpython.com
Классическая азартная игра на руках, в которой вы всегда выигрываете
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, игра, юмор"""
import time, sys
print('''Rock, Paper, Scissors, by Al Sweigart al@inventwithpython.com
- Rock beats scissors.
- Paper beats rocks.
- Scissors beats paper.
''')

312   Проект 60. Камень, ножницы, бумага (беспроигрышная версия)
15. # Переменная для отслеживания количества выигрышей.
16. wins = 0
17.
18. while True: # Основной цикл программы.
19.
while True: # Запрашиваем, пока игрок не введет R, P, S или Q.
20.
print('{} Wins, 0 Losses, 0 Ties'.format(wins))
21.
print('Enter your move: (R)ock (P)aper (S)cissors or (Q)uit')
22.
playerMove = input('> ').upper()
23.
if playerMove == 'Q':
24.
print('Thanks for playing!')
25.
sys.exit()
26.
27.
if playerMove == 'R' or playerMove == 'P' or playerMove == 'S':
28.
break
29.
else:
30.
print('Type one of R, P, S, or Q.')
31.
32.
# Отображаем на экране выбранный игроком ход:
33.
if playerMove == 'R':
34.
print('ROCK versus...')
35.
elif playerMove == 'P':
36.
print('PAPER versus...')
37.
elif playerMove == 'S':
38.
print('SCISSORS versus...')
39.
40.
# Считаем до трех с драматическими паузами:
41.
time.sleep(0.5)
42.
print('1...')
43.
time.sleep(0.25)
44.
print('2...')
45.
time.sleep(0.25)
46.
print('3...')
47.
time.sleep(0.25)
48.
49.
# Отображаем на экране выбранный компьютером ход:
50.
if playerMove == 'R':
51.
print('SCISSORS')
52.
elif playerMove == 'P':
53.
print('ROCK')
54.
elif playerMove == 'S':
55.
print('PAPER')
56.
57.
time.sleep(0.5)
58.
59.
print('You win!')
60.
wins = wins + 1

Когда вы введете исходный код и запустите егонесколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:

Исследование программы  

313

добавьте в игру ходы «ящерица» и «Спок». Ящерица травит Спока и ест бумагу, но ее давит камень и обезглавливают ножницы. Спок ломает ножницы
и испаряет камень, но его травит ящерица, а бумага содержит улики против
него;
пусть игрок получает очко за каждую победу. При выигрыше игрок может
также выбирать «пан или пропал» и, конечно, выиграть больше очков в 2, 4,
8, 16 и т. д. раз в последующих раундах.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если удалить или закомментировать строки с 33-й по 57-ю?
2. Что будет, если input('> ').upper() в строке 22 заменить на input('> ')?

61
Шифр ROT13

Название шифра ROT13, одного из простейших алгоритмов шифрования, означает rotate 13 spaces («сдвинуть
на 13 позиций»). В этом шифре буквам от A до Z соответствуют числа от 0 до 25 таким образом, что зашифрованная буква отстоит на 13 позиций от незашифрованной:
A превращается в N, B — в O и т. д. Процесс дешифрования
совпадает с процессом шифрования, благодаря чему написание
программы становится тривиальным. Однако и взломать этот шифр очень просто.
Поэтому чаще всего ROT13 используется для сокрытия несекретной информации,
например спойлеров или ответов в викторинах, чтобы просто не прочитать их
случайно. Дополнительную информацию о шифре ROT13 можно найти в статье
«Википедии»: https://ru.wikipedia.org/wiki/ROT13. Если вас интересует более общая
информация о шифрах и их взломе, то можете прочитать мою книгу Cracking Codes
with Python.

Программа в действии
Результат выполнения rot13cipher.py выглядит следующим образом:
ROT13 Cipher, by Al Sweigart al@inventwithpython.com
Enter a message to encrypt/decrypt (or QUIT):
> Meet me by the rose bushes tonight.
The translated message is:

Описание работы  

315

Zrrg zr ol gur ebfr ohfurf gbavtug.
(Copied to clipboard.)
Enter a message to encrypt/decrypt (or QUIT):
--сокращено--

Описание работы
Значительная часть кода программы ROT13 совпадает с программой проекта 6,
хотя она намного проще, ведь в ней всегда применяется ключ 13. А поскольку за
шифрование и расшифровку отвечает один и тот же код (строки с 27-й по 39-ю),
можно не спрашивать пользователя, какой режим необходим.
Единственное отличие: эта программа сохраняет регистр исходного сообщения,
а не преобразует автоматически его в верхний регистр. Например, Hello в зашифрованном виде выглядит как Uryyb, а HELLO — как URYYB.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.

"""Шифр ROT13, (c) Эл Свейгарт al@inventwithpython.com
Простейший шифр сдвига для шифрования и дешифровки текста.
Подробнее — в статье https://ru.wikipedia.org/wiki/ROT13
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, криптография"""
try:

import pyperclip # pyperclip копирует текст в буфер обмена.
except ImportError:
pass # Если pyperclip не установлена, ничего не делаем. Не проблема.
# Задаем константы:
UPPER_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
LOWER_LETTERS = 'abcdefghijklmnopqrstuvwxyz'
print('ROT13 Cipher, by Al Sweigart al@inventwithpython.com')
print()
while True: # Основной цикл программы.
print('Enter a message to encrypt/decrypt (or QUIT):')
message = input('> ')
if message.upper() == 'QUIT':
break # Выходим из основного цикла программы.
# Сдвигаем буквы в сообщении на 13 позиций.
translated = ''
for character in message:
if character.isupper():
# Выполняем конкатенацию символа в верхнем регистре.
transCharIndex = (UPPER_LETTERS.find(character) + 13) % 26

316   Проект 61. Шифр ROT13
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.

translated += UPPER_LETTERS[transCharIndex]
elif character.islower():
# Выполняем конкатенацию символа в нижнем регистре.
transCharIndex = (LOWER_LETTERS.find(character) + 13) % 26
translated += LOWER_LETTERS[transCharIndex]
else:
# Выполняем конкатенацию символа в исходном виде.
translated += character
# Отображаем преобразованное сообщение:
print('The translated message is:')
print(translated)
print()
try:

# Копируем преобразованное сообщение в буфер обмена:
pyperclip.copy(translated)
print('(Copied to clipboard.)')
except:
pass

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если character.isupper() в строке 29 заменить на charac­ter.is­
lower()?
2. Что будет, если print(translated) в строке 43 заменить на print(message)?

62
Вращающийся куб

Этот проект демонстрирует на экране динамическое изображение вращающегося трехмерного куба, используя
тригонометрические функции. При желании вы можете
приспособить математику вращения трехмерной точки
и функцию line() к собственным программам.
И хотя текстовые символы блоков, с помощью которых мы будем рисовать куб, не слишком напоминают тонкие прямые линии,
подобные рисунки называются каркасной моделью (wireframe model), поскольку
визуализируют только ребра поверхностей объекта. На рис. 62.1 показана каркасная
модель куба и икосферы — приближения сферы, составленной из треугольников.

Рис. 62.1. Каркасные модели для куба (слева) и икосферы (справа)

318   Проект 62. Вращающийся куб

Программа в действии
На рис. 62.2 показан результат выполнения rotatingcube.py.

Рис. 62.2. Каркасная модель куба, которую наша программа рисует на экране

Описание работы
Наш алгоритм состоит из двух основных составляющих: функции line() и функции rotatePoint(). У куба — восемь точек, по одной для каждого угла. Программа
хранит эти углы в виде кортежей (x, y, z) в списке CUBE_CORNERS. Кроме того, эти
точки задают связи граней куба. При вращении всех точек в одном направлении
на одинаковый угол возникает иллюзия вращения куба.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.

"""Вращающийся куб, (c) Эл Свейгарт al@inventwithpython.com
Динамическое изображение вращающегося куба. Нажмите Ctrl+C для останова.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, графика, математическая"""
# Эту программу следует запускать в окне терминала/командной оболочки.
import math, time, sys, os
# Задаем константы:

Описание работы  

11.
12.
13.
14.
15.

319

PAUSE_AMOUNT = 0.1 # Пауза на 1/10 секунды.
WIDTH, HEIGHT = 80, 24
SCALEX = (WIDTH - 4) // 8
SCALEY = (HEIGHT - 4) // 8
# Высота текстовых ячеек в два раза превышает ширину,
# так что задаем масштаб по оси y:
16. SCALEY *= 2
17. TRANSLATEX = (WIDTH - 4) // 2
18. TRANSLATEY = (HEIGHT - 4) // 2
19.
20. # (!) Попробуйте заменить это значение на '#', или '*',
# или еще какой-либо символ:
21. LINE_CHAR = chr(9608) # Символ 9608 — '█'
22.
23. # (!) Попробуйте обнулить два из этих значений, чтобы вращать куб
24. # относительно только одной оси координат:
25. X_ROTATE_SPEED = 0.03
26. Y_ROTATE_SPEED = 0.08
27. Z_ROTATE_SPEED = 0.13
28.
29. # Координаты XYZ данная программа хранит в списках, координату X —
30. # по индексу 0, Y — 1, а Z — 2. Эти константы повышают удобочитаемость
31. # кода при обращении к координатам в этих списках.
32. X = 0
33. Y = 1
34. Z = 2
35.
36.
37. def line(x1, y1, x2, y2):
38.
"""Возвращает список точек, лежащих на прямой между заданными точками.
39.
40.
Использует алгоритм Брезенхэма. Подробнее — в статье на
41.
https://ru.wikipedia.org/wiki/Алгоритм_Брезенхэма"""
42.
points = [] # Содержит точки прямой.
43.
# "Steep" означает, что уклон прямой больше 45 градусов
44.
# или меньше –45 градусов:
45.
46.
# Проверяем на предмет частного случая, когда начальная и конечная точки
47.
# являются в определенном смысле соседними, что данная функция не умеет
48.
# хорошо обрабатывать, поэтому возвращаем заранее подготовленный список:
49.
if (x1 == x2 and y1 == y2 + 1) or (y1 == y2 and x1 == x2 + 1):
50.
return [(x1, y1), (x2, y2)]
51.
52.
isSteep = abs(y2 - y1) > abs(x2 - x1)
53.
if isSteep:
54.
# Этот алгоритм умеет работать только с некрутыми прямыми,
55.
# так что делаем уклон не таким крутым, а потом возвращаем обратно.
56.
x1, y1 = y1, x1 # Меняем местами x1 и y1
57.
x2, y2 = y2, x2 # Меняем местами x2 и y2
58.
isReversed = x1 > x2 # True, если прямая идет справа налево.
59.

320   Проект 62. Вращающийся куб
60.
if isReversed: # Получаем точки на прямой, идущей справа налево.
61.
x1, x2 = x2, x1 # Меняем местами x1 и x2
62.
y1, y2 = y2, y1 # Меняем местами y1 и y2
63.
64.
deltax = x2 - x1
65.
deltay = abs(y2 - y1)
66.
extray = int(deltax / 2)
67.
currenty = y2
68.
if y1 < y2:
69.
ydirection = 1
70.
else:
71.
ydirection = -1
72.
# Вычисляем y для каждого x в этой прямой:
73.
for currentx in range(x2, x1 - 1, -1):
74.
if isSteep:
75.
points.append((currenty, currentx))
76.
else:
77.
points.append((currentx, currenty))
78.
extray -= deltay
79.
if extray
>*
*>
>
>
>
|
|
i|
j|
k|****l|
m|
n|
o|
p|
+--^--+-----+-----+-----+-----+-----+-----+--v--+
|*****|
|
|
|
|*****|
|
|*
*<
<
<
|
|*
*<
|
|****d|
c|
b|
a|
|****r|
q|
+-----+-----+-----+--^--+
+--v--+-----+
^
v
Home
Goal
OOOOOOO
.......
It is O's turn. Press Enter to flip...
Flips: H-H-H-H Select token to move 4 spaces: home quit
> home
O landed on a flower space and gets to go again.
Press Enter to continue...
--сокращено--

Описание работы
Подобно проекту 43, клетки на доске, нарисованной с помощью ASCII-графики,
помечены буквами от a до t. Бросив кости, игрок может выбрать клетку со своей
фишкой, чтобы пойти ею, либо выбрать home, чтобы начать ходить фишкой из дома
на доску. Доска в программе представлена с помощью ассоциативного массива
с ключами от 'a' до 't' и значениями 'X' и 'O' для фишек (и ' ' для пустых клеток).
Кроме того, в этом ассоциативном массиве есть ключи 'x_home', 'o_home', 'x_goal'
и 'o_goal', значениями для которых служат строки из семи символов, отража­ющие
заполнение домов и финишных полей. Символы 'X' и 'O' в этих строковых значениях обозначают фишки в доме или на финише, а '.' обозначает незанятое место.
Функция displayBoard() отображает эти строковые значения из семи символов
на экране.

Описание работы  

1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.

"""Царская игра Ура, (c) Эл Свейгарт al@inventwithpython.com
Игра 5000-летней давности из Месопотамии. Два игрока бьют фишки
друг друга, стремясь достичь финиша.
Подробнее — в статье на https://ru.wikipedia.org/wiki/Царская_игра_Ура
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, настольная игра, игра, для двух игроков
"""
import random, sys
X_PLAYER = 'X'
O_PLAYER = 'O'
EMPTY = ' '
# Задаем
X_HOME =
O_HOME =
X_GOAL =
O_GOAL =

константы для меток клеток доски:
'x_home'
'o_home'
'x_goal'
'o_goal'

# Клетки доски слева направо, сверху вниз:
ALL_SPACES = 'hgfetsijklmnopdcbarq'
X_TRACK = 'HefghijklmnopstG' # (H означает Дом, G означает Финиш.)
O_TRACK = 'HabcdijklmnopqrG'
FLOWER_SPACES = ('h', 't', 'l', 'd', 'r')
BOARD_TEMPLATE = """
{}
{}
Home
Goal
v
^
+-----+-----+-----+--v--+
+--^--+-----+
|*****|
|
|
|
|*****|
|
|* {} *< {} < {} < {} |
|* {} *< {} |
|****h|
g|
f|
e|
|****t|
s|
+--v--+-----+-----+-----+-----+-----+-----+--^--+
|
|
|
|*****|
|
|
|
|
| {} > {} > {} >* {} *> {} > {} > {} > {} |
|
i|
j|
k|****l|
m|
n|
o|
p|
+--^--+-----+-----+-----+-----+-----+-----+--v--+
|*****|
|
|
|
|*****|
|
|* {} *< {} < {} < {} |
|* {} *< {} |
|****d|
c|
b|
a|
|****r|
q|
+-----+-----+-----+--^--+
+--v--+-----+
^
v
Home
Goal
{}
{}
"""
def main():
print('''The Royal Game of Ur, by Al Sweigart

327

328   Проект 63. Царская игра Ура
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.

This is a 5,000 year old game. Two players must move their tokens
from their home to their goal. On your turn you flip four coins and can
move one token a number of spaces equal to the heads you got.
Ur is a racing game; the first player to move all seven of their tokens
to their goal wins. To do this, tokens must travel from their home to
their goal:
X Home
X Goal
v
^
+---+---+---+-v-+
+-^-+---+
|v>>>>>>>>>>>>>>>^ |
|>>>>>>>>>>>>>>>>>>>>>>>>>>>>>v |
+^--+---+---+---+---+---+---+-v-+
|^ |
|
|
|
|
| v |
|^ ').upper()

Описание работы  

90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.

if response == 'QUIT':
sys.exit()
if response in (w + a + s + d).replace(' ', ''):
return response

def makeMove(board, move):
"""Производит заданный ход move на заданной доске board."""
# Примечание: эта функция предполагает, что ход допустим.
bx, by = findBlankSpace(board)
if move == 'W':
board[bx][by],
elif move == 'A':
board[bx][by],
elif move == 'S':
board[bx][by],
elif move == 'D':
board[bx][by],

board[bx][by+1] = board[bx][by+1], board[bx][by]
board[bx+1][by] = board[bx+1][by], board[bx][by]
board[bx][by-1] = board[bx][by-1], board[bx][by]
board[bx-1][by] = board[bx-1][by], board[bx][by]

def makeRandomMove(board):
"""Передвигает костяшку случайным образом."""
blankx, blanky = findBlankSpace(board)
validMoves = []
if blanky != 3:
validMoves.append('W')
if blankx != 3:
validMoves.append('A')
if blanky != 0:
validMoves.append('S')
if blankx != 0:
validMoves.append('D')
makeMove(board, random.choice(validMoves))
def getNewPuzzle(moves=200):
"""Генерируем новую игру с помощью случайных ходов из упорядоченного
состояния."""
board = getNewBoard()
for i in range(moves):
makeRandomMove(board)
return board

# Если программа не импортируется, а запускается, выполняем запуск:
if __name__ == '__main__':
main()

353

354   Проект 68. Игра в 15
Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
создать более сложный вариант этой головоломки, на доске 5 × 5;
создать режим «автоматического решения головоломки», в котором сохраняется текущее расположение костяшек, после чего программа выполняет
до 40 случайных ходов и прекращает работу, если головоломка решена.
В противном случае программа загружает сохраненное состояние и делает
еще 40 случайных ходов.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если getNewPuzzle() в строке 22 заменить на getNewPuzzle(1)?
2. Что будет, если getNewPuzzle() в строке 22 заменить на getNewPuzzle(0)?
3. Что будет, если удалить или закомментировать sys.exit() в строке 31?

69
Бега улиток

Вряд ли вы получите много адреналина от бегущих...
улиток. Но нехватку скорости они с лихвой компенсируют очаровательностью ASCII-графики. Все улитки
(изображаемые с помощью символа @ в качестве раковины
и v в качестве двух глазных стебельков) медленно, но верно
движутся к финишной прямой. В гонках может участвовать
до восьми улиток, оставляющих позади след слизи, каждая со
своим, выбранным пользователем именем. Эта программа хорошо подходит для
начинающих.

Программа в действии
Результат выполнения snailrace.py выглядит следующим образом:
Snail Race, by Al Sweigart al@inventwithpython.com
@v 3
Enter snail #1's name:
> Alice
Enter snail #2's name:
> Bob
Enter snail #3's name:
> Carol

356   Проект 69. Бега улиток
START
|

Alice
......@v
Bob
.....@v
Carol
.......@v
--сокращено--

FINISH
|

Описание работы
В этой программе используются две структуры данных, хранящихся в двух переменных: snailNames представляет собой список строковых значений для имен улиток,
а snailProgress — ассоциативный массив, ключами которого служат имена улиток,
а значения представляют собой целые числа, показывающие, сколько знако-мест
проползла каждая улитка. В строках с 79-й по 82-ю на основе данных из этих переменных все улитки отрисовываются в соответствующих местах экрана.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.

"""Бега улиток, (c) Эл Свейгарт al@inventwithpython.com
Бега быстроногих улиток!
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, графика, для начинающих, игра, многопользовательская"""
import random, time, sys
# Задаем константы:
MAX_NUM_SNAILS = 8
MAX_NAME_LENGTH = 20
FINISH_LINE = 40 # (!) Попробуйте изменить это число.
print('''Snail Race, by Al Sweigart al@inventwithpython.com
@v ')
if response.isdecimal():
numSnailsRacing = int(response)
if 1 < numSnailsRacing pppiiiii
+================================+
I O O O O O O O | O O I
I | | | | | | | | | | I
I | | | | | | | O | | I
+================================+
I | | | | | | | | | O I
I | | | | | | | | | O I
I O O O O O O O O O O I
I O O O O O O O O O | I
I O O O O O O O O O | I
I O O O O O O O O O O I
+==0==0==0==0==0==0==0==5==0==3==+
+q w e r t y u i o p

Описание работы  

361

-a s d f g h j k l ;
(Enter a number, "quit", or a stream of up/down letters.)
--сокращено--

Описание работы
Функция displayAbacus() принимает аргумент number, на основе которого и визуализируются костяшки на абаке. На соробане всегда ровно 80 возможных позиций
для костяшек '0' и сегментов прутьев '|', как видно из фигурных скобок ({})
в многострочном строковом значении в строках кода с 127-й по 139-ю. Еще десять
пар фигурных скобок соответствуют цифрам в аргументе number.
Необходимо создать список строковых значений, чтобы заполнить эти фигурные
скобки, в порядке слева направо, сверху вниз. Код из функции displayAbacus()
заполняет список hasBead значениями True для отображения '0' и False для
отображения '|'. Первые десять значений в списке — верхний ряд «небес». Мы
будем ставить костяшку в этот ряд, только если цифра в столбце — 0, 1, 2, 3 или 4,
поскольку костяшка «неба» не окажется в этом ряду, если цифра в столбце — от 0
до 4. Для оставшихся рядов мы добавим булевы значения в hasBead.
В строках с 118-й по 123-ю hasBead используется для создания списка abacusChar,
содержащего сами строковые значения '0' и '|'. В сочетании с numberList в строке 126 программа формирует список chars, заполняющий фигурные скобки ({})
в многострочном строковом значении ASCII-графики соробана.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.

"""Японский абак — соробан, (c) Эл Свейгарт al@inventwithpython.com
Моделирование японского счетного инструмента типа абака.
Больше информации — в статье https://ru.wikipedia.org/wiki/Соробан
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, графика, математическая, имитационное моделирование"""
NUMBER_OF_DIGITS = 10
def main():
print('Soroban - The Japanese Abacus')
print('By Al Sweigart al@inventwithpython.com')
print()
abacusNumber = 0

# Число, отображаемое на абаке.

while True: # Основной цикл программы.
displayAbacus(abacusNumber)
displayControls()
commands = input('> ')
if commands == 'quit':

362   Проект 70. Соробан — японский абак
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.

# Выходим из программы:
break
elif commands.isdecimal():
# Задаем число на абаке:
abacusNumber = int(commands)
else:
# Обрабатываем команды увеличения/уменьшения:
for letter in commands:
if letter == 'q':
abacusNumber += 1000000000
elif letter == 'a':
abacusNumber -= 1000000000
elif letter == 'w':
abacusNumber += 100000000
elif letter == 's':
abacusNumber -= 100000000
elif letter == 'e':
abacusNumber += 10000000
elif letter == 'd':
abacusNumber -= 10000000
elif letter == 'r':
abacusNumber += 1000000
elif letter == 'f':
abacusNumber -= 1000000
elif letter == 't':
abacusNumber += 100000
elif letter == 'g':
abacusNumber -= 100000
elif letter == 'y':
abacusNumber += 10000
elif letter == 'h':
abacusNumber -= 10000
elif letter == 'u':
abacusNumber += 1000
elif letter == 'j':
abacusNumber -= 1000
elif letter == 'i':
abacusNumber += 100
elif letter == 'k':
abacusNumber -= 100
elif letter == 'o':
abacusNumber += 10
elif letter == 'l':
abacusNumber -= 10
elif letter == 'p':
abacusNumber += 1
elif letter == ';':
abacusNumber -= 1
# Абак не может отображать отрицательные числа:
if abacusNumber < 0:

Описание работы  

363

74.
abacusNumber = 0 # Заменяем все отрицательные числа на 0.
75.
# Абак не может отображать числа, превышающие 9 999 999 999:
76.
if abacusNumber > 9999999999:
77.
abacusNumber = 9999999999
78.
79.
80. def displayAbacus(number):
81.
numberList = list(str(number).zfill(NUMBER_OF_DIGITS))
82.
83.
hasBead = [] # Содержит для каждой позиции костяшек True или False.
84.
85.
# Костяшка в верхнем ряду "неба" соответствует цифрам 0, 1, 2, 3 или 4.
86.
for i in range(NUMBER_OF_DIGITS):
87.
hasBead.append(numberList[i] in '01234')
88.
89.
# Костяшка в верхнем ряду "неба" соответствует цифрам 5, 6, 7, 8 или 9.
90.
for i in range(NUMBER_OF_DIGITS):
91.
hasBead.append(numberList[i] in '56789')
92.
93.
# Костяшка в первом (верхнем) ряду "земли" выражает все цифры, кроме 0.
94.
for i in range(NUMBER_OF_DIGITS):
95.
hasBead.append(numberList[i] in '12346789')
96.
97.
# Костяшка в втором ряду "земли" выражает цифры 2, 3, 4, 7, 8 или 9.
98.
for i in range(NUMBER_OF_DIGITS):
99.
hasBead.append(numberList[i] in '234789')
100.
101.
# Костяшка в третьем ряду "земли" выражает цифры 0, 3, 4, 5, 8 или 9.
102.
for i in range(NUMBER_OF_DIGITS):
103.
hasBead.append(numberList[i] in '034589')
104.
105.
# Костяшка в четвертом ряду "земли" выражает цифры 0, 1, 2, 4, 5, 6 или 9.
106.
for i in range(NUMBER_OF_DIGITS):
107.
hasBead.append(numberList[i] in '014569')
108.
109.
# Костяшка в пятом ряду "земли" выражает цифры 0, 1, 2, 5, 6 или 7.
110.
for i in range(NUMBER_OF_DIGITS):
111.
hasBead.append(numberList[i] in '012567')
112.
113.
# Костяшка в шестом ряду "земли" выражает цифры 0, 1, 2, 3, 5, 6, 7 или 8.
114.
for i in range(NUMBER_OF_DIGITS):
115.
hasBead.append(numberList[i] in '01235678')
116.
117.
# Преобразуем эти значения True/False в символы O или |.
118.
abacusChar = []
119.
for i, beadPresent in enumerate(hasBead):
120.
if beadPresent:
121.
abacusChar.append('O')
122.
else:
123.
abacusChar.append('|')
124.

364   Проект 70. Соробан — японский абак
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.

# Рисуем абак с символами O/|.
chars = abacusChar + numberList
print("""
+================================+
I {} {} {} {} {} {} {} {} {} {} I
I | | | | | | | | | | I
I {} {} {} {} {} {} {} {} {} {} I
+================================+
I {} {} {} {} {} {} {} {} {} {} I
I {} {} {} {} {} {} {} {} {} {} I
I {} {} {} {} {} {} {} {} {} {} I
I {} {} {} {} {} {} {} {} {} {} I
I {} {} {} {} {} {} {} {} {} {} I
I {} {} {} {} {} {} {} {} {} {} I
+=={}=={}=={}=={}=={}=={}=={}=={}=={}=={}==+""".format(*chars))
def displayControls():
print(' +q w e r t y u i o p')
print(' -a s d f g h j k l ;')
print('(Enter a number, "quit", or a stream of up/down letters.)')
if __name__ == '__main__':
main()

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если abacusNumber = 0 в строке 15 заменить на abacusNumber = 9999?
2. Что будет, если abacusChar.append('O') в строке 121 заменить на abacus­
Char.append('@')?

71
Повторение музыки

Эта игра на запоминание напоминает электронную
игрушку «Саймон» (Simon) и использует сторонний
модуль playsound для проигрывания четырех различных
звуков, соответствующих клавишам A, S, D и F на клавиатуре. По мере того как вы правильно воспроизводите выдаваемую игрой мелодию, она становится все длиннее и длиннее.
Сколько звуков вы можете удержать в кратковременной памяти?
Если взглянуть на код, то можно заметить, что в функцию playsound.playsound()
передается имя звукового файла для проигрывания. Скачать соответствующие
звуковые файлы можно по следующим URL:
https://inventwithpython.com/soundA.wav;
https://inventwithpython.com/soundS.wav;
https://inventwithpython.com/soundD.wav;
https://inventwithpython.com/soundF.wav.

Перед запуском программы поместите эти файлы в тот же каталог, в котором расположен soundmimic.py. Подробнее о модуле playsound можно узнать на сайте
https://pypi.org/project/playsound/. Пользователям, работающим в macOS, для работы
playsound необходимо также установить модуль pyobjc с https://pypi.org/project/
pyobjc/.

366   Проект 71. Повторение музыки

Программа в действии
Результат выполнения soundmimic.py выглядит следующим образом:
Sound Mimic, by Al Sweigart al@inventwithpython.com
Try to memorize a pattern of A S D F letters (each with its own sound)
as it gets longer and longer.
Press Enter to begin...

Pattern: S

Enter the pattern:
> s
Correct!

Pattern: S F

Enter the pattern:
> sf
Correct!

Pattern: S F F

Enter the pattern:
> sff
Correct!

Pattern: S F F D
--сокращено--

Описание работы
Эта программа импортирует модуль playsound, проигрывающий звуковые файлы.
Модуль включает одну функцию, playsound(), которой можно передать имя файла
типа .wav или .mp3 для проигрывания. С каждым новым раундом игры программа
добавляет в список pattern выбранную случайным образом букву (A, S, D или F)
и проигрывает содержащиеся в списке звуки. По мере роста списка pattern растет
и длина мелодии, которую должен запомнить игрок.
1.
2.
3.
4.
5.
6.
7.
8.
9.

"""Повторение музыки, (c) Эл Свейгарт al@inventwithpython.com
Игра с подбором звуковых соответствий. Попробуйте запомнить все
возрастающую последовательность букв. Создана под впечатлением
от электронной игры "Саймон".
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, для начинающих, игра"""
import random, sys, time

Описание работы  

10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.

#
#
#
#
#

367

Скачайте звуковые файлы с этих URL (или воспользуйтесь своими):
https://inventwithpython.com/soundA.wav
https://inventwithpython.com/soundS.wav
https://inventwithpython.com/soundD.wav
https://inventwithpython.com/soundF.wav

try:

import playsound
except ImportError:
print('The playsound module needs to be installed to run this')
print('program. On Windows, open a Command Prompt and run:')
print('pip install playsound')
print('On macOS and Linux, open a Terminal and run:')
print('pip3 install playsound')
sys.exit()
print('''Sound Mimic, by Al Sweigart al@inventwithpython.com
Try to memorize a pattern of A S D F letters (each with its own sound)
as it gets longer and longer.''')
input('Press Enter to begin...')
pattern = ''
while True:
print('\n' * 60)

# Очищаем экран, выводя несколько символов новой строки.

# Добавляем в pattern случайную букву:
pattern = pattern + random.choice('ASDF')
# Выводим pattern на экран (и проигрываем соответствующие звуки):
print('Pattern: ', end='')
for letter in pattern:
print(letter, end=' ', flush=True)
playsound.playsound('sound' + letter + '.wav')
time.sleep(1) # Добавляем в конце небольшую паузу.
print('\n' * 60) # Очищаем экран с помощью вывода нескольких символов
# новой строки.
# Просим пользователя ввести последовательность:
print('Enter the pattern:')
response = input('> ').upper()
if response != pattern:
print('Incorrect!')
print('The pattern was', pattern)
else:
print('Correct!')
for letter in pattern:
playsound.playsound('sound' + letter + '.wav')

368   Проект 71. Повторение музыки
61.
62.
63.
64.
65.
66.
67.

if response != pattern:
print('You scored', len(pattern) - 1, 'points.')
print('Thanks for playing!')
break
time.sleep(1)

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если удалить или закомментировать print('\n' * 60) в строке 47?
2. Что будет, если response != pattern в строке 62 заменить на False?

72
Губкорегистр

Наверное, вы видели мем «Язвительный Губка Боб»:
картинку с Губкой Бобом Квадратные Штаны, заголовок
которой написан в регистре, где саркастично чередуются
строчные и заглавные буквы, вот так: «мЕмЫ с ГуБкОй
БоБоМ нЕ дЕлАюТ вАс ОсТрОуМнЕе». Ради разнообразия
иногда в тексте регистр не чередуется.
Сообщения преобразуются в «губкорегистр» в этой программе с помощью строковых методов upper() и lower(). Вдобавок программа устроена так, что другие
программы могут импортировать ее в качестве модуля, задействуя оператор import
spongecase, после чего вызывать функцию spongecase.englishToSpongecase().

пРоГрАмМа В дЕйСтВиИ
Результат выполнения spongecase.py выглядит следующим образом:
sPoNgEcAsE, bY aL sWeIGaRt Al@iNvEnTwItHpYtHoN.cOm
eNtEr YoUr MeSsAgE:
> Using SpongeBob memes does not make you witty.
uSiNg SpOnGeBoB MeMeS dOeS NoT mAkE YoU wItTy.
(cOpIed SpOnGeTexT to ClIpbOaRd.)

370   Проект 72. Губкорегистр

оПиСаНиЕ рАбОтЫ
В строке 35 кода этой программы используется цикл for для обхода всех символов
строки message. Переменная useUpper содержит булево значение, указывающее,
нужно ли перевести символ в верхний регистр (True) или нижний (False). Строки 46 и 47 переключают (то есть меняют на противоположное) булево значение
в переменной useUpper в 90 % итераций. В результате регистр практически всегда
чередуется с верхнего на нижний.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.

"""гУбКоРеГиСтР, (c) Эл Свейгарт al@inventwithpython.com
Преобразует сообщения на английском языке в гУбКоТеКсТ.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: крошечная, для начинающих, слова"""
import random
try:

import pyperclip # pyperclip копирует текст в буфер обмена.
except ImportError:
pass # Если pyperclip не установлена, ничего не делаем. Не проблема.
def main():
"""Запускаем программу "ГубкоТЕКСТ"."""
print('''sPoNgEcAsE, bY aL sWeIGaRt Al@iNvEnTwItHpYtHoN.cOm
eNtEr YoUr MeSsAgE:''')
spongetext = englishToSpongecase(input('> '))
print()
print(spongetext)
try:

pyperclip.copy(spongetext)
print('(cOpIed SpOnGeTexT to ClIpbOaRd.)')
except:
pass # Если pyperclip не установлена, ничего не делаем.
def englishToSpongecase(message):
"""Возвращаем заданную строку в губкорегистре."""
spongetext = ''
useUpper = False
for character in message:
if not character.isalpha():
spongetext += character
continue
if useUpper:
spongetext += character.upper()

Исследование программы  

371

42.
else:
43.
spongetext += character.lower()
44.
45.
# Меняем регистр в 90 % случаев.
46.
if random.randint(1, 100) 0 and action[0] in ('R', 'N', 'U', 'O', 'Q'):
# Игрок ввел допустимое действие.
break
if len(action.split()) == 2:
space, number = action.split()
if len(space) != 2:
continue
column, row = space
if column not in list('ABCDEFGHI'):
print('There is no column', column)
continue
if not row.isdecimal() or not (1 ')
if text.upper() == 'QUIT':
print('Thanks for playing!')
sys.exit()
tts.say(text) # Текст, который должен произнести механизм TTS.
tts.runAndWait() # Запускаем произнесение этого текста механизмом TTS.

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

75
Три карты Монте

«Три карты Монте» — мошенничество, часто применяемое для облапошивания легковерных туристов и прочих
простофиль. На картонную коробку кладутся рубашкой
вверх три игральные карты, одна из которых — дама червей.
Сдающий карты проворно их перетасовывает, после чего
просит простофилю показать, какая из них — дама червей.
При этом сдающий применяет все возможные уловки, чтобы
спрятать карту или сжульничать каким-либо другим образом, лишая жертву всякого шанса выиграть. У сдающего также часто бывают сообщники в толпе зрителей,
якобы выигрывающие игру (чтобы жертва думала, что тоже может выиграть) или
специально с треском проигрывающие (чтобы жертва думала, что может сыграть
намного лучше).
Наша программа демонстрирует три карты, а затем быстро производит ряд их перестановок. В конце она очищает экран и просит игрока выбрать карту. Сможете ли
вы уследить за «красной леди»? Чтобы почувствовать по-настоящему, как играют
в три карты, можете включить опцию жульничества, в результате чего игрок всегда
будет проигрывать, даже если выберет нужную карту.

382   Проект 75. Три карты Монте

Программа в действии
Результат выполнения threecardmonte.py выглядит следующим образом:
Three-Card Monte, by Al Sweigart al@inventwithpython.com
Find the red lady (the Queen of Hearts)! Keep an eye on how
the cards move.
Here are the cards:
___
___
___
|J | |Q | |8 |
| ♦ | | ♥ | | ♣ |
|__J| |__Q| |__8|
Press Enter when you are ready to begin...
swapping left and middle...
swapping right and middle...
swapping middle and left...
swapping right and left...
swapping left and middle...
--сокращено-
Which card has the Queen of Hearts? (LEFT MIDDLE RIGHT)
> middle
___
___
___
|Q | |8 | |J |
| ♥ | | ♣ | | ♦ |
|__Q| |__8| |__J|
You lost!
Thanks for playing, sucker!

Описание работы
Каждая из игральных карт в этой программе представлена с помощью кортежа (достоинство, масть). Достоинство описывается строковым значением, например, '2',
'10', 'Q' или 'K', а масть представляет собой строковое значение с эмодзи червей,
треф, пик или бубен. Поскольку с помощью клавиатуры ввести символы эмодзи
нельзя, мы вызовем для их генерации в строках с 16-й по 19-ю функцию chr().
Кортеж ('9', '♦') соответствует девятке бубен.
Вместо того чтобы выводить эти кортежи непосредственно, функция displayCards()
в строках с 28-й по 43-ю анализирует их и выводит на экран их представления
в виде ASCII-графики, как в проекте 4. Аргумент cards данной функции представляет собой список кортежей игральных карт, благодаря чему можно выводить
несколько карт в ряд.

Описание работы  

1. """Три карты Монте (c) Эл Свейгарт al@inventwithpython.com
2. Найдите даму червей после перемешивания карт.
3. (В реальной жизни мошенник обычно прячет даму червей в руке, так что
4. вы никогда не выиграете.)
5. Подробнее — в статье на https://en.wikipedia.org/wiki/Three-card_Monte
6. Код размещен на https://nostarch.com/big-book-small-python-projects
7. Теги: большая, карточная игра, игра"""
8.
9. import random, time
10.
11. # Задаем константы:
12. NUM_SWAPS = 16
# (!) Попробуйте заменить это значение на 30 или 100.
13. DELAY
= 0.8 # (!) Попробуйте заменить это значение на 2.0 или 0.0.
14.
15. # Символы карточных мастей:
16. HEARTS
= chr(9829) # Символ 9829 – '♥'
17. DIAMONDS = chr(9830) # Символ 9830 – '♦'
18. SPADES
= chr(9824) # Символ 9824 – '♠'
19. CLUBS
= chr(9827) # Символ 9827 – '♣'
20. # Список кодов chr можно найти на https://inventwithpython.com/chr
21.
22. # Индексы списка из трех карт:
23. LEFT
= 0
24. MIDDLE = 1
25. RIGHT = 2
26.
27.
28. def displayCards(cards):
29.
"""Отображает карты из списка cards кортежей (достоинство, масть).
30.
"""
31.
rows = ['', '', '', '', ''] # Содержит текст для вывода на экран.
32.
33.
for i, card in enumerate(cards):
34.
rank, suit = card # card представляет собой структуру
35.
# данных — кортеж.
36.
rows[0] += ' ___ ' # Выводим верхнюю линию карты.
37.
rows[1] += '|{} | '.format(rank.ljust(2))
38.
rows[2] += '| {} | '.format(suit)
39.
rows[3] += '|_{}| '.format(rank.rjust(2, '_'))
40.
41.
# Построчно выводим на экран:
42.
for i in range(5):
43.
print(rows[i])
44.
45.
46. def getRandomCard():
47.
"""Возвращает случайную карту — НЕ даму червей."""
48.
while True: # Подбираем карты, пока не получим НЕ даму червей.
49.
rank = random.choice(list('23456789JQKA') + ['10'])
50.
suit = random.choice([HEARTS, DIAMONDS, SPADES, CLUBS])

383

384   Проект 75. Три карты Монте
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.

# Возвращаем карту, если это не дама червей:
if rank != 'Q' and suit != HEARTS:
return (rank, suit)

print('Three-Card Monte, by Al Sweigart al@inventwithpython.com')
print()
print('Find the red lady (the Queen of Hearts)! Keep an eye on how')
print('the cards move.')
print()
# Отображаем исходную раскладку карт:
cards = [('Q', HEARTS), getRandomCard(), getRandomCard()]
random.shuffle(cards) # Помещаем даму червей в случайное место.
print('Here are the cards:')
displayCards(cards)
input('Press Enter when you are ready to begin...')
# Отображаем на экране перетасовки карт:
for i in range(NUM_SWAPS):
swap = random.choice(['l-m', 'm-r', 'l-r', 'm-l', 'r-m', 'r-l'])
if swap == 'l-m':
print('swapping left and middle...')
cards[LEFT], cards[MIDDLE] = cards[MIDDLE], cards[LEFT]
elif swap == 'm-r':
print('swapping middle and right...')
cards[MIDDLE], cards[RIGHT] = cards[RIGHT], cards[MIDDLE]
elif swap == 'l-r':
print('swapping left and right...')
cards[LEFT], cards[RIGHT] = cards[RIGHT], cards[LEFT]
elif swap == 'm-l':
print('swapping middle and left...')
cards[MIDDLE], cards[LEFT] = cards[LEFT], cards[MIDDLE]
elif swap == 'r-m':
print('swapping right and middle...')
cards[RIGHT], cards[MIDDLE] = cards[MIDDLE], cards[RIGHT]
elif swap == 'r-l':
print('swapping right and left...')
cards[RIGHT], cards[LEFT] = cards[LEFT], cards[RIGHT]
time.sleep(DELAY)
# Выводим несколько символов новой строки, чтобы скрыть перетасовки.
print('\n' * 60)
# Просим пользователя найти даму червей:
while True: # Спрашиваем, пока не будет введено LEFT, MIDDLE или RIGHT.
print('Which card has the Queen of Hearts? (LEFT MIDDLE RIGHT)')

ы    
Описание работ

101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.

385

guess = input('> ').upper()
# Находим индекс в cards введенной пользователем позиции:
if guess in ['LEFT', 'MIDDLE', 'RIGHT']:
if guess == 'LEFT':
guessIndex = 0
elif guess == 'MIDDLE':
guessIndex = 1
elif guess == 'RIGHT':
guessIndex = 2
break
# (!) Раскомментируйте этот код, чтобы игрок всегда проигрывал:
#if cards[guessIndex] == ('Q', HEARTS):
#
# Игрок выиграл, так что перемещаем даму.
#
possibleNewIndexes = [0, 1, 2]
#
possibleNewIndexes.remove(guessIndex) # Убираем индекс дамы.
#
newInd = random.choice(possibleNewIndexes) # Выбираем новый индекс.
#
# Помещаем даму червей на позицию, соответствующую новому индексу:
#
cards[guessIndex], cards[newInd] = cards[newInd], cards[guessIndex]
displayCards(cards)

# Отображаем все карты.

# Проверяем, выиграл ли игрок:
if cards[guessIndex] == ('Q', HEARTS):
print('You won!')
print('Thanks for playing!')
else:
print('You lost!')
print('Thanks for playing, sucker!')

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!). Можете также
сами попробовать придумать, как сделать следующее:
воспользоваться методикой вывода символов возврата на одну позицию из
проекта 57, чтобы отобразить на краткое время каждое из сообщений о перестановке карт, после чего с помощью символа \b убрать его перед выводом
следующего;
создать игру Монте на четыре карты, чтобы усложнить задачу игрока.

386   Проект 75. Три карты Монте

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если [('Q', HEARTS), getRandomCard(), getRandomCard()] в строке 64 заменить на [('Q', HEARTS), ('Q', HEARTS), ('Q', HEARTS)]?
2. Что будет, если выражение list('23456789JQKA') в строке 49 заменить на
list('ABCDEFGHIJK')?
3. Что будет, если удалить или закомментировать time.sleep(DELAY) в строке 93?

76
Крестики-нолики

Крестики-нолики — классическая игра на бумаге, играемая
на поле 3 × 3. Игроки по очереди рисуют свои метки X
и O, пытаясь выстроить их три в ряд. Большинство игр
в крестики-нолики заканчиваются ничьей, хотя можно
и перехитрить противника, если он недостаточно внимателен.

Программа в действии
Результат выполнения tictactoe.py выглядит следующим образом:
Welcome to Tic-Tac-Toe!
| |
1 2 3
-+-+| |
4 5 6
-+-+| |
7 8 9
What is X's move? (1-9)
> 1
X| |
1 2 3
-+-+| |
4 5 6
-+-+| |
7 8 9
What is O's move? (1-9)
--сокращено--

388   Проект 76. Крестики-нолики
X|O|X 1 2 3
-+-+X|O|O 4 5 6
-+-+O|X|X 7 8 9
The game is a tie!
Thanks for playing!

Описание работы
Для представления поля крестиков-ноликов в данной программе используется
ассоциативный массив с ключами от '1' до '9' для отдельных клеток доски. Нумеруются клетки при этом точно так же, как и на клавиатуре телефона. Значениями
в этом ассоциативном массиве служат символы 'X' и 'O' для оставленных игроками
отметок и ' ' для пустой клетки.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.

"""Крестики-нолики, (c) Эл Свейгарт al@inventwithpython.com
Классическая настольная игра.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, настольная игра, игра, для двух игроков"""
ALL_SPACES = ['1', '2', '3', '4', '5', '6', '7', '8', '9']
X, O, BLANK = 'X', 'O', ' ' # Константы для строковых значений.
def main():
print('Welcome to Tic-Tac-Toe!')
gameBoard = getBlankBoard() # Создаем ассоциативный массив для доски
# крестиков-ноликов.
currentPlayer, nextPlayer = X, O # Сначала ходит X, а затем O.
while True: # Основной цикл программы.
# Отображаем доску на экране:
print(getBoardStr(gameBoard))
# Спрашиваем игрока, пока он не введет число от 1 до 9:
move = None
while not isValidSpace(gameBoard, move):
print('What is {}\'s move? (1-9)'.format(currentPlayer))
move = input('> ')
updateBoard(gameBoard, move, currentPlayer) # Делаем ход.
# Проверяем, не закончилась ли игра:
if isWinner(gameBoard, currentPlayer): # Проверяем, кто победил.
print(getBoardStr(gameBoard))
print(currentPlayer + ' has won the game!')
break
elif isBoardFull(gameBoard): # Проверяем на ничью.
print(getBoardStr(gameBoard))
print('The game is a tie!')

Описание работы  

34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.

break
# Переход хода к следующему игроку:
currentPlayer, nextPlayer = nextPlayer, currentPlayer
print('Thanks for playing!')
def getBlankBoard():
"""Создаем новую пустую доску для крестиков-ноликов."""
# Карта номеров клеток: 1|2|3
#
-+-+#
4|5|6
#
-+-+#
7|8|9
# Ключи — числа от 1 до 9, значения — X, O и BLANK:
board = {}
for space in ALL_SPACES:
board[space] = BLANK # Все клетки начинаются в пустом состоянии.
return board
def getBoardStr(board):
"""Возвращает текстовое представление доски."""
return '''
{}|{}|{} 1 2 3
-+-+{}|{}|{} 4 5 6
-+-+{}|{}|{} 7 8 9'''.format(board['1'], board['2'], board['3'],
board['4'], board['5'], board['6'],
board['7'], board['8'], board['9'])
def isValidSpace(board, space):
"""Возвращает True, если space на board представляет собой
допустимый номер клетки, причем эта клетка пуста."""
return space in ALL_SPACES and board[space] == BLANK
def isWinner(board, player):
"""Возвращает True, если игрок player победил на этой доске."""
# Для удобочитаемости используются более короткие названия переменных:
b, p = board, player
# Проверяем наличие трех отметок на одной из трех строк,
# двух диагоналей или в одном из трех столбцов.
return ((b['1'] == b['2'] == b['3'] == p) or # Верхняя строка
(b['4'] == b['5'] == b['6'] == p) or # Средняя строка
(b['7'] == b['8'] == b['9'] == p) or # Нижняя строка
(b['1'] == b['4'] == b['7'] == p) or # Левый столбец
(b['2'] == b['5'] == b['8'] == p) or # Средний столбец
(b['3'] == b['6'] == b['9'] == p) or # Нижний столбец
(b['3'] == b['5'] == b['7'] == p) or # Диагональ
(b['1'] == b['5'] == b['9'] == p))
# Диагональ

389

390   Проект 76. Крестики-нолики
84.
85. def isBoardFull(board):
86.
"""Возвращает True, если все клетки на доске заполнены."""
87.
for space in ALL_SPACES:
88.
if board[space] == BLANK:
89.
return False # Если хоть одна клетка пуста — возвращаем False.
90.
return True # Незаполненных клеток нет, возвращаем True.
91.
92.
93. def updateBoard(board, space, mark):
94.
"""Присваиваем клетке (space) на доске (board) значение (mark)."""
95.
board[space] = mark
96.
97. if __name__ == '__main__':
98.
main() # Вызываем main(), если этот модуль не импортируется,
99.
# а запускается.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если X, O, BLANK = 'X', 'O', ' ' в строке 7 заменить на X, O, BLANK =
= 'X', 'X', ' '?
2. Что будет, если board[space] = mark в строке 95 заменить на board[space] = X?
3. Что будет, если board[space] = BLANK в строке 50 заменить на board[space] = X?

77
Ханойская башня

«Ханойская башня» — головоломка, в которой дано три
стержня, на один из которых нанизаны диски различного
размера. Цель игры — перенести стопку дисков на другой
стержень. При этом можно переносить за один раз только
один диск и нельзя класть больший диск на меньший. Решение
данной головоломки требует определенного алгоритма. Сможете
ли вы додуматься до него? (Подсказка: попробуйте сначала решить более простой
вариант задачи, в котором переменная TOTAL_DISKS равна 3 или 4.)

Программа в действии
Результат выполнения towerofhanoi.py выглядит следующим образом:
The Tower of Hanoi, by Al Sweigart al@inventwithpython.com
Move the tower of disks, one disk at a time, to another tower. Larger
disks cannot rest on top of a smaller disk.
More info at https://en.wikipedia.org/wiki/Tower_of_Hanoi
||
@_1@
@@_2@@
@@@_3@@@
@@@@_4@@@@
@@@@@_5@@@@@
A

||
||
||
||
||
||
B

||
||
||
||
||
||
C

392   Проект 77. Ханойская башня
Enter the letters of "from" and "to" towers, or QUIT.
(e.g. AB to moves a disk from tower A to tower B.)
> ab
||
||
||
||
||
||
@@_2@@
||
||
@@@_3@@@
||
||
@@@@_4@@@@
||
||
@@@@@_5@@@@@
@_1@
||
A
B
C
Enter the letters of "from" and "to" towers, or QUIT.
(e.g. AB to moves a disk from tower A to tower B.)
--сокращено--

Описание работы
В качестве структуры данных для представления башни в нашей программе используется список. Первое число в списке соответствует нижнему диску, а последнее —
верхнему. Например, [5, 4, 2] соответствует следующей башне:
||
||
@@_2@@
@@@@_4@@@@
@@@@@_5@@@@@

Методы append() и pop() списков Python позволяют добавлять и удалять значения из конца списка соответственно. Подобно тому как выражения someList[0]
и someList[1] позволяют обращаться к первому и второму элементам списка,
можно использовать отрицательные индексы для обращения к значениям с конца
списка с помощью выражений вида someList[-1] и someList[-2] для последнего
и предпоследнего значений в списке соответственно, что очень удобно для выбора
диска, находящегося на верху башни.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.

"""Ханойская башня, (c) Эл Свейгарт al@inventwithpython.com
Головоломка спереносом столбиков
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, игра, головоломка"""
import copy
import sys
TOTAL_DISKS = 5

# Чем больше дисков, тем сложнее головоломка.

# В начале все диски находятся на башне A:
COMPLETE_TOWER = list(range(TOTAL_DISKS, 0, -1))

Описание работы  

14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.

393

def main():
print("""The Tower of Hanoi, by Al Sweigart al@inventwithpython.com
Move the tower of disks, one disk at a time, to another tower. Larger
disks cannot rest on top of a smaller disk.
More info at https://en.wikipedia.org/wiki/Tower_of_Hanoi
"""
)
# Задаем башни. Конец списка соответствует верху башни.
towers = {'A': copy.copy(COMPLETE_TOWER), 'B': [], 'C': []}
while True: # Выполняем один ход.
# Отображаем башни и диски:
displayTowers(towers)
# Просим пользователя сделать ход:
fromTower, toTower = askForPlayerMove(towers)
# Переносим верхний диск с fromTower на toTower:
disk = towers[fromTower].pop()
towers[toTower].append(disk)
# Проверяем, не решил ли уже головоломку пользователь:
if COMPLETE_TOWER in (towers['B'], towers['C']):
displayTowers(towers) # Отображаем башни последний раз.
print('You have solved the puzzle! Well done!')
sys.exit()
def askForPlayerMove(towers):
"""Просит игрока сделать ход. Возвращает (fromTower, toTower)."""
while True:

# Продолжаем спрашивать игрока, пока он не введет допустимый
# ход.
print('Enter the letters of "from" and "to" towers, or QUIT.')
print('(e.g. AB to moves a disk from tower A to tower B.)')
response = input('> ').upper().strip()
if response == 'QUIT':
print('Thanks for playing!')
sys.exit()
# Убеждаемся, что игрок ввел корректные буквы башен:
if response not in ('AB', 'AC', 'BA', 'BC', 'CA', 'CB'):
print('Enter one of AB, AC, BA, BC, CA, or CB.')
continue # Просим игрока ввести ход снова.
# Синтаксический сахар — более наглядные названия переменных:
fromTower, toTower = response[0], response[1]

394   Проект 77. Ханойская башня
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.

if len(towers[fromTower]) == 0:
# Исходная башня не должна быть пустой:
print('You selected a tower with no disks.')
continue # Просим игрока ввести ход снова.
elif len(towers[toTower]) == 0:
# На пустую целевую башню можно перенести любой диск:
return fromTower, toTower
elif towers[toTower][-1] < towers[fromTower][-1]:
print('Can\'t put larger disks on top of smaller ones.')
continue # Просим игрока ввести ход снова.
else:
# Ход допустим, возвращаем выбранные башни:
return fromTower, toTower
def displayTowers(towers):
"""Отображаем текущее состояние."""
# Отображаем три башни:
for level in range(TOTAL_DISKS, -1, -1):
for tower in (towers['A'], towers['B'], towers['C']):
if level >= len(tower):
displayDisk(0) # Отображаем пустой стержень без дисков.
else:
displayDisk(tower[level]) # Отображаем диск.
print()
# Отображаем метки башен A, B и C.
emptySpace = ' ' * (TOTAL_DISKS)
print('{0} A{0}{0} B{0}{0} C\n'.format(emptySpace))
def displayDisk(width):
"""Отображаем диск заданной ширины. Ширина 0 означает отсутствие диска."""
emptySpace = ' ' * (TOTAL_DISKS - width)
if width == 0:
# Отображаем сегмент стержня без диска:
print(emptySpace + '||' + emptySpace, end='')
else:
# Отображаем диск:
disk = '@' * width
numLabel = str(width).rjust(2, '_')
print(emptySpace + disk + numLabel + disk + emptySpace, end='')
# Если программа не импортируется, а запускается, производим запуск игры:
if __name__ == '__main__':
main()

Исследование программы  

395

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если удалить или закомментировать строки 73, 74 и 75?
2. Что будет, если emptySpace = ' ' * (TOTAL_DISKS - width) в строке 100 заменить
на emptySpace = ' '?
3. Что будет, если width == 0 в строке 102 заменить на width != 0?

78
Вопросы с подвохом

Каким станет желтый камень, если его бросить в голубой пруд? Существует ли праздник 4 Июля в Англии?
Как может доктор обходиться 30 дней без сна? Если вы
думаете, что знаете ответы на эти вопросы, то, вероятно,
ошибаетесь: 54 вопроса в этой программе были специально
составлены так, чтобы ответы на них были просты, очевидны
и неправильны. Поиск настоящих ответов требует определенных умственных способностей.
Не копируйте код из этой книги, чтобы не портить все удовольствие, ведь тогда
вы сразу увидите ответы. Лучше скачайте эту игру с https://inventwithpython.com/
trickquestions.py и запустите ее, не глядя на исходный код.

Программа в действии
Результат выполнения trickquestions.py выглядит следующим образом:
Trick Questions, by Al Sweigart al@inventwithpython.com
Can you figure out the answers to these trick questions?
(Enter QUIT to quit at any time.)
Press Enter to begin...
--сокращено-Question: 1

Описание работы  

397

Score: 0 / 54
QUESTION: A 39 year old person was born on the 22nd of February. What year is
their birthday?
ANSWER: 1981
Incorrect! The answer is: Their birthday is on February 22nd of every year.
Press Enter for the next question...
--сокращено-Question: 2
Score: 0 / 54
QUESTION: If there are ten apples and you take away two, how many do you have?
ANSWER: Eight
Incorrect! The answer is: Two.
Press Enter for the next question...
--сокращено--

Описание работы
В переменной QUESTIONS содержится список ассоциативных массивов. Каждый
массив соответствует одному вопросу с подвохом и содержит ключи 'question',
'answer' и 'accept'. Значения для ключей 'question' и 'answer' представляют
собой строковые значения, соответственно отображаемые программой в момент,
когда задается вопрос игроку или показывается ответ. Значение для ключа 'accept'
представляет собой список строковых значений. Любое введенное игроком значение
из этого списка считается правильным, что позволяет ему вводить в качестве ответа
текст в свободной форме. Программа достаточно точно определяет, правильный
ли ответ получила.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.

"""Вопросы с подвохом, (c) Эл Свейгарт al@inventwithpython.com
Викторина из нескольких вопросов с подвохом.
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, юмор"""
import random, sys

# QUESTIONS представляет собой список ассоциативных массивов, каждый
# из которых содержит вопрос с подвохом и ответ на него. В таком
# ассоциативном массиве есть ключи 'question' (с текстом вопроса),
# 'answer' (с текстом ответа) и 'accept' (со списком строковых
# значений, которые программа считает правильными ответами на вопрос).
# (!) Придумайте собственные вопросы с подвохом и добавьте сюда:
QUESTIONS = [
{'question': "How many times can you take 2 apples from a pile of 10 apples?",
'answer': "Once. Then you have a pile of 8 apples.",
'accept': ['once', 'one', '1']},
{'question': 'What begins with "e" and ends with "e" but only has one
➥ letter in it?',
19.
'answer': "An envelope.",
20.
'accept': ['envelope']},

398   Проект 78. Вопросы с подвохом
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.

{'question': "Is it possible to draw a square with three sides?",
'answer': "Yes. All squares have three sides. They also have a fourth side.",
'accept': ['yes']},
{'question': "How many times can a piece of paper be folded in half by hand
➥ without unfolding?",
'answer': "Once. Then you are folding it in quarters.",
'accept': ['one', '1', 'once']},
{'question': "What does a towel get as it dries?",
'answer': "Wet.",
'accept': ['wet']},
{'question': "What does a towel get as it dries?",
'answer': "Drier.",
'accept': ['drier', 'dry']},
{'question': "Imagine you are in a haunted house full of evil ghosts. What do
➥ you have to do to stay safe?",
'answer': "Nothing. You're only imagining it.",
'accept': ['nothing', 'stop']},
{'question': "A taxi driver is going the wrong way down a one-way street. She
➥ passes ten cops but doesn't get a ticket. Why not?",
'answer': "She was walking.",
'accept': ['walk']},
{'question': "What does a yellow stone thrown into a blue pond become?",
'answer': "Wet.",
'accept': ['wet']},
{'question': "How many miles does must a cyclist bike to get to training?",
'answer': "None. They're training as soon as they get on the bike.",
'accept': ['none', 'zero', '0']},
{'question': "What building do people want to leave as soon as they enter?",
'answer': "An airport.",
'accept': ['airport', 'bus', 'port', 'train', 'station', 'stop']},
{'question': "If you're in the middle of a square house facing the west side
➥ with the south side to your left and the north side to your right, which
➥ side of the house are you next to?",
'answer': "None. You're in the middle.",
'accept': ['none', 'middle', 'not', 'any']},
{'question': "How much dirt is in a hole 3 meters wide, 3 meters long,
➥ and 3 meters deep?",
'answer': "There is no dirt in a hole.",
'accept': ['no', 'none', 'zero']},
{'question': "A girl mails a letter from America to Japan. How many miles did
➥ the stamp move?",
'answer': "Zero. The stamp was in the same place on the envelope the
➥ whole time.",
'accept': ['zero', '0', 'none', 'no']},
{'question': "What was the highest mountain on Earth the day before Mount
➥ Everest was discovered?",
'answer': "Mount Everest was still the highest mountain of Earth the day
➥ before it was discovered.",
'accept': ['everest']},
{'question': "How many fingers do most people have on their two hands?",
'answer': "Eight. They also have two thumbs.",

Описание работы  

62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.

399

'accept': ['eight', '8']},
{'question': "The 4th of July is a holiday in America. Do they have
➥ a 4th of July in England?",
'answer': "Yes. All countries have a 4th of July on their calendar.",
'accept': ['yes']},
{'question': "Which letter of the alphabet makes honey?",
'answer': "None. A bee is an insect, not a letter.",
'accept': ['no', 'none', 'not']},
{'question': "How can a doctor go 30 days without sleep?",
'answer': "By sleeping at night.",
'accept': ['night', 'evening']},
{'question': "How many months have 28 days?",
'answer': "12. All months have 28 days. Some have more days as well.",
'accept': ['12', 'twelve', 'all']},
{'question': "How many two cent stamps are in a dozen?",
'answer': "A dozen.",
'accept': ['12', 'twelve', 'dozen']},
{'question': "Why is it illegal for a person living in North Dakota to be
➥ buried in South Dakota?",
'answer': "Because it is illegal to bury someone alive.",
'accept': ['alive', 'living', 'live']},
{'question': "How many heads does a two-headed coin have?",
'answer': "Zero. Coins are just circular pieces of metal. They don't have
➥ heads.",
'accept': ['zero', 'none', 'no', '0']},
{'question': "What kind of vehicle has four wheels and flies?",
'answer': "A garbage truck.",
'accept': ['garbage', 'dump', 'trash']},
{'question': "What kind of vehicle has four wheels and flies?",
'answer': "An airplane.",
'accept': ['airplane', 'plane']},
{'question': "What five-letter word becomes shorter by adding two letters?",
'answer': "Short.",
'accept': ['short']},
{'question': "Gwen's mother has five daughters. Four are named Haha, Hehe,
➥ Hihi, and Hoho. What's the fifth daughter's name?",
'answer': "Gwen.",
'accept': ['gwen']},
{'question': "How long is a fence if there are three fence posts each one
➥ meter apart?",
'answer': "Two meters long.",
'accept': ['2', 'two']},
{'question': "How many legs does a dog have if you count its tail as a leg?",
'answer': "Four. Calling a tail a leg doesn't make it one.",
'accept': ['four', '4']},
{'question': "How much more are 1976 pennies worth compared to 1975 pennies?",
'answer': "One cent.",
'accept': ['1', 'one']},
{'question': "What two things can you never eat for breakfast?",
'answer': "Lunch and dinner.",
'accept': ['lunch', 'dinner', 'supper']},

400   Проект 78. Вопросы с подвохом
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.

{'question': "How many birthdays does the average person have?",
'answer': "One. You're only born once.",
'accept': ['one', '1', 'once' 'born']},
{'question': "Where was the United States Declaration of Independence
➥ signed?",
'answer': "It was signed at the bottom.",
'accept': ['bottom']},
{'question': "A person puts two walnuts in their pocket but only has one
thing in their pocket five minutes later. What is it?",
'answer': "A hole.",
'accept': ['hole']},
{'question': "What did the sculptor make that no one could see?",
'answer': "Noise.",
'accept': ['noise']},
{'question': "If you drop a raw egg on a concrete floor, will it crack?",
'answer': "No. Concrete is very hard to crack.",
'accept': ['no']},
{'question': "If it takes ten people ten hours to build a fence, how many
➥ hours does it take five people to build it?",
'answer': "Zero. It's already built.",
'accept': ['zero', 'no', '0', 'already', 'built']},
{'question': "Which is heavier, 100 pounds of rocks or 100 pounds of
➥ feathers?",
'answer': "Neither. They weigh the same.",
'accept': ['neither', 'none', 'no', 'same', 'even', 'balance']},
{'question': "What do you have to do to survive being bitten by a poisonous
➥ snake?",
'answer': "Nothing. Only venomous snakes are deadly.",
'accept': ['nothing', 'anything']},
{'question': "What three consecutive days don't include Sunday, Wednesday, or
Friday?",
'answer': "Yesterday, today, and tomorrow.",
'accept': ['yesterday', 'today', 'tomorrow']},
{'question': "If there are ten apples and you take away two, how many do you
➥ have?",
'answer': "Two.",
'accept': ['2', 'two']},
{'question': "A 39 year old person was born on the 22nd of February. What
➥ year is their birthday?",
'answer': "Their birthday is on February 22nd of every year.",
'accept': ['every', 'each']},
{'question': "How far can you walk in the woods?",
'answer': "Halfway. Then you are walking out of the woods.",
'accept': ['half', '1/2']},
{'question': "Can a man marry his widow's sister?",
'answer': "No, because he's dead.",
'accept': ['no']},
{'question': "What do you get if you divide one hundred by half?",
'answer': "One hundred divided by half is two hundred. One hundred divided
➥ by two is fifty.",
'accept': ['two', '200']},

Описание работы  

150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.

401

{'question': "What do you call someone who always knows where their
➥ spouse is?",
'answer': "A widow or widower.",
'accept': ['widow', 'widower']},
{'question': "How can someone take a photo but not be a photographer?",
'answer': "They can be a thief.",
'accept': ['thief', 'steal', 'take', 'literal']},
{'question': "An electric train leaves the windy city of Chicago at 4pm on
➥ a Monday heading south at 100 kilometers per hour. Which way does the smoke
➥ blow from the smokestack?",
'answer': "Electric trains don't have smokestacks.",
'accept': ["don't", "doesn't", 'not', 'no', 'none']},
{'question': 'What is the only word that rhymes with "orange"?',
'answer': "Orange.",
'accept': ['orange']},
{'question': "Who is the U.S. President if the U.S. Vice President dies?",
'answer': "The current U.S. President.",
'accept': ['president', 'current', 'already']},
{'question': "A doctor gives you three pills with instructions to take one
➥ every half-hour. How long will the pills last?",
'answer': "One hour.",
'accept': ['1', 'one']},
{'question': "Where is there an ocean with no water?",
'answer': "On a map.",
'accept': ['map']},
{'question': "What is the size of a rhino but weighs nothing?",
'answer': "A rhino's shadow.",
'accept': ['shadow']},
{'question': "The clerk at a butcher shop is exactly 177 centimeters tall.
What do they weigh?",
'answer': "The clerk weighs meat.",
'accept': ['meat']}]
CORRECT_TEXT = ['Correct!', 'That is right.', "You're right.",
'You got it.', 'Righto!']
INCORRECT_TEXT = ['Incorrect!', "Nope, that isn't it.", 'Nope.',
'Not quite.', 'You missed it.']
print('''Trick Questions, by Al Sweigart al@inventwithpython.com
Can you figure out the answers to these trick questions?
(Enter QUIT to quit at any time.)
''')
input('Press Enter to begin...')
random.shuffle(QUESTIONS)
score = 0
for questionNumber, qa in enumerate(QUESTIONS):
print('\n' * 40) # Очищаем экран.

# Основной цикл программы.

402   Проект 78. Вопросы с подвохом
196.
print('Question:', questionNumber + 1)
197.
print('Score:', score, '/', len(QUESTIONS))
198.
print('QUESTION:', qa['question'])
199.
response = input(' ANSWER: ').lower()
200.
201.
if response == 'quit':
202.
print('Thanks for playing!')
203.
sys.exit()
204.
205.
correct = False
206.
for acceptanceWord in qa['accept']:
207.
if acceptanceWord in response:
208.
correct = True
209.
210.
if correct:
211.
text = random.choice(CORRECT_TEXT)
212.
print(text, qa['answer'])
213.
score += 1
214.
else:
215.
text = random.choice(INCORRECT_TEXT)
216.
print(text, 'The answer is:', qa['answer'])
217.
response = input('Press Enter for the next question...').lower()
218.
219.
if response == 'quit':
220.
print('Thanks for playing!')
221.
sys.exit()
222.
223. print("That's all the questions. Thanks for playing!")

Когда вы введете исходный код и запустите его несколько раз, попробуйте поэкспериментировать с внесением в него изменений. Идеи касательно возможных
небольших изменений вы найдете в комментариях, помеченных (!).

Исследование программы
Это очень простая программа, так что вариантов ее модификации не так уж много.
Вместо этого подумайте, где еще может пригодиться программа вида «вопрос —
ответ».

79

Игра «2048»

Веб-разработчик Габриэле Чирулли (Gabriele Cirulli)
создал игру «2048» за одни выходные. В ее основе лежит
игра «1024» от Veewo Studios, созданная, в свою очередь,
под впечатлением от игры Threes! от команды разработчиков Sirvo. В игре «2048» нужно объединять числа на доске
4 × 4, таким образом убирая их с экрана. Две двойки объединяются в четверку, две четверки объединяются в восьмерку и т. д.
При каждом объединении на игровую доску добавляется новая двойка. Цель: достичь 2048, прежде чем вся доска заполнится.

404   Проект 79. Игра «2048»

Программа в действии
Результат выполнения twentyfortyeight.py выглядит следующим образом:
Twenty Forty-Eight, by Al Sweigart al@inventwithpython.com
--сокращено-+-----+-----+-----+-----+
|
|
|
|
|
|
|
| 2 | 16 |
|
|
|
|
|
+-----+-----+-----+-----+
|
|
|
|
|
|
| 16 | 4 | 2 |
|
|
|
|
|
+-----+-----+-----+-----+
|
|
|
|
|
| 2 |
| 4 | 32 |
|
|
|
|
|
+-----+-----+-----+-----+
|
|
|
|
|
|
|
|
| 2 |
|
|
|
|
|
+-----+-----+-----+-----+
Score: 80
Enter move: (WASD or Q to quit)
--сокращено--

Описание работы
Поведение этой программы реализуется с помощью структур данных для столбцов,
представленных списками из четырех строковых значений: BLANK (строковое значение, состоящее из одного пробела), '2', '4', '8' и т. д. Первое значение в данном
списке соответствует низу столбца, а последнее — верху. Объединяющиеся в столбце
числа всегда сдвигаются вниз, а игрок может сдвигать «плитки» вверх, вниз, влево
или вправо. Можете представить себе, что сила тяжести тянет «плитки» в этих направлениях. Например, на рис. 79.1 показана доска с «плитками», сдвигающимися
вправо. Создаем четыре списка для столбцов:
['2', '4', '8', ' ']
[' ', ' ', ' ', '4']
[' ', ' ', ' ', '2']
[' ', ' ', ' ', ' ']

Функция combineTilesInColumn() получает на входе один список для столбца и возвращает другой, в котором все совпадающие числа объединены и сдвинуты вниз.

Описание работы  

405

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

Рис. 79.1. Столбцы (выделены) в случае сдвига вправо
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.

"""Игра «2048», (c) Эл Свейгарт al@inventwithpython.com
Игра, в которой при сдвиге "плиток" объединяются числа, растущие
в геометрической прогрессии. На основе игры "2048" Габриэле Чирулли —
клона игры "1024" от Veewo Studios, которая является клоном игры Threes!
Подробнее — в статье https://ru.wikipedia.org/wiki/2048_(игра)
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: большая, игра, головоломка"""
import random, sys
# Задаем константы:
BLANK = '' # Значение, соответствующее пустой клетке на доске.
def main():
print('''Twenty Forty-Eight, by Al Sweigart al@inventwithpython.com
Slide all the tiles on the board in one of four directions. Tiles with
like numbers will combine into larger-numbered tiles. A new 2 tile is
added to the board on each move. You win if you can create a 2048 tile.
You lose if the board fills up the tiles before then.''')
input('Press Enter to begin...')

406   Проект 79. Игра «2048»
24.
gameBoard = getNewBoard()
25.
26.
while True: # Основной цикл программы.
27.
drawBoard(gameBoard)
28.
print('Score:', getScore(gameBoard))
29.
playerMove = askForPlayerMove()
30.
gameBoard = makeMove(gameBoard, playerMove)
31.
addTwoToBoard(gameBoard)
32.
33.
if isFull(gameBoard):
34.
drawBoard(gameBoard)
35.
print('Game Over - Thanks for playing!')
36.
sys.exit()
37.
38.
39. def getNewBoard():
40.
"""Возвращает новую структуру данных для доски,
41.
которая представляет собой ассоциативный массив, ключи которого —
42.
кортежи (x, y), а значения находятся в соответствующих клетках.
43.
Эти значения равны либо числам — степеням двойки, либо BLANK.
44.
Система координат выглядит вот так:
45.
X0 1 2 3
46.
Y+-+-+-+-+
47.
0| | | | |
48.
+-+-+-+-+
49.
1| | | | |
50.
+-+-+-+-+
51.
2| | | | |
52.
+-+-+-+-+
53.
3| | | | |
54.
+-+-+-+-+"""
55.
56.
newBoard = {} # Содержит возвращаемую структуру данных доски.
57.
# Проходим по всем клеткам доски, устанавливая "плиткам" значение BLANK:
58.
for x in range(4):
59.
for y in range(4):
60.
newBoard[(x, y)] = BLANK
61.
62.
# Выбираем две случайные клетки для двух начальных двоек:
63.
startingTwosPlaced = 0 # Число выбранных изначально клеток.
64.
while startingTwosPlaced < 2: # Повторяем, чтобы продублировать клетки.
65.
randomSpace = (random.randint(0, 3), random.randint(0, 3))
66.
# Проверяем, что случайно выбранная клетка еще не занята:
67.
if newBoard[randomSpace] == BLANK:
68.
newBoard[randomSpace] = 2
69.
startingTwosPlaced = startingTwosPlaced + 1
70.
71.
return newBoard
72.
73.
74. def drawBoard(board):

Описание работы  

75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.

407

"""Отрисовываем структуру данных для доски на экране."""
# Проходим по всем возможным клеткам слева направо, сверху вниз
# и создаем список меток всех клеток.
labels = [] # Список строковых значений для числа/BLANK данной "плитки".
for y in range(4):
for x in range(4):
tile = board[(x, y)] # Получаем значение "плитки" в этой клетке.
# Убеждаемся, что длина метки равна 5:
labelForThisTile = str(tile).center(5)
labels.append(labelForThisTile)
# {} заменяются метками соответствующих "плиток":
print("""
+-----+-----+-----+-----+
|
|
|
|
|
|{}|{}|{}|{}|
|
|
|
|
|
+-----+-----+-----+-----+
|
|
|
|
|
|{}|{}|{}|{}|
|
|
|
|
|
+-----+-----+-----+-----+
|
|
|
|
|
|{}|{}|{}|{}|
|
|
|
|
|
+-----+-----+-----+-----+
|
|
|
|
|
|{}|{}|{}|{}|
|
|
|
|
|
+-----+-----+-----+-----+
""".format(*labels))
def getScore(board):
"""Возвращает сумму всех "плиток" в структуре данных для доски."""
score = 0
# Проходим в цикле по всем клеткам, прибавляя "плитки" к score:
for x in range(4):
for y in range(4):
# Прибавляем к score только непустые "плитки":
if board[(x, y)] != BLANK:
score = score + board[(x, y)]
return score
def combineTilesInColumn(column):
"""column представляет собой список из четырех "плиток". Индекс 0
соответствует низу столбца column, а сила тяжести тянет "плитки" вниз,
в случае равных значений они объединяются. Например, combineTilesInCo­
lumn([2, BLANK, 2, BLANK]) возвращает [4, BLANK, BLANK, BLANK]."""

408   Проект 79. Игра «2048»
126.
127.
# Копируем в combinedTiles только числа (не BLANK) из column
128.
combinedTiles = [] # Список непустых "плиток" в column.
129.
for i in range(4):
130.
if column[i] != BLANK:
131.
combinedTiles.append(column[i])
132.
# Продолжаем присоединять к списку значения BLANK, пока не получится
133.
# четыре "плитки":
134.
while len(combinedTiles) < 4:
135.
combinedTiles.append(BLANK)
136.
137.
# Объединяем числа, если число сверху — такое же, и удваиваем.
138.
for i in range(3): # Пропускаем индекс 3: это верхняя клетка.
139.
if combinedTiles[i] == combinedTiles[i + 1]:
140.
combinedTiles[i] *= 2 # Удваиваем число на "плитке".
141.
# Сдвигаем расположенные вверху "плитки" на одну клетку:
142.
for aboveIndex in range(i + 1, 3):
143.
combinedTiles[aboveIndex] = combinedTiles[aboveIndex + 1]
144.
combinedTiles[3] = BLANK # Самая верхняя клетка — всегда пустая.
145.
return combinedTiles
146.
147.
148. def makeMove(board, move):
149.
"""Производит ход на доске.
150.
151.
Аргумент move — 'W', 'A', 'S' или 'D'. Функция возвращает
152.
получившуюся структуру данных для доски (board)."""
153.
154.
# board разбивается на четыре столбца, различные
155.
# в зависимости от направления хода:
156.
if move == 'W':
157.
allColumnsSpaces = [[(0, 0), (0, 1), (0, 2), (0, 3)],
158.
[(1, 0), (1, 1), (1, 2), (1, 3)],
159.
[(2, 0), (2, 1), (2, 2), (2, 3)],
160.
[(3, 0), (3, 1), (3, 2), (3, 3)]]
161.
elif move == 'A':
162.
allColumnsSpaces = [[(0, 0), (1, 0), (2, 0), (3, 0)],
163.
[(0, 1), (1, 1), (2, 1), (3, 1)],
164.
[(0, 2), (1, 2), (2, 2), (3, 2)],
165.
[(0, 3), (1, 3), (2, 3), (3, 3)]]
166.
elif move == 'S':
167.
allColumnsSpaces = [[(0, 3), (0, 2), (0, 1), (0, 0)],
168.
[(1, 3), (1, 2), (1, 1), (1, 0)],
169.
[(2, 3), (2, 2), (2, 1), (2, 0)],
170.
[(3, 3), (3, 2), (3, 1), (3, 0)]]
171.
elif move == 'D':
172.
allColumnsSpaces = [[(3, 0), (2, 0), (1, 0), (0, 0)],
173.
[(3, 1), (2, 1), (1, 1), (0, 1)],
174.
[(3, 2), (2, 2), (1, 2), (0, 2)],
175.
[(3, 3), (2, 3), (1, 3), (0, 3)]]
176.
# Структура данных board после выполнения хода:

Описание работы  

177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.

boardAfterMove = {}
for columnSpaces in allColumnsSpaces:

# Проходим в цикле по всем
# четырем столбцам.
# Получаем "плитки" для этого столбца (первая "плитка"
# соответствует "низу" столбца):
firstTileSpace = columnSpaces[0]
secondTileSpace = columnSpaces[1]
thirdTileSpace = columnSpaces[2]
fourthTileSpace = columnSpaces[3]
firstTile = board[firstTileSpace]
secondTile = board[secondTileSpace]
thirdTile = board[thirdTileSpace]
fourthTile = board[fourthTileSpace]
# Формируем столбец и объединяем "плитки" в нем:
column = [firstTile, secondTile, thirdTile, fourthTile]
combinedTilesColumn = combineTilesInColumn(column)
# Формируем новую структуру данных для доски
# с объединенными "плитками":
boardAfterMove[firstTileSpace] = combinedTilesColumn[0]
boardAfterMove[secondTileSpace] = combinedTilesColumn[1]
boardAfterMove[thirdTileSpace] = combinedTilesColumn[2]
boardAfterMove[fourthTileSpace] = combinedTilesColumn[3]

197.
198.
199.
200.
201.
202.
return boardAfterMove
203.
204.
205. def askForPlayerMove():
206.
"""Просим игрока указать направление следующего хода (или выйти).
207.
208.
Проверяем ход на допустимость: 'W', 'A', 'S' или 'D'."""
209.
print('Enter move: (WASD or Q to quit)')
210.
while True: # Выполняем цикл, пока не будет введен допустимый ход.
211.
move = input('> ').upper()
212.
if move == 'Q':
213.
# Завершаем программу:
214.
print('Thanks for playing!')
215.
sys.exit()
216.
217.
# Возвращаем допустимый ход или выполняем еще итерацию
# и спрашиваем снова:
218.
if move in ('W', 'A', 'S', 'D'):
219.
return move
220.
else:
221.
print('Enter one of "W", "A", "S", "D", or "Q".')
222.
223.
224. def addTwoToBoard(board):
225.
"""Добавляет на доску две случайные новые "плитки"."""

409

410   Проект 79. Игра «2048»
226.
while True:
227.
randomSpace = (random.randint(0, 3), random.randint(0, 3))
228.
if board[randomSpace] == BLANK:
229.
board[randomSpace] = 2
230.
return # Выполняем возврат из функции после обнаружения
231.
# непустой "плитки".
232.
233. def isFull(board):
234.
"""Возвращает True, если в структуре данных для доски нет пустых клеток."""
235.
# Проходим в цикле по всем клеткам доски:
236.
for x in range(4):
237.
for y in range(4):
238.
# Если клетка пуста, возвращаем False:
239.
if board[(x, y)] == BLANK:
240.
return False
241.
return True # Пустых клеток нет, возвращаем True.
242.
243.
244. # Если программа не импортируется, а запускается, выполняем запуск игры:
245. if __name__ == '__main__':
246.
try:
247.
main()
248.
except KeyboardInterrupt:
249.
sys.exit() # Если нажато Ctrl+C — завершаем программу.

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если return score в строке 118 заменить на return 9999?
2. Что будет, если выражение board[randomSpace] = 2 в строке 229 заменить
на board[randomSpace] = 256?

80
Шифр Виженера

Шифр Виженера, ошибочно приписываемый криптографу XIX века Блезу де Виженеру (Blaise de Vigenère)
(другие исследователи независимо от Виженера открыли
его раньше), в течение нескольких столетий не поддавался
взлому. По существу, он представляет собой шифр Цезаря,
только с состоящим из нескольких частей ключом. Так называемый ключ Виженера представляет собой слово или даже случайную последовательность букв. Каждой букве соответствует число, показывающее,
на сколько необходимо сдвинуть эту букву в сообщении: A соответствует сдвигу
буквы в сообщении на 0 букв, B — на 1, C — на 2 и т. д.
Например, если ключ Виженера — слово CAT, C означает сдвиг на 2 буквы, A — на 0,
а T — на 19. Первая буква сообщения сдвигается на 2 буквы, вторая не сдвигается,
а третья — сдвигается на 19. Для четвертой буквы просто используется ключ для
первой, равный 2.
Именно использование нескольких ключей шифра Цезаря обеспечивает криптостойкость шифра Виженера. Возможное количество комбинаций слишком велико
для прямого перебора. В то же время шифр Виженера устойчив к частотному
анализу — слабой стороне шифра простой подстановки. Многие столетия шифр
Виженера был последним словом в криптографии.
Вы увидите, что программы для шифра Виженера и шифра Цезаря во многом похожи. Подробнее о шифре Виженера можно прочитать в статье «Википедии»: https://

412   Проект 80. Шифр Виженера
ru.wikipedia.org/wiki/Шифр_Виженера. Если вам хотелось бы узнать больше о шифрах

и методах их взлома — можете прочитать мою книгу Cracking Codes with Python.

Программа в действии
Результат выполнения vigenere.py выглядит следующим образом:
Vigenère Cipher, by Al Sweigart al@inventwithpython.com
The Vigenère cipher is a polyalphabetic substitution cipher that was
powerful enough to remain unbroken for centuries.
Do you want to (e)ncrypt or (d)ecrypt?
> e
Please specify the key to use.
It can be a word or any combination of letters:
> PIZZA
Enter the message to encrypt.
> Meet me by the rose bushes tonight.
Encrypted message:
Bmds mt jx sht znre qcrgeh bnmivps.
Full encrypted text copied to clipboard.

Описание работы
Поскольку процессы шифрования и расшифровки очень похожи, и за тот и за
другой отвечает функция translateMessage() . Функции encryptMessage()
и decryptMessage() — всего лишь адаптеры для translateMessage(). Другими
словами, это функции, адаптирующие свои аргументы, передающие их в другую
функцию, а затем возвращающие то, что вернула эта другая функция. Эти функции-адаптеры вызываются в нашей программе примерно так, как вызывались
encryptMessage() и decryptMessage() в проекте 66. Можете импортировать эти
проекты в качестве модулей в других программах и использовать их код шифрования/расшифровки, не прибегая к копированию/вставке кода непосредственно
в новую программу.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.

"""Шифр Виженера, (c) Эл Свейгарт al@inventwithpython.com
Шифр Виженера — многоалфавитный шифр подстановки, настолько
эффективный, что его не могли взломать многие столетия.
Подробнее — в статье https://ru.wikipedia.org/wiki/Шифр_Виженера
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: короткая, криптография, математическая"""
try:

import pyperclip # pyperclip копирует текст в буфер обмена.
except ImportError:
pass # Если pyperclip не установлена, ничего не делаем. Не проблема.

Описание работы  

12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.

413

# Все возможные символы для шифрования/дешифровки:
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def main():
print('''Vigenère Cipher, by Al Sweigart al@inventwithpython.com
The Vigenère cipher is a polyalphabetic substitution cipher that was
powerful enough to remain unbroken for centuries.''')
# Спрашиваем у пользователя, хочет он шифровать или расшифровывать:
while True: # Повторяем вопрос, пока пользователь не введет e или d.
print('Do you want to (e)ncrypt or (d)ecrypt?')
response = input('> ').lower()
if response.startswith('e'):
myMode = 'encrypt'
break
elif response.startswith('d'):
myMode = 'decrypt'
break
print('Please enter the letter e or d.')
# Просим пользователя ввести ключ шифрования:
while True: # Повторяем вопрос, пока пользователь не введет допустимый ключ
print('Please specify the key to use.')
print('It can be a word or any combination of letters:')
response = input('> ').upper()
if response.isalpha():
myKey = response
break
# Просим пользователя ввести сообщение для шифрования/расшифровки:
print('Enter the message to {}.'.format(myMode))
myMessage = input('> ')
# Производим шифрование/расшифровку:
if myMode == 'encrypt':
translated = encryptMessage(myMessage, myKey)
elif myMode == 'decrypt':
translated = decryptMessage(myMessage, myKey)
print('%sed message:' % (myMode.title()))
print(translated)
try:

pyperclip.copy(translated)
print('Full %sed text copied to clipboard.' % (myMode))
except:
pass # Если pyperclip не установлена, ничего не делаем.

414   Проект 80. Шифр Виженера
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.

def encryptMessage(message, key):
"""Шифрует сообщение message в соответствии с ключом key."""
return translateMessage(message, key, 'encrypt')
def decryptMessage(message, key):
"""Расшифровывает сообщение message в соответствии с ключом key."""
return translateMessage(message, key, 'decrypt')
def translateMessage(message, key, mode):
"""Зашифровывает или расшифровывает сообщение в соответствии с ключом."""
translated = [] # Для хранения строкового значения
# зашифрованного/расшифрованного сообщения.
keyIndex = 0
key = key.upper()
for symbol in message: # Проходим в цикле по всем символам сообщения.
num = LETTERS.find(symbol.upper())
if num != -1: # -1 означает, что symbol.upper() не входит в LETTERS.
if mode == 'encrypt':
# Прибавляем при шифровании:
num += LETTERS.find(key[keyIndex])
elif mode == 'decrypt':
# Вычитаем при расшифровке:
num -= LETTERS.find(key[keyIndex])
num %= len(LETTERS)

# Учитываем возможный переход по кругу.

# Добавляем зашифрованный/расшифрованный символ в translated.
if symbol.isupper():
translated.append(LETTERS[num])
elif symbol.islower():
translated.append(LETTERS[num].lower())
keyIndex += 1 # Переходим к следующей букве ключа.
if keyIndex == len(key):
keyIndex = 0
else:
# Просто добавляем символ без шифрования/расшифровки:
translated.append(symbol)
return ''.join(translated)
# Если программа не импортируется, а запускается, производим запуск:
if __name__ == '__main__':
main()

Исследование программы  

415

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что происходит при шифровании с ключом 'A'?
2. Какую ошибку вызовет удаление или комментирование myKey = response
в строке 40?

81
Головоломка с ведрами воды

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

Описание работы  

417

Программа в действии
Результат выполнения waterbucket.py выглядит следующим образом:
Water Bucket Puzzle, by Al Sweigart al@inventwithpython.com
Try to get 4L of water into one of these
buckets:
8|
|
7|
|
6|
|
5|
|
4|
|
3|
|
2|
|
1|
|
+------+
8L

5|
|
4|
|
3|
|
2|
|
1|
|
+------+
5L

3|
|
2|
|
1|
|
+------+
3L

You can:
(F)ill the bucket
(E)mpty the bucket
(P)our one bucket into another
(Q)uit
> f
Select a bucket 8, 5, 3, or QUIT:
> 5
Try to get 4L of water into one of these
buckets:
8|
|
7|
|
6|
|
5|
| 5|WWWWWW|
4|
| 4|WWWWWW|
3|
| 3|WWWWWW|
2|
| 2|WWWWWW|
1|
| 1|WWWWWW|
+------+
+------+
8L
5L
--сокращено--

3|
|
2|
|
1|
|
+------+
3L

Описание работы
В переменной waterInBucket содержится ассоциативный массив, описывающий
состояние ведер воды. Ключами этого ассоциативного массива служат строковые

418   Проект 81. Головоломка с ведрами воды
значения '8', '5' и '3', соответствующие ведрам, а значения — целочисленные
(и описывают количество литров воды в соответствующем ведре).
В строках с 48-й по 59-ю этот ассоциативный массив используется для визуализации
ведер и воды на экране. Содержащий значения либо 'WWWWWW' (представляющие
воду), либо ' ' (представляющие воздух) список waterDisplay передается в строковый метод format(). Первые восемь строковых значений в списке waterDisplay
служат для заполнения восьмилитрового ведра, следующие пять — пятилитрового,
а последние три — трехлитрового.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.

"""Головоломка с ведрами воды, (c) Эл Свейгарт al@inventwithpython.com
Головоломка с переливанием воды.
Подробнее — в статье https://en.wikipedia.org/wiki/Water_pouring_puzzle
Код размещен на https://nostarch.com/big-book-small-python-projects
Теги: """
import sys
print('Water Bucket Puzzle, by Al Sweigart al@inventwithpython.com')
GOAL = 4 # Точный объем воды, необходимый для победы.
steps = 0 # Содержит количество ходов, выполненных игроком для решения
# головоломки.
# Объем воды в каждом из ведер:
waterInBucket = {'8': 0, '5': 0, '3': 0}
while True: # Основной цикл игры.
# Отображаем текущее состояние ведер:
print()
print('Try to get ' + str(GOAL) + 'L of water into one of these')
print('buckets:')
waterDisplay = []

# Содержит строковые значения для воды и пустого места.

# Получаем строковые значения для восьмилитрового ведра:
for i in range(1, 9):
if waterInBucket['8'] < i:
waterDisplay.append('
') # Добавляем пустое место.
else:
waterDisplay.append('WWWWWW') # Добавляем воду.
# Получаем строковые значения для пятилитрового ведра:
for i in range(1, 6):
if waterInBucket['5'] < i:
waterDisplay.append('
') # Добавляем пустое место.
else:
waterDisplay.append('WWWWWW') # Добавляем воду.
# Получаем строковые значения для трехлитрового ведра:

Описание работы  

41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.

for i in range(1, 4):
if waterInBucket['3'] < i:
waterDisplay.append('
')
else:
waterDisplay.append('WWWWWW')

419

# Добавляем пустое место.
# Добавляем воду.

# Отображаем на экране ведра, каждое со своим объемом воды:
print('''
8|{7}|
7|{6}|
6|{5}|
5|{4}| 5|{12}|
4|{3}| 4|{11}|
3|{2}| 3|{10}| 3|{15}|
2|{1}| 2|{9}| 2|{14}|
1|{0}| 1|{8}| 1|{13}|
+------+
+------+
+------+
8L
5L
3L
'''.format(*waterDisplay))
# Проверяем, не содержится ли в каком-то из ведер нужное количество воды:
for waterAmount in waterInBucket.values():
if waterAmount == GOAL:
print('Good job! You solved it in', steps, 'steps!')
sys.exit()
# Спрашиваем у игрока, какое действие он хочет произвести с ведром:
print('You can:')
print(' (F)ill the bucket')
print(' (E)mpty the bucket')
print(' (P)our one bucket into another')
print(' (Q)uit')
while True: # Спрашиваем, пока пользователь не укажет допустимое
# действие.
move = input('> ').upper()
if move == 'QUIT' or move == 'Q':
print('Thanks for playing!')
sys.exit()
if move in ('F', 'E', 'P'):
break # Игрок выбрал допустимое действие.
print('Enter F, E, P, or Q')
# Предлагаем игроку выбрать ведро:
while True: # Спрашиваем, пока не будет указано допустимое ведро.
print('Select a bucket 8, 5, 3, or QUIT:')
srcBucket = input('> ').upper()
if srcBucket == 'QUIT':
print('Thanks for playing!')
sys.exit()

420   Проект 81. Головоломка с ведрами воды
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.

if srcBucket in ('8', '5', '3'):
break # Игрок выбрал допустимое ведро.
# Производим выбранное действие:
if move == 'F':
# Устанавливаем количество воды в максимальное значение.
srcBucketSize = int(srcBucket)
waterInBucket[srcBucket] = srcBucketSize
steps += 1
elif move == 'E':
waterInBucket[srcBucket] = 0

# Устанавливаем количество воды
# в нулевое значение.

steps += 1
elif move == 'P':
# Предлагаем игроку выбрать ведро, в которое наливать воду:
while True: # Спрашиваем, пока не будет указано допустимое ведро.
print('Select a bucket to pour into: 8, 5, or 3')
dstBucket = input('> ').upper()
if dstBucket in ('8', '5', '3'):
break # Игрок выбрал допустимое ведро.
# Определяем наливаемый объем воды:
dstBucketSize = int(dstBucket)
emptySpaceInDstBucket = dstBucketSize - waterInBucket[dstBucket]
waterInSrcBucket = waterInBucket[srcBucket]
amountToPour = min(emptySpaceInDstBucket, waterInSrcBucket)
# Выливаем воду из ведра:
waterInBucket[srcBucket] -= amountToPour
# Наливаем вылитую воду в другое ведро:
waterInBucket[dstBucket] += amountToPour
steps += 1
elif move == 'C':
pass # Если игрок выбрал Cancel, ничего не делаем.

Когда вы введете исходный код и запустите его несколько раз, попробуйте по­
экспериментировать с внесением в него изменений. Можете также сами попробовать придумать, как сделать следующее:
разнообразить игру, добавив настройки для изменения размеров трех ведер
и целевого объема воды;
добавить «подсказку», при которой программа исследует количество воды
в ведрах и подбирает рекомендуемый следующий шаг. Если программа не
может вычислить следующий шаг, то просто выводит надпись «Не знаю, что
вы можете сделать дальше. Может, просто начать игру сначала?».

Исследование программы  

421

Исследование программы
Попробуйте найти ответы на следующие вопросы. Поэкспериментируйте с изменениями кода и запустите программу снова, чтобы увидеть, как они повлияют
на ее работу.
1. Что будет, если waterInBucket[srcBucket] = 0 в строке 104 заменить на
waterInBucket[srcBucket] = 1?
2. Что будет, если {'8': 0, '5': 0, '3': 0} в строке 16 заменить на {'8': 0, '5':
4, '3': 0}?
3. Что будет, если {'8': 0, '5': 0, '3': 0} в строке 16 заменить на {'8': 9, '5':
0, '3': 0}?

A
Указатель тегов

Проекты в этой книге снабжены тегами, описывающими
тип соответствующей программы. Первый тег описывает
ее размер: крошечная (от 1 до 63 строк), короткая (от 64 до
127 строк), большая (от 128 до 255 строк) и очень большая
(256 строк и более). Ниже представлен список проектов по
тегам размера:
крошечная: #3 Сообщение в виде битовой карты, #7 Взлом шифра Цезаря, #12 Гипотеза Коллатца, #14 Обратный отсчет, #15 Глубокая пещера,
#16 Ромбы, #19 Цифровые часы, #20 Цифровой поток, #24 Разложение на
множители, #25 Быстрый стрелок, #31 Угадай число, #32 Простак, #35 Гексагональная сетка, #40 П0г0в0рим (leetspeak), #42 Магический хрустальный шар, #46 Моделирование статистики за миллион бросков игральных
костей, #49 Таблица умножения, #50 Девяносто девять бутылок, #52 Счет
в различных системах счисления, #56 Простые числа, #57 Индикатор хода
выполнения, #58 Радуга, #60 Камень, ножницы, бумага (беспроигрышная
версия), #61 Шифр ROT13, #65 Ковер из «Сияния», #67 Синусовидное сообщение, #72 Губкорегистр, #74 Преобразование текста в речь;
короткая: #1 Бейглз, #2 Парадокс дней рождения, #5 Отскакивающий от
краев логотип DVD, #6 Шифр Цезаря, #8 Генерация календарей, #10 Чо-хан,
#13 Игра «Жизнь» Конвея, #18 Выбрасыватель игральных костей, #21 Визуализация ДНК, #26 Фибоначчи, #29 Моделирование лесного пожара,
#51 ДевяНосто деевяять буутылок, #53 Периодическая таблица элементов,

Указатель тегов  

423

#54 Поросячья латынь, #55 Лотерея Powerball, #59 Камень, ножницы, бумага,
#64 Семисегментный модуль индикации, #66 Простой шифр подстановки,
#69 Бега улиток, #71 Повторение музыки, #76 Крестики-нолики, #77 Ханойская башня, #80 Шифр Виженера;
большая: #4 Блек-джек, #9 Морковка в коробке, #11 Генератор заголовковприманок, #17 Арифметика с игральными костями, #22 Утята, #23 Гравировщик, #28 Заливка, #30 Четыре в ряд, #33 Мини-игра со взломом,
#34 «Виселица» и «гильотина», #36 Песочные часы, #37 Голодные роботы, #39 Муравей Лэнгтона, #41 Под счастливой звездой, #43 Манкала,
#44 Бегущий в лабиринте 2D, #47 Генератор картин в стиле Мондриана,
#48 Парадокс Монти Холла, #62 Вращающийся куб, #63 Царская игра
Ура, #68 Игра в 15, #70 Соробан — японский абак, #73Головоломка судоку,
#75 Три карты Монте, #78 Вопросы с подвохом, #79 Игра «2048», #81 Головоломка с ведрами воды;
очень большая: #27 Аквариум, #38 «Я обвиняю!», #45 Бегущий в лабиринте
3D.
Остальные теги описывают различные особенности программы:
bext: #5 Отскакивающий от краев логотип DVD, #27 Аквариум, #28 Заливка, #29 Моделирование лесного пожара, #36 Песочные часы, #39 Муравей
Лэнгтона, #47 Генератор картин в стиле Мондриана, #58 Радуга;
головоломка: #1 Бейглз, #33 Мини-игра со взломом, #34 «Виселица»
и «гильотина», #38 «Я обвиняю!», #68 Игра в 15, #73 Головоломка судоку,
#77 Ханойская башня, #79 2048, #81 Головоломка с ведрами воды;
графика: #3 Сообщение в виде битовой карты, #5 Отскакивающий от
краев логотип DVD, #13 Игра «Жизнь» Конвея, #14 Обратный отсчет,
#15 Глубокая пещера, #16 Ромбы, #19 Цифровые часы, #20 Цифровой поток, #21 Визуализация ДНК, #22 Утята, #23 Гравировщик, #27 Аквариум,
#33 Мини-игра со взломом, #35 Гексагональная сетка, #36 Песочные часы,
#39 Муравей Лэнгтона, #45 Бегущий в лабиринте 3D, #47 Генератор картин в стиле Мондриана, #58 Радуга, #62 Вращающийся куб, #65 Ковер из
«Сияния»,#67 Синусовидное сообщение, #69 Бега улиток, #70 Соробан —
японский абак;
для двух игроков: #9 Морковка в коробке, #30 Четыре в ряд, #43 Манкала,
#63 Царская игра Ура, #76 Крестики-нолики;
для начинающих: #6 Шифр Цезаря, #7 Взлом шифра Цезаря, #9 Морковка
в коробке, #10 Чо-хан, #11 Генератор заголовков-приманок, #12 Гипотеза
Коллатца, #15 Глубокая пещера, #16 Ромбы, #20 Цифровой поток, #24 Разложение на множители, #25 Быстрый стрелок, #31 Угадай число, #32 Простак, #35 Гексагональная сетка, #40 П0г0в0рим (leetspeak), #42 Магический
хрустальный шар, #46 Моделирование статистики за миллион бросков

424   Приложение A. Указатель тегов
игральных костей, #49 Таблица умножения, #50 Девяносто девять бутылок,
#58 Радуга, #65 Ковер из «Сияния», #69 Бега улиток, #71 Повторение музыки, #72 Губкорегистр, #74 Преобразование текста в речь;
игра: #1 Бейглз, #4 Блек-джек, #9 Морковка в коробке, #10 Чо-хан, #17 Арифметика с игральными костями, #25 Быстрый стрелок, #28 Заливка, #30 Четыре в ряд, #31 Угадай число, #33 Мини-игра со взломом, #34 «Виселица»
и «гильотина», #37 Голодные роботы, #38 «Я обвиняю!», #41 Под счастливой
звездой, #43 Манкала, #44 Бегущий в лабиринте 2D, #45 Бегущий в лабиринте 3D, #48 Парадокс Монти Холла, #59 Камень, ножницы, бумага, #60 Камень, ножницы, бумага (беспроигрышная версия), #63 Царская игра Ура,
#68 Игра в 15, #69 Бега улиток, #71 Повторение музыки, #73 Головоломка
судоку, #75 Три карты Монте, #76 Крестики-нолики, #77 Ханойская башня,
#79 2048, #81 Головоломка с ведрами воды;
имитационное моделирование: #2 Парадокс дней рождения, #13 Игра
«Жизнь» Конвея, #18 Выбрасыватель игральных костей, #29 Моделирование
лесного пожара, #36 Песочные часы, #39 Муравей Лэнгтона, #46Моделирование статистики за миллион бросков игральных костей, #48 Парадокс
Монти Холла, #55 Лотерея Powerball, #70 Соробан — японский абак;
карточная игра: #4 Блек-джек, #75 Три карты Монте;
криптография: #6 Шифр Цезаря, #7 Взлом шифра Цезаря, #61 Шифр
ROT13, #66 Простой шифр подстановки, #80 Шифр Виженера;
лабиринт: #44 Бегущий в лабиринте 2D, #45 Бегущий в лабиринте 3D;
математическая: #2 Парадокс дней рождения, #6 Шифр Цезаря, #7 Взлом
шифра Цезаря, #12 Гипотеза Коллатца, #17 Арифметика с игральными костями, #24 Разложение на множители, #26 Фибоначчи, #46 Моделирование
статистики за миллион бросков игральных костей, #48 Парадокс Монти
Холла, #49 Таблица умножения, #52 Счет в различных системах счисления,
#56 Простые числа, #62 Вращающийся куб, #66 Простой шифр подстановки, #70 Соробан — японский абак, #80 Шифр Виженера, #81 Головоломка
с ведрами воды;
многопользовательская: #41 Под счастливой звездой, #69 Бега улиток;
модуль: #57 Индикатор хода выполнения, #64 Семисегментный модуль
индикации;
настольная игра: #30 Четыре в ряд, #43 Манкала, #63 Царская игра Ура,
#76 Крестики-нолики;
научная: #21 Визуализация ДНК, #53 Периодическая таблица элементов;
объектно-ориентированная: #22 Утята, #73 Головоломка судоку;

Указатель тегов  

425

прокрутка: #15 Глубокая пещера, #20 Цифровой поток, #21 Визуализация
ДНК, #22 Утята, #50 Девяносто девять бутылок, #51 ДевяНосто деевяять
буутылок, #56 Простые числа, #58 Радуга;
слова: #11 Генератор заголовков-приманок, #34 «Виселица» и «Гильотина»,
#40 П0г0в0рим (leetspeak), #51 ДевяНосто деевяять буутылок, #54 Поросячья латынь, #72 Губкорегистр;
юмор: #11 Генератор заголовков-приманок, #32 Простак, #38 «Я обвиняю!»,
#42 Магический хрустальный шар, #55 Лотерея Powerball, #60 Камень, ножницы, бумага (беспроигрышная версия), #78 Вопросы с подвохом.

Б
Таблица кодов символов

Функция print() позволяет с легкостью вывести на экран
любой символ, который только можно набрать на клавиатуре. Однако существует много других символов, которые
может понадобиться отобразить на экране: символы мастей
карт (червы, бубны, трефы, пики); линии; затененные прямоугольники; стрелки; музыкальные ноты и т. д. Строковые
значения для этих символов можно получить, передав их числовой код, называемый кодом символа Unicode, функции chr(). Текст хранится
в компьютерах в виде последовательности чисел, и каждому символу соответствует
свое число. В этом приложении вы найдете список подобных кодов символов.

Использование функций chr() и ord()
Встроенная функция chr() языка Python принимает аргумент типа integer и возвращает строковое представление соответствующего этому числу символа. Функция ord() производит противоположное действие: принимает строковый аргумент,
содержащий отдельный символ, и возвращает соответствующее данному символу
число — его код в соответствии со стандартом Unicode.
Например, введите в интерактивной командной оболочке следующие команды:
>>> chr(65)
'A'
>>> ord('A')

Использование функций chr() и ord()  

427

65
>>> chr(66)
'B'
>>> chr(9829)
'♥'

Не все числа являются допустимыми кодами символов, подходящих для выведения
на экран. Спектр символов, которые можно отобразить в окне терминала, куда выводятся текстовые результаты работы программ, ограничен. Выводимый программой
символ должен также поддерживаться шрифтом, используемым окном терминала.
Вместо любых символов, которые оно не может отобразить, окно терминала выводит на экран замещающий символ, .
Диапазон отображаемых окнами терминала Windows символов еще более ограничен. Данный набор символов известен под названием Windows Glyph List 4, он
приведен в этом приложении и в статье «Википедии»: https://ru.wikipedia.org/wiki/
Windows_Glyph_List_4.
Коды символов часто указываются в виде шестнадцатеричных чисел, а не привычных нам десятичных. В шестнадцатеричных числах, помимо десятичных цифр
от 0 до 9, содержатся еще и буквы от A до F. Шестнадцатеричные числа часто записываются с префиксом 0x, который и означает, что следующее за ним число —
шестнадцатеричное.
Преобразовать десятичное целое в строковое значение соответствующего шестнадцатеричного числа можно с помощью функции hex(). Преобразовать строковое
представление шестнадцатеричного числа в десятичное целое можно с помощью
функции int(), передав ей в качестве второго аргумента 16. Например, введите
в интерактивной командной оболочке следующее:
>>> hex(9)
'0x9'
>>> hex(10)
'0xa'
>>> hex(15)
'0xf'
>>> hex(16)
'0x10'
>>> hex(17)
'0x11'
>>> int('0x11', 16)
17
>>> int('11', 16)
17

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

428   Приложение Б. Таблица кодов символов

Таблица кодов символов
Ниже приведены все коды символов Unicode из набора, известного как Windows
Glyph List 4, то есть символов, поддерживаемых программой терминала Windows,
CMD. И macOS, и Linux способны отображать больше символов, чем представлено
в данном списке, но ради совместимости ваших программ на языке Python я рекомендую ограничиться символами из этой таблицы.
32

56 8

80 P

104 h

162 ¢

33 !

57 9

81 Q

105 i

163 £

34 «

58 :

82 R

106 j

164 ¤

35 #

59 ;

83 S

107 k

165 ¥

36 $

60 <

84 T

108 l

166 ¦

37 %

61 =

85 U

109 m

167 §

38 &

62 >

86 V

110 n

168 ¨

39 '

63 ?

87 W

111 o

169 ©

40 (

64 @

88 X

112 p

170 ª

41 )

65 A

89 Y

113 q

171 «

42 *

66 B

90 Z

114 r

172 ¬

43 +

67 C

91 [

115 s

173 -

44 ,

68 D

92 \

116 t

174 ®

45 -

69 E

93 ]

117 u

175 ¯

46 .

70 F

94 ^

118 v

176 °

47 /

71 G

95 _

119 w

177 ±

48 0

72 H

96 `

120 x

178 ²

49 1

73 I

97 a

121 y

179 ³

50 2

74 J

98 b

122 z

180 ´

51 3

75 K

99 c

123 {

181 µ

52 4

76 L

100 d

124 |

182 ¶

53 5

77 M

101 e

125 }

183 ·

54 6

78 N

102 f

126 ~

184 ¸

55 7

79 O

103 g

161 ¡

185 ¹

Таблица кодов символов  

186 º

216 Ø

247 ÷

283 ě

336 Ő

187 «

217 Ù

248 ø

286 Ğ

337 ő

188 ¼

218 Ú

249 ù

287 ğ

338 Œ

189 ½

219 Û

250 ú

290 Ģ

339 œ

190 ¾

220 Ü

251 û

291 ģ

340 Ŕ

191 ¿

221 Ý

252 ü

298 Ī

341 ŕ

192 À

223 ß

253 ý

299 ī

342 Ŗ

193 Á

224 à

255 ÿ

302 Į

343 ŗ

194 Â

225 á

256 Ā

303 į

344 Ř

195 Ã

226 â

257 ā

304 İ

345 ř

196 Ä

227 ã

258 Ă

305 ı

346 Ś

197 Å

228 ä

259 ă

310 Ķ

347 ś

198 Æ

229 å

260 Ą

311 ķ

350 Ş

199 Ç

230 æ

261 ą

313 Ĺ

351 ş

200 È

231 ç

262 Ć

314 ĺ

352 Š

201 É

232 è

263 ć

315 Ļ

353 š

202 Ê

233 é

268 Č

316 ļ

354 Ţ

203 Ë

234 ê

269 č

317 Ľ

355 ţ

204 Ì

235 ë

270 Ď

318 ľ

356 Ť

205 Í

236 ì

271 ď

321 Ł

357 ť

206 Î

237 í

272 Đ

322 ł

362 Ū

207 Ï

238 î

273 đ

323 Ń

363 ū

209 Ñ

239 ï

274 Ē

324 ń

366 Ů

210 Ò

241 ñ

275 ē

325 Ņ

367 ů

211 Ó

242 ò

278 Ė

326 ņ

368 Ű

212 Ô

243 ó

279 ė

327 Ň

369 ű

213 Õ

244 ô

280 Ę

328 ň

370 Ų

214 Ö

245 õ

281 ę

332 Ō

371 ų

215 ×

246 ö

282 Ě

333 ō

376 Ÿ

429

430   Приложение Б. Таблица кодов символов
377 Ź

918 Ζ

948 δ

1027 Ѓ

1057 С

378 ź

919 Η

949 ε

1028 Є

1058 Т

379 Ż

920 Θ

950 ζ

1029 Ѕ

1059 У

380 ż

921 Ι

951 η

1030 І

1060 Ф

381 Ž

922 Κ

952 θ

1031 Ї

1061 Х

382 ž

923 Λ

953 ι

1032 Ј

1062 Ц

402 ƒ

924 Μ

954 κ

1033 Љ

1063 Ч

710 ˆ

925 Ν

955 λ

1034 Њ

1064 Ш

711 ˇ

926 Ξ

956 μ

1035 Ћ

1065 Щ

728 ˘

927 Ο

957 ν

1036 Ќ

1066 Ъ

729 ˙

928 Π

958 ξ

1038 Ў

1067 Ы

731 ˛

929 Ρ

959 ο

1039 Џ

1068 Ь

732 ˜

931 Σ

960 π

1040 А

1069 Э

733 ˝

932 Τ

961 ρ

1041 Б

1070 Ю

900 ΄

933 Υ

962 ς

1042 В

1071 Я

901 ΅

934 Φ

963 σ

1043 Г

1072 а

902 Ά

935 Χ

964 τ

1044 Д

1073 б

904 Έ

936 Ψ

965 υ

1045 Е

1074 в

905 Ή

937 Ω

966 φ

1046 Ж

1075 г

906 Ί

938 Ϊ

967 χ

1047 З

1076 д

908 Ό

939 Ϋ

968 ψ

1048 И

1077 е

910 Ύ

940 ά

969 ω

1049 Й

1078 ж

911 Ώ

941 έ

970 ϊ

1050 К

1079 з

912 ΐ

942 ή

971 ϋ

1051 Л

1080 и

913 Α

943 ί

972 ό

1052 М

1081 й

914 Β

944 ΰ

973 ύ

1053 Н

1082 к

915 Γ

945 α

974 ώ

1054 О

1083 л

916 Δ

946 β

1025 Ё

1055 П

1084 м

917 Ε

947 γ

1026 Ђ

1056 Р

1085 н

Таблица кодов символов  

1086 о

1116 ќ

8745 ∩

9562 ╚

9642 ▪

1087 п

1118 ў

8776 ≈

9563 ╛

9643 ▫

1088 р

1119 џ

8801 ≡

9564 ╜

9644 ▬

1089 с

1168 Ґ

8804 ≤

9565 ╝

9650 ▲

1090 т

1169 ґ

8805 ≥

9566 ╞

9658 ►

1091 у

8211 –

8976 ⌐

9567 ╟

9660 ▼

1092 ф

8212 —

8992 ⌠

9568 ╠

9668 ◄

1093 х

8213 ―

8993 ⌡

9569 ╡

9674 ◊

1094 ц

8216 ‘

9472 ─

9570 ╢

9675 ○

1095 ч

8217 ’

9474 │

9571 ╣

9679 ●

1096 ш

8218 ‚

9484 ┌

9572 ╤

9688 ◘

1097 щ

8220 “

9488 ┐

9573 ╥

9689 ◙

1098 ъ

8221 ”

9492 └

9574 ╦

9702 ◦

1099 ы

8222 „

9496 ┘

9575 ╧

9786 ☺

1100 ь

8224 †

9500 ├

9576 ╨

9787 ☻

1101 э

8225 ‡

9508 ┤

9577 ╩

9788 ☼

1102 ю

8226 •

9516 ┬

9578 ╪

9792 ♀

1103 я

8230 …

9524 ┴

9579 ╫

9794 ♂

1105 ё

8240 ‰

9532 ┼

9580 ╬

9824 ♠

1106 ђ

8249 ‹

9552 ═

9600 ▀

9827 ♣

1107 ѓ

8250 ›

9553 ║

9604 ▄

9829 ♥

1108 є

8319 ⁿ

9554 ╒

9608 █

9830 ♦

1109 ѕ

8359 Pts

9555 ╓

9612 ▌

9834 ♪

1110 і

8364 €

9556 ╔

9616 ▐

9835 ♫

1111 ї

8470 №

9557 ╕

9617 ░

1112 ј

8482 ™

9558 ╖

9618 ▒

1113 љ

8729 ∙

9559 ╗

9619 ▓

1114 њ

8730 √

9560 ╘

9632 ■

1115 ћ

8734 ∞

9561 ╙

9633 □

431

Эл Свейгарт
Большая книга проектов Python
Перевел с английского И. Пальти

Заведующая редакцией
Ю. Сергиенко
Ведущий редактор
Н. Гринчик
Литературный редактор
Н. Хлебина
Художественный редактор
В. Мостипан
Корректоры
М. Молчанова, Е. Павлович
Верстка
Л. Егорова

Изготовлено в России. Изготовитель: ООО «Прогресс книга».
Место нахождения и фактический адрес: 194044, Россия, г. Санкт-Петербург,
Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373.
Дата изготовления: 02.2022. Наименование: книжная продукция. Срок годности: не ограничен.
Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные
профессиональные, технические и научные.
Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01.
Подписано в печать 10.12.21. Формат 70×100/16. Бумага офсетная. Усл. п. л. 34,830. Тираж 1500. Заказ 0000.