Compare commits

..

15 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
kobold-ai 26ff0660d3 Feature: In-game timer, anonymous leaderboard, witty tiered finale results, legal pages, button renames 2026-09-21 13:23:29 +02:00
kobold-ai 16bffa3be8 Chore: Remove legacy PHP and flat JSON files 2026-09-21 13:11:56 +02:00
48 changed files with 3938 additions and 1613 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;
}
}
```
+302 -17
View File
@@ -12,15 +12,15 @@ from sqlalchemy.orm import Session
from dotenv import load_dotenv
from app.database import get_db
from app.models import Question, QuizSession, AdminUser
from app.models import Question, QuizSession, AdminUser, LeaderboardEntry
from app.matcher import is_answer_correct, levenshtein_distance, normalize_text
from app.auth import verify_password
from app.seed import seed_db
from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequest
from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequest, LeaderboardSubmitRequest
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")
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)
return {
"id": q.id,
"question_number": number,
"total_questions": total,
"category": q.category,
@@ -58,6 +59,86 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
"difficulty": q.difficulty or "medium"
}
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
else:
max_possible = (total * 100) if total <= 10 else total
pct = (score / max_possible) if max_possible > 0 else 0
is_de = (lang != "en")
if pct >= 0.90:
grade = "O"
title = "🧙‍♂️ Großmeister der Zauberei" if is_de else "🧙‍♂️ Grandmaster of Wizardry"
subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)"
desc = (
"Unglaublich! Albus Dumbledore überlegt bereits, dir das Büro des Schulleiters zu überlassen. "
"Du kennst selbst die geheimsten Winkel von Hogwarts besser als der Fast Kopflose Nick. "
"Das Zaubereiministerium verneigt sich in Ehrfurcht!"
if is_de else
"Incredible! Albus Dumbledore is already considering handing you the headmaster's office. "
"You know the secret passages of Hogwarts better than Nearly Headless Nick. "
"The Ministry of Magic bows in deep awe!"
)
badge_class = "tier-outstanding"
elif pct >= 0.70:
grade = "E"
title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard"
subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)"
desc = (
"Hermine Granger nickt anerkennend und Professor McGonagall vergibt 50 Punkte für dein Haus! "
"Dein Zauberstab zuckt blitzschnell und dein Wissen lässt selbst Ravenclaws erblassen."
if is_de else
"Hermione Granger nods in deep approval and Professor McGonagall awards 50 points to your House! "
"Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous."
)
badge_class = "tier-exceeds"
elif pct >= 0.50:
grade = "A"
title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student"
subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)"
desc = (
"Bestanden! Ein paar Kessel sind zwar im Zaubertränke-Unterricht explodiert und Peeves hat dich mit Kreide beworfen, "
"aber du hast dich tapfer durchgeschlagen. Eine solide magische Leistung!"
if is_de else
"Passed! A few cauldrons blew up during Potions class and Peeves threw chalk at your head, "
"but you bravely fought your way through. A solid magical performance!"
)
badge_class = "tier-acceptable"
elif pct >= 0.20:
grade = "M"
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)"
desc = (
"Au weia... Hat Ron Weasley dir seinen mit Tesafilm geklebten Zauberstab geliehen? "
"Deine Eule hat sich wohl im Nebel verflogen. Zeit für ein paar Extrastunden in der Bibliothek bei Madam Pince!"
if is_de else
"Yikes... Did Ron Weasley lend you his Spellotaped wand? "
"Your owl must have gotten lost in the fog. Time for extra study sessions in the library with Madam Pince!"
)
badge_class = "tier-poor"
else:
grade = "T"
title = "🧌 Bergtroll im Kerker" if is_de else "🧌 Mountain Troll in the Dungeon"
subtitle = "Note: T (Troll)" if is_de else "Grade: T (Troll)"
desc = (
"Alarm im Schloss! Du hast mit der Holzkeule auf die Fragen eingeschlagen. "
"Schnell weg, bevor Hausmeister Filch dich beim lebenslangen Nachsitzen in den Kerkern erwischt!"
if is_de else
"Troll in the dungeon! You smashed the questions with a wooden club. "
"Run quickly before caretaker Filch catches you for lifelong detention in the dungeons!"
)
badge_class = "tier-troll"
return {
"grade": grade,
"title": title,
"subtitle": subtitle,
"description": desc,
"badge_class": badge_class
}
@app.on_event("startup")
def startup_event():
seed_db()
@@ -73,26 +154,79 @@ async def index(request: Request, db: Session = Depends(get_db)):
context={"question_count": active_count}
)
VALID_CATEGORIES = ["all", "books", "movies", "creatures", "spells", "characters", "hogwarts", "quidditch", "lore", "general"]
@app.post("/api/game/start")
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"
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
# Fetch active questions matching difficulty (or all active)
query = db.query(Question).filter(Question.status == "active")
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:
# Fallback to all active if filtered pool empty
pool = db.query(Question).filter(Question.status == "active").all()
if not pool:
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
# Randomly sample 10 questions (or all if fewer)
sample_size = min(10, len(pool))
selected_questions = random.sample(pool, sample_size)
recent_set = set(payload.recent_ids or []) if payload else set()
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]
session_id = str(uuid.uuid4())
@@ -104,7 +238,8 @@ async def start_game(payload: GameStartRequest = None, db: Session = Depends(get
attempts_current=0,
is_finished=False,
lang=lang,
difficulty=difficulty
difficulty=difficulty,
created_at=datetime.datetime.utcnow()
)
db.add(new_session)
db.commit()
@@ -131,13 +266,19 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
question_ids = json.loads(session.question_ids)
if session.current_question_idx >= len(question_ids):
session.is_finished = True
session.finished_at = datetime.datetime.utcnow()
if session.created_at:
session.duration_seconds = max(1, int((session.finished_at - session.created_at).total_seconds()))
db.commit()
return {
"correct": False,
"score": session.score,
"points_earned": 0,
"question_id": None,
"attempts_used": session.attempts_current,
"attempts_left": 0,
"is_finished": True,
"duration_seconds": session.duration_seconds,
"next_question": None
}
@@ -149,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)
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.attempts_current = 0
@@ -157,6 +306,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if is_finished:
session.is_finished = True
session.finished_at = datetime.datetime.utcnow()
if session.created_at:
session.duration_seconds = max(1, int((session.finished_at - session.created_at).total_seconds()))
db.commit()
@@ -169,9 +320,12 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return {
"correct": True,
"score": session.score,
"points_earned": points_earned,
"question_id": current_q_id,
"attempts_used": 0,
"attempts_left": 3,
"is_finished": is_finished,
"duration_seconds": session.duration_seconds,
"revealed_answer": None,
"next_question": next_q_data
}
@@ -180,7 +334,6 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
attempts_left = max(0, 3 - session.attempts_current)
if attempts_left == 0:
# 3 failed attempts: move to next question, reveal answer
if session_lang == "en" and current_q.answer_en:
revealed = current_q.answer_en
else:
@@ -193,6 +346,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
if is_finished:
session.is_finished = True
session.finished_at = datetime.datetime.utcnow()
if session.created_at:
session.duration_seconds = max(1, int((session.finished_at - session.created_at).total_seconds()))
db.commit()
@@ -205,9 +360,12 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return {
"correct": False,
"score": session.score,
"points_earned": 0,
"question_id": current_q_id,
"attempts_used": 3,
"attempts_left": 0,
"is_finished": is_finished,
"duration_seconds": session.duration_seconds,
"revealed_answer": revealed,
"next_question": next_q_data
}
@@ -216,6 +374,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return {
"correct": False,
"score": session.score,
"points_earned": 0,
"question_id": current_q_id,
"attempts_used": session.attempts_current,
"attempts_left": attempts_left,
"is_finished": False,
@@ -247,13 +407,135 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
return {"hint": hint_text}
@app.get("/finale", response_class=HTMLResponse)
async def finale(request: Request, score: int = 0, total: int = 10, 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)
if time >= 60:
minutes = time // 60
secs = time % 60
time_str = f"{minutes} Min. {secs} Sek." if lang != "en" else f"{minutes} min. {secs} sec."
else:
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(
request=request,
name="finale.html",
context={"score": score, "total": total, "lang": lang}
context={
"score": score,
"total": total,
"max_score": max_score,
"time_seconds": time,
"time_formatted": time_str,
"lang": lang,
"difficulty": clean_diff,
"tier": tier
}
)
@app.get("/bestenliste", response_class=HTMLResponse)
async def bestenliste_page(request: Request, lang: str = "de"):
return templates.TemplateResponse(
request=request,
name="bestenliste.html",
context={"lang": lang}
)
@app.get("/impressum", response_class=HTMLResponse)
async def impressum_page(request: Request, lang: str = "de"):
return templates.TemplateResponse(
request=request,
name="impressum.html",
context={"lang": lang}
)
@app.get("/datenschutz", response_class=HTMLResponse)
async def datenschutz_page(request: Request, lang: str = "de"):
return templates.TemplateResponse(
request=request,
name="datenschutz.html",
context={"lang": lang}
)
@app.post("/api/leaderboard")
async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = Depends(get_db)):
nick = payload.nickname.strip()
if not nick:
wizard_adjectives = ["Mutiger", "Schlauer", "Weiser", "Flinke", "Listiger", "Fliegender"]
wizard_nouns = ["Greif", "Phönix", "Zauberer", "Drache", "Niffler", "Thestral"]
nick = f"{random.choice(wizard_adjectives)}_{random.choice(wizard_nouns)}_{random.randint(10, 99)}"
nick = nick[:30]
time_sec = max(1, payload.time_seconds)
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "all"
entry = LeaderboardEntry(
nickname=nick,
score=payload.score,
total_questions=payload.total_questions or 10,
time_seconds=time_sec,
difficulty=diff,
lang=payload.lang or "de",
session_id=payload.session_id,
created_at=datetime.datetime.utcnow()
)
db.add(entry)
db.commit()
db.refresh(entry)
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.time_seconds < entry.time_seconds)) |
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds == entry.time_seconds) & (LeaderboardEntry.id < entry.id))
).count()
rank = better_count + 1
return {
"success": True,
"id": entry.id,
"entry_id": entry.id,
"rank": rank,
"nickname": entry.nickname,
"score": entry.score,
"total_questions": entry.total_questions,
"time_seconds": entry.time_seconds
}
@app.get("/api/leaderboard")
async def get_leaderboard(difficulty: str = None, limit: int = 20, db: Session = Depends(get_db)):
query = db.query(LeaderboardEntry)
if difficulty and difficulty != "all":
query = query.filter(LeaderboardEntry.difficulty == difficulty)
entries = query.order_by(
LeaderboardEntry.score.desc(),
LeaderboardEntry.time_seconds.asc(),
LeaderboardEntry.created_at.asc()
).limit(min(100, limit)).all()
result = []
for idx, e in enumerate(entries, 1):
m = e.time_seconds // 60
s = e.time_seconds % 60
time_str = f"{m:02d}:{s:02d}"
result.append({
"rank": idx,
"id": e.id,
"nickname": e.nickname,
"score": e.score,
"total_questions": e.total_questions,
"time_seconds": e.time_seconds,
"time_formatted": time_str,
"difficulty": e.difficulty,
"date": e.created_at.strftime("%d.%m.%Y") if e.created_at else ""
})
return result
@app.get("/vorschlag", response_class=HTMLResponse)
async def vorschlag_page(request: Request):
return templates.TemplateResponse(
@@ -277,10 +559,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
opts.append(opt.strip())
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"
new_suggestion = Question(
category="community",
category=chosen_cat,
question=payload.frage.strip(),
canonical_answer=payload.antwort.strip(),
hint=payload.tipp.strip() if payload.tipp else None,
@@ -293,9 +576,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
)
db.add(new_suggestion)
db.commit()
db.refresh(new_suggestion)
return {
"success": True,
"id": new_suggestion.id,
"message": "Vielen Dank! Dein Vorschlag wurde eingereicht und wird von uns geprüft, bevor er im Quiz erscheint."
}
+74 -11
View File
@@ -33,11 +33,47 @@ def levenshtein_distance(s1: str, s2: str) -> int:
previous_row = current_row
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:
clean_user = normalize_text(user_input)
if not clean_user:
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)]
if 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:
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:
continue
if clean_user == target:
return True
# For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match
if len(target) <= 3:
continue
# For medium words (4-6 chars), allow max 1 typo
max_allowed = 1 if len(target) <= 6 else 2
dist = levenshtein_distance(clean_user, target)
if dist <= max_allowed:
return True
for u in user_variants:
if u == target:
return True
# For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match
if len(target) <= 3 or len(u) <= 3:
continue
# For medium words (4-6 chars), allow max 1 typo
max_allowed = 1 if len(target) <= 6 else 2
dist = levenshtein_distance(u, target)
if dist <= max_allowed:
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
+14
View File
@@ -41,9 +41,23 @@ class QuizSession(Base):
is_finished = Column(Boolean, default=False)
lang = Column(String(10), default="de") # 'de' or 'en'
difficulty = Column(String(20), default="all") # 'all', 'easy', 'medium', 'hard'
duration_seconds = Column(Integer, nullable=True)
created_at = Column(DateTime, default=datetime.datetime.utcnow)
finished_at = Column(DateTime, nullable=True)
class LeaderboardEntry(Base):
__tablename__ = "leaderboard_entries"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
nickname = Column(String(50), nullable=False, index=True)
score = Column(Integer, nullable=False, index=True)
total_questions = Column(Integer, default=10)
time_seconds = Column(Integer, nullable=False, index=True)
difficulty = Column(String(20), default="all", index=True)
lang = Column(String(10), default="de")
session_id = Column(String(36), nullable=True)
created_at = Column(DateTime, default=datetime.datetime.utcnow, index=True)
class AdminUser(Base):
__tablename__ = "admin_users"
+15
View File
@@ -4,6 +4,9 @@ from typing import Optional, List
class GameStartRequest(BaseModel):
lang: Optional[str] = "de"
difficulty: Optional[str] = "all"
category: Optional[str] = "all"
recent_ids: Optional[List[int]] = None
wrong_ids: Optional[List[int]] = None
class GameStartResponse(BaseModel):
session_id: str
@@ -24,6 +27,8 @@ class GameCheckRequest(BaseModel):
class GameCheckResponse(BaseModel):
correct: bool
score: int
points_earned: Optional[int] = 0
question_id: Optional[int] = None
attempts_used: int
attempts_left: int
max_attempts: int = 3
@@ -34,12 +39,22 @@ class GameCheckResponse(BaseModel):
class SuggestionCreateRequest(BaseModel):
frage: str
antwort: str
category: Optional[str] = "general"
tipp: Optional[str] = None
name: Optional[str] = "Gast"
question_type: Optional[str] = "text"
falsche_antworten: Optional[List[str]] = None
difficulty: Optional[str] = "medium"
class LeaderboardSubmitRequest(BaseModel):
nickname: str
score: int
total_questions: int = 10
time_seconds: int
difficulty: Optional[str] = "all"
lang: Optional[str] = "de"
session_id: Optional[str] = None
class QuestionAdminCreate(BaseModel):
category: str = "harry_potter"
question: str
+4
View File
@@ -4,6 +4,7 @@ from app.database import engine, SessionLocal, Base
from app.models import Question, AdminUser
from app.auth import hash_password
from app.seed_40 import seed_40_questions
from app.seed_lore_pack import seed_lore_pack
def seed_db():
Base.metadata.create_all(bind=engine)
@@ -29,6 +30,9 @@ def seed_db():
print("Question table is empty, seeding 40 initial bilingual questions...")
seed_40_questions()
# 3. Ensure Lore Pack (Books, Movies, Hard Creatures incl. Blast-Ended Skrewts) is present
seed_lore_pack()
finally:
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()
+754 -474
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

+285 -122
View File
@@ -2,73 +2,123 @@ let currentSessionId = null;
let currentQuestionNumber = 1;
let totalQuestions = 10;
let currentScore = 0;
let currentStreak = 0;
let attemptsUsed = 0;
const maxAttempts = 3;
let selectedDifficulty = "all";
let selectedCategory = "all";
let lastQuestionData = null;
let timerStartTime = null;
let timerInterval = null;
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 = {
de: {
pageTitle: "✨ Magisches Hogwarts-Escape-Quiz ✨",
pageSubtitle: "10 knifflige Rätsel aus der Zauberwelt warten auf dich",
envelopeLetter: "Hogwarts-Schule für Hexerei und Zauberei",
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?",
diffLabel: "Schwierigkeitsgrad wählen:",
diffAll: "Alle gemischt",
pageTitle: "Harry Potter Wissensquiz",
pageSubtitle: "Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.",
diffLabel: "Schwierigkeitsgrad",
diffAll: "Alle Stufen",
diffEasy: "Leicht",
diffMed: "Mittel",
diffHard: "Schwer",
startBtn: "📜 Siegel brechen & Brief öffnen",
startBtnCasting: "✨ Zauber wird gewirkt...",
pointsLabel: "🏆 Hauspunkte",
diffHard: "Schwer (Experte)",
catLabel: "Themengebiet wählen",
catAll: "Alle Kategorien",
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",
ofLabel: "von",
ofLabel: "/",
attemptLabel: "Versuch",
checkBtn: "✨ Zauber prüfen",
hintBtn: "💡 Zaubertipp",
hintHeading: "📜 Notiz aus der verbotenen Abteilung:",
placeholder: "Deine Antwort hier eingeben...",
checkBtn: "✓ Antwort prüfen",
hintBtn: "💡 Tipp anzeigen",
hintHeading: "💡 Hinweis:",
placeholder: "Deine Antwort eingeben...",
noHint: "Kein Tipp vorhanden.",
hintFailed: "Tipp konnte nicht geladen werden.",
correctFeedback: "✨ Richtig! 1 Hauspunkt für dein Haus! ✨",
wrongFeedback: "Leider falsch! Noch {attempts} Versuch(e).",
exhaustedWrong: "Leider nicht richtig (3 Fehlversuche).",
solutionLabel: "Richtige Lösung:",
wrongFeedback: "Nicht ganz! Noch {attempts} Versuch(e).",
exhaustedWrong: "Leider nicht gelöst (3 Fehlversuche).",
solutionLabel: "Richtige Antwort:",
startError: "Fehler beim Starten des Quiz: ",
networkError: "Netzwerkfehler: "
},
en: {
pageTitle: "✨ Magical Hogwarts Escape Quiz ✨",
pageSubtitle: "10 tricky wizarding world puzzles await you",
envelopeLetter: "Hogwarts School of Witchcraft and Wizardry",
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?",
diffLabel: "Choose difficulty level:",
diffAll: "All mixed",
pageTitle: "Harry Potter Trivia Quiz",
pageSubtitle: "Test your knowledge across books, movies, creatures, and spells — straight to the action.",
diffLabel: "Difficulty Level",
diffAll: "All Levels",
diffEasy: "Easy",
diffMed: "Medium",
diffHard: "Hard",
startBtn: "📜 Break Seal & Open Letter",
startBtnCasting: "✨ Casting spell...",
pointsLabel: "🏆 House Points",
diffHard: "Hard (Expert)",
catLabel: "Select Topic",
catAll: "All Categories",
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",
ofLabel: "of",
ofLabel: "/",
attemptLabel: "Attempt",
checkBtn: "✨ Check Spell",
hintBtn: "💡 Magic Hint",
hintHeading: "📜 Note from the Restricted Section:",
placeholder: "Type your answer here...",
checkBtn: "✓ Check Answer",
hintBtn: "💡 Show Hint",
hintHeading: "💡 Hint:",
placeholder: "Type your answer...",
noHint: "No hint available.",
hintFailed: "Could not load hint.",
correctFeedback: "✨ Correct! 1 House Point for your House! ✨",
wrongFeedback: "Incorrect! {attempts} attempt(s) remaining.",
exhaustedWrong: "Unfortunately incorrect (3 failed attempts).",
solutionLabel: "Correct solution:",
wrongFeedback: "Not quite! {attempts} attempt(s) left.",
exhaustedWrong: "Out of attempts (3 failed tries).",
solutionLabel: "Correct answer:",
startError: "Error starting quiz: ",
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)();
function playSound(type) {
@@ -76,33 +126,31 @@ function playSound(type) {
audioCtx.resume();
}
if (type === 'magic') {
// Magical bright arpeggio chime
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
notes.forEach((freq, index) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.15, audioCtx.currentTime + index * 0.08);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.08 + 0.35);
gain.gain.setValueAtTime(0.12, audioCtx.currentTime + index * 0.07);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.07 + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(audioCtx.currentTime + index * 0.08);
osc.stop(audioCtx.currentTime + index * 0.08 + 0.35);
osc.start(audioCtx.currentTime + index * 0.07);
osc.stop(audioCtx.currentTime + index * 0.07 + 0.3);
});
} else if (type === 'wrong') {
// Soft error buzz
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.25);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.25);
osc.type = 'triangle';
osc.frequency.setValueAtTime(180, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(95, audioCtx.currentTime + 0.22);
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.22);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.25);
osc.stop(audioCtx.currentTime + 0.22);
}
}
@@ -113,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) {
applyQuizTranslations(lang);
}
function applyQuizTranslations(lang) {
const t = UI_TEXT[lang] || UI_TEXT.de;
const pageTitle = document.getElementById("pageTitle");
const pageSubtitle = document.getElementById("pageSubtitle");
const envelopeLetter = document.getElementById("envelopeLetter");
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");
const setTxt = (id, val) => {
const el = document.getElementById(id);
if (el && val) el.innerText = val;
};
if (pageTitle) pageTitle.innerText = t.pageTitle;
if (pageSubtitle) pageSubtitle.innerText = t.pageSubtitle;
if (envelopeLetter) envelopeLetter.innerText = t.envelopeLetter;
if (introDesc) introDesc.innerHTML = t.introDesc;
if (diffLabel) diffLabel.innerText = t.diffLabel;
if (diffAllText) diffAllText.innerText = t.diffAll;
if (diffEasyText) diffEasyText.innerText = t.diffEasy;
if (diffMedText) diffMedText.innerText = t.diffMed;
if (diffHardText) diffHardText.innerText = t.diffHard;
if (startBtn && !startBtn.disabled) startBtn.innerText = t.startBtn;
if (checkBtn) checkBtn.innerText = t.checkBtn;
if (hintBtn) hintBtn.innerText = t.hintBtn;
if (hintBtnMC) hintBtnMC.innerText = t.hintBtn;
if (hintHeading) hintHeading.innerText = t.hintHeading;
setTxt("pageTitle", t.pageTitle);
setTxt("pageSubtitle", t.pageSubtitle);
setTxt("diffLabel", t.diffLabel);
setTxt("diffAllText", t.diffAll);
setTxt("diffEasyText", t.diffEasy);
setTxt("diffMedText", t.diffMed);
setTxt("diffHardText", t.diffHard);
setTxt("catLabel", t.catLabel);
setTxt("catAllText", t.catAll);
setTxt("catBooksText", t.catBooks);
setTxt("catMoviesText", t.catMovies);
setTxt("catCreaturesText", t.catCreatures);
setTxt("catSpellsText", t.catSpells);
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 (lastQuestionData) {
@@ -156,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() {
const envelope = document.getElementById("envelope");
const startBtn = document.getElementById("startBtn");
const startQuizBtn = document.getElementById("startQuizBtn");
const t = UI_TEXT[currentLang] || UI_TEXT.de;
startBtn.disabled = true;
startBtn.innerText = t.startBtnCasting;
envelope.classList.add("envelope-opened");
// Start background music if enabled or first click
const bgm = document.getElementById("bgm");
if (bgm && bgm.paused) {
bgm.play().then(() => {
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(() => {});
if (startQuizBtn) {
startQuizBtn.disabled = true;
startQuizBtn.innerText = t.startBtnCasting;
}
playSound('magic');
sessionQuestionIds = [];
currentStreak = 0;
updateStreakUI(false);
try {
const recentIds = getRecentQuestionIds();
const wrongIds = getWrongQuestionIds();
const res = await fetch("/api/game/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
lang: currentLang,
difficulty: selectedDifficulty
difficulty: selectedDifficulty,
category: selectedCategory,
recent_ids: recentIds,
wrong_ids: wrongIds
})
});
@@ -200,29 +321,56 @@ async function startGame() {
currentScore = data.score;
attemptsUsed = data.attempts_used;
setTimeout(() => {
document.getElementById("introCard").classList.add("hidden");
document.getElementById("gameCard").classList.remove("hidden");
renderQuestion(data);
}, 1100);
const introCard = document.getElementById("introCard");
if (introCard) {
introCard.classList.add("hidden");
}
document.getElementById("gameCard").classList.remove("hidden");
startTimer();
renderQuestion(data);
} catch (err) {
alert(t.startError + err.message);
startBtn.disabled = false;
startBtn.innerText = t.startBtn;
envelope.classList.remove("envelope-opened");
if (startQuizBtn) {
startQuizBtn.disabled = false;
startQuizBtn.innerText = t.startQuizBtn;
}
}
}
function startTimer() {
timerStartTime = Date.now();
elapsedSeconds = 0;
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(updateTimer, 1000);
updateTimer();
}
function updateTimer() {
if (!timerStartTime) return;
elapsedSeconds = Math.floor((Date.now() - timerStartTime) / 1000);
const m = Math.floor(elapsedSeconds / 60);
const s = elapsedSeconds % 60;
const badge = document.getElementById("timerBadge");
if (badge) {
badge.innerText = `⏱️ ${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
}
function renderQuestion(qData) {
lastQuestionData = qData;
if (qData.id && !sessionQuestionIds.includes(qData.id)) {
sessionQuestionIds.push(qData.id);
}
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("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
document.getElementById("badgeCategory").innerText = `⚡ ${qData.category.replace("_", " ").toUpperCase()}`;
document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`;
const rawCat = (qData.category || "general").toLowerCase();
document.getElementById("badgeCategory").innerText = catMap[rawCat] || `⚡ ${rawCat.toUpperCase()}`;
// Difficulty badge
const badgeDiff = document.getElementById("badgeDifficulty");
if (badgeDiff) {
const diff = qData.difficulty || "medium";
@@ -233,7 +381,7 @@ function renderQuestion(qData) {
diffText = t.diffEasy;
diffIcon = "🟢";
} else if (diff === "hard") {
diffText = t.diffHard;
diffText = currentLang === "en" ? "Hard" : "Schwer";
diffIcon = "🔴";
}
badgeDiff.innerText = `${diffIcon} ${diffText}`;
@@ -305,7 +453,6 @@ async function submitAnswer(answer, clickedBtn = null) {
if (checkBtn) checkBtn.disabled = true;
if (input) input.disabled = true;
// Disable all choice buttons
const choiceBtns = document.querySelectorAll(".choice-btn");
choiceBtns.forEach(b => b.disabled = true);
if (clickedBtn) {
@@ -328,20 +475,32 @@ async function submitAnswer(answer, clickedBtn = null) {
const result = await res.json();
currentScore = result.score;
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`;
const frageBox = document.getElementById("frageBox");
const feedbackBox = document.getElementById("feedbackBox");
if (result.correct) {
// Success!
playSound('magic');
currentStreak += 1;
updateStreakUI(true);
if (clickedBtn) {
clickedBtn.classList.remove("choice-selected");
clickedBtn.classList.add("choice-correct");
}
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(() => {
frageBox.classList.remove("sparkle-effect");
@@ -352,11 +511,13 @@ async function submitAnswer(answer, clickedBtn = null) {
attemptsUsed = 0;
renderQuestion(result.next_question);
}
}, 1200);
}, 1100);
} else {
// Incorrect
playSound('wrong');
currentStreak = 0;
updateStreakUI(false);
if (clickedBtn) {
clickedBtn.classList.remove("choice-selected");
clickedBtn.classList.add("choice-wrong");
@@ -368,7 +529,10 @@ async function submitAnswer(answer, clickedBtn = null) {
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
if (result.attempts_left === 0) {
// Exhausted all attempts
if (result.question_id) {
recordQuestionWrong(result.question_id);
}
feedbackBox.innerHTML = `
<div class="feedback-wrong">${t.exhaustedWrong}</div>
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
@@ -385,7 +549,6 @@ async function submitAnswer(answer, clickedBtn = null) {
}, 2200);
} else {
// Still attempts remaining
const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left);
feedbackBox.innerHTML = `<span class="feedback-wrong">${msg}</span>`;
if (checkBtn) checkBtn.disabled = false;
@@ -393,7 +556,6 @@ async function submitAnswer(answer, clickedBtn = null) {
input.disabled = false;
input.select();
}
// Re-enable choice buttons
choiceBtns.forEach(b => b.disabled = false);
}
}
@@ -424,11 +586,12 @@ async function requestHint() {
}
function finishGame() {
if (timerInterval) clearInterval(timerInterval);
saveRecentQuestions(sessionQuestionIds);
document.getElementById("progressBar").style.width = "100%";
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&lang=${currentLang}`;
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}&diff=${encodeURIComponent(selectedDifficulty)}`;
}
// Enter key to submit
document.addEventListener("DOMContentLoaded", () => {
const input = document.getElementById("answerInput");
if (input) {
+53 -8
View File
@@ -1,10 +1,20 @@
const SUGGESTION_TEXT = {
de: {
title: "✨ Neue Frage für das Quiz vorschlagen ✨",
subtitle: "Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!",
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.",
nameLabel: "Dein Name / Zauberer-Alias (optional):",
title: "✍️ Neue Quiz-Frage einreichen",
subtitle: "Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!",
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 / Alias (optional):",
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: *",
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
diffLabel: "Schwierigkeitsgrad:",
@@ -28,11 +38,21 @@ const SUGGESTION_TEXT = {
successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨"
},
en: {
title: "✨ Suggest a New Quiz Question ✨",
subtitle: "Enrich the magical quiz with tricky Harry Potter or escape puzzles!",
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.",
nameLabel: "Your Name / Wizard Alias (optional):",
title: "✍️ Submit a New Quiz Question",
subtitle: "Enrich PotterTrivia with your own questions about the books, movies, or magical creatures!",
alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are briefly reviewed for accuracy before going live in the public quiz pool.",
nameLabel: "Your Name / Alias (optional):",
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: *",
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
diffLabel: "Difficulty Level:",
@@ -75,11 +95,32 @@ function onLanguageChange(lang) {
const tippInput = document.getElementById("tipp");
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 (subtitle) subtitle.innerText = t.subtitle;
if (alertBox) alertBox.innerHTML = t.alert;
if (nameLabel) nameLabel.innerText = t.nameLabel;
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 (frageInput) frageInput.placeholder = t.fragePlaceholder;
if (diffLabel) diffLabel.innerText = t.diffLabel;
@@ -151,8 +192,12 @@ async function submitSuggestion(event) {
submitBtn.innerText = t.submitBtnCasting;
try {
const catElem = document.getElementById("kategorie");
const category = catElem ? catElem.value : "general";
const payload = {
name: name || "Gast",
category: category,
frage: frage,
antwort: antwort,
tipp: tipp || null,
+10 -4
View File
@@ -225,9 +225,15 @@
<div class="form-group">
<label for="category">Kategorie:</label>
<select name="category" id="category" class="input-text">
<option value="harry_potter" selected>Harry Potter / Escape</option>
<option value="insider">Persönlich / Insider</option>
<option value="general">Allgemeines Rätsel</option>
<option value="books">📖 Buchwissen</option>
<option value="movies">🎬 Filmwissen</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>
</div>
@@ -309,5 +315,5 @@
{% endblock %}
{% block scripts %}
<script src="/static/js/admin.js"></script>
<script src="/static/js/admin.js?v=2.3"></script>
{% endblock %}
+30 -63
View File
@@ -3,42 +3,39 @@
<head>
<meta charset="UTF-8">
<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="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 %}
</head>
<body>
<audio id="bgm" src="/static/audio/music.mp3" loop preload="auto"></audio>
<header class="site-header">
<div class="header-inner">
<a href="/" class="brand-link">
<span class="brand-sparkle">⚡</span>
<span class="brand-title">Hogwarts Quiz</span>
<span class="brand-badge">⚡</span>
<span class="brand-title">Potter<span class="brand-accent">Trivia</span></span>
</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="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="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button>
</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()">
<span id="menuIcon">☰</span>
</button>
</div>
<nav id="navLinks" class="nav-links">
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">✨ Zum Quiz</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>
</header>
@@ -47,13 +44,15 @@
</main>
<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">
<a href="/impressum" class="footer-link" id="footerImpressum">⚖️ Impressum</a>
<span class="footer-sep">·</span>
<a href="/datenschutz" class="footer-link" id="footerDatenschutz">🛡️ Datenschutz</a>
</div>
</footer>
<script>
const bgm = document.getElementById("bgm");
const audioIcon = document.getElementById("audioIcon");
const audioText = document.getElementById("audioText");
const navLinks = document.getElementById("navLinks");
const menuIcon = document.getElementById("menuIcon");
@@ -79,44 +78,24 @@
}
const navQuiz = document.getElementById("navQuiz");
const navLeaderboard = document.getElementById("navLeaderboard");
const navSuggest = document.getElementById("navSuggest");
const navAdmin = document.getElementById("navAdmin");
const footerText = document.getElementById("footerText");
const footerImpressum = document.getElementById("footerImpressum");
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 (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) {
footerText.innerText = isDe
? "⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡"
: "⚡ Magical Hogwarts Escape Quiz · Powered by Love & Wizardry ⚡";
}
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");
? "⚡ PotterTrivia · Das unabhängige Harry-Potter-Wissensquiz"
: "⚡ PotterTrivia · The Independent Wizarding World Trivia Quiz";
}
if (footerImpressum) footerImpressum.innerText = isDe ? "⚖️ Impressum" : "⚖️ Legal Notice";
if (footerDatenschutz) footerDatenschutz.innerText = isDe ? "🛡️ Datenschutz" : "🛡️ Privacy Policy";
}
function toggleMobileMenu() {
@@ -129,18 +108,6 @@
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", () => {
updateLanguageUI();
});
+205
View File
@@ -0,0 +1,205 @@
{% extends "base.html" %}
{% block title %}{% if lang == 'en' %}🏆 Hogwarts Hall of Fame – Leaderboard{% else %}🏆 Die Große Hogwarts-Ruhmeshalle – Bestenliste{% endif %}{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<div class="card leaderboard-card">
<div class="leaderboard-header" style="text-align: center; margin-bottom: 20px;">
<span class="sparkle-icon">🏆</span>
<h1 class="glow-title" id="lbPageTitle">
{% if lang == 'en' %}🏆 The Grand Hogwarts Hall of Fame 🏆{% else %}🏆 Die Große Hogwarts-Ruhmeshalle 🏆{% endif %}
</h1>
<p class="subtitle" id="lbPageSubtitle">
{% if lang == 'en' %}The most brilliant witches and wizards of all time{% else %}Die schlauesten Hexen und Zauberer aller Zeiten{% endif %}
</p>
</div>
<!-- Filter tabs for difficulty -->
<div class="diff-filter-tabs">
<button type="button" class="tab-btn active" data-diff="all" onclick="filterLeaderboard('all', this)" id="tabAll">
🎲 {% if lang == 'en' %}All Mixed{% else %}Alle gemischt{% endif %}
</button>
<button type="button" class="tab-btn" data-diff="easy" onclick="filterLeaderboard('easy', this)" id="tabEasy">
🟢 {% if lang == 'en' %}Easy{% else %}Leicht{% endif %}
</button>
<button type="button" class="tab-btn" data-diff="medium" onclick="filterLeaderboard('medium', this)" id="tabMed">
🟡 {% if lang == 'en' %}Medium{% else %}Mittel{% endif %}
</button>
<button type="button" class="tab-btn" data-diff="hard" onclick="filterLeaderboard('hard', this)" id="tabHard">
🔴 {% if lang == 'en' %}Hard{% else %}Schwer{% endif %}
</button>
</div>
<div class="table-responsive" style="margin-top: 20px;">
<table class="lb-table">
<thead>
<tr>
<th style="width: 50px;">#</th>
<th id="thWizard">Zauberer</th>
<th id="thScore">Punkte</th>
<th id="thDuration">Zeit</th>
<th id="thDiff">Schwierigkeit</th>
<th id="thDate">Datum</th>
</tr>
</thead>
<tbody id="leaderboardBody">
<tr>
<td colspan="6" style="text-align: center; padding: 25px; color: var(--text-muted);">
✨ Lade die Pergamente der Ruhmeshalle...
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn-group" style="margin-top: 30px;">
<a href="/" class="btn btn-primary" id="btnPlayNow">
✨ {% if lang == 'en' %}Play Now & Secure Rank{% else %}Jetzt spielen & Platz sichern{% endif %}
</a>
<a href="/vorschlag" class="btn btn-secondary" id="btnSuggestQ">
✍️ {% if lang == 'en' %}Submit Question{% else %}Frage einreichen{% endif %}
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentFilter = "all";
const LB_I18N = {
de: {
title: "🏆 Die Große Hogwarts-Ruhmeshalle 🏆",
sub: "Die schlauesten Hexen und Zauberer aller Zeiten",
tabAll: "🎲 Alle gemischt",
tabEasy: "🟢 Leicht",
tabMed: "🟡 Mittel",
tabHard: "🔴 Schwer",
thWizard: "Zauberer",
thScore: "Punkte",
thDuration: "Zeit",
thDiff: "Schwierigkeit",
thDate: "Datum",
btnPlay: "✨ Jetzt spielen & Platz sichern",
btnSuggest: "✍️ Frage einreichen",
empty: "Noch keine Einträge vorhanden. Sei der Erste, der sich verewigt!",
loading: "✨ Lade die Pergamente der Ruhmeshalle..."
},
en: {
title: "🏆 The Grand Hogwarts Hall of Fame 🏆",
sub: "The most brilliant witches and wizards of all time",
tabAll: "🎲 All Mixed",
tabEasy: "🟢 Easy",
tabMed: "🟡 Medium",
tabHard: "🔴 Hard",
thWizard: "Wizard",
thScore: "Points",
thDuration: "Time",
thDiff: "Difficulty",
thDate: "Date",
btnPlay: "✨ Play Now & Secure Rank",
btnSuggest: "✍️ Submit Question",
empty: "No entries yet. Be the first to enter the Hall of Fame!",
loading: "✨ Unrolling the scrolls of the Hall of Fame..."
}
};
function onLanguageChange(lang) {
const isDe = (lang !== "en");
const t = isDe ? LB_I18N.de : LB_I18N.en;
const title = document.getElementById("lbPageTitle");
const sub = document.getElementById("lbPageSubtitle");
const tabAll = document.getElementById("tabAll");
const tabEasy = document.getElementById("tabEasy");
const tabMed = document.getElementById("tabMed");
const tabHard = document.getElementById("tabHard");
const thWizard = document.getElementById("thWizard");
const thScore = document.getElementById("thScore");
const thDuration = document.getElementById("thDuration");
const thDiff = document.getElementById("thDiff");
const thDate = document.getElementById("thDate");
const btnPlay = document.getElementById("btnPlayNow");
const btnSuggest = document.getElementById("btnSuggestQ");
if (title) title.innerText = t.title;
if (sub) sub.innerText = t.sub;
if (tabAll) tabAll.innerText = t.tabAll;
if (tabEasy) tabEasy.innerText = t.tabEasy;
if (tabMed) tabMed.innerText = t.tabMed;
if (tabHard) tabHard.innerText = t.tabHard;
if (thWizard) thWizard.innerText = t.thWizard;
if (thScore) thScore.innerText = t.thScore;
if (thDuration) thDuration.innerText = t.thDuration;
if (thDiff) thDiff.innerText = t.thDiff;
if (thDate) thDate.innerText = t.thDate;
if (btnPlay) btnPlay.innerText = t.btnPlay;
if (btnSuggest) btnSuggest.innerText = t.btnSuggest;
loadLeaderboard(currentFilter);
}
function filterLeaderboard(diff, btn) {
currentFilter = diff;
document.querySelectorAll(".diff-filter-tabs .tab-btn").forEach(b => b.classList.remove("active"));
if (btn) btn.classList.add("active");
loadLeaderboard(diff);
}
async function loadLeaderboard(diff) {
const tbody = document.getElementById("leaderboardBody");
const isDe = (currentLang !== "en");
const t = isDe ? LB_I18N.de : LB_I18N.en;
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 25px; color: var(--text-muted);">${t.loading}</td></tr>`;
try {
const url = (diff && diff !== "all") ? `/api/leaderboard?difficulty=${diff}` : "/api/leaderboard";
const res = await fetch(url);
const entries = await res.json();
if (!entries || entries.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; padding: 30px; color: var(--text-muted);">${t.empty}</td></tr>`;
return;
}
tbody.innerHTML = "";
const medals = ["🥇", "🥈", "🥉"];
entries.forEach(e => {
const tr = document.createElement("tr");
const rankDisplay = medals[e.rank - 1] || `${e.rank}.`;
const diffBadgeClass = `badge-diff-${e.difficulty || 'medium'}`;
let diffLabel = e.difficulty;
if (e.difficulty === "easy") diffLabel = isDe ? "🟢 Leicht" : "🟢 Easy";
else if (e.difficulty === "medium") diffLabel = isDe ? "🟡 Mittel" : "🟡 Medium";
else if (e.difficulty === "hard") diffLabel = isDe ? "🔴 Schwer" : "🔴 Hard";
else diffLabel = isDe ? "🎲 Gemischt" : "🎲 Mixed";
tr.innerHTML = `
<td><strong>${rankDisplay}</strong></td>
<td><strong>${escapeHtml(e.nickname)}</strong></td>
<td><span class="gold-text font-weight-bold">${e.score} Pkt.</span></td>
<td>${e.time_formatted}</td>
<td><span class="badge badge-diff ${diffBadgeClass}">${diffLabel}</span></td>
<td class="text-muted" style="font-size: 0.85rem;">${e.date}</td>
`;
tbody.appendChild(tr);
});
} catch (err) {
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center; color: #ff7675; padding: 20px;">Fehler beim Laden: ${err.message}</td></tr>`;
}
}
function escapeHtml(str) {
if (!str) return "";
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
document.addEventListener("DOMContentLoaded", () => {
onLanguageChange(currentLang);
});
</script>
{% endblock %}
+254
View File
@@ -0,0 +1,254 @@
{% extends "base.html" %}
{% block title %}{% if lang == 'en' %}🛡️ Privacy Policy (Datenschutz){% else %}🛡️ Datenschutzerklärung{% endif %}{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<div class="card legal-card">
<h1 class="glow-title" id="dsTitle">
{% if lang == 'en' %}🛡️ Privacy Policy{% else %}🛡️ Datenschutzerklärung{% endif %}
</h1>
<p class="subtitle" id="dsSub">
{% if lang == 'en' %}Information pursuant to the EU General Data Protection Regulation (GDPR / DSGVO){% else %}Informationen zur Datenverarbeitung gemäß Art. 13 DSGVO{% endif %}
</p>
<div class="legal-content" id="dsContent">
{% if lang == 'en' %}
<h3>1. Responsible Controller</h3>
<p>
Responsible for data processing on <code>dev.waiswuaschd.de</code>:<br>
Maximilian R. (Waiswuaschd Project)<br>
Email: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">2. General Principles: Data Minimization & Privacy by Design</h3>
<p>
We treat your data with the utmost care. This portal is built with strict data minimization in mind:
<strong>No user accounts are required, no tracking tools (e.g. Google Analytics, Meta Pixel) are used, and no data is sold to third parties.</strong>
</p>
<h3 style="margin-top: 20px;">3. Server Log Files</h3>
<p>
When accessing our website, the web server automatically records standard access data (server logs):
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li>IP address of the requesting device</li>
<li>Date and time of access</li>
<li>Requested URL and HTTP status code</li>
<li>Browser type and version</li>
</ul>
<p style="margin-top: 8px;">
This data is processed pursuant to Art. 6(1)(f) GDPR to ensure technical system stability and IT security (e.g. DDoS protection). Log files are automatically deleted after standard server retention periods.
</p>
<h3 style="margin-top: 20px;">4. Cookies & Local Browser Storage</h3>
<p>
We use strictly necessary technical storage only:
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li><strong>Session Cookie (<code>quiz_session</code>):</strong> A temporary identifier required to coordinate your 10 quiz questions. It expires when you close your browser session.</li>
<li><strong>Browser LocalStorage:</strong> Used solely to remember your chosen language (<code>quiz_lang</code>) and background music setting (<code>bgm_enabled</code>) between pages.</li>
</ul>
<h3 style="margin-top: 20px;">5. Anonymous Leaderboard & Question Suggestions</h3>
<p>
Entering your name into the <em>Hogwarts Hall of Fame (Leaderboard)</em> or submitting a quiz question is completely voluntary.
We explicitly ask you <strong>not</strong> to provide your real name, but rather a playful wizard alias or nickname (e.g. <code>Golden_Snitch_99</code>).
</p>
<h3 style="margin-top: 20px;">6. Your Rights under the GDPR</h3>
<p>
Under EU GDPR, you have the right to information (Art. 15), correction (Art. 16), deletion (Art. 17), and restriction of processing (Art. 18).
For any data privacy inquiries, please contact us at <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>.
</p>
{% else %}
<h3>1. Verantwortliche Stelle</h3>
<p>
Verantwortlich für die Datenverarbeitung auf <code>dev.waiswuaschd.de</code>:<br>
Maximilian R. (Waiswuaschd Projekt)<br>
E-Mail: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">2. Grundsatz: Datensparsamkeit & Privatsphäre</h3>
<p>
Der Schutz deiner persönlichen Daten ist uns ein wichtiges Anliegen. Unser Quiz-Portal ist nach dem Prinzip der <strong>Datensparsamkeit</strong> konzipiert:
Es gibt <strong>keine Pflicht zur Registrierung, keine Tracking-Tools (wie Google Analytics oder Facebook-Pixel) und keine Weitergabe von Daten an Werbedienstleister</strong>.
</p>
<h3 style="margin-top: 20px;">3. Server-Logfiles</h3>
<p>
Beim Aufrufen unserer Webseiten erfasst der Webserver automatisch technische Informationen (Server-Logfiles):
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li>IP-Adresse des anfragenden Geräts</li>
<li>Datum und Uhrzeit des Zugriffs</li>
<li>Abgerufene URL und HTTP-Statuscode</li>
<li>Browsertyp und Betriebssystem</li>
</ul>
<p style="margin-top: 8px;">
Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO zur Gewährleistung der Systemsicherheit, Stabilität und Abwehr von Cyberangriffen. Logfiles werden nach Ablauf der regulären Server-Aufbewahrungsfrist automatisch gelöscht.
</p>
<h3 style="margin-top: 20px;">4. Cookies & Lokale Speicherung (LocalStorage)</h3>
<p>
Wir setzen ausschließlich technisch notwendige Hilfsmittel ein:
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li><strong>Session-Cookie (<code>quiz_session</code>):</strong> Wird benötigt, um deinen Spielstand und deine 10 Fragen während der laufenden Quizrunde zuzuordnen.</li>
<li><strong>Lokaler Browser-Speicher (LocalStorage):</strong> Speichert lediglich deine Spracheinstellung (<code>quiz_lang</code>) und Musikpräferenz (<code>bgm_enabled</code>), damit du diese beim nächsten Seitenaufruf nicht neu einstellen musst.</li>
</ul>
<h3 style="margin-top: 20px;">5. Anonyme Bestenliste & Frage-Vorschläge</h3>
<p>
Der Eintrag in die <em>Hogwarts-Ruhmeshalle (Bestenliste)</em> sowie das Einreichen eigener Quizfragen erfolgt auf rein freiwilliger Basis.
Wir bitten dich ausdrücklich darum, <strong>keine Klarnamen</strong> einzugeben, sondern ein magisches Pseudonym bzw. einen Alias (z. B. <code>Zauberlehrling_42</code>).
</p>
<h3 style="margin-top: 20px;">6. Deine Rechte gemäß DSGVO</h3>
<p>
Du hast nach der DSGVO jederzeit das Recht auf unentgeltliche Auskunft über deine gespeicherten Daten (Art. 15), Berichtigung (Art. 16) oder Löschung (Art. 17). Wende dich bei Fragen gerne direkt per E-Mail an uns.
</p>
{% endif %}
</div>
<div class="btn-group" style="margin-top: 30px;">
<a href="/" class="btn btn-primary" id="btnBackQuizDs">
✨ {% if lang == 'en' %}Back to Quiz{% else %}Zurück zum Quiz{% endif %}
</a>
<a href="/impressum" class="btn btn-secondary" id="btnToImp">
⚖️ {% if lang == 'en' %}Legal Notice{% else %}Zum Impressum{% endif %}
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const DS_TEXTS = {
de: {
title: "🛡️ Datenschutzerklärung",
sub: "Informationen zur Datenverarbeitung gemäß Art. 13 DSGVO",
btnBack: "✨ Zurück zum Quiz",
btnToImp: "⚖️ Zum Impressum",
html: `
<h3>1. Verantwortliche Stelle</h3>
<p>
Verantwortlich für die Datenverarbeitung auf <code>dev.waiswuaschd.de</code>:<br>
Maximilian R. (Waiswuaschd Projekt)<br>
E-Mail: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">2. Grundsatz: Datensparsamkeit & Privatsphäre</h3>
<p>
Der Schutz deiner persönlichen Daten ist uns ein wichtiges Anliegen. Unser Quiz-Portal ist nach dem Prinzip der <strong>Datensparsamkeit</strong> konzipiert:
Es gibt <strong>keine Pflicht zur Registrierung, keine Tracking-Tools (wie Google Analytics oder Facebook-Pixel) und keine Weitergabe von Daten an Werbedienstleister</strong>.
</p>
<h3 style="margin-top: 20px;">3. Server-Logfiles</h3>
<p>
Beim Aufrufen unserer Webseiten erfasst der Webserver automatisch technische Informationen (Server-Logfiles):
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li>IP-Adresse des anfragenden Geräts</li>
<li>Datum und Uhrzeit des Zugriffs</li>
<li>Abgerufene URL und HTTP-Statuscode</li>
<li>Browsertyp und Betriebssystem</li>
</ul>
<p style="margin-top: 8px;">
Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO zur Gewährleistung der Systemsicherheit, Stabilität und Abwehr von Cyberangriffen. Logfiles werden nach Ablauf der regulären Server-Aufbewahrungsfrist automatisch gelöscht.
</p>
<h3 style="margin-top: 20px;">4. Cookies & Lokale Speicherung (LocalStorage)</h3>
<p>
Wir setzen ausschließlich technisch notwendige Hilfsmittel ein:
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li><strong>Session-Cookie (<code>quiz_session</code>):</strong> Wird benötigt, um deinen Spielstand und deine 10 Fragen während der laufenden Quizrunde zuzuordnen.</li>
<li><strong>Lokaler Browser-Speicher (LocalStorage):</strong> Speichert lediglich deine Spracheinstellung (<code>quiz_lang</code>) und Musikpräferenz (<code>bgm_enabled</code>), damit du diese beim nächsten Seitenaufruf nicht neu einstellen musst.</li>
</ul>
<h3 style="margin-top: 20px;">5. Anonyme Bestenliste & Frage-Vorschläge</h3>
<p>
Der Eintrag in die <em>Hogwarts-Ruhmeshalle (Bestenliste)</em> sowie das Einreichen eigener Quizfragen erfolgt auf rein freiwilliger Basis.
Wir bitten dich ausdrücklich darum, <strong>keine Klarnamen</strong> einzugeben, sondern ein magisches Pseudonym bzw. einen Alias (z. B. <code>Zauberlehrling_42</code>).
</p>
<h3 style="margin-top: 20px;">6. Deine Rechte gemäß DSGVO</h3>
<p>
Du hast nach der DSGVO jederzeit das Recht auf unentgeltliche Auskunft über deine gespeicherten Daten (Art. 15), Berichtigung (Art. 16) oder Löschung (Art. 17). Wende dich bei Fragen gerne direkt per E-Mail an uns.
</p>
`
},
en: {
title: "🛡️ Privacy Policy",
sub: "Information pursuant to the EU General Data Protection Regulation (GDPR / DSGVO)",
btnBack: "✨ Back to Quiz",
btnToImp: "⚖️ Legal Notice",
html: `
<h3>1. Responsible Controller</h3>
<p>
Responsible for data processing on <code>dev.waiswuaschd.de</code>:<br>
Maximilian R. (Waiswuaschd Project)<br>
Email: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">2. General Principles: Data Minimization & Privacy by Design</h3>
<p>
We treat your data with the utmost care. This portal is built with strict data minimization in mind:
<strong>No user accounts are required, no tracking tools (e.g. Google Analytics, Meta Pixel) are used, and no data is sold to third parties.</strong>
</p>
<h3 style="margin-top: 20px;">3. Server Log Files</h3>
<p>
When accessing our website, the web server automatically records standard access data (server logs):
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li>IP address of the requesting device</li>
<li>Date and time of access</li>
<li>Requested URL and HTTP status code</li>
<li>Browser type and version</li>
</ul>
<p style="margin-top: 8px;">
This data is processed pursuant to Art. 6(1)(f) GDPR to ensure technical system stability and IT security (e.g. DDoS protection). Log files are automatically deleted after standard server retention periods.
</p>
<h3 style="margin-top: 20px;">4. Cookies & Local Browser Storage</h3>
<p>
We use strictly necessary technical storage only:
</p>
<ul style="margin-left: 20px; color: var(--text-muted);">
<li><strong>Session Cookie (<code>quiz_session</code>):</strong> A temporary identifier required to coordinate your 10 quiz questions. It expires when you close your browser session.</li>
<li><strong>Browser LocalStorage:</strong> Used solely to remember your chosen language (<code>quiz_lang</code>) and background music setting (<code>bgm_enabled</code>) between pages.</li>
</ul>
<h3 style="margin-top: 20px;">5. Anonymous Leaderboard & Question Suggestions</h3>
<p>
Entering your name into the <em>Hogwarts Hall of Fame (Leaderboard)</em> or submitting a quiz question is completely voluntary.
We explicitly ask you <strong>not</strong> to provide your real name, but rather a playful wizard alias or nickname (e.g. <code>Golden_Snitch_99</code>).
</p>
<h3 style="margin-top: 20px;">6. Your Rights under the GDPR</h3>
<p>
Under EU GDPR, you have the right to information (Art. 15), correction (Art. 16), deletion (Art. 17), and restriction of processing (Art. 18).
For any data privacy inquiries, please contact us at <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>.
</p>
`
}
};
function onLanguageChange(lang) {
const isDe = (lang !== "en");
const t = isDe ? DS_TEXTS.de : DS_TEXTS.en;
document.getElementById("dsTitle").innerText = t.title;
document.getElementById("dsSub").innerText = t.sub;
document.getElementById("dsContent").innerHTML = t.html;
document.getElementById("btnBackQuizDs").innerText = t.btnBack;
document.getElementById("btnToImp").innerText = t.btnToImp;
}
document.addEventListener("DOMContentLoaded", () => {
onLanguageChange(currentLang);
});
</script>
{% endblock %}
+371 -56
View File
@@ -1,58 +1,89 @@
{% extends "base.html" %}
{% block title %}{% if lang == 'en' %}🎉 The Grand Finale – Magical Quiz{% else %}🎉 Das große Finale – Magisches Quiz{% endif %}{% endblock %}
{% block title %}{% if lang == 'en' %}⚡ Hogwarts Report Card – Grand Finale{% else %}⚡ Das große Hogwarts-Zeugnis{% endif %}{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<div class="card finale-card">
<div class="finale-header">
<span class="sparkle-icon">✨</span>
<h1 class="glow-title" id="finaleTitle">🎉 Happy Birthday Schatz! 🎉</h1>
<h1 class="glow-title" id="finaleTitle">
{% if lang == 'en' %}⚡ The Great Hogwarts Report Card ⚡{% else %}⚡ Das große Hogwarts-Zeugnis ⚡{% endif %}
</h1>
<span class="sparkle-icon">✨</span>
</div>
<!-- Score & Time Stats -->
<div class="score-banner">
<p class="score-highlight" id="finaleScoreText">
{% if lang == 'en' %}
You secured <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ total }}</span> possible House Points for your House!
You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ max_score }}</span> House Points!
{% else %}
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> möglichen Hauspunkten für dein Haus gesichert!
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ max_score }}</span> Hauspunkten erzielt!
{% endif %}
</p>
<p class="time-highlight" id="finaleTimeText">
⏱️ {% if lang == 'en' %}Time needed:{% else %}Benötigte Zeit:{% endif %} <strong class="gold-text">{{ time_formatted }}</strong>
</p>
</div>
<div class="reward-box">
<h2 id="rewardTitle">
{% if lang == 'en' %}
💛 Your Magical Reward: An Unforgettable Evening at Home 💛
{% else %}
💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛
{% endif %}
</h2>
<p class="reward-description" id="rewardDesc">
{% if lang == 'en' %}
We'll order delicious food and make ourselves totally comfortable: relax, laugh, and enjoy quality time together.<br><br>
Plus, we'll play a brand new <strong>Online Escape Game</strong> of your choice!
{% else %}
Wir holen uns lecker Essen und machen es uns so richtig gemütlich: entspannen, lachen und die gemeinsame Zeit genießen.<br><br>
Dazu spielen wir ein brandneues <strong>Online Escape Game</strong> deiner Wahl!
{% endif %}
</p>
<p class="love-note" id="loveNote">
{% if lang == 'en' %}
I'm so excited for our wonderful evening together! 💫
{% else %}
Ich freue mich riesig auf unseren gemeinsamen Abend! 💫
{% endif %}
</p>
<!-- Tiered Wizard Evaluation -->
<div class="reward-box tier-box" id="tierBox">
<div class="tier-badge-container">
<span id="tierGrade" class="tier-grade-badge {{ tier.badge_class }}">{{ tier.grade }}</span>
</div>
<h2 id="tierTitle">{{ tier.title }}</h2>
<p class="tier-subtitle" id="tierSubtitle">{{ tier.subtitle }}</p>
<p class="reward-description" id="tierDesc">{{ tier.description }}</p>
</div>
<!-- Leaderboard Submission Card -->
<div class="card leaderboard-entry-card" id="lbFormCard">
<h3 id="lbCardTitle">🏆 Trage dich in die Ruhmeshalle ein!</h3>
<p class="subtitle" id="lbCardSub">Völlig anonym – wähle deinen Zauberer-Spitznamen:</p>
<form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;">
<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">
<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">
📜 Verewigen
</button>
</div>
</form>
<div id="lbFeedback" class="feedback-box hidden" style="margin-top: 10px;"></div>
<!-- Mini Leaderboard Table -->
<div id="miniLbContainer" class="mini-lb-table-wrapper hidden" style="margin-top: 15px;">
<h4 id="miniLbTitle" style="color: var(--gold); margin-bottom: 8px;">Top Zauberer heute:</h4>
<div class="table-responsive">
<table class="lb-table">
<thead>
<tr>
<th>#</th>
<th id="thNick">Zauberer</th>
<th id="thPoints">Punkte</th>
<th id="thTime">Zeit</th>
</tr>
</thead>
<tbody id="miniLbBody"></tbody>
</table>
</div>
</div>
</div>
<div class="btn-group finale-actions">
<a href="/" class="btn btn-primary" id="playAgainBtn">
{% if lang == 'en' %}🔄 Play Again{% else %}🔄 Erneut spielen{% endif %}
{% if lang == 'en' %}🔄 Play Again{% else %}🔄 Noch einmal spielen{% endif %}
</a>
<a href="/bestenliste" class="btn btn-secondary" id="hallOfFameBtn">
{% if lang == 'en' %}🏆 Hall of Fame{% else %}🏆 Zur Bestenliste{% endif %}
</a>
<a href="/vorschlag" class="btn btn-secondary" id="suggestBtn">
{% if lang == 'en' %}✍️ Suggest Your Own Question{% else %}✍️ Eigene Frage vorschlagen{% endif %}
{% if lang == 'en' %}✍️ Submit Question{% else %}✍️ Frage vorschlagen{% endif %}
</a>
</div>
</div>
@@ -61,46 +92,330 @@
{% block scripts %}
<script>
const finaleScore = {{ score }};
const finaleTotal = {{ total }};
const score = {{ score }};
const total = {{ total }};
const maxScore = {{ max_score }};
const timeSec = {{ time_seconds }};
const difficulty = "{{ difficulty }}";
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 = {
de: {
title: "⚡ Das große Hogwarts-Zeugnis ⚡",
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>`,
lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!",
lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:",
lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi",
diceLabel: "Zufall",
lbBtn: "📜 Verewigen",
lbBtnSending: "✨ Wird verewigt...",
miniLbTitle: "Top Zauberer:",
thNick: "Zauberer",
thPoints: "Punkte",
thTime: "Zeit",
playAgain: "🔄 Noch einmal spielen",
hallOfFame: "🏆 Zur Bestenliste",
suggest: "✍️ Frage vorschlagen",
rankSuccess: (rank) => `✨ Glückwunsch! Du stehst auf **Platz #${rank}** der Ruhmeshalle!`
},
en: {
title: "⚡ The Great Hogwarts Report Card ⚡",
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>`,
lbTitle: "🏆 Enter the Hogwarts Hall of Fame!",
lbSub: "Completely anonymous – choose your wizard nickname:",
lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi",
diceLabel: "Random",
lbBtn: "📜 Enter Score",
lbBtnSending: "✨ Entering score...",
miniLbTitle: "Top Wizards:",
thNick: "Wizard",
thPoints: "Points",
thTime: "Time",
playAgain: "🔄 Play Again",
hallOfFame: "🏆 Hall of Fame",
suggest: "✍️ Submit Question",
rankSuccess: (rank) => `✨ Congratulations! You are **Rank #${rank}** in the Hall of Fame!`
}
};
function formatSecondsDe(sec) {
if (sec >= 60) {
const m = Math.floor(sec / 60);
const s = sec % 60;
return `${m} Min. ${s} Sek.`;
}
return `${sec} Sek.`;
}
function formatSecondsEn(sec) {
if (sec >= 60) {
const m = Math.floor(sec / 60);
const s = sec % 60;
return `${m} min. ${s} sec.`;
}
return `${sec} sec.`;
}
const TIERS_DATA = {
10: {
grade: "O",
badge_class: "tier-outstanding",
de: {
title: "🧙‍♂️ Großmeister der Zauberei",
sub: "Note: O (Ohnegleichen)",
desc: "Unglaublich! Albus Dumbledore überlegt bereits, dir das Büro des Schulleiters zu überlassen. Du kennst selbst die geheimsten Winkel von Hogwarts besser als der Fast Kopflose Nick. Das Zaubereiministerium verneigt sich in Ehrfurcht!"
},
en: {
title: "🧙‍♂️ Grandmaster of Wizardry",
sub: "Grade: O (Outstanding)",
desc: "Incredible! Albus Dumbledore is already considering handing you the headmaster's office. You know the secret passages of Hogwarts better than Nearly Headless Nick. The Ministry of Magic bows in deep awe!"
}
},
8: {
grade: "E",
badge_class: "tier-exceeds",
de: {
title: "⚡ Meister-Hexe / Zauberer",
sub: "Note: E (Erwartungen übertroffen)",
desc: "Hermine Granger nickt anerkennend und Professor McGonagall vergibt 50 Punkte für dein Haus! Dein Zauberstab zuckt blitzschnell und dein Wissen lässt selbst Ravenclaws erblassen."
},
en: {
title: "⚡ Master Witch / Wizard",
sub: "Grade: E (Exceeds Expectations)",
desc: "Hermione Granger nods in deep approval and Professor McGonagall awards 50 points to your House! Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous."
}
},
5: {
grade: "A",
badge_class: "tier-acceptable",
de: {
title: "📜 Solider Hogwarts-Schüler",
sub: "Note: A (Annehmbar)",
desc: "Bestanden! Ein paar Kessel sind zwar im Zaubertränke-Unterricht explodiert und Peeves hat dich mit Kreide beworfen, aber du hast dich tapfer durchgeschlagen. Eine solide magische Leistung!"
},
en: {
title: "📜 Solid Hogwarts Student",
sub: "Grade: A (Acceptable)",
desc: "Passed! A few cauldrons blew up during Potions class and Peeves threw chalk at your head, but you bravely fought your way through. A solid magical performance!"
}
},
2: {
grade: "M",
badge_class: "tier-poor",
de: {
title: "🧹 Quaffel-Fänger in Not",
sub: "Note: M (Mies)",
desc: "Au weia... Hat Ron Weasley dir seinen mit Tesafilm geklebten Zauberstab geliehen? Deine Eule hat sich wohl im Nebel verflogen. Zeit für ein paar Extrastunden in der Bibliothek bei Madam Pince!"
},
en: {
title: "🧹 Quaffle Catcher in Distress",
sub: "Grade: P (Poor)",
desc: "Yikes... Did Ron Weasley lend you his Spellotaped wand? Your owl must have gotten lost in the fog. Time for extra study sessions in the library with Madam Pince!"
}
},
0: {
grade: "T",
badge_class: "tier-troll",
de: {
title: "🧌 Bergtroll im Kerker",
sub: "Note: T (Troll)",
desc: "Alarm im Schloss! Du hast mit der Holzkeule auf die Fragen eingeschlagen. Schnell weg, bevor Hausmeister Filch dich beim lebenslangen Nachsitzen in den Kerkern erwischt!"
},
en: {
title: "🧌 Mountain Troll in the Dungeon",
sub: "Grade: T (Troll)",
desc: "Troll in the dungeon! You smashed the questions with a wooden club. Run quickly before caretaker Filch catches you for lifelong detention in the dungeons!"
}
}
};
function getTierForScore(s) {
const pct = (s <= 10 && total <= 10) ? (s / total) : (s / maxScore);
if (pct >= 0.90) return TIERS_DATA[10];
if (pct >= 0.70) return TIERS_DATA[8];
if (pct >= 0.50) return TIERS_DATA[5];
if (pct >= 0.20) return TIERS_DATA[2];
return TIERS_DATA[0];
}
function onLanguageChange(lang) {
const isDe = lang === "de";
const isDe = (lang !== "en");
const t = isDe ? FINALE_I18N.de : FINALE_I18N.en;
const title = document.getElementById("finaleTitle");
const scoreText = document.getElementById("finaleScoreText");
const rewardTitle = document.getElementById("rewardTitle");
const rewardDesc = document.getElementById("rewardDesc");
const loveNote = document.getElementById("loveNote");
const timeText = document.getElementById("finaleTimeText");
const lbTitle = document.getElementById("lbCardTitle");
const lbSub = document.getElementById("lbCardSub");
const nicknameInput = document.getElementById("nicknameInput");
const diceLabel = document.getElementById("diceLabel");
const lbSubmitBtn = document.getElementById("lbSubmitBtn");
const miniLbTitle = document.getElementById("miniLbTitle");
const thNick = document.getElementById("thNick");
const thPoints = document.getElementById("thPoints");
const thTime = document.getElementById("thTime");
const playAgain = document.getElementById("playAgainBtn");
const hallOfFame = document.getElementById("hallOfFameBtn");
const suggest = document.getElementById("suggestBtn");
if (title) title.innerText = isDe ? "🎉 Happy Birthday Schatz! 🎉" : "🎉 Happy Birthday Darling! 🎉";
if (scoreText) {
scoreText.innerHTML = isDe
? `Du hast <span class="gold-text">${finaleScore}</span> von <span class="gold-text">${finaleTotal}</span> möglichen Hauspunkten für dein Haus gesichert!`
: `You secured <span class="gold-text">${finaleScore}</span> out of <span class="gold-text">${finaleTotal}</span> possible House Points for your House!`;
if (title) title.innerText = t.title;
if (scoreText) scoreText.innerHTML = t.scoreText;
if (timeText) timeText.innerHTML = t.timeText;
if (lbTitle) lbTitle.innerText = t.lbTitle;
if (lbSub) lbSub.innerText = t.lbSub;
if (diceLabel) diceLabel.innerText = t.diceLabel;
if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder;
if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn;
if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle;
if (thNick) thNick.innerText = t.thNick;
if (thPoints) thPoints.innerText = t.thPoints;
if (thTime) thTime.innerText = t.thTime;
if (playAgain) playAgain.innerText = t.playAgain;
if (hallOfFame) hallOfFame.innerText = t.hallOfFame;
if (suggest) suggest.innerText = t.suggest;
// Tier evaluation
const tier = getTierForScore(score);
const tierContent = isDe ? tier.de : tier.en;
const tierGrade = document.getElementById("tierGrade");
const tierTitle = document.getElementById("tierTitle");
const tierSubtitle = document.getElementById("tierSubtitle");
const tierDesc = document.getElementById("tierDesc");
if (tierGrade) {
tierGrade.className = `tier-grade-badge ${tier.badge_class}`;
tierGrade.innerText = tier.grade;
}
if (rewardTitle) {
rewardTitle.innerText = isDe
? "💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛"
: "💛 Your Magical Reward: An Unforgettable Evening at Home 💛";
if (tierTitle) tierTitle.innerText = tierContent.title;
if (tierSubtitle) tierSubtitle.innerText = tierContent.sub;
if (tierDesc) tierDesc.innerText = tierContent.desc;
}
async function submitScoreToLeaderboard(event) {
event.preventDefault();
if (scoreAlreadySubmitted) return;
const isDe = (currentLang !== "en");
const t = isDe ? FINALE_I18N.de : FINALE_I18N.en;
const nickname = document.getElementById("nicknameInput").value.trim();
const submitBtn = document.getElementById("lbSubmitBtn");
const feedback = document.getElementById("lbFeedback");
submitBtn.disabled = true;
submitBtn.innerText = t.lbBtnSending;
try {
const res = await fetch("/api/leaderboard", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nickname: nickname,
score: score,
total_questions: total,
time_seconds: timeSec,
difficulty: difficulty,
lang: currentLang
})
});
const data = await res.json();
if (res.ok && data.success) {
scoreAlreadySubmitted = true;
feedback.className = "feedback-box feedback-success";
feedback.innerHTML = `
<div style="background: rgba(46, 204, 113, 0.2); border: 1px solid #2ecc71; color: #55efc4; padding: 12px; border-radius: 8px;">
✨ <strong>${data.nickname}</strong>: ${t.rankSuccess(data.rank)}
</div>
`;
feedback.classList.remove("hidden");
document.getElementById("nicknameInput").disabled = true;
submitBtn.innerText = isDe ? "✅ Eingetragen" : "✅ Entered";
loadMiniLeaderboard();
} else {
feedback.className = "feedback-box alert alert-danger";
feedback.innerText = data.detail || (isDe ? "Fehler beim Eintragen." : "Error entering score.");
feedback.classList.remove("hidden");
submitBtn.disabled = false;
submitBtn.innerText = t.lbBtn;
}
} catch (err) {
feedback.className = "feedback-box alert alert-danger";
feedback.innerText = "Netzwerkfehler: " + err.message;
feedback.classList.remove("hidden");
submitBtn.disabled = false;
submitBtn.innerText = t.lbBtn;
}
if (rewardDesc) {
rewardDesc.innerHTML = isDe
? "Wir holen uns lecker Essen und machen es uns so richtig gemütlich: entspannen, lachen und die gemeinsame Zeit genießen.<br><br>Dazu spielen wir ein brandneues <strong>Online Escape Game</strong> deiner Wahl!"
: "We'll order delicious food and make ourselves totally comfortable: relax, laugh, and enjoy quality time together.<br><br>Plus, we'll play a brand new <strong>Online Escape Game</strong> of your choice!";
}
async function loadMiniLeaderboard() {
try {
const url = "/api/leaderboard?limit=5" + (difficulty && difficulty !== "all" ? `&difficulty=${encodeURIComponent(difficulty)}` : "");
const res = await fetch(url);
const entries = await res.json();
const body = document.getElementById("miniLbBody");
const container = document.getElementById("miniLbContainer");
body.innerHTML = "";
const medals = ["🥇", "🥈", "🥉"];
entries.forEach(e => {
const tr = document.createElement("tr");
const rankDisplay = medals[e.rank - 1] || `${e.rank}.`;
tr.innerHTML = `
<td><strong>${rankDisplay}</strong></td>
<td>${e.nickname}</td>
<td><span class="gold-text">${e.score} Pkt.</span></td>
<td>${e.time_formatted}</td>
`;
body.appendChild(tr);
});
container.classList.remove("hidden");
} catch (err) {
console.error("Could not load mini leaderboard", err);
}
if (loveNote) {
loveNote.innerText = isDe
? "Ich freue mich riesig auf unseren gemeinsamen Abend! 💫"
: "I'm so excited for our wonderful evening together! 💫";
}
if (playAgain) playAgain.innerText = isDe ? "🔄 Erneut spielen" : "🔄 Play Again";
if (suggest) suggest.innerText = isDe ? "✍️ Eigene Frage vorschlagen" : "✍️ Suggest Your Own Question";
}
document.addEventListener("DOMContentLoaded", () => {
onLanguageChange(currentLang);
loadMiniLeaderboard();
rollRandomName();
});
</script>
{% endblock %}
+164
View File
@@ -0,0 +1,164 @@
{% extends "base.html" %}
{% block title %}{% if lang == 'en' %}⚖️ Legal Notice (Impressum){% else %}⚖️ Impressum{% endif %}{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<div class="card legal-card">
<h1 class="glow-title" id="impTitle">
{% if lang == 'en' %}⚖️ Legal Notice (Impressum){% else %}⚖️ Impressum{% endif %}
</h1>
<p class="subtitle" id="impSub">
{% if lang == 'en' %}Information according to § 5 DDG{% else %}Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG){% endif %}
</p>
<div class="legal-content" id="impContent">
{% if lang == 'en' %}
<h3>Website Operator & Contact</h3>
<p>
<strong>Magisches Hogwarts Escape Quiz</strong><br>
A private, non-commercial web project on <code>dev.waiswuaschd.de</code>.<br>
Operator: Maximilian R. (Waiswuaschd Project)<br>
Contact: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">Non-Commercial Fan Project Disclaimer</h3>
<p>
This website is a purely non-commercial, private fan project created for educational, puzzle, and entertainment purposes.
No commercial profit or advertising revenue is generated through this service.
</p>
<h3 style="margin-top: 20px;">Copyright & Trademarks</h3>
<p>
<strong>Harry Potter</strong> characters, names, and related indicia are trademarks of and © Warner Bros. Entertainment Inc. and J.K. Rowling.
Harry Potter publishing rights © Bloomsbury Publishing Plc and Carlsen Verlag.
All rights belong to their respective copyright and trademark owners. No infringement is intended.
</p>
<h3 style="margin-top: 20px;">Liability for Content & External Links</h3>
<p>
As a service provider, we are responsible for our own content on these pages under general laws. We make every effort to keep information up to date and accurate, but cannot accept liability for external links to third-party websites over whose content we have no control.
</p>
{% else %}
<h3>Diensteanbieter & Kontakt</h3>
<p>
<strong>Magisches Hogwarts-Escape-Quiz</strong><br>
Ein rein privates, nicht-kommerzielles Projekt auf <code>dev.waiswuaschd.de</code>.<br>
Betreiber: Maximilian R. (Waiswuaschd Projekt)<br>
E-Mail-Kontakt: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">Nicht-kommerzielles Fan-Projekt</h3>
<p>
Diese Webseite ist ein reines Fan-Projekt, das unentgeltlich und aus Freude an Zauberrätseln und Harry Potter betrieben wird. Es werden keinerlei kommerzielle Interessen verfolgt, keine Werbung geschaltet und keine Einnahmen erzielt.
</p>
<h3 style="margin-top: 20px;">Urheberrecht & Markenzeichen</h3>
<p>
<strong>Harry Potter</strong>, Charaktere, Namen und zugehörige Kennzeichen sind Marken von und © Warner Bros. Entertainment Inc. und J.K. Rowling.<br>
Die Verlagsrechte liegen bei Bloomsbury Publishing Plc sowie Carlsen Verlag GmbH. Alle Rechte verbleiben bei den jeweiligen Rechteinhabern. Es besteht keinerlei Absicht zur Verletzung von Marken- oder Urheberrechten.
</p>
<h3 style="margin-top: 20px;">Haftung für Inhalte und Links</h3>
<p>
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Verlinkungen zu externen Webseiten Dritter unterliegen der Haftung der jeweiligen Betreiber.
</p>
{% endif %}
</div>
<div class="btn-group" style="margin-top: 30px;">
<a href="/" class="btn btn-primary" id="btnBackQuiz">
✨ {% if lang == 'en' %}Back to Quiz{% else %}Zurück zum Quiz{% endif %}
</a>
<a href="/datenschutz" class="btn btn-secondary" id="btnToPrivacy">
🛡️ {% if lang == 'en' %}Privacy Policy{% else %}Zur Datenschutzerklärung{% endif %}
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const IMP_TEXTS = {
de: {
title: "⚖️ Impressum",
sub: "Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG)",
btnBack: "✨ Zurück zum Quiz",
btnToPrivacy: "🛡️ Zur Datenschutzerklärung",
html: `
<h3>Diensteanbieter & Kontakt</h3>
<p>
<strong>Magisches Hogwarts-Escape-Quiz</strong><br>
Ein rein privates, nicht-kommerzielles Projekt auf <code>dev.waiswuaschd.de</code>.<br>
Betreiber: Maximilian R. (Waiswuaschd Projekt)<br>
E-Mail-Kontakt: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">Nicht-kommerzielles Fan-Projekt</h3>
<p>
Diese Webseite ist ein reines Fan-Projekt, das unentgeltlich und aus Freude an Zauberrätseln und Harry Potter betrieben wird. Es werden keinerlei kommerzielle Interessen verfolgt, keine Werbung geschaltet und keine Einnahmen erzielt.
</p>
<h3 style="margin-top: 20px;">Urheberrecht & Markenzeichen</h3>
<p>
<strong>Harry Potter</strong>, Charaktere, Namen und zugehörige Kennzeichen sind Marken von und © Warner Bros. Entertainment Inc. und J.K. Rowling.<br>
Die Verlagsrechte liegen bei Bloomsbury Publishing Plc sowie Carlsen Verlag GmbH. Alle Rechte verbleiben bei den jeweiligen Rechteinhabern. Es besteht keinerlei Absicht zur Verletzung von Marken- oder Urheberrechten.
</p>
<h3 style="margin-top: 20px;">Haftung für Inhalte und Links</h3>
<p>
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Verlinkungen zu externen Webseiten Dritter unterliegen der Haftung der jeweiligen Betreiber.
</p>
`
},
en: {
title: "⚖️ Legal Notice (Impressum)",
sub: "Information according to § 5 DDG",
btnBack: "✨ Back to Quiz",
btnToPrivacy: "🛡️ Privacy Policy",
html: `
<h3>Website Operator & Contact</h3>
<p>
<strong>Magical Hogwarts Escape Quiz</strong><br>
A private, non-commercial web project on <code>dev.waiswuaschd.de</code>.<br>
Operator: Maximilian R. (Waiswuaschd Project)<br>
Contact: <a href="mailto:kontakt@waiswuaschd.de" style="color: var(--gold);">kontakt@waiswuaschd.de</a>
</p>
<h3 style="margin-top: 20px;">Non-Commercial Fan Project Disclaimer</h3>
<p>
This website is a purely non-commercial, private fan project created for educational, puzzle, and entertainment purposes.
No commercial profit or advertising revenue is generated through this service.
</p>
<h3 style="margin-top: 20px;">Copyright & Trademarks</h3>
<p>
<strong>Harry Potter</strong> characters, names, and related indicia are trademarks of and © Warner Bros. Entertainment Inc. and J.K. Rowling.
Harry Potter publishing rights © Bloomsbury Publishing Plc and Carlsen Verlag.
All rights belong to their respective copyright and trademark owners. No infringement is intended.
</p>
<h3 style="margin-top: 20px;">Liability for Content & External Links</h3>
<p>
As a service provider, we are responsible for our own content on these pages under general laws. We make every effort to keep information up to date and accurate, but cannot accept liability for external links to third-party websites over whose content we have no control.
</p>
`
}
};
function onLanguageChange(lang) {
const isDe = (lang !== "en");
const t = isDe ? IMP_TEXTS.de : IMP_TEXTS.en;
document.getElementById("impTitle").innerText = t.title;
document.getElementById("impSub").innerText = t.sub;
document.getElementById("impContent").innerHTML = t.html;
document.getElementById("btnBackQuiz").innerText = t.btnBack;
document.getElementById("btnToPrivacy").innerText = t.btnToPrivacy;
}
document.addEventListener("DOMContentLoaded", () => {
onLanguageChange(currentLang);
});
</script>
{% endblock %}
+93 -49
View File
@@ -1,63 +1,107 @@
{% extends "base.html" %}
{% block title %}✨ Magisches Harry-Potter-Escape-Quiz ✨{% endblock %}
{% block title %}⚡ PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %}
{% block content %}
<div class="quiz-wrapper">
<header class="quiz-header">
<h1 class="glow-title" id="pageTitle">✨ Magisches Hogwarts-Escape-Quiz ✨</h1>
<p class="subtitle" id="pageSubtitle">10 knifflige Rätsel aus der Zauberwelt warten auf dich</p>
<div class="hero-pill" id="heroPill">⚡ {{ question_count }} Fragen im Wissens-Pool · Buch & Film</div>
<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>
<!-- Brief-Intro / Startmaske -->
<div id="introCard" class="card intro-card">
<div class="intro-envelope" id="envelope">
<div class="envelope-body">
<div class="envelope-flap"></div>
<div class="wax-seal">H</div>
<div class="letter-preview">
<p class="letter-text" id="envelopeLetter">Hogwarts-Schule für Hexerei und Zauberei</p>
<!-- Kompaktes Direct-Start Dashboard -->
<div id="introCard" class="card start-dashboard-card">
<div class="dashboard-grid">
<!-- Sektion 1: Schwierigkeitsgrad -->
<div class="config-section">
<div class="config-header">
<span class="config-step">1</span>
<span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad</span>
</div>
<div class="difficulty-chips" role="radiogroup" aria-label="Schwierigkeitsgrad">
<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 Stufen</span>
</button>
<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>
</button>
<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>
</button>
<button type="button" class="diff-chip" data-diff="hard" onclick="selectDifficulty('hard')">
<span class="diff-icon">🔴</span>
<span class="diff-text" id="diffHardText">Schwer (Experte)</span>
</button>
</div>
</div>
<!-- Sektion 2: Kategorie-Filter -->
<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="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">
<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>
</button>
<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>
</button>
<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>
</button>
<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>
</button>
</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="startBtn" class="btn btn-primary btn-large" onclick="startGame()">
📜 Siegel brechen & Brief öffnen
<button id="startQuizBtn" class="btn btn-primary btn-large btn-launch-quiz" onclick="startGame()">
⚡ Quiz jetzt starten ➔
</button>
</div>
</div>
<!-- Spielbereich (initial versteckt) -->
<div id="gameCard" class="game-area hidden">
<!-- Fortschritt & Punkte -->
<!-- HUD: Fortschritt, Streak, Zeit & Punkte -->
<div class="status-panel">
<div class="progress-info">
<span id="questionCounter">Frage 1 von 10</span>
<span id="pointsCounter" class="points-badge">🏆 Hauspunkte: 0</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>
</div>
<span id="pointsCounter" class="points-badge">🏆 0 Pkt.</span>
</div>
<div class="progress-container">
<div id="progressBar" class="progress-bar"></div>
@@ -68,24 +112,24 @@
<div id="frageBox" class="card question-card">
<div class="question-meta">
<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>
</div>
<span id="attemptBadge" class="attempt-badge">Versuch 1 von 3</span>
<span id="attemptBadge" class="attempt-badge">Versuch 1 / 3</span>
</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') -->
<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">
<button id="checkBtn" class="btn btn-primary" onclick="checkAnswer()">
✨ Zauber prüfen
✓ Antwort prüfen
</button>
<button id="hintBtn" class="btn btn-secondary" onclick="requestHint()">
💡 Zaubertipp
💡 Tipp anzeigen
</button>
</div>
</div>
@@ -93,15 +137,15 @@
<!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') -->
<div id="choiceAnswerGroup" class="choice-group hidden">
<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()">
💡 Zaubertipp
💡 Tipp anzeigen
</button>
</div>
</div>
<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>
</div>
@@ -112,5 +156,5 @@
{% endblock %}
{% block scripts %}
<script src="/static/js/quiz.js"></script>
<script src="/static/js/quiz.js?v=4.0"></script>
{% endblock %}
+19 -4
View File
@@ -5,11 +5,11 @@
{% block content %}
<div class="quiz-wrapper">
<div class="card vorschlag-card">
<h1 class="glow-title" id="suggTitle">✨ Neue Frage für das Quiz vorschlagen ✨</h1>
<p class="subtitle" id="suggSubtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p>
<h1 class="glow-title" id="suggTitle">✍️ Neue Quiz-Frage einreichen</h1>
<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">
ℹ️ <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>
<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>
</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">
<label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label>
<select id="diff_select" name="difficulty" class="input-text">
@@ -70,5 +85,5 @@
{% endblock %}
{% block scripts %}
<script src="/static/js/vorschlag.js"></script>
<script src="/static/js/vorschlag.js?v=2.3"></script>
{% endblock %}
-25
View File
@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Fragen-Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/style.css">
</head>
<body class="light-theme">
<h1>Fragen-Editor</h1>
<div id="fragen"></div>
<h2>Neue Frage hinzufügen</h2>
<div class="box">
<input id="newFrage" placeholder="Frage">
<input id="newAntwort" placeholder="Antwort">
<input id="newTipp" placeholder="Tipp">
<button onclick="addFrage()">Hinzufügen</button>
</div>
<script src="js/admin.js"></script>
</body>
</html>
-160
View File
@@ -1,160 +0,0 @@
:root {
--bg: #0b0b15;
--fg: #f5f5f5;
--accent: #c9a86a;
--accent-soft: #e6c98f;
--box-bg: rgba(255,255,255,0.05);
}
body {
background: var(--bg);
color: var(--fg);
font-family: "Georgia", serif;
text-align: center;
padding: 20px;
}
.hidden { display: none; }
.box {
border: 2px solid var(--accent);
padding: 20px;
margin: 20px auto;
width: 90%;
max-width: 600px;
background: var(--box-bg);
border-radius: 10px;
}
input, textarea {
padding: 10px;
border-radius: 5px;
border: none;
margin-top: 10px;
width: 80%;
max-width: 250px;
text-align: center;
}
button {
padding: 10px 20px;
background: var(--accent);
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
font-weight: bold;
color: #000;
}
button:hover {
background: var(--accent-soft);
}
/* Fortschrittsleiste */
.progress-container {
width: 90%;
max-width: 600px;
margin: 10px auto;
background: rgba(255,255,255,0.1);
border-radius: 10px;
border: 1px solid var(--accent);
overflow: hidden;
}
.progress-bar {
height: 14px;
background: linear-gradient(90deg, #c9a86a, #e6c98f);
width: 0%;
transition: width 0.3s;
}
/* Briefumschlag */
.intro-envelope {
position: relative;
width: 220px;
height: 150px;
margin: 0 auto 20px;
}
.envelope-body {
width: 100%;
height: 100%;
background: #f5f5dc;
border-radius: 5px;
border: 2px solid #c9a86a;
position: relative;
overflow: hidden;
z-index: 1;
}
.envelope-flap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #e6d8a8;
clip-path: polygon(0 0, 50% 60%, 100% 0);
transform-origin: top;
transition: transform 0.8s ease-in-out;
z-index: 2;
}
.open .envelope-flap {
transform: rotateX(180deg);
}
.sparkle::after {
content: "";
position: absolute;
top: -10px;
left: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: #ffeaa7;
box-shadow:
0 0 8px #ffeaa7,
-20px -10px 6px rgba(255,255,255,0.6),
15px -5px 6px rgba(255,255,255,0.5),
-10px 10px 6px rgba(255,255,255,0.4),
20px 15px 6px rgba(255,255,255,0.5);
animation: sparkle 0.7s ease-out forwards;
}
@keyframes sparkle {
from { opacity: 1; transform: translate(-50%, 0) scale(1); }
to { opacity: 0; transform: translate(-50%, -20px) scale(0.5); }
}
/* loesungen.html */
.loesungen-list .box {
background: #222;
border-left: 4px solid var(--accent);
border: none;
}
/* vorschlag.html & admin.html */
body.light-theme {
background: #f5f5f5;
color: #000;
}
body.light-theme .box {
background: white;
box-shadow: 0 0 5px rgba(0,0,0,0.1);
}
textarea {
resize: vertical;
font-family: Arial, sans-serif;
}
label {
display: block;
margin-top: 15px;
text-align: left;
font-weight: bold;
}
/* admin.html */
.box strong {
display: block;
margin-bottom: 10px;
}
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>400 – Verirrter Zauber</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>400 – Verirrter Zauber</h1>
<p>Der letzte Zauberspruch war etwas durcheinander geraten.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>401 – Zutritt nur für Zauberer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>401 – Zutritt nur für Zauberer</h1>
<p>Für diesen Bereich brauchst du noch eine magische Legitimation.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>403 – Verbotener Bereich</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>403 – Verbotener Bereich</h1>
<p>Dieser Teil der Zauberwelt ist streng geschützt.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>404 – Karte des Rumtreibers leer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>404 – Seite nicht gefunden</h1>
<p>Die Karte des Rumtreibers zeigt hier nichts an.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>405 – Unzulässiger Zauber</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>405 – Unzulässiger Zauber</h1>
<p>Diese Art von Zauberspruch ist hier nicht erlaubt.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>500 – Explosion im Zaubertrank-Labor</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>500 – Explosion im Zaubertrank-Labor</h1>
<p>Irgendetwas ist gründlich schiefgegangen. Die Hauselfen räumen schon auf.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>502 – Fehlgeleiteter Eulenflug</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>502 – Fehlgeleiteter Eulenflug</h1>
<p>Die Nachricht hat das Ziel nicht richtig erreicht.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>503 – Zaubereiministerium beschäftigt</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { background:#0b0b15;color:#f5f5f5;font-family:Georgia,serif;text-align:center;padding:20px; }
.box { border:2px solid #c9a86a;padding:20px;margin:20px auto;width:90%;max-width:600px;background:rgba(255,255,255,0.05);border-radius:10px; }
a { color:#c9a86a;text-decoration:none;font-weight:bold; }
</style>
</head>
<body>
<div class="box">
<h1>503 – Zaubereiministerium beschäftigt</h1>
<p>Die magischen Dienste sind gerade ausgelastet. Versuche es später erneut.</p>
<p><a href="index.html">Zurück zum magischen Quiz</a></p>
</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

-28
View File
@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Finale – Magisches Quiz</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="box">
<h1>🎉 Happy Birthday Schatz 🎉</h1>
<p id="scoreText"></p>
<h2>💛 Der Gewinn ist ein schöner Abend Zuhause 💛</h2>
<p>
Wir holen uns lecker essen und machen es uns gemütlich, entspannen, lachen und genießen die Zeit zusammen.
Wir kaufen ein Online escape game. Nach deiner Wahl, da ich nicht mehr weiß welche wir hatten.
</p>
<p>Ich freue mich auf unseren Abend.</p>
<button onclick="window.location.href='index.html'">Erneut spielen</button>
</div>
<script src="js/finale.js"></script>
</body>
</html>
-77
View File
@@ -1,77 +0,0 @@
[
{
"frage": "Wie lautet der zweite Vorname von Harry?",
"antwort": "james",
"tipp": "Er stammt von seinem Vater."
},
{
"frage": "Wie heißt die Hüterin der verbotenen Abteilung?",
"antwort": "madam pince",
"tipp": "Ihr Nachname klingt wie 'zwicken'."
},
{
"frage": "Welches magische Wesen bewacht die Kammer des Schreckens?",
"antwort": "basilisk",
"tipp": "Ein tödlicher Blick."
},
{
"frage": "Wie lautet der Vorname von Dumbledores Bruder?",
"antwort": "aberforth",
"tipp": "Er betreibt den Eberkopf."
},
{
"frage": "Welcher Zauber versiegelt Türen?",
"antwort": "colloportus",
"tipp": "Kommt im fünften Band häufig vor."
},
{
"frage": "Wie heißt der Hauself der Familie Black?",
"antwort": "kreacher",
"tipp": "Er ist nicht besonders freundlich."
},
{
"frage": "Wie lautet der Zauber, der Gegner entwaffnet?",
"antwort": "expelliarmus",
"tipp": "Harry benutzt ihn ständig."
},
{
"frage": "Mein Lieblingsessen?",
"antwort": "Raclette",
"tipp": "Käse :)"
},
{
"frage": "Dein Lieblingsessen?",
"antwort": "Ravioli",
"tipp": "Gefüllt ist's. "
},
{
"frage": "Wo haben wir uns das erste mal geküsst?",
"antwort": "Olching",
"tipp": "Nur der Ort ist wichtig"
},
{
"frage": "Mein erster Geburtstagskuchen für dich?",
"antwort": "Donauwelle",
"tipp": "Erst gestern war es witztig"
},
{
"frage": "Erster Urlaub?",
"antwort": "Den Haag",
"tipp": "Nicht das land sondern der Ort"
},
{
"frage": "Lieblingskörperstelle?",
"antwort": "Ombelico",
"tipp": "Es hat so schön geprickelt"
},
{
"frage": "Als filmpaar sind wir am ehesten?",
"antwort": "Monroe&Rosalee",
"tipp": "Grimm ist die Serie"
},
{
"frage": "Wie heißt unser erster Hund?",
"antwort": "Molly",
"tipp": "mal was einfaches"
}
]
-7
View File
@@ -1,7 +0,0 @@
[
{
"frage": "Hagrid schlüsselbund hat wie viele Schlüssel?",
"antwort": "7",
"tipp": "weil es immer 3,5 oder 7 sind."
}
]
-34
View File
@@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Magisches Harry-Potter-Escape-Quiz</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<audio id="bgm" src="music.mp3" loop></audio>
<h1>✨ Magisches Spatzi-Quiz ✨</h1>
<div class="progress-container">
<div id="progressBar" class="progress-bar"></div>
</div>
<div id="progressLabel">Hauspunkte: 0</div>
<div id="intro" class="box">
<div class="intro-envelope" id="envelope">
<div class="envelope-body">
<div class="envelope-flap"></div>
</div>
</div>
<p>Ein verzauberter Briefumschlag ist vor dir erschienen.</p>
<button onclick="startGame()">Brief öffnen</button>
</div>
<div id="questions"></div>
<script src="js/index.js"></script>
</body>
</html>
-54
View File
@@ -1,54 +0,0 @@
let daten = [];
async function load() {
const res = await fetch("fragen.json");
daten = await res.json();
render();
}
function render() {
const container = document.getElementById("fragen");
container.innerHTML = "";
daten.forEach((f, i) => {
container.innerHTML += `
<div class="box">
<strong>Frage ${i+1}</strong>
<input value="${f.frage}" onchange="edit(${i}, 'frage', this.value)">
<input value="${f.antwort}" onchange="edit(${i}, 'antwort', this.value)">
<input value="${f.tipp}" onchange="edit(${i}, 'tipp', this.value)">
<button onclick="removeFrage(${i})">Löschen</button>
</div>
`;
});
}
function edit(i, field, value) {
daten[i][field] = value;
save();
}
function addFrage() {
daten.push({
frage: document.getElementById("newFrage").value,
antwort: document.getElementById("newAntwort").value,
tipp: document.getElementById("newTipp").value
});
save();
}
function removeFrage(i) {
daten.splice(i, 1);
save();
}
async function save() {
await fetch("save.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(daten)
});
load();
}
load();
-10
View File
@@ -1,10 +0,0 @@
function getParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
const score = getParam("score") || 0;
const max = getParam("max") || 10;
document.getElementById("scoreText").innerText =
`Du hast ${score} von ${max} möglichen Hauspunkten erreicht.`;
-110
View File
@@ -1,110 +0,0 @@
function levenshtein(a, b) {
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
Array.from({ length: b.length + 1 }, (_, j) =>
i === 0 ? j : j === 0 ? i : 0
)
);
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
);
}
}
return matrix[a.length][b.length];
}
let fragen = [];
let index = 0;
let punkte = 0;
let versuche = [];
function startGame() {
document.getElementById("envelope").classList.add("open");
setTimeout(() => {
document.getElementById("intro").classList.add("hidden");
document.getElementById("bgm").play();
loadQuestions();
}, 900);
}
async function loadQuestions() {
const res = await fetch("fragen.json");
fragen = await res.json();
versuche = Array(fragen.length).fill(0);
renderFrage();
updateProgress();
}
function renderFrage() {
const f = fragen[index];
document.getElementById("questions").innerHTML = `
<div class="box" id="frageBox">
<h2>Frage ${index + 1}</h2>
<p>${f.frage}</p>
<input id="antwort">
<button onclick="check()">Prüfen</button>
<button onclick="showTip()">Tipp</button>
<p class="tip hidden" id="tipp">${f.tipp}</p>
<p id="versuche">Versuche: 0 / 3</p>
<p id="feedback"></p>
</div>
`;
}
function showTip() {
document.getElementById("tipp").classList.remove("hidden");
}
function check() {
const input = document.getElementById("antwort").value.toLowerCase().trim();
const korrekt = fragen[index].antwort.toLowerCase().trim();
versuche[index]++;
document.getElementById("versuche").innerText = `Versuche: ${versuche[index]} / 3`;
const dist = levenshtein(input, korrekt);
if (dist <= 2) {
punkte++;
addSparkle();
next();
return;
}
if (versuche[index] >= 3) {
next();
return;
}
document.getElementById("feedback").innerText = "Leider falsch.";
}
function addSparkle() {
const el = document.getElementById("frageBox");
el.classList.add("sparkle");
setTimeout(() => el.classList.remove("sparkle"), 700);
}
function next() {
index++;
updateProgress();
if (index >= fragen.length) {
window.location.href = `finale.html?score=${punkte}&max=${fragen.length}`;
return;
}
renderFrage();
}
function updateProgress() {
const percent = (index / fragen.length) * 100;
document.getElementById("progressBar").style.width = percent + "%";
document.getElementById("progressLabel").innerText = `Hauspunkte: ${punkte}`;
}
-17
View File
@@ -1,17 +0,0 @@
async function load() {
const res = await fetch("fragen.json");
const fragen = await res.json();
const container = document.getElementById("loesungen");
fragen.forEach((f, i) => {
container.innerHTML += `
<div class="box">
${i + 1}. ${f.frage}<br>
<strong>${f.antwort}</strong>
</div>
`;
});
}
load();
-31
View File
@@ -1,31 +0,0 @@
async function send() {
const frage = document.getElementById("frage").value.trim();
const antwort = document.getElementById("antwort").value.trim();
const tipp = document.getElementById("tipp").value.trim();
if (!frage || !antwort) {
alert("Bitte mindestens Frage und Antwort ausfüllen.");
return;
}
const payload = {
frage: frage,
antwort: antwort,
tipp: tipp
};
const res = await fetch("save_vorschlag.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (res.ok) {
alert("Danke! Dein Vorschlag wurde gespeichert.");
document.getElementById("frage").value = "";
document.getElementById("antwort").value = "";
document.getElementById("tipp").value = "";
} else {
alert("Fehler beim Speichern. Bitte später erneut versuchen.");
}
}
-15
View File
@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Lösungen – Magisches Quiz</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<h1>🔐 Lösungen zum Harry-Potter-Escape-Quiz</h1>
<div id="loesungen" class="loesungen-list"></div>
<script src="js/loesungen.js"></script>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
User-agent: *
-5
View File
@@ -1,5 +0,0 @@
<?php
$data = file_get_contents("php://input");
file_put_contents("fragen.json", $data);
echo "OK";
?>
-37
View File
@@ -1,37 +0,0 @@
<?php
header("Content-Type: application/json");
// Datei, in die gespeichert wird
$file = "fragen_vorschlaege.json";
// Eingehende Daten lesen
$input = file_get_contents("php://input");
$data = json_decode($input, true);
if (!$data || !isset($data["frage"]) || !isset($data["antwort"])) {
http_response_code(400);
echo json_encode(["error" => "Ungültige Daten"]);
exit;
}
// Existierende Vorschläge laden
$existing = [];
if (file_exists($file)) {
$json = file_get_contents($file);
$existing = json_decode($json, true);
if (!is_array($existing)) {
$existing = [];
}
}
// Neuen Vorschlag anhängen
$existing[] = $data;
// Datei speichern
if (file_put_contents($file, json_encode($existing, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
echo json_encode(["success" => true]);
} else {
http_response_code(500);
echo json_encode(["error" => "Konnte Datei nicht schreiben"]);
}
?>
-31
View File
@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Frage vorschlagen</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/style.css">
</head>
<body class="light-theme">
<h1>✨ Frage vorschlagen ✨</h1>
<p>Hier kannst du eine neue Frage für das Quiz vorschlagen.
Die Frage wird gespeichert und später geprüft.</p>
<div class="box">
<label>Frage:</label>
<textarea id="frage" rows="2" placeholder="Deine Frage..."></textarea>
<label>Antwort:</label>
<input id="antwort" placeholder="Die richtige Antwort...">
<label>Tipp (optional):</label>
<input id="tipp" placeholder="Hinweis für die Spieler...">
<button onclick="send()">Vorschlag einreichen</button>
</div>
<script src="js/vorschlag.js"></script>
</body>
</html>
+52
View File
@@ -0,0 +1,52 @@
import os
from sqlalchemy import create_engine, text
from dotenv import load_dotenv
load_dotenv()
DB_USER = os.getenv("DB_USER", "v2_usr_quiz")
DB_PASS = os.getenv("DB_PASS", "Quiz_Pass_2026_Secure!")
DB_HOST = os.getenv("DB_HOST", "db.waiswuaschd.de")
DB_PORT = os.getenv("DB_PORT", "3306")
DB_NAME = os.getenv("DB_NAME", "v2_db_quiz")
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
engine = create_engine(DATABASE_URL)
def run_migration():
with engine.connect() as conn:
print("Running migrate_v4...")
# 1. Add duration_seconds to quiz_sessions
res = conn.execute(text("SHOW COLUMNS FROM quiz_sessions LIKE 'duration_seconds'")).fetchall()
if not res:
print("Adding duration_seconds to quiz_sessions...")
conn.execute(text("ALTER TABLE quiz_sessions ADD COLUMN duration_seconds INT NULL"))
conn.commit()
print("Added duration_seconds column.")
else:
print("Column duration_seconds already exists.")
# 2. Create leaderboard_entries table
print("Ensuring leaderboard_entries table exists...")
conn.execute(text("""
CREATE TABLE IF NOT EXISTS leaderboard_entries (
id INT AUTO_INCREMENT PRIMARY KEY,
nickname VARCHAR(50) NOT NULL,
score INT NOT NULL,
total_questions INT NOT NULL DEFAULT 10,
time_seconds INT NOT NULL,
difficulty VARCHAR(20) NOT NULL DEFAULT 'all',
lang VARCHAR(10) NOT NULL DEFAULT 'de',
session_id VARCHAR(36) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_leaderboard_score_time (score DESC, time_seconds ASC),
INDEX idx_leaderboard_diff (difficulty),
INDEX idx_leaderboard_created (created_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"""))
conn.commit()
print("leaderboard_entries table ready.")
if __name__ == "__main__":
run_migration()
+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