import pygame import random import sys import os import subprocess import numpy as np import time SCREEN_WIDTH = 1920 SCREEN_HEIGHT = 1080 FPS = 120 TOP_BAR_HEIGHT = SCREEN_HEIGHT // 8 GAME_AREA_HEIGHT = SCREEN_HEIGHT // 1.6 BOTTOM_BAR_HEIGHT = SCREEN_HEIGHT // 4 TRAILER_WIDTH = int(SCREEN_WIDTH * 0.885416666667) TRAILER_HEIGHT = int(SCREEN_HEIGHT * 0.2778) TRAILER_X = (SCREEN_WIDTH - TRAILER_WIDTH) // 4 TRAILER_Y = TOP_BAR_HEIGHT + 200 CASE_SPEED_SLOW = 2 CASE_SPEED_FAST = 6 CASE_COLORS = [(200, 0, 0), (0, 200, 0), (0, 0, 200)] SNAP_THRESHOLD = 60 FAST_TO_SLOW_DISTANCE = 120 UNIT_LENGTH = TRAILER_HEIGHT // 3 UNIT_HEIGHT = TRAILER_HEIGHT // 3 pygame.init() pygame.joystick.init() if pygame.joystick.get_count() > 0: joystick = pygame.joystick.Joystick(0) joystick.init() print(f"Controller erkannt: {joystick.get_name()}") else: print("Kein Controller gefunden.") pygame.mixer.init() sound_fail = pygame.mixer.Sound("fail.ogg") sound_fail.set_volume(0.6) sound_place = pygame.mixer.Sound("place.ogg") sound_place.set_volume(0.6) sound_roll = pygame.mixer.Sound("roll.ogg") sound_roll.set_volume(0.3) current_case_visible = True # Animationssteuerung transition_fps = 60 transition_counter = 0 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) def generate_beep(frequency=440, duration_ms=500, volume=0.5): sample_rate = 44100 n_samples = int(sample_rate * (duration_ms / 1000.0)) t = np.linspace(0, duration_ms / 1000.0, n_samples, False) wave = np.sin(2 * np.pi * frequency * t) * volume audio = (wave * 32767).astype(np.int16) return pygame.sndarray.make_sound(np.column_stack([audio, audio])) sound_tut1 = generate_beep(440, 600) sound_tut2 = generate_beep(440, 400) sound_tuuut = generate_beep(600, 1000) pygame.display.set_caption("Habegger Loader Game") pygame.event.set_grab(True) clock = pygame.time.Clock() font = pygame.font.SysFont(None, 48) PLAYING = 1 state = PLAYING shake_timer = 0 stacked_cases = [] case_sequence = [] case_index = 0 current_case = None collision_x = TRAILER_X def load_instruction_image(): try: image = pygame.image.load("images/tutorial.png") return pygame.transform.scale(image, (SCREEN_WIDTH, SCREEN_HEIGHT)) except: return None instruction_image = load_instruction_image() def show_instruction_screen(): if not instruction_image: return waiting = True while waiting: screen.blit(instruction_image, (0, 0)) pygame.display.flip() for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif (event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE) or \ (event.type == pygame.JOYBUTTONDOWN and event.button == 0): waiting = False def generate_fill_sequence(): global case_sequence try: with open("case_sequences.txt", "r") as f: sets = f.read().strip().split("\n\n") chosen = random.choice(sets).strip().split("\n") sequence = [] for line in chosen: parts = line.strip().split(",") if len(parts) == 3: w, h, rot = int(parts[0]), int(parts[1]), parts[2].strip().lower() == 'true' sequence.append({'w': w, 'h': h, 'rotated': rot}) case_sequence = sequence except: case_sequence = [{'w': 2, 'h': 1, 'rotated': False}, {'w': 1, 'h': 2, 'rotated': False}] def load_background(): try: image = pygame.image.load("images/background.png") return pygame.transform.scale(image, (SCREEN_WIDTH, GAME_AREA_HEIGHT)) except: return None background_image = load_background() case_images = {} def load_case_images(): for w in range(1, 5): for h in range(1, 4): key = f"case_w{w}_h{h}" path = f"images/{key}.png" if os.path.exists(path): case_images[key] = pygame.image.load(path).convert_alpha() load_case_images() class Case: def animate_snap(self): pygame.mixer.Channel(1).stop() global collision_x # Snap-Animation: langsam einrasten an die Rückwand target_x = collision_x steps = 10 delta = (self.x - target_x) / steps for _ in range(steps): self.x -= delta draw_game() pygame.display.flip() pygame.time.delay(int(1000 / transition_fps)) self.x = target_x self.placed = True self.snapped = True collision_x = self.x + self.width def mark_occupied(self): for base in reversed(stacked_cases): height_ok = base.y - self.height >= TRAILER_Y width_ok = self.width <= base.width space_free = all(not ( other.y + other.height == base.y and other.x < base.x + base.width and other.x + other.width > base.x ) for other in stacked_cases) and not getattr(base, 'occupied', False) if height_ok and width_ok and space_free: base.occupied = True break def can_place_on_top(self): if self.placed: return False for base in reversed(stacked_cases): if base.y <= TRAILER_Y: continue height_ok = base.y - self.height >= TRAILER_Y width_ok = self.width <= base.width aligned_with_barrier = abs(base.x + base.width - collision_x) < 5 space_free = all(not ( other.y + other.height == base.y and other.x < base.x + base.width and other.x + other.width > base.x ) for other in stacked_cases) if height_ok and width_ok and aligned_with_barrier and space_free: return True return False def __init__(self, size=None): self.rotated = False global case_index if not case_sequence: generate_fill_sequence() if size is None: size = case_sequence[case_index % len(case_sequence)] case_index += 1 self.length_units, self.height_units = size['w'], size['h'] if size['rotated']: self.length_units, self.height_units = self.height_units, self.length_units elif self.height_units * UNIT_LENGTH <= TRAILER_WIDTH and self.length_units * UNIT_HEIGHT <= TRAILER_HEIGHT and random.choice( [True, False]): self.length_units, self.height_units = self.height_units, self.length_units self.width = self.length_units * UNIT_LENGTH self.height = self.height_units * UNIT_HEIGHT self.x = SCREEN_WIDTH self.spawn_y = TRAILER_Y + TRAILER_HEIGHT - self.height self.y = self.spawn_y self.entry_angle = 0 self.color = random.choice(CASE_COLORS) self.placed = False self.allow_snap = False self.snapped = False self.occupied = False def move(self): if not self.placed and not pygame.mixer.Channel(1).get_busy(): pygame.mixer.Channel(1).play(sound_roll, loops=-1) return distance_to_obstacle = self.x - collision_x speed = CASE_SPEED_FAST if distance_to_obstacle > FAST_TO_SLOW_DISTANCE else CASE_SPEED_SLOW self.x -= speed if self.x <= collision_x + SNAP_THRESHOLD: self.allow_snap = True def collides_with_barrier(self): return not self.snapped and self.x <= collision_x def draw(self, surface): angle = getattr(self, 'entry_angle', 0) if self.rotated: key = f"case_w{self.height_units}_h{self.length_units}" else: key = f"case_w{self.length_units}_h{self.height_units}" image = case_images.get(key) if image: if self.rotated: image_to_draw = pygame.transform.scale(image, (self.height, self.width)) else: image_to_draw = pygame.transform.scale(image, (self.width, self.height)) if self.rotated: image_to_draw = pygame.transform.rotate(image_to_draw, 90) if angle != 0: image_to_draw = pygame.transform.rotate(image_to_draw, angle) rect = image_to_draw.get_rect(center=(self.x + self.width // 2, self.y + self.height // 3)) surface.blit(image_to_draw, rect) else: surface.blit(image_to_draw, (self.x, self.y)) else: pygame.draw.rect(surface, self.color, (self.x, self.y, self.width, self.height)) def animate_tip(self): global current_case_visible current_case_visible = False key = f"case_w{self.length_units}_h{self.height_units}" image = case_images.get(key) if image: original = pygame.transform.scale(image, (self.width, self.height)).convert_alpha() else: original = pygame.Surface((self.width, self.height), pygame.SRCALPHA) pygame.draw.rect(original, self.color, (0, 0, self.width, self.height)) steps = 10 for step in range(steps + 1): angle = step * (90 / steps) rotated = pygame.transform.rotate(original, angle) new_rect = rotated.get_rect(bottomleft=(self.x, self.y + self.height)) draw_game() screen.blit(rotated, new_rect) pygame.display.flip() pygame.time.delay(int(1000 / transition_fps)) self.width, self.height = self.height, self.width self.length_units, self.height_units = self.height_units, self.length_units self.rotated = not getattr(self, 'rotated', False) self.x = max(collision_x + SNAP_THRESHOLD // 2, self.x) self.y = TRAILER_Y + TRAILER_HEIGHT - self.height current_case_visible = True def animate_place_on_top(self): pygame.mixer.Channel(1).stop() global current_case_visible, collision_x for base in reversed(stacked_cases): height_ok = base.y - self.height >= TRAILER_Y width_ok = self.width <= base.width space_free = all(not ( other.y + other.height == base.y and other.x < base.x + base.width and other.x + other.width > base.x ) for other in stacked_cases) if height_ok and width_ok and space_free: start_y = self.y target_y = base.y - self.height steps = 10 for step in range(steps): self.y = start_y - ((start_y - target_y) * (step + 1) / steps) draw_game() pygame.display.flip() pygame.time.delay(int(1000 / transition_fps)) self.y = target_y start_x = self.x target_x = base.x + base.width - self.width for step in range(steps): self.x = start_x - ((start_x - target_x) * (step + 1) / steps) draw_game() pygame.display.flip() pygame.time.delay(int(1000 / transition_fps)) self.x = target_x self.placed = True self.snapped = True self.mark_occupied() collision_x = self.x + self.width return True current_case_visible = True return False def snap_to_wall(self): global collision_x self.x = collision_x self.placed = True self.snapped = True collision_x = self.x + self.width def fail_snap(self): pygame.mixer.Channel(1).stop() global collision_x self.x = collision_x + SNAP_THRESHOLD self.placed = True collision_x = self.x + self.width def draw_progress_bar(surface, percent): bar_width = SCREEN_WIDTH * 0.25 bar_height = 30 x = SCREEN_WIDTH - bar_width - 140 y = TOP_BAR_HEIGHT // 2 - bar_height // 2 gap = 8 segment_width = (bar_width - 4 * gap) / 5 red = min(255, int(percent * 2.55)) green = max(0, 255 - int(percent * 2.55)) color = (red, green, 0) for i in range(5): segment_x = x + i * (segment_width + gap) pygame.draw.rect(surface, (50, 50, 50), (segment_x, y, segment_width, bar_height), border_radius=10) if percent >= (i + 1) * 20: pygame.draw.rect(surface, color, (segment_x, y, segment_width, bar_height), border_radius=10) text = font.render(f"{int(percent)}%", True, (255, 255, 255)) surface.blit(text, (x + bar_width + 10, y)) def draw_title(surface): title = font.render("Habegger Trailer Load", True, (255, 255, 255)) surface.blit(title, (40, TOP_BAR_HEIGHT // 2 - title.get_height() // 2)) def calculate_load_percent(): total_volume = TRAILER_WIDTH * TRAILER_HEIGHT used_volume = sum([c.width * c.height for c in stacked_cases]) return min(100, (used_volume / total_volume) * 100) next_queue = [] def draw_game(hide_trailer=False): global current_case global shake_timer global transition_counter if transition_counter > 0: transition_counter -= 1 pygame.time.delay(int(1000 / FPS)) global current_case global shake_timer offset_x = random.randint(-5, 5) if shake_timer > 0 else 0 offset_y = random.randint(-5, 5) if shake_timer > 0 else 0 if shake_timer > 0: shake_timer -= 1 screen.fill((0, 0, 0)) pygame.draw.rect(screen, (0, 0, 0), (0, 0, SCREEN_WIDTH, TOP_BAR_HEIGHT)) draw_title(screen) percent = calculate_load_percent() draw_progress_bar(screen, percent) if background_image: screen.blit(background_image, (0, TOP_BAR_HEIGHT)) else: pygame.draw.rect(screen, (0, 0, 0), (0, TOP_BAR_HEIGHT, SCREEN_WIDTH, GAME_AREA_HEIGHT)) if not hide_trailer: pygame.draw.rect(screen, (100, 100, 100), (TRAILER_X + offset_x, TRAILER_Y + offset_y, TRAILER_WIDTH, TRAILER_HEIGHT)) for c in stacked_cases: c.draw(screen) if current_case and current_case_visible: current_case.draw(screen) pygame.draw.line(screen, (255, 0, 0), (collision_x, TRAILER_Y), (collision_x, TRAILER_Y + TRAILER_HEIGHT), 3) pygame.draw.rect(screen, (0, 0, 0), (0, TOP_BAR_HEIGHT + GAME_AREA_HEIGHT, SCREEN_WIDTH, BOTTOM_BAR_HEIGHT)) if current_case and current_case.allow_snap: on_top_possible = current_case.can_place_on_top() if on_top_possible: snap_text = font.render("OnTop möglich!", True, (255, 255, 0)) else: snap_text = font.render("Snap bereit!", True, (0, 255, 0)) screen.blit(snap_text, (SCREEN_WIDTH - SCREEN_WIDTH // 3 - snap_text.get_width() // 2, SCREEN_HEIGHT - BOTTOM_BAR_HEIGHT // 2 - snap_text.get_height() // 2)) # Vorschau "Next" label = font.render("Next:", True, (255, 255, 255)) screen.blit(label, (40, SCREEN_HEIGHT - BOTTOM_BAR_HEIGHT + 10)) preview_x = 40 for i, case in enumerate(next_queue[:3]): w_units = case['w'] h_units = case['h'] if case['rotated']: w_units, h_units = h_units, w_units key = f"case_w{w_units}_h{h_units}" image = case_images.get(key) w = w_units * UNIT_LENGTH h = h_units * UNIT_HEIGHT if image: scaled = pygame.transform.scale(image, (w, h)) screen.blit(scaled, (preview_x, SCREEN_HEIGHT - BOTTOM_BAR_HEIGHT + 40)) else: pygame.draw.rect(screen, CASE_COLORS[i % len(CASE_COLORS)], (preview_x, SCREEN_HEIGHT - BOTTOM_BAR_HEIGHT + 40, w, h)) preview_x += max(w, 80) + 20 def spawn_case(): global running global can_use_on_top can_use_on_top = True global case_sequence, case_index, next_queue if not case_sequence: generate_fill_sequence() # Vorschau initial füllen while len(next_queue) < 3: index = (case_index + len(next_queue)) % len(case_sequence) next_queue.append(case_sequence[index]) # aktuelles Case bestimmen if case_index >= len(case_sequence): return None case_data = case_sequence[case_index % len(case_sequence)] case_index += 1 # Vorschau nachrücken if next_queue: next_queue.pop(0) while len(next_queue) < 3: index = (case_index + len(next_queue)) % len(case_sequence) next_queue.append(case_sequence[index]) return Case(size=case_data) def show_start_sequence(): center_x = TRAILER_X + TRAILER_WIDTH // 2 center_y = TRAILER_Y + TRAILER_HEIGHT // 2 colors = [(255, 0, 0), (255, 200, 0), (0, 255, 0)] delays = [1000, 1000, 1000] # ms pro Punkt sounds = [sound_tut1, sound_tut2, sound_tuuut] radius = 90 for i in range(3): draw_game(hide_trailer=True) pygame.draw.circle(screen, colors[i], (center_x, center_y), radius) sounds[i].play() pygame.display.flip() pygame.time.delay(delays[i]) # Reißverschluss-Effekt zip_sound = pygame.mixer.Sound("zip.ogg") zip_sound.set_volume(1) zip_sound.play() duration = 200 steps = 60 for step in range(steps + 1): width = int(TRAILER_WIDTH * (step / steps)) draw_game(hide_trailer=True) pygame.draw.rect(screen, (100, 100, 100), (TRAILER_X, TRAILER_Y, width, TRAILER_HEIGHT)) pygame.display.flip() pygame.time.delay(duration // steps) pygame.time.delay(1000) show_instruction_screen() show_start_sequence() current_case = spawn_case() def show_game_over(score): try: with open("last_user.txt", "r", encoding="utf-8") as f: lines = f.readlines() username = lines[1].strip() if len(lines) > 1 else "Spieler" try: result = subprocess.run( ["curl", "-X", "POST", "https://hag.putschgi.ch/Yb54CO5Ypybx/api/submit_points.php", "-d", f"name={username}", "-d", f"score={int(score)}", "-d", "api_key=hag-trailer-8051"], capture_output=True, text=True ) if result.returncode != 0 or not "success" in result.stdout.lower(): raise Exception("Serverfehler") except: error_text = font.render("Fehler: Punkte konnten nicht gesendet werden!", True, (255, 100, 100)) screen.blit(error_text, (SCREEN_WIDTH // 2 - error_text.get_width() // 2, SCREEN_HEIGHT // 2 + 60)) pygame.display.flip() pygame.time.delay(2000) except: username = "Spieler" pygame.mixer.stop() if score >= 75: message = f"Gratuliere {username}! Du hast {int(score)}% geladen! Drücke A oder SPACE um zu beenden." else: message = f"{username}, du hast {int(score)}% geladen. Drücke A oder SPACE um zu beenden." overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) overlay.set_alpha(220) overlay.fill((0, 0, 0)) screen.blit(overlay, (0, 0)) text = font.render(message, True, (255, 255, 255)) screen.blit(text, (SCREEN_WIDTH // 2 - text.get_width() // 2, SCREEN_HEIGHT // 2 - text.get_height() // 2)) pygame.display.flip() waiting = True while waiting: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif ( (event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE) or (event.type == pygame.JOYBUTTONDOWN and event.button == 0) ): pygame.quit() sys.exit() running = True can_use_tilt = True can_use_on_top = True game_finished = False while running: if not game_finished and collision_x > TRAILER_X + TRAILER_WIDTH: show_game_over(calculate_load_percent()) game_finished = True continue if not game_finished and current_case is None and case_index >= len(case_sequence): show_game_over(calculate_load_percent()) game_finished = True continue clock.tick(FPS) if transition_counter > 0: transition_counter -= 1 draw_game() pygame.display.flip() pygame.time.delay(int(1000 / FPS)) continue for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN or event.type == pygame.JOYBUTTONDOWN or event.type == pygame.JOYHATMOTION: if state == PLAYING: can_tip = current_case.length_units * UNIT_HEIGHT <= TRAILER_HEIGHT if ( (event.type == pygame.KEYDOWN and event.key == pygame.K_LEFT) or (event.type == pygame.JOYHATMOTION and event.value[0] == -1) or (event.type == pygame.JOYBUTTONDOWN and event.button == 2) ): if current_case and current_case.allow_snap and can_use_tilt and can_tip: current_case.animate_tip() can_use_tilt = False elif ( (event.type == pygame.KEYDOWN and event.key == pygame.K_UP) or (event.type == pygame.JOYHATMOTION and event.value[1] == 1) or (event.type == pygame.JOYBUTTONDOWN and event.button == 3) ): if current_case and current_case.allow_snap and can_use_on_top and current_case.can_place_on_top(): if current_case.animate_place_on_top(): transition_counter = transition_fps sound_place.play() stacked_cases.append(current_case) current_case = spawn_case() if current_case is None and not game_finished: show_game_over(calculate_load_percent()) game_finished = True continue can_use_tilt = True can_use_on_top = True else: current_case.fail_snap() sound_place.play() stacked_cases.append(current_case) current_case = spawn_case() if current_case is None and not game_finished: show_game_over(calculate_load_percent()) game_finished = True continue shake_timer = 10 elif ( (event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE) or (event.type == pygame.JOYBUTTONDOWN and event.button == 0) ): if current_case and current_case.allow_snap: current_case.animate_snap() can_use_tilt = True can_use_on_top = True sound_place.play() transition_counter = transition_fps stacked_cases.append(current_case) current_case = spawn_case() if state == PLAYING: if current_case is None: continue if current_case: current_case.move() if not game_finished: can_tip = current_case.length_units * UNIT_HEIGHT <= TRAILER_HEIGHT if collision_x + current_case.width > TRAILER_X + TRAILER_WIDTH and not current_case.can_place_on_top() and not current_case.allow_snap and not can_tip: show_game_over(calculate_load_percent()) game_finished = True continue if calculate_load_percent() >= 99.9 and case_index >= len(case_sequence): show_game_over(100) game_finished = True continue if not current_case.placed and current_case.collides_with_barrier(): current_case.fail_snap() if current_case.x + current_case.width > TRAILER_X + TRAILER_WIDTH: if stacked_cases and stacked_cases[-1] == current_case: stacked_cases.pop() else: stacked_cases.append(current_case) can_use_tilt = True can_use_on_top = True sound_fail.play() current_case = spawn_case() shake_timer = 10 draw_game() pygame.display.flip() pygame.quit() sys.exit()