Журнал LinuxFormat - перейти на главную

LXF113-114:Python

Материал из Linuxformat
Перейти к: навигация, поиск
Пишем игру-головоломку, изучая PyGame


Содержание

Кодируем: Башни Ханоя

В последней статье этого цикла Майк Сондерс задействует все изученные нами приемы в одном проекте.

В прошлых номерах мы с вами написали клон Space Invaders и гоночную игру. Мы узнали, как работать со спрайтами, выводить текст, проигрывать музыку и обрабатывать ввод с клавиатуры. Отличная подготовка к последнему проекту! После длительных размышлений мы решили, что лучшим способом обобщить полученные знания будет игра «Ханойские башни».

Если вы никогда раньше не играли в ханойские башни, вот суть процесса: у вас есть три стопки, в которые можно складывать диски. Вначале все диски находятся в левой стопке, и цель игры – переместить их в правую. Но брать диски можно только по одному, причем на меньший диск нельзя класть больший. Все диски имеют разный размер, и после решения головоломки на правой башне внизу должен лежать самый большой диск, а вверху – самый маленький.

В нашей версии с воображаемым названием PyHanoi будет всего три диска – это упростит код. В конце мы узнаем, как можно добавить еще дисков. Это поможет вам, если у вас уже есть некоторый опыт программирования. Но не будем зацикливаться на теории – перейдем сразу к делу!

Часть 1 Готовим почву

Как и раньше, мы будем использовать Python и PyGame: очень простой в понимании язык программирования и очень полезный набор подпрограмм для мультимедиа. На нашем DVD в разделе Журнал/Coding вы найдете файл PyHanoi.tar.gz; скопируйте его в свой домашний каталог и распакуйте. В получившейся директории вы найдете подкаталог data – он содержит изображения и музыкальные файлы, которые позволят персонализировать игру; вот что вам нужно о них знать.

  • backdrop.jpg Фоновая картинка игры размером 640 х 480 пикселей. Если захотите изменить ее, подбирайте светлую и неконтрастную, что-

бы диски было хорошо видно.

  • disc1.png, disc2.png и disc3.png Изображения дисков. Мы смотрим на башни спереди, и диски показаны как прямоугольники. Откройте их в GIMP, чтобы определить разрешение, если хотите создать новые.
  • highlight1.png и highlight2.png Это желтый и красный прямоугольники, которые будут отображаться поверх одной из групп дисков и перемещаться, когда пользователь нажимает стрелки вправо и влево. Мы будем использовать желтый прямоугольник, когда пользователь выбирает группу дисков, и красный, когда он нажал Enter, подтверждая отмеченный вариант.
  • music.mod Музыкальный файл в формате MOD (например, созданный в SoundTracker), который будет проигрываться бесконечно.

Смело изменяйте любой из этих файлов (или попросите кого-нибудь другого поработать над ними, пока будете изучать код!). Или просто ими воспользуйтесь. Прежде чем писать код, составим короткий алгоритм игры. В PyHanoi игрок сначала выделяет одну из стопок и нажимает Enter для выбора диска. Затем он выделяет другую стопку и перемещает диск. Если ход является корректным, мы обновляем счетчик в переменной moves. Таким образом, алгоритм будет следующим:

  1. Нарисовать все на экране (фон, диски, счетчик очков, выделить текущие группы дисков).
  2. Проверить нажатия клавиш (стрелка влево/вправо для перемещения выделения, Enter для выбора диска и Esc для выхода).
  3. Попробовать переместить диск, если игрок выбрал одну группу дисков, а затем другую. В случае успеха учесть «+1» в счетчике ходов.
  4. Перейти к пункту 1.

Во врезке «Списки для башен» можно узнать, как мы будем использовать те или иные возможности Python для реализации групп дисков нашей игры. После того, как разберетесь с этим, запустите программу (python pyhanoi.py) и увидите, как она работает.

Часть 2 Разбираемся в коде

В PyHanoi имеются функции (подпрограммы), поэтому лучше пройтись по коду в порядке его выполнения. В начале файла pyhanoi.py мы видим:

