Compare commits

..

13 Commits

Author SHA1 Message Date
kobold-ai 6c62676d0d feat: Add lore pack, UI adjustments, requirements.txt and INSTALL.md 2026-10-29 08:51:28 +01:00
kobold-ai fb789e8866 Add Escape Adventure navigation link to bootle.dev.waiswuaschd.de 2026-09-22 16:27:10 +02:00
kobold-ai ecf9d8283a Revert: Return to 611a94a envelope structure and remove parchment_texture.jpg without extra adaptations (v3.2) 2026-09-21 20:37:09 +02:00
kobold-ai 2b5ae98b91 Design: Restore clean antique envelope outlines without muddy image texture, add sharp V-flap and body fold lines (v3.1) 2026-09-21 20:18:02 +02:00
kobold-ai ea5bc12e21 Design: Combine CodePen 4-flap pocket geometry with majestic slide-out animation 2026-09-21 19:50:44 +02:00
kobold-ai 8c4768747b Design: Implement CodePen-style opening envelope with Hogwarts aesthetics, central wax seal, and subtle top-left return address 2026-09-21 18:49:40 +02:00
kobold-ai 7e20d243e8 Design: Envelope back with distinct V-flap closure, top-left sender address only, and completely sealed interior 2026-09-21 17:41:19 +02:00
kobold-ai 3e5c3c1eeb Enhance: Authentic seamless 4-flap closed envelope with 100% hidden interior letter 2026-09-21 17:12:05 +02:00
kobold-ai 1cbea5e84f Enhance: Conceal closed letter sheet, enrich Hogwarts lore story, and allow calm reading before boarding Hogwarts Express 2026-09-21 16:53:45 +02:00
kobold-ai 611a94ab43 Feature: Restore closed antique envelope state with 3D flap opening and sliding letter reveal on seal break 2026-09-21 16:40:02 +02:00
kobold-ai 8d8018347c Design: Antique Hogwarts parchment letter with light marbling, realistic shading, sketched seal, and handwriting font 2026-09-21 16:33:11 +02:00
kobold-ai b1d88da681 Feature: Leaderboard difficulty isolation, proposal category select, numeric answer matcher, 25 new questions (65 total) 2026-09-21 14:23:08 +02:00
kobold-ai a938123271 Feature: 100-point scoring, question memory & spaced repetition, random wizard name generator, cache-busting v2.2, gold footer pills 2026-09-21 13:53:33 +02:00
19 changed files with 2544 additions and 851 deletions
+89
View File
@@ -0,0 +1,89 @@
# Magisches Harry Potter Escape Quiz Portal
Interaktives Harry Potter Escape Quiz Portal (FastAPI & MariaDB).
## Systemanforderungen
- Debian 12 / 13 (oder kompatibles Linux)
- Python 3.13+ mit `python3-venv`
- Nginx (Reverse Proxy mit SSL)
- MariaDB Server auf \`db.waiswuaschd.de:3306\` (Datenbank: \`v2_db_quiz\`)
## 1. Installation
```bash
# Repository klonen
cd /home/maxi/Projekt
git clone https://gitea.sp13l.de/kobold-ai/quiz.git quiz.waiswuaschd.de
cd quiz.waiswuaschd.de
# Python Virtualenv erstellen und Abh?ngigkeiten installieren
python3 -m venv venv
./venv/bin/pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
```
## 2. Umgebungsvariablen (.env)
Erstelle im Projektverzeichnis eine `.env` Datei:
```ini
DB_HOST=db.waiswuaschd.de
DB_PORT=3306
DB_NAME=v2_db_quiz
DB_USER=v2_usr_quiz
DB_PASS=Quiz_Pass_2026_Secure!
SECRET_KEY=hogwarts_magic_super_secret_key_2026_dev_quiz
ADMIN_USER=admin
ADMIN_PASS=Alohomora2026!
PORT=8001
```
## 3. Systemd Service (/etc/systemd/system/quiz.service)
```ini
[Unit]
Description=Magisches Quiz Portal (FastAPI)
After=network.target
[Service]
User=maxi
Group=maxi
WorkingDirectory=/home/maxi/Projekt/quiz.waiswuaschd.de
ExecStart=/home/maxi/Projekt/quiz.waiswuaschd.de/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001 --workers 1
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
```
Aktivieren und starten:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now quiz
```
## 4. Nginx Reverse Proxy
```nginx
server {
listen 80;
server_name quiz.dev.waiswuaschd.de;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name quiz.dev.waiswuaschd.de;
# SSL Zertifikate hier konfigurieren (z. B. via certbot)
location /static/ {
alias /home/maxi/Projekt/quiz.waiswuaschd.de/app/static/;
add_header Cache-Control "public, max-age=3600, must-revalidate";
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
+105 -14
View File
@@ -20,7 +20,7 @@ from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequ
load_dotenv() load_dotenv()
app = FastAPI(title="Magisches Harry-Potter-Escape-Quiz") app = FastAPI(title="PotterTrivia – Das Harry-Potter-Wissensquiz")
SECRET_KEY = os.getenv("SECRET_KEY", "hogwarts_secret_default_key_2026") SECRET_KEY = os.getenv("SECRET_KEY", "hogwarts_secret_default_key_2026")
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="quiz_session") app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="quiz_session")
@@ -49,6 +49,7 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
random.shuffle(options_list) random.shuffle(options_list)
return { return {
"id": q.id,
"question_number": number, "question_number": number,
"total_questions": total, "total_questions": total,
"category": q.category, "category": q.category,
@@ -59,10 +60,15 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
} }
def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict: def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
if total <= 10 and score <= 10:
pct = (score / total) if total > 0 else 0 pct = (score / total) if total > 0 else 0
else:
max_possible = (total * 100) if total <= 10 else total
pct = (score / max_possible) if max_possible > 0 else 0
is_de = (lang != "en") is_de = (lang != "en")
if score >= 10 or pct >= 1.0: if pct >= 0.90:
grade = "O" grade = "O"
title = "🧙‍♂️ Großmeister der Zauberei" if is_de else "🧙‍♂️ Grandmaster of Wizardry" title = "🧙‍♂️ Großmeister der Zauberei" if is_de else "🧙‍♂️ Grandmaster of Wizardry"
subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)" subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)"
@@ -76,7 +82,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
"The Ministry of Magic bows in deep awe!" "The Ministry of Magic bows in deep awe!"
) )
badge_class = "tier-outstanding" badge_class = "tier-outstanding"
elif score >= 8 or pct >= 0.8: elif pct >= 0.70:
grade = "E" grade = "E"
title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard" title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard"
subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)" subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)"
@@ -88,7 +94,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
"Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous." "Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous."
) )
badge_class = "tier-exceeds" badge_class = "tier-exceeds"
elif score >= 5 or pct >= 0.5: elif pct >= 0.50:
grade = "A" grade = "A"
title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student" title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student"
subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)" subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)"
@@ -100,7 +106,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
"but you bravely fought your way through. A solid magical performance!" "but you bravely fought your way through. A solid magical performance!"
) )
badge_class = "tier-acceptable" badge_class = "tier-acceptable"
elif score >= 2 or pct >= 0.2: elif pct >= 0.20:
grade = "M" grade = "M"
title = "🧹 Quaffel-Fänger in Not" if is_de else "🧹 Quaffle Catcher in Distress" title = "🧹 Quaffel-Fänger in Not" if is_de else "🧹 Quaffle Catcher in Distress"
subtitle = "Note: M (Mies)" if is_de else "Grade: P (Poor)" subtitle = "Note: M (Mies)" if is_de else "Grade: P (Poor)"
@@ -148,23 +154,79 @@ async def index(request: Request, db: Session = Depends(get_db)):
context={"question_count": active_count} context={"question_count": active_count}
) )
VALID_CATEGORIES = ["all", "books", "movies", "creatures", "spells", "characters", "hogwarts", "quidditch", "lore", "general"]
@app.post("/api/game/start") @app.post("/api/game/start")
async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)): async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)):
lang = payload.lang if payload and payload.lang in ["de", "en"] else "de" lang = payload.lang if payload and payload.lang in ["de", "en"] else "de"
difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all" difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all"
category = payload.category if payload and payload.category in VALID_CATEGORIES else "all"
base_query = db.query(Question).filter(Question.status == "active")
if category != "all":
if category == "books":
cat_query = base_query.filter((Question.category == "books") | (Question.question.like("%Kröter%")))
else:
cat_query = base_query.filter(Question.category == category)
else:
cat_query = base_query
query = db.query(Question).filter(Question.status == "active")
if difficulty != "all": if difficulty != "all":
query = query.filter(Question.difficulty == difficulty) strict_pool = cat_query.filter(Question.difficulty == difficulty).all()
if len(strict_pool) >= 10 or category == "all":
pool = strict_pool
else:
# Keep all difficulty matches in this category and top up from the same category
cat_all = cat_query.all()
strict_ids = {q.id for q in strict_pool}
extra = [q for q in cat_all if q.id not in strict_ids]
random.shuffle(extra)
pool = strict_pool + extra[:max(0, 10 - len(strict_pool))]
else:
pool = cat_query.all()
pool = query.all()
if not pool: if not pool:
pool = db.query(Question).filter(Question.status == "active").all() pool = db.query(Question).filter(Question.status == "active").all()
if not pool: if not pool:
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.") raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
sample_size = min(10, len(pool)) recent_set = set(payload.recent_ids or []) if payload else set()
selected_questions = random.sample(pool, sample_size) wrong_set = set(payload.wrong_ids or []) if payload else set()
# Priority pool 1: Questions user previously failed (learning pool)
wrong_pool = [q for q in pool if q.id in wrong_set]
# Priority pool 2: Fresh questions not seen recently and not in wrong pool
fresh_pool = [q for q in pool if q.id not in recent_set and q.id not in wrong_set]
# Priority pool 3: Recent questions fallback
recent_pool = [q for q in pool if q.id in recent_set and q.id not in wrong_set]
selected_questions = []
# Up to 3 questions from wrong_pool for spaced repetition
if wrong_pool:
k_wrong = min(3, len(wrong_pool))
selected_questions.extend(random.sample(wrong_pool, k_wrong))
# Fill remaining from fresh_pool
needed = min(10, len(pool)) - len(selected_questions)
if needed > 0 and fresh_pool:
k_fresh = min(needed, len(fresh_pool))
selected_questions.extend(random.sample(fresh_pool, k_fresh))
# If still needed, fill from recent_pool
needed = min(10, len(pool)) - len(selected_questions)
if needed > 0 and recent_pool:
k_recent = min(needed, len(recent_pool))
selected_questions.extend(random.sample(recent_pool, k_recent))
# Fallback to any remaining from pool
needed = min(10, len(pool)) - len(selected_questions)
if needed > 0:
remaining = [q for q in pool if q not in selected_questions]
if remaining:
selected_questions.extend(random.sample(remaining, min(needed, len(remaining))))
# Shuffle to mix learning and fresh questions
random.shuffle(selected_questions)
question_ids = [q.id for q in selected_questions] question_ids = [q.id for q in selected_questions]
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
@@ -211,6 +273,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return { return {
"correct": False, "correct": False,
"score": session.score, "score": session.score,
"points_earned": 0,
"question_id": None,
"attempts_used": session.attempts_current, "attempts_used": session.attempts_current,
"attempts_left": 0, "attempts_left": 0,
"is_finished": True, "is_finished": True,
@@ -226,7 +290,15 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en) correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
if correct: if correct:
session.score += 1 # Tiered points based on attempt
if session.attempts_current == 0:
points_earned = 100
elif session.attempts_current == 1:
points_earned = 70
else:
points_earned = 40
session.score += points_earned
session.current_question_idx += 1 session.current_question_idx += 1
session.attempts_current = 0 session.attempts_current = 0
@@ -248,6 +320,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return { return {
"correct": True, "correct": True,
"score": session.score, "score": session.score,
"points_earned": points_earned,
"question_id": current_q_id,
"attempts_used": 0, "attempts_used": 0,
"attempts_left": 3, "attempts_left": 3,
"is_finished": is_finished, "is_finished": is_finished,
@@ -286,6 +360,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return { return {
"correct": False, "correct": False,
"score": session.score, "score": session.score,
"points_earned": 0,
"question_id": current_q_id,
"attempts_used": 3, "attempts_used": 3,
"attempts_left": 0, "attempts_left": 0,
"is_finished": is_finished, "is_finished": is_finished,
@@ -298,6 +374,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return { return {
"correct": False, "correct": False,
"score": session.score, "score": session.score,
"points_earned": 0,
"question_id": current_q_id,
"attempts_used": session.attempts_current, "attempts_used": session.attempts_current,
"attempts_left": attempts_left, "attempts_left": attempts_left,
"is_finished": False, "is_finished": False,
@@ -329,7 +407,8 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
return {"hint": hint_text} return {"hint": hint_text}
@app.get("/finale", response_class=HTMLResponse) @app.get("/finale", response_class=HTMLResponse)
async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de"): async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de", diff: str = "all"):
max_score = 1000 if (total <= 10 and score > 10) else (total * 100 if total <= 10 else total)
tier = get_tier_evaluation(score, total, lang) tier = get_tier_evaluation(score, total, lang)
if time >= 60: if time >= 60:
minutes = time // 60 minutes = time // 60
@@ -338,15 +417,19 @@ async def finale(request: Request, score: int = 0, total: int = 10, time: int =
else: else:
time_str = f"{time} Sek." if lang != "en" else f"{time} sec." time_str = f"{time} Sek." if lang != "en" else f"{time} sec."
clean_diff = diff if diff in ["easy", "medium", "hard"] else "all"
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="finale.html", name="finale.html",
context={ context={
"score": score, "score": score,
"total": total, "total": total,
"max_score": max_score,
"time_seconds": time, "time_seconds": time,
"time_formatted": time_str, "time_formatted": time_str,
"lang": lang, "lang": lang,
"difficulty": clean_diff,
"tier": tier "tier": tier
} }
) )
@@ -401,7 +484,11 @@ async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = De
db.commit() db.commit()
db.refresh(entry) db.refresh(entry)
better_count = db.query(LeaderboardEntry).filter( better_query = db.query(LeaderboardEntry)
if entry.difficulty and entry.difficulty != "all":
better_query = better_query.filter(LeaderboardEntry.difficulty == entry.difficulty)
better_count = better_query.filter(
(LeaderboardEntry.score > entry.score) | (LeaderboardEntry.score > entry.score) |
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds < entry.time_seconds)) | ((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds < entry.time_seconds)) |
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds == entry.time_seconds) & (LeaderboardEntry.id < entry.id)) ((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds == entry.time_seconds) & (LeaderboardEntry.id < entry.id))
@@ -410,6 +497,7 @@ async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = De
return { return {
"success": True, "success": True,
"id": entry.id,
"entry_id": entry.id, "entry_id": entry.id,
"rank": rank, "rank": rank,
"nickname": entry.nickname, "nickname": entry.nickname,
@@ -471,10 +559,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
opts.append(opt.strip()) opts.append(opt.strip())
options_json = json.dumps(opts, ensure_ascii=False) options_json = json.dumps(opts, ensure_ascii=False)
chosen_cat = payload.category if payload.category in ["books", "movies", "hogwarts", "characters", "spells", "creatures", "quidditch", "lore", "general"] else "general"
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium" diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
new_suggestion = Question( new_suggestion = Question(
category="community", category=chosen_cat,
question=payload.frage.strip(), question=payload.frage.strip(),
canonical_answer=payload.antwort.strip(), canonical_answer=payload.antwort.strip(),
hint=payload.tipp.strip() if payload.tipp else None, hint=payload.tipp.strip() if payload.tipp else None,
@@ -487,9 +576,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
) )
db.add(new_suggestion) db.add(new_suggestion)
db.commit() db.commit()
db.refresh(new_suggestion)
return { return {
"success": True, "success": True,
"id": new_suggestion.id,
"message": "Vielen Dank! Dein Vorschlag wurde eingereicht und wird von uns geprüft, bevor er im Quiz erscheint." "message": "Vielen Dank! Dein Vorschlag wurde eingereicht und wird von uns geprüft, bevor er im Quiz erscheint."
} }
+67 -4
View File
@@ -33,11 +33,47 @@ def levenshtein_distance(s1: str, s2: str) -> int:
previous_row = current_row previous_row = current_row
return previous_row[-1] return previous_row[-1]
WORD_TO_NUM = {
"null": "0", "zero": "0",
"eins": "1", "ein": "1", "one": "1",
"zwei": "2", "two": "2",
"drei": "3", "three": "3",
"vier": "4", "four": "4",
"fuenf": "5", "five": "5",
"sechs": "6", "six": "6",
"sieben": "7", "seven": "7",
"acht": "8", "eight": "8",
"neun": "9", "nine": "9",
"zehn": "10", "ten": "10",
"elf": "11", "eleven": "11",
"zwoelf": "12", "twelve": "12",
"dreizehn": "13", "thirteen": "13",
"vierzehn": "14", "fourteen": "14",
"fuenfzehn": "15", "fifteen": "15",
"sechzehn": "16", "sixteen": "16",
"siebzehn": "17", "seventeen": "17",
"achtzehn": "18", "eighteen": "18",
"neunzehn": "19", "nineteen": "19",
"zwanzig": "20", "twenty": "20",
"fuenfzig": "50", "fifty": "50",
"hundert": "100", "einhundert": "100", "one hundred": "100",
"einhundertfuenfzig": "150", "one hundred fifty": "150", "one hundred and fifty": "150"
}
NUM_TO_WORDS = {}
for w, n in WORD_TO_NUM.items():
NUM_TO_WORDS.setdefault(n, []).append(w)
def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None, canonical_en: str | None = None) -> bool: def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None, canonical_en: str | None = None) -> bool:
clean_user = normalize_text(user_input) clean_user = normalize_text(user_input)
if not clean_user: if not clean_user:
return False return False
# Also consider digit conversion of user input if a word was typed
user_variants = [clean_user]
if clean_user in WORD_TO_NUM:
user_variants.append(WORD_TO_NUM[clean_user])
targets = [normalize_text(canonical)] targets = [normalize_text(canonical)]
if canonical_en: if canonical_en:
clean_en = normalize_text(canonical_en) clean_en = normalize_text(canonical_en)
@@ -56,18 +92,45 @@ def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None
if part_clean: if part_clean:
targets.append(part_clean) targets.append(part_clean)
for target in targets: # Expand any numeric target with corresponding number words and vice-versa
expanded_targets = list(targets)
for t in targets:
if t in NUM_TO_WORDS:
expanded_targets.extend(NUM_TO_WORDS[t])
if t in WORD_TO_NUM:
expanded_targets.append(WORD_TO_NUM[t])
for target in expanded_targets:
if not target: if not target:
continue continue
if clean_user == target: for u in user_variants:
if u == target:
return True return True
# For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match # For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match
if len(target) <= 3: if len(target) <= 3 or len(u) <= 3:
continue continue
# For medium words (4-6 chars), allow max 1 typo # For medium words (4-6 chars), allow max 1 typo
max_allowed = 1 if len(target) <= 6 else 2 max_allowed = 1 if len(target) <= 6 else 2
dist = levenshtein_distance(clean_user, target) dist = levenshtein_distance(u, target)
if dist <= max_allowed: if dist <= max_allowed:
return True return True
# Support compound answers connected by 'und' / 'and' (e.g. "Mantikor und Feuerkrabbe")
for sep in (" und ", " and "):
if sep in target:
parts = [p.strip() for p in target.split(sep) if len(p.strip()) >= 3]
if len(parts) == 2:
u_words = u.split()
matched_both = True
for part in parts:
if part in u:
continue
# Check fuzzy match against tokens or reversed order
part_matched = any(levenshtein_distance(w, part) <= (1 if len(part) <= 6 else 2) for w in u_words)
if not part_matched:
matched_both = False
break
if matched_both:
return True
return False return False
+6
View File
@@ -4,6 +4,9 @@ from typing import Optional, List
class GameStartRequest(BaseModel): class GameStartRequest(BaseModel):
lang: Optional[str] = "de" lang: Optional[str] = "de"
difficulty: Optional[str] = "all" difficulty: Optional[str] = "all"
category: Optional[str] = "all"
recent_ids: Optional[List[int]] = None
wrong_ids: Optional[List[int]] = None
class GameStartResponse(BaseModel): class GameStartResponse(BaseModel):
session_id: str session_id: str
@@ -24,6 +27,8 @@ class GameCheckRequest(BaseModel):
class GameCheckResponse(BaseModel): class GameCheckResponse(BaseModel):
correct: bool correct: bool
score: int score: int
points_earned: Optional[int] = 0
question_id: Optional[int] = None
attempts_used: int attempts_used: int
attempts_left: int attempts_left: int
max_attempts: int = 3 max_attempts: int = 3
@@ -34,6 +39,7 @@ class GameCheckResponse(BaseModel):
class SuggestionCreateRequest(BaseModel): class SuggestionCreateRequest(BaseModel):
frage: str frage: str
antwort: str antwort: str
category: Optional[str] = "general"
tipp: Optional[str] = None tipp: Optional[str] = None
name: Optional[str] = "Gast" name: Optional[str] = "Gast"
question_type: Optional[str] = "text" question_type: Optional[str] = "text"
+4
View File
@@ -4,6 +4,7 @@ from app.database import engine, SessionLocal, Base
from app.models import Question, AdminUser from app.models import Question, AdminUser
from app.auth import hash_password from app.auth import hash_password
from app.seed_40 import seed_40_questions from app.seed_40 import seed_40_questions
from app.seed_lore_pack import seed_lore_pack
def seed_db(): def seed_db():
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@@ -29,6 +30,9 @@ def seed_db():
print("Question table is empty, seeding 40 initial bilingual questions...") print("Question table is empty, seeding 40 initial bilingual questions...")
seed_40_questions() seed_40_questions()
# 3. Ensure Lore Pack (Books, Movies, Hard Creatures incl. Blast-Ended Skrewts) is present
seed_lore_pack()
finally: finally:
db.close() db.close()
+396
View File
@@ -0,0 +1,396 @@
import json
import os
import sys
from sqlalchemy import text
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.database import engine, SessionLocal
from app.models import Question
NEW_QUESTIONS_DATA = [
# --- EASY (1-8) ---
{
"category": "characters",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Wie heißt der gutmütige Wildhüter von Hogwarts und treue Freund von Harry?",
"canonical_answer": "Rubeus Hagrid",
"options": ["Rubeus Hagrid", "Argus Filch", "Garrick Ollivander", "Cornelius Fudge"],
"hint": "Er liebt gefährliche magische Kreaturen wie Drachen und Riesenspinnen.",
"question_en": "What is the name of the kind Hogwarts gamekeeper and loyal friend of Harry?",
"answer_en": "Rubeus Hagrid",
"options_en": ["Rubeus Hagrid", "Argus Filch", "Garrick Ollivander", "Cornelius Fudge"],
"hint_en": "He loves dangerous magical creatures like dragons and giant spiders."
},
{
"category": "lore",
"difficulty": "easy",
"question_type": "text",
"question": "Wie viele Jahre dauert die reguläre Schulzeit eines Schülers in Hogwarts von der 1. bis zur Abschlussklasse? (Bitte Zahl eingeben)",
"canonical_answer": "7",
"accepted_answers": json.dumps(["7", "sieben", "seven"]),
"options": None,
"hint": "Man startet mit 11 Jahren und legt im fünften Jahr die Z.A.G.-Prüfungen ab.",
"question_en": "How many years does a student typically attend Hogwarts from first year to final graduation? (Please enter as a number)",
"answer_en": "7",
"options_en": None,
"hint_en": "Students start at age 11 and take their N.E.W.T.s in their final year."
},
{
"category": "spells",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welcher Zauberspruch erzeugt helles Licht an der Spitze des Zauberstabs?",
"canonical_answer": "Lumos",
"options": ["Lumos", "Nox", "Alohomora", "Accio"],
"hint": "Der Gegenfluch zum Erlöschen des Lichts lautet Nox.",
"question_en": "Which wand-lighting charm produces bright light at the tip of the wand?",
"answer_en": "Lumos",
"options_en": ["Lumos", "Nox", "Alohomora", "Accio"],
"hint_en": "Its counter-curse to extinguish the light is Nox."
},
{
"category": "creatures",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welche Art von magischem Wesen bewacht die Falltür zum Stein der Weisen und hört auf den Namen Fluffy?",
"canonical_answer": "Dreiköpfiger Hund",
"options": ["Dreiköpfiger Hund", "Basilisken-Schlange", "Ungarischer Hornschwanz", "Zentaur"],
"hint": "Er schläft sofort ein, sobald er Musik hört.",
"question_en": "What kind of magical beast guards the trapdoor to the Philosopher's Stone and answers to the name Fluffy?",
"answer_en": "Three-headed dog",
"options_en": ["Three-headed dog", "Basilisk serpent", "Hungarian Horntail", "Centaur"],
"hint_en": "He instantly falls fast asleep the moment music plays."
},
{
"category": "quidditch",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welchen berühmten Besen erhält Harry in seinem ersten Schuljahr von Professor McGonagall geschenkt?",
"canonical_answer": "Nimbus 2000",
"options": ["Nimbus 2000", "Feuerblitz", "Sauberwisch 7", "Komet 260"],
"hint": "Das damals schnellste Rennbesenmodell der Zauberwelt.",
"question_en": "Which famous broomstick does Harry receive in his first year as a gift from Professor McGonagall?",
"answer_en": "Nimbus 2000",
"options_en": ["Nimbus 2000", "Firebolt", "Cleansweep Seven", "Comet 260"],
"hint_en": "At the time, the fastest racing broom in the wizarding world."
},
{
"category": "lore",
"difficulty": "easy",
"question_type": "text",
"question": "Wie heißt die berühmte Einkaufsstraße für Zauberbedarf im Herzen von London?",
"canonical_answer": "Winkelgasse",
"accepted_answers": json.dumps(["Winkelgasse", "Diagon Alley"]),
"options": None,
"hint": "Man erreicht sie durch das Klopfen an einer Backsteinmauer im Hinterhof des Tropfenden Kessels.",
"question_en": "What is the name of the famous wizarding shopping alley located in London?",
"answer_en": "Diagon Alley",
"options_en": None,
"hint_en": "Accessed by tapping bricks behind the Leaky Cauldron pub."
},
{
"category": "characters",
"difficulty": "easy",
"question_type": "text",
"question": "Wie lautet der Rufname von Harrys rothaarigem bestem Schulfreund?",
"canonical_answer": "Ron",
"accepted_answers": json.dumps(["Ron", "Ronald", "Ron Weasley", "Ronald Weasley"]),
"options": None,
"hint": "Vollständiger Name: Ronald Bilius Weasley.",
"question_en": "What is the first name of Harry's red-haired best school friend?",
"answer_en": "Ron",
"options_en": None,
"hint_en": "Full name: Ronald Bilius Weasley."
},
{
"category": "general",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welches magische Transportmittel bringt die Schüler alljährlich am 1. September nach Hogwarts?",
"canonical_answer": "Hogwarts-Express",
"options": ["Hogwarts-Express", "Fliegender Ford Anglia", "Flohpulver-Netzwerk", "Nachtbus"],
"hint": "Ein scharlachroter Dampfzug von Gleis 9 3/4.",
"question_en": "Which magical transport takes students to Hogwarts every year on September 1st?",
"answer_en": "Hogwarts Express",
"options_en": ["Hogwarts Express", "Flying Ford Anglia", "Floo Powder Network", "Knight Bus"],
"hint_en": "A scarlet steam engine leaving Platform 9 3/4."
},
# --- MEDIUM (9-18) ---
{
"category": "hogwarts",
"difficulty": "medium",
"question_type": "text",
"question": "In welchem Schuljahr müssen Hogwarts-Schüler ihre Z.A.G.-Prüfungen ablegen? (Bitte Zahl eingeben)",
"canonical_answer": "5",
"accepted_answers": json.dumps(["5", "fuenf", "five", "fünf", "5."]),
"options": None,
"hint": "In diesem Jahr treibt Großinquisitorin Dolores Umbridge ihr Unwesen an der Schule.",
"question_en": "In which school year do Hogwarts students sit their O.W.L. exams? (Please enter as a number)",
"answer_en": "5",
"options_en": None,
"hint_en": "The school year during which Dolores Umbridge torments Hogwarts."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "text",
"question": "Mit welchem Zauberspruch kann man verschlossene Schlösser, Türen und Fenster magisch entriegeln?",
"canonical_answer": "Alohomora",
"accepted_answers": json.dumps(["Alohomora"]),
"options": None,
"hint": "Hermine nutzt diesen Spruch im ersten Band, um dem Hausmeister Filch zu entkommen.",
"question_en": "Which unlocking charm opens locked doors and windows?",
"answer_en": "Alohomora",
"options_en": None,
"hint_en": "Hermione uses this spell in the first book to escape caretaker Filch."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche Spezies magischer Wesen betreibt und bewacht die Zaubererbank Gringotts?",
"canonical_answer": "Kobolde",
"options": ["Kobolde", "Zwerge", "Hauselfen", "Zentauren"],
"hint": "Griphook gehört diesem stolzen und scharfsinnigen Volk an.",
"question_en": "Which species of magical beings operates and guards the wizarding bank Gringotts?",
"answer_en": "Goblins",
"options_en": ["Goblins", "Dwarves", "House-elves", "Centaurs"],
"hint_en": "Griphook belongs to this clever and proud species."
},
{
"category": "lore",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher verzauberte Gegenstand erlaubte es Hermine im 3. Schuljahr, mehrere Unterrichtsstunden gleichzeitig zu besuchen?",
"canonical_answer": "Zeitumkehrer",
"options": ["Zeitumkehrer", "Denkarium", "Tarnumhang", "Spickoskop"],
"hint": "Eine kleine Sanduhr an einer goldenen Halskette, vergeben von Professor McGonagall.",
"question_en": "Which enchanted object allowed Hermione to attend multiple classes at the same time in her 3rd year?",
"answer_en": "Time-Turner",
"options_en": ["Time-Turner", "Pensieve", "Invisibility Cloak", "Sneakoscope"],
"hint_en": "A miniature hourglass necklace authorized by Professor McGonagall."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche Gestalt nimmt der mächtige Patronus von Harry Potter an?",
"canonical_answer": "Hirsch",
"options": ["Hirsch", "Silberne Hirschkuh", "Otter", "Phönix"],
"hint": "Prongs – das Animagus-Tier seines Vaters James Potter.",
"question_en": "What form does Harry Potter's corporeal Patronus take?",
"answer_en": "Stag",
"options_en": ["Stag", "Silver Doe", "Otter", "Phoenix"],
"hint_en": "Prongs – the Animagus form of his father James Potter."
},
{
"category": "quidditch",
"difficulty": "medium",
"question_type": "text",
"question": "Wie viele Spieler stehen insgesamt pro Quidditch-Mannschaft auf dem Spielfeld? (Bitte Zahl eingeben)",
"canonical_answer": "7",
"accepted_answers": json.dumps(["7", "sieben", "seven"]),
"options": None,
"hint": "3 Jäger, 2 Treiber, 1 Hüter und 1 Sucher.",
"question_en": "How many players are on the pitch for each Quidditch team? (Please enter as a number)",
"answer_en": "7",
"options_en": None,
"hint_en": "3 Chasers, 2 Beaters, 1 Keeper, and 1 Seeker."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche Gestalt nimmt ein Irrwicht an, wenn er Professor Remus Lupin gegenübersteht?",
"canonical_answer": "Vollmond",
"accepted_answers": json.dumps(["Vollmond", "Mond", "Full Moon", "Moon"]),
"options": ["Vollmond", "Silberner Dolch", "Dementor", "Korsett"],
"hint": "Wegen seiner Lykanthropie fürchtet Lupin das Licht des Mondes.",
"question_en": "What shape does a boggart take when facing Professor Remus Lupin?",
"answer_en": "Full moon",
"options_en": ["Full moon", "Silver dagger", "Dementor", "Corset"],
"hint_en": "Because of his lycanthropy, Lupin fears the light of the moon."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher dieser Zaubersprüche gehört NICHT zu den drei verbotenen Unverzeihlichen Flüchen?",
"canonical_answer": "Sectumsempra",
"options": ["Sectumsempra", "Crucio", "Imperio", "Avada Kedavra"],
"hint": "Diesen finsteren Fluch erfand der Halbblutprinz für seine Feinde.",
"question_en": "Which of these dark spells is NOT one of the three Unforgivable Curses?",
"answer_en": "Sectumsempra",
"options_en": ["Sectumsempra", "Crucio", "Imperio", "Avada Kedavra"],
"hint_en": "Invented by the Half-Blood Prince 'for enemies'."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "text",
"question": "Wie heißt der stolze Hippogreif, der von Hagrid unterrichtet und später von Harry und Hermine gerettet wird?",
"canonical_answer": "Seidenschnabel",
"accepted_answers": json.dumps(["Seidenschnabel", "Buckbeak", "Witherwings"]),
"options": None,
"hint": "Vor seiner Berührung muss man ihm fest in die Augen blicken und sich verbeugen.",
"question_en": "What is the name of the majestic Hippogriff introduced by Hagrid and later rescued by Harry and Hermione?",
"answer_en": "Buckbeak",
"options_en": None,
"hint_en": "One must maintain eye contact and bow respectfully before approaching him."
},
{
"category": "lore",
"difficulty": "medium",
"question_type": "text",
"question": "Aus welchem Holz und magischem Kern besteht Harrys originaler Zauberstab?",
"canonical_answer": "Stechpalme und Phönixfeder",
"accepted_answers": json.dumps(["Stechpalme und Phönixfeder", "Stechpalme", "Phönixfeder", "Holly and phoenix feather", "Holly"]),
"options": None,
"hint": "Der Phönix Fawkes spendete genau zwei Federn – eine für Harry, eine für Voldemort.",
"question_en": "What wood and core make up Harry Potter's original wand from Ollivanders?",
"answer_en": "Holly and Phoenix Feather",
"options_en": None,
"hint_en": "The phoenix Fawkes gave exactly two feathers – one to Harry's wand and one to Voldemort's."
},
# --- HARD (19-25) ---
{
"category": "lore",
"difficulty": "hard",
"question_type": "text",
"question": "Wie alt muss eine Hexe oder ein Zauberer in Großbritannien sein, um die magische Volljährigkeit zu erreichen? (Bitte Zahl eingeben)",
"canonical_answer": "17",
"accepted_answers": json.dumps(["17", "siebzehn", "seventeen"]),
"options": None,
"hint": "Ab diesem Alter erlischt auch die magische 'Spur' des Zaubereiministeriums.",
"question_en": "At what age does a witch or wizard in Britain legally come of age in the magical world? (Please enter as a number)",
"answer_en": "17",
"options_en": None,
"hint_en": "At this age, the Ministry of Magic's 'Trace' breaks."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie lautet der vollständige Mädchenname von Lord Voldemorts unglücklicher Mutter?",
"canonical_answer": "Merope Gaunt",
"options": ["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Helena Ravenclaw"],
"hint": "Sie verliebte sich in den Muggel Tom Riddle Senior und stammte von Salazar Slytherin ab.",
"question_en": "What is the full maiden name of Lord Voldemort's tragic mother?",
"answer_en": "Merope Gaunt",
"options_en": ["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Helena Ravenclaw"],
"hint_en": "A direct descendant of Salazar Slytherin who bewitched Tom Riddle Sr."
},
{
"category": "creatures",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welche geflügelten, skelettartigen Wesen ziehen die scheinbar herrenlosen Kutschen von Hogsmeade zum Schloss?",
"canonical_answer": "Thestrale",
"options": ["Thestrale", "Abraxaner", "Hippogreife", "Gryffins"],
"hint": "Sie sind unsichtbar für alle, die noch nie den Tod eines Menschen miterlebt haben.",
"question_en": "Which winged, skeletal creatures pull the carriages from Hogsmeade to Hogwarts Castle?",
"answer_en": "Thestrals",
"options_en": ["Thestrals", "Abraxans", "Hippogriffs", "Griffins"],
"hint_en": "They can only be seen by those who have witnessed death."
},
{
"category": "lore",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt das steinerne Becken im Büro des Schulleiters, in dem man Gedanken und Erinnerungen ablegen und betrachten kann?",
"canonical_answer": "Denkarium",
"accepted_answers": json.dumps(["Denkarium", "Pensieve"]),
"options": None,
"hint": "Auf Englisch wird es Pensieve genannt.",
"question_en": "What is the name of the stone basin in the Headmaster's office used to view stored memories?",
"answer_en": "Pensieve",
"options_en": None,
"hint_en": "In German it is known as Denkarium."
},
{
"category": "spells",
"difficulty": "hard",
"question_type": "text",
"question": "Welcher Zauberspruch wird verwendet, um Erinnerungen aus dem Gedächtnis einer Person zu löschen oder zu verändern?",
"canonical_answer": "Obliviate",
"accepted_answers": json.dumps(["Obliviate", "Vergisszauber"]),
"options": None,
"hint": "Gilderoy Lockharts Spezialgebiet, der sich damit am Ende selbst das Gedächtnis raubte.",
"question_en": "Which memory charm is used to erase or modify specific memories from a person's mind?",
"answer_en": "Obliviate",
"options_en": None,
"hint_en": "Gilderoy Lockhart's specialty, which ultimately backfired on him."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "In welches winzige Insekt konnte sich die Klatschreporterin Rita Kimmkorn als unregistrierter Animagus verwandeln?",
"canonical_answer": "Käfer",
"options": ["Käfer", "Fliege", "Spinne", "Wespe"],
"hint": "Hermine fängt sie in einem unzerbrechlichen Glasgefäß ein.",
"question_en": "Into which tiny insect could gossip reporter Rita Skeeter transform as an unregistered Animagus?",
"answer_en": "Beetle",
"options_en": ["Beetle", "Fly", "Spider", "Wasp"],
"hint_en": "Hermione captures her inside an unbreakable glass jar."
},
{
"category": "hogwarts",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt der Poltergeist von Hogwarts, der Schülern Streiche spielt und nur vor dem Blutigen Baron Respekt hat?",
"canonical_answer": "Peeves",
"accepted_answers": json.dumps(["Peeves", "Peeves der Poltergeist"]),
"options": None,
"hint": "Er wirft gerne mit Kreide, Kandelabern und Stöcken nach Passanten.",
"question_en": "What is the name of the Hogwarts poltergeist who terrorizes students and only fears the Bloody Baron?",
"answer_en": "Peeves",
"options_en": None,
"hint_en": "He loves dropping chandeliers, throwing chalk, and singing mocking rhymes."
}
]
def seed_expand():
db = SessionLocal()
try:
inserted = 0
for item in NEW_QUESTIONS_DATA:
# Check if question already exists by question text
existing = db.query(Question).filter(Question.question == item["question"]).first()
if existing:
continue
opts_json = json.dumps(item["options"], ensure_ascii=False) if item.get("options") else None
opts_en_json = json.dumps(item["options_en"], ensure_ascii=False) if item.get("options_en") else None
q = Question(
category=item["category"],
difficulty=item["difficulty"],
question_type=item["question_type"],
question=item["question"],
canonical_answer=item["canonical_answer"],
accepted_answers=item.get("accepted_answers"),
options=opts_json,
hint=item.get("hint"),
question_en=item.get("question_en"),
answer_en=item.get("answer_en"),
options_en=opts_en_json,
hint_en=item.get("hint_en"),
status="active",
sort_order=0
)
db.add(q)
inserted += 1
db.commit()
total_count = db.query(Question).filter(Question.status == "active").count()
print(f"Successfully inserted {inserted} new questions! Total active questions: {total_count}")
finally:
db.close()
if __name__ == "__main__":
seed_expand()
+722
View File
@@ -0,0 +1,722 @@
import json
from app.database import SessionLocal
from app.models import Question
LORE_PACK_QUESTIONS = [
# --- CREATURES (Magische Geschöpfe) ---
{
"category": "creatures",
"difficulty": "hard",
"question_type": "text",
"question": "Aus welcher Kreuzung zweier magischer Geschöpfe züchtete Hagrid die Knallrümpfigen Kröter?",
"canonical_answer": "Mantikor und Feuerkrabbe",
"accepted_answers": json.dumps([
"Mantikor und Feuerkrabbe",
"Feuerkrabbe und Mantikor",
"Mantikor Feuerkrabbe",
"Feuerkrabbe Mantikor",
"Manticore and Fire Crab",
"Fire Crab and Manticore"
], ensure_ascii=False),
"hint": "Eines ist ein hochgefährliches griechisches Ungeheuer mit Skorpionschwanz, das andere ein schildkrötenähnliches Wesen von den Fidschi-Inseln.",
"question_en": "Which two magical creatures did Hagrid crossbreed to create the Blast-Ended Skrewts?",
"answer_en": "Manticore and Fire Crab",
"hint_en": "One is a deadly Greek beast with a scorpion tail, the other is a turtle-like creature native to Fiji."
},
{
"category": "creatures",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welcher der drei Köpfe der dreiköpfigen Schlange 'Runespoor' ist laut Newt Scamander derjenige, der Pläne schmiedet?",
"canonical_answer": "Der linke Kopf",
"options": json.dumps(["Der linke Kopf", "Der mittlere Kopf", "Der rechte Kopf", "Alle drei gleichzeitig"], ensure_ascii=False),
"hint": "Der mittlere Kopf ist der Träumer und der rechte Kopf der Kritiker.",
"question_en": "According to Newt Scamander, which of the three heads of the Runespoor snake is the planner?",
"answer_en": "The left head",
"options_en": json.dumps(["The left head", "The middle head", "The right head", "All three simultaneously"], ensure_ascii=False),
"hint_en": "The middle head is the dreamer and the right head is the critic."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "text",
"question": "Wie heißt die riesige Spinne (Acromantula), die Hagrid schon während seiner Schulzeit in einem Schrank in Hogwarts versteckte?",
"canonical_answer": "Aragog",
"accepted_answers": json.dumps(["Aragog"], ensure_ascii=False),
"hint": "Sein Name beginnt mit 'A' und er lebt später mit seiner großen Kolonie im Verbotenen Wald.",
"question_en": "What is the name of the giant Acromantula spider that Hagrid hid in a cupboard at Hogwarts during his school days?",
"answer_en": "Aragog",
"hint_en": "His name starts with 'A' and he later lives with his large colony in the Forbidden Forest."
},
{
"category": "creatures",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welche Gefahrenklasse (Ministeriumsklassifizierung) hat ein ausgewachsener Nundu laut 'Phantastische Tierwesen'?",
"canonical_answer": "XXXXX (Bekannte Zauberertöter)",
"options": json.dumps([
"XXXXX (Bekannte Zauberertöter)",
"XXXX (Gefährlich / erfordert Fachwissen)",
"XXX (Für fähige Zauberer kein Problem)",
"XX (Harmlos / kann gezähmt werden)"
], ensure_ascii=False),
"hint": "Es braucht mindestens hundert zusammenarbeitende Zauberer, um einen einzigen Nundu zu überwältigen.",
"question_en": "What Ministry of Magic classification does an adult Nundu hold according to 'Fantastic Beasts'?",
"answer_en": "XXXXX (Known wizard killer)",
"options_en": json.dumps([
"XXXXX (Known wizard killer)",
"XXXX (Dangerous / requires specialist knowledge)",
"XXX (Competent wizard should cope)",
"XX (Harmless / may be domesticated)"
], ensure_ascii=False),
"hint_en": "It takes at least a hundred skilled wizards working together to subdue a single Nundu."
},
{
"category": "creatures",
"difficulty": "easy",
"question_type": "text",
"question": "Welches kleine, pelzige Tierwesen wird magisch von allem Glitzernden und von Goldmünzen angezogen?",
"canonical_answer": "Niffler",
"accepted_answers": json.dumps(["Niffler", "Ein Niffler"], ensure_ascii=False),
"hint": "Newt Scamander hat eines dieser langnasigen Beuteltiere in seinem Koffer.",
"question_en": "Which small, furry magical creature is irresistibly drawn to anything shiny and gold coins?",
"answer_en": "Niffler",
"hint_en": "Newt Scamander keeps one of these long-snouted burrowing creatures in his case."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie nennt man das unsichtbare geflügelte Pferd, das nur von Menschen gesehen werden kann, die den Tod bereits bewusst miterlebt haben?",
"canonical_answer": "Thestral",
"options": json.dumps(["Thestral", "Hippogreif", "Abraxaner", "Kelpie"], ensure_ascii=False),
"hint": "Sie ziehen die Kutschen vom Bahnhof Hogsmeade zum Schloss Hogwarts.",
"question_en": "What is the name of the invisible winged horse that can only be seen by those who have witnessed death?",
"answer_en": "Thestral",
"options_en": json.dumps(["Thestral", "Hippogriff", "Abraxan", "Kelpie"], ensure_ascii=False),
"hint_en": "They pull the carriages from Hogsmeade station up to Hogwarts Castle."
},
# --- BOOKS (Buchwissen) ---
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Wie lautet das vollständige Akronym der Organisation, die Hermine Granger im 4. Buch zur Befreiung der Hauselfen gründet?",
"canonical_answer": "B.ELFE.R.",
"accepted_answers": json.dumps(["B.ELFE.R.", "BELFER", "B.ELFE.R", "S.P.E.W.", "SPEW", "Bund für Elfenrechte"]),
"hint": "Es steht für 'Bund für Elfenrechte' (im Englischen: S.P.E.W.).",
"question_en": "What is the acronym of the organization Hermione Granger founds in her fourth year to campaign for house-elf rights?",
"answer_en": "S.P.E.W.",
"hint_en": "It stands for 'Society for the Promotion of Elfish Welfare' (in German: B.ELFE.R.)."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wer schenkt Harry im 4. Buch (im Gegensatz zum Film!) das Kraut 'Dianthuskraut' (Gillyweed) für die zweite Aufgabe des Trimagischen Turniers?",
"canonical_answer": "Dobby",
"options": json.dumps(["Dobby", "Neville Longbottom", "Alastor Moody", "Cedric Diggory"], ensure_ascii=False),
"hint": "Er stiehlt es aus Professor Snapes Vorratskammer, nachdem er ein inszeniertes Gespräch von Barty Crouch Jr. belauscht hat.",
"question_en": "In the Goblet of Fire book (unlike the movie!), who actually gives Harry the Gillyweed before the Second Task?",
"answer_en": "Dobby",
"options_en": json.dumps(["Dobby", "Neville Longbottom", "Alastor Moody", "Cedric Diggory"], ensure_ascii=False),
"hint_en": "He steals it from Professor Snape's private stores after overhearing a staged conversation by Barty Crouch Jr."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt der Poltergeist von Hogwarts, der in allen sieben Büchern für Chaos sorgt, aber aus den Verfilmungen komplett gestrichen wurde?",
"canonical_answer": "Peeves",
"accepted_answers": json.dumps(["Peeves", "Poltergeist Peeves"]),
"hint": "Nur der Blutige Baron hat ihn wirklich unter Kontrolle.",
"question_en": "What is the name of the Hogwarts poltergeist who causes chaos throughout all seven books but was completely cut from the films?",
"answer_en": "Peeves",
"hint_en": "Only the Bloody Baron can truly keep him in line."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welches Rätsel stellt Professor Snape in Band 1 als Schutz vor dem Stein der Weisen auf, das im ersten Kinofilm komplett fehlt?",
"canonical_answer": "Ein Logikrätsel mit sieben Zaubertrank-Flaschen",
"options": json.dumps([
"Ein Logikrätsel mit sieben Zaubertrank-Flaschen",
"Ein Duell gegen eine verzauberte Rüstung",
"Ein Schachbrett aus flüssigem Quecksilber",
"Eine Tür, die nur auf Parsel reagiert"
], ensure_ascii=False),
"hint": "Hermine löst es mit reiner Logik, während ein schwarzes und ein violettes Feuer die Ausgänge blockieren.",
"question_en": "Which obstacle guarding the Philosopher's Stone was created by Professor Snape in the first book, but omitted from the film?",
"answer_en": "A logic puzzle with seven potion bottles",
"options_en": json.dumps([
"A logic puzzle with seven potion bottles",
"A duel against an enchanted suit of armor",
"A chessboard made of liquid mercury",
"A door that only opens to Parseltongue"
], ensure_ascii=False),
"hint_en": "Hermione solves it using pure logic while black and purple flames block the exits."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt die Hauselfe der Familie Barty Crouch, die beim Quidditch-Weltpokal mit Harrys Zauberstab in der Hand gefunden wird?",
"canonical_answer": "Winky",
"accepted_answers": json.dumps(["Winky", "Hauselfe Winky"]),
"hint": "Ihr Name beginnt mit 'W' und sie verfällt nach ihrer Entlassung dem Butterbier.",
"question_en": "What is the name of the Crouch family's female house-elf who is found holding Harry's wand at the Quidditch World Cup?",
"answer_en": "Winky",
"hint_en": "Her name starts with 'W' and she later develops a Butterbeer habit in the Hogwarts kitchens."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie heißt Tom Riddles Mutter mit vollem Geburtsnamen, deren tragische Geschichte Harry in Dumbledores Denkarium im 6. Buch erfährt?",
"canonical_answer": "Merope Gaunt",
"options": json.dumps(["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Eileen Prince"], ensure_ascii=False),
"hint": "Sie war eine Nachfahrin von Salazar Slytherin und verliebte sich in den Muggel Tom Riddle Senior.",
"question_en": "What was the maiden name of Tom Riddle's mother, whose tragic story Harry learns in Dumbledore's Pensieve in book 6?",
"answer_en": "Merope Gaunt",
"options_en": json.dumps(["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Eileen Prince"], ensure_ascii=False),
"hint_en": "She was a direct descendant of Salazar Slytherin and fell in love with the Muggle Tom Riddle Sr."
},
{
"category": "books",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Auf welcher Krankenstation im St.-Mungo-Hospital begegnen Harry, Ron und Hermine im 5. Buch zufällig Gilderoy Lockhart wieder?",
"canonical_answer": "Janus-Thickey-Station (Langzeit-Fluchschäden)",
"options": json.dumps([
"Janus-Thickey-Station (Langzeit-Fluchschäden)",
"Dai-Llewellyn-Station (Tierbisse)",
"Artefakt-Unfälle im Erdgeschoss",
"Teekessel-Verbrennungsstation"
], ensure_ascii=False),
"hint": "Auf derselben geschlossenen Station im 4. Stock liegen auch Frank und Alice Longbottom.",
"question_en": "On which ward of St Mungo's Hospital do Harry, Ron, and Hermione bump into Gilderoy Lockhart in the fifth book?",
"answer_en": "Janus Thickey Ward (Long-Term Spell Damage)",
"options_en": json.dumps([
"Janus Thickey Ward (Long-Term Spell Damage)",
"Dai Llewellyn Ward (Creature Bites)",
"Artifact Accidents on the Ground Floor",
"Potion and Plant Poisoning Ward"
], ensure_ascii=False),
"hint_en": "Frank and Alice Longbottom are patients on the very same fourth-floor ward."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "text",
"question": "Welchen Beruf übt Charlie Weasley in Rumänien aus?",
"canonical_answer": "Drachenwärter",
"accepted_answers": json.dumps([
"Drachenwärter", "Drachenforscher", "Drachenzüchter", "Drachenpfleger",
"Er arbeitet mit Drachen", "Drachen", "Dragonologist", "Dragon keeper"
], ensure_ascii=False),
"hint": "Er nimmt im 1. Buch den kleinen Norwegischen Stachelbuckel Norbert in seine Obhut.",
"question_en": "What is Charlie Weasley's profession in Romania?",
"answer_en": "Dragonkeeper",
"hint_en": "In the first book, his friends fly down to take Hagrid's Norwegian Ridgeback Norbert into their care."
},
{
"category": "books",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Was tut Harry im Buch 'Die Heiligtümer des Todes' mit dem Elderstab, bevor er ihn an seinen endgültigen Ort zurückbringt?",
"canonical_answer": "Er repariert seinen eigenen zerbrochenen Zauberstab",
"options": json.dumps([
"Er repariert seinen eigenen zerbrochenen Zauberstab",
"Er zerbricht ihn und wirft ihn von der Brücke",
"Er schenkt ihn Ron Weasley",
"Er übergibt ihn dem Zaubereiministerium"
], ensure_ascii=False),
"hint": "Mit 'Reparo' gelingt ihm etwas, was Ollivander zuvor für unmöglich gehalten hatte.",
"question_en": "In the Deathly Hallows book, what does Harry do with the Elder Wand before returning it to Dumbledore's tomb?",
"answer_en": "He repairs his own broken holly and phoenix wand",
"options_en": json.dumps([
"He repairs his own broken holly and phoenix wand",
"He snaps it in half and throws it off the bridge",
"He gifts it to Ron Weasley",
"He locks it inside Gringotts"
], ensure_ascii=False),
"hint_en": "Using 'Reparo', he accomplishes something Ollivander thought was impossible."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie heißt der Kobold, der im 4. Buch von Ludo Bagman mit Leprechaun-Gold betrogen wird?",
"canonical_answer": "Gornuk (bzw. Ragnok / Griphooks Kollegen)",
"options": json.dumps(["Ragnok", "Bogrod", "Travers", "Yaxley"], ensure_ascii=False),
"hint": "Er trifft sich im Eberkopf mit Ludo Bagman, um seine Schulden einzufordern.",
"question_en": "Which goblin confronts Ludo Bagman in the Hog's Head inn after being paid in vanishing Leprechaun gold?",
"answer_en": "Ragnok",
"options_en": json.dumps(["Ragnok", "Bogrod", "Travers", "Yaxley"], ensure_ascii=False),
"hint_en": "He meets Ludo Bagman in the Hog's Head to demand repayment of his bet."
},
{
"category": "books",
"difficulty": "medium",
"question_type": "text",
"question": "Welches magische Radio-Programm hören Ron, Harry und Hermine im 7. Buch auf der Flucht, für das man ein wechselndes Passwort braucht?",
"canonical_answer": "PotterWatch",
"accepted_answers": json.dumps(["PotterWatch", "Potter Watch", "Potterwatch"]),
"hint": "Moderiert wird die Sendung von Lee Jordan unter dem Decknamen 'Fluss' (River).",
"question_en": "What is the name of the secret rebel radio broadcast that Harry, Ron, and Hermione listen to while on the run in book 7?",
"answer_en": "Potterwatch",
"hint_en": "The program is hosted by Lee Jordan under the codename 'River'."
},
{
"category": "books",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wer war die ursprüngliche Besitzerin des Diadems von Ravenclaw und zugleich deren Tochter (die Graue Dame)?",
"canonical_answer": "Helena Ravenclaw",
"options": json.dumps(["Helena Ravenclaw", "Rowena Ravenclaw", "Helga Hufflepuff", "Millicent Bagnold"], ensure_ascii=False),
"hint": "Sie stahl das Diadem ihrer Mutter und versteckte es in einem hohlen Baum in Albanien.",
"question_en": "What is the real name of the Grey Lady, the ghost of Ravenclaw Tower and daughter of Rowena Ravenclaw?",
"answer_en": "Helena Ravenclaw",
"options_en": json.dumps(["Helena Ravenclaw", "Rowena Ravenclaw", "Helga Hufflepuff", "Millicent Bagnold"], ensure_ascii=False),
"hint_en": "She stole her mother's diadem and hid it inside a hollow tree in Albania."
},
# --- MOVIES (Filmwissen) ---
{
"category": "movies",
"difficulty": "easy",
"question_type": "text",
"question": "Welcher britische Schauspieler verkörperte Harry Potter in allen acht Kinofilmen?",
"canonical_answer": "Daniel Radcliffe",
"accepted_answers": json.dumps(["Daniel Radcliffe", "Radcliffe"]),
"hint": "Er war bei den Dreharbeiten zum ersten Film erst elf Jahre alt.",
"question_en": "Which British actor portrayed Harry Potter across all eight feature films?",
"answer_en": "Daniel Radcliffe",
"hint_en": "He was only eleven years old when filming began on the first movie."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher Regisseur inszenierte den dritten Kinofilm 'Harry Potter und der Gefangene von Askaban' (2004)?",
"canonical_answer": "Alfonso Cuarón",
"options": json.dumps(["Alfonso Cuarón", "Chris Columbus", "David Yates", "Mike Newell"], ensure_ascii=False),
"hint": "Der mexikanische Regisseur prägte den düsteren, künstlerischen Look des dritten Teils.",
"question_en": "Which director helmed the third movie, 'Harry Potter and the Prisoner of Azkaban' (2004)?",
"answer_en": "Alfonso Cuarón",
"options_en": json.dumps(["Alfonso Cuarón", "Chris Columbus", "David Yates", "Mike Newell"], ensure_ascii=False),
"hint_en": "The Mexican filmmaker gave the third installment its signature darker, atmospheric visual style."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "text",
"question": "Welcher weltberühmte Filmkomponist schrieb das ikonische Hauptthema 'Hedwig's Theme' für die ersten Harry-Potter-Filme?",
"canonical_answer": "John Williams",
"accepted_answers": json.dumps(["John Williams", "Williams"]),
"hint": "Er komponierte auch die Musik für Star Wars, Indiana Jones und Jurassic Park.",
"question_en": "Which legendary film composer wrote the iconic main theme 'Hedwig's Theme' for the first Harry Potter movies?",
"answer_en": "John Williams",
"hint_en": "He also composed the scores for Star Wars, Indiana Jones, and Jurassic Park."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher Schauspieler übernahm ab dem dritten Film die Rolle von Albus Dumbledore, nachdem Richard Harris verstorben war?",
"canonical_answer": "Michael Gambon",
"options": json.dumps(["Michael Gambon", "Ian McKellen", "Gary Oldman", "Ralph Fiennes"], ensure_ascii=False),
"hint": "Er spielte Dumbledore vom dritten bis zum achten Film.",
"question_en": "Which actor took over the role of Albus Dumbledore starting in the third film after Richard Harris passed away?",
"answer_en": "Michael Gambon",
"options_en": json.dumps(["Michael Gambon", "Ian McKellen", "Gary Oldman", "Ralph Fiennes"], ensure_ascii=False),
"hint_en": "He portrayed Dumbledore from Prisoner of Azkaban through Deathly Hallows – Part 2."
},
{
"category": "movies",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Über welches berühmte schottische Eisenbahnviadukt fährt der Hogwarts-Express in den Filmen?",
"canonical_answer": "Glenfinnan-Viadukt",
"options": json.dumps(["Glenfinnan-Viadukt", "Ribblehead-Viadukt", "Clifton-Brücke", "Forth Bridge"], ensure_ascii=False),
"hint": "Es liegt in den schottischen Highlands am Loch Shiel.",
"question_en": "Which famous Scottish railway viaduct does the Hogwarts Express cross in the movies?",
"answer_en": "Glenfinnan Viaduct",
"options_en": json.dumps(["Glenfinnan Viaduct", "Ribblehead Viaduct", "Clifton Bridge", "Forth Bridge"], ensure_ascii=False),
"hint_en": "It is located in the Scottish Highlands overlooking Loch Shiel."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welche Augenfarbe hat Harry Potter in den Kinofilmen (weil Daniel Radcliffe die farbigen Kontaktlinsen nicht vertrug), obwohl sie in den Büchern leuchtend grün sind?",
"canonical_answer": "Blau",
"options": json.dumps(["Blau", "Braun", "Grau", "Bernsteinfarben"], ensure_ascii=False),
"hint": "Daniel Radcliffes natürliche Augenfarbe.",
"question_en": "What color are Harry Potter's eyes in the movies (because Daniel Radcliffe had an allergic reaction to contact lenses), even though they are bright green in the books?",
"answer_en": "Blue",
"options_en": json.dumps(["Blue", "Brown", "Hazel", "Amber"], ensure_ascii=False),
"hint_en": "It is Daniel Radcliffe's natural eye color."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "text",
"question": "Welcher unvergessene britische Schauspieler verkörperte Severus Snape in allen acht Filmen?",
"canonical_answer": "Alan Rickman",
"accepted_answers": json.dumps(["Alan Rickman", "Rickman"]),
"hint": "J.K. Rowling verriet ihm schon vor dem Erscheinen des letzten Buches das Geheimnis hinter Snapes 'Always'.",
"question_en": "Which beloved British actor portrayed Severus Snape across all eight Harry Potter films?",
"answer_en": "Alan Rickman",
"hint_en": "J.K. Rowling privately told him the secret behind Snape's 'Always' years before the final book was published."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "An welchem historischen englischen Bahnhof wurden die Außenaufnahmen von Hogsmeade Station in den ersten Filmen gedreht?",
"canonical_answer": "Goathland Station (North Yorkshire)",
"options": json.dumps([
"Goathland Station (North Yorkshire)",
"Paddington Station (London)",
"Waverley Station (Edinburgh)",
"York Central Station"
], ensure_ascii=False),
"hint": "Eine malerische Museumsbahnstation der North Yorkshire Moors Railway.",
"question_en": "At which historic English heritage railway station were the Hogsmeade Station scenes filmed in the early movies?",
"answer_en": "Goathland Station (North Yorkshire)",
"options_en": json.dumps([
"Goathland Station (North Yorkshire)",
"Paddington Station (London)",
"Waverley Station (Edinburgh)",
"York Central Station"
], ensure_ascii=False),
"hint_en": "A picturesque station on the North Yorkshire Moors Railway."
},
{
"category": "movies",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Auf welchem echten Londoner Bahnhof wurden die Szenen am Gleis 9¾ gedreht?",
"canonical_answer": "King's Cross",
"options": json.dumps(["King's Cross", "Waterloo", "Victoria Station", "Euston"], ensure_ascii=False),
"hint": "Die Außenfassade im 2. Film zeigt das benachbarte St Pancras Renaissance Hotel.",
"question_en": "At which real-life London railway station were the Platform 9¾ scenes filmed?",
"answer_en": "King's Cross",
"options_en": json.dumps(["King's Cross", "Waterloo", "Victoria Station", "Euston"], ensure_ascii=False),
"hint_en": "Today, fans can take photos with a luggage trolley disappearing into the brick wall there."
},
{
"category": "movies",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie heißt die fiktive Zauberrockband im 4. Film beim Weihnachtsball, deren Sänger von Pulp-Frontmann Jarvis Cocker gespielt wurde?",
"canonical_answer": "The Weird Sisters (Schwestern des Schicksals)",
"options": json.dumps([
"The Weird Sisters (Schwestern des Schicksals)",
"Celestina Warbeck & the Banshees",
"The Hobgoblins",
"Frog Choir"
], ensure_ascii=False),
"hint": "Auch Mitglieder der Band Radiohead spielten in dieser Filmszene in der Band mit!",
"question_en": "What is the name of the wizarding rock band at the Yule Ball in the fourth movie, fronted by Pulp's Jarvis Cocker?",
"answer_en": "The Weird Sisters",
"options_en": json.dumps([
"The Weird Sisters",
"Celestina Warbeck & the Banshees",
"The Hobgoblins",
"The Frog Choir"
], ensure_ascii=False),
"hint_en": "Members of Radiohead also appeared in the band lineup for that scene!"
},
# --- SPELLS & POTIONS (Zaubersprüche & Tränke) ---
{
"category": "spells",
"difficulty": "hard",
"question_type": "text",
"question": "Wie lautet der Gegenzauber zum Schwebezauber 'Levicorpus', mit dem man das Opfer wieder auf den Boden holt?",
"canonical_answer": "Liberacorpus",
"accepted_answers": json.dumps(["Liberacorpus", "Libera corpus"]),
"hint": "Harry findet sowohl den Zauber als auch den Gegenzauber handschriftlich im Buch des Halbblutprinzen.",
"question_en": "What is the counter-jinx to 'Levicorpus' that lowers the victim back to the ground?",
"answer_en": "Liberacorpus",
"hint_en": "Harry discovers both the spell and its counter-jinx written in the margins of the Half-Blood Prince's textbook."
},
{
"category": "spells",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welcher seltene Stein aus dem Magen einer Ziege wirkt als universelles Gegengift gegen die meisten Zaubertrank-Gifte?",
"canonical_answer": "Bezoar",
"options": json.dumps(["Bezoar", "Mondstein", "Blutstein", "Flussstein"], ensure_ascii=False),
"hint": "Snape fragt Harry danach in seiner allerersten Zaubertränkestunde, und Harry rettet damit später Rons Leben.",
"question_en": "Which stone taken from the stomach of a goat acts as an antidote to most poisons?",
"answer_en": "Bezoar",
"options_en": json.dumps(["Bezoar", "Moonstone", "Bloodstone", "Runestone"], ensure_ascii=False),
"hint_en": "Snape quizzes Harry about it in his very first Potions lesson, and Harry later uses one to save Ron's life."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "text",
"question": "Welcher dunkle Fluch, den der Halbblutprinz erfunden hat, schlitzt das Opfer wie mit einem unsichtbaren Schwert auf ('Für Feinde')?",
"canonical_answer": "Sectumsempra",
"accepted_answers": json.dumps(["Sectumsempra", "Sectum Sempra"]),
"hint": "Harry wendet ihn im 6. Teil im Waschraum gegen Draco Malfoy an.",
"question_en": "Which dark curse invented by the Half-Blood Prince slashes the target as if with an invisible sword ('For Enemies')?",
"answer_en": "Sectumsempra",
"hint_en": "Harry uses it against Draco Malfoy in the bathroom in year six."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Wie lange dauert die aufwendige Zubereitung des Vielsaft-Tranks (Polyjuice Potion) ungefähr?",
"canonical_answer": "Einen ganzen Monat",
"options": json.dumps(["Einen ganzen Monat", "Genau 24 Stunden", "Sieben Tage", "Nur eine Vollmondnacht"], ensure_ascii=False),
"hint": "Unter anderem muss Florfliegenkraut 21 Tage lang schmoren.",
"question_en": "Approximately how long does it take to brew Polyjuice Potion from start to finish?",
"answer_en": "One full month",
"options_en": json.dumps(["One full month", "Exactly 24 hours", "Seven days", "A single full-moon night"], ensure_ascii=False),
"hint_en": "Lacewing flies alone must stew for twenty-one days."
},
# --- HOGWARTS & PLACES ---
{
"category": "hogwarts",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie gelangt man in die Schulküche von Hogwarts, die direkt unter der Großen Halle liegt?",
"canonical_answer": "Man kitzelt die Birne auf einem Obst-Gemälde",
"options": json.dumps([
"Man kitzelt die Birne auf einem Obst-Gemälde",
"Man klopft dreimal auf ein Essigfass",
"Man sagt das Passwort 'Kürbispastete' zu einer Rüstung",
"Man zieht an einer goldenen Bratpfanne"
], ensure_ascii=False),
"hint": "Die Frucht kichert daraufhin und verwandelt sich in einen grünen Türgriff.",
"question_en": "How do you open the secret entrance to the Hogwarts kitchens located beneath the Great Hall?",
"answer_en": "Tickle the pear in the painting of a bowl of fruit",
"options_en": json.dumps([
"Tickle the pear in the painting of a bowl of fruit",
"Tap a vinegar barrel in the rhythm of Helga Hufflepuff",
"Whisper 'Pumpkin Pasties' to a suit of armor",
"Pull on a golden ladle"
], ensure_ascii=False),
"hint_en": "The fruit giggles and transforms into a green door handle."
},
{
"category": "hogwarts",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Was unterscheidet den Eingang zum Ravenclaw-Gemeinschaftsraum von denen der anderen drei Häuser?",
"canonical_answer": "Man muss eine philosophische Rätselfrage des Türklopfers beantworten statt ein Passwort zu nennen",
"options": json.dumps([
"Man muss eine philosophische Rätselfrage des Türklopfers beantworten statt ein Passwort zu nennen",
"Man kann nur auf einem Besen durch das Turmfenster hineinfliegen",
"Man muss seinen Zauberstab auf eine Waage legen",
"Die Tür öffnet sich nur bei Mondschein"
], ensure_ascii=False),
"hint": "Der bronzene Adler-Türklopfer fragt z. B.: 'Was war zuerst da, der Phönix oder die Flamme?'",
"question_en": "How does entering the Ravenclaw common room differ from the other three Hogwarts houses?",
"answer_en": "You must answer a philosophical riddle posed by a bronze eagle knocker instead of giving a password",
"options_en": json.dumps([
"You must answer a philosophical riddle posed by a bronze eagle knocker instead of giving a password",
"You can only enter by flying through the tower window on a broom",
"You must weigh your wand on an enchanted scale",
"The door only opens under moonlight"
], ensure_ascii=False),
"hint_en": "For example, the bronze eagle asks: 'Which came first, the phoenix or the flame?'"
},
{
"category": "hogwarts",
"difficulty": "medium",
"question_type": "text",
"question": "Unter welchem Baum auf dem Schulgelände von Hogwarts beginnt der geheime Tunnel, der direkt in die Heulende Hütte nach Hogsmeade führt?",
"canonical_answer": "Peitschende Weide",
"accepted_answers": json.dumps(["Peitschende Weide", "Die Peitschende Weide", "Whomping Willow"]),
"hint": "Er wurde gepflanzt, als Remus Lupin als Schüler nach Hogwarts kam.",
"question_en": "Beneath which violent tree on the Hogwarts grounds is the secret tunnel leading to the Shrieking Shack?",
"answer_en": "Whomping Willow",
"hint_en": "It was planted the year Remus Lupin arrived at Hogwarts as a student."
},
{
"category": "hogwarts",
"difficulty": "easy",
"question_type": "text",
"question": "Wie heißt das einzige komplett von Zauberern bewohnte Dorf in Großbritannien, das in der Nähe von Hogwarts liegt?",
"canonical_answer": "Hogsmeade",
"accepted_answers": json.dumps(["Hogsmeade"]),
"hint": "Schüler ab der dritten Klasse dürfen es an bestimmten Wochenenden mit einer Unterschrift besuchen.",
"question_en": "What is the name of the only all-wizarding village in Britain, located near Hogwarts?",
"answer_en": "Hogsmeade",
"hint_en": "Students in their third year and above may visit it on certain weekends with a signed permission slip."
},
# --- QUIDDITCH ---
{
"category": "quidditch",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welches spektakuläre Flugmanöver führt Viktor Krum beim Finale der Quidditch-Weltmeisterschaft 1994 aus, um den gegnerischen Sucher Aidan Lynch in den Boden stürzen zu lassen?",
"canonical_answer": "Wronski-Bluff (Wronski Feint)",
"options": json.dumps([
"Wronski-Bluff (Wronski Feint)",
"Porskoff-Ploy",
"Falkenkopf-Angriffsformation",
"Faultier-Rolle (Sloth Grip Roll)"
], ensure_ascii=False),
"hint": "Der Sucher täuscht im Sturzflug vor, den Goldenen Schnatz am Boden gesehen zu haben, und zieht im letzten Moment hoch.",
"question_en": "Which famous diving maneuver does Viktor Krum execute during the 1994 Quidditch World Cup Final to trick Irish Seeker Aidan Lynch?",
"answer_en": "Wronski Feint",
"options_en": json.dumps([
"Wronski Feint",
"Porskoff Ploy",
"Hawkshead Attacking Formation",
"Sloth Grip Roll"
], ensure_ascii=False),
"hint_en": "The Seeker dives steeply toward the ground pretending to have spotted the Snitch, pulling up at the very last second."
},
{
"category": "quidditch",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welches magische Wesen war vor der Erfindung des Goldenen Schnatzes das ursprüngliche Jagdziel beim Quidditch?",
"canonical_answer": "Der Goldene Schnatzer (Golden Snidget)",
"options": json.dumps([
"Der Goldene Schnatzer (Golden Snidget)",
"Der Fwuuper",
"Der Augurey",
"Der Jobberknoll"
], ensure_ascii=False),
"hint": "Ein kugelrunder kleiner goldener Vogel, der dadurch fast ausgerottet wurde, bevor Bowman Wright den metallenen Schnatz schmiedete.",
"question_en": "Before the invention of the metallic Golden Snitch, which small magical bird was hunted during Quidditch matches?",
"answer_en": "Golden Snidget",
"options_en": json.dumps([
"Golden Snidget",
"Fwooper",
"Augurey",
"Jobberknoll"
], ensure_ascii=False),
"hint_en": "The tiny spherical golden bird was nearly hunted to extinction before Bowman Wright forged the enchanted metal replacement."
},
{
"category": "quidditch",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche beiden Nationalmannschaften standen sich im Finale der Quidditch-Weltmeisterschaft 1994 gegenüber?",
"canonical_answer": "Irland und Bulgarien",
"options": json.dumps([
"Irland und Bulgarien",
"England und Transsilvanien",
"Schottland und Frankreich",
"Irland und Rumänien"
], ensure_ascii=False),
"hint": "Viktor Krum fing zwar den Schnatz, doch die andere Mannschaft gewann trotzdem mit 170 zu 160 Punkten!",
"question_en": "Which two national teams faced off in the final of the 1994 Quidditch World Cup?",
"answer_en": "Ireland and Bulgaria",
"options_en": json.dumps([
"Ireland and Bulgaria",
"England and Transylvania",
"Scotland and France",
"Ireland and Romania"
], ensure_ascii=False),
"hint_en": "Viktor Krum caught the Snitch, yet the opposing team still won the match 170 to 160!"
},
{
"category": "quidditch",
"difficulty": "easy",
"question_type": "text",
"question": "Wie heißt das schnellste Rennbesen-Modell der Welt, das Harry im dritten Schuljahr anonym von seinem Paten Sirius Black geschenkt bekommt?",
"canonical_answer": "Feuerblitz",
"accepted_answers": json.dumps(["Feuerblitz", "Der Feuerblitz", "Firebolt"]),
"hint": "Er löst Harrys zerstörten Nimbus 2000 ab.",
"question_en": "What is the name of the world-class racing broom Harry receives anonymously from his godfather Sirius Black in his third year?",
"answer_en": "Firebolt",
"hint_en": "It replaces Harry's destroyed Nimbus 2000."
},
# --- CHARACTERS ---
{
"category": "characters",
"difficulty": "hard",
"question_type": "text",
"question": "In welches Tier verwandelt sich Rita Kimmkorn (Rita Skeeter) als nicht registrierter Animagus, um unbemerkt Gespräche zu belauschen?",
"canonical_answer": "Käfer",
"accepted_answers": json.dumps(["Käfer", "Ein Käfer", "Wasserkäfer", "Grüner Käfer", "Beetle"]),
"hint": "Hermine fängt sie am Ende des 4. Schuljahres in einem unzerbrechlichen Schraubglas.",
"question_en": "Into which insect does journalist Rita Skeeter transform as an unregistered Animagus to eavesdrop on conversations?",
"answer_en": "Beetle",
"hint_en": "Hermione traps her inside an unbreakable glass jar at the end of year four."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wer hinterließ die geheimnisvolle Notiz mit den Initialen 'R.A.B.' in der falschen Horkrux-Höhle?",
"canonical_answer": "Regulus Arcturus Black",
"options": json.dumps([
"Regulus Arcturus Black",
"Rodolphus Augustus Bell",
"Rufus Albus Bones",
"Rabastan Lestrange"
], ensure_ascii=False),
"hint": "Der jüngere Bruder von Sirius Black, der mit Hilfe des Hauselfen Kreacher das echte Medaillon stahl.",
"question_en": "Who left the mysterious note signed 'R.A.B.' inside the fake Horcrux locket in the sea cave?",
"answer_en": "Regulus Arcturus Black",
"options_en": json.dumps([
"Regulus Arcturus Black",
"Rodolphus Augustus Bell",
"Rufus Albus Bones",
"Rabastan Lestrange"
], ensure_ascii=False),
"hint_en": "Sirius Black's younger brother, who stole the real locket with the help of the house-elf Kreacher."
}
]
def seed_lore_pack():
db = SessionLocal()
try:
existing_questions = {q.question.strip().lower(): q for q in db.query(Question).all()}
max_sort = db.query(Question.sort_order).order_by(Question.sort_order.desc()).first()
next_sort = (max_sort[0] + 1) if max_sort and max_sort[0] else 100
added = 0
for item in LORE_PACK_QUESTIONS:
q_key = item["question"].strip().lower()
if q_key in existing_questions:
continue
new_q = Question(
category=item["category"],
difficulty=item["difficulty"],
question_type=item["question_type"],
question=item["question"],
canonical_answer=item["canonical_answer"],
accepted_answers=item.get("accepted_answers"),
options=item.get("options"),
hint=item.get("hint"),
question_en=item.get("question_en"),
answer_en=item.get("answer_en"),
options_en=item.get("options_en"),
hint_en=item.get("hint_en"),
status="active",
sort_order=next_sort,
submitted_by="System Lore Pack"
)
db.add(new_q)
next_sort += 1
added += 1
db.commit()
print(f"Lore Pack Seeding complete: {added} new questions added. Total active: {db.query(Question).filter(Question.status == 'active').count()}")
finally:
db.close()
if __name__ == "__main__":
seed_lore_pack()
+600 -558
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

+258 -119
View File
@@ -2,76 +2,123 @@ let currentSessionId = null;
let currentQuestionNumber = 1; let currentQuestionNumber = 1;
let totalQuestions = 10; let totalQuestions = 10;
let currentScore = 0; let currentScore = 0;
let currentStreak = 0;
let attemptsUsed = 0; let attemptsUsed = 0;
const maxAttempts = 3; const maxAttempts = 3;
let selectedDifficulty = "all"; let selectedDifficulty = "all";
let selectedCategory = "all";
let lastQuestionData = null; let lastQuestionData = null;
let timerStartTime = null; let timerStartTime = null;
let timerInterval = null; let timerInterval = null;
let elapsedSeconds = 0; let elapsedSeconds = 0;
const CATEGORY_LABELS = {
de: {
all: "✨ Alle Kategorien",
books: "📖 Buchwissen",
movies: "🎬 Filmwissen",
creatures: "🐉 Magische Geschöpfe",
spells: "🪄 Zaubersprüche & Tränke",
characters: "🧙‍♂️ Charaktere",
hogwarts: "🏰 Hogwarts & Orte",
quidditch: "🧹 Quidditch",
lore: "🔮 Artefakte & Lore",
general: "⚡ Allgemein",
harry_potter: "⚡ Harry Potter"
},
en: {
all: "✨ All Categories",
books: "📖 Book Lore",
movies: "🎬 Movie Trivia",
creatures: "🐉 Magical Creatures",
spells: "🪄 Spells & Potions",
characters: "🧙‍♂️ Characters",
hogwarts: "🏰 Hogwarts & Places",
quidditch: "🧹 Quidditch",
lore: "🔮 Artifacts & Lore",
general: "⚡ General",
harry_potter: "⚡ Harry Potter"
}
};
const UI_TEXT = { const UI_TEXT = {
de: { de: {
pageTitle: "✨ Magisches Hogwarts-Escape-Quiz ✨", pageTitle: "Harry Potter Wissensquiz",
pageSubtitle: "10 knifflige Rätsel aus der Zauberwelt warten auf dich", pageSubtitle: "Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.",
envelopeLetter: "Hogwarts-Schule für Hexerei und Zauberei", diffLabel: "Schwierigkeitsgrad",
introDesc: "Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?", diffAll: "Alle Stufen",
diffLabel: "Schwierigkeitsgrad wählen:",
diffAll: "Alle gemischt",
diffEasy: "Leicht", diffEasy: "Leicht",
diffMed: "Mittel", diffMed: "Mittel",
diffHard: "Schwer", diffHard: "Schwer (Experte)",
startBtn: "📜 Siegel brechen & Brief öffnen", catLabel: "Themengebiet wählen",
startBtnCasting: "✨ Zauber wird gewirkt...", catAll: "Alle Kategorien",
pointsLabel: "🏆 Hauspunkte", catBooks: "Buchwissen",
catMovies: "Filmwissen",
catCreatures: "Magische Geschöpfe",
catSpells: "Zaubersprüche & Tränke",
catChars: "Charaktere",
catHogwarts: "Hogwarts & Orte",
catQuidditch: "Quidditch",
catLore: "Artefakte & Lore",
dashboardRules: "<span>🎯 10 Fragen pro Runde</span><span>·</span><span>🔥 Streak-Zähler</span><span>·</span><span>💡 Bis zu 3 Versuche je Frage</span>",
startQuizBtn: "⚡ Quiz jetzt starten ➔",
startBtnCasting: "⚡ Runde wird geladen...",
pointsLabel: "Pkt.",
questionLabel: "Frage", questionLabel: "Frage",
ofLabel: "von", ofLabel: "/",
attemptLabel: "Versuch", attemptLabel: "Versuch",
checkBtn: "✨ Antwort prüfen", checkBtn: "✓ Antwort prüfen",
hintBtn: "💡 Hinweis anzeigen", hintBtn: "💡 Tipp anzeigen",
hintHeading: "📜 Notiz aus der verbotenen Abteilung:", hintHeading: "💡 Hinweis:",
placeholder: "Deine Antwort hier eingeben...", placeholder: "Deine Antwort eingeben...",
noHint: "Kein Tipp vorhanden.", noHint: "Kein Tipp vorhanden.",
hintFailed: "Tipp konnte nicht geladen werden.", hintFailed: "Tipp konnte nicht geladen werden.",
correctFeedback: "✨ Richtig! 1 Hauspunkt für dein Haus! ✨", wrongFeedback: "Nicht ganz! Noch {attempts} Versuch(e).",
wrongFeedback: "Leider falsch! Noch {attempts} Versuch(e).", exhaustedWrong: "Leider nicht gelöst (3 Fehlversuche).",
exhaustedWrong: "Leider nicht richtig (3 Fehlversuche).", solutionLabel: "Richtige Antwort:",
solutionLabel: "Richtige Lösung:",
startError: "Fehler beim Starten des Quiz: ", startError: "Fehler beim Starten des Quiz: ",
networkError: "Netzwerkfehler: " networkError: "Netzwerkfehler: "
}, },
en: { en: {
pageTitle: "✨ Magical Hogwarts Escape Quiz ✨", pageTitle: "Harry Potter Trivia Quiz",
pageSubtitle: "10 tricky wizarding world puzzles await you", pageSubtitle: "Test your knowledge across books, movies, creatures, and spells — straight to the action.",
envelopeLetter: "Hogwarts School of Witchcraft and Wizardry", diffLabel: "Difficulty Level",
introDesc: "An enchanted letter from Hogwarts is floating in the air before you.<br>Are you ready to take the exams and secure House Points?", diffAll: "All Levels",
diffLabel: "Choose difficulty level:",
diffAll: "All mixed",
diffEasy: "Easy", diffEasy: "Easy",
diffMed: "Medium", diffMed: "Medium",
diffHard: "Hard", diffHard: "Hard (Expert)",
startBtn: "📜 Break Seal & Open Letter", catLabel: "Select Topic",
startBtnCasting: "✨ Casting spell...", catAll: "All Categories",
pointsLabel: "🏆 House Points", catBooks: "Book Lore",
catMovies: "Movie Trivia",
catCreatures: "Magical Creatures",
catSpells: "Spells & Potions",
catChars: "Characters",
catHogwarts: "Hogwarts & Places",
catQuidditch: "Quidditch",
catLore: "Artifacts & Lore",
dashboardRules: "<span>🎯 10 Questions per Round</span><span>·</span><span>🔥 Answer Streak</span><span>·</span><span>💡 Up to 3 Attempts</span>",
startQuizBtn: "⚡ Start Quiz Now ➔",
startBtnCasting: "⚡ Loading Round...",
pointsLabel: "Pts",
questionLabel: "Question", questionLabel: "Question",
ofLabel: "of", ofLabel: "/",
attemptLabel: "Attempt", attemptLabel: "Attempt",
checkBtn: "✨ Check Answer", checkBtn: "✓ Check Answer",
hintBtn: "💡 Show Hint", hintBtn: "💡 Show Hint",
hintHeading: "📜 Note from the Restricted Section:", hintHeading: "💡 Hint:",
placeholder: "Type your answer here...", placeholder: "Type your answer...",
noHint: "No hint available.", noHint: "No hint available.",
hintFailed: "Could not load hint.", hintFailed: "Could not load hint.",
correctFeedback: "✨ Correct! 1 House Point for your House! ✨", wrongFeedback: "Not quite! {attempts} attempt(s) left.",
wrongFeedback: "Incorrect! {attempts} attempt(s) remaining.", exhaustedWrong: "Out of attempts (3 failed tries).",
exhaustedWrong: "Unfortunately incorrect (3 failed attempts).", solutionLabel: "Correct answer:",
solutionLabel: "Correct solution:",
startError: "Error starting quiz: ", startError: "Error starting quiz: ",
networkError: "Network error: " networkError: "Network error: "
} }
}; };
// Web Audio API Sound Generator for magic chimes and buzzers // Web Audio API Sound Generator for crisp trivia feedback
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) { function playSound(type) {
@@ -79,33 +126,31 @@ function playSound(type) {
audioCtx.resume(); audioCtx.resume();
} }
if (type === 'magic') { if (type === 'magic') {
// Magical bright arpeggio chime
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
notes.forEach((freq, index) => { notes.forEach((freq, index) => {
const osc = audioCtx.createOscillator(); const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain(); const gain = audioCtx.createGain();
osc.type = 'sine'; osc.type = 'sine';
osc.frequency.value = freq; osc.frequency.value = freq;
gain.gain.setValueAtTime(0.15, audioCtx.currentTime + index * 0.08); gain.gain.setValueAtTime(0.12, audioCtx.currentTime + index * 0.07);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.08 + 0.35); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.07 + 0.3);
osc.connect(gain); osc.connect(gain);
gain.connect(audioCtx.destination); gain.connect(audioCtx.destination);
osc.start(audioCtx.currentTime + index * 0.08); osc.start(audioCtx.currentTime + index * 0.07);
osc.stop(audioCtx.currentTime + index * 0.08 + 0.35); osc.stop(audioCtx.currentTime + index * 0.07 + 0.3);
}); });
} else if (type === 'wrong') { } else if (type === 'wrong') {
// Soft error buzz
const osc = audioCtx.createOscillator(); const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain(); const gain = audioCtx.createGain();
osc.type = 'sawtooth'; osc.type = 'triangle';
osc.frequency.setValueAtTime(150, audioCtx.currentTime); osc.frequency.setValueAtTime(180, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.25); osc.frequency.exponentialRampToValueAtTime(95, audioCtx.currentTime + 0.22);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime); gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.25); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.22);
osc.connect(gain); osc.connect(gain);
gain.connect(audioCtx.destination); gain.connect(audioCtx.destination);
osc.start(); osc.start();
osc.stop(audioCtx.currentTime + 0.25); osc.stop(audioCtx.currentTime + 0.22);
} }
} }
@@ -116,42 +161,66 @@ function selectDifficulty(diff) {
}); });
} }
function selectCategory(cat) {
selectedCategory = cat;
document.querySelectorAll(".cat-chip").forEach(chip => {
chip.classList.toggle("active", chip.getAttribute("data-cat") === cat);
});
}
function updateStreakUI(animate = false) {
const badge = document.getElementById("streakBadge");
if (!badge) return;
badge.innerText = currentStreak >= 2 ? `🔥 ${currentStreak} Streak` : `🔥 ${currentStreak}`;
badge.classList.toggle("streak-hot", currentStreak >= 2);
if (animate) {
badge.classList.remove("streak-pop");
void badge.offsetWidth;
badge.classList.add("streak-pop");
}
}
function onLanguageChange(lang) { function onLanguageChange(lang) {
applyQuizTranslations(lang); applyQuizTranslations(lang);
} }
function applyQuizTranslations(lang) { function applyQuizTranslations(lang) {
const t = UI_TEXT[lang] || UI_TEXT.de; const t = UI_TEXT[lang] || UI_TEXT.de;
const pageTitle = document.getElementById("pageTitle"); const setTxt = (id, val) => {
const pageSubtitle = document.getElementById("pageSubtitle"); const el = document.getElementById(id);
const envelopeLetter = document.getElementById("envelopeLetter"); if (el && val) el.innerText = val;
const introDesc = document.getElementById("introDesc"); };
const diffLabel = document.getElementById("diffLabel");
const diffAllText = document.getElementById("diffAllText");
const diffEasyText = document.getElementById("diffEasyText");
const diffMedText = document.getElementById("diffMedText");
const diffHardText = document.getElementById("diffHardText");
const startBtn = document.getElementById("startBtn");
const checkBtn = document.getElementById("checkBtn");
const hintBtn = document.getElementById("hintBtn");
const hintBtnMC = document.getElementById("hintBtnMC");
const hintHeading = document.getElementById("hintHeading");
const answerInput = document.getElementById("answerInput");
if (pageTitle) pageTitle.innerText = t.pageTitle; setTxt("pageTitle", t.pageTitle);
if (pageSubtitle) pageSubtitle.innerText = t.pageSubtitle; setTxt("pageSubtitle", t.pageSubtitle);
if (envelopeLetter) envelopeLetter.innerText = t.envelopeLetter; setTxt("diffLabel", t.diffLabel);
if (introDesc) introDesc.innerHTML = t.introDesc; setTxt("diffAllText", t.diffAll);
if (diffLabel) diffLabel.innerText = t.diffLabel; setTxt("diffEasyText", t.diffEasy);
if (diffAllText) diffAllText.innerText = t.diffAll; setTxt("diffMedText", t.diffMed);
if (diffEasyText) diffEasyText.innerText = t.diffEasy; setTxt("diffHardText", t.diffHard);
if (diffMedText) diffMedText.innerText = t.diffMed; setTxt("catLabel", t.catLabel);
if (diffHardText) diffHardText.innerText = t.diffHard; setTxt("catAllText", t.catAll);
if (startBtn && !startBtn.disabled) startBtn.innerText = t.startBtn; setTxt("catBooksText", t.catBooks);
if (checkBtn) checkBtn.innerText = t.checkBtn; setTxt("catMoviesText", t.catMovies);
if (hintBtn) hintBtn.innerText = t.hintBtn; setTxt("catCreaturesText", t.catCreatures);
if (hintBtnMC) hintBtnMC.innerText = t.hintBtn; setTxt("catSpellsText", t.catSpells);
if (hintHeading) hintHeading.innerText = t.hintHeading; setTxt("catCharsText", t.catChars);
setTxt("catHogwartsText", t.catHogwarts);
setTxt("catQuidditchText", t.catQuidditch);
setTxt("catLoreText", t.catLore);
const rulesEl = document.getElementById("dashboardRules");
if (rulesEl) rulesEl.innerHTML = t.dashboardRules;
const startQuizBtn = document.getElementById("startQuizBtn");
if (startQuizBtn && !startQuizBtn.disabled) startQuizBtn.innerText = t.startQuizBtn;
setTxt("checkBtn", t.checkBtn);
setTxt("hintBtn", t.hintBtn);
setTxt("hintBtnMC", t.hintBtn);
setTxt("hintHeading", t.hintHeading);
const answerInput = document.getElementById("answerInput");
if (answerInput) answerInput.placeholder = t.placeholder; if (answerInput) answerInput.placeholder = t.placeholder;
if (lastQuestionData) { if (lastQuestionData) {
@@ -159,36 +228,85 @@ function applyQuizTranslations(lang) {
} }
} }
let sessionQuestionIds = [];
function getRecentQuestionIds() {
try {
const stored = localStorage.getItem("quiz_recent_qids");
return stored ? JSON.parse(stored) : [];
} catch(e) {
return [];
}
}
function getWrongQuestionIds() {
try {
const stored = localStorage.getItem("quiz_wrong_qids");
return stored ? JSON.parse(stored) : [];
} catch(e) {
return [];
}
}
function recordQuestionWrong(qid) {
if (!qid) return;
try {
const wrong = getWrongQuestionIds();
if (!wrong.includes(qid)) {
wrong.push(qid);
if (wrong.length > 30) wrong.shift();
localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong));
}
} catch(e) {}
}
function recordQuestionMastered(qid) {
if (!qid) return;
try {
let wrong = getWrongQuestionIds();
if (wrong.includes(qid)) {
wrong = wrong.filter(id => id !== qid);
localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong));
}
} catch(e) {}
}
function saveRecentQuestions(ids) {
if (!ids || ids.length === 0) return;
try {
let recent = getRecentQuestionIds();
recent = Array.from(new Set([...ids, ...recent])).slice(0, 20);
localStorage.setItem("quiz_recent_qids", JSON.stringify(recent));
} catch(e) {}
}
async function startGame() { async function startGame() {
const envelope = document.getElementById("envelope"); const startQuizBtn = document.getElementById("startQuizBtn");
const startBtn = document.getElementById("startBtn");
const t = UI_TEXT[currentLang] || UI_TEXT.de; const t = UI_TEXT[currentLang] || UI_TEXT.de;
startBtn.disabled = true; if (startQuizBtn) {
startBtn.innerText = t.startBtnCasting; startQuizBtn.disabled = true;
envelope.classList.add("envelope-opened"); startQuizBtn.innerText = t.startBtnCasting;
}
// Start background music if enabled or first click playSound('magic');
const bgm = document.getElementById("bgm"); sessionQuestionIds = [];
if (bgm && bgm.paused) { currentStreak = 0;
bgm.play().then(() => { updateStreakUI(false);
const icon = document.getElementById("audioIcon");
const text = document.getElementById("audioText");
if (icon && text) {
icon.innerText = "🔊";
text.innerText = (currentLang === "de") ? "Musik: An" : "Music: On";
}
localStorage.setItem("bgm_enabled", "true");
}).catch(() => {});
}
try { try {
const recentIds = getRecentQuestionIds();
const wrongIds = getWrongQuestionIds();
const res = await fetch("/api/game/start", { const res = await fetch("/api/game/start", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
lang: currentLang, lang: currentLang,
difficulty: selectedDifficulty difficulty: selectedDifficulty,
category: selectedCategory,
recent_ids: recentIds,
wrong_ids: wrongIds
}) })
}); });
@@ -203,18 +321,20 @@ async function startGame() {
currentScore = data.score; currentScore = data.score;
attemptsUsed = data.attempts_used; attemptsUsed = data.attempts_used;
setTimeout(() => { const introCard = document.getElementById("introCard");
document.getElementById("introCard").classList.add("hidden"); if (introCard) {
introCard.classList.add("hidden");
}
document.getElementById("gameCard").classList.remove("hidden"); document.getElementById("gameCard").classList.remove("hidden");
startTimer(); startTimer();
renderQuestion(data); renderQuestion(data);
}, 1100);
} catch (err) { } catch (err) {
alert(t.startError + err.message); alert(t.startError + err.message);
startBtn.disabled = false; if (startQuizBtn) {
startBtn.innerText = t.startBtn; startQuizBtn.disabled = false;
envelope.classList.remove("envelope-opened"); startQuizBtn.innerText = t.startQuizBtn;
}
} }
} }
@@ -239,13 +359,18 @@ function updateTimer() {
function renderQuestion(qData) { function renderQuestion(qData) {
lastQuestionData = qData; lastQuestionData = qData;
if (qData.id && !sessionQuestionIds.includes(qData.id)) {
sessionQuestionIds.push(qData.id);
}
const t = UI_TEXT[currentLang] || UI_TEXT.de; const t = UI_TEXT[currentLang] || UI_TEXT.de;
const catMap = CATEGORY_LABELS[currentLang] || CATEGORY_LABELS.de;
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`; document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`; document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`;
document.getElementById("badgeCategory").innerText = `⚡ ${qData.category.replace("_", " ").toUpperCase()}`;
const rawCat = (qData.category || "general").toLowerCase();
document.getElementById("badgeCategory").innerText = catMap[rawCat] || `⚡ ${rawCat.toUpperCase()}`;
// Difficulty badge
const badgeDiff = document.getElementById("badgeDifficulty"); const badgeDiff = document.getElementById("badgeDifficulty");
if (badgeDiff) { if (badgeDiff) {
const diff = qData.difficulty || "medium"; const diff = qData.difficulty || "medium";
@@ -256,7 +381,7 @@ function renderQuestion(qData) {
diffText = t.diffEasy; diffText = t.diffEasy;
diffIcon = "🟢"; diffIcon = "🟢";
} else if (diff === "hard") { } else if (diff === "hard") {
diffText = t.diffHard; diffText = currentLang === "en" ? "Hard" : "Schwer";
diffIcon = "🔴"; diffIcon = "🔴";
} }
badgeDiff.innerText = `${diffIcon} ${diffText}`; badgeDiff.innerText = `${diffIcon} ${diffText}`;
@@ -328,7 +453,6 @@ async function submitAnswer(answer, clickedBtn = null) {
if (checkBtn) checkBtn.disabled = true; if (checkBtn) checkBtn.disabled = true;
if (input) input.disabled = true; if (input) input.disabled = true;
// Disable all choice buttons
const choiceBtns = document.querySelectorAll(".choice-btn"); const choiceBtns = document.querySelectorAll(".choice-btn");
choiceBtns.forEach(b => b.disabled = true); choiceBtns.forEach(b => b.disabled = true);
if (clickedBtn) { if (clickedBtn) {
@@ -351,20 +475,32 @@ async function submitAnswer(answer, clickedBtn = null) {
const result = await res.json(); const result = await res.json();
currentScore = result.score; currentScore = result.score;
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`; document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`;
const frageBox = document.getElementById("frageBox"); const frageBox = document.getElementById("frageBox");
const feedbackBox = document.getElementById("feedbackBox"); const feedbackBox = document.getElementById("feedbackBox");
if (result.correct) { if (result.correct) {
// Success!
playSound('magic'); playSound('magic');
currentStreak += 1;
updateStreakUI(true);
if (clickedBtn) { if (clickedBtn) {
clickedBtn.classList.remove("choice-selected"); clickedBtn.classList.remove("choice-selected");
clickedBtn.classList.add("choice-correct"); clickedBtn.classList.add("choice-correct");
} }
frageBox.classList.add("sparkle-effect"); frageBox.classList.add("sparkle-effect");
feedbackBox.innerHTML = `<span class="feedback-correct">${t.correctFeedback}</span>`;
if (result.question_id) {
recordQuestionMastered(result.question_id);
}
const pts = result.points_earned || 100;
const streakBonusText = currentStreak >= 3 ? ` · 🔥 ${currentStreak}er Serie!` : "";
const ptsText = (currentLang === "en")
? `✓ Correct! +${pts} Points${currentStreak >= 3 ? ` · 🔥 ${currentStreak} Streak!` : ""}`
: `✓ Richtig! +${pts} Punkte${streakBonusText}`;
feedbackBox.innerHTML = `<span class="feedback-correct">${ptsText}</span>`;
setTimeout(() => { setTimeout(() => {
frageBox.classList.remove("sparkle-effect"); frageBox.classList.remove("sparkle-effect");
@@ -375,11 +511,13 @@ async function submitAnswer(answer, clickedBtn = null) {
attemptsUsed = 0; attemptsUsed = 0;
renderQuestion(result.next_question); renderQuestion(result.next_question);
} }
}, 1200); }, 1100);
} else { } else {
// Incorrect
playSound('wrong'); playSound('wrong');
currentStreak = 0;
updateStreakUI(false);
if (clickedBtn) { if (clickedBtn) {
clickedBtn.classList.remove("choice-selected"); clickedBtn.classList.remove("choice-selected");
clickedBtn.classList.add("choice-wrong"); clickedBtn.classList.add("choice-wrong");
@@ -391,7 +529,10 @@ async function submitAnswer(answer, clickedBtn = null) {
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`; document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
if (result.attempts_left === 0) { if (result.attempts_left === 0) {
// Exhausted all attempts if (result.question_id) {
recordQuestionWrong(result.question_id);
}
feedbackBox.innerHTML = ` feedbackBox.innerHTML = `
<div class="feedback-wrong">${t.exhaustedWrong}</div> <div class="feedback-wrong">${t.exhaustedWrong}</div>
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div> <div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
@@ -408,7 +549,6 @@ async function submitAnswer(answer, clickedBtn = null) {
}, 2200); }, 2200);
} else { } else {
// Still attempts remaining
const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left); const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left);
feedbackBox.innerHTML = `<span class="feedback-wrong">${msg}</span>`; feedbackBox.innerHTML = `<span class="feedback-wrong">${msg}</span>`;
if (checkBtn) checkBtn.disabled = false; if (checkBtn) checkBtn.disabled = false;
@@ -416,7 +556,6 @@ async function submitAnswer(answer, clickedBtn = null) {
input.disabled = false; input.disabled = false;
input.select(); input.select();
} }
// Re-enable choice buttons
choiceBtns.forEach(b => b.disabled = false); choiceBtns.forEach(b => b.disabled = false);
} }
} }
@@ -448,11 +587,11 @@ async function requestHint() {
function finishGame() { function finishGame() {
if (timerInterval) clearInterval(timerInterval); if (timerInterval) clearInterval(timerInterval);
saveRecentQuestions(sessionQuestionIds);
document.getElementById("progressBar").style.width = "100%"; document.getElementById("progressBar").style.width = "100%";
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}`; window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}&diff=${encodeURIComponent(selectedDifficulty)}`;
} }
// Enter key to submit
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const input = document.getElementById("answerInput"); const input = document.getElementById("answerInput");
if (input) { if (input) {
+53 -8
View File
@@ -1,10 +1,20 @@
const SUGGESTION_TEXT = { const SUGGESTION_TEXT = {
de: { de: {
title: "✨ Neue Frage für das Quiz vorschlagen ✨", title: "✍️ Neue Quiz-Frage einreichen",
subtitle: "Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!", subtitle: "Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!",
alert: "ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status <em>„Prüfung ausstehend“</em>. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird.", alert: "ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.",
nameLabel: "Dein Name / Zauberer-Alias (optional):", nameLabel: "Dein Name / Alias (optional):",
namePlaceholder: "z. B. Hermine Granger", namePlaceholder: "z. B. Hermine Granger",
catLabel: "Kategorie / Themenbereich:",
catBooks: "📖 Buchwissen (Buch-Details)",
catMovies: "🎬 Filmwissen (Kino & Drehorte)",
catHogwarts: "🏰 Hogwarts & Orte",
catCharacters: "🧙‍♂️ Charaktere & Personen",
catSpells: "🪄 Zaubersprüche & Tränke",
catCreatures: "🐉 Magische Geschöpfe",
catQuidditch: "🧹 Quidditch & Sport",
catLore: "🔮 Artefakte & Geschichte",
catGeneral: "⚡ Allgemeines Wissen",
frageLabel: "Deine Frage: *", frageLabel: "Deine Frage: *",
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?", fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
diffLabel: "Schwierigkeitsgrad:", diffLabel: "Schwierigkeitsgrad:",
@@ -28,11 +38,21 @@ const SUGGESTION_TEXT = {
successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨" successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨"
}, },
en: { en: {
title: "✨ Suggest a New Quiz Question ✨", title: "✍️ Submit a New Quiz Question",
subtitle: "Enrich the magical quiz with tricky Harry Potter or escape puzzles!", subtitle: "Enrich PotterTrivia with your own questions about the books, movies, or magical creatures!",
alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are not immediately public; they automatically receive the status <em>'Pending Review'</em>. The Ministry of Magic tests and verifies the answer before approval.", alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are briefly reviewed for accuracy before going live in the public quiz pool.",
nameLabel: "Your Name / Wizard Alias (optional):", nameLabel: "Your Name / Alias (optional):",
namePlaceholder: "e.g. Hermione Granger", namePlaceholder: "e.g. Hermione Granger",
catLabel: "Category / Topic Area:",
catBooks: "📖 Book Lore (Book Details)",
catMovies: "🎬 Movie Trivia (Cinema & Filming)",
catHogwarts: "🏰 Hogwarts & Places",
catCharacters: "🧙‍♂️ Characters & People",
catSpells: "🪄 Spells & Potions",
catCreatures: "🐉 Magical Creatures",
catQuidditch: "🧹 Quidditch & Sport",
catLore: "🔮 Artifacts & Lore",
catGeneral: "⚡ General Knowledge",
frageLabel: "Your Question: *", frageLabel: "Your Question: *",
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?", fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
diffLabel: "Difficulty Level:", diffLabel: "Difficulty Level:",
@@ -75,11 +95,32 @@ function onLanguageChange(lang) {
const tippInput = document.getElementById("tipp"); const tippInput = document.getElementById("tipp");
const submitBtn = document.getElementById("submitBtn"); const submitBtn = document.getElementById("submitBtn");
const catLabel = document.getElementById("suggCatLabel");
const catBooks = document.getElementById("catBooks");
const catMovies = document.getElementById("catMovies");
const catHogwarts = document.getElementById("catHogwarts");
const catCharacters = document.getElementById("catCharacters");
const catSpells = document.getElementById("catSpells");
const catCreatures = document.getElementById("catCreatures");
const catQuidditch = document.getElementById("catQuidditch");
const catLore = document.getElementById("catLore");
const catGeneral = document.getElementById("catGeneral");
if (title) title.innerText = t.title; if (title) title.innerText = t.title;
if (subtitle) subtitle.innerText = t.subtitle; if (subtitle) subtitle.innerText = t.subtitle;
if (alertBox) alertBox.innerHTML = t.alert; if (alertBox) alertBox.innerHTML = t.alert;
if (nameLabel) nameLabel.innerText = t.nameLabel; if (nameLabel) nameLabel.innerText = t.nameLabel;
if (nameInput) nameInput.placeholder = t.namePlaceholder; if (nameInput) nameInput.placeholder = t.namePlaceholder;
if (catLabel) catLabel.innerText = t.catLabel;
if (catBooks) catBooks.innerText = t.catBooks;
if (catMovies) catMovies.innerText = t.catMovies;
if (catHogwarts) catHogwarts.innerText = t.catHogwarts;
if (catCharacters) catCharacters.innerText = t.catCharacters;
if (catSpells) catSpells.innerText = t.catSpells;
if (catCreatures) catCreatures.innerText = t.catCreatures;
if (catQuidditch) catQuidditch.innerText = t.catQuidditch;
if (catLore) catLore.innerText = t.catLore;
if (catGeneral) catGeneral.innerText = t.catGeneral;
if (frageLabel) frageLabel.innerText = t.frageLabel; if (frageLabel) frageLabel.innerText = t.frageLabel;
if (frageInput) frageInput.placeholder = t.fragePlaceholder; if (frageInput) frageInput.placeholder = t.fragePlaceholder;
if (diffLabel) diffLabel.innerText = t.diffLabel; if (diffLabel) diffLabel.innerText = t.diffLabel;
@@ -151,8 +192,12 @@ async function submitSuggestion(event) {
submitBtn.innerText = t.submitBtnCasting; submitBtn.innerText = t.submitBtnCasting;
try { try {
const catElem = document.getElementById("kategorie");
const category = catElem ? catElem.value : "general";
const payload = { const payload = {
name: name || "Gast", name: name || "Gast",
category: category,
frage: frage, frage: frage,
antwort: antwort, antwort: antwort,
tipp: tipp || null, tipp: tipp || null,
+10 -4
View File
@@ -225,9 +225,15 @@
<div class="form-group"> <div class="form-group">
<label for="category">Kategorie:</label> <label for="category">Kategorie:</label>
<select name="category" id="category" class="input-text"> <select name="category" id="category" class="input-text">
<option value="harry_potter" selected>Harry Potter / Escape</option> <option value="books">📖 Buchwissen</option>
<option value="insider">Persönlich / Insider</option> <option value="movies">🎬 Filmwissen</option>
<option value="general">Allgemeines Rätsel</option> <option value="creatures">🐉 Magische Geschöpfe</option>
<option value="spells">🪄 Zaubersprüche & Tränke</option>
<option value="characters" selected>🧙‍♂️ Charaktere</option>
<option value="hogwarts">🏰 Hogwarts & Orte</option>
<option value="quidditch">🧹 Quidditch</option>
<option value="lore">🔮 Artefakte & Lore</option>
<option value="general">⚡ Allgemein</option>
</select> </select>
</div> </div>
@@ -309,5 +315,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/admin.js"></script> <script src="/static/js/admin.js?v=2.3"></script>
{% endblock %} {% endblock %}
+19 -64
View File
@@ -3,43 +3,39 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title>{% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %}</title> <title>{% block title %}PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %}</title>
<link rel="icon" href="/static/favicon.ico"> <link rel="icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/css/style.css"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css?v=4.0">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<audio id="bgm" src="/static/audio/music.mp3" loop preload="auto"></audio>
<header class="site-header"> <header class="site-header">
<div class="header-inner"> <div class="header-inner">
<a href="/" class="brand-link"> <a href="/" class="brand-link">
<span class="brand-sparkle">⚡</span> <span class="brand-badge">⚡</span>
<span class="brand-title">Hogwarts Quiz</span> <span class="brand-title">Potter<span class="brand-accent">Trivia</span></span>
</a> </a>
<nav id="navLinks" class="nav-links">
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">🎯 Quiz</a>
<a href="/bestenliste" class="nav-link" id="navLeaderboard" onclick="closeMobileMenu()">🏆 Bestenliste</a>
<a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
<a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">⚙️ Admin</a>
</nav>
<div class="header-right"> <div class="header-right">
<div class="lang-switch-group" role="group" aria-label="Sprachauswahl / Language"> <div class="lang-switch-group" role="group" aria-label="Sprachauswahl / Language">
<button type="button" id="langDe" class="lang-btn active" onclick="setLanguage('de')" title="Deutsch">🇩🇪 DE</button> <button type="button" id="langDe" class="lang-btn active" onclick="setLanguage('de')" title="Deutsch">🇩🇪 DE</button>
<button type="button" id="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button> <button type="button" id="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button>
</div> </div>
<button id="audioToggle" class="audio-btn" title="Musik an/aus" onclick="toggleAudio()">
<span id="audioIcon">🔇</span>
<span id="audioText" class="audio-label">Musik: Aus</span>
</button>
<button id="menuToggle" class="menu-toggle-btn" aria-label="Menü öffnen" onclick="toggleMobileMenu()"> <button id="menuToggle" class="menu-toggle-btn" aria-label="Menü öffnen" onclick="toggleMobileMenu()">
<span id="menuIcon">☰</span> <span id="menuIcon">☰</span>
</button> </button>
</div> </div>
<nav id="navLinks" class="nav-links">
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">✨ Zum Quiz</a>
<a href="/bestenliste" class="nav-link" id="navLeaderboard" onclick="closeMobileMenu()">🏆 Bestenliste</a>
<a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
<a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">🔒 Admin-Cockpit</a>
</nav>
</div> </div>
</header> </header>
@@ -48,7 +44,7 @@
</main> </main>
<footer class="footer"> <footer class="footer">
<p id="footerText">⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p> <p id="footerText">⚡ PotterTrivia · Das unabhängige Harry-Potter-Wissensquiz</p>
<div class="footer-links"> <div class="footer-links">
<a href="/impressum" class="footer-link" id="footerImpressum">⚖️ Impressum</a> <a href="/impressum" class="footer-link" id="footerImpressum">⚖️ Impressum</a>
<span class="footer-sep">·</span> <span class="footer-sep">·</span>
@@ -57,9 +53,6 @@
</footer> </footer>
<script> <script>
const bgm = document.getElementById("bgm");
const audioIcon = document.getElementById("audioIcon");
const audioText = document.getElementById("audioText");
const navLinks = document.getElementById("navLinks"); const navLinks = document.getElementById("navLinks");
const menuIcon = document.getElementById("menuIcon"); const menuIcon = document.getElementById("menuIcon");
@@ -92,43 +85,17 @@
const footerImpressum = document.getElementById("footerImpressum"); const footerImpressum = document.getElementById("footerImpressum");
const footerDatenschutz = document.getElementById("footerDatenschutz"); const footerDatenschutz = document.getElementById("footerDatenschutz");
if (navQuiz) navQuiz.innerText = isDe ? "✨ Zum Quiz" : "✨ To Quiz"; if (navQuiz) navQuiz.innerText = isDe ? "🎯 Quiz" : "🎯 Quiz";
if (navLeaderboard) navLeaderboard.innerText = isDe ? "🏆 Bestenliste" : "🏆 Leaderboard"; if (navLeaderboard) navLeaderboard.innerText = isDe ? "🏆 Bestenliste" : "🏆 Leaderboard";
if (navSuggest) navSuggest.innerText = isDe ? "✍️ Frage einreichen" : "✍️ Submit Question"; if (navSuggest) navSuggest.innerText = isDe ? "✍️ Frage einreichen" : "✍️ Submit Question";
if (navAdmin) navAdmin.innerText = isDe ? "🔒 Admin-Cockpit" : "🔒 Admin Cockpit"; if (navAdmin) navAdmin.innerText = isDe ? "⚙️ Admin" : "⚙️ Admin";
if (footerText) { if (footerText) {
footerText.innerText = isDe footerText.innerText = isDe
? "⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡" ? "⚡ PotterTrivia · Das unabhängige Harry-Potter-Wissensquiz"
: "⚡ Magical Hogwarts Escape Quiz · Powered by Love & Wizardry ⚡"; : "⚡ PotterTrivia · The Independent Wizarding World Trivia Quiz";
} }
if (footerImpressum) footerImpressum.innerText = isDe ? "⚖️ Impressum" : "⚖️ Legal Notice"; if (footerImpressum) footerImpressum.innerText = isDe ? "⚖️ Impressum" : "⚖️ Legal Notice";
if (footerDatenschutz) footerDatenschutz.innerText = isDe ? "🛡️ Datenschutz" : "🛡️ Privacy Policy"; if (footerDatenschutz) footerDatenschutz.innerText = isDe ? "🛡️ Datenschutz" : "🛡️ Privacy Policy";
if (audioText) {
const isPlaying = bgm && !bgm.paused;
if (isDe) {
audioText.innerText = isPlaying ? "Musik: An" : "Musik: Aus";
} else {
audioText.innerText = isPlaying ? "Music: On" : "Music: Off";
}
}
}
function toggleAudio() {
const isDe = currentLang === "de";
if (bgm.paused) {
bgm.play().then(() => {
audioIcon.innerText = "🔊";
audioText.innerText = isDe ? "Musik: An" : "Music: On";
localStorage.setItem("bgm_enabled", "true");
}).catch(err => {
console.log("Audio play blocked:", err);
});
} else {
bgm.pause();
audioIcon.innerText = "🔇";
audioText.innerText = isDe ? "Musik: Aus" : "Music: Off";
localStorage.setItem("bgm_enabled", "false");
}
} }
function toggleMobileMenu() { function toggleMobileMenu() {
@@ -141,18 +108,6 @@
menuIcon.innerText = "☰"; menuIcon.innerText = "☰";
} }
// Auto-resume audio if previously enabled
document.addEventListener("click", function initAudioOnInteraction() {
if (localStorage.getItem("bgm_enabled") === "true" && bgm.paused) {
bgm.play().then(() => {
audioIcon.innerText = "🔊";
audioText.innerText = (currentLang === "de") ? "Musik: An" : "Music: On";
}).catch(() => {});
}
document.removeEventListener("click", initAudioOnInteraction);
}, { once: true });
// Initialize UI on load
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
updateLanguageUI(); updateLanguageUI();
}); });
+1 -1
View File
@@ -181,7 +181,7 @@
tr.innerHTML = ` tr.innerHTML = `
<td><strong>${rankDisplay}</strong></td> <td><strong>${rankDisplay}</strong></td>
<td><strong>${escapeHtml(e.nickname)}</strong></td> <td><strong>${escapeHtml(e.nickname)}</strong></td>
<td><span class="gold-text font-weight-bold">${e.score}/${e.total_questions}</span></td> <td><span class="gold-text font-weight-bold">${e.score} Pkt.</span></td>
<td>${e.time_formatted}</td> <td>${e.time_formatted}</td>
<td><span class="badge badge-diff ${diffBadgeClass}">${diffLabel}</span></td> <td><span class="badge badge-diff ${diffBadgeClass}">${diffLabel}</span></td>
<td class="text-muted" style="font-size: 0.85rem;">${e.date}</td> <td class="text-muted" style="font-size: 0.85rem;">${e.date}</td>
+59 -11
View File
@@ -17,9 +17,9 @@
<div class="score-banner"> <div class="score-banner">
<p class="score-highlight" id="finaleScoreText"> <p class="score-highlight" id="finaleScoreText">
{% if lang == 'en' %} {% if lang == 'en' %}
You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ total }}</span> House Points! You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ max_score }}</span> House Points!
{% else %} {% else %}
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> Hauspunkten erzielt! Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ max_score }}</span> Hauspunkten erzielt!
{% endif %} {% endif %}
</p> </p>
<p class="time-highlight" id="finaleTimeText"> <p class="time-highlight" id="finaleTimeText">
@@ -45,6 +45,9 @@
<form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;"> <form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;">
<div class="input-with-btn"> <div class="input-with-btn">
<input type="text" id="nicknameInput" class="input-text" placeholder="z. B. Fliegender_Besen oder Lord_Voldi" maxlength="30" required autocomplete="off"> <input type="text" id="nicknameInput" class="input-text" placeholder="z. B. Fliegender_Besen oder Lord_Voldi" maxlength="30" required autocomplete="off">
<button type="button" id="diceBtn" class="btn btn-secondary btn-dice" onclick="rollRandomName()" title="Zufälliger Zauberer-Name">
🎲 <span class="dice-label" id="diceLabel">Zufall</span>
</button>
<button type="submit" id="lbSubmitBtn" class="btn btn-primary"> <button type="submit" id="lbSubmitBtn" class="btn btn-primary">
📜 Verewigen 📜 Verewigen
</button> </button>
@@ -91,17 +94,56 @@
<script> <script>
const score = {{ score }}; const score = {{ score }};
const total = {{ total }}; const total = {{ total }};
const maxScore = {{ max_score }};
const timeSec = {{ time_seconds }}; const timeSec = {{ time_seconds }};
const difficulty = "{{ difficulty }}";
let scoreAlreadySubmitted = false; let scoreAlreadySubmitted = false;
const WIZARD_NAMES = {
de: [
"Fliegender_Besen", "Goldener_Schnatz", "Butterbier_Genießer",
"Severus_Snape_Fan", "Dumbledores_Phaenix", "Hogwarts_Spion",
"Peeves_Geist", "Niffler_Raeuber", "Gryffindor_Loewe",
"Ravenclaw_Adler", "Hufflepuff_Dachs", "Slytherin_Schlange",
"Mondkalb_Freund", "Zauberschach_Meister", "Kessel_Explodierer",
"Alraunen_Zaehmer", "Krummbein_Fan", "Tarnumhang_Traeger",
"Zeitumkehrer_Reisender", "Kuerbissaft_Trinker", "Patronus_Beschwoerer",
"Erlesener_Zauberstab", "Kopfloser_Reiter", "Schokofrosch_Sammler",
"Verbotene_Abteilung", "Hippogreif_Reiter", "Hauselfen_Freund"
],
en: [
"Flying_Broom", "Golden_Snitch", "Butterbeer_Lover",
"Severus_Fan", "Dumbledores_Phoenix", "Hogwarts_Spy",
"Peeves_Poltergeist", "Niffler_Bandit", "Gryffindor_Lion",
"Ravenclaw_Eagle", "Hufflepuff_Badger", "Slytherin_Snake",
"Mooncalf_Buddy", "Wizard_Chess_Master", "Cauldron_Exploder",
"Mandrake_Whisperer", "Crookshanks_Fan", "Invisibility_Bearer",
"Time_Turner_Voyager", "Pumpkin_Juice_Drinker", "Patronus_Caster",
"Elder_Wand_Seeker", "Headless_Hunt_Guest", "Chocolate_Frog_Collector",
"Restricted_Section", "Hippogriff_Rider", "House_Elf_Defender"
]
};
function rollRandomName() {
const isDe = (currentLang !== "en");
const list = isDe ? WIZARD_NAMES.de : WIZARD_NAMES.en;
const name = list[Math.floor(Math.random() * list.length)];
const input = document.getElementById("nicknameInput");
if (input) {
input.value = name;
input.focus();
}
}
const FINALE_I18N = { const FINALE_I18N = {
de: { de: {
title: "⚡ Das große Hogwarts-Zeugnis ⚡", title: "⚡ Das große Hogwarts-Zeugnis ⚡",
scoreText: `Du hast <span class="gold-text">${score}</span> von <span class="gold-text">${total}</span> Hauspunkten erzielt!`, scoreText: `Du hast <span class="gold-text">${score}</span> von <span class="gold-text">${maxScore}</span> Hauspunkten erzielt!`,
timeText: `⏱️ Benötigte Zeit: <strong class="gold-text">${formatSecondsDe(timeSec)}</strong>`, timeText: `⏱️ Benötigte Zeit: <strong class="gold-text">${formatSecondsDe(timeSec)}</strong>`,
lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!", lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!",
lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:", lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:",
lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi", lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi",
diceLabel: "Zufall",
lbBtn: "📜 Verewigen", lbBtn: "📜 Verewigen",
lbBtnSending: "✨ Wird verewigt...", lbBtnSending: "✨ Wird verewigt...",
miniLbTitle: "Top Zauberer:", miniLbTitle: "Top Zauberer:",
@@ -115,11 +157,12 @@
}, },
en: { en: {
title: "⚡ The Great Hogwarts Report Card ⚡", title: "⚡ The Great Hogwarts Report Card ⚡",
scoreText: `You achieved <span class="gold-text">${score}</span> out of <span class="gold-text">${total}</span> House Points!`, scoreText: `You achieved <span class="gold-text">${score}</span> out of <span class="gold-text">${maxScore}</span> House Points!`,
timeText: `⏱️ Time needed: <strong class="gold-text">${formatSecondsEn(timeSec)}</strong>`, timeText: `⏱️ Time needed: <strong class="gold-text">${formatSecondsEn(timeSec)}</strong>`,
lbTitle: "🏆 Enter the Hogwarts Hall of Fame!", lbTitle: "🏆 Enter the Hogwarts Hall of Fame!",
lbSub: "Completely anonymous – choose your wizard nickname:", lbSub: "Completely anonymous – choose your wizard nickname:",
lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi", lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi",
diceLabel: "Random",
lbBtn: "📜 Enter Score", lbBtn: "📜 Enter Score",
lbBtnSending: "✨ Entering score...", lbBtnSending: "✨ Entering score...",
miniLbTitle: "Top Wizards:", miniLbTitle: "Top Wizards:",
@@ -225,10 +268,11 @@
}; };
function getTierForScore(s) { function getTierForScore(s) {
if (s >= 10) return TIERS_DATA[10]; const pct = (s <= 10 && total <= 10) ? (s / total) : (s / maxScore);
if (s >= 8) return TIERS_DATA[8]; if (pct >= 0.90) return TIERS_DATA[10];
if (s >= 5) return TIERS_DATA[5]; if (pct >= 0.70) return TIERS_DATA[8];
if (s >= 2) return TIERS_DATA[2]; if (pct >= 0.50) return TIERS_DATA[5];
if (pct >= 0.20) return TIERS_DATA[2];
return TIERS_DATA[0]; return TIERS_DATA[0];
} }
@@ -242,6 +286,7 @@
const lbTitle = document.getElementById("lbCardTitle"); const lbTitle = document.getElementById("lbCardTitle");
const lbSub = document.getElementById("lbCardSub"); const lbSub = document.getElementById("lbCardSub");
const nicknameInput = document.getElementById("nicknameInput"); const nicknameInput = document.getElementById("nicknameInput");
const diceLabel = document.getElementById("diceLabel");
const lbSubmitBtn = document.getElementById("lbSubmitBtn"); const lbSubmitBtn = document.getElementById("lbSubmitBtn");
const miniLbTitle = document.getElementById("miniLbTitle"); const miniLbTitle = document.getElementById("miniLbTitle");
const thNick = document.getElementById("thNick"); const thNick = document.getElementById("thNick");
@@ -256,6 +301,7 @@
if (timeText) timeText.innerHTML = t.timeText; if (timeText) timeText.innerHTML = t.timeText;
if (lbTitle) lbTitle.innerText = t.lbTitle; if (lbTitle) lbTitle.innerText = t.lbTitle;
if (lbSub) lbSub.innerText = t.lbSub; if (lbSub) lbSub.innerText = t.lbSub;
if (diceLabel) diceLabel.innerText = t.diceLabel;
if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder; if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder;
if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn; if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn;
if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle; if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle;
@@ -305,7 +351,7 @@
score: score, score: score,
total_questions: total, total_questions: total,
time_seconds: timeSec, time_seconds: timeSec,
difficulty: "all", difficulty: difficulty,
lang: currentLang lang: currentLang
}) })
}); });
@@ -341,7 +387,8 @@
async function loadMiniLeaderboard() { async function loadMiniLeaderboard() {
try { try {
const res = await fetch("/api/leaderboard?limit=5"); const url = "/api/leaderboard?limit=5" + (difficulty && difficulty !== "all" ? `&difficulty=${encodeURIComponent(difficulty)}` : "");
const res = await fetch(url);
const entries = await res.json(); const entries = await res.json();
const body = document.getElementById("miniLbBody"); const body = document.getElementById("miniLbBody");
const container = document.getElementById("miniLbContainer"); const container = document.getElementById("miniLbContainer");
@@ -354,7 +401,7 @@
tr.innerHTML = ` tr.innerHTML = `
<td><strong>${rankDisplay}</strong></td> <td><strong>${rankDisplay}</strong></td>
<td>${e.nickname}</td> <td>${e.nickname}</td>
<td><span class="gold-text">${e.score}/${e.total_questions}</span></td> <td><span class="gold-text">${e.score} Pkt.</span></td>
<td>${e.time_formatted}</td> <td>${e.time_formatted}</td>
`; `;
body.appendChild(tr); body.appendChild(tr);
@@ -368,6 +415,7 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
onLanguageChange(currentLang); onLanguageChange(currentLang);
loadMiniLeaderboard(); loadMiniLeaderboard();
rollRandomName();
}); });
</script> </script>
{% endblock %} {% endblock %}
+84 -41
View File
@@ -1,64 +1,107 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}✨ Magisches Harry-Potter-Escape-Quiz ✨{% endblock %} {% block title %}⚡ PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %}
{% block content %} {% block content %}
<div class="quiz-wrapper"> <div class="quiz-wrapper">
<header class="quiz-header"> <header class="quiz-header">
<h1 class="glow-title" id="pageTitle">✨ Magisches Hogwarts-Escape-Quiz ✨</h1> <div class="hero-pill" id="heroPill">⚡ {{ question_count }} Fragen im Wissens-Pool · Buch & Film</div>
<p class="subtitle" id="pageSubtitle">10 knifflige Rätsel aus der Zauberwelt warten auf dich</p> <h1 class="glow-title" id="pageTitle">Harry Potter Wissensquiz</h1>
<p class="subtitle" id="pageSubtitle">Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.</p>
</header> </header>
<!-- Brief-Intro / Startmaske --> <!-- Kompaktes Direct-Start Dashboard -->
<div id="introCard" class="card intro-card"> <div id="introCard" class="card start-dashboard-card">
<div class="intro-envelope" id="envelope"> <div class="dashboard-grid">
<div class="envelope-body"> <!-- Sektion 1: Schwierigkeitsgrad -->
<div class="envelope-flap"></div> <div class="config-section">
<div class="wax-seal">H</div> <div class="config-header">
<div class="letter-preview"> <span class="config-step">1</span>
<p class="letter-text" id="envelopeLetter">Hogwarts-Schule für Hexerei und Zauberei</p> <span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad</span>
</div> </div>
</div>
</div>
<div class="intro-content">
<p class="intro-text" id="introDesc">
Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>
Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?
</p>
<!-- Difficulty Selector Chips -->
<div class="difficulty-selection">
<span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad wählen:</span>
<div class="difficulty-chips" role="radiogroup" aria-label="Schwierigkeitsgrad"> <div class="difficulty-chips" role="radiogroup" aria-label="Schwierigkeitsgrad">
<button type="button" class="diff-chip active" data-diff="all" onclick="selectDifficulty('all')"> <button type="button" class="diff-chip active" data-diff="all" onclick="selectDifficulty('all')">
<span class="diff-icon">🎲</span> <span class="diff-text" id="diffAllText">Alle gemischt</span> <span class="diff-icon">🎲</span>
<span class="diff-text" id="diffAllText">Alle Stufen</span>
</button> </button>
<button type="button" class="diff-chip" data-diff="easy" onclick="selectDifficulty('easy')"> <button type="button" class="diff-chip" data-diff="easy" onclick="selectDifficulty('easy')">
<span class="diff-icon">🟢</span> <span class="diff-text" id="diffEasyText">Leicht</span> <span class="diff-icon">🟢</span>
<span class="diff-text" id="diffEasyText">Leicht</span>
</button> </button>
<button type="button" class="diff-chip" data-diff="medium" onclick="selectDifficulty('medium')"> <button type="button" class="diff-chip" data-diff="medium" onclick="selectDifficulty('medium')">
<span class="diff-icon">🟡</span> <span class="diff-text" id="diffMedText">Mittel</span> <span class="diff-icon">🟡</span>
<span class="diff-text" id="diffMedText">Mittel</span>
</button> </button>
<button type="button" class="diff-chip" data-diff="hard" onclick="selectDifficulty('hard')"> <button type="button" class="diff-chip" data-diff="hard" onclick="selectDifficulty('hard')">
<span class="diff-icon">🔴</span> <span class="diff-text" id="diffHardText">Schwer</span> <span class="diff-icon">🔴</span>
<span class="diff-text" id="diffHardText">Schwer (Experte)</span>
</button> </button>
</div> </div>
</div> </div>
<button id="startBtn" class="btn btn-primary btn-large" onclick="startGame()"> <!-- Sektion 2: Kategorie-Filter -->
📜 Siegel brechen & Brief öffnen <div class="config-section">
<div class="config-header">
<span class="config-step">2</span>
<span class="difficulty-label" id="catLabel">Themengebiet wählen</span>
</div>
<div class="category-chips" role="radiogroup" aria-label="Themengebiet">
<button type="button" class="cat-chip active" data-cat="all" onclick="selectCategory('all')">
<span>✨</span> <span id="catAllText">Alle Kategorien</span>
</button>
<button type="button" class="cat-chip" data-cat="books" onclick="selectCategory('books')">
<span>📖</span> <span id="catBooksText">Buchwissen</span>
</button>
<button type="button" class="cat-chip" data-cat="movies" onclick="selectCategory('movies')">
<span>🎬</span> <span id="catMoviesText">Filmwissen</span>
</button>
<button type="button" class="cat-chip" data-cat="creatures" onclick="selectCategory('creatures')">
<span>🐉</span> <span id="catCreaturesText">Magische Geschöpfe</span>
</button>
<button type="button" class="cat-chip" data-cat="spells" onclick="selectCategory('spells')">
<span>🪄</span> <span id="catSpellsText">Zaubersprüche & Tränke</span>
</button>
<button type="button" class="cat-chip" data-cat="characters" onclick="selectCategory('characters')">
<span>🧙‍♂️</span> <span id="catCharsText">Charaktere</span>
</button>
<button type="button" class="cat-chip" data-cat="hogwarts" onclick="selectCategory('hogwarts')">
<span>🏰</span> <span id="catHogwartsText">Hogwarts & Orte</span>
</button>
<button type="button" class="cat-chip" data-cat="quidditch" onclick="selectCategory('quidditch')">
<span>🧹</span> <span id="catQuidditchText">Quidditch</span>
</button>
<button type="button" class="cat-chip" data-cat="lore" onclick="selectCategory('lore')">
<span>🔮</span> <span id="catLoreText">Artefakte & Lore</span>
</button>
</div>
</div>
</div>
<div class="dashboard-footer">
<div class="dashboard-rules" id="dashboardRules">
<span>🎯 10 Fragen pro Runde</span>
<span>·</span>
<span>🔥 Streak-Zähler</span>
<span>·</span>
<span>💡 Bis zu 3 Versuche je Frage</span>
</div>
<button id="startQuizBtn" class="btn btn-primary btn-large btn-launch-quiz" onclick="startGame()">
⚡ Quiz jetzt starten ➔
</button> </button>
</div> </div>
</div> </div>
<!-- Spielbereich (initial versteckt) --> <!-- Spielbereich (initial versteckt) -->
<div id="gameCard" class="game-area hidden"> <div id="gameCard" class="game-area hidden">
<!-- Fortschritt & Punkte & Zeit --> <!-- HUD: Fortschritt, Streak, Zeit & Punkte -->
<div class="status-panel"> <div class="status-panel">
<div class="progress-info"> <div class="progress-info">
<span id="questionCounter">Frage 1 von 10</span> <span id="questionCounter" class="hud-pill">Frage 1 / 10</span>
<div class="hud-center-group">
<span id="streakBadge" class="streak-badge" title="Antwort-Serie in Folge">🔥 0</span>
<span id="timerBadge" class="timer-badge">⏱️ 00:00</span> <span id="timerBadge" class="timer-badge">⏱️ 00:00</span>
<span id="pointsCounter" class="points-badge">🏆 Hauspunkte: 0</span> </div>
<span id="pointsCounter" class="points-badge">🏆 0 Pkt.</span>
</div> </div>
<div class="progress-container"> <div class="progress-container">
<div id="progressBar" class="progress-bar"></div> <div id="progressBar" class="progress-bar"></div>
@@ -69,24 +112,24 @@
<div id="frageBox" class="card question-card"> <div id="frageBox" class="card question-card">
<div class="question-meta"> <div class="question-meta">
<div class="meta-left"> <div class="meta-left">
<span id="badgeCategory" class="badge">⚡ Harry Potter</span> <span id="badgeCategory" class="badge">⚡ Allgemein</span>
<span id="badgeDifficulty" class="badge badge-diff">🟢 Leicht</span> <span id="badgeDifficulty" class="badge badge-diff">🟢 Leicht</span>
</div> </div>
<span id="attemptBadge" class="attempt-badge">Versuch 1 von 3</span> <span id="attemptBadge" class="attempt-badge">Versuch 1 / 3</span>
</div> </div>
<h2 id="frageText" class="question-text">Lade verzauberte Schriftrollen...</h2> <h2 id="frageText" class="question-text">Lade nächste Frage...</h2>
<!-- Freitext-Eingabe (wenn question_type == 'text') --> <!-- Freitext-Eingabe (wenn question_type == 'text') -->
<div id="textAnswerGroup" class="answer-form"> <div id="textAnswerGroup" class="answer-form">
<input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort hier eingeben..." autocomplete="off"> <input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort eingeben..." autocomplete="off">
<div class="btn-group"> <div class="btn-group">
<button id="checkBtn" class="btn btn-primary" onclick="checkAnswer()"> <button id="checkBtn" class="btn btn-primary" onclick="checkAnswer()">
✨ Antwort prüfen ✓ Antwort prüfen
</button> </button>
<button id="hintBtn" class="btn btn-secondary" onclick="requestHint()"> <button id="hintBtn" class="btn btn-secondary" onclick="requestHint()">
💡 Hinweis anzeigen 💡 Tipp anzeigen
</button> </button>
</div> </div>
</div> </div>
@@ -94,15 +137,15 @@
<!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') --> <!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') -->
<div id="choiceAnswerGroup" class="choice-group hidden"> <div id="choiceAnswerGroup" class="choice-group hidden">
<div id="choiceOptionsContainer" class="choice-grid"></div> <div id="choiceOptionsContainer" class="choice-grid"></div>
<div class="btn-group" style="margin-top: 15px;"> <div class="btn-group" style="margin-top: 16px;">
<button id="hintBtnMC" class="btn btn-secondary" onclick="requestHint()"> <button id="hintBtnMC" class="btn btn-secondary" onclick="requestHint()">
💡 Hinweis anzeigen 💡 Tipp anzeigen
</button> </button>
</div> </div>
</div> </div>
<div id="hintBox" class="hint-box hidden"> <div id="hintBox" class="hint-box hidden">
<strong id="hintHeading">📜 Notiz aus der verbotenen Abteilung:</strong> <strong id="hintHeading">💡 Hinweis:</strong>
<p id="hintText"></p> <p id="hintText"></p>
</div> </div>
@@ -113,5 +156,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/quiz.js"></script> <script src="/static/js/quiz.js?v=4.0"></script>
{% endblock %} {% endblock %}
+19 -4
View File
@@ -5,11 +5,11 @@
{% block content %} {% block content %}
<div class="quiz-wrapper"> <div class="quiz-wrapper">
<div class="card vorschlag-card"> <div class="card vorschlag-card">
<h1 class="glow-title" id="suggTitle">✨ Neue Frage für das Quiz vorschlagen ✨</h1> <h1 class="glow-title" id="suggTitle">✍️ Neue Quiz-Frage einreichen</h1>
<p class="subtitle" id="suggSubtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p> <p class="subtitle" id="suggSubtitle">Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!</p>
<div class="info-alert" id="suggAlert"> <div class="info-alert" id="suggAlert">
ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status <em>„Prüfung ausstehend“</em>. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird. ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.
</div> </div>
<form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)"> <form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)">
@@ -23,6 +23,21 @@
<textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea> <textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea>
</div> </div>
<div class="form-group">
<label for="kategorie" id="suggCatLabel">Kategorie / Themenbereich:</label>
<select id="kategorie" name="category" class="input-text">
<option value="books" id="catBooks">📖 Buchwissen (Buch-Details)</option>
<option value="movies" id="catMovies">🎬 Filmwissen (Kino & Drehorte)</option>
<option value="creatures" id="catCreatures">🐉 Magische Geschöpfe</option>
<option value="spells" id="catSpells">🪄 Zaubersprüche & Tränke</option>
<option value="characters" id="catCharacters" selected>🧙‍♂️ Charaktere & Personen</option>
<option value="hogwarts" id="catHogwarts">🏰 Hogwarts & Orte</option>
<option value="quidditch" id="catQuidditch">🧹 Quidditch & Sport</option>
<option value="lore" id="catLore">🔮 Artefakte & Geschichte</option>
<option value="general" id="catGeneral">⚡ Allgemeines Wissen</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label> <label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label>
<select id="diff_select" name="difficulty" class="input-text"> <select id="diff_select" name="difficulty" class="input-text">
@@ -70,5 +85,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/vorschlag.js"></script> <script src="/static/js/vorschlag.js?v=2.3"></script>
{% endblock %} {% endblock %}
+29
View File
@@ -0,0 +1,29 @@
annotated-doc==0.0.5
annotated-types==0.8.0
anyio==4.15.1
cffi==2.1.1
click==8.5.0
cryptography==50.0.1
fastapi==0.141.1
greenlet==3.5.6
h11==0.16.0
httptools==0.8.0
idna==3.20
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
pycparser==3.0
pydantic==2.13.5
pydantic_core==2.46.5
PyMySQL==1.2.3
python-dotenv==1.2.3
python-multipart==0.0.32
PyYAML==6.0.3
SQLAlchemy==2.0.54
starlette==1.6.0
typing-inspection==0.4.4
typing_extensions==4.16.0
uvicorn==0.53.0
uvloop==0.22.1
watchfiles==1.3.0
websockets==17.1