Files

275 lines
8.9 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())