from pygame import *

Эта строка одинакова во всех наших проектах: она сообщает Python о нашем желании использовать все возможности библиотеки PyGame (звездочка здесь является шаблоном, как в оболочке). Далее идут наши функции:

def wait_for_key():
...
def draw_discs():
...
def try_move(first, second):

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

 init()
 screen = display.set_mode((640,480))
 display.set_caption(‘PyHanoi’)

Данный код инициализирует PyGame, создает окно размером 640 x 480 пикселей и устанавливает его заголовок. Теперь загрузим вышеупомянутые изображения:

 backdrop = image.load(‘data/backdrop.jpg)
 disc1 = image.load(‘data/disc1.png)
 disc2 = image.load(‘data/disc2.png)
 disc3 = image.load(‘data/disc3.png)
 highlight1 = image.load(‘data/highlight1.png)
 highlight2 = image.load(‘data/highlight2.png)

В PyGame' это делается очень просто: вызовите функцию image.load() с именем файла в качестве аргумента, и она вернет объект изображения, который можно вывести на экран и поработать с ним. Кроме изображений, нам понадобится шрифт для счетчика ходов и сообщения об ошибке при попытке некорректного перемещения:

 movesfont = font.Font(None, 40)

Этот код создает новый объект шрифта movesfont с гарнитурой ‘None’ и размером 40. Вместо ‘None’ можно указать настоящее имя шрифта, но его может не оказаться на другом компьютере – поэтому проще использовать ‘None’ (при этом загружается общий шрифт семейства Sans Serif).

Пока все идет молчком; добавим-ка немного музыки.

 mixer.music.load(‘data/music.mod’)
 mixer.music.play(-1)

Здесь мы загружаем и проигрываем музыкальный файл в формате MOD с помощью модуля mixer библиотеки PyGame. Параметр -1 означает, что музыка будет звучать бесконечно (в смысле, пока мы не выйдем из игры).

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

 stack = [[3, 2, 1], [], []]

Эта строка кода создает три списка, обозначенных квадратными скобками. Списки соответствуют башням. В начале игры все диски находятся на первой башне, поэтому первый список содержит [3, 2, 1] – самый большой диск с номером 3 внизу и самый маленький с номером 1 наверху (и его уже можно перемещать). Два других списка соответствуют пустым башням, поэтому они пустые [].

Нам также нужно создать несколько переменных:

 moves = 0
 position = 0
 selected1 = -1
 selected2 = -1

Первая – наш счетчик ходов, содержащий общее число перемещений за всю игру. Вторая обозначает башню, выделенную желтым прямоугольником – игрок сможет двигать его с помощью стрелок вправо и влево. У нас есть три башни с номерами 0, 1 и 2; так, если position равна 0, то первой будет выделена первая башня. Переменные selected1 и selected2 будут хранить номер башни, на которой игрок нажал Enter, чтобы переместить диск. В самом начале игры и в любой момент, когда игрок не выделяет башни для перемещения, эти переменные будут равны -1, то есть они ни на что не указывают.

Наша следующая задача – начать основной цикл игры:

  quit = 0
  while (quit == 0):
   screen.blit(backdrop, (0,0))
   draw_discs()
   if selected1 == -1:
     screen.blit(highlight1, (position * 200 + 30, 250))
   else:
     screen.blit(highlight2, (position * 200 + 30, 250))
   movestext = movesfont.render(‘Moves: ‘ + str(moves), True, (255,255,255), (0,0,0))
   screen.blit(movestext, (5,5))
   display.update()

Мы создаем новую переменную quit и устанавливаем ее в 0. Основной цикл игры начинается с команды while. Будут выполняться все команды с отступом, пока переменная quit не сменит свое значение с нуля на какое-то другое (например, если нажата клавиша Esc, как мы скоро увидим). В начале цикла надо все нарисовать, поэтому сначала мы «блитируем» (выводим) фоновое изображение в главное окно, начиная с точки с координатами 0,0 – это его левый верхний угол. В PyGame, как и в большинстве графических библиотек, начало координат находится в левом верхнем углу, и координаты отсчитываются от нуля. Таким образом, координаты правого нижнего угла окна будут равны 639, 479.

