import asyncio import pygame import random import sys import os import time from pygame import FULLSCREEN # === Konstanten === SCREEN_WIDTH = 1920 SCREEN_HEIGHT = 1080 FPS = 60 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 = 3 CASE_SPEED_FAST = 8 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 # === Globale Variablen === current_case_visible = True transition_fps = 60 transition_counter = 0 collision_x = TRAILER_X stacked_cases = [] case_sequence = [] case_index = 0 current_case = None next_queue = [] running = True can_use_tilt = True can_use_on_top = True game_finished = False shake_timer = 0 state = 1 # PLAYING prepared_form = None ready_to_submit = False touch_start = None touch_start_time = None def init_sounds(): global sound_fail, sound_place, sound_roll, sound_tut1, sound_tut2, sound_tuuut, zip_sound pygame.mixer.init() sound_fail = pygame.mixer.Sound("sounds/fail.ogg") sound_fail.set_volume(0.6) sound_place = pygame.mixer.Sound("sounds/place.ogg") sound_place.set_volume(0.6) sound_roll = pygame.mixer.Sound("sounds/roll.ogg") sound_roll.set_volume(0.3) sound_tut1 = pygame.mixer.Sound("sounds/tut1.ogg") sound_tut1.set_volume(1) sound_tut2 = pygame.mixer.Sound("sounds/tut2.ogg") sound_tut2.set_volume(1) sound_tuuut = pygame.mixer.Sound("sounds/tuuut.ogg") sound_tuuut.set_volume(1) zip_sound = pygame.mixer.Sound("sounds/zip.ogg") zip_sound.set_volume(1) print("Sounds erfolgreich geladen.") case_images = {} async def touch(event): global touch_start, touch_start_time if event.type == pygame.FINGERDOWN and not touch_start: touch_start = (event.x * SCREEN_WIDTH, event.y * SCREEN_HEIGHT) touch_start_time = time.time() print("touch_start") return False elif event.type == pygame.FINGERUP and touch_start: end = (event.x * SCREEN_WIDTH, event.y * SCREEN_HEIGHT) duration = time.time() - touch_start_time dx = end[0] - touch_start[0] dy = end[1] - touch_start[1] print("touch_end") print(f"Touch dx={dx}, dy={dy}, duration={duration}") touch_start = None touch_start_time = None if abs(dx) < 30 and abs(dy) < 30 and duration < 0.1: return "snap" elif dx < -50 and abs(dy) < 80 and duration > 0.08: return "tilt" elif dy < -50 and abs(dx) < 80 and duration > 0.08: return "ontop" else: print("touch problem") return False return False async def is_SPACE_event(event): # Tastatur oder Joystick A if (event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE) or \ (event.type == pygame.JOYBUTTONDOWN and event.button == 0): return True # Mausklick (nur Linksklick, für Desktop-Tests) if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: return True return False async def is_LEFT_event(event): # Tastatur oder Joystick links 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)): return True return False async def is_UP_event(event): # Tastatur oder Joystick oben if ((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)): return True return False async def init_game(): global screen, clock, font, controls pygame.init() pygame.joystick.init() init_sounds() if pygame.joystick.get_count() > 0: joystick = pygame.joystick.Joystick(0) joystick.init() print(f"Controller erkannt: {joystick.get_name()}") controls = "tutorial_controller" else: controls = "tutorial" print("Kein Controller gefunden.") screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Habegger Loader Game") clock = pygame.time.Clock() font = pygame.font.SysFont(None, 48) class Case: def __init__(self, size=None): global case_index self.rotated = False 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 self.rotated = False # <==== WICHTIG: HINZUFÜGEN!!! global current_case_visible current_case_visible = True def move(self): global collision_x if not self.placed and not pygame.mixer.Channel(1).get_busy(): if sound_roll: pygame.mixer.Channel(1).play(sound_roll, loops=-1) 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 async def fail_snap(self): global collision_x steps = 5 bounce_distance = 20 # Pixel zurückspringen for _ in range(steps): self.x += bounce_distance / steps draw_game() pygame.display.flip() await asyncio.sleep(0) self.placed = True self.snapped = True collision_x = self.x + self.width 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 collides_with_barrier(self): return not self.snapped and self.x <= collision_x def draw(self, surface=None): global screen surface = surface or screen 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)) async def animate_snap(self): if sound_roll: pygame.mixer.Channel(1).stop() global collision_x target_x = collision_x steps = 10 delta = (self.x - target_x) / steps for _ in range(steps): self.x -= delta draw_game() pygame.display.flip() await asyncio.sleep(0) self.x = target_x self.placed = True self.snapped = True collision_x = self.x + self.width async 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() await asyncio.sleep(0) 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 async def animate_place_on_top(self): if sound_roll: 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() await asyncio.sleep(0) 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() await asyncio.sleep(0) self.x = target_x self.placed = True self.snapped = True collision_x = self.x + self.width return True current_case_visible = True return False def load_case_images(): global 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() async def show_start_sequence(): global zip_sound 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): await asyncio.sleep(0) 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.play() duration = 200 steps = 60 for step in range(steps + 1): await asyncio.sleep(0) 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) def load_instruction_image(): global controls try: image = pygame.image.load("images/"+controls+".png") return pygame.transform.scale(image, (SCREEN_WIDTH, SCREEN_HEIGHT)) except: return None def load_background(): try: image = pygame.image.load("images/background.png") return pygame.transform.scale(image, (SCREEN_WIDTH, GAME_AREA_HEIGHT)) except: return None def draw_game(hide_trailer=False): 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)) title_font = pygame.font.SysFont(None, 64) title_surface = title_font.render("Habegger Loader Game", True, (255, 255, 255)) screen.blit(title_surface, (50, 50)) # === Ladebalken zeichnen === percent_full = calculate_load_percent() bar_width = 400 bar_height = 30 bar_x = SCREEN_WIDTH - bar_width - 50 bar_y = 50 pygame.draw.rect(screen, (80, 80, 80), (bar_x, bar_y, bar_width, bar_height)) pygame.draw.rect(screen, (0, 255, 0), (bar_x, bar_y, bar_width * (percent_full / 100), bar_height)) bar_font = pygame.font.SysFont(None, 36) bar_text = bar_font.render(f"{int(percent_full)}% geladen", True, (255, 255, 255)) screen.blit(bar_text, (bar_x + bar_width // 2 - bar_text.get_width() // 2, bar_y + bar_height // 2 - bar_text.get_height() // 2)) # === Vorschau nächstes Case (nur Text) === preview_box_width = 150 preview_box_height = 80 preview_box_x = (SCREEN_WIDTH - preview_box_width) // 2 preview_box_y = SCREEN_HEIGHT - preview_box_height - 20 # Grauer Hintergrundkasten pygame.draw.rect(screen, (80, 80, 80), (preview_box_x, preview_box_y, preview_box_width, preview_box_height), border_radius=12) # Überschrift: "B x H" title_font = pygame.font.SysFont(None, 28) title_surface = title_font.render("Next: (B x H)", True, (200, 200, 200)) title_x = preview_box_x + (preview_box_width - title_surface.get_width()) // 2 title_y = preview_box_y + 5 screen.blit(title_surface, (title_x, title_y)) # Text für nächstes Case (z.B. "2x1") if next_queue: next_case = next_queue[0] preview_text = f"{next_case['w']}x{next_case['h']}" preview_font = pygame.font.SysFont(None, 48) text_surface = preview_font.render(preview_text, True, (255, 255, 255)) text_x = preview_box_x + (preview_box_width - text_surface.get_width()) // 2 text_y = title_y + title_surface.get_height() + 5 screen.blit(text_surface, (text_x, text_y)) # === Echte Vorschau der nächsten 3 Cases (horizontal in Top-Bar) === preview_start_x = 50 preview_start_y = SCREEN_HEIGHT - 200 spacing_between_cases = 80 # Fester Abstand zwischen den Vorschau-Cases title_next = title_font.render("Next 3:", True, (200, 200, 200)) title_x = 50 title_y = SCREEN_HEIGHT - 250 screen.blit(title_next, (title_x, title_y)) current_x = preview_start_x for i, case_data in enumerate(next_queue[:3]): preview_width = case_data['w'] * (UNIT_LENGTH // 2) preview_height = case_data['h'] * (UNIT_HEIGHT // 2) preview_case_surface = pygame.Surface((preview_width, preview_height), pygame.SRCALPHA) # Bild laden oder Rechteck zeichnen key = f"case_w{case_data['w']}_h{case_data['h']}" image = case_images.get(key) if image: image = pygame.transform.scale(image, (preview_width, preview_height)) preview_case_surface.blit(image, (0, 0)) else: pygame.draw.rect(preview_case_surface, (180, 180, 180), (0, 0, preview_width, preview_height)) preview_y = preview_start_y # Blit auf Hauptscreen screen.blit(preview_case_surface, (current_x, preview_y)) # Nächste X-Position anpassen current_x += preview_width + spacing_between_cases 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) async def show_instruction_screen(image): if not image: return waiting = True while waiting: screen.blit(image, (0, 0)) pygame.display.flip() await asyncio.sleep(0) for event in pygame.event.get(): await asyncio.sleep(0) result = await touch(event) if event.type == pygame.QUIT: pygame.quit() sys.exit() elif await is_SPACE_event(event) or (result == "snap"): print ("test") waiting = False async def fail_current_case(): global can_use_tilt, can_use_on_top, game_finished, shake_timer if sound_fail: sound_fail.play() await 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 spawn_case_in_game() shake_timer = 10 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 spawn_case_in_game(): global current_case, next_queue, case_index if not case_sequence: generate_fill_sequence() while len(next_queue) < 3: index = (case_index + len(next_queue)) % len(case_sequence) next_queue.append(case_sequence[index]) if case_index >= len(case_sequence): current_case = None return case_data = case_sequence[case_index % len(case_sequence)] case_index += 1 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]) current_case = Case(size=case_data) async def show_game_over(score): import sys import urllib.parse try: import js except ImportError: js = None username = "Spieler" try: query = js.window.location.search params = urllib.parse.parse_qs(query[1:]) username = params.get("name", ["Spieler"])[0] except Exception: pass data = { "name": username, "score": int(score), "api_key": "hag-trailer-8051" } def is_browser(): return sys.platform in ("emscripten", "wasi") if is_browser(): global prepared_form, ready_to_submit if not is_browser() or js is None: return try: form = js.document.createElement("form") form.method = "POST" form.action = "https://hag-game.carabella.ch/submit_points.php" for key, value in data.items(): input_field = js.document.createElement("input") input_field.type = "hidden" input_field.name = key input_field.value = str(value) form.appendChild(input_field) js.document.body.appendChild(form) prepared_form = form ready_to_submit = True print("Formular vorbereitet. Warten auf Bestätigung (SPACE oder Klick).") except Exception as e: print("Fehler beim Formular vorbereiten:", e) else: try: import urllib.request body = urllib.parse.urlencode(data).encode() req = urllib.request.Request( url="https://hag-game.carabella.ch/submit_points.php", data=body, method="POST", headers={ "Content-Type": "application/x-www-form-urlencoded", } ) with urllib.request.urlopen(req) as response: if response.getcode() != 200: raise Exception("HTTP error") print("Punkte erfolgreich lokal gesendet!") except Exception as e: print("Fehler beim lokalen Senden:", e) pygame.mixer.stop() overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) overlay.set_alpha(220) overlay.fill((0, 0, 0)) screen.blit(overlay, (0, 0)) font_big = pygame.font.SysFont(None, 72) message = f"Spiel beendet! Punkte: {int(score)}%" text = font_big.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: await asyncio.sleep(0) for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() elif await is_SPACE_event(event) or await (touch(event) == "snap"): if ready_to_submit and prepared_form: print("Benutzer bestätigt, Punkte werden jetzt gesendet.") prepared_form.submit() pygame.quit() sys.exit() 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) async def main(): global running, can_use_tilt, can_use_on_top, game_finished global current_case, transition_counter, state global screen, clock, font, instruction_image, background_image global touch_start await init_game() load_case_images() instruction_image = load_instruction_image() background_image = load_background() generate_fill_sequence() await asyncio.sleep(1) await show_instruction_screen(instruction_image) await show_start_sequence() spawn_case_in_game() 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: await 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): await show_game_over(calculate_load_percent()) game_finished = True continue clock.tick(FPS) await asyncio.sleep(0) if transition_counter > 0: transition_counter -= 1 draw_game() pygame.display.flip() await asyncio.sleep(0) 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 or event.type == pygame.FINGERDOWN or touch_start or (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1): if state == 1: # PLAYING result = "none" if event.type == pygame.FINGERUP and touch_start: result = await touch(event) elif event.type == pygame.FINGERDOWN and not touch_start: await touch(event) if await is_LEFT_event(event) or (result == "ontop"): can_tip = current_case.length_units * UNIT_HEIGHT <= TRAILER_HEIGHT if current_case and current_case.allow_snap and can_use_tilt and can_tip: await current_case.animate_tip() can_use_tilt = False elif await is_UP_event(event) or (result == "tilt"): if current_case and current_case.allow_snap and can_use_on_top and current_case.can_place_on_top(): if await current_case.animate_place_on_top(): transition_counter = transition_fps if sound_place: sound_place.play() stacked_cases.append(current_case) spawn_case_in_game() can_use_tilt = True can_use_on_top = True else: await current_case.fail_snap() if sound_place: sound_place.play() stacked_cases.append(current_case) spawn_case_in_game() shake_timer = 10 elif await is_SPACE_event(event) or (result == "snap"): print(result) if current_case and current_case.allow_snap: await current_case.animate_snap() can_use_tilt = True can_use_on_top = True if sound_place: sound_place.play() transition_counter = transition_fps stacked_cases.append(current_case) spawn_case_in_game() if state == 1: # PLAYING if current_case is None: continue if current_case: current_case.move() if current_case.collides_with_barrier(): await fail_current_case() 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: await show_game_over(calculate_load_percent()) game_finished = True continue if calculate_load_percent() >= 99.9 and case_index >= len(case_sequence): await show_game_over(100) game_finished = True continue if not current_case.placed and current_case.collides_with_barrier(): await 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 if sound_fail: sound_fail.play() spawn_case_in_game() shake_timer = 10 draw_game() pygame.display.flip() pygame.quit() sys.exit() # === Spielstart === if __name__ == "__main__": asyncio.run(main())