--- /dev/null
+#!/usr/bin/env python3
+
+import sys
+from functools import partial
+from pathlib import Path
+from time import time
+
+import pygame
+
+
+def scale(a, b):
+ return (int(a[0] * b[0]), int(a[1] * b[1]))
+
+
+class Door:
+ OPEN = 0
+ CLOSED = 100
+ BACKGROUND = "black"
+ FOREGROUND = "brown"
+ BUTTON_COLOR = "black"
+ DOOR_MARGIN = (0.01, 0.05)
+ PADDING = (0.014, 0.022)
+ WIDTH = 0.3
+
+ DOOR_GAP = 0.01
+ BUTTON_X_OFF = 1.02
+
+ DEFAULT_STATE = CLOSED
+ OPEN_TIMEOUT_SEC = 5
+
+ def __init__(self, rect):
+ self.rect = rect
+ self.button_rect = pygame.Rect(
+ (rect.left + self.BUTTON_X_OFF * rect.width, rect.top),
+ (rect.height // 2, rect.height // 2),
+ )
+ self.state = self.DEFAULT_STATE
+ self.direction = 0
+ self.timeout = None
+
+ @classmethod
+ def get_doors(cls, num_levels):
+ pad = ElevatorApp.scale(cls.PADDING)
+ size = ElevatorApp.scale((cls.WIDTH, 1 / num_levels))
+ return [
+ cls(
+ pygame.Rect(
+ (pad[0], size[1] * i + pad[1]),
+ (size[0] - 2 * pad[0], size[1] - 2 * pad[1]),
+ )
+ )
+ for i in range(num_levels)
+ ]
+
+ def open(self):
+ self.direction = -1
+
+ def update(self):
+ dirty = self.direction != 0
+ if self.timeout is not None and time() >= self.timeout:
+ self.direction = -1 if self.state == self.CLOSED else 1
+ self.timeout = None
+ dirty = True
+ self.state += self.direction
+ if self.direction:
+ if self.state <= self.OPEN:
+ self.state = self.OPEN
+ self.direction = 0
+ self.timeout = time() + self.OPEN_TIMEOUT_SEC
+ elif self.state >= self.CLOSED:
+ self.state = self.CLOSED
+ self.direction = 0
+ return dirty
+
+ def draw(self, surf):
+ subsurf = surf.subsurface(self.rect)
+ subsurf.fill(self.BACKGROUND)
+ door_scale = partial(scale, subsurf.get_size())
+ left_open = self.DOOR_MARGIN[0]
+ left_closed = (1 - self.DOOR_GAP) / 2
+ right_closed = (1 + self.DOOR_GAP) / 2
+ right_open = 1 - self.DOOR_MARGIN[0]
+
+ door_size = (
+ (1 - 2 * self.DOOR_MARGIN[0] - self.DOOR_GAP) / 2,
+ 1 - 2 * self.DOOR_MARGIN[1],
+ )
+
+ state_fract = self.state / self.CLOSED
+ left_current = left_open + (left_closed - left_open) * state_fract
+ right_current = right_open - (right_open - right_closed) * state_fract
+
+ if left_current < door_size[0]:
+ rect = pygame.Rect(
+ door_scale((0, self.DOOR_MARGIN[1])),
+ door_scale((left_current, door_size[1])),
+ )
+ else:
+ rect = pygame.Rect(
+ door_scale((left_current - door_size[0], self.DOOR_MARGIN[1])),
+ door_scale(door_size),
+ )
+ pygame.draw.rect(subsurf, self.FOREGROUND, rect)
+
+ if right_current > (1 - door_size[0]):
+ rect = pygame.Rect(
+ door_scale((right_current, self.DOOR_MARGIN[1])),
+ door_scale((1 - right_current, door_size[1])),
+ )
+ else:
+ rect = pygame.Rect(
+ door_scale((right_current, self.DOOR_MARGIN[1])),
+ door_scale(door_size),
+ )
+ pygame.draw.rect(subsurf, self.FOREGROUND, rect)
+ pygame.draw.rect(surf, self.BUTTON_COLOR, self.button_rect)
+
+
+class Elevator:
+ MARGIN_Y = 0.022
+ OFF_X = 0.37
+ WIDTH = 0.05
+ CABIN_MARGINS = (0.005, 0.01)
+ BACKGROUND = "black"
+ CABIN_COLOR = "darkorange3"
+ CABLE_COLOR = "yellow"
+ LEVEL_STEPS = 100000
+
+ def __init__(self, num_levels):
+ self.num_levels = num_levels
+ self.current_level = 0
+ self.destination = None
+
+ def update(self):
+ if self.destination is None:
+ return False
+ dest = self.destination * self.LEVEL_STEPS
+ self.current_level = (min if dest > self.current_level else max)(
+ dest,
+ self.current_level + 800 * (
+ 1 if dest > self.current_level else -1
+ )
+ )
+ return True
+
+ def draw(self, surf):
+ shaft_height = 1 - 2 * self.MARGIN_Y
+ cabin_height = shaft_height / self.num_levels
+ rect = pygame.Rect(
+ ElevatorApp.scale((self.OFF_X, self.MARGIN_Y)),
+ ElevatorApp.scale((self.WIDTH, shaft_height)),
+ )
+ cabin_rect = pygame.Rect(
+ ElevatorApp.scale(
+ (
+ self.OFF_X + self.CABIN_MARGINS[0],
+ self.MARGIN_Y + self.CABIN_MARGINS[1] + cabin_height * (
+ self.num_levels - 1 -
+ self.current_level / self.LEVEL_STEPS
+ ),
+ )
+ ),
+ ElevatorApp.scale(
+ (
+ self.WIDTH - 2 * self.CABIN_MARGINS[0],
+ cabin_height - 2 * self.CABIN_MARGINS[1],
+ )
+ ),
+ )
+ pygame.draw.rect(surf, self.BACKGROUND, rect)
+ pygame.draw.rect(surf, self.CABIN_COLOR, cabin_rect)
+ pygame.draw.line(surf, self.CABLE_COLOR, rect.midtop, cabin_rect.midtop)
+
+
+class ElevatorPanel:
+ OUTER_RECT = ((0.53, 0.1), (0.4, 0.8))
+ OUTER_COLOR = "darkorange2"
+ INNER_COLOR = "burlywood4"
+ INNER_SIZE = (0.8, 0.8)
+ BUTTON_COLOR = "navajowhite"
+ LABEL_COLOR = "black"
+ ALARM_COLOR = "goldenrod"
+ BUTTON_SIZE = 0.9
+
+ def __init__(self, num_levels):
+ self.num_levels = num_levels
+ self.outer_rect = pygame.Rect(
+ ElevatorApp.scale(self.OUTER_RECT[0]),
+ ElevatorApp.scale(self.OUTER_RECT[1]),
+ )
+ inner_margin = ((1 - self.INNER_SIZE[0]) / 2, (1 - self.INNER_SIZE[1]) / 2)
+ self.inner_rect = pygame.Rect(
+ (
+ self.outer_rect.left + self.outer_rect.width * inner_margin[0],
+ self.outer_rect.top + self.outer_rect.height * inner_margin[1],
+ ),
+ scale(self.outer_rect.size, self.INNER_SIZE)
+ )
+ num_buttons = self.num_levels + 1
+ button_outer_size = int(self.inner_rect.height / num_buttons)
+ button_inner_size = int(self.inner_rect.height * self.BUTTON_SIZE / num_buttons)
+ button_space = button_outer_size - button_inner_size
+ self.font = pygame.font.Font(None, size=button_outer_size * 2 // 3)
+ self.buttons = [
+ pygame.Rect(
+ (
+ self.inner_rect.centerx - button_inner_size // 2,
+ self.inner_rect.top
+ + int((self.inner_rect.height - button_space) / num_buttons) * (num_buttons - 1 - i)
+ + button_space
+ ),
+ (button_inner_size, button_inner_size),
+ ) for i in range(num_buttons)
+ ]
+ last_topleft = self.buttons[0].topleft
+ self.buttons.extend(
+ (
+ pygame.Rect(
+ (last_topleft[0] - button_outer_size, last_topleft[1]),
+ (button_inner_size, button_inner_size)
+ ),
+ pygame.Rect(
+ (last_topleft[0] + button_outer_size, last_topleft[1]),
+ (button_inner_size, button_inner_size)
+ ),
+ )
+ )
+
+ def draw(self, surf):
+ pygame.draw.rect(surf, self.OUTER_COLOR, self.outer_rect)
+ pygame.draw.rect(surf, self.INNER_COLOR, self.inner_rect)
+ for i, button in enumerate(self.buttons):
+ pygame.draw.rect(surf, self.BUTTON_COLOR, button)
+ if i == 0 or i > self.num_levels:
+ continue
+ fs = self.font.render(str(i - 1), True, self.LABEL_COLOR)
+ fs_size = fs.get_size()
+ surf.blit(
+ fs,
+ (button.centerx - fs_size[0] // 2, button.centery - fs_size[1] // 2)
+ )
+ alarm = self.buttons[0]
+ bell_orig = pygame.image.load(Path(__file__).parent / "assets" / "bell.png")
+ bell = pygame.transform.scale(bell_orig, (alarm.width // 2, alarm.height // 2))
+ bell_rect = bell.get_rect()
+ surf.blit(
+ bell, (alarm.centerx - bell_rect.centerx, alarm.centery - bell_rect.centery)
+ )
+ for button, s in zip(self.buttons[self.num_levels + 1:], ("<|>", ">|<")):
+ fs = self.font.render(s, True, self.LABEL_COLOR)
+ fs_size = fs.get_size()
+ surf.blit(
+ fs,
+ (button.centerx - fs_size[0] // 2, button.centery - fs_size[1] // 2)
+ )
+
+
+class ElevatorApp:
+ FPS = 60
+ SET_MODE_KWARGS = {
+ "flags": pygame.FULLSCREEN,
+ #"size": (1024, 768),
+ }
+ BACKGROUND = "green4"
+
+ def __init__(self):
+ pygame.init()
+ pygame.display.set_mode(**self.SET_MODE_KWARGS)
+ pygame.display.set_caption(sys.argv[0])
+ self.running = True
+ self.dirty = False
+ self.clock = pygame.time.Clock()
+ self.num_levels = 5
+ self.doors = Door.get_doors(self.num_levels)
+ self.elevator = Elevator(self.num_levels)
+ self.elevator_panel = ElevatorPanel(self.num_levels)
+ self.last_pos = None
+
+ @staticmethod
+ def scale(value):
+ size = pygame.display.get_surface().get_size()
+ return int(size[0] * value[0]), int(size[1] * value[1])
+
+ def handle_event(self, ev):
+ if ev.type == pygame.QUIT:
+ self.running = False
+ elif ev.type == pygame.KEYDOWN:
+ if ev.key == pygame.K_ESCAPE:
+ self.running = False
+ elif ev.type == pygame.MOUSEBUTTONDOWN:
+ if ev.button == 1:
+ for i, door in enumerate(self.doors):
+ if door.button_rect.collidepoint(ev.pos):
+ self.elevator.destination = self.num_levels - 1 - i
+ self.last_pos = ev.pos
+ elif ev.type == pygame.MOUSEBUTTONUP:
+ self.last_pos = ev.pos
+ elif ev.type == pygame.MOUSEMOTION:
+ self.last_pos = ev.pos
+ elif ev.type == pygame.WINDOWEXPOSED:
+ self.dirty = True
+
+ def update(self):
+ for door in self.doors:
+ self.dirty |= door.update()
+ self.dirty |= self.elevator.update()
+
+ def draw(self):
+ surf = pygame.display.get_surface()
+ surf.fill(self.BACKGROUND)
+ for door in self.doors:
+ door.draw(surf)
+ self.elevator.draw(surf)
+ self.elevator_panel.draw(surf)
+ pygame.display.update()
+
+ def run(self):
+ while True:
+ for ev in pygame.event.get():
+ self.handle_event(ev)
+ if not self.running:
+ return
+ self.update()
+ if self.dirty:
+ self.draw()
+ self.dirty = False
+ self.clock.tick(self.FPS)
+
+
+if __name__ == "__main__":
+ ElevatorApp().run()