Прорисовав фон, мы вызываем функцию draw_discs(): она проходит по трем башням и отрисовывает все находящиеся на них диски в соответствующих местах экрана. Затем мы рисуем прямоугольник выделения: если пользователь перемещает диск и нажал Enter, мы изображаем highlight2 (красный прямоугольник) в текущей позиции. В противном случае, рисуем только highlight1 (желтый прямоугольник). Горизонтальная координата прямоугольника на экране определяется выражением position * 200 + 30: в зависимости от значения переменной position он будет выведен на расстоянии 30, 230 или 430 пикселей справа (и всегда на 250 пикселей вниз от начала координат).

Затем мы создаем изображение movestext путем обращения к созданному ранее объекту movesfont. Первый параметр содержит слово Moves:, объединенное с количеством ходов (конвертированным в строку с помощью функции str()). Второй параметр, ‘True’, велит использовать сглаживание. Затем идут два цвета (в нашем случае – белый текст на черном фоне). В конце мы выводим созданное изображение movestext на экран.

Обрабатываем ввод игрока

Обратите внимание, что до сих пор все графические операции осуществлялись в скрытом графическом буфере: это делалось по соображениям производительности. Их результат не появится на экране, пока не будет вызван метод display.update(). Пора обработать ввод с клавиатуры от игрока:

 ourevent = event.wait()
 if ourevent.type == KEYDOWN:
   if ourevent.key == K_ESCAPE:
     quit = 1
   if ourevent.key == K_LEFT and position > 0:
     position -= 1
   if ourevent.key == K_RIGHT and position < 2:
     position += 1

Здесь все очевидно: мы ожидаем появления события, которым может быть обновление экрана, перемещение мыши и т.д. Нам интересно только событие KEYDOWN: оно возникает, когда пользователь нажимает какую-то клавишу. И стоит пользователю так сделать, как мы проверяем, что это за клавиша: если Esc, то устанавливаем созданную ранее переменную quit в 1, это остановит основной цикл игры.

Если пользователь нажал стрелку вправо или влево, нужно обновить переменную position, определяющую, где нужно нарисовать прямоугольник выделения. Но нужно также проверить, не пытается ли игрок переместить прямоугольник слишком далеко, поэтому условия position > 0 и position < 2 ограничивает значения переменной множеством из 0, 1 и 2 – другими словами, прямоугольник нельзя перемещать за пределы башен.

Следующий фрагмент чуть сложнее. Если пользователь нажимает Enter, это означает, что он хочет выбрать башню, на которую нужно переместить диск:

   if ourevent.key == K_RETURN:
     if selected1 == -1:
       selected1 = position

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

   else:
     selected2 = position
     if selected2 == selected1:
       selected1 = -1
       selected2 = -1

Мы записываем в selected2 текущую позицию. А что будет, если игрок выберет одну и ту же башню как источник и место назначения? Ну, в этом случае делать ничего не нужно, поэтому мы сбрасываем переменные selected1 и selected2 в их исходные значения -1 – это означает, что все начинается снова.

Если были выбраны разные башни, выполняется этот код:

   else:
     x = try_move(selected1, selected2)
    moves += x
    selected1 = -1
    selected2 = -1

Чтобы определить, можно ли переместить диск с башни selected1 на башню selected2, мы вызываем функцию try_move() (через минуту мы о ней поговорим). Функция try_move() возвращает 1, если диск перемещен успешно, и 0 в противном случае. Мы обновляем счетчик перемещений и сбрасываем selected1 и selected2 для нового перемещения.

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

 def wait_for_key():
   ourevent = event.wait()
   while ourevent.type != KEYDOWN:
       ourevent = event.wait()

Она просто приостанавливает выполнение программы вплоть до нажатия клавиши. Рисуем диски:

 def draw_discs():
   offset = 50
   for x in range (0, 3):
     if stack[x] == [3, 2, 1]:
        screen.blit(disc3, (offset, 400))
        screen.blit(disc2, (offset+25, 380))
        screen.blit(disc1, (offset+50, 360))
   ...
   offset += 200

