diff --git a/INSTALL_README.txt b/INSTALL_README.txt new file mode 100644 index 0000000..71acb36 --- /dev/null +++ b/INSTALL_README.txt @@ -0,0 +1,3 @@ +python.exe -m pip install --upgrade pip + +pip install pillow pyzbar opencv-python numpy pygame pygetwindow requests pyautogui diff --git a/fail.ogg b/fail.ogg new file mode 100644 index 0000000..f5fc68c Binary files /dev/null and b/fail.ogg differ diff --git a/fonts/Tisa Sans Pro Regular.ttf b/fonts/Tisa Sans Pro Regular.ttf new file mode 100644 index 0000000..ab30785 Binary files /dev/null and b/fonts/Tisa Sans Pro Regular.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Black Italic.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Black Italic.ttf new file mode 100644 index 0000000..6de725d Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Black Italic.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Black.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Black.ttf new file mode 100644 index 0000000..b9253eb Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Black.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Bold Italic.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Bold Italic.ttf new file mode 100644 index 0000000..cd95394 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Bold Italic.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Bold.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Bold.ttf new file mode 100644 index 0000000..f549321 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Bold.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro ExtraBold Italic.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraBold Italic.ttf new file mode 100644 index 0000000..a5b405d Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraBold Italic.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro ExtraBold.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraBold.ttf new file mode 100644 index 0000000..e3af405 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraBold.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro ExtraLight Italic.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraLight Italic.ttf new file mode 100644 index 0000000..2024a02 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraLight Italic.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro ExtraLight.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraLight.ttf new file mode 100644 index 0000000..2e836c0 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro ExtraLight.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Italic.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Italic.ttf new file mode 100644 index 0000000..3193ce2 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Italic.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Light Italic.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Light Italic.ttf new file mode 100644 index 0000000..8ede868 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Light Italic.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Light.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Light.ttf new file mode 100644 index 0000000..ca8faeb Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Light.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Medium Italic.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Medium Italic.ttf new file mode 100644 index 0000000..6d2f283 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Medium Italic.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Medium.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Medium.ttf new file mode 100644 index 0000000..573a256 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Medium.ttf differ diff --git a/fonts/tisa-sans-pro/Tisa Sans Pro Regular.ttf b/fonts/tisa-sans-pro/Tisa Sans Pro Regular.ttf new file mode 100644 index 0000000..ab30785 Binary files /dev/null and b/fonts/tisa-sans-pro/Tisa Sans Pro Regular.ttf differ diff --git a/loader_game.py b/loader_game.py new file mode 100644 index 0000000..c807276 --- /dev/null +++ b/loader_game.py @@ -0,0 +1,656 @@ +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() diff --git a/place.ogg b/place.ogg new file mode 100644 index 0000000..3a35499 Binary files /dev/null and b/place.ogg differ diff --git a/qr_scan.py b/qr_scan.py new file mode 100644 index 0000000..c099da1 --- /dev/null +++ b/qr_scan.py @@ -0,0 +1,188 @@ +import cv2 +from pyzbar.pyzbar import decode +import numpy as np +from PIL import ImageFont, ImageDraw, Image +import os +import subprocess +import requests +import pygetwindow as gw +import pyautogui +import time + +# Nach 2 Sekunden suchen wir das Fenster +time.sleep(2) +for w in gw.getWindowsWithTitle("QR Scanner"): # oder z. B. "pygame", "Loader", etc. + w.activate() + w.maximize() # optional + pyautogui.click(w.left + 100, w.top + 100) # sicherer Klick ins Fenster + break + +API_URL = "https://hag.putschgi.ch/Yb54CO5Ypybx/api/qr.php?action=verify_hash" +API_SECRET = "ddlKC6sgE4EMvsU9VQeIVeYrsPGpE104" + +# Fenstergröße & Farben +window_width, window_height = 1920, 1080 + # Habegger Gold +COLOR_GREEN = (0, 255, 0) +COLOR_YELLOW = (0, 255, 255) +COLOR_BACKGROUND = (45, 46, 47) # HTML-Farbe #2d2e2f +COLOR_WHITE = (255, 255, 255) +COLOR_GOLD = (131, 188, 214) # BGR für Habegger-Gold + +# Font & Logo +FONT_PATH = os.path.join("fonts", "Tisa Sans Pro Regular.ttf" + "") # Alternativ: Georgia.ttf, falls vorhanden +FONT_SIZE = 48 +font = ImageFont.truetype(FONT_PATH, FONT_SIZE) +logo_path = "images/habegger_logo_goldbar_whitefont.png" # Logo mit sichtbarem Text +logo_pil = Image.open(logo_path).convert("RGBA") +logo_pil.thumbnail((300, 100)) +logo_pil = Image.alpha_composite(Image.new("RGBA", logo_pil.size, (0, 0, 0, 0)), logo_pil) +logo_img = np.array(logo_pil) +logo_img = cv2.cvtColor(logo_img, cv2.COLOR_RGBA2BGRA) + +def draw_unicode_text(cv2_image, text, position, color=COLOR_WHITE): + img_pil = Image.fromarray(cv2_image) + draw = ImageDraw.Draw(img_pil) + draw.text(position, text, font=font, fill=color) + return np.array(img_pil) + +def parse_qr_data(data): + from urllib.parse import urlparse, parse_qs + if "?code=" in data: + parsed = urlparse(data) + query = parse_qs(parsed.query) + code = query.get("code", [""])[0] + return code + return None + + +def start_game(mail, name): + with open("last_user.txt", "w", encoding="utf-8-sig") as f: + f.write(f"{mail}\n{name}") + subprocess.run(["python", "loader_game.py"]) + +# Endlosschleife für den Scanner +while True: + detected_color = COLOR_GOLD + detected_time = None + cap = cv2.VideoCapture(0) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) + + # Rahmenfarbe wird später im Loop anhand der Zeit gesetzt + last_display_text = "" + start_timer = None + game_started = False + + while True: + success, frame = cap.read() + if not success: + break + + decoded_objects = decode(frame) + if not decoded_objects: + inverted_frame = cv2.bitwise_not(frame) + decoded_objects = decode(inverted_frame) + + # QR-Status zurücksetzen nach 5 Sek. + if detected_time and time.time() - detected_time >= 5.0: + detected_color = COLOR_GOLD + frame_color = COLOR_GOLD + + if decoded_objects: + detected_time = time.time() + + for obj in decoded_objects: + points = obj.polygon + if len(points) > 4: + hull = cv2.convexHull(np.array([point for point in points], dtype=np.float32)) + points = hull.reshape(-1, 2) + for i in range(len(points)): + pt1 = tuple(points[i]) + pt2 = tuple(points[(i + 1) % len(points)]) + cv2.line(frame, pt1, pt2, COLOR_GREEN, 3) + + data = obj.data.decode("utf-8") + code = parse_qr_data(data) + if not code: + detected_color = COLOR_YELLOW + detected_time = time.time() + last_display_text = "Kein gültiger QR-Code erkannt." + break + + try: + res = requests.post(API_URL, json={"code": code, "auth": API_SECRET}, timeout=5) + print("RAW Response:", res.text) + result = res.json() + + except Exception as e: + detected_color = COLOR_YELLOW + detected_time = time.time() + last_display_text = f"API Fehler: {e}" + break + + if not result.get("valid"): + detected_color = COLOR_YELLOW + last_display_text = "Ungültiger Code." + elif result.get("registered"): + name = result.get("name", "Unbekannt") + detected_color = COLOR_GREEN + last_display_text = f"Hallo {name}!" + start_timer = time.time() + game_started = True + last_mail = code + last_name = name + else: + detected_color = COLOR_YELLOW + last_display_text = "QR gültig, aber nicht registriert." + detected_time = time.time() + break + + if start_timer and time.time() - start_timer >= 5.0: + cap.release() + cv2.destroyAllWindows() + start_game(last_mail, last_name) + break + + display_frame = np.full((window_height, window_width, 3), COLOR_BACKGROUND, dtype=np.uint8) + cam_h, cam_w = frame.shape[:2] + x_offset = (window_width - cam_w) // 2 + y_offset = (window_height - cam_h) // 2 + display_frame[y_offset:y_offset+cam_h, x_offset:x_offset+cam_w] = frame + + display_frame = draw_unicode_text(display_frame, + "Scanne deinen QR-Code um das Spiel zu starten.", + (80, 100), COLOR_WHITE) + + if last_display_text: + display_frame = draw_unicode_text(display_frame, last_display_text, + (80, window_height - 100), COLOR_WHITE) + + # Kamera-Rahmen + if detected_time and time.time() - detected_time < 5.0: + current_color = detected_color + else: + current_color = COLOR_GOLD + + cv2.rectangle(display_frame, (x_offset, y_offset), + (x_offset + cam_w, y_offset + cam_h), current_color, 6) + + # Logo oben rechts ohne Alpha-Blending-Probleme + logo_pos = (window_width - logo_img.shape[1] - 40, 40) + overlay = np.zeros_like(display_frame, dtype=np.uint8) + overlay[logo_pos[1]:logo_pos[1]+logo_img.shape[0], logo_pos[0]:logo_pos[0]+logo_img.shape[1]] = logo_img[:, :, :3] + mask = logo_img[:, :, 3] # alpha channel + for c in range(3): + display_frame[logo_pos[1]:logo_pos[1]+logo_img.shape[0], logo_pos[0]:logo_pos[0]+logo_img.shape[1], c] = ( + logo_img[:, :, c] * (mask / 255.0) + display_frame[logo_pos[1]:logo_pos[1]+logo_img.shape[0], logo_pos[0]:logo_pos[0]+logo_img.shape[1], c] * (1.0 - mask / 255.0)).astype(np.uint8) + + cv2.namedWindow("QR Scanner", cv2.WINDOW_NORMAL) + cv2.setWindowProperty("QR Scanner", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) + cv2.imshow("QR Scanner", display_frame) + + key = cv2.waitKey(1) + if key == 27 or key == ord('q'): + cap.release() + cv2.destroyAllWindows() + exit() diff --git a/qr_scan_old.py b/qr_scan_old.py new file mode 100644 index 0000000..fa2c5d6 --- /dev/null +++ b/qr_scan_old.py @@ -0,0 +1,160 @@ +import cv2 +from pyzbar.pyzbar import decode +import time +import numpy as np +from urllib.parse import urlparse, parse_qs, unquote +from PIL import ImageFont, ImageDraw, Image +import os +import subprocess + +# Fenstergröße & Farben +window_width, window_height = 1920, 1080 + # Habegger Gold +COLOR_GREEN = (0, 255, 0) +COLOR_YELLOW = (0, 255, 255) +COLOR_BACKGROUND = (45, 46, 47) # HTML-Farbe #2d2e2f +COLOR_WHITE = (255, 255, 255) +COLOR_GOLD = (131, 188, 214) # BGR für Habegger-Gold + +# Font & Logo +FONT_PATH = os.path.join("fonts", "Tisa Sans Pro Regular.ttf" + "") # Alternativ: Georgia.ttf, falls vorhanden +FONT_SIZE = 48 +font = ImageFont.truetype(FONT_PATH, FONT_SIZE) +logo_path = "images/habegger_logo_goldbar_whitefont.png" # Logo mit sichtbarem Text +logo_pil = Image.open(logo_path).convert("RGBA") +logo_pil.thumbnail((300, 100)) +logo_pil = Image.alpha_composite(Image.new("RGBA", logo_pil.size, (0, 0, 0, 0)), logo_pil) +logo_img = np.array(logo_pil) +logo_img = cv2.cvtColor(logo_img, cv2.COLOR_RGBA2BGRA) + +def draw_unicode_text(cv2_image, text, position, color=COLOR_WHITE): + img_pil = Image.fromarray(cv2_image) + draw = ImageDraw.Draw(img_pil) + draw.text(position, text, font=font, fill=color) + return np.array(img_pil) + +def parse_qr_data(data): + try: + parsed = urlparse(data) + query = parse_qs(parsed.query) + mail = query.get("mail", [""])[0] + name = query.get("name", [""])[0] + name = unquote(name, encoding="utf-8", errors="replace") + name = (name.replace("鐼", "ö") + .replace("ü", "ü") + .replace("ä", "ä") + .replace("Ö", "Ö") + .replace("Ü", "Ü") + .replace("Ãß", "ß")) + if "@habegger.ch" in mail.lower() and name.strip(): + return mail.strip(), name.strip() + except: + pass + return None, None + +def start_game(mail, name): + with open("last_user.txt", "w", encoding="utf-8-sig") as f: + f.write(f"{mail}\n{name}") + subprocess.run(["python", "loader_game.py"]) + +# Endlosschleife für den Scanner +while True: + detected_color = COLOR_GOLD + detected_time = None + cap = cv2.VideoCapture(0) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) + + # Rahmenfarbe wird später im Loop anhand der Zeit gesetzt + last_display_text = "" + start_timer = None + game_started = False + + while True: + success, frame = cap.read() + if not success: + break + + decoded_objects = decode(frame) + # QR-Status zurücksetzen nach 5 Sek. + if detected_time and time.time() - detected_time >= 5.0: + detected_color = COLOR_GOLD + frame_color = COLOR_GOLD + + if decoded_objects: + detected_time = time.time() + for obj in decoded_objects: + points = obj.polygon + if len(points) > 4: + hull = cv2.convexHull(np.array([point for point in points], dtype=np.float32)) + points = hull.reshape(-1, 2) + for i in range(len(points)): + pt1 = tuple(points[i]) + pt2 = tuple(points[(i + 1) % len(points)]) + cv2.line(frame, pt1, pt2, COLOR_GREEN, 3) + + data = obj.data.decode("utf-8") + mail, name = parse_qr_data(data) + + if mail and name: + detected_color = COLOR_GREEN + last_display_text = f"Richtiger QR-Code: Hallo {name}" + detected_time = time.time() + start_timer = time.time() + game_started = True + last_mail = mail + last_name = name + else: + detected_color = COLOR_YELLOW + detected_time = time.time() + last_display_text = "Bitte dein HAG Last-Thursday Code scannen." + break + + if start_timer and time.time() - start_timer >= 5.0: + cap.release() + cv2.destroyAllWindows() + start_game(last_mail, last_name) + break + + display_frame = np.full((window_height, window_width, 3), COLOR_BACKGROUND, dtype=np.uint8) + cam_h, cam_w = frame.shape[:2] + x_offset = (window_width - cam_w) // 2 + y_offset = (window_height - cam_h) // 2 + display_frame[y_offset:y_offset+cam_h, x_offset:x_offset+cam_w] = frame + + display_frame = draw_unicode_text(display_frame, + "Scanne deinen QR-Code um das Spiel zu starten.", + (80, 100), COLOR_WHITE) + + if last_display_text: + display_frame = draw_unicode_text(display_frame, last_display_text, + (80, window_height - 100), COLOR_WHITE) + + # Kamera-Rahmen + if detected_time and time.time() - detected_time < 5.0: + current_color = detected_color + else: + current_color = COLOR_GOLD + + cv2.rectangle(display_frame, (x_offset, y_offset), + (x_offset + cam_w, y_offset + cam_h), current_color, 6) + + # Logo oben rechts ohne Alpha-Blending-Probleme + logo_pos = (window_width - logo_img.shape[1] - 40, 40) + overlay = np.zeros_like(display_frame, dtype=np.uint8) + overlay[logo_pos[1]:logo_pos[1]+logo_img.shape[0], logo_pos[0]:logo_pos[0]+logo_img.shape[1]] = logo_img[:, :, :3] + mask = logo_img[:, :, 3] # alpha channel + for c in range(3): + display_frame[logo_pos[1]:logo_pos[1]+logo_img.shape[0], logo_pos[0]:logo_pos[0]+logo_img.shape[1], c] = ( + logo_img[:, :, c] * (mask / 255.0) + display_frame[logo_pos[1]:logo_pos[1]+logo_img.shape[0], logo_pos[0]:logo_pos[0]+logo_img.shape[1], c] * (1.0 - mask / 255.0)).astype(np.uint8) + + cv2.namedWindow("QR Scanner", cv2.WINDOW_NORMAL) + cv2.setWindowProperty("QR Scanner", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) + cv2.imshow("QR Scanner", display_frame) + + key = cv2.waitKey(1) + if key == 27 or key == ord('q'): + cap.release() + cv2.destroyAllWindows() + exit() diff --git a/roll.ogg b/roll.ogg new file mode 100644 index 0000000..dcef05c Binary files /dev/null and b/roll.ogg differ diff --git a/zip.ogg b/zip.ogg new file mode 100644 index 0000000..aa5237d Binary files /dev/null and b/zip.ogg differ