diff --git a/compute.py b/compute.py index 37dede6..d398fcb 100644 --- a/compute.py +++ b/compute.py @@ -59,6 +59,29 @@ def matmul(matrix_a: Matrix, matrix_b: Matrix) -> Matrix: import math +def matvec(a: Matrix, v: list[float]) -> list[float]: + """Return the product of a matrix and a column vector. + + Parameters + ---------- + a: list[list[float]] + Matrix of shape ``(m, n)``. + v: list[float] + Column vector of shape ``(n,)``. + + Returns + ------- + list[float] + Resulting vector of shape ``(m,)``. + """ + if not isinstance(a, list) or not isinstance(v, list): + raise TypeError("Matrix and vector must be lists of numbers.") + if not a or not a[0] or not v: + raise ValueError("Matrix and vector must be non-empty.") + if any(len(row) != len(v) for row in a): + raise ValueError(f"Inner dimensions mismatch: vector has {len(v)} entries.") + return [sum(x * y for x, y in zip(row, v)) for row in a] + def rot_2D(theta: float) -> Matrix: """Return a 2×2 rotation matrix for the given angle ``theta``. diff --git a/game.py b/game.py new file mode 100644 index 0000000..2dcc7ae --- /dev/null +++ b/game.py @@ -0,0 +1,110 @@ +"""Basisklasse ``Game`` für einfache pygame-Anwendungen.""" + +import pygame + + +class Game: + """Basisklasse für pygame-Spiele. + + Unterklassen passen das Verhalten durch Überschreiben von + :meth:`handle_event`, :meth:`update_game` und :meth:`draw_game` an. + """ + + def __init__( + self, + title: str, + fps: int = 60, + size: tuple[int, int] = (640, 400), + ) -> None: + """Legt Fenstertitel, Bildrate und Fenstergröße fest. + + Args: + title: Titel des Fensters. + fps: Maximale Bilder pro Sekunde. + size: Fenstergröße als (Breite, Höhe) in Pixeln. + """ + self.title: str = title + self.fps: int = fps + self.size: tuple[int, int] = size + self.clock: pygame.time.Clock = pygame.time.Clock() + self.dt: float = 0 + self.screen: pygame.Surface | None = None + + def init_game(self) -> None: + """Initialisiert pygame und erzeugt das Fenster.""" + pygame.init() + pygame.display.set_caption(self.title) + self.screen = pygame.display.set_mode(self.size) + + def game_loop(self) -> None: + """Führt die Hauptschleife aus, bis Events oder Update ``False`` liefern.""" + while True: + # Berechnung der Zeitdifferenz seit dem letzten Frame + self.dt = self.clock.tick(self.fps) / 1000 + if self.event_handling() == False: + break + if self.update_game() == False: + break + self.draw_game() + + def exit_game(self) -> None: + """Beendet pygame.""" + pygame.quit() + + def clear(self, color: tuple[int, int, int] = (0, 0, 0)) -> None: + """Füllt den Bildschirm mit einer Hintergrundfarbe. + + Args: + color: Hintergrundfarbe als (R, G, B). + """ + self.screen.fill(color) + + def display(self) -> None: + """Bringt den gezeichneten Frame zur Anzeige.""" + pygame.display.flip() + + def event_handling(self) -> bool: + """Verteilt alle anstehenden Events an :meth:`handle_event`. + + Bleibt in der Unterklasse unverändert. + + Returns: + ``False``, sobald ein Event das Spiel beenden soll, sonst ``True``. + """ + for event in pygame.event.get(): + if not self.handle_event(event): + return False + return True + + def handle_event(self, event: pygame.event.Event) -> bool: + """Verarbeitet ein einzelnes Event. + + Wird in der Unterklasse überschrieben. + + Args: + event: Das zu verarbeitende pygame-Event. + + Returns: + ``False`` bei ``pygame.QUIT``, sonst ``True``. + """ + if event.type == pygame.QUIT: + return False + return True + + def update_game(self) -> bool: + """Aktualisiert den Spielzustand um einen Frame. + + Returns: + ``False``, wenn das Spiel beendet werden soll, sonst ``True``. + """ + return True + + def draw_game(self) -> None: + """Zeichnet den aktuellen Frame und bringt ihn zur Anzeige.""" + pygame.display.flip() + + def run(self) -> None: + """Startet das Spiel: Initialisierung, Hauptschleife, Beenden.""" + self.init_game() + self.game_loop() + self.exit_game() \ No newline at end of file diff --git a/moon.py b/moon.py new file mode 100644 index 0000000..a9f8321 --- /dev/null +++ b/moon.py @@ -0,0 +1,77 @@ +"""Der Mond kreist um die Erde.""" + +import pygame + +from compute import matvec, rot_2D +from game import Game + +EARTH_RADIUS = 30 +MOON_RADIUS = 10 +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +BLUE = (0, 0, 255) + +MOON_DIST = 120 # Abstand Erde -> Mond +ANGLE = 0.02 # Drehwinkel pro Bild + + +class Moon(Game): + """Der Mond kreist um die Erde.""" + + def __init__( + self, + title: str = "Moon", + fps: int = 60, + size: tuple[int, int] = (640, 400), + ) -> None: + """Legt Startposition, Drehwinkel und Rotationsmatrix fest.""" + super().__init__(title, fps, size) + self.center = (size[0] / 2, size[1] / 2) # Erdposition + self.angle = self.drehwinkel() # Drehwinkel pro Bild + self.pos = self.startposition() # Vektor relativ zur Erde + + def drehwinkel(self) -> float: + """Winkel, um den pro Bild weitergedreht wird (Hook für Apollo).""" + return ANGLE + + def startposition(self) -> list[float]: + """Startposition als Vektor relativ zum Mittelpunkt.""" + return [MOON_DIST, 0.0] + + def update_game(self) -> bool: + """Dreht den Mond ein Stück weiter.""" + self.pos = self.drehe(self.pos) + return True + + def drehe(self, vektor: list[float]) -> list[float]: + """Dreht einen Vektor mit der Rotationsmatrix weiter.""" + return matvec(rot_2D(self.angle), vektor) + + def absolut(self, vektor: list[float]) -> tuple[float, float]: + """Rechnet einen relativen Vektor in Bildschirmkoordinaten um.""" + return (self.center[0] + vektor[0], self.center[1] + vektor[1]) + + def draw_game(self) -> None: + """Zeichnet Erde und Mond.""" + self.draw_space() + self.display() + + def draw_space(self): + self.clear() + self.draw_earth() + self.draw_body(self.absolut(self.pos), MOON_RADIUS, WHITE) + + def draw_earth(self) -> None: + """Zeichnet die Erde ins Zentrum.""" + pygame.draw.circle(self.screen, BLUE, self.center, EARTH_RADIUS) + + def draw_body( + self, pos: tuple[float, float], + radius: int, farbe: tuple[int, int, int], + ) -> None: + """Zeichnet einen Himmelskörper an einer Position.""" + pygame.draw.circle(self.screen, farbe, pos, radius) + +if __name__ == "__main__": + moon = Moon() + moon.run() \ No newline at end of file