Прокачиваем систему управления лифтами на Python
hard

Прокачиваем систему управления лифтами на Python

Добавляем реализм, очередь этажей и возможность отключения

В предыдущей статье мы начали писать проект на Python c системой управления лифтами. Сегодня усложним его логику и сделаем так, что наша система будет больше похожа на настоящую.

Чтобы было проще разобраться в том, что будет происходить дальше, под катом — код из первой части, будем допиливать его.

# импортируем pygame
import pygame

# задаём константы для неизменяющихся значений
SCREEN_WIDTH = 470
SCREEN_HEIGHT = 770
FLOOR_COUNT = 11
FREIGHT_ELEVATOR_WIDTH = 75
ELEVATOR_WIDTH = 50
ELEVATOR_HEIGHT = 70
BUTTON_SIZE = 20
FLOOR_HEIGHT = 70
# константа чёрного цвета
BLACK = (0, 0, 0)

# инициализируем модули pygame
pygame.init()

# создаём объект для главного экрана
# с указанием ширины и высоты
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# указываем название для главного окна
pygame.display.set_caption("Два лифта")

# загружаем изображения
# картинка для лифта
elevator_img = pygame.image.load("elevator.jpg")
# картинка для красной кнопки
button_red_img = pygame.image.load("button1_elevator1.png").convert_alpha()
# картинка для синей кнопки
button_blue_img = pygame.image.load("button1_elevator2.png").convert_alpha()
# картинка для грузового лифта
freight_elevator_img = pygame.image.load("freight_elevator.jpg")
# картинка для фона
back_img = pygame.image.load("back.jpg")

# масштабируем изображения под заданные в константах размеры
elevator_img = pygame.transform.scale(elevator_img, (ELEVATOR_WIDTH, ELEVATOR_HEIGHT))
button_red_img = pygame.transform.scale(button_red_img, (BUTTON_SIZE, BUTTON_SIZE))
button_blue_img = pygame.transform.scale(button_blue_img, (BUTTON_SIZE, BUTTON_SIZE))
freight_elevator_img = pygame.transform.scale(freight_elevator_img, (FREIGHT_ELEVATOR_WIDTH, ELEVATOR_HEIGHT))
back_img = pygame.transform.scale(back_img, (SCREEN_WIDTH, SCREEN_HEIGHT))


# класс Кнопок вызова
class Button:
   # конструктор __init__ создаёт кнопку
   def __init__(self, x, y, image, action):
       # рисуем прямоугольник с заданными координатами
       self.rect = pygame.Rect(x, y, BUTTON_SIZE, BUTTON_SIZE)
       # в прямоугольник вписываем изображение
       self.image = image
       # привязываем кнопку к функции, которую укажем при создании кнопки
       self.action = action

   # метод для отрисовки кнопки на главном экране с заданными координатами
   def draw(self, screen):
       # метод blit рисует новый объект на изображении screen
       screen.blit(self.image, (self.rect.x, self.rect.y))

   # метод проверки нажатия кнопки
   def check_click(self, pos):
       # collidepoint проверяет действие курсора
       if self.rect.collidepoint(pos):
           # если пользователь кликнул на кнопку,
           # включаем указанную при создании функцию
           self.action()


