#!/usr/bin/env python3 """Einstiegspunkt des Habegger Loader Games. Ablauf – immer in dieser Reihenfolge, auch nach jedem Ergebnis:: Anleitung → Moduswahl → Startsequenz (Ampel + Reissverschluss) → Spiel → Abschluss (Prozent, Spruch, 10 s Cooldown) → Anleitung → … Beendet wird ausschliesslich mit ``Esc`` (bzw. Controller-Taste B). Beispiele:: ./run.sh # Fenster ./run.sh --fullscreen # Vollbild (Messe-/Eventbetrieb) ./run.sh --smoke 10 # Selbsttest ohne Eingabegeräte """ from __future__ import annotations import argparse import enum import random import sys import pygame from loader_game import config from loader_game.assets import Assets from loader_game.difficulty import DEFAULT_DIFFICULTY, Difficulty from loader_game.game import Game, HandleResult, State from loader_game.input_map import ( Action, actions_from_event, controller_hint_lines, describe_controller_setup, is_device_event, open_controllers, ) from loader_game.intro import StartSequence, Tutorial from loader_game.menu import DifficultyMenu from loader_game.sequences import load_sequences class Phase(enum.Enum): TUTORIAL = "tutorial" # Anleitungsbild, wartet auf A/Space MENU = "menu" # Stufenauswahl START = "start" # Ampel + Reissverschluss PLAY = "play" # laufende Runde def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Habegger Loader Game") parser.add_argument( "--fullscreen", action="store_true", help="Vollbild starten (Standard: Fenster; F11 schaltet jederzeit um)", ) parser.add_argument( "--seed", type=int, default=None, help="Zufalls-Seed für reproduzierbare Koffer-Sequenzen", ) parser.add_argument( "--smoke", type=float, default=None, metavar="SEKUNDEN", help="Selbsttest: spielt ohne Eingaben und beendet sich nach N Sekunden", ) return parser.parse_args(argv) def create_screen(fullscreen: bool) -> pygame.Surface: """Fenster mit virtueller 1920x1080-Auflösung anlegen. ``pygame.SCALED`` sorgt dafür, dass alles automatisch auf die tatsächliche Fenster-/Bildschirmgrösse skaliert wird (Retina, 1440x900, 4K …). """ flags = pygame.SCALED | (pygame.FULLSCREEN if fullscreen else pygame.RESIZABLE) size = (config.SCREEN_WIDTH, config.SCREEN_HEIGHT) try: return pygame.display.set_mode(size, flags, vsync=1) except pygame.error: return pygame.display.set_mode(size, flags) def _demo_action(game: Game, rng: random.Random) -> Action | None: """Einfache Auto-Strategie für den Selbsttest.""" case = game.current if case is None or game.animation is not None or not case.allow_snap: return None return rng.choice([Action.PRIMARY, Action.PRIMARY, Action.PRIMARY, Action.UP, Action.LEFT]) def main(argv: list[str] | None = None) -> int: args = parse_args(argv) pygame.mixer.pre_init(config.AUDIO_SAMPLE_RATE, -16, 2, 512) pygame.init() if not pygame.display.get_init(): print("Kein Display verfügbar (ohne Bildschirm gestartet?).", file=sys.stderr) return 1 pygame.display.set_caption(config.WINDOW_TITLE) screen = create_screen(fullscreen=args.fullscreen) # Controller öffnen und offen halten – sonst liefert SDL keine Events. open_controllers() assets = Assets() assets.load() print(f"Python {sys.version.split()[0]}, pygame {pygame.version.ver}") for line in controller_hint_lines(): print(line) print(assets.audio_info()) sequences, warnings = load_sequences() for warning in warnings: print("Sequenz-Warnung:", warning, file=sys.stderr) rng = random.Random(args.seed) difficulty: Difficulty = DEFAULT_DIFFICULTY round_number = 0 previous_sequence: int | None = None phase = Phase.TUTORIAL tutorial: Tutorial | None = Tutorial(assets) menu: DifficultyMenu | None = None start: StartSequence | None = None game: Game | None = None def apply_action(action: Action) -> bool: """Verarbeitet eine Aktion in der aktuellen Phase. Rückgabe ``False`` = Programm beenden. """ nonlocal phase, tutorial, menu, start, game, difficulty if action is Action.BACK: return False if phase is Phase.TUTORIAL and tutorial is not None: tutorial.handle(action) return True if phase is Phase.MENU and menu is not None: selected = menu.handle(action) if selected is not None: difficulty = selected start = StartSequence(assets) phase = Phase.START return True if phase is Phase.START and start is not None: start.handle(action) return True if phase is Phase.PLAY and game is not None: result = game.handle(action) if result is HandleResult.QUIT: return False if result is HandleResult.NEW_ROUND: tutorial = Tutorial(assets) phase = Phase.TUTORIAL return True def advance(dt: float) -> None: """Weiterrechnen und Phasenwechsel ohne Eingabe.""" nonlocal phase, tutorial, menu, start, game, round_number, previous_sequence if phase is Phase.TUTORIAL and tutorial is not None: if tutorial.update(dt): tutorial = None menu = DifficultyMenu(assets, selected=difficulty) phase = Phase.MENU elif phase is Phase.START and start is not None: if start.update(dt): start = None game = Game( assets, sequences, difficulty=difficulty, rng=rng, previous_sequence=previous_sequence, ) previous_sequence = game.sequence_index round_number += 1 first = ", ".join(spec.label for spec in game.sequence[:5]) print( f"Runde {round_number}: Sequenz {game.sequence_index + 1}/{len(sequences)} " f"({len(game.sequence)} Koffer, Start: {first} …), " f"Stufe {difficulty.label}" ) phase = Phase.PLAY elif phase is Phase.PLAY and game is not None: game.update(dt) def draw() -> None: if phase is Phase.TUTORIAL and tutorial is not None: tutorial.draw(screen) elif phase is Phase.MENU and menu is not None: menu.draw(screen) elif phase is Phase.START and start is not None: start.draw(screen) elif phase is Phase.PLAY and game is not None: game.draw(screen) pygame.display.flip() clock = pygame.time.Clock() running = True smoke_remaining = args.smoke smoke_timer = 0.0 while running: dt = min(clock.tick(config.FPS) / 1000.0, 0.1) # ---------------------------------------------------------- Eingabe for event in pygame.event.get(): if event.type == pygame.QUIT: running = False break if event.type == pygame.KEYDOWN and event.key == pygame.K_F11: pygame.display.toggle_fullscreen() continue if is_device_event(event): print(describe_controller_setup()) open_controllers() continue for action in actions_from_event(event): if not apply_action(action): running = False break if not running: break advance(dt) # Selbsttest: ohne Eingabegeräte mitspielen if smoke_remaining is not None: smoke_remaining -= dt if smoke_remaining <= 0: running = False else: smoke_timer -= dt if smoke_timer <= 0: smoke_timer = 0.3 if phase is Phase.PLAY and game is not None: demo = _demo_action(game, rng) if demo is not None and not apply_action(demo): running = False elif not apply_action(Action.PRIMARY): running = False draw() if game is not None: percent = ( game.percent if game.state is State.FINISHED else game.trailer.load_percent() ) print( f"Spiel beendet: Stufe={difficulty.label}, Beladung={percent:.0f}%, " f"Koffer im Auflieger={len(game.trailer.stack)}" ) pygame.quit() return 0 if __name__ == "__main__": sys.exit(main())