From 5285e2aae0ea9e3d1b31dcb3d990e711b49ab77e Mon Sep 17 00:00:00 2001 From: mar77i Date: Mon, 19 Aug 2024 23:31:42 +0200 Subject: [PATCH] fully functioning elevator --- elevator.py | 294 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 198 insertions(+), 96 deletions(-) diff --git a/elevator.py b/elevator.py index 3007e4b..70b8b57 100755 --- a/elevator.py +++ b/elevator.py @@ -1,15 +1,90 @@ #!/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: @@ -19,42 +94,45 @@ 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: @@ -73,15 +151,15 @@ class Door: 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], ) @@ -91,29 +169,34 @@ class Door: 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: @@ -125,76 +208,91 @@ 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) @@ -202,12 +300,12 @@ class ElevatorPanel: 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) @@ -215,11 +313,11 @@ class ElevatorPanel: 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) ), @@ -239,13 +337,10 @@ class ElevatorPanel: 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() @@ -258,29 +353,24 @@ class ElevatorPanel: 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 @@ -289,9 +379,21 @@ class ElevatorApp: 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 @@ -301,14 +403,14 @@ class ElevatorApp: 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) -- 2.47.0