diff --git a/apollo.py b/apollo.py new file mode 100644 index 0000000..fe2c895 --- /dev/null +++ b/apollo.py @@ -0,0 +1,47 @@ +"""Die Raumkapsel Apollo kreist um den Mond.""" + +import pygame + +from compute import rot_2d +from moon import MOON_RADIUS, Moon, Point + +APOLLO_RADIUS = 4 +APOLLO_DISTANCE = MOON_RADIUS + 15 # Abstand Mond-Apollo (Mittelpunkte) +RED = (255, 0, 0) + + +class Apollo(Moon): + """Erweitert die Mond-Simulation um die Raumkapsel Apollo.""" + + def __init__( + self, + title: str = "Apollo", + fps: int = 60, + size: tuple[int, int] = (640, 400), + ) -> None: + """Legt Startposition und Rotationsmatrix von Apollo fest.""" + super().__init__(title, fps, size) + self.position_apollo: Point = (APOLLO_DISTANCE, 0.0) # relativ zum Mond + # doppelte Winkelgeschwindigkeit, entgegengesetzte Drehrichtung + self.rot_apollo = rot_2d(-2 * self.theta, unit="rad") + + def update_game(self) -> bool: + """Bewegt erst den Mond, dann Apollo.""" + running = super().update_game() + self.position_apollo = self.rotate(self.position_apollo, self.rot_apollo) + return running + + def draw_bodies(self) -> None: + """Zeichnet Erde und Mond und zusätzlich Apollo.""" + super().draw_bodies() + moon_x, moon_y = self.moon_screen_position() + apollo_pos = ( + moon_x + self.position_apollo[0], + moon_y + self.position_apollo[1], + ) + pygame.draw.circle(self.screen, RED, apollo_pos, APOLLO_RADIUS) + + +if __name__ == "__main__": + apollo = Apollo() + apollo.run() \ No newline at end of file diff --git a/compute.py b/compute.py index 34668d1..0f18bd8 100644 --- a/compute.py +++ b/compute.py @@ -6,6 +6,8 @@ import math from collections.abc import Iterable from typing import Literal +from tabulate import tabulate + __all__ = ["matmul", "rot_2d", "rotate", "transpose"] _NUMERIC_TYPES = (int, float) @@ -259,3 +261,5 @@ if __name__ == "__main__": print("All tests passed.") #This is a test commit print("This is a test") +if __name__ == "__main__": + print(tabulate(rot_2d(1.0))) \ No newline at end of file diff --git a/game.py b/game.py new file mode 100644 index 0000000..e88c245 --- /dev/null +++ b/game.py @@ -0,0 +1,98 @@ +"""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 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..817c57f --- /dev/null +++ b/moon.py @@ -0,0 +1,67 @@ +import pygame + +from compute import matmul, rot_2d, transpose +from game import Game + +EARTH_RADIUS = 30 +MOON_RADIUS = 10 +MOON_DISTANCE = 120 # Abstand Erde-Mond (Mittelpunkte) +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +BLUE = (0, 0, 255) + +Point = tuple[float, float] + + +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.position_moon: Point = (MOON_DISTANCE, 0.0) # relativ zur Erde + self.theta = 0.01 # radian per frame + self.rot_moon = rot_2d(self.theta, unit="rad") + self.center = (size[0] // 2, size[1] // 2) + + @staticmethod + def rotate(point: Point, rotation: list[list[float]]) -> Point: + """Dreht einen Punkt mit einer 2x2-Rotationsmatrix um den Ursprung.""" + column = transpose([list(point)]) # [x, y] -> [[x], [y]] + result = matmul(rotation, column) # [[x'], [y']] + return (result[0][0], result[1][0]) + + def update_game(self) -> bool: + """Dreht den Mond ein Stück weiter.""" + self.position_moon = self.rotate(self.position_moon, self.rot_moon) + return True + + def moon_screen_position(self) -> Point: + """Rechnet die Mondposition relativ zur Erde in Bildschirmkoordinaten um.""" + return ( + self.center[0] + self.position_moon[0], + self.center[1] + self.position_moon[1], + ) + + def draw_bodies(self) -> None: + """Zeichnet Erde und Mond. Unterklassen können weitere Körper ergänzen.""" + pygame.draw.circle(self.screen, BLUE, self.center, EARTH_RADIUS) + pygame.draw.circle(self.screen, WHITE, self.moon_screen_position(), MOON_RADIUS) + + def draw_game(self) -> None: + """Löscht den Bildschirm, zeichnet alle Körper und zeigt das Bild an.""" + if self.screen is None: + return + self.screen.fill(BLACK) + self.draw_bodies() + super().draw_game() + + +if __name__ == "__main__": + moon = Moon() + moon.run() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 46e3b47..3a776b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ [project] name = "matrixmania" -version = "0.1.0" \ No newline at end of file +version = "0.1.0" +dependencies = ["tabulate", "ruff"] \ No newline at end of file