Files
Habegger_loader_game/main.py
T

252 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Einstiegspunkt des Habegger Loader Games.
Ablauf: Tutorialbild (nur beim Programmstart) → Stufenauswahl → Ampel-Start →
Runde → Ergebnis ("Nochmals?") → Stufenauswahl → …
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
from loader_game.intro import Intro
from loader_game.menu import DifficultyMenu
from loader_game.sequences import load_sequences
class Phase(enum.Enum):
INTRO = "intro" # Tutorial und/oder Ampel-Start
MENU = "menu" # Stufenauswahl
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)
assets = Assets()
assets.load()
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.INTRO
after_intro = Phase.MENU # nach dem Tutorial kommt die Stufenauswahl
intro: Intro | None = Intro(assets, show_instruction=True)
menu: DifficultyMenu | 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, intro, menu, game, difficulty, after_intro
if action is Action.BACK:
return False
if phase is Phase.INTRO and intro is not None:
intro.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
intro = Intro(assets, show_instruction=False)
after_intro = Phase.PLAY
phase = Phase.INTRO
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:
# Wie beim Programmstart: Anleitung -> Stufenauswahl -> Ampel
intro = Intro(assets, show_instruction=True)
after_intro = Phase.MENU
menu = None
phase = Phase.INTRO
return True
def advance(dt: float) -> None:
"""Weiterrechnen und Phasenwechsel ohne Eingabe."""
nonlocal phase, intro, menu, game, round_number, previous_sequence
if phase is Phase.INTRO and intro is not None:
if intro.update(dt):
intro = None
if after_intro is Phase.PLAY:
game = Game(
assets,
sequences,
difficulty=difficulty,
rng=rng,
previous_sequence=previous_sequence,
)
previous_sequence = game.sequence_index
round_number += 1
start = ", ".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: {start} …), "
f"Stufe {difficulty.label}"
)
else:
menu = DifficultyMenu(assets, selected=difficulty)
phase = after_intro
elif phase is Phase.PLAY and game is not None:
game.update(dt)
def draw() -> None:
if phase is Phase.INTRO and intro is not None:
intro.draw(screen)
elif phase is Phase.MENU and menu is not None:
menu.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
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())