335 lines
13 KiB
Python
335 lines
13 KiB
Python
import pygame
|
||
import sys
|
||
import random
|
||
import os
|
||
from typing import List, Tuple
|
||
|
||
# ----------------------------------------------------------
|
||
# Design / Layout
|
||
# ----------------------------------------------------------
|
||
WINDOW_W, WINDOW_H = 900, 900
|
||
BG_COLOR = (10, 58, 94) # Global-Match Blau
|
||
HUD_BAR_H = 88
|
||
PADDING = 28
|
||
GRID_COLS, GRID_ROWS = 4, 3 # 12 Karten (6 Paare)
|
||
GAP = 18
|
||
CARD_RADIUS = 20
|
||
CARD_BORDER = (220, 226, 235)
|
||
CARD_FACE = (255, 255, 255)
|
||
CARD_TEXT = (22, 44, 66)
|
||
BADGE_BG = (18, 122, 138)
|
||
BADGE_TEXT = (255, 255, 255)
|
||
HIGHLIGHT = (255, 224, 128)
|
||
|
||
LOGO_FILE = "LogoSpiel.png" # <— Dein Logo (Rückseite)
|
||
|
||
MISMATCH_DELAY_MS = 900
|
||
SINGLEPLAYER_SECONDS = 90
|
||
FPS = 60
|
||
|
||
# ----------------------------------------------------------
|
||
# Daten (Deutsch)
|
||
# ----------------------------------------------------------
|
||
DATA = {
|
||
"Europa": [
|
||
("Deutschland", "Berlin"), ("Frankreich", "Paris"), ("Italien", "Rom"),
|
||
("Spanien", "Madrid"), ("Portugal", "Lissabon"), ("Niederlande", "Amsterdam"),
|
||
("Belgien", "Brüssel"), ("Österreich", "Wien"), ("Schweiz", "Bern"),
|
||
("Polen", "Warschau"), ("Tschechien", "Prag"), ("Ungarn", "Budapest"),
|
||
("Dänemark", "Kopenhagen"), ("Schweden", "Stockholm"), ("Norwegen", "Oslo")
|
||
],
|
||
"Amerika": [
|
||
("USA", "Washington, D.C."), ("Kanada", "Ottawa"), ("Mexiko", "Mexiko-Stadt"),
|
||
("Brasilien", "Brasília"), ("Argentinien", "Buenos Aires"), ("Chile", "Santiago"),
|
||
("Peru", "Lima"), ("Kolumbien", "Bogotá"), ("Venezuela", "Caracas"),
|
||
("Uruguay", "Montevideo"), ("Bolivien", "Sucre"), ("Ecuador", "Quito")
|
||
],
|
||
"Asien": [
|
||
("China", "Peking"), ("Japan", "Tokio"), ("Südkorea", "Seoul"),
|
||
("Indien", "Neu-Delhi"), ("Pakistan", "Islamabad"),
|
||
("Bangladesch", "Dhaka"), ("Sri Lanka", "Sri Jayewardenepura Kotte"),
|
||
("Nepal", "Kathmandu"), ("Saudi-Arabien", "Riad"),
|
||
("VAE", "Abu Dhabi"), ("Türkei", "Ankara"), ("Indonesien", "Jakarta"),
|
||
("Malaysia", "Kuala Lumpur"), ("Thailand", "Bangkok"), ("Vietnam", "Hanoi")
|
||
],
|
||
}
|
||
|
||
# ----------------------------------------------------------
|
||
# Helpers
|
||
# ----------------------------------------------------------
|
||
def load_logo_scaled(size: Tuple[int, int]) -> pygame.Surface:
|
||
"""Lädt LogoSpiel.png und skaliert mit 10% Rand. Fallback: dunkles GM-Pattern."""
|
||
w, h = size
|
||
surf = pygame.Surface((w, h), pygame.SRCALPHA)
|
||
# weiße Karte
|
||
pygame.draw.rect(surf, CARD_FACE, (0, 0, w, h), border_radius=CARD_RADIUS)
|
||
pygame.draw.rect(surf, CARD_BORDER, (0, 0, w, h), width=2, border_radius=CARD_RADIUS)
|
||
|
||
inner_pad = int(min(w, h) * 0.12)
|
||
logo_rect = pygame.Rect(inner_pad, inner_pad, w - 2*inner_pad, h - 2*inner_pad)
|
||
|
||
if os.path.exists(LOGO_FILE):
|
||
try:
|
||
img = pygame.image.load(LOGO_FILE).convert_alpha()
|
||
img = scale_to_fit(img, logo_rect.size)
|
||
surf.blit(img, (
|
||
logo_rect.centerx - img.get_width() // 2,
|
||
logo_rect.centery - img.get_height() // 2
|
||
))
|
||
return surf
|
||
except Exception:
|
||
pass
|
||
|
||
# Fallback: Text "GLOBAL MATCH"
|
||
font = pygame.font.SysFont(None, int(h * 0.16), bold=True)
|
||
t1 = font.render("GLOBAL", True, (10, 80, 110))
|
||
t2 = font.render("MATCH", True, (240, 180, 60))
|
||
surf.blit(t1, (w//2 - t1.get_width()//2, h//2 - t1.get_height()))
|
||
surf.blit(t2, (w//2 - t2.get_width()//2, h//2 + 4))
|
||
return surf
|
||
|
||
def scale_to_fit(img: pygame.Surface, target_size: Tuple[int, int]) -> pygame.Surface:
|
||
tw, th = target_size
|
||
iw, ih = img.get_width(), img.get_height()
|
||
scale = min(tw / iw, th / ih)
|
||
return pygame.transform.smoothscale(img, (int(iw * scale), int(ih * scale)))
|
||
|
||
def wrap_text(text: str, font: pygame.font.Font, max_w: int) -> List[str]:
|
||
words = text.split()
|
||
lines, line = [], ""
|
||
for w in words:
|
||
t = (line + " " + w).strip()
|
||
if font.size(t)[0] <= max_w:
|
||
line = t
|
||
else:
|
||
if line:
|
||
lines.append(line)
|
||
line = w
|
||
if line:
|
||
lines.append(line)
|
||
return lines
|
||
|
||
def pick_payloads(level: str, n_pairs=6) -> List[dict]:
|
||
pool = DATA[level][:] if level in DATA else DATA["Europa"][:]
|
||
random.shuffle(pool)
|
||
chosen = pool[:n_pairs]
|
||
payloads = []
|
||
pid = 0
|
||
for land, cap in chosen:
|
||
payloads.append({"pair": pid, "label": land, "kind": "country"})
|
||
payloads.append({"pair": pid, "label": cap, "kind": "capital"})
|
||
pid += 1
|
||
random.shuffle(payloads)
|
||
return payloads
|
||
|
||
# ----------------------------------------------------------
|
||
# Card
|
||
# ----------------------------------------------------------
|
||
class Card:
|
||
def __init__(self, rect: pygame.Rect, payload: dict, fonts, back_img: pygame.Surface):
|
||
self.rect = rect
|
||
self.payload = payload
|
||
self.font, self.small = fonts
|
||
self.back = pygame.transform.smoothscale(back_img, (rect.w, rect.h))
|
||
self.front = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
|
||
self.is_revealed = False
|
||
self.is_matched = False
|
||
self._render_front()
|
||
|
||
def _render_front(self):
|
||
w, h = self.rect.w, self.rect.h
|
||
# weiße Karte + Rand
|
||
pygame.draw.rect(self.front, CARD_FACE, (0, 0, w, h), border_radius=CARD_RADIUS)
|
||
pygame.draw.rect(self.front, CARD_BORDER, (0, 0, w, h), width=2, border_radius=CARD_RADIUS)
|
||
|
||
# Badge
|
||
badge = "Land" if self.payload["kind"] == "country" else "Hauptstadt"
|
||
bsurf = self.small.render(badge, True, BADGE_TEXT)
|
||
bw, bh = bsurf.get_size()
|
||
badge_bg = pygame.Surface((bw + 18, bh + 8), pygame.SRCALPHA)
|
||
pygame.draw.rect(badge_bg, BADGE_BG, badge_bg.get_rect(), border_radius=12)
|
||
badge_bg.blit(bsurf, (9, 4))
|
||
self.front.blit(badge_bg, (w - badge_bg.get_width() - 12, 12))
|
||
|
||
# Text
|
||
lines = wrap_text(self.payload["label"], self.font, w - 28)
|
||
total_h = len(lines) * self.font.get_height()
|
||
y = h//2 - total_h//2
|
||
for line in lines:
|
||
ts = self.font.render(line, True, CARD_TEXT)
|
||
self.front.blit(ts, (w//2 - ts.get_width()//2, y))
|
||
y += self.font.get_height()
|
||
|
||
def draw(self, screen: pygame.Surface):
|
||
screen.blit(self.front if (self.is_revealed or self.is_matched) else self.back,
|
||
(self.rect.x, self.rect.y))
|
||
|
||
def handle_click(self, pos) -> bool:
|
||
if self.rect.collidepoint(pos) and not self.is_matched and not self.is_revealed:
|
||
self.is_revealed = True
|
||
return True
|
||
return False
|
||
|
||
# ----------------------------------------------------------
|
||
# Main Game
|
||
# ----------------------------------------------------------
|
||
def run_memory(level: str = "Europa", mode: str = "single"):
|
||
"""
|
||
level: 'Europa' | 'Amerika' | 'Asien'
|
||
mode : 'single' (Zeit) | 'two' (Punkte, abwechselnd)
|
||
"""
|
||
pygame.init()
|
||
screen = pygame.display.set_mode((WINDOW_W, WINDOW_H))
|
||
pygame.display.set_caption(f"Global Match – {level} ({'1 Spieler' if mode=='single' else '2 Spieler'})")
|
||
clock = pygame.time.Clock()
|
||
|
||
ui_font = pygame.font.SysFont(None, 32, bold=True)
|
||
big_font = pygame.font.SysFont(None, 50, bold=True)
|
||
card_font = pygame.font.SysFont(None, 28, bold=True)
|
||
small_font = pygame.font.SysFont(None, 20, bold=True)
|
||
|
||
# Grid berechnen
|
||
grid_w = WINDOW_W - 2 * PADDING
|
||
grid_h = WINDOW_H - HUD_BAR_H - 2 * PADDING
|
||
cell_w = (grid_w - (GRID_COLS - 1) * GAP) // GRID_COLS
|
||
cell_h = (grid_h - (GRID_ROWS - 1) * GAP) // GRID_ROWS
|
||
card_size = min(cell_w, cell_h)
|
||
start_x = PADDING + (grid_w - (card_size * GRID_COLS + GAP * (GRID_COLS - 1))) // 2
|
||
start_y = HUD_BAR_H + PADDING + (grid_h - (card_size * GRID_ROWS + GAP * (GRID_ROWS - 1))) // 2
|
||
|
||
# Karten erzeugen
|
||
back_img = load_logo_scaled((card_size, card_size))
|
||
payloads = pick_payloads(level, n_pairs=6)
|
||
|
||
cards: List[Card] = []
|
||
k = 0
|
||
for r in range(GRID_ROWS):
|
||
for c in range(GRID_COLS):
|
||
if k >= len(payloads):
|
||
continue
|
||
rect = pygame.Rect(start_x + c * (card_size + GAP),
|
||
start_y + r * (card_size + GAP),
|
||
card_size, card_size)
|
||
cards.append(Card(rect, payloads[k], (card_font, small_font), back_img))
|
||
k += 1
|
||
|
||
# Spielstatus
|
||
revealed: List[Card] = []
|
||
lock_until = 0
|
||
total_matches = 0
|
||
need_matches = len(payloads) // 2
|
||
p_turn = 1
|
||
score = {1: 0, 2: 0}
|
||
time_left = SINGLEPLAYER_SECONDS * 1000 if mode == "single" else None
|
||
end_text = None
|
||
|
||
# Loop
|
||
while True:
|
||
dt = clock.tick(FPS)
|
||
now = pygame.time.get_ticks()
|
||
|
||
# Events
|
||
for e in pygame.event.get():
|
||
if e.type == pygame.QUIT:
|
||
pygame.quit()
|
||
sys.exit()
|
||
if e.type == pygame.KEYDOWN and end_text:
|
||
if e.key in (pygame.K_RETURN, pygame.K_SPACE):
|
||
pygame.quit()
|
||
return
|
||
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1 and not end_text:
|
||
if now < lock_until:
|
||
continue
|
||
for card in cards:
|
||
if card.handle_click(e.pos):
|
||
revealed.append(card)
|
||
if len(revealed) == 2:
|
||
a, b = revealed
|
||
# Match (gleiches Paar, aber unterschiedliche Art)
|
||
if a.payload["pair"] == b.payload["pair"] and a.payload["kind"] != b.payload["kind"]:
|
||
a.is_matched = b.is_matched = True
|
||
revealed.clear()
|
||
total_matches += 1
|
||
if mode == "two":
|
||
score[p_turn] += 1
|
||
if total_matches == need_matches:
|
||
if mode == "single":
|
||
end_text = "Geschafft! Alle 6 Paare."
|
||
else:
|
||
if score[1] > score[2]:
|
||
end_text = f"Spielende – Spieler 1 gewinnt ({score[1]}:{score[2]})"
|
||
elif score[2] > score[1]:
|
||
end_text = f"Spielende – Spieler 2 gewinnt ({score[2]}:{score[1]})"
|
||
else:
|
||
end_text = f"Unentschieden ({score[1]}:{score[2]})"
|
||
else:
|
||
lock_until = now + MISMATCH_DELAY_MS
|
||
elif len(revealed) > 2:
|
||
# Sicherheitsreset – nur die letzten 2 behalten
|
||
for old in revealed[:-2]:
|
||
old.is_revealed = False
|
||
revealed = revealed[-2:]
|
||
|
||
# Timer (Singleplayer)
|
||
if not end_text and mode == "single" and time_left is not None:
|
||
time_left -= dt
|
||
if time_left <= 0:
|
||
time_left = 0
|
||
end_text = "Zeit abgelaufen!"
|
||
|
||
# Mismatch zurückdrehen + Spielerwechsel
|
||
if not end_text and len(revealed) == 2 and lock_until and now >= lock_until:
|
||
a, b = revealed
|
||
a.is_revealed = False
|
||
b.is_revealed = False
|
||
revealed.clear()
|
||
lock_until = 0
|
||
if mode == "two":
|
||
p_turn = 2 if p_turn == 1 else 1
|
||
|
||
# ----------------- Render -----------------
|
||
screen.fill(BG_COLOR)
|
||
|
||
# HUD
|
||
pygame.draw.rect(screen, (7, 42, 70), (0, 0, WINDOW_W, HUD_BAR_H))
|
||
title = ui_font.render(f"Global Match – {level}", True, (255, 255, 255))
|
||
screen.blit(title, (PADDING, HUD_BAR_H//2 - title.get_height()//2))
|
||
|
||
if mode == "single":
|
||
tsec = (time_left // 1000) if time_left is not None else 0
|
||
timer = ui_font.render(f"Zeit: {tsec}s", True, HIGHLIGHT)
|
||
screen.blit(timer, (WINDOW_W - PADDING - timer.get_width(), HUD_BAR_H//2 - timer.get_height()//2))
|
||
else:
|
||
s = ui_font.render(f"P1: {score[1]} P2: {score[2]} Zug: P{p_turn}", True, HIGHLIGHT)
|
||
screen.blit(s, (WINDOW_W - PADDING - s.get_width(), HUD_BAR_H//2 - s.get_height()//2))
|
||
|
||
# Karten
|
||
for card in cards:
|
||
card.draw(screen)
|
||
|
||
# Overlay / Ende
|
||
if end_text:
|
||
overlay = pygame.Surface((WINDOW_W, WINDOW_H), pygame.SRCALPHA)
|
||
overlay.fill((0, 0, 0, 140))
|
||
screen.blit(overlay, (0, 0))
|
||
msg = big_font.render(end_text, True, (255, 255, 255))
|
||
sub = ui_font.render("ENTER/SPACE zum Beenden", True, (240, 240, 240))
|
||
screen.blit(msg, (WINDOW_W//2 - msg.get_width()//2, WINDOW_H//2 - 30))
|
||
screen.blit(sub, (WINDOW_W//2 - sub.get_width()//2, WINDOW_H//2 + 20))
|
||
|
||
pygame.display.flip()
|
||
|
||
# ----------------------------------------------------------
|
||
# Direktstart (zu Testzwecken)
|
||
# ----------------------------------------------------------
|
||
if __name__ == "__main__":
|
||
lvl = "Europa"
|
||
md = "single"
|
||
if len(sys.argv) >= 2:
|
||
lvl = sys.argv[1]
|
||
if len(sys.argv) >= 3:
|
||
md = sys.argv[2]
|
||
run_memory(level=lvl, mode=md)
|
||
|
||
|