diff --git a/main.py b/main.py new file mode 100644 index 0000000..5b5eb5b --- /dev/null +++ b/main.py @@ -0,0 +1,724 @@ +import asyncio +import pygame +import random +import sys +import os + +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 + + +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 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(): + 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 + +async def fail_current_case(): + global can_use_tilt, can_use_on_top, game_finished, shake_timer + 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 + +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" + 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: + query = js.window.location.search + params = urllib.parse.parse_qs(query[1:]) + username = params.get("name", ["Spieler"])[0] + except Exception: + pass + + 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 ( + (event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE) or + (event.type == pygame.JOYBUTTONDOWN and event.button == 0) + ): + 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 + + 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: + if state == 1: # PLAYING + 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)): + 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 ((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 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 ((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: + 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())