]> git.mar77i.info Git - elevator/commitdiff
fully functioning elevator
authormar77i <mar77i@protonmail.ch>
Mon, 19 Aug 2024 21:31:42 +0000 (23:31 +0200)
committermar77i <mar77i@protonmail.ch>
Mon, 19 Aug 2024 21:31:42 +0000 (23:31 +0200)
elevator.py

index 3007e4b0b94dd366b5fdbbb50219ad7c20687dfa..70b8b572b854188dc23eeeb6257c1ae8f228c9d3 100755 (executable)
@@ -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)