Функция пробегает по башням (в цикле for x in range) и для каждой из них проверяет порядок дисков. Для первой башни диски отображаются на расстоянии 50 пикселей от левого края экрана с использованием переменной offset, а переходя к следующей башне, мы прибавляем к смещению 200 и двигаемся дальше вправо. От самого большого до самого малого ширина дисков уменьшается на 50 пикселей, поэтому для центрирования дисков на башне используются выражения offset+25 и offset+50.

Обратите внимание, что код функции приведен здесь не полностью: есть несколько одинаковых проверок и операций вывода для дисков различных типов, но их код мало чем отличается. Так что строки, помеченные здесь многоточием, можно посмотреть в исходном файле pyhanoi.py.

Наконец, мы пришли к самой важной функции – коду, выполняющему перемещение диска. Он принимает в качестве аргументов два числа – номера исходной и целевой башен:

 def try_move(first, second):
   if len(stack[first]) == 0:
     return 0

Номер исходной башни содержится в переменной first, целевой – в second. Мы проверяем, равна ли нулю длина списка для первой башни; если да, то с нее нечего снимать, операция перемещения некорректна и мы возвращаемся к основному коду (0 означает, что перемещения не произошло). Но если первый список не пуст:

  if len(stack[first]) > 0 and len(stack[second]) == 0:
     a = stack[first].pop()
     stack[second].append(a)
     return 1

Мы проверяем, пуст ли список целевой башни. Если это так, мы просто извлекаем число из списка для первой башни и добавляем его в список для второй. Вуаля – диск на новом месте! Но вдруг оба списка не пусты? Мы не можем просто взять диск из первого списка и добавить во второй: нужно следовать правилам и не позволять игроку класть больший диск на меньший.

   else:
    if len(stack[first]) > 0 and len(stack[second]) > 0:
      a = stack[first].pop()
      b = stack[second].pop()

Здесь мы записываем во временные переменные a и b верхние диски каждой башни. Затем

      if a > b:
        invalidtext = movesfont.render(‘Invalid move!’, True, (255,255,255), (0,0,0))
        screen.blit(invalidtext, (235,200))
        display.update()
        wait_for_key()
        stack[first].append(a)
        stack[second].append(b)
        return 0

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

    else:
      stack[second].append(b)
      stack[second].append(a)
     return 1

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


Не останавливайтесь на достигнутом

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

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

Дайте нам знать, что у вас получится! Если у вас есть вопросы по коду или вы хотите поделиться идеями с другими читателями, присоединяйтесь к дискуссиям на Линуксфоруме (http://unixforum.org). LXF

Списки и башни

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

foo = [1, 2, 3]

Эта строка кода объявляет список foo, состоящий из трех элементов (целочисленных переменных). Для извлечения элемента из конца списка используется метод pop:

a = foo.pop()

Переменная а теперь содержит значение 3, а foo[1, 2]. Размер списка изменяется автоматически – в конце не появляется пустого или нулевого элемента. Метод append добавляет число в список:

 foo.append(99)

Теперь foo содержит [1, 2, 99]. Списки позволят нам очень просто создать стопки – ханойские башни, куда мы будем помещать диски (добавляя в список числа). Самому большому диску соответствует число 3, самому маленькому – 1.


Чтобы код был простым (и легко расширяемым), создадим список из трех башен, каждый элемент которого будет содержать подсписок дисков, которые в данный момент находятся на башне. Посмотрите на рисунок: первая башня – stack[0], вторая – stack[1] и третья – stack[2]. Каждая содержит список своих дисков. Башня со всеми дисками будет содержать элементы [3, 2, 1], т.е. самый большой диск (3) находится внизу башни, а самый маленький (1) – наверху. Мы можем взять самый маленький диск сверху и переместить его на другую башню (добавить в другой список).

Персональные инструменты
купить
подписаться
Яндекс.Метрика