#!/usr/bin/env python3
import sys
-from functools import partial
-from pathlib import Path
+from dataclasses import dataclass
+from io import BytesIO
+from math import pi, sin
+from struct import pack
from time import time
import pygame
+tau = pi * 2
-def scale(a, b):
- return (int(a[0] * b[0]), int(a[1] * b[1]))
+
+class Vec(pygame.math.Vector2):
+ def __mul__(self, other):
+ return super().elementwise().__mul__(other)
+
+ def __rmul__(self, other):
+ return super().elementwise().__rmul__(other)
+
+
+class Rect(pygame.Rect):
+ def __init__(self, topleft, size):
+ super().__init__(
+ (int(topleft[0]), int(topleft[1])), (int(size[0]), int(size[1]))
+ )
+
+
+@dataclass
+class Circle:
+ color: str
+ center: Vec
+ radius: float
+
+
+@dataclass
+class Polygon:
+ color: str
+ points: list[Vec]
+
+
+def vector_draw(surf, rect, item):
+ if isinstance(item, Circle):
+ pygame.draw.circle(
+ surf,
+ item.color,
+ rect.topleft + item.center * rect.size,
+ item.radius * rect.width,
+ )
+ elif isinstance(item, Polygon):
+ points = [rect.topleft + point * rect.size for point in item.points]
+ pygame.draw.polygon(surf, item.color, points)
+ else:
+ raise ValueError(f"item: {item}")
+
+
+BELL = [
+ Circle("goldenrod4", Vec(0.5, 0.775), 0.1),
+ Polygon(
+ "goldenrod",
+ [
+ Vec(0.3, 0.3),
+ Vec(0.25, 0.7),
+ Vec(0.2, 0.8),
+ Vec(0.8, 0.8),
+ Vec(0.75, 0.7),
+ Vec(0.7, 0.3),
+ ]
+ ),
+ Circle("goldenrod", Vec(0.5, 0.3), 0.2),
+]
+
+
+def beep():
+ if pygame.mixer.get_busy():
+ return
+
+ freq, volume, length_sec = 432 * 2 ** 1.5, 0.125, 1.5
+ SAMPLERATE, FORMAT, CHANNELS = pygame.mixer.get_init()
+
+ bio = BytesIO()
+ assert 0 <= volume <= 1
+ for n in range(int(length_sec * SAMPLERATE)):
+ value = int(sin(n * freq * tau / SAMPLERATE) * 2 ** abs(FORMAT) * volume)
+ bio.write(pack(f"<{'h' * CHANNELS}", *(value,) * CHANNELS))
+ pygame.mixer.find_channel().play(pygame.mixer.Sound(buffer=bio.getvalue()))
class Door:
FOREGROUND = "brown"
BUTTON_COLOR = "black"
DOOR_MARGIN = (0.01, 0.05)
- PADDING = (0.014, 0.022)
+ PADDING = Vec(0.014, 0.022)
WIDTH = 0.3
DOOR_GAP = 0.01
- BUTTON_X_OFF = 1.02
+ BUTTON_OFFSET = Vec(1.02, 0.25)
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.button_rect = Rect(
+ rect.topleft + self.BUTTON_OFFSET * rect.size,
+ Vec(rect.height, rect.height) // 3,
)
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))
+ def get_doors(cls, surf_size, num_levels):
+ pad = cls.PADDING * surf_size
+ size = Vec(cls.WIDTH, 1 / num_levels) * surf_size
return [
- cls(
- pygame.Rect(
- (pad[0], size[1] * i + pad[1]),
- (size[0] - 2 * pad[0], size[1] - 2 * pad[1]),
- )
- )
+ cls(Rect(pad + Vec(0, size[1] * (num_levels - 1 - i)), size - 2 * pad))
for i in range(num_levels)
]
def open(self):
+ if self.direction == 1:
+ return
self.direction = -1
+ def close(self):
+ if self.direction == -1:
+ return
+ self.direction = 1
+ self.timeout = None
+
def update(self):
dirty = self.direction != 0
if self.timeout is not None and time() >= self.timeout:
return dirty
def draw(self, surf):
+ # clean me up!
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 = (
+ door_size = Vec(
(1 - 2 * self.DOOR_MARGIN[0] - self.DOOR_GAP) / 2,
1 - 2 * self.DOOR_MARGIN[1],
)
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])),
+ rect = Rect(
+ Vec(0, self.DOOR_MARGIN[1]) * self.rect.size,
+ Vec(left_current, door_size[1]) * self.rect.size,
)
else:
- rect = pygame.Rect(
- door_scale((left_current - door_size[0], self.DOOR_MARGIN[1])),
- door_scale(door_size),
+ rect = Rect(
+ Vec(left_current - door_size[0], self.DOOR_MARGIN[1]) * self.rect.size,
+ door_size * self.rect.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])),
+ rect = Rect(
+ Vec(right_current, self.DOOR_MARGIN[1]) * self.rect.size,
+ Vec(1 - right_current, door_size[1]) * self.rect.size,
)
else:
- rect = pygame.Rect(
- door_scale((right_current, self.DOOR_MARGIN[1])),
- door_scale(door_size),
+ rect = Rect(
+ Vec(right_current, self.DOOR_MARGIN[1]) * self.rect.size,
+ door_size * self.rect.size,
)
pygame.draw.rect(subsurf, self.FOREGROUND, rect)
- pygame.draw.rect(surf, self.BUTTON_COLOR, self.button_rect)
+ pygame.draw.circle(
+ surf,
+ self.BUTTON_COLOR,
+ self.button_rect.center,
+ self.button_rect.height // 2,
+ )
class Elevator:
CABIN_COLOR = "darkorange3"
CABLE_COLOR = "yellow"
LEVEL_STEPS = 100000
+ SPEED = 800
- def __init__(self, num_levels):
- self.num_levels = num_levels
+ def __init__(self, doors):
+ self.doors = doors
self.current_level = 0
self.destination = None
+ self.queued_dest = None
def update(self):
if self.destination is None:
+ if all(door.state == door.CLOSED for door in self.doors):
+ self.destination = self.queued_dest
+ self.queued_dest = None
return False
dest = self.destination * self.LEVEL_STEPS
+ if dest == self.current_level:
+ state = self.doors[self.destination].state
+ if state == Door.CLOSED:
+ self.doors[self.destination].open()
+ elif state == Door.OPEN:
+ self.destination = None
self.current_level = (min if dest > self.current_level else max)(
dest,
- self.current_level + 800 * (
+ self.current_level + self.SPEED * (
1 if dest > self.current_level else -1
)
)
return True
def draw(self, surf):
+ surf_size = surf.get_size()
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_height = shaft_height / len(self.doors)
+ rect = Rect(
+ surf_size * Vec(self.OFF_X, self.MARGIN_Y),
+ surf_size * Vec(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
- ),
- )
+ cabin_rect = Rect(
+ surf_size * Vec(
+ self.OFF_X + self.CABIN_MARGINS[0],
+ self.MARGIN_Y + self.CABIN_MARGINS[1] + cabin_height * (
+ len(self.doors) - 1
+ - self.current_level / self.LEVEL_STEPS
+ ),
),
- ElevatorApp.scale(
- (
- self.WIDTH - 2 * self.CABIN_MARGINS[0],
- cabin_height - 2 * self.CABIN_MARGINS[1],
- )
+ surf_size * Vec(
+ 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)
+ def goto(self, level):
+ self.queued_dest = level
+
+ def open(self):
+ level, mod = divmod(self.current_level, self.LEVEL_STEPS)
+ if mod == 0:
+ self.doors[level].open()
+
+ def close(self):
+ level, mod = divmod(self.current_level, self.LEVEL_STEPS)
+ if mod == 0:
+ self.doors[level].close()
+
class ElevatorPanel:
- OUTER_RECT = ((0.53, 0.1), (0.4, 0.8))
+ OUTER_RECT = (Vec(0.53, 0.1), Vec(0.4, 0.8))
OUTER_COLOR = "darkorange2"
INNER_COLOR = "burlywood4"
- INNER_SIZE = (0.8, 0.8)
+ INNER_SIZE = Vec(0.8, 0.8)
BUTTON_COLOR = "navajowhite"
LABEL_COLOR = "black"
ALARM_COLOR = "goldenrod"
BUTTON_SIZE = 0.9
- def __init__(self, num_levels):
+ def __init__(self, surf_size, 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)
+ self.outer_rect = Rect(*(v * surf_size for v in self.OUTER_RECT))
+ inner_margin = ((1, 1) - self.INNER_SIZE) / 2
+ self.inner_rect = Rect(
+ self.outer_rect.topleft + inner_margin * self.outer_rect.size,
+ self.INNER_SIZE * self.outer_rect.size,
)
num_buttons = self.num_levels + 1
button_outer_size = int(self.inner_rect.height / 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(
+ 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
+ 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(
+ Rect(
(last_topleft[0] - button_outer_size, last_topleft[1]),
(button_inner_size, button_inner_size)
),
- pygame.Rect(
+ Rect(
(last_topleft[0] + button_outer_size, last_topleft[1]),
(button_inner_size, button_inner_size)
),
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 shape in BELL:
+ vector_draw(surf, self.buttons[0], shape)
+
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()
class ElevatorApp:
FPS = 60
SET_MODE_KWARGS = {
- "flags": pygame.FULLSCREEN,
- #"size": (1024, 768),
+ #"flags": pygame.FULLSCREEN,
+ "size": (1024, 768),
}
BACKGROUND = "green4"
def __init__(self):
pygame.init()
- pygame.display.set_mode(**self.SET_MODE_KWARGS)
+ surf = 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)
+ surf_size = surf.get_size()
+ self.elevator = Elevator(Door.get_doors(surf_size, self.num_levels))
+ self.elevator_panel = ElevatorPanel(surf_size, 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
self.running = False
elif ev.type == pygame.MOUSEBUTTONDOWN:
if ev.button == 1:
- for i, door in enumerate(self.doors):
+ for i, door in enumerate(self.elevator.doors):
if door.button_rect.collidepoint(ev.pos):
- self.elevator.destination = self.num_levels - 1 - i
+ self.elevator.goto(i)
+ break
+ for i, button in enumerate(self.elevator_panel.buttons):
+ if button.collidepoint(ev.pos):
+ if i == 0:
+ beep()
+ elif i - 1 < self.num_levels:
+ self.elevator.goto(i - 1)
+ elif i == self.num_levels + 1:
+ self.elevator.open()
+ elif i == self.num_levels + 2:
+ self.elevator.close()
+ break
self.last_pos = ev.pos
elif ev.type == pygame.MOUSEBUTTONUP:
self.last_pos = ev.pos
self.dirty = True
def update(self):
- for door in self.doors:
+ for door in self.elevator.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:
+ for door in self.elevator.doors:
door.draw(surf)
self.elevator.draw(surf)
self.elevator_panel.draw(surf)