Compare commits
13 Commits
26ff0660d3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c62676d0d | |||
| fb789e8866 | |||
| ecf9d8283a | |||
| 2b5ae98b91 | |||
| ea5bc12e21 | |||
| 8c4768747b | |||
| 7e20d243e8 | |||
| 3e5c3c1eeb | |||
| 1cbea5e84f | |||
| 611a94ab43 | |||
| 8d8018347c | |||
| b1d88da681 | |||
| a938123271 |
+89
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
+106
-15
@@ -20,7 +20,7 @@ from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequ
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
app = FastAPI(title="Magisches Harry-Potter-Escape-Quiz")
|
app = FastAPI(title="PotterTrivia – Das Harry-Potter-Wissensquiz")
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY", "hogwarts_secret_default_key_2026")
|
SECRET_KEY = os.getenv("SECRET_KEY", "hogwarts_secret_default_key_2026")
|
||||||
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="quiz_session")
|
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="quiz_session")
|
||||||
@@ -49,6 +49,7 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
|
|||||||
random.shuffle(options_list)
|
random.shuffle(options_list)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"id": q.id,
|
||||||
"question_number": number,
|
"question_number": number,
|
||||||
"total_questions": total,
|
"total_questions": total,
|
||||||
"category": q.category,
|
"category": q.category,
|
||||||
@@ -59,10 +60,15 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
|
def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
|
||||||
pct = (score / total) if total > 0 else 0
|
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")
|
is_de = (lang != "en")
|
||||||
|
|
||||||
if score >= 10 or pct >= 1.0:
|
if pct >= 0.90:
|
||||||
grade = "O"
|
grade = "O"
|
||||||
title = "🧙♂️ Großmeister der Zauberei" if is_de else "🧙♂️ Grandmaster of Wizardry"
|
title = "🧙♂️ Großmeister der Zauberei" if is_de else "🧙♂️ Grandmaster of Wizardry"
|
||||||
subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)"
|
subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)"
|
||||||
@@ -76,7 +82,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
|
|||||||
"The Ministry of Magic bows in deep awe!"
|
"The Ministry of Magic bows in deep awe!"
|
||||||
)
|
)
|
||||||
badge_class = "tier-outstanding"
|
badge_class = "tier-outstanding"
|
||||||
elif score >= 8 or pct >= 0.8:
|
elif pct >= 0.70:
|
||||||
grade = "E"
|
grade = "E"
|
||||||
title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard"
|
title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard"
|
||||||
subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)"
|
subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)"
|
||||||
@@ -88,7 +94,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
|
|||||||
"Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous."
|
"Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous."
|
||||||
)
|
)
|
||||||
badge_class = "tier-exceeds"
|
badge_class = "tier-exceeds"
|
||||||
elif score >= 5 or pct >= 0.5:
|
elif pct >= 0.50:
|
||||||
grade = "A"
|
grade = "A"
|
||||||
title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student"
|
title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student"
|
||||||
subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)"
|
subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)"
|
||||||
@@ -100,7 +106,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
|
|||||||
"but you bravely fought your way through. A solid magical performance!"
|
"but you bravely fought your way through. A solid magical performance!"
|
||||||
)
|
)
|
||||||
badge_class = "tier-acceptable"
|
badge_class = "tier-acceptable"
|
||||||
elif score >= 2 or pct >= 0.2:
|
elif pct >= 0.20:
|
||||||
grade = "M"
|
grade = "M"
|
||||||
title = "🧹 Quaffel-Fänger in Not" if is_de else "🧹 Quaffle Catcher in Distress"
|
title = "🧹 Quaffel-Fänger in Not" if is_de else "🧹 Quaffle Catcher in Distress"
|
||||||
subtitle = "Note: M (Mies)" if is_de else "Grade: P (Poor)"
|
subtitle = "Note: M (Mies)" if is_de else "Grade: P (Poor)"
|
||||||
@@ -148,23 +154,79 @@ async def index(request: Request, db: Session = Depends(get_db)):
|
|||||||
context={"question_count": active_count}
|
context={"question_count": active_count}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
VALID_CATEGORIES = ["all", "books", "movies", "creatures", "spells", "characters", "hogwarts", "quidditch", "lore", "general"]
|
||||||
|
|
||||||
@app.post("/api/game/start")
|
@app.post("/api/game/start")
|
||||||
async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)):
|
async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)):
|
||||||
lang = payload.lang if payload and payload.lang in ["de", "en"] else "de"
|
lang = payload.lang if payload and payload.lang in ["de", "en"] else "de"
|
||||||
difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all"
|
difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all"
|
||||||
|
category = payload.category if payload and payload.category in VALID_CATEGORIES else "all"
|
||||||
|
|
||||||
|
base_query = db.query(Question).filter(Question.status == "active")
|
||||||
|
if category != "all":
|
||||||
|
if category == "books":
|
||||||
|
cat_query = base_query.filter((Question.category == "books") | (Question.question.like("%Kröter%")))
|
||||||
|
else:
|
||||||
|
cat_query = base_query.filter(Question.category == category)
|
||||||
|
else:
|
||||||
|
cat_query = base_query
|
||||||
|
|
||||||
query = db.query(Question).filter(Question.status == "active")
|
|
||||||
if difficulty != "all":
|
if difficulty != "all":
|
||||||
query = query.filter(Question.difficulty == difficulty)
|
strict_pool = cat_query.filter(Question.difficulty == difficulty).all()
|
||||||
|
if len(strict_pool) >= 10 or category == "all":
|
||||||
|
pool = strict_pool
|
||||||
|
else:
|
||||||
|
# Keep all difficulty matches in this category and top up from the same category
|
||||||
|
cat_all = cat_query.all()
|
||||||
|
strict_ids = {q.id for q in strict_pool}
|
||||||
|
extra = [q for q in cat_all if q.id not in strict_ids]
|
||||||
|
random.shuffle(extra)
|
||||||
|
pool = strict_pool + extra[:max(0, 10 - len(strict_pool))]
|
||||||
|
else:
|
||||||
|
pool = cat_query.all()
|
||||||
|
|
||||||
pool = query.all()
|
|
||||||
if not pool:
|
if not pool:
|
||||||
pool = db.query(Question).filter(Question.status == "active").all()
|
pool = db.query(Question).filter(Question.status == "active").all()
|
||||||
if not pool:
|
if not pool:
|
||||||
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
||||||
|
|
||||||
sample_size = min(10, len(pool))
|
recent_set = set(payload.recent_ids or []) if payload else set()
|
||||||
selected_questions = random.sample(pool, sample_size)
|
wrong_set = set(payload.wrong_ids or []) if payload else set()
|
||||||
|
|
||||||
|
# Priority pool 1: Questions user previously failed (learning pool)
|
||||||
|
wrong_pool = [q for q in pool if q.id in wrong_set]
|
||||||
|
# Priority pool 2: Fresh questions not seen recently and not in wrong pool
|
||||||
|
fresh_pool = [q for q in pool if q.id not in recent_set and q.id not in wrong_set]
|
||||||
|
# Priority pool 3: Recent questions fallback
|
||||||
|
recent_pool = [q for q in pool if q.id in recent_set and q.id not in wrong_set]
|
||||||
|
|
||||||
|
selected_questions = []
|
||||||
|
# Up to 3 questions from wrong_pool for spaced repetition
|
||||||
|
if wrong_pool:
|
||||||
|
k_wrong = min(3, len(wrong_pool))
|
||||||
|
selected_questions.extend(random.sample(wrong_pool, k_wrong))
|
||||||
|
|
||||||
|
# Fill remaining from fresh_pool
|
||||||
|
needed = min(10, len(pool)) - len(selected_questions)
|
||||||
|
if needed > 0 and fresh_pool:
|
||||||
|
k_fresh = min(needed, len(fresh_pool))
|
||||||
|
selected_questions.extend(random.sample(fresh_pool, k_fresh))
|
||||||
|
|
||||||
|
# If still needed, fill from recent_pool
|
||||||
|
needed = min(10, len(pool)) - len(selected_questions)
|
||||||
|
if needed > 0 and recent_pool:
|
||||||
|
k_recent = min(needed, len(recent_pool))
|
||||||
|
selected_questions.extend(random.sample(recent_pool, k_recent))
|
||||||
|
|
||||||
|
# Fallback to any remaining from pool
|
||||||
|
needed = min(10, len(pool)) - len(selected_questions)
|
||||||
|
if needed > 0:
|
||||||
|
remaining = [q for q in pool if q not in selected_questions]
|
||||||
|
if remaining:
|
||||||
|
selected_questions.extend(random.sample(remaining, min(needed, len(remaining))))
|
||||||
|
|
||||||
|
# Shuffle to mix learning and fresh questions
|
||||||
|
random.shuffle(selected_questions)
|
||||||
question_ids = [q.id for q in selected_questions]
|
question_ids = [q.id for q in selected_questions]
|
||||||
session_id = str(uuid.uuid4())
|
session_id = str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -211,6 +273,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": False,
|
"correct": False,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": 0,
|
||||||
|
"question_id": None,
|
||||||
"attempts_used": session.attempts_current,
|
"attempts_used": session.attempts_current,
|
||||||
"attempts_left": 0,
|
"attempts_left": 0,
|
||||||
"is_finished": True,
|
"is_finished": True,
|
||||||
@@ -226,7 +290,15 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
|
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
|
||||||
|
|
||||||
if correct:
|
if correct:
|
||||||
session.score += 1
|
# Tiered points based on attempt
|
||||||
|
if session.attempts_current == 0:
|
||||||
|
points_earned = 100
|
||||||
|
elif session.attempts_current == 1:
|
||||||
|
points_earned = 70
|
||||||
|
else:
|
||||||
|
points_earned = 40
|
||||||
|
|
||||||
|
session.score += points_earned
|
||||||
session.current_question_idx += 1
|
session.current_question_idx += 1
|
||||||
session.attempts_current = 0
|
session.attempts_current = 0
|
||||||
|
|
||||||
@@ -248,6 +320,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": True,
|
"correct": True,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": points_earned,
|
||||||
|
"question_id": current_q_id,
|
||||||
"attempts_used": 0,
|
"attempts_used": 0,
|
||||||
"attempts_left": 3,
|
"attempts_left": 3,
|
||||||
"is_finished": is_finished,
|
"is_finished": is_finished,
|
||||||
@@ -286,6 +360,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": False,
|
"correct": False,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": 0,
|
||||||
|
"question_id": current_q_id,
|
||||||
"attempts_used": 3,
|
"attempts_used": 3,
|
||||||
"attempts_left": 0,
|
"attempts_left": 0,
|
||||||
"is_finished": is_finished,
|
"is_finished": is_finished,
|
||||||
@@ -298,6 +374,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": False,
|
"correct": False,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": 0,
|
||||||
|
"question_id": current_q_id,
|
||||||
"attempts_used": session.attempts_current,
|
"attempts_used": session.attempts_current,
|
||||||
"attempts_left": attempts_left,
|
"attempts_left": attempts_left,
|
||||||
"is_finished": False,
|
"is_finished": False,
|
||||||
@@ -329,7 +407,8 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
|
|||||||
return {"hint": hint_text}
|
return {"hint": hint_text}
|
||||||
|
|
||||||
@app.get("/finale", response_class=HTMLResponse)
|
@app.get("/finale", response_class=HTMLResponse)
|
||||||
async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de"):
|
async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de", diff: str = "all"):
|
||||||
|
max_score = 1000 if (total <= 10 and score > 10) else (total * 100 if total <= 10 else total)
|
||||||
tier = get_tier_evaluation(score, total, lang)
|
tier = get_tier_evaluation(score, total, lang)
|
||||||
if time >= 60:
|
if time >= 60:
|
||||||
minutes = time // 60
|
minutes = time // 60
|
||||||
@@ -338,15 +417,19 @@ async def finale(request: Request, score: int = 0, total: int = 10, time: int =
|
|||||||
else:
|
else:
|
||||||
time_str = f"{time} Sek." if lang != "en" else f"{time} sec."
|
time_str = f"{time} Sek." if lang != "en" else f"{time} sec."
|
||||||
|
|
||||||
|
clean_diff = diff if diff in ["easy", "medium", "hard"] else "all"
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="finale.html",
|
name="finale.html",
|
||||||
context={
|
context={
|
||||||
"score": score,
|
"score": score,
|
||||||
"total": total,
|
"total": total,
|
||||||
|
"max_score": max_score,
|
||||||
"time_seconds": time,
|
"time_seconds": time,
|
||||||
"time_formatted": time_str,
|
"time_formatted": time_str,
|
||||||
"lang": lang,
|
"lang": lang,
|
||||||
|
"difficulty": clean_diff,
|
||||||
"tier": tier
|
"tier": tier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -401,7 +484,11 @@ async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = De
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(entry)
|
db.refresh(entry)
|
||||||
|
|
||||||
better_count = db.query(LeaderboardEntry).filter(
|
better_query = db.query(LeaderboardEntry)
|
||||||
|
if entry.difficulty and entry.difficulty != "all":
|
||||||
|
better_query = better_query.filter(LeaderboardEntry.difficulty == entry.difficulty)
|
||||||
|
|
||||||
|
better_count = better_query.filter(
|
||||||
(LeaderboardEntry.score > entry.score) |
|
(LeaderboardEntry.score > entry.score) |
|
||||||
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds < entry.time_seconds)) |
|
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds < entry.time_seconds)) |
|
||||||
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds == entry.time_seconds) & (LeaderboardEntry.id < entry.id))
|
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds == entry.time_seconds) & (LeaderboardEntry.id < entry.id))
|
||||||
@@ -410,6 +497,7 @@ async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = De
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
"id": entry.id,
|
||||||
"entry_id": entry.id,
|
"entry_id": entry.id,
|
||||||
"rank": rank,
|
"rank": rank,
|
||||||
"nickname": entry.nickname,
|
"nickname": entry.nickname,
|
||||||
@@ -471,10 +559,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
|||||||
opts.append(opt.strip())
|
opts.append(opt.strip())
|
||||||
options_json = json.dumps(opts, ensure_ascii=False)
|
options_json = json.dumps(opts, ensure_ascii=False)
|
||||||
|
|
||||||
|
chosen_cat = payload.category if payload.category in ["books", "movies", "hogwarts", "characters", "spells", "creatures", "quidditch", "lore", "general"] else "general"
|
||||||
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
|
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
|
||||||
|
|
||||||
new_suggestion = Question(
|
new_suggestion = Question(
|
||||||
category="community",
|
category=chosen_cat,
|
||||||
question=payload.frage.strip(),
|
question=payload.frage.strip(),
|
||||||
canonical_answer=payload.antwort.strip(),
|
canonical_answer=payload.antwort.strip(),
|
||||||
hint=payload.tipp.strip() if payload.tipp else None,
|
hint=payload.tipp.strip() if payload.tipp else None,
|
||||||
@@ -487,9 +576,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
|||||||
)
|
)
|
||||||
db.add(new_suggestion)
|
db.add(new_suggestion)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
db.refresh(new_suggestion)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
"id": new_suggestion.id,
|
||||||
"message": "Vielen Dank! Dein Vorschlag wurde eingereicht und wird von uns geprüft, bevor er im Quiz erscheint."
|
"message": "Vielen Dank! Dein Vorschlag wurde eingereicht und wird von uns geprüft, bevor er im Quiz erscheint."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+74
-11
@@ -33,11 +33,47 @@ def levenshtein_distance(s1: str, s2: str) -> int:
|
|||||||
previous_row = current_row
|
previous_row = current_row
|
||||||
return previous_row[-1]
|
return previous_row[-1]
|
||||||
|
|
||||||
|
WORD_TO_NUM = {
|
||||||
|
"null": "0", "zero": "0",
|
||||||
|
"eins": "1", "ein": "1", "one": "1",
|
||||||
|
"zwei": "2", "two": "2",
|
||||||
|
"drei": "3", "three": "3",
|
||||||
|
"vier": "4", "four": "4",
|
||||||
|
"fuenf": "5", "five": "5",
|
||||||
|
"sechs": "6", "six": "6",
|
||||||
|
"sieben": "7", "seven": "7",
|
||||||
|
"acht": "8", "eight": "8",
|
||||||
|
"neun": "9", "nine": "9",
|
||||||
|
"zehn": "10", "ten": "10",
|
||||||
|
"elf": "11", "eleven": "11",
|
||||||
|
"zwoelf": "12", "twelve": "12",
|
||||||
|
"dreizehn": "13", "thirteen": "13",
|
||||||
|
"vierzehn": "14", "fourteen": "14",
|
||||||
|
"fuenfzehn": "15", "fifteen": "15",
|
||||||
|
"sechzehn": "16", "sixteen": "16",
|
||||||
|
"siebzehn": "17", "seventeen": "17",
|
||||||
|
"achtzehn": "18", "eighteen": "18",
|
||||||
|
"neunzehn": "19", "nineteen": "19",
|
||||||
|
"zwanzig": "20", "twenty": "20",
|
||||||
|
"fuenfzig": "50", "fifty": "50",
|
||||||
|
"hundert": "100", "einhundert": "100", "one hundred": "100",
|
||||||
|
"einhundertfuenfzig": "150", "one hundred fifty": "150", "one hundred and fifty": "150"
|
||||||
|
}
|
||||||
|
|
||||||
|
NUM_TO_WORDS = {}
|
||||||
|
for w, n in WORD_TO_NUM.items():
|
||||||
|
NUM_TO_WORDS.setdefault(n, []).append(w)
|
||||||
|
|
||||||
def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None, canonical_en: str | None = None) -> bool:
|
def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None, canonical_en: str | None = None) -> bool:
|
||||||
clean_user = normalize_text(user_input)
|
clean_user = normalize_text(user_input)
|
||||||
if not clean_user:
|
if not clean_user:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Also consider digit conversion of user input if a word was typed
|
||||||
|
user_variants = [clean_user]
|
||||||
|
if clean_user in WORD_TO_NUM:
|
||||||
|
user_variants.append(WORD_TO_NUM[clean_user])
|
||||||
|
|
||||||
targets = [normalize_text(canonical)]
|
targets = [normalize_text(canonical)]
|
||||||
if canonical_en:
|
if canonical_en:
|
||||||
clean_en = normalize_text(canonical_en)
|
clean_en = normalize_text(canonical_en)
|
||||||
@@ -56,18 +92,45 @@ def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None
|
|||||||
if part_clean:
|
if part_clean:
|
||||||
targets.append(part_clean)
|
targets.append(part_clean)
|
||||||
|
|
||||||
for target in targets:
|
# Expand any numeric target with corresponding number words and vice-versa
|
||||||
|
expanded_targets = list(targets)
|
||||||
|
for t in targets:
|
||||||
|
if t in NUM_TO_WORDS:
|
||||||
|
expanded_targets.extend(NUM_TO_WORDS[t])
|
||||||
|
if t in WORD_TO_NUM:
|
||||||
|
expanded_targets.append(WORD_TO_NUM[t])
|
||||||
|
|
||||||
|
for target in expanded_targets:
|
||||||
if not target:
|
if not target:
|
||||||
continue
|
continue
|
||||||
if clean_user == target:
|
for u in user_variants:
|
||||||
return True
|
if u == target:
|
||||||
# For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match
|
return True
|
||||||
if len(target) <= 3:
|
# For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match
|
||||||
continue
|
if len(target) <= 3 or len(u) <= 3:
|
||||||
# For medium words (4-6 chars), allow max 1 typo
|
continue
|
||||||
max_allowed = 1 if len(target) <= 6 else 2
|
# For medium words (4-6 chars), allow max 1 typo
|
||||||
dist = levenshtein_distance(clean_user, target)
|
max_allowed = 1 if len(target) <= 6 else 2
|
||||||
if dist <= max_allowed:
|
dist = levenshtein_distance(u, target)
|
||||||
return True
|
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
|
return False
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from typing import Optional, List
|
|||||||
class GameStartRequest(BaseModel):
|
class GameStartRequest(BaseModel):
|
||||||
lang: Optional[str] = "de"
|
lang: Optional[str] = "de"
|
||||||
difficulty: Optional[str] = "all"
|
difficulty: Optional[str] = "all"
|
||||||
|
category: Optional[str] = "all"
|
||||||
|
recent_ids: Optional[List[int]] = None
|
||||||
|
wrong_ids: Optional[List[int]] = None
|
||||||
|
|
||||||
class GameStartResponse(BaseModel):
|
class GameStartResponse(BaseModel):
|
||||||
session_id: str
|
session_id: str
|
||||||
@@ -24,6 +27,8 @@ class GameCheckRequest(BaseModel):
|
|||||||
class GameCheckResponse(BaseModel):
|
class GameCheckResponse(BaseModel):
|
||||||
correct: bool
|
correct: bool
|
||||||
score: int
|
score: int
|
||||||
|
points_earned: Optional[int] = 0
|
||||||
|
question_id: Optional[int] = None
|
||||||
attempts_used: int
|
attempts_used: int
|
||||||
attempts_left: int
|
attempts_left: int
|
||||||
max_attempts: int = 3
|
max_attempts: int = 3
|
||||||
@@ -34,6 +39,7 @@ class GameCheckResponse(BaseModel):
|
|||||||
class SuggestionCreateRequest(BaseModel):
|
class SuggestionCreateRequest(BaseModel):
|
||||||
frage: str
|
frage: str
|
||||||
antwort: str
|
antwort: str
|
||||||
|
category: Optional[str] = "general"
|
||||||
tipp: Optional[str] = None
|
tipp: Optional[str] = None
|
||||||
name: Optional[str] = "Gast"
|
name: Optional[str] = "Gast"
|
||||||
question_type: Optional[str] = "text"
|
question_type: Optional[str] = "text"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from app.database import engine, SessionLocal, Base
|
|||||||
from app.models import Question, AdminUser
|
from app.models import Question, AdminUser
|
||||||
from app.auth import hash_password
|
from app.auth import hash_password
|
||||||
from app.seed_40 import seed_40_questions
|
from app.seed_40 import seed_40_questions
|
||||||
|
from app.seed_lore_pack import seed_lore_pack
|
||||||
|
|
||||||
def seed_db():
|
def seed_db():
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
@@ -29,6 +30,9 @@ def seed_db():
|
|||||||
print("Question table is empty, seeding 40 initial bilingual questions...")
|
print("Question table is empty, seeding 40 initial bilingual questions...")
|
||||||
seed_40_questions()
|
seed_40_questions()
|
||||||
|
|
||||||
|
# 3. Ensure Lore Pack (Books, Movies, Hard Creatures incl. Blast-Ended Skrewts) is present
|
||||||
|
seed_lore_pack()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
+601
-559
File diff suppressed because it is too large
Load Diff
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 751 KiB |
+261
-122
@@ -2,76 +2,123 @@ let currentSessionId = null;
|
|||||||
let currentQuestionNumber = 1;
|
let currentQuestionNumber = 1;
|
||||||
let totalQuestions = 10;
|
let totalQuestions = 10;
|
||||||
let currentScore = 0;
|
let currentScore = 0;
|
||||||
|
let currentStreak = 0;
|
||||||
let attemptsUsed = 0;
|
let attemptsUsed = 0;
|
||||||
const maxAttempts = 3;
|
const maxAttempts = 3;
|
||||||
let selectedDifficulty = "all";
|
let selectedDifficulty = "all";
|
||||||
|
let selectedCategory = "all";
|
||||||
let lastQuestionData = null;
|
let lastQuestionData = null;
|
||||||
let timerStartTime = null;
|
let timerStartTime = null;
|
||||||
let timerInterval = null;
|
let timerInterval = null;
|
||||||
let elapsedSeconds = 0;
|
let elapsedSeconds = 0;
|
||||||
|
|
||||||
|
const CATEGORY_LABELS = {
|
||||||
|
de: {
|
||||||
|
all: "✨ Alle Kategorien",
|
||||||
|
books: "📖 Buchwissen",
|
||||||
|
movies: "🎬 Filmwissen",
|
||||||
|
creatures: "🐉 Magische Geschöpfe",
|
||||||
|
spells: "🪄 Zaubersprüche & Tränke",
|
||||||
|
characters: "🧙♂️ Charaktere",
|
||||||
|
hogwarts: "🏰 Hogwarts & Orte",
|
||||||
|
quidditch: "🧹 Quidditch",
|
||||||
|
lore: "🔮 Artefakte & Lore",
|
||||||
|
general: "⚡ Allgemein",
|
||||||
|
harry_potter: "⚡ Harry Potter"
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
all: "✨ All Categories",
|
||||||
|
books: "📖 Book Lore",
|
||||||
|
movies: "🎬 Movie Trivia",
|
||||||
|
creatures: "🐉 Magical Creatures",
|
||||||
|
spells: "🪄 Spells & Potions",
|
||||||
|
characters: "🧙♂️ Characters",
|
||||||
|
hogwarts: "🏰 Hogwarts & Places",
|
||||||
|
quidditch: "🧹 Quidditch",
|
||||||
|
lore: "🔮 Artifacts & Lore",
|
||||||
|
general: "⚡ General",
|
||||||
|
harry_potter: "⚡ Harry Potter"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const UI_TEXT = {
|
const UI_TEXT = {
|
||||||
de: {
|
de: {
|
||||||
pageTitle: "✨ Magisches Hogwarts-Escape-Quiz ✨",
|
pageTitle: "Harry Potter Wissensquiz",
|
||||||
pageSubtitle: "10 knifflige Rätsel aus der Zauberwelt warten auf dich",
|
pageSubtitle: "Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.",
|
||||||
envelopeLetter: "Hogwarts-Schule für Hexerei und Zauberei",
|
diffLabel: "Schwierigkeitsgrad",
|
||||||
introDesc: "Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?",
|
diffAll: "Alle Stufen",
|
||||||
diffLabel: "Schwierigkeitsgrad wählen:",
|
|
||||||
diffAll: "Alle gemischt",
|
|
||||||
diffEasy: "Leicht",
|
diffEasy: "Leicht",
|
||||||
diffMed: "Mittel",
|
diffMed: "Mittel",
|
||||||
diffHard: "Schwer",
|
diffHard: "Schwer (Experte)",
|
||||||
startBtn: "📜 Siegel brechen & Brief öffnen",
|
catLabel: "Themengebiet wählen",
|
||||||
startBtnCasting: "✨ Zauber wird gewirkt...",
|
catAll: "Alle Kategorien",
|
||||||
pointsLabel: "🏆 Hauspunkte",
|
catBooks: "Buchwissen",
|
||||||
|
catMovies: "Filmwissen",
|
||||||
|
catCreatures: "Magische Geschöpfe",
|
||||||
|
catSpells: "Zaubersprüche & Tränke",
|
||||||
|
catChars: "Charaktere",
|
||||||
|
catHogwarts: "Hogwarts & Orte",
|
||||||
|
catQuidditch: "Quidditch",
|
||||||
|
catLore: "Artefakte & Lore",
|
||||||
|
dashboardRules: "<span>🎯 10 Fragen pro Runde</span><span>·</span><span>🔥 Streak-Zähler</span><span>·</span><span>💡 Bis zu 3 Versuche je Frage</span>",
|
||||||
|
startQuizBtn: "⚡ Quiz jetzt starten ➔",
|
||||||
|
startBtnCasting: "⚡ Runde wird geladen...",
|
||||||
|
pointsLabel: "Pkt.",
|
||||||
questionLabel: "Frage",
|
questionLabel: "Frage",
|
||||||
ofLabel: "von",
|
ofLabel: "/",
|
||||||
attemptLabel: "Versuch",
|
attemptLabel: "Versuch",
|
||||||
checkBtn: "✨ Antwort prüfen",
|
checkBtn: "✓ Antwort prüfen",
|
||||||
hintBtn: "💡 Hinweis anzeigen",
|
hintBtn: "💡 Tipp anzeigen",
|
||||||
hintHeading: "📜 Notiz aus der verbotenen Abteilung:",
|
hintHeading: "💡 Hinweis:",
|
||||||
placeholder: "Deine Antwort hier eingeben...",
|
placeholder: "Deine Antwort eingeben...",
|
||||||
noHint: "Kein Tipp vorhanden.",
|
noHint: "Kein Tipp vorhanden.",
|
||||||
hintFailed: "Tipp konnte nicht geladen werden.",
|
hintFailed: "Tipp konnte nicht geladen werden.",
|
||||||
correctFeedback: "✨ Richtig! 1 Hauspunkt für dein Haus! ✨",
|
wrongFeedback: "Nicht ganz! Noch {attempts} Versuch(e).",
|
||||||
wrongFeedback: "Leider falsch! Noch {attempts} Versuch(e).",
|
exhaustedWrong: "Leider nicht gelöst (3 Fehlversuche).",
|
||||||
exhaustedWrong: "Leider nicht richtig (3 Fehlversuche).",
|
solutionLabel: "Richtige Antwort:",
|
||||||
solutionLabel: "Richtige Lösung:",
|
|
||||||
startError: "Fehler beim Starten des Quiz: ",
|
startError: "Fehler beim Starten des Quiz: ",
|
||||||
networkError: "Netzwerkfehler: "
|
networkError: "Netzwerkfehler: "
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
pageTitle: "✨ Magical Hogwarts Escape Quiz ✨",
|
pageTitle: "Harry Potter Trivia Quiz",
|
||||||
pageSubtitle: "10 tricky wizarding world puzzles await you",
|
pageSubtitle: "Test your knowledge across books, movies, creatures, and spells — straight to the action.",
|
||||||
envelopeLetter: "Hogwarts School of Witchcraft and Wizardry",
|
diffLabel: "Difficulty Level",
|
||||||
introDesc: "An enchanted letter from Hogwarts is floating in the air before you.<br>Are you ready to take the exams and secure House Points?",
|
diffAll: "All Levels",
|
||||||
diffLabel: "Choose difficulty level:",
|
|
||||||
diffAll: "All mixed",
|
|
||||||
diffEasy: "Easy",
|
diffEasy: "Easy",
|
||||||
diffMed: "Medium",
|
diffMed: "Medium",
|
||||||
diffHard: "Hard",
|
diffHard: "Hard (Expert)",
|
||||||
startBtn: "📜 Break Seal & Open Letter",
|
catLabel: "Select Topic",
|
||||||
startBtnCasting: "✨ Casting spell...",
|
catAll: "All Categories",
|
||||||
pointsLabel: "🏆 House Points",
|
catBooks: "Book Lore",
|
||||||
|
catMovies: "Movie Trivia",
|
||||||
|
catCreatures: "Magical Creatures",
|
||||||
|
catSpells: "Spells & Potions",
|
||||||
|
catChars: "Characters",
|
||||||
|
catHogwarts: "Hogwarts & Places",
|
||||||
|
catQuidditch: "Quidditch",
|
||||||
|
catLore: "Artifacts & Lore",
|
||||||
|
dashboardRules: "<span>🎯 10 Questions per Round</span><span>·</span><span>🔥 Answer Streak</span><span>·</span><span>💡 Up to 3 Attempts</span>",
|
||||||
|
startQuizBtn: "⚡ Start Quiz Now ➔",
|
||||||
|
startBtnCasting: "⚡ Loading Round...",
|
||||||
|
pointsLabel: "Pts",
|
||||||
questionLabel: "Question",
|
questionLabel: "Question",
|
||||||
ofLabel: "of",
|
ofLabel: "/",
|
||||||
attemptLabel: "Attempt",
|
attemptLabel: "Attempt",
|
||||||
checkBtn: "✨ Check Answer",
|
checkBtn: "✓ Check Answer",
|
||||||
hintBtn: "💡 Show Hint",
|
hintBtn: "💡 Show Hint",
|
||||||
hintHeading: "📜 Note from the Restricted Section:",
|
hintHeading: "💡 Hint:",
|
||||||
placeholder: "Type your answer here...",
|
placeholder: "Type your answer...",
|
||||||
noHint: "No hint available.",
|
noHint: "No hint available.",
|
||||||
hintFailed: "Could not load hint.",
|
hintFailed: "Could not load hint.",
|
||||||
correctFeedback: "✨ Correct! 1 House Point for your House! ✨",
|
wrongFeedback: "Not quite! {attempts} attempt(s) left.",
|
||||||
wrongFeedback: "Incorrect! {attempts} attempt(s) remaining.",
|
exhaustedWrong: "Out of attempts (3 failed tries).",
|
||||||
exhaustedWrong: "Unfortunately incorrect (3 failed attempts).",
|
solutionLabel: "Correct answer:",
|
||||||
solutionLabel: "Correct solution:",
|
|
||||||
startError: "Error starting quiz: ",
|
startError: "Error starting quiz: ",
|
||||||
networkError: "Network error: "
|
networkError: "Network error: "
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Web Audio API Sound Generator for magic chimes and buzzers
|
// Web Audio API Sound Generator for crisp trivia feedback
|
||||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
|
||||||
function playSound(type) {
|
function playSound(type) {
|
||||||
@@ -79,33 +126,31 @@ function playSound(type) {
|
|||||||
audioCtx.resume();
|
audioCtx.resume();
|
||||||
}
|
}
|
||||||
if (type === 'magic') {
|
if (type === 'magic') {
|
||||||
// Magical bright arpeggio chime
|
|
||||||
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
|
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
|
||||||
notes.forEach((freq, index) => {
|
notes.forEach((freq, index) => {
|
||||||
const osc = audioCtx.createOscillator();
|
const osc = audioCtx.createOscillator();
|
||||||
const gain = audioCtx.createGain();
|
const gain = audioCtx.createGain();
|
||||||
osc.type = 'sine';
|
osc.type = 'sine';
|
||||||
osc.frequency.value = freq;
|
osc.frequency.value = freq;
|
||||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime + index * 0.08);
|
gain.gain.setValueAtTime(0.12, audioCtx.currentTime + index * 0.07);
|
||||||
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.08 + 0.35);
|
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + index * 0.07 + 0.3);
|
||||||
osc.connect(gain);
|
osc.connect(gain);
|
||||||
gain.connect(audioCtx.destination);
|
gain.connect(audioCtx.destination);
|
||||||
osc.start(audioCtx.currentTime + index * 0.08);
|
osc.start(audioCtx.currentTime + index * 0.07);
|
||||||
osc.stop(audioCtx.currentTime + index * 0.08 + 0.35);
|
osc.stop(audioCtx.currentTime + index * 0.07 + 0.3);
|
||||||
});
|
});
|
||||||
} else if (type === 'wrong') {
|
} else if (type === 'wrong') {
|
||||||
// Soft error buzz
|
|
||||||
const osc = audioCtx.createOscillator();
|
const osc = audioCtx.createOscillator();
|
||||||
const gain = audioCtx.createGain();
|
const gain = audioCtx.createGain();
|
||||||
osc.type = 'sawtooth';
|
osc.type = 'triangle';
|
||||||
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
|
osc.frequency.setValueAtTime(180, audioCtx.currentTime);
|
||||||
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.25);
|
osc.frequency.exponentialRampToValueAtTime(95, audioCtx.currentTime + 0.22);
|
||||||
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
|
||||||
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.25);
|
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.22);
|
||||||
osc.connect(gain);
|
osc.connect(gain);
|
||||||
gain.connect(audioCtx.destination);
|
gain.connect(audioCtx.destination);
|
||||||
osc.start();
|
osc.start();
|
||||||
osc.stop(audioCtx.currentTime + 0.25);
|
osc.stop(audioCtx.currentTime + 0.22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,42 +161,66 @@ function selectDifficulty(diff) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectCategory(cat) {
|
||||||
|
selectedCategory = cat;
|
||||||
|
document.querySelectorAll(".cat-chip").forEach(chip => {
|
||||||
|
chip.classList.toggle("active", chip.getAttribute("data-cat") === cat);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStreakUI(animate = false) {
|
||||||
|
const badge = document.getElementById("streakBadge");
|
||||||
|
if (!badge) return;
|
||||||
|
badge.innerText = currentStreak >= 2 ? `🔥 ${currentStreak} Streak` : `🔥 ${currentStreak}`;
|
||||||
|
badge.classList.toggle("streak-hot", currentStreak >= 2);
|
||||||
|
if (animate) {
|
||||||
|
badge.classList.remove("streak-pop");
|
||||||
|
void badge.offsetWidth;
|
||||||
|
badge.classList.add("streak-pop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onLanguageChange(lang) {
|
function onLanguageChange(lang) {
|
||||||
applyQuizTranslations(lang);
|
applyQuizTranslations(lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyQuizTranslations(lang) {
|
function applyQuizTranslations(lang) {
|
||||||
const t = UI_TEXT[lang] || UI_TEXT.de;
|
const t = UI_TEXT[lang] || UI_TEXT.de;
|
||||||
const pageTitle = document.getElementById("pageTitle");
|
const setTxt = (id, val) => {
|
||||||
const pageSubtitle = document.getElementById("pageSubtitle");
|
const el = document.getElementById(id);
|
||||||
const envelopeLetter = document.getElementById("envelopeLetter");
|
if (el && val) el.innerText = val;
|
||||||
const introDesc = document.getElementById("introDesc");
|
};
|
||||||
const diffLabel = document.getElementById("diffLabel");
|
|
||||||
const diffAllText = document.getElementById("diffAllText");
|
|
||||||
const diffEasyText = document.getElementById("diffEasyText");
|
|
||||||
const diffMedText = document.getElementById("diffMedText");
|
|
||||||
const diffHardText = document.getElementById("diffHardText");
|
|
||||||
const startBtn = document.getElementById("startBtn");
|
|
||||||
const checkBtn = document.getElementById("checkBtn");
|
|
||||||
const hintBtn = document.getElementById("hintBtn");
|
|
||||||
const hintBtnMC = document.getElementById("hintBtnMC");
|
|
||||||
const hintHeading = document.getElementById("hintHeading");
|
|
||||||
const answerInput = document.getElementById("answerInput");
|
|
||||||
|
|
||||||
if (pageTitle) pageTitle.innerText = t.pageTitle;
|
setTxt("pageTitle", t.pageTitle);
|
||||||
if (pageSubtitle) pageSubtitle.innerText = t.pageSubtitle;
|
setTxt("pageSubtitle", t.pageSubtitle);
|
||||||
if (envelopeLetter) envelopeLetter.innerText = t.envelopeLetter;
|
setTxt("diffLabel", t.diffLabel);
|
||||||
if (introDesc) introDesc.innerHTML = t.introDesc;
|
setTxt("diffAllText", t.diffAll);
|
||||||
if (diffLabel) diffLabel.innerText = t.diffLabel;
|
setTxt("diffEasyText", t.diffEasy);
|
||||||
if (diffAllText) diffAllText.innerText = t.diffAll;
|
setTxt("diffMedText", t.diffMed);
|
||||||
if (diffEasyText) diffEasyText.innerText = t.diffEasy;
|
setTxt("diffHardText", t.diffHard);
|
||||||
if (diffMedText) diffMedText.innerText = t.diffMed;
|
setTxt("catLabel", t.catLabel);
|
||||||
if (diffHardText) diffHardText.innerText = t.diffHard;
|
setTxt("catAllText", t.catAll);
|
||||||
if (startBtn && !startBtn.disabled) startBtn.innerText = t.startBtn;
|
setTxt("catBooksText", t.catBooks);
|
||||||
if (checkBtn) checkBtn.innerText = t.checkBtn;
|
setTxt("catMoviesText", t.catMovies);
|
||||||
if (hintBtn) hintBtn.innerText = t.hintBtn;
|
setTxt("catCreaturesText", t.catCreatures);
|
||||||
if (hintBtnMC) hintBtnMC.innerText = t.hintBtn;
|
setTxt("catSpellsText", t.catSpells);
|
||||||
if (hintHeading) hintHeading.innerText = t.hintHeading;
|
setTxt("catCharsText", t.catChars);
|
||||||
|
setTxt("catHogwartsText", t.catHogwarts);
|
||||||
|
setTxt("catQuidditchText", t.catQuidditch);
|
||||||
|
setTxt("catLoreText", t.catLore);
|
||||||
|
|
||||||
|
const rulesEl = document.getElementById("dashboardRules");
|
||||||
|
if (rulesEl) rulesEl.innerHTML = t.dashboardRules;
|
||||||
|
|
||||||
|
const startQuizBtn = document.getElementById("startQuizBtn");
|
||||||
|
if (startQuizBtn && !startQuizBtn.disabled) startQuizBtn.innerText = t.startQuizBtn;
|
||||||
|
|
||||||
|
setTxt("checkBtn", t.checkBtn);
|
||||||
|
setTxt("hintBtn", t.hintBtn);
|
||||||
|
setTxt("hintBtnMC", t.hintBtn);
|
||||||
|
setTxt("hintHeading", t.hintHeading);
|
||||||
|
|
||||||
|
const answerInput = document.getElementById("answerInput");
|
||||||
if (answerInput) answerInput.placeholder = t.placeholder;
|
if (answerInput) answerInput.placeholder = t.placeholder;
|
||||||
|
|
||||||
if (lastQuestionData) {
|
if (lastQuestionData) {
|
||||||
@@ -159,36 +228,85 @@ function applyQuizTranslations(lang) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sessionQuestionIds = [];
|
||||||
|
|
||||||
|
function getRecentQuestionIds() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("quiz_recent_qids");
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch(e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWrongQuestionIds() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("quiz_wrong_qids");
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch(e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordQuestionWrong(qid) {
|
||||||
|
if (!qid) return;
|
||||||
|
try {
|
||||||
|
const wrong = getWrongQuestionIds();
|
||||||
|
if (!wrong.includes(qid)) {
|
||||||
|
wrong.push(qid);
|
||||||
|
if (wrong.length > 30) wrong.shift();
|
||||||
|
localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong));
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordQuestionMastered(qid) {
|
||||||
|
if (!qid) return;
|
||||||
|
try {
|
||||||
|
let wrong = getWrongQuestionIds();
|
||||||
|
if (wrong.includes(qid)) {
|
||||||
|
wrong = wrong.filter(id => id !== qid);
|
||||||
|
localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong));
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecentQuestions(ids) {
|
||||||
|
if (!ids || ids.length === 0) return;
|
||||||
|
try {
|
||||||
|
let recent = getRecentQuestionIds();
|
||||||
|
recent = Array.from(new Set([...ids, ...recent])).slice(0, 20);
|
||||||
|
localStorage.setItem("quiz_recent_qids", JSON.stringify(recent));
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
async function startGame() {
|
async function startGame() {
|
||||||
const envelope = document.getElementById("envelope");
|
const startQuizBtn = document.getElementById("startQuizBtn");
|
||||||
const startBtn = document.getElementById("startBtn");
|
|
||||||
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
||||||
|
|
||||||
startBtn.disabled = true;
|
if (startQuizBtn) {
|
||||||
startBtn.innerText = t.startBtnCasting;
|
startQuizBtn.disabled = true;
|
||||||
envelope.classList.add("envelope-opened");
|
startQuizBtn.innerText = t.startBtnCasting;
|
||||||
|
|
||||||
// Start background music if enabled or first click
|
|
||||||
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(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playSound('magic');
|
||||||
|
sessionQuestionIds = [];
|
||||||
|
currentStreak = 0;
|
||||||
|
updateStreakUI(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const recentIds = getRecentQuestionIds();
|
||||||
|
const wrongIds = getWrongQuestionIds();
|
||||||
|
|
||||||
const res = await fetch("/api/game/start", {
|
const res = await fetch("/api/game/start", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
lang: currentLang,
|
lang: currentLang,
|
||||||
difficulty: selectedDifficulty
|
difficulty: selectedDifficulty,
|
||||||
|
category: selectedCategory,
|
||||||
|
recent_ids: recentIds,
|
||||||
|
wrong_ids: wrongIds
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,18 +321,20 @@ async function startGame() {
|
|||||||
currentScore = data.score;
|
currentScore = data.score;
|
||||||
attemptsUsed = data.attempts_used;
|
attemptsUsed = data.attempts_used;
|
||||||
|
|
||||||
setTimeout(() => {
|
const introCard = document.getElementById("introCard");
|
||||||
document.getElementById("introCard").classList.add("hidden");
|
if (introCard) {
|
||||||
document.getElementById("gameCard").classList.remove("hidden");
|
introCard.classList.add("hidden");
|
||||||
startTimer();
|
}
|
||||||
renderQuestion(data);
|
document.getElementById("gameCard").classList.remove("hidden");
|
||||||
}, 1100);
|
startTimer();
|
||||||
|
renderQuestion(data);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(t.startError + err.message);
|
alert(t.startError + err.message);
|
||||||
startBtn.disabled = false;
|
if (startQuizBtn) {
|
||||||
startBtn.innerText = t.startBtn;
|
startQuizBtn.disabled = false;
|
||||||
envelope.classList.remove("envelope-opened");
|
startQuizBtn.innerText = t.startQuizBtn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,13 +359,18 @@ function updateTimer() {
|
|||||||
|
|
||||||
function renderQuestion(qData) {
|
function renderQuestion(qData) {
|
||||||
lastQuestionData = qData;
|
lastQuestionData = qData;
|
||||||
|
if (qData.id && !sessionQuestionIds.includes(qData.id)) {
|
||||||
|
sessionQuestionIds.push(qData.id);
|
||||||
|
}
|
||||||
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
||||||
|
const catMap = CATEGORY_LABELS[currentLang] || CATEGORY_LABELS.de;
|
||||||
|
|
||||||
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
|
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
|
||||||
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
|
document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`;
|
||||||
document.getElementById("badgeCategory").innerText = `⚡ ${qData.category.replace("_", " ").toUpperCase()}`;
|
|
||||||
|
const rawCat = (qData.category || "general").toLowerCase();
|
||||||
|
document.getElementById("badgeCategory").innerText = catMap[rawCat] || `⚡ ${rawCat.toUpperCase()}`;
|
||||||
|
|
||||||
// Difficulty badge
|
|
||||||
const badgeDiff = document.getElementById("badgeDifficulty");
|
const badgeDiff = document.getElementById("badgeDifficulty");
|
||||||
if (badgeDiff) {
|
if (badgeDiff) {
|
||||||
const diff = qData.difficulty || "medium";
|
const diff = qData.difficulty || "medium";
|
||||||
@@ -256,7 +381,7 @@ function renderQuestion(qData) {
|
|||||||
diffText = t.diffEasy;
|
diffText = t.diffEasy;
|
||||||
diffIcon = "🟢";
|
diffIcon = "🟢";
|
||||||
} else if (diff === "hard") {
|
} else if (diff === "hard") {
|
||||||
diffText = t.diffHard;
|
diffText = currentLang === "en" ? "Hard" : "Schwer";
|
||||||
diffIcon = "🔴";
|
diffIcon = "🔴";
|
||||||
}
|
}
|
||||||
badgeDiff.innerText = `${diffIcon} ${diffText}`;
|
badgeDiff.innerText = `${diffIcon} ${diffText}`;
|
||||||
@@ -328,7 +453,6 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
if (checkBtn) checkBtn.disabled = true;
|
if (checkBtn) checkBtn.disabled = true;
|
||||||
if (input) input.disabled = true;
|
if (input) input.disabled = true;
|
||||||
|
|
||||||
// Disable all choice buttons
|
|
||||||
const choiceBtns = document.querySelectorAll(".choice-btn");
|
const choiceBtns = document.querySelectorAll(".choice-btn");
|
||||||
choiceBtns.forEach(b => b.disabled = true);
|
choiceBtns.forEach(b => b.disabled = true);
|
||||||
if (clickedBtn) {
|
if (clickedBtn) {
|
||||||
@@ -351,20 +475,32 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
|
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
currentScore = result.score;
|
currentScore = result.score;
|
||||||
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
|
document.getElementById("pointsCounter").innerText = `🏆 ${currentScore} ${t.pointsLabel}`;
|
||||||
|
|
||||||
const frageBox = document.getElementById("frageBox");
|
const frageBox = document.getElementById("frageBox");
|
||||||
const feedbackBox = document.getElementById("feedbackBox");
|
const feedbackBox = document.getElementById("feedbackBox");
|
||||||
|
|
||||||
if (result.correct) {
|
if (result.correct) {
|
||||||
// Success!
|
|
||||||
playSound('magic');
|
playSound('magic');
|
||||||
|
currentStreak += 1;
|
||||||
|
updateStreakUI(true);
|
||||||
|
|
||||||
if (clickedBtn) {
|
if (clickedBtn) {
|
||||||
clickedBtn.classList.remove("choice-selected");
|
clickedBtn.classList.remove("choice-selected");
|
||||||
clickedBtn.classList.add("choice-correct");
|
clickedBtn.classList.add("choice-correct");
|
||||||
}
|
}
|
||||||
frageBox.classList.add("sparkle-effect");
|
frageBox.classList.add("sparkle-effect");
|
||||||
feedbackBox.innerHTML = `<span class="feedback-correct">${t.correctFeedback}</span>`;
|
|
||||||
|
if (result.question_id) {
|
||||||
|
recordQuestionMastered(result.question_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pts = result.points_earned || 100;
|
||||||
|
const streakBonusText = currentStreak >= 3 ? ` · 🔥 ${currentStreak}er Serie!` : "";
|
||||||
|
const ptsText = (currentLang === "en")
|
||||||
|
? `✓ Correct! +${pts} Points${currentStreak >= 3 ? ` · 🔥 ${currentStreak} Streak!` : ""}`
|
||||||
|
: `✓ Richtig! +${pts} Punkte${streakBonusText}`;
|
||||||
|
feedbackBox.innerHTML = `<span class="feedback-correct">${ptsText}</span>`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
frageBox.classList.remove("sparkle-effect");
|
frageBox.classList.remove("sparkle-effect");
|
||||||
@@ -375,11 +511,13 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
attemptsUsed = 0;
|
attemptsUsed = 0;
|
||||||
renderQuestion(result.next_question);
|
renderQuestion(result.next_question);
|
||||||
}
|
}
|
||||||
}, 1200);
|
}, 1100);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Incorrect
|
|
||||||
playSound('wrong');
|
playSound('wrong');
|
||||||
|
currentStreak = 0;
|
||||||
|
updateStreakUI(false);
|
||||||
|
|
||||||
if (clickedBtn) {
|
if (clickedBtn) {
|
||||||
clickedBtn.classList.remove("choice-selected");
|
clickedBtn.classList.remove("choice-selected");
|
||||||
clickedBtn.classList.add("choice-wrong");
|
clickedBtn.classList.add("choice-wrong");
|
||||||
@@ -391,7 +529,10 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
|
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
|
||||||
|
|
||||||
if (result.attempts_left === 0) {
|
if (result.attempts_left === 0) {
|
||||||
// Exhausted all attempts
|
if (result.question_id) {
|
||||||
|
recordQuestionWrong(result.question_id);
|
||||||
|
}
|
||||||
|
|
||||||
feedbackBox.innerHTML = `
|
feedbackBox.innerHTML = `
|
||||||
<div class="feedback-wrong">${t.exhaustedWrong}</div>
|
<div class="feedback-wrong">${t.exhaustedWrong}</div>
|
||||||
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
|
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
|
||||||
@@ -408,7 +549,6 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
}, 2200);
|
}, 2200);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Still attempts remaining
|
|
||||||
const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left);
|
const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left);
|
||||||
feedbackBox.innerHTML = `<span class="feedback-wrong">${msg}</span>`;
|
feedbackBox.innerHTML = `<span class="feedback-wrong">${msg}</span>`;
|
||||||
if (checkBtn) checkBtn.disabled = false;
|
if (checkBtn) checkBtn.disabled = false;
|
||||||
@@ -416,7 +556,6 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
input.disabled = false;
|
input.disabled = false;
|
||||||
input.select();
|
input.select();
|
||||||
}
|
}
|
||||||
// Re-enable choice buttons
|
|
||||||
choiceBtns.forEach(b => b.disabled = false);
|
choiceBtns.forEach(b => b.disabled = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,11 +587,11 @@ async function requestHint() {
|
|||||||
|
|
||||||
function finishGame() {
|
function finishGame() {
|
||||||
if (timerInterval) clearInterval(timerInterval);
|
if (timerInterval) clearInterval(timerInterval);
|
||||||
|
saveRecentQuestions(sessionQuestionIds);
|
||||||
document.getElementById("progressBar").style.width = "100%";
|
document.getElementById("progressBar").style.width = "100%";
|
||||||
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}`;
|
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}&diff=${encodeURIComponent(selectedDifficulty)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter key to submit
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const input = document.getElementById("answerInput");
|
const input = document.getElementById("answerInput");
|
||||||
if (input) {
|
if (input) {
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
const SUGGESTION_TEXT = {
|
const SUGGESTION_TEXT = {
|
||||||
de: {
|
de: {
|
||||||
title: "✨ Neue Frage für das Quiz vorschlagen ✨",
|
title: "✍️ Neue Quiz-Frage einreichen",
|
||||||
subtitle: "Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!",
|
subtitle: "Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!",
|
||||||
alert: "ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status <em>„Prüfung ausstehend“</em>. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird.",
|
alert: "ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.",
|
||||||
nameLabel: "Dein Name / Zauberer-Alias (optional):",
|
nameLabel: "Dein Name / Alias (optional):",
|
||||||
namePlaceholder: "z. B. Hermine Granger",
|
namePlaceholder: "z. B. Hermine Granger",
|
||||||
|
catLabel: "Kategorie / Themenbereich:",
|
||||||
|
catBooks: "📖 Buchwissen (Buch-Details)",
|
||||||
|
catMovies: "🎬 Filmwissen (Kino & Drehorte)",
|
||||||
|
catHogwarts: "🏰 Hogwarts & Orte",
|
||||||
|
catCharacters: "🧙♂️ Charaktere & Personen",
|
||||||
|
catSpells: "🪄 Zaubersprüche & Tränke",
|
||||||
|
catCreatures: "🐉 Magische Geschöpfe",
|
||||||
|
catQuidditch: "🧹 Quidditch & Sport",
|
||||||
|
catLore: "🔮 Artefakte & Geschichte",
|
||||||
|
catGeneral: "⚡ Allgemeines Wissen",
|
||||||
frageLabel: "Deine Frage: *",
|
frageLabel: "Deine Frage: *",
|
||||||
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
|
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
|
||||||
diffLabel: "Schwierigkeitsgrad:",
|
diffLabel: "Schwierigkeitsgrad:",
|
||||||
@@ -28,11 +38,21 @@ const SUGGESTION_TEXT = {
|
|||||||
successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨"
|
successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨"
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
title: "✨ Suggest a New Quiz Question ✨",
|
title: "✍️ Submit a New Quiz Question",
|
||||||
subtitle: "Enrich the magical quiz with tricky Harry Potter or escape puzzles!",
|
subtitle: "Enrich PotterTrivia with your own questions about the books, movies, or magical creatures!",
|
||||||
alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are not immediately public; they automatically receive the status <em>'Pending Review'</em>. The Ministry of Magic tests and verifies the answer before approval.",
|
alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are briefly reviewed for accuracy before going live in the public quiz pool.",
|
||||||
nameLabel: "Your Name / Wizard Alias (optional):",
|
nameLabel: "Your Name / Alias (optional):",
|
||||||
namePlaceholder: "e.g. Hermione Granger",
|
namePlaceholder: "e.g. Hermione Granger",
|
||||||
|
catLabel: "Category / Topic Area:",
|
||||||
|
catBooks: "📖 Book Lore (Book Details)",
|
||||||
|
catMovies: "🎬 Movie Trivia (Cinema & Filming)",
|
||||||
|
catHogwarts: "🏰 Hogwarts & Places",
|
||||||
|
catCharacters: "🧙♂️ Characters & People",
|
||||||
|
catSpells: "🪄 Spells & Potions",
|
||||||
|
catCreatures: "🐉 Magical Creatures",
|
||||||
|
catQuidditch: "🧹 Quidditch & Sport",
|
||||||
|
catLore: "🔮 Artifacts & Lore",
|
||||||
|
catGeneral: "⚡ General Knowledge",
|
||||||
frageLabel: "Your Question: *",
|
frageLabel: "Your Question: *",
|
||||||
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
|
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
|
||||||
diffLabel: "Difficulty Level:",
|
diffLabel: "Difficulty Level:",
|
||||||
@@ -75,11 +95,32 @@ function onLanguageChange(lang) {
|
|||||||
const tippInput = document.getElementById("tipp");
|
const tippInput = document.getElementById("tipp");
|
||||||
const submitBtn = document.getElementById("submitBtn");
|
const submitBtn = document.getElementById("submitBtn");
|
||||||
|
|
||||||
|
const catLabel = document.getElementById("suggCatLabel");
|
||||||
|
const catBooks = document.getElementById("catBooks");
|
||||||
|
const catMovies = document.getElementById("catMovies");
|
||||||
|
const catHogwarts = document.getElementById("catHogwarts");
|
||||||
|
const catCharacters = document.getElementById("catCharacters");
|
||||||
|
const catSpells = document.getElementById("catSpells");
|
||||||
|
const catCreatures = document.getElementById("catCreatures");
|
||||||
|
const catQuidditch = document.getElementById("catQuidditch");
|
||||||
|
const catLore = document.getElementById("catLore");
|
||||||
|
const catGeneral = document.getElementById("catGeneral");
|
||||||
|
|
||||||
if (title) title.innerText = t.title;
|
if (title) title.innerText = t.title;
|
||||||
if (subtitle) subtitle.innerText = t.subtitle;
|
if (subtitle) subtitle.innerText = t.subtitle;
|
||||||
if (alertBox) alertBox.innerHTML = t.alert;
|
if (alertBox) alertBox.innerHTML = t.alert;
|
||||||
if (nameLabel) nameLabel.innerText = t.nameLabel;
|
if (nameLabel) nameLabel.innerText = t.nameLabel;
|
||||||
if (nameInput) nameInput.placeholder = t.namePlaceholder;
|
if (nameInput) nameInput.placeholder = t.namePlaceholder;
|
||||||
|
if (catLabel) catLabel.innerText = t.catLabel;
|
||||||
|
if (catBooks) catBooks.innerText = t.catBooks;
|
||||||
|
if (catMovies) catMovies.innerText = t.catMovies;
|
||||||
|
if (catHogwarts) catHogwarts.innerText = t.catHogwarts;
|
||||||
|
if (catCharacters) catCharacters.innerText = t.catCharacters;
|
||||||
|
if (catSpells) catSpells.innerText = t.catSpells;
|
||||||
|
if (catCreatures) catCreatures.innerText = t.catCreatures;
|
||||||
|
if (catQuidditch) catQuidditch.innerText = t.catQuidditch;
|
||||||
|
if (catLore) catLore.innerText = t.catLore;
|
||||||
|
if (catGeneral) catGeneral.innerText = t.catGeneral;
|
||||||
if (frageLabel) frageLabel.innerText = t.frageLabel;
|
if (frageLabel) frageLabel.innerText = t.frageLabel;
|
||||||
if (frageInput) frageInput.placeholder = t.fragePlaceholder;
|
if (frageInput) frageInput.placeholder = t.fragePlaceholder;
|
||||||
if (diffLabel) diffLabel.innerText = t.diffLabel;
|
if (diffLabel) diffLabel.innerText = t.diffLabel;
|
||||||
@@ -151,8 +192,12 @@ async function submitSuggestion(event) {
|
|||||||
submitBtn.innerText = t.submitBtnCasting;
|
submitBtn.innerText = t.submitBtnCasting;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const catElem = document.getElementById("kategorie");
|
||||||
|
const category = catElem ? catElem.value : "general";
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: name || "Gast",
|
name: name || "Gast",
|
||||||
|
category: category,
|
||||||
frage: frage,
|
frage: frage,
|
||||||
antwort: antwort,
|
antwort: antwort,
|
||||||
tipp: tipp || null,
|
tipp: tipp || null,
|
||||||
|
|||||||
@@ -225,9 +225,15 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="category">Kategorie:</label>
|
<label for="category">Kategorie:</label>
|
||||||
<select name="category" id="category" class="input-text">
|
<select name="category" id="category" class="input-text">
|
||||||
<option value="harry_potter" selected>Harry Potter / Escape</option>
|
<option value="books">📖 Buchwissen</option>
|
||||||
<option value="insider">Persönlich / Insider</option>
|
<option value="movies">🎬 Filmwissen</option>
|
||||||
<option value="general">Allgemeines Rätsel</option>
|
<option value="creatures">🐉 Magische Geschöpfe</option>
|
||||||
|
<option value="spells">🪄 Zaubersprüche & Tränke</option>
|
||||||
|
<option value="characters" selected>🧙♂️ Charaktere</option>
|
||||||
|
<option value="hogwarts">🏰 Hogwarts & Orte</option>
|
||||||
|
<option value="quidditch">🧹 Quidditch</option>
|
||||||
|
<option value="lore">🔮 Artefakte & Lore</option>
|
||||||
|
<option value="general">⚡ Allgemein</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -309,5 +315,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/admin.js"></script>
|
<script src="/static/js/admin.js?v=2.3"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+20
-65
@@ -3,43 +3,39 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
||||||
<title>{% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %}</title>
|
<title>{% block title %}PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %}</title>
|
||||||
<link rel="icon" href="/static/favicon.ico">
|
<link rel="icon" href="/static/favicon.ico">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css?v=4.0">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<audio id="bgm" src="/static/audio/music.mp3" loop preload="auto"></audio>
|
|
||||||
|
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="header-inner">
|
<div class="header-inner">
|
||||||
<a href="/" class="brand-link">
|
<a href="/" class="brand-link">
|
||||||
<span class="brand-sparkle">⚡</span>
|
<span class="brand-badge">⚡</span>
|
||||||
<span class="brand-title">Hogwarts Quiz</span>
|
<span class="brand-title">Potter<span class="brand-accent">Trivia</span></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<nav id="navLinks" class="nav-links">
|
||||||
|
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">🎯 Quiz</a>
|
||||||
|
<a href="/bestenliste" class="nav-link" id="navLeaderboard" onclick="closeMobileMenu()">🏆 Bestenliste</a>
|
||||||
|
<a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
|
||||||
|
<a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">⚙️ Admin</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="lang-switch-group" role="group" aria-label="Sprachauswahl / Language">
|
<div class="lang-switch-group" role="group" aria-label="Sprachauswahl / Language">
|
||||||
<button type="button" id="langDe" class="lang-btn active" onclick="setLanguage('de')" title="Deutsch">🇩🇪 DE</button>
|
<button type="button" id="langDe" class="lang-btn active" onclick="setLanguage('de')" title="Deutsch">🇩🇪 DE</button>
|
||||||
<button type="button" id="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button>
|
<button type="button" id="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="audioToggle" class="audio-btn" title="Musik an/aus" onclick="toggleAudio()">
|
|
||||||
<span id="audioIcon">🔇</span>
|
|
||||||
<span id="audioText" class="audio-label">Musik: Aus</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="menuToggle" class="menu-toggle-btn" aria-label="Menü öffnen" onclick="toggleMobileMenu()">
|
<button id="menuToggle" class="menu-toggle-btn" aria-label="Menü öffnen" onclick="toggleMobileMenu()">
|
||||||
<span id="menuIcon">☰</span>
|
<span id="menuIcon">☰</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav id="navLinks" class="nav-links">
|
|
||||||
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">✨ Zum Quiz</a>
|
|
||||||
<a href="/bestenliste" class="nav-link" id="navLeaderboard" onclick="closeMobileMenu()">🏆 Bestenliste</a>
|
|
||||||
<a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
|
|
||||||
<a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">🔒 Admin-Cockpit</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -48,7 +44,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p id="footerText">⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p>
|
<p id="footerText">⚡ PotterTrivia · Das unabhängige Harry-Potter-Wissensquiz</p>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/impressum" class="footer-link" id="footerImpressum">⚖️ Impressum</a>
|
<a href="/impressum" class="footer-link" id="footerImpressum">⚖️ Impressum</a>
|
||||||
<span class="footer-sep">·</span>
|
<span class="footer-sep">·</span>
|
||||||
@@ -57,9 +53,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const bgm = document.getElementById("bgm");
|
|
||||||
const audioIcon = document.getElementById("audioIcon");
|
|
||||||
const audioText = document.getElementById("audioText");
|
|
||||||
const navLinks = document.getElementById("navLinks");
|
const navLinks = document.getElementById("navLinks");
|
||||||
const menuIcon = document.getElementById("menuIcon");
|
const menuIcon = document.getElementById("menuIcon");
|
||||||
|
|
||||||
@@ -92,43 +85,17 @@
|
|||||||
const footerImpressum = document.getElementById("footerImpressum");
|
const footerImpressum = document.getElementById("footerImpressum");
|
||||||
const footerDatenschutz = document.getElementById("footerDatenschutz");
|
const footerDatenschutz = document.getElementById("footerDatenschutz");
|
||||||
|
|
||||||
if (navQuiz) navQuiz.innerText = isDe ? "✨ Zum Quiz" : "✨ To Quiz";
|
if (navQuiz) navQuiz.innerText = isDe ? "🎯 Quiz" : "🎯 Quiz";
|
||||||
if (navLeaderboard) navLeaderboard.innerText = isDe ? "🏆 Bestenliste" : "🏆 Leaderboard";
|
if (navLeaderboard) navLeaderboard.innerText = isDe ? "🏆 Bestenliste" : "🏆 Leaderboard";
|
||||||
if (navSuggest) navSuggest.innerText = isDe ? "✍️ Frage einreichen" : "✍️ Submit Question";
|
if (navSuggest) navSuggest.innerText = isDe ? "✍️ Frage einreichen" : "✍️ Submit Question";
|
||||||
if (navAdmin) navAdmin.innerText = isDe ? "🔒 Admin-Cockpit" : "🔒 Admin Cockpit";
|
if (navAdmin) navAdmin.innerText = isDe ? "⚙️ Admin" : "⚙️ Admin";
|
||||||
if (footerText) {
|
if (footerText) {
|
||||||
footerText.innerText = isDe
|
footerText.innerText = isDe
|
||||||
? "⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡"
|
? "⚡ PotterTrivia · Das unabhängige Harry-Potter-Wissensquiz"
|
||||||
: "⚡ Magical Hogwarts Escape Quiz · Powered by Love & Wizardry ⚡";
|
: "⚡ PotterTrivia · The Independent Wizarding World Trivia Quiz";
|
||||||
}
|
}
|
||||||
if (footerImpressum) footerImpressum.innerText = isDe ? "⚖️ Impressum" : "⚖️ Legal Notice";
|
if (footerImpressum) footerImpressum.innerText = isDe ? "⚖️ Impressum" : "⚖️ Legal Notice";
|
||||||
if (footerDatenschutz) footerDatenschutz.innerText = isDe ? "🛡️ Datenschutz" : "🛡️ Privacy Policy";
|
if (footerDatenschutz) footerDatenschutz.innerText = isDe ? "🛡️ Datenschutz" : "🛡️ Privacy Policy";
|
||||||
if (audioText) {
|
|
||||||
const isPlaying = bgm && !bgm.paused;
|
|
||||||
if (isDe) {
|
|
||||||
audioText.innerText = isPlaying ? "Musik: An" : "Musik: Aus";
|
|
||||||
} else {
|
|
||||||
audioText.innerText = isPlaying ? "Music: On" : "Music: Off";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAudio() {
|
|
||||||
const isDe = currentLang === "de";
|
|
||||||
if (bgm.paused) {
|
|
||||||
bgm.play().then(() => {
|
|
||||||
audioIcon.innerText = "🔊";
|
|
||||||
audioText.innerText = isDe ? "Musik: An" : "Music: On";
|
|
||||||
localStorage.setItem("bgm_enabled", "true");
|
|
||||||
}).catch(err => {
|
|
||||||
console.log("Audio play blocked:", err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
bgm.pause();
|
|
||||||
audioIcon.innerText = "🔇";
|
|
||||||
audioText.innerText = isDe ? "Musik: Aus" : "Music: Off";
|
|
||||||
localStorage.setItem("bgm_enabled", "false");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMobileMenu() {
|
function toggleMobileMenu() {
|
||||||
@@ -141,18 +108,6 @@
|
|||||||
menuIcon.innerText = "☰";
|
menuIcon.innerText = "☰";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-resume audio if previously enabled
|
|
||||||
document.addEventListener("click", function initAudioOnInteraction() {
|
|
||||||
if (localStorage.getItem("bgm_enabled") === "true" && bgm.paused) {
|
|
||||||
bgm.play().then(() => {
|
|
||||||
audioIcon.innerText = "🔊";
|
|
||||||
audioText.innerText = (currentLang === "de") ? "Musik: An" : "Music: On";
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
document.removeEventListener("click", initAudioOnInteraction);
|
|
||||||
}, { once: true });
|
|
||||||
|
|
||||||
// Initialize UI on load
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
updateLanguageUI();
|
updateLanguageUI();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><strong>${rankDisplay}</strong></td>
|
<td><strong>${rankDisplay}</strong></td>
|
||||||
<td><strong>${escapeHtml(e.nickname)}</strong></td>
|
<td><strong>${escapeHtml(e.nickname)}</strong></td>
|
||||||
<td><span class="gold-text font-weight-bold">${e.score}/${e.total_questions}</span></td>
|
<td><span class="gold-text font-weight-bold">${e.score} Pkt.</span></td>
|
||||||
<td>${e.time_formatted}</td>
|
<td>${e.time_formatted}</td>
|
||||||
<td><span class="badge badge-diff ${diffBadgeClass}">${diffLabel}</span></td>
|
<td><span class="badge badge-diff ${diffBadgeClass}">${diffLabel}</span></td>
|
||||||
<td class="text-muted" style="font-size: 0.85rem;">${e.date}</td>
|
<td class="text-muted" style="font-size: 0.85rem;">${e.date}</td>
|
||||||
|
|||||||
+59
-11
@@ -17,9 +17,9 @@
|
|||||||
<div class="score-banner">
|
<div class="score-banner">
|
||||||
<p class="score-highlight" id="finaleScoreText">
|
<p class="score-highlight" id="finaleScoreText">
|
||||||
{% if lang == 'en' %}
|
{% if lang == 'en' %}
|
||||||
You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ total }}</span> House Points!
|
You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ max_score }}</span> House Points!
|
||||||
{% else %}
|
{% else %}
|
||||||
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> Hauspunkten erzielt!
|
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ max_score }}</span> Hauspunkten erzielt!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p class="time-highlight" id="finaleTimeText">
|
<p class="time-highlight" id="finaleTimeText">
|
||||||
@@ -45,6 +45,9 @@
|
|||||||
<form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;">
|
<form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;">
|
||||||
<div class="input-with-btn">
|
<div class="input-with-btn">
|
||||||
<input type="text" id="nicknameInput" class="input-text" placeholder="z. B. Fliegender_Besen oder Lord_Voldi" maxlength="30" required autocomplete="off">
|
<input type="text" id="nicknameInput" class="input-text" placeholder="z. B. Fliegender_Besen oder Lord_Voldi" maxlength="30" required autocomplete="off">
|
||||||
|
<button type="button" id="diceBtn" class="btn btn-secondary btn-dice" onclick="rollRandomName()" title="Zufälliger Zauberer-Name">
|
||||||
|
🎲 <span class="dice-label" id="diceLabel">Zufall</span>
|
||||||
|
</button>
|
||||||
<button type="submit" id="lbSubmitBtn" class="btn btn-primary">
|
<button type="submit" id="lbSubmitBtn" class="btn btn-primary">
|
||||||
📜 Verewigen
|
📜 Verewigen
|
||||||
</button>
|
</button>
|
||||||
@@ -91,17 +94,56 @@
|
|||||||
<script>
|
<script>
|
||||||
const score = {{ score }};
|
const score = {{ score }};
|
||||||
const total = {{ total }};
|
const total = {{ total }};
|
||||||
|
const maxScore = {{ max_score }};
|
||||||
const timeSec = {{ time_seconds }};
|
const timeSec = {{ time_seconds }};
|
||||||
|
const difficulty = "{{ difficulty }}";
|
||||||
let scoreAlreadySubmitted = false;
|
let scoreAlreadySubmitted = false;
|
||||||
|
|
||||||
|
const WIZARD_NAMES = {
|
||||||
|
de: [
|
||||||
|
"Fliegender_Besen", "Goldener_Schnatz", "Butterbier_Genießer",
|
||||||
|
"Severus_Snape_Fan", "Dumbledores_Phaenix", "Hogwarts_Spion",
|
||||||
|
"Peeves_Geist", "Niffler_Raeuber", "Gryffindor_Loewe",
|
||||||
|
"Ravenclaw_Adler", "Hufflepuff_Dachs", "Slytherin_Schlange",
|
||||||
|
"Mondkalb_Freund", "Zauberschach_Meister", "Kessel_Explodierer",
|
||||||
|
"Alraunen_Zaehmer", "Krummbein_Fan", "Tarnumhang_Traeger",
|
||||||
|
"Zeitumkehrer_Reisender", "Kuerbissaft_Trinker", "Patronus_Beschwoerer",
|
||||||
|
"Erlesener_Zauberstab", "Kopfloser_Reiter", "Schokofrosch_Sammler",
|
||||||
|
"Verbotene_Abteilung", "Hippogreif_Reiter", "Hauselfen_Freund"
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
"Flying_Broom", "Golden_Snitch", "Butterbeer_Lover",
|
||||||
|
"Severus_Fan", "Dumbledores_Phoenix", "Hogwarts_Spy",
|
||||||
|
"Peeves_Poltergeist", "Niffler_Bandit", "Gryffindor_Lion",
|
||||||
|
"Ravenclaw_Eagle", "Hufflepuff_Badger", "Slytherin_Snake",
|
||||||
|
"Mooncalf_Buddy", "Wizard_Chess_Master", "Cauldron_Exploder",
|
||||||
|
"Mandrake_Whisperer", "Crookshanks_Fan", "Invisibility_Bearer",
|
||||||
|
"Time_Turner_Voyager", "Pumpkin_Juice_Drinker", "Patronus_Caster",
|
||||||
|
"Elder_Wand_Seeker", "Headless_Hunt_Guest", "Chocolate_Frog_Collector",
|
||||||
|
"Restricted_Section", "Hippogriff_Rider", "House_Elf_Defender"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function rollRandomName() {
|
||||||
|
const isDe = (currentLang !== "en");
|
||||||
|
const list = isDe ? WIZARD_NAMES.de : WIZARD_NAMES.en;
|
||||||
|
const name = list[Math.floor(Math.random() * list.length)];
|
||||||
|
const input = document.getElementById("nicknameInput");
|
||||||
|
if (input) {
|
||||||
|
input.value = name;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const FINALE_I18N = {
|
const FINALE_I18N = {
|
||||||
de: {
|
de: {
|
||||||
title: "⚡ Das große Hogwarts-Zeugnis ⚡",
|
title: "⚡ Das große Hogwarts-Zeugnis ⚡",
|
||||||
scoreText: `Du hast <span class="gold-text">${score}</span> von <span class="gold-text">${total}</span> Hauspunkten erzielt!`,
|
scoreText: `Du hast <span class="gold-text">${score}</span> von <span class="gold-text">${maxScore}</span> Hauspunkten erzielt!`,
|
||||||
timeText: `⏱️ Benötigte Zeit: <strong class="gold-text">${formatSecondsDe(timeSec)}</strong>`,
|
timeText: `⏱️ Benötigte Zeit: <strong class="gold-text">${formatSecondsDe(timeSec)}</strong>`,
|
||||||
lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!",
|
lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!",
|
||||||
lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:",
|
lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:",
|
||||||
lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi",
|
lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi",
|
||||||
|
diceLabel: "Zufall",
|
||||||
lbBtn: "📜 Verewigen",
|
lbBtn: "📜 Verewigen",
|
||||||
lbBtnSending: "✨ Wird verewigt...",
|
lbBtnSending: "✨ Wird verewigt...",
|
||||||
miniLbTitle: "Top Zauberer:",
|
miniLbTitle: "Top Zauberer:",
|
||||||
@@ -115,11 +157,12 @@
|
|||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
title: "⚡ The Great Hogwarts Report Card ⚡",
|
title: "⚡ The Great Hogwarts Report Card ⚡",
|
||||||
scoreText: `You achieved <span class="gold-text">${score}</span> out of <span class="gold-text">${total}</span> House Points!`,
|
scoreText: `You achieved <span class="gold-text">${score}</span> out of <span class="gold-text">${maxScore}</span> House Points!`,
|
||||||
timeText: `⏱️ Time needed: <strong class="gold-text">${formatSecondsEn(timeSec)}</strong>`,
|
timeText: `⏱️ Time needed: <strong class="gold-text">${formatSecondsEn(timeSec)}</strong>`,
|
||||||
lbTitle: "🏆 Enter the Hogwarts Hall of Fame!",
|
lbTitle: "🏆 Enter the Hogwarts Hall of Fame!",
|
||||||
lbSub: "Completely anonymous – choose your wizard nickname:",
|
lbSub: "Completely anonymous – choose your wizard nickname:",
|
||||||
lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi",
|
lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi",
|
||||||
|
diceLabel: "Random",
|
||||||
lbBtn: "📜 Enter Score",
|
lbBtn: "📜 Enter Score",
|
||||||
lbBtnSending: "✨ Entering score...",
|
lbBtnSending: "✨ Entering score...",
|
||||||
miniLbTitle: "Top Wizards:",
|
miniLbTitle: "Top Wizards:",
|
||||||
@@ -225,10 +268,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getTierForScore(s) {
|
function getTierForScore(s) {
|
||||||
if (s >= 10) return TIERS_DATA[10];
|
const pct = (s <= 10 && total <= 10) ? (s / total) : (s / maxScore);
|
||||||
if (s >= 8) return TIERS_DATA[8];
|
if (pct >= 0.90) return TIERS_DATA[10];
|
||||||
if (s >= 5) return TIERS_DATA[5];
|
if (pct >= 0.70) return TIERS_DATA[8];
|
||||||
if (s >= 2) return TIERS_DATA[2];
|
if (pct >= 0.50) return TIERS_DATA[5];
|
||||||
|
if (pct >= 0.20) return TIERS_DATA[2];
|
||||||
return TIERS_DATA[0];
|
return TIERS_DATA[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +286,7 @@
|
|||||||
const lbTitle = document.getElementById("lbCardTitle");
|
const lbTitle = document.getElementById("lbCardTitle");
|
||||||
const lbSub = document.getElementById("lbCardSub");
|
const lbSub = document.getElementById("lbCardSub");
|
||||||
const nicknameInput = document.getElementById("nicknameInput");
|
const nicknameInput = document.getElementById("nicknameInput");
|
||||||
|
const diceLabel = document.getElementById("diceLabel");
|
||||||
const lbSubmitBtn = document.getElementById("lbSubmitBtn");
|
const lbSubmitBtn = document.getElementById("lbSubmitBtn");
|
||||||
const miniLbTitle = document.getElementById("miniLbTitle");
|
const miniLbTitle = document.getElementById("miniLbTitle");
|
||||||
const thNick = document.getElementById("thNick");
|
const thNick = document.getElementById("thNick");
|
||||||
@@ -256,6 +301,7 @@
|
|||||||
if (timeText) timeText.innerHTML = t.timeText;
|
if (timeText) timeText.innerHTML = t.timeText;
|
||||||
if (lbTitle) lbTitle.innerText = t.lbTitle;
|
if (lbTitle) lbTitle.innerText = t.lbTitle;
|
||||||
if (lbSub) lbSub.innerText = t.lbSub;
|
if (lbSub) lbSub.innerText = t.lbSub;
|
||||||
|
if (diceLabel) diceLabel.innerText = t.diceLabel;
|
||||||
if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder;
|
if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder;
|
||||||
if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn;
|
if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn;
|
||||||
if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle;
|
if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle;
|
||||||
@@ -305,7 +351,7 @@
|
|||||||
score: score,
|
score: score,
|
||||||
total_questions: total,
|
total_questions: total,
|
||||||
time_seconds: timeSec,
|
time_seconds: timeSec,
|
||||||
difficulty: "all",
|
difficulty: difficulty,
|
||||||
lang: currentLang
|
lang: currentLang
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -341,7 +387,8 @@
|
|||||||
|
|
||||||
async function loadMiniLeaderboard() {
|
async function loadMiniLeaderboard() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/leaderboard?limit=5");
|
const url = "/api/leaderboard?limit=5" + (difficulty && difficulty !== "all" ? `&difficulty=${encodeURIComponent(difficulty)}` : "");
|
||||||
|
const res = await fetch(url);
|
||||||
const entries = await res.json();
|
const entries = await res.json();
|
||||||
const body = document.getElementById("miniLbBody");
|
const body = document.getElementById("miniLbBody");
|
||||||
const container = document.getElementById("miniLbContainer");
|
const container = document.getElementById("miniLbContainer");
|
||||||
@@ -354,7 +401,7 @@
|
|||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><strong>${rankDisplay}</strong></td>
|
<td><strong>${rankDisplay}</strong></td>
|
||||||
<td>${e.nickname}</td>
|
<td>${e.nickname}</td>
|
||||||
<td><span class="gold-text">${e.score}/${e.total_questions}</span></td>
|
<td><span class="gold-text">${e.score} Pkt.</span></td>
|
||||||
<td>${e.time_formatted}</td>
|
<td>${e.time_formatted}</td>
|
||||||
`;
|
`;
|
||||||
body.appendChild(tr);
|
body.appendChild(tr);
|
||||||
@@ -368,6 +415,7 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
onLanguageChange(currentLang);
|
onLanguageChange(currentLang);
|
||||||
loadMiniLeaderboard();
|
loadMiniLeaderboard();
|
||||||
|
rollRandomName();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+94
-51
@@ -1,64 +1,107 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}✨ Magisches Harry-Potter-Escape-Quiz ✨{% endblock %}
|
{% block title %}⚡ PotterTrivia – Das Harry-Potter-Wissensquiz{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="quiz-wrapper">
|
<div class="quiz-wrapper">
|
||||||
<header class="quiz-header">
|
<header class="quiz-header">
|
||||||
<h1 class="glow-title" id="pageTitle">✨ Magisches Hogwarts-Escape-Quiz ✨</h1>
|
<div class="hero-pill" id="heroPill">⚡ {{ question_count }} Fragen im Wissens-Pool · Buch & Film</div>
|
||||||
<p class="subtitle" id="pageSubtitle">10 knifflige Rätsel aus der Zauberwelt warten auf dich</p>
|
<h1 class="glow-title" id="pageTitle">Harry Potter Wissensquiz</h1>
|
||||||
|
<p class="subtitle" id="pageSubtitle">Teste dein Wissen über Bücher, Filme, Geschöpfe und Zaubersprüche – direkt & ohne Umwege.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Brief-Intro / Startmaske -->
|
<!-- Kompaktes Direct-Start Dashboard -->
|
||||||
<div id="introCard" class="card intro-card">
|
<div id="introCard" class="card start-dashboard-card">
|
||||||
<div class="intro-envelope" id="envelope">
|
<div class="dashboard-grid">
|
||||||
<div class="envelope-body">
|
<!-- Sektion 1: Schwierigkeitsgrad -->
|
||||||
<div class="envelope-flap"></div>
|
<div class="config-section">
|
||||||
<div class="wax-seal">H</div>
|
<div class="config-header">
|
||||||
<div class="letter-preview">
|
<span class="config-step">1</span>
|
||||||
<p class="letter-text" id="envelopeLetter">Hogwarts-Schule für Hexerei und Zauberei</p>
|
<span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad</span>
|
||||||
|
</div>
|
||||||
|
<div 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>
|
||||||
</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="dashboard-footer">
|
||||||
<div class="difficulty-selection">
|
<div class="dashboard-rules" id="dashboardRules">
|
||||||
<span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad wählen:</span>
|
<span>🎯 10 Fragen pro Runde</span>
|
||||||
<div class="difficulty-chips" role="radiogroup" aria-label="Schwierigkeitsgrad">
|
<span>·</span>
|
||||||
<button type="button" class="diff-chip active" data-diff="all" onclick="selectDifficulty('all')">
|
<span>🔥 Streak-Zähler</span>
|
||||||
<span class="diff-icon">🎲</span> <span class="diff-text" id="diffAllText">Alle gemischt</span>
|
<span>·</span>
|
||||||
</button>
|
<span>💡 Bis zu 3 Versuche je Frage</span>
|
||||||
<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>
|
</div>
|
||||||
|
<button id="startQuizBtn" class="btn btn-primary btn-large btn-launch-quiz" onclick="startGame()">
|
||||||
<button id="startBtn" class="btn btn-primary btn-large" onclick="startGame()">
|
⚡ Quiz jetzt starten ➔
|
||||||
📜 Siegel brechen & Brief öffnen
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Spielbereich (initial versteckt) -->
|
<!-- Spielbereich (initial versteckt) -->
|
||||||
<div id="gameCard" class="game-area hidden">
|
<div id="gameCard" class="game-area hidden">
|
||||||
<!-- Fortschritt & Punkte & Zeit -->
|
<!-- HUD: Fortschritt, Streak, Zeit & Punkte -->
|
||||||
<div class="status-panel">
|
<div class="status-panel">
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
<span id="questionCounter">Frage 1 von 10</span>
|
<span id="questionCounter" class="hud-pill">Frage 1 / 10</span>
|
||||||
<span id="timerBadge" class="timer-badge">⏱️ 00:00</span>
|
<div class="hud-center-group">
|
||||||
<span id="pointsCounter" class="points-badge">🏆 Hauspunkte: 0</span>
|
<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>
|
||||||
<div class="progress-container">
|
<div class="progress-container">
|
||||||
<div id="progressBar" class="progress-bar"></div>
|
<div id="progressBar" class="progress-bar"></div>
|
||||||
@@ -69,24 +112,24 @@
|
|||||||
<div id="frageBox" class="card question-card">
|
<div id="frageBox" class="card question-card">
|
||||||
<div class="question-meta">
|
<div class="question-meta">
|
||||||
<div class="meta-left">
|
<div class="meta-left">
|
||||||
<span id="badgeCategory" class="badge">⚡ Harry Potter</span>
|
<span id="badgeCategory" class="badge">⚡ Allgemein</span>
|
||||||
<span id="badgeDifficulty" class="badge badge-diff">🟢 Leicht</span>
|
<span id="badgeDifficulty" class="badge badge-diff">🟢 Leicht</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="attemptBadge" class="attempt-badge">Versuch 1 von 3</span>
|
<span id="attemptBadge" class="attempt-badge">Versuch 1 / 3</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 id="frageText" class="question-text">Lade verzauberte Schriftrollen...</h2>
|
<h2 id="frageText" class="question-text">Lade nächste Frage...</h2>
|
||||||
|
|
||||||
<!-- Freitext-Eingabe (wenn question_type == 'text') -->
|
<!-- Freitext-Eingabe (wenn question_type == 'text') -->
|
||||||
<div id="textAnswerGroup" class="answer-form">
|
<div id="textAnswerGroup" class="answer-form">
|
||||||
<input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort hier eingeben..." autocomplete="off">
|
<input type="text" id="answerInput" class="input-answer" placeholder="Deine Antwort eingeben..." autocomplete="off">
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button id="checkBtn" class="btn btn-primary" onclick="checkAnswer()">
|
<button id="checkBtn" class="btn btn-primary" onclick="checkAnswer()">
|
||||||
✨ Antwort prüfen
|
✓ Antwort prüfen
|
||||||
</button>
|
</button>
|
||||||
<button id="hintBtn" class="btn btn-secondary" onclick="requestHint()">
|
<button id="hintBtn" class="btn btn-secondary" onclick="requestHint()">
|
||||||
💡 Hinweis anzeigen
|
💡 Tipp anzeigen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,15 +137,15 @@
|
|||||||
<!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') -->
|
<!-- Multiple-Choice-Auswahl (wenn question_type == 'multiple_choice') -->
|
||||||
<div id="choiceAnswerGroup" class="choice-group hidden">
|
<div id="choiceAnswerGroup" class="choice-group hidden">
|
||||||
<div id="choiceOptionsContainer" class="choice-grid"></div>
|
<div id="choiceOptionsContainer" class="choice-grid"></div>
|
||||||
<div class="btn-group" style="margin-top: 15px;">
|
<div class="btn-group" style="margin-top: 16px;">
|
||||||
<button id="hintBtnMC" class="btn btn-secondary" onclick="requestHint()">
|
<button id="hintBtnMC" class="btn btn-secondary" onclick="requestHint()">
|
||||||
💡 Hinweis anzeigen
|
💡 Tipp anzeigen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hintBox" class="hint-box hidden">
|
<div id="hintBox" class="hint-box hidden">
|
||||||
<strong id="hintHeading">📜 Notiz aus der verbotenen Abteilung:</strong>
|
<strong id="hintHeading">💡 Hinweis:</strong>
|
||||||
<p id="hintText"></p>
|
<p id="hintText"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -113,5 +156,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/quiz.js"></script>
|
<script src="/static/js/quiz.js?v=4.0"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="quiz-wrapper">
|
<div class="quiz-wrapper">
|
||||||
<div class="card vorschlag-card">
|
<div class="card vorschlag-card">
|
||||||
<h1 class="glow-title" id="suggTitle">✨ Neue Frage für das Quiz vorschlagen ✨</h1>
|
<h1 class="glow-title" id="suggTitle">✍️ Neue Quiz-Frage einreichen</h1>
|
||||||
<p class="subtitle" id="suggSubtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p>
|
<p class="subtitle" id="suggSubtitle">Bereichere PotterTrivia mit deinen eigenen Fragen zu Büchern, Filmen oder magischen Geschöpfen!</p>
|
||||||
|
|
||||||
<div class="info-alert" id="suggAlert">
|
<div class="info-alert" id="suggAlert">
|
||||||
ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status <em>„Prüfung ausstehend“</em>. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird.
|
ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden kurz auf Richtigkeit geprüft, bevor sie im öffentlichen Quiz-Pool freigeschaltet werden.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)">
|
<form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)">
|
||||||
@@ -23,6 +23,21 @@
|
|||||||
<textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea>
|
<textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="kategorie" id="suggCatLabel">Kategorie / Themenbereich:</label>
|
||||||
|
<select id="kategorie" name="category" class="input-text">
|
||||||
|
<option value="books" id="catBooks">📖 Buchwissen (Buch-Details)</option>
|
||||||
|
<option value="movies" id="catMovies">🎬 Filmwissen (Kino & Drehorte)</option>
|
||||||
|
<option value="creatures" id="catCreatures">🐉 Magische Geschöpfe</option>
|
||||||
|
<option value="spells" id="catSpells">🪄 Zaubersprüche & Tränke</option>
|
||||||
|
<option value="characters" id="catCharacters" selected>🧙♂️ Charaktere & Personen</option>
|
||||||
|
<option value="hogwarts" id="catHogwarts">🏰 Hogwarts & Orte</option>
|
||||||
|
<option value="quidditch" id="catQuidditch">🧹 Quidditch & Sport</option>
|
||||||
|
<option value="lore" id="catLore">🔮 Artefakte & Geschichte</option>
|
||||||
|
<option value="general" id="catGeneral">⚡ Allgemeines Wissen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label>
|
<label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label>
|
||||||
<select id="diff_select" name="difficulty" class="input-text">
|
<select id="diff_select" name="difficulty" class="input-text">
|
||||||
@@ -70,5 +85,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/vorschlag.js"></script>
|
<script src="/static/js/vorschlag.js?v=2.3"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user