275 lines
8.9 KiB
Python
Executable File
275 lines
8.9 KiB
Python
Executable File
#!/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())
|