# класс Этажей
# создаёт этаж с кнопками для вызова двух лифтов.
# их координаты зависят от номера этажа (y)
class Floor:
   # конструктор __init__ создаёт этаж
   def __init__(self, y, elevator1, elevator2):
       # передаём номер этажа
       self.y = y
       # передаём на каждый этаж по два объекта кнопок,
       # размещаем на нужных местах соответственно этажу
       # и привязываем к нужному лифту через лямбда-функцию
       self.buttons = [
           Button(110, y + FLOOR_HEIGHT // 2 - BUTTON_SIZE // 2, button_red_img, lambda: elevator1.set_target(y)),
           Button(335, y + FLOOR_HEIGHT // 2 - BUTTON_SIZE // 2, button_blue_img, lambda: elevator2.set_target(y)),
       ]

   # метод отрисовки этажа и кнопки
   def draw(self, screen):
       # рисуем линию на главном экране с толщиной 1
       # от нулевой координаты до конца
       # экрана по оси х и на высоте этаже y
       pygame.draw.line(screen, BLACK, (0, self.y), (SCREEN_WIDTH, self.y), 1)
       # рисуем кнопки на каждом этаже
       for button in self.buttons:
           button.draw(screen)


# класс Лифта
# определяем лифт с координатами, размерами и изображением
class Elevator:
   def __init__(self, x, y, width, height, image):
       # начальные координаты
       self.x = x
       self.y = y
       # размеры лифта
       self.width = width
       self.height = height
       # изображение
       self.image = image
       # target_y задаёт этаж, куда должен ехать лифт
       self.target_y = y
       # скорость лифта
       self.speed = 2

   # метод для движения лифтов
   def move(self):
       # если лифт находится ниже этажа нажатой кнопкой...
       if self.y < self.target_y:
           # координата лифта увеличивается с установленной скоростью
           self.y += self.speed
       # если лифт находится выше этажа нажатой кнопкой...
       elif self.y > self.target_y:
           # координата лифта уменьшается с установленной скоростью
           self.y -= self.speed

   # метод для отрисовки лифтов на главном экране
   def draw(self, screen):
       screen.blit(self.image, (self.x, self.y))


# класс для создания всей Системы Лифтов
class ElevatorSystem:
   def __init__(self):
       # cоздаём два лифта: грузовой и пассажирский
       self.elevator1 = Elevator(150, SCREEN_HEIGHT - FLOOR_HEIGHT,
                                 FREIGHT_ELEVATOR_WIDTH, ELEVATOR_HEIGHT, freight_elevator_img)
       self.elevator2 = Elevator(255, SCREEN_HEIGHT - FLOOR_HEIGHT,
                                 ELEVATOR_WIDTH, ELEVATOR_HEIGHT, elevator_img)
       # создаём массив этажей
       self.floors = [
           Floor(SCREEN_HEIGHT - FLOOR_HEIGHT * (i + 1), self.elevator1, self.elevator2)
           for i in range(FLOOR_COUNT)
       ]

   # метод обновления положения лифтов, созданный в классе Elevator
   def update(self):
       self.elevator1.move()
       self.elevator2.move()

   # метод отрисовки объектов
   def draw(self, screen):
       # рисуем этажи через метод draw класса Floor
       for floor in self.floors:
           floor.draw(screen)
       # рисуем лифты через метод draw класса Elevator
       self.elevator1.draw(screen)
       self.elevator2.draw(screen)


# функция для установки целевого этажа для лифта
def elevator_set_target(self, y):
   self.target_y = y


# устанавливаем целевой этаж в объект Лифта
Elevator.set_target = elevator_set_target


# основная функция
def main():
   # объект частоты обновления экрана
   clock = pygame.time.Clock()
   # флаг-метка для работы главного цикла
   running = True
   # создаём объект класса Системы Лифтов
   system = ElevatorSystem()

   # пока флаг-метка равен True, работает цикл всей системы
   while running:
       # отрисовываем фон
       screen.blit(back_img, (0, 0))

       # проверяем список событий при запущенной программе
       for event in pygame.event.get():
           # если пользователь закрыл главное окно...
           if event.type == pygame.QUIT:
               # завершаем цикл
               running = False
           # если пользователь кликнул мышкой...
           elif event.type == pygame.MOUSEBUTTONDOWN:
               # сохраняем координаты клика в переменную pos
               pos = pygame.mouse.get_pos()
               # проверяем все этажи и кнопки для проверки,
               # совпадают ли координаты клика с одной из кнопок
               for floor in system.floors:
                   for button in floor.buttons:
                       button.check_click(pos)

       # обновляет состояние лифтов
       system.update()
       # отрисовывает лифты на нужных позициях
       system.draw(screen)
       # обновляем изображение для пользователя, создавая анимацию
       pygame.display.flip()
       # устанавливаем скорость обновления кадров до 30 в секунду
       clock.tick(30)

   # завершаем работу после окончания цикла
   pygame.quit()


# запускаем главную функцию
main()

Что сделали в предыдущей статье

С помощью библиотеки для создания игр и визуализации pygame мы построили дом в 11 этажей и два лифта — пассажирский и грузовой. На каждом этаже рядом с лифтами есть кнопка вызова. Если на неё нажать, лифт едет:

Что сделаем сегодня

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

Сегодня реализуем очередь: сделаем так, что лифты запоминают вызовы и объезжают все этажи, на которых нажали кнопки. Нажатая кнопка будет отображаться ярче.

Ещё добавим немного реалистичности: научимся отключать лифты на любом этаже. В нашем примере добавим возможность отключения и включения пассажирского лифта на 3-м этаже. Если лифт отключён, кнопка становится темнее и нажать на неё уже нельзя:

Что нам понадобится

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

Для реализации очереди этажей нам понадобится список — стандартная коллекция элементов в Python. В список можно добавлять элементы, удалять или изменять их. Мы сделаем так, что все нажатые кнопки будут добавляться в список, и пока лифт не доедет до этажа, повторно нажать их нельзя — всё как в реальной жизни.

Прокачиваем кнопки

Чтобы научить лифты быть более реалистичными, начнём с кнопок. Для этого в классе Button нам нужно предусмотреть два новых состояния для каждого этажа:

  • нажата кнопка или нет;
  • работает кнопка или нет.

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

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

Нам нужны два новых состояния — назовём их self.is_active и self.enabled. Оба состояния будут работать как логические переменные, то есть принимать значения True или False. По умолчанию кнопки будут неактивны и не отключены:

# класс Кнопок вызова
class Button:
   # конструктор __init__ создаёт кнопку
   def __init__(self, x, y, image, action, enabled=True):
       # рисуем прямоугольник с заданными координатами
       self.rect = pygame.Rect(x, y, BUTTON_SIZE, BUTTON_SIZE)
       # в прямоугольник вписываем изображение
       self.image = image
       # привязываем кнопку к функции, которую укажем при создании кнопки
       self.action = action
       # свойство — нажата кнопка или нет
       self.is_active = False
       # свойство — отключена кнопка или нет
       self.enabled = enabled

На новые условия можно ввести проверки и отображать визуально каждое изменение. Сделаем так:

  • Будем проверять оба состояния.
  • По умолчанию все кнопки будут слегка затемнены.
  • Если кнопка отключена, затемним её сильнее.
  • Если кнопка нажата, отображаем оригинальное яркое изображение. Получится, что кнопка загорается после нажатия.

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

Теперь запишем эту логику в коде:

    # метод для отрисовки кнопки на главном экране с заданными координатами
    def draw(self, screen_func):
        # если кнопка отключена:
        if not self.enabled:
            # создаём изображение и указываем через параметр
            # pygame.SRCALPHA, что у него будут уровни прозрачности
            disabled_image = pygame.Surface((BUTTON_SIZE, BUTTON_SIZE), pygame.SRCALPHA)
            # копируем исходное изображение кнопки
            disabled_image.blit(self.image, (0, 0))
            # накладываем на исходное изображение тёмный полупрозрачный серый цвет
            disabled_image.fill((50, 50, 50, 200), special_flags=pygame.BLEND_RGBA_MULT)
            # метод blit рисует новый объект на изображении screen_func
            screen_func.blit(disabled_image, (self.rect.x, self.rect.y))
        # если кнопка не нажата:
        elif not self.is_active:
            # создаём изображение и указываем через параметр
            # pygame.SRCALPHA, что у него будут уровни прозрачности
            inactive_image = pygame.Surface((BUTTON_SIZE, BUTTON_SIZE), pygame.SRCALPHA)
            # копируем исходное изображение кнопки
            inactive_image.blit(self.image, (0, 0))
            # накладываем на исходное изображение полупрозрачный серый цвет
            inactive_image.fill((100, 100, 100, 150), special_flags=pygame.BLEND_RGBA_MULT)
            # метод blit рисует новый объект на изображении screen_func
            screen_func.blit(inactive_image, (self.rect.x, self.rect.y))
        # если кнопка активна, рисуем оригинальное изображение без затемнения
        # это создаёт эффект горящей кнопки
        else:
            # метод blit рисует новый объект на изображении screen_func
            screen_func.blit(self.image, (self.rect.x, self.rect.y))

Следующий шаг — добавим проверку нажатия.

Если кнопка неактивна, в рабочем состоянии и на неё нажимает пользователь, запускаем действие action, внутри которого на самом деле может быть что угодно. Сейчас это запуск лифта, но мы можем придумать любой алгоритм и передать его в кнопку. Тогда, как только пользователь нажмёт на кнопку, будет запущен этот алгоритм.

Пишем код проверки перед нажатием:

# метод проверки нажатия кнопки
def check_click(self, pos):
   # проверяем, активна ли кнопка, включена и нажал ли на неё пользователь
   if not self.is_active and self.enabled and self.rect.collidepoint(pos):
       # если пользователь кликнул на неактивную работающую
       # кнопку, включаем указанную при создании функцию
       self.action()
       # активируем кнопку после нажатия
       self.is_active = True

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

Метод состоит из одной строки, в которой следует указать нужное состояние кнопки.

# метод для управления состоянием кнопки — нажата или нет
def set_active(self, state):
   # устанавливаем True или False
   self.is_active = state

# метод для управления состоянием кнопки — работает или нет
def set_enabled(self, state):
   # устанавливаем True или False
   self.enabled = state

После кнопок идёт класс этажей, его оставляем без изменений. 

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

# класс Этажей
# создаёт этаж с кнопками для вызова двух лифтов
# их координаты зависят от номера этажа (y)
class Floor:
   # конструктор __init__ создаёт этаж
   def __init__(self, y, elevator1, elevator2):
       # передаём номер этажа
       self.y = y
       # передаём на каждый этаж по два объекта кнопок,
       # размещаем на нужных местах соответственно этажу
       # и привязываем к нужному лифту через лямбда-функцию
       self.buttons = [
           Button(110, y + FLOOR_HEIGHT // 2 - BUTTON_SIZE // 2, button1_elevator1, lambda: elevator1.set_target(y)),
           Button(335, y + FLOOR_HEIGHT // 2 - BUTTON_SIZE // 2, button1_elevator2, lambda: elevator2.set_target(y)),
       ]

   # метод отрисовки этажа и кнопки
   def draw(self, screen_func):
       # рисуем линию на главном экране с толщиной 1
       # от нулевой координаты до конца
       # экрана по оси х и на высоте этажа y
       pygame.draw.line(screen, BLACK, (0, self.y), (SCREEN_WIDTH, self.y), 1)
       # рисуем кнопки на каждом этаже
       for button in self.buttons:
           button.draw(screen_func)

Добавляем в лифты очередь и работу с отключёнными кнопками

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

Сначала добавляем в класс очередь вызванных этажей — это будет список для сохранения вызовов. Каждый раз при нажатии кнопки номер этажа добавляется в список. После этого сделаем так, чтобы одна и та же кнопка не могла добавиться повторно, если лифт ещё не доехал до этажа.

Код для очереди:

# класс Лифта
class Elevator:
   # определяем лифт с координатами, размерами и изображением
   def __init__(self, x, y, width, height, image):
       # начальные координаты
       self.x = x
       self.y = y
       # размеры лифта
       self.width = width
       self.height = height
       # изображение
       self.image = image
       # добавляем очередь для нажатых кнопок
       self.queue = []
       # скорость лифта
       self.speed = 2

Сразу добавим проверку, что кнопка вызывает лифт только один раз — и если этаж уже в очереди, то ничего не произойдёт:

# метод для установки этажа, куда должен ехать лифт
def set_target(self, y):
   # если этаж уже не находится в списке, добавляем его туда
   if y not in self.queue:
       self.queue.append(y)

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

# Метод для движения лифтов
def move(self):
   # проверяем, есть ли в очереди этажи для передвижения
   if not self.queue:
       # если в очереди-списке нет этажей, то есть
       # нет нажатых кнопок, метод сразу завершается
       return

Если же в этом списке что-то есть, берём первый этаж в очереди. На всякий случай напомним, что нумерация элементов в Python начинается с нуля:

# берём первый элемент в очереди, к которому должен ехать лифт
target_y = self.queue[0]

Движение работает так:

  • если лифт ниже вызываемого этажа, увеличиваем координату по вертикальной оси, и лифт едет вверх;
  • если выше — уменьшаем, и лифт опускается.

Координата изменяется каждый раз на величину скорости. Если скорость увеличить, лифт будет менять координаты быстрее, но менее плавно. Запускаем лифт:

# берём первый элемент в очереди, к которому должен ехать лифт
target_y = self.queue[0]
# если лифт ниже целевого этажа, едем вверх,
# то есть увеличиваем координату y
if self.y < target_y:
   self.y += self.speed
   if self.y > target_y:
       self.y = target_y
# если лифт выше целевого этажа, едем вниз,
# то есть уменьшаем координату y
elif self.y > target_y:
   self.y -= self.speed
   if self.y < target_y:
       self.y = target_y

Когда лифт приехал в пункт назначения, убираем этаж из очереди и деактивируем кнопку вызова на этаже. Визуально она «гаснет» и её снова можно будет нажать:

# как только лифт достиг целевого этажа,
# убираем элемент из списка командой .pop()
if self.y == target_y:
   self.queue.pop(0)
   # проверяем кнопки...
   for floor in system.floors:
       # и деактивируем кнопку на текущем этаже
       if floor.y == target_y:
           for button in floor.buttons:
               button.set_active(False)

Последний метод для лифта — отображение на главном экране визуализации:

# метод для отрисовки лифтов на главном экране
def draw(self, screen_func):
   screen_func.blit(self.image, (self.x, self.y))

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

ТЕСТОВАЯ ФУНКЦИЯ ДЛЯ ЗАПУСКА (программисты так иногда делают для проверки того, что получилось)

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

def draw_floors_demo():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("Этажи и кнопки — Демонстрация")
    clock = pygame.time.Clock()

    # создаём тестовые данные
    elevator1 = Elevator(150, SCREEN_HEIGHT - FLOOR_HEIGHT, FREIGHT_ELEVATOR_WIDTH, ELEVATOR_HEIGHT, freight_elevator_img)
    elevator2 = Elevator(255, SCREEN_HEIGHT - FLOOR_HEIGHT, ELEVATOR_WIDTH, ELEVATOR_HEIGHT, elevator_img)
    floors = [Floor(SCREEN_HEIGHT - FLOOR_HEIGHT * (i + 1), elevator1, elevator2) for i in range(FLOOR_COUNT)]

    running = True
    while running:
        screen.blit(back_img, (0, 0))
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        # рисуем этажи и кнопки
        for floor in floors:
            floor.draw(screen)

        pygame.display.flip()
        clock.tick(30)

    pygame.quit()

# вызов функции для демонстрации
draw_floors_demo()

Сейчас при запуске тестовой функции у нас есть всё, кроме лифтов. Видно, что кнопки полупрозрачные, то есть неактивные. Нажать их пока не получается, отключить какую-нибудь из них — тоже:

Подключаем новые функции в систему лифтов

У нас есть класс ElevatorSystem, который связывает между собой остальные элементы системы. В конструкторе класса мы создаём два лифта и массив этажей с кнопками. На экране появляются все элементы, но пока ничего не движется. Напомним этот фрагмент кода:

# класс для создания всей Системы Лифтов
class ElevatorSystem:
   def __init__(self):
       # cоздаём два лифта: грузовой и пассажирский
       self.elevator1 = Elevator(150, SCREEN_HEIGHT - FLOOR_HEIGHT,
                                 FREIGHT_ELEVATOR_WIDTH, ELEVATOR_HEIGHT, freight_elevator_img)
       self.elevator2 = Elevator(255, SCREEN_HEIGHT - FLOOR_HEIGHT,
                                 ELEVATOR_WIDTH, ELEVATOR_HEIGHT, elevator_img)
       # создаём массив этажей
       self.floors = [
           Floor(SCREEN_HEIGHT - FLOOR_HEIGHT * (i + 1), self.elevator1, self.elevator2)
           for i in range(FLOOR_COUNT)
       ]

Сейчас мы создадим два новых метода для отключения кнопок на любом этаже. В этих методах сначала будем проверять номера отключаемых этажей. Эти проверки не пропустят отключение минус первого этажа или 12-го, если у нас их всего 11. Если передать правильный номер лифта и этажа, метод вызовет нужный метод кнопки для явного управления состоянием и скажет, что кнопку нужно включить или отключить.

# Отключает кнопку для конкретного лифта на указанном этаже
def disable_elevator_button(self, floor_index, elevator_index):
   # если выбранный этаж находится в пределах допустимого количества этажей...
   if 0 <= floor_index < len(self.floors) and 0 <= elevator_index < len(self.floors[floor_index].buttons):
       # выключаем кнопку
       self.floors[floor_index].buttons[elevator_index].set_enabled(False)

# Включает кнопку для конкретного лифта на указанном этаже
def enable_elevator_button(self, floor_index, elevator_index):
   # если выбранный этаж находится в пределах допустимого количества этажей...
   if 0 <= floor_index < len(self.floors) and 0 <= elevator_index < len(self.floors[floor_index].buttons):
       # включаем кнопку
       self.floors[floor_index].buttons[elevator_index].set_enabled(True)

Теперь можно обновлять состояние обоих лифтов и рисовать этажи и лифты на нужных позициях через уже существующие методы класса:

# метод обновления положения лифтов, созданный в классе Elevator
def update(self):
   self.elevator1.move()
   self.elevator2.move()

# метод отрисовки объектов
def draw(self, screen_func):
   # рисуем этажи через метод draw класса Floor
   for floor in self.floors:
       floor.draw(screen_func)
   # рисуем лифты через метод draw класса Elevator
   self.elevator1.draw(screen_func)
   self.elevator2.draw(screen_func)

Обновляем основную функцию

Для запуска визуализации и работы всей системы у нас есть основная функция. 

В её начале мы задаём основные настройки из pygame: указываем частоту обновления экрана, флаг для маркировки запуска главного цикла и переменную с объектом системы лифтов. Ещё для удобства сделаем переменную с системой лифтов доступной из любых мест программы. Для этого объявляем для неё глобальную область видимости:

# основная функция
def main():
   # объект частоты обновления экрана
   clock = pygame.time.Clock()
   # объявляем глобальную область видимости для переменной
   global system
   # создаём объект класса Системы Лифтов
   system = ElevatorSystem()
   # флаг-метка для работы главного цикла
   running = True

Добавляем основные проверки:

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

# пока флаг-метка равен True, работает цикл всей системы
while running:
   # отрисовываем фон
   screen.blit(back_img, (0, 0))

   # проверяем список событий при запущенной программе
   for event in pygame.event.get():
       # если пользователь закрыл главное окно...
       if event.type == pygame.QUIT:
           # завершаем цикл
           running = False
       # если пользователь кликнул мышкой...
       elif event.type == pygame.MOUSEBUTTONDOWN:
           # сохраняем координаты клика в переменную pos
           pos = pygame.mouse.get_pos()
           # проверяем все этажи и кнопки для проверки,
           # совпадают ли координаты клика с одной из кнопок
           for floor in system.floors:
               for button in floor.buttons:
                   button.check_click(pos)

Чтобы отключить или включить лифт на определённом этаже, это нужно прямо указать в коде. У нас уже есть проверка номера этажа, поэтому если указать несуществующий номер этажа или лифта, методы отключения и включения просто не сработают:

# Пример отключения кнопки для пассажирского лифта на 3-м этаже
elif event.type == pygame.KEYDOWN:
   if event.key == pygame.K_1:  # Клавиша "1" отключает пассажирский лифт на 3-м этаже
       system.disable_elevator_button(2, 1)  # Отключить кнопку пассажирского лифта

   elif event.key == pygame.K_2:  # Клавиша "2" включает пассажирский лифт на 3-м этаже
       system.enable_elevator_button(2, 1)  # Включить кнопку пассажирского лифта

Чтобы визуализация pygame работала, нужно сделать ещё несколько вещей:

  • Обновить состояние системы после каждой итерации основного цикла — это будет запускать методы движения для обоих лифтов. Если очередь вызова пуста, лифты останутся на месте. 
  • Отобразить лифты на новых позициях в каждом новом кадре, если они движутся.
  • Обновить изображение для пользователя.
  • Установить скорость обновления кадров.

Добавляем:

# обновляет состояние лифтов
system.update()
# отрисовывает лифты на нужных позициях
system.draw(screen)
# обновляем изображение для пользователя, создавая анимацию
pygame.display.flip()
# устанавливаем скорость обновления кадров до 30 в секунду
clock.tick(30)

Когда главный цикл функции завершился, отключаем главное окно визуализации:

# завершаем работу после окончания цикла
pygame.quit()

Осталось запустить основную функцию — и можно управлять лифтами:

# запускаем главную функцию
main()

Это всё?

Нет, до идеальной лифтовой системы ещё далеко. Вот чего не хватает:

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

Добавим эти возможности в следующий раз, а вы пока подпишитесь, чтобы не пропустить продолжение.

# импортируем pygame
import pygame

# Константы
SCREEN_WIDTH = 470
SCREEN_HEIGHT = 770
FLOOR_COUNT = 11
FREIGHT_ELEVATOR_WIDTH = 75
ELEVATOR_WIDTH = 50
ELEVATOR_HEIGHT = 70
BUTTON_SIZE = 20
FLOOR_HEIGHT = 70
BLACK = (0, 0, 0)

# Инициализация Pygame
pygame.init()
# создаём объект для главного экрана
# с указанием ширины и высоты
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# указываем название для главного окна
pygame.display.set_caption("Два лифта")

# загружаем изображения
# картинка для лифта
elevator_img = pygame.image.load("elevator.jpg").convert_alpha()
# картинка для красной кнопки
button1_elevator1 = pygame.image.load("button1_elevator1.png").convert_alpha()
# картинка для синей кнопки
button1_elevator2 = pygame.image.load("button1_elevator2.png").convert_alpha()
# картинка для грузового лифта
freight_elevator_img = pygame.image.load("freight_elevator.jpg").convert_alpha()
# картинка для фона
back_img = pygame.image.load("back.jpg").convert_alpha()

# масштабируем изображения под заданные в константах размеры
elevator_img = pygame.transform.scale(elevator_img, (ELEVATOR_WIDTH, ELEVATOR_HEIGHT))
button1_elevator1 = pygame.transform.scale(button1_elevator1, (BUTTON_SIZE, BUTTON_SIZE))
button1_elevator2 = pygame.transform.scale(button1_elevator2, (BUTTON_SIZE, BUTTON_SIZE))
freight_elevator_img = pygame.transform.scale(freight_elevator_img, (FREIGHT_ELEVATOR_WIDTH, ELEVATOR_HEIGHT))
back_img = pygame.transform.scale(back_img, (SCREEN_WIDTH, SCREEN_HEIGHT))


# класс Кнопок вызова
class Button:
   # конструктор __init__ создаёт кнопку
   def __init__(self, x, y, image, action, enabled=True):
       # рисуем прямоугольник с заданными координатами
       self.rect = pygame.Rect(x, y, BUTTON_SIZE, BUTTON_SIZE)
       # в прямоугольник вписываем изображение
       self.image = image
       # привязываем кнопку к функции, которую укажем при создании кнопки
       self.action = action
       # свойство - нажата кнопка или нет
       self.is_active = False
       # свойство - отключена кнопка или нет
       self.enabled = enabled

   # метод для отрисовки кнопки на главном экране с заданными координатами
   def draw(self, screen_func):
       # если кнопка отключена:
       if not self.enabled:
           # создаём изображение и указываем через параметр
           # pygame.SRCALPHA, что у него будут уровни прозрачности
           disabled_image = pygame.Surface((BUTTON_SIZE, BUTTON_SIZE), pygame.SRCALPHA)
           # копируем исходное изображение кнопки
           disabled_image.blit(self.image, (0, 0))
           # накладываем на исходное изображение тёмный полупрозрачный серый цвет
           disabled_image.fill((50, 50, 50, 200), special_flags=pygame.BLEND_RGBA_MULT)
           # метод blit рисует новый объект на изображении screen_func
           screen_func.blit(disabled_image, (self.rect.x, self.rect.y))
       # если кнопка не нажата:
       elif not self.is_active:
           # создаём изображение и указываем через параметр
           # pygame.SRCALPHA, что у него будут уровни прозрачности
           inactive_image = pygame.Surface((BUTTON_SIZE, BUTTON_SIZE), pygame.SRCALPHA)
           # копируем исходное изображение кнопки
           inactive_image.blit(self.image, (0, 0))
           # накладываем на исходное изображение полупрозрачный серый цвет
           inactive_image.fill((100, 100, 100, 150), special_flags=pygame.BLEND_RGBA_MULT)
           # метод blit рисует новый объект на изображении screen_func
           screen_func.blit(inactive_image, (self.rect.x, self.rect.y))
       # если кнопка активна, рисуем оригинальное изображение без затемнения
       # это создаёт эффект "горящей" кнопки
       else:
           # метод blit рисует новый объект на изображении screen_func
           screen_func.blit(self.image, (self.rect.x, self.rect.y))

   # метод проверки нажатия кнопки
   def check_click(self, pos):
       # проверяем, активна ли кнопка, включена и нажал ли на неё пользователь
       if not self.is_active and self.enabled and self.rect.collidepoint(pos):
           # если пользователь кликнул на неактивную работающую
           # кнопку, включаем указанную при создании функцию
           self.action()
           # активируем кнопку после нажатия
           self.is_active = True

   # метод для управления состоянием кнопки - нажата или нет
   def set_active(self, state):
       # устанавливаем True или False
       self.is_active = state

   # метод для управления состоянием кнопки - работает или нет
   def set_enabled(self, state):
       # устанавливаем True или False
       self.enabled = state


# класс Этажей
# создаёт этаж с кнопками для вызова двух лифтов.
# их координаты зависят от номера этажа (y)
class Floor:
   # конструктор __init__ создаёт этаж
   def __init__(self, y, elevator1, elevator2):
       # передаём номер этажа
       self.y = y
       # передаём на каждый этаж по два объекта кнопок,
       # размещаем на нужных местах соответственно этажу
       # и привязываем к нужному лифту через лямбда-функцию
       self.buttons = [
           Button(110, y + FLOOR_HEIGHT // 2 - BUTTON_SIZE // 2, button1_elevator1, lambda: elevator1.set_target(y)),
           Button(335, y + FLOOR_HEIGHT // 2 - BUTTON_SIZE // 2, button1_elevator2, lambda: elevator2.set_target(y)),
       ]

   # метод отрисовки этажа и кнопки
   def draw(self, screen_func):
       # рисуем линию на главном экране с толщиной 1
       # от нулевой координаты до конца
       # экрана по оси х и на высоте этаже y
       pygame.draw.line(screen, BLACK, (0, self.y), (SCREEN_WIDTH, self.y), 1)
       # рисуем кнопки на каждом этаже
       for button in self.buttons:
           button.draw(screen_func)


# класс Лифта
class Elevator:
   # определяем лифт с координатами, размерами и изображением
   def __init__(self, x, y, width, height, image):
       # начальные координаты
       self.x = x
       self.y = y
       # размеры лифта
       self.width = width
       self.height = height
       # изображение
       self.image = image
       # добавляем очередь для нажатых кнопок
       self.queue = []
       # скорость лифта
       self.speed = 2

   # метод для установки этажа, куда должен ехать лифт
   def set_target(self, y):
       # если этаж уже не находится в списке, добавляем его туда
       if y not in self.queue:
           self.queue.append(y)

   # Метод для движения лифтов
   def move(self):
       # проверяем, есть ли в очереди этажи для передвижения
       if not self.queue:
           # если в очереди-списке нет этажей, то есть
           # нет нажатых кнопок, метод сразу завершается
           return

       # берём первый элемент в очереди, к которому должен ехать лифт
       target_y = self.queue[0]
       # если лифт ниже целевого этажа, едем вверх,
       # то есть увеличиваем координату y
       if self.y < target_y:
           self.y += self.speed
           if self.y > target_y:
               self.y = target_y
       # если лифт выше целевого этажа, едем вниз,
       # то есть уменьшаем координату y
       elif self.y > target_y:
           self.y -= self.speed
           if self.y < target_y:
               self.y = target_y

       # как только лифт достиг целевого этажа,
       # убираем элемент из списка командой .pop()
       if self.y == target_y:
           self.queue.pop(0)
           # проверяем кнопки...
           for floor in system.floors:
               # и деактивируем кнопку на текущем этаже
               if floor.y == target_y:
                   for button in floor.buttons:
                       button.set_active(False)

   # метод для отрисовки лифтов на главном экране
   def draw(self, screen_func):
       screen_func.blit(self.image, (self.x, self.y))


# класс для создания всей Системы Лифтов
class ElevatorSystem:
   def __init__(self):
       # cоздаём два лифта: грузовой и пассажирский
       self.elevator1 = Elevator(150, SCREEN_HEIGHT - FLOOR_HEIGHT,
                                 FREIGHT_ELEVATOR_WIDTH, ELEVATOR_HEIGHT, freight_elevator_img)
       self.elevator2 = Elevator(255, SCREEN_HEIGHT - FLOOR_HEIGHT,
                                 ELEVATOR_WIDTH, ELEVATOR_HEIGHT, elevator_img)
       # создаём массив этажей
       self.floors = [
           Floor(SCREEN_HEIGHT - FLOOR_HEIGHT * (i + 1), self.elevator1, self.elevator2)
           for i in range(FLOOR_COUNT)
       ]

   # Отключает кнопку для конкретного лифта на указанном этаже
   def disable_elevator_button(self, floor_index, elevator_index):
       # если выбранный этаж находится в пределах допустимного количества этажей...
       if 0 <= floor_index < len(self.floors) and 0 <= elevator_index < len(self.floors[floor_index].buttons):
           # включаем кнопку
           self.floors[floor_index].buttons[elevator_index].set_enabled(False)

   # Включает кнопку для конкретного лифта на указанном этаже
   def enable_elevator_button(self, floor_index, elevator_index):
       # если выбранный этаж находится в пределах допустимного количества этажей...
       if 0 <= floor_index < len(self.floors) and 0 <= elevator_index < len(self.floors[floor_index].buttons):
           # включаем кнопку
           self.floors[floor_index].buttons[elevator_index].set_enabled(True)

   # метод обновления положения лифтов, созданный в классе Elevator
   def update(self):
       self.elevator1.move()
       self.elevator2.move()

   # метод отрисовки объектов
   def draw(self, screen_func):
       # рисуем этажи через метод draw класса Floor
       for floor in self.floors:
           floor.draw(screen_func)
       # рисуем лифты через метод draw класса Elevator
       self.elevator1.draw(screen_func)
       self.elevator2.draw(screen_func)


# основная функция
def main():
   # объект частоты обновления экрана
   clock = pygame.time.Clock()
   # объявляем глобальную область видимости для переменной
   global system
   # создаём объект класса Системы Лифтов
   system = ElevatorSystem()
   # флаг-метка для работы главного цикла
   running = True

   # пока флаг-метка равен True, работает цикл всей системы
   while running:
       # отрисовываем фон
       screen.blit(back_img, (0, 0))

       # проверяем список событий при запущенной программе
       for event in pygame.event.get():
           # если пользователь закрыл главное окно...
           if event.type == pygame.QUIT:
               # завершаем цикл
               running = False
           # если пользователь кликнул мышкой...
           elif event.type == pygame.MOUSEBUTTONDOWN:
               # сохраняем координаты клика в переменную pos
               pos = pygame.mouse.get_pos()
               # проверяем все этажи и кнопки для проверки,
               # совпадают ли координаты клика с одной из кнопок
               for floor in system.floors:
                   for button in floor.buttons:
                       button.check_click(pos)

           # Пример отключения кнопки для пассажирского лифта на 3-м этаже
           elif event.type == pygame.KEYDOWN:
               if event.key == pygame.K_1:  # Клавиша "1" отключает пассажирский лифт на 3-м этаже
                   system.disable_elevator_button(2, 1)

               elif event.key == pygame.K_2:  # Клавиша "2" включает пассажирский лифт на 3-м этаже
                   system.enable_elevator_button(2, 1)

       # обновляет состояние лифтов
       system.update()
       # отрисовывает лифты на нужных позициях
       system.draw(screen)
       # обновляем изображение для пользователя, создавая анимацию
       pygame.display.flip()
       # устанавливаем скорость обновления кадров до 30 в секунду
       clock.tick(30)

   # завершаем работу после окончания цикла
   pygame.quit()


# запускаем главную функцию
main()

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
hard
[anycomment]
Exit mobile version