Feature: 40 bilingual questions, random 10 game run, difficulty levels, and full i18n support
This commit is contained in:
+87
-26
@@ -16,7 +16,7 @@ from app.models import Question, QuizSession, AdminUser
|
||||
from app.matcher import is_answer_correct, levenshtein_distance, normalize_text
|
||||
from app.auth import verify_password
|
||||
from app.seed import seed_db
|
||||
from app.schemas import GameCheckRequest, SuggestionCreateRequest
|
||||
from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequest
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -29,25 +29,33 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
|
||||
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
|
||||
|
||||
def format_question_for_client(q: Question, number: int, total: int) -> dict:
|
||||
def format_question_for_client(q: Question, number: int, total: int, lang: str = "de") -> dict:
|
||||
options_list = None
|
||||
if q.question_type == "multiple_choice" and q.options:
|
||||
if lang == "en" and q.question_en:
|
||||
q_text = q.question_en
|
||||
opts_raw = q.options_en or q.options
|
||||
else:
|
||||
q_text = q.question
|
||||
opts_raw = q.options
|
||||
|
||||
if q.question_type == "multiple_choice" and opts_raw:
|
||||
try:
|
||||
parsed = json.loads(q.options)
|
||||
parsed = json.loads(opts_raw)
|
||||
if isinstance(parsed, list):
|
||||
options_list = list(parsed)
|
||||
random.shuffle(options_list)
|
||||
except Exception:
|
||||
options_list = [opt.strip() for opt in q.options.split(",") if opt.strip()]
|
||||
options_list = [opt.strip() for opt in opts_raw.split(",") if opt.strip()]
|
||||
random.shuffle(options_list)
|
||||
|
||||
return {
|
||||
"question_number": number,
|
||||
"total_questions": total,
|
||||
"category": q.category,
|
||||
"question": q.question,
|
||||
"question": q_text,
|
||||
"question_type": q.question_type or "text",
|
||||
"options": options_list
|
||||
"options": options_list,
|
||||
"difficulty": q.difficulty or "medium"
|
||||
}
|
||||
|
||||
@app.on_event("startup")
|
||||
@@ -66,13 +74,26 @@ async def index(request: Request, db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
@app.post("/api/game/start")
|
||||
async def start_game(db: Session = Depends(get_db)):
|
||||
# Fetch all active questions in order
|
||||
questions = db.query(Question).filter(Question.status == "active").order_by(Question.sort_order.asc(), Question.id.asc()).all()
|
||||
if not questions:
|
||||
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
||||
async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)):
|
||||
lang = payload.lang if payload and payload.lang in ["de", "en"] else "de"
|
||||
difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all"
|
||||
|
||||
question_ids = [q.id for q in questions]
|
||||
# Fetch active questions matching difficulty (or all active)
|
||||
query = db.query(Question).filter(Question.status == "active")
|
||||
if difficulty != "all":
|
||||
query = query.filter(Question.difficulty == difficulty)
|
||||
|
||||
pool = query.all()
|
||||
if not pool:
|
||||
# Fallback to all active if filtered pool empty
|
||||
pool = db.query(Question).filter(Question.status == "active").all()
|
||||
if not pool:
|
||||
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
||||
|
||||
# Randomly sample 10 questions (or all if fewer)
|
||||
sample_size = min(10, len(pool))
|
||||
selected_questions = random.sample(pool, sample_size)
|
||||
question_ids = [q.id for q in selected_questions]
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
new_session = QuizSession(
|
||||
@@ -81,18 +102,22 @@ async def start_game(db: Session = Depends(get_db)):
|
||||
current_question_idx=0,
|
||||
question_ids=json.dumps(question_ids),
|
||||
attempts_current=0,
|
||||
is_finished=False
|
||||
is_finished=False,
|
||||
lang=lang,
|
||||
difficulty=difficulty
|
||||
)
|
||||
db.add(new_session)
|
||||
db.commit()
|
||||
|
||||
first_q = questions[0]
|
||||
res = format_question_for_client(first_q, 1, len(question_ids))
|
||||
first_q = selected_questions[0]
|
||||
res = format_question_for_client(first_q, 1, len(question_ids), lang=lang)
|
||||
res.update({
|
||||
"session_id": session_id,
|
||||
"score": 0,
|
||||
"attempts_used": 0,
|
||||
"max_attempts": 3
|
||||
"max_attempts": 3,
|
||||
"lang": lang,
|
||||
"difficulty": difficulty
|
||||
})
|
||||
return res
|
||||
|
||||
@@ -102,6 +127,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not session or session.is_finished:
|
||||
raise HTTPException(status_code=400, detail="Ungültige oder beendete Spiel-Session.")
|
||||
|
||||
session_lang = session.lang or "de"
|
||||
question_ids = json.loads(session.question_ids)
|
||||
if session.current_question_idx >= len(question_ids):
|
||||
session.is_finished = True
|
||||
@@ -120,7 +146,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not current_q:
|
||||
raise HTTPException(status_code=500, detail="Frage nicht in Datenbank gefunden.")
|
||||
|
||||
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers)
|
||||
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
|
||||
|
||||
if correct:
|
||||
session.score += 1
|
||||
@@ -138,7 +164,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not is_finished:
|
||||
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
||||
if next_q:
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids), lang=session_lang)
|
||||
|
||||
return {
|
||||
"correct": True,
|
||||
@@ -154,8 +180,12 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
attempts_left = max(0, 3 - session.attempts_current)
|
||||
|
||||
if attempts_left == 0:
|
||||
# 3 failed attempts: move to next question, reveal answer so player learns it
|
||||
revealed = current_q.canonical_answer
|
||||
# 3 failed attempts: move to next question, reveal answer
|
||||
if session_lang == "en" and current_q.answer_en:
|
||||
revealed = current_q.answer_en
|
||||
else:
|
||||
revealed = current_q.canonical_answer
|
||||
|
||||
session.current_question_idx += 1
|
||||
session.attempts_current = 0
|
||||
|
||||
@@ -170,7 +200,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not is_finished:
|
||||
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
||||
if next_q:
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids), lang=session_lang)
|
||||
|
||||
return {
|
||||
"correct": False,
|
||||
@@ -199,20 +229,29 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
|
||||
if not session or session.is_finished:
|
||||
raise HTTPException(status_code=400, detail="Ungültige Session.")
|
||||
|
||||
session_lang = session.lang or "de"
|
||||
question_ids = json.loads(session.question_ids)
|
||||
if session.current_question_idx >= len(question_ids):
|
||||
return {"hint": "Kein Hinweis verfügbar."}
|
||||
return {"hint": "No hint available." if session_lang == "en" else "Kein Hinweis verfügbar."}
|
||||
|
||||
current_q_id = question_ids[session.current_question_idx]
|
||||
current_q = db.query(Question).filter(Question.id == current_q_id).first()
|
||||
return {"hint": current_q.hint if current_q and current_q.hint else "Kein Tipp verfügbar."}
|
||||
if not current_q:
|
||||
return {"hint": "No hint available." if session_lang == "en" else "Kein Hinweis verfügbar."}
|
||||
|
||||
if session_lang == "en":
|
||||
hint_text = current_q.hint_en if current_q.hint_en else (current_q.hint or "No hint available.")
|
||||
else:
|
||||
hint_text = current_q.hint or "Kein Tipp verfügbar."
|
||||
|
||||
return {"hint": hint_text}
|
||||
|
||||
@app.get("/finale", response_class=HTMLResponse)
|
||||
async def finale(request: Request, score: int = 0, total: int = 15):
|
||||
async def finale(request: Request, score: int = 0, total: int = 10, lang: str = "de"):
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="finale.html",
|
||||
context={"score": score, "total": total}
|
||||
context={"score": score, "total": total, "lang": lang}
|
||||
)
|
||||
|
||||
@app.get("/vorschlag", response_class=HTMLResponse)
|
||||
@@ -238,6 +277,8 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
||||
opts.append(opt.strip())
|
||||
options_json = json.dumps(opts, ensure_ascii=False)
|
||||
|
||||
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
|
||||
|
||||
new_suggestion = Question(
|
||||
category="community",
|
||||
question=payload.frage.strip(),
|
||||
@@ -245,6 +286,7 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
||||
hint=payload.tipp.strip() if payload.tipp else None,
|
||||
question_type=q_type,
|
||||
options=options_json,
|
||||
difficulty=diff,
|
||||
status="needs_review", # User Question Need Test / Review
|
||||
submitted_by=payload.name.strip() if payload.name else "Gast",
|
||||
sort_order=999
|
||||
@@ -327,6 +369,10 @@ async def admin_add_question(
|
||||
hint: str = Form(None),
|
||||
category: str = Form("harry_potter"),
|
||||
question_type: str = Form("text"),
|
||||
difficulty: str = Form("medium"),
|
||||
question_en: str = Form(None),
|
||||
answer_en: str = Form(None),
|
||||
hint_en: str = Form(None),
|
||||
wrong_option_1: str = Form(None),
|
||||
wrong_option_2: str = Form(None),
|
||||
wrong_option_3: str = Form(None),
|
||||
@@ -344,6 +390,8 @@ async def admin_add_question(
|
||||
opts.append(w.strip())
|
||||
options_json = json.dumps(opts, ensure_ascii=False)
|
||||
|
||||
diff = difficulty.strip() if difficulty and difficulty.strip() in ["easy", "medium", "hard"] else "medium"
|
||||
|
||||
new_q = Question(
|
||||
category=category.strip(),
|
||||
question=question.strip(),
|
||||
@@ -351,6 +399,10 @@ async def admin_add_question(
|
||||
hint=hint.strip() if hint else None,
|
||||
question_type=question_type.strip(),
|
||||
options=options_json,
|
||||
difficulty=diff,
|
||||
question_en=question_en.strip() if question_en else None,
|
||||
answer_en=answer_en.strip() if answer_en else None,
|
||||
hint_en=hint_en.strip() if hint_en else None,
|
||||
status="active",
|
||||
sort_order=sort_order,
|
||||
submitted_by=f"Admin ({request.session['admin_user']})"
|
||||
@@ -402,6 +454,10 @@ async def admin_edit_question(
|
||||
canonical_answer: str = Form(...),
|
||||
hint: str = Form(None),
|
||||
question_type: str = Form("text"),
|
||||
difficulty: str = Form("medium"),
|
||||
question_en: str = Form(None),
|
||||
answer_en: str = Form(None),
|
||||
hint_en: str = Form(None),
|
||||
options_raw: str = Form(None),
|
||||
status_val: str = Form("active"),
|
||||
sort_order: int = Form(0),
|
||||
@@ -418,6 +474,11 @@ async def admin_edit_question(
|
||||
q.canonical_answer = canonical_answer.strip()
|
||||
q.hint = hint.strip() if hint else None
|
||||
q.question_type = question_type.strip()
|
||||
if difficulty and difficulty.strip() in ["easy", "medium", "hard"]:
|
||||
q.difficulty = difficulty.strip()
|
||||
q.question_en = question_en.strip() if question_en else None
|
||||
q.answer_en = answer_en.strip() if answer_en else None
|
||||
q.hint_en = hint_en.strip() if hint_en else None
|
||||
|
||||
if options_raw and options_raw.strip():
|
||||
val = options_raw.strip()
|
||||
|
||||
+5
-1
@@ -33,12 +33,16 @@ def levenshtein_distance(s1: str, s2: str) -> int:
|
||||
previous_row = current_row
|
||||
return previous_row[-1]
|
||||
|
||||
def is_answer_correct(user_input: str, canonical: str, accepted_raw: 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)
|
||||
if not clean_user:
|
||||
return False
|
||||
|
||||
targets = [normalize_text(canonical)]
|
||||
if canonical_en:
|
||||
clean_en = normalize_text(canonical_en)
|
||||
if clean_en and clean_en not in targets:
|
||||
targets.append(clean_en)
|
||||
|
||||
if accepted_raw:
|
||||
try:
|
||||
|
||||
+14
-2
@@ -7,12 +7,22 @@ class Question(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
category = Column(String(50), default="harry_potter", index=True)
|
||||
difficulty = Column(String(20), default="medium", index=True) # easy, medium, hard
|
||||
question_type = Column(String(20), default="text") # 'text' or 'multiple_choice'
|
||||
|
||||
# German content (default)
|
||||
question = Column(Text, nullable=False)
|
||||
canonical_answer = Column(String(255), nullable=False)
|
||||
accepted_answers = Column(Text, nullable=True) # JSON or comma-delimited
|
||||
question_type = Column(String(20), default="text") # 'text' or 'multiple_choice'
|
||||
options = Column(Text, nullable=True) # JSON list of options for multiple choice
|
||||
hint = Column(Text, nullable=True)
|
||||
|
||||
# English content
|
||||
question_en = Column(Text, nullable=True)
|
||||
answer_en = Column(String(255), nullable=True)
|
||||
options_en = Column(Text, nullable=True) # JSON list of options in English
|
||||
hint_en = Column(Text, nullable=True)
|
||||
|
||||
accepted_answers = Column(Text, nullable=True) # JSON or comma-delimited
|
||||
status = Column(String(30), default="active", index=True) # active, needs_review, rejected, archived
|
||||
submitted_by = Column(String(100), nullable=True)
|
||||
sort_order = Column(Integer, default=0)
|
||||
@@ -29,6 +39,8 @@ class QuizSession(Base):
|
||||
question_ids = Column(Text, nullable=False) # JSON array string: "[1, 2, 3...]"
|
||||
attempts_current = Column(Integer, default=0)
|
||||
is_finished = Column(Boolean, default=False)
|
||||
lang = Column(String(10), default="de") # 'de' or 'en'
|
||||
difficulty = Column(String(20), default="all") # 'all', 'easy', 'medium', 'hard'
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
finished_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
class GameStartRequest(BaseModel):
|
||||
lang: Optional[str] = "de"
|
||||
difficulty: Optional[str] = "all"
|
||||
|
||||
class GameStartResponse(BaseModel):
|
||||
session_id: str
|
||||
question_number: int
|
||||
@@ -10,6 +14,8 @@ class GameStartResponse(BaseModel):
|
||||
score: int
|
||||
attempts_used: int
|
||||
max_attempts: int = 3
|
||||
difficulty: Optional[str] = "medium"
|
||||
lang: Optional[str] = "de"
|
||||
|
||||
class GameCheckRequest(BaseModel):
|
||||
session_id: str
|
||||
@@ -32,6 +38,7 @@ class SuggestionCreateRequest(BaseModel):
|
||||
name: Optional[str] = "Gast"
|
||||
question_type: Optional[str] = "text"
|
||||
falsche_antworten: Optional[List[str]] = None
|
||||
difficulty: Optional[str] = "medium"
|
||||
|
||||
class QuestionAdminCreate(BaseModel):
|
||||
category: str = "harry_potter"
|
||||
|
||||
+6
-46
@@ -3,6 +3,7 @@ import json
|
||||
from app.database import engine, SessionLocal, Base
|
||||
from app.models import Question, AdminUser
|
||||
from app.auth import hash_password
|
||||
from app.seed_40 import seed_40_questions
|
||||
|
||||
def seed_db():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@@ -22,52 +23,11 @@ def seed_db():
|
||||
db.commit()
|
||||
print(f"Created default admin user: {admin_user}")
|
||||
|
||||
# 2. Existing Questions from legacy/fragen.json
|
||||
legacy_questions_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "legacy", "fragen.json")
|
||||
if os.path.exists(legacy_questions_path):
|
||||
with open(legacy_questions_path, "r", encoding="utf-8") as f:
|
||||
legacy_questions = json.load(f)
|
||||
|
||||
count = 0
|
||||
for idx, q in enumerate(legacy_questions):
|
||||
# Check if question text already in DB
|
||||
existing = db.query(Question).filter(Question.question == q.get("frage", "").strip()).first()
|
||||
if not existing:
|
||||
db.add(Question(
|
||||
category="harry_potter",
|
||||
question=q.get("frage", "").strip(),
|
||||
canonical_answer=q.get("antwort", "").strip(),
|
||||
hint=q.get("tipp", "").strip(),
|
||||
status="active",
|
||||
sort_order=idx + 1,
|
||||
submitted_by="Admin"
|
||||
))
|
||||
count += 1
|
||||
db.commit()
|
||||
print(f"Imported {count} active questions from legacy/fragen.json")
|
||||
|
||||
# 3. Existing Suggestions from legacy/fragen_vorschlaege.json
|
||||
legacy_sugg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "legacy", "fragen_vorschlaege.json")
|
||||
if os.path.exists(legacy_sugg_path):
|
||||
with open(legacy_sugg_path, "r", encoding="utf-8") as f:
|
||||
legacy_sugg = json.load(f)
|
||||
|
||||
count_sugg = 0
|
||||
for q in legacy_sugg:
|
||||
existing = db.query(Question).filter(Question.question == q.get("frage", "").strip()).first()
|
||||
if not existing:
|
||||
db.add(Question(
|
||||
category="harry_potter",
|
||||
question=q.get("frage", "").strip(),
|
||||
canonical_answer=q.get("antwort", "").strip(),
|
||||
hint=q.get("tipp", "").strip(),
|
||||
status="needs_review",
|
||||
sort_order=999,
|
||||
submitted_by="Gast (Legacy Vorschlag)"
|
||||
))
|
||||
count_sugg += 1
|
||||
db.commit()
|
||||
print(f"Imported {count_sugg} pending suggestions from legacy/fragen_vorschlaege.json")
|
||||
# 2. Seed 40 questions if database is empty
|
||||
question_count = db.query(Question).count()
|
||||
if question_count == 0:
|
||||
print("Question table is empty, seeding 40 initial bilingual questions...")
|
||||
seed_40_questions()
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
+609
@@ -0,0 +1,609 @@
|
||||
import json
|
||||
from app.database import engine, SessionLocal
|
||||
from app.models import Question
|
||||
from sqlalchemy import text
|
||||
|
||||
QUESTIONS_DATA = [
|
||||
# --- EASY (1-14) ---
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie heißt Harrys schneeweiße Eule?",
|
||||
"canonical_answer": "Hedwig",
|
||||
"options": ["Hedwig", "Errol", "Pigwidgeon", "Fawkes"],
|
||||
"hint": "Harry bekam sie von Hagrid in der Winkelgasse geschenkt.",
|
||||
"question_en": "What is the name of Harry's snowy pet owl?",
|
||||
"answer_en": "Hedwig",
|
||||
"options_en": ["Hedwig", "Errol", "Pigwidgeon", "Fawkes"],
|
||||
"hint_en": "Harry received her as a gift from Hagrid in Diagon Alley."
|
||||
},
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welche Farben repräsentieren das Haus Gryffindor?",
|
||||
"canonical_answer": "Rot und Gold",
|
||||
"options": ["Rot und Gold", "Grün und Silber", "Blau und Bronze", "Gelb und Schwarz"],
|
||||
"hint": "Sie erinnern an ein loderndes Löwenfeuer.",
|
||||
"question_en": "Which colours represent the House of Gryffindor?",
|
||||
"answer_en": "Red and Gold",
|
||||
"options_en": ["Red and Gold", "Green and Silver", "Blue and Bronze", "Yellow and Black"],
|
||||
"hint_en": "They evoke the fiery colours of a proud lion."
|
||||
},
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Von welchem Gleis am Bahnhof King's Cross fährt der Hogwarts-Express ab?",
|
||||
"canonical_answer": "9 3/4",
|
||||
"options": None,
|
||||
"hint": "Eine Bruchzahl zwischen Bahnsteig 9 und 10.",
|
||||
"question_en": "From which platform at King's Cross station does the Hogwarts Express depart?",
|
||||
"answer_en": "9 3/4",
|
||||
"options_en": None,
|
||||
"hint_en": "A fraction between platforms 9 and 10."
|
||||
},
|
||||
{
|
||||
"category": "quidditch",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wie viele Punkte bringt das Fangen des Goldenen Schnatzes im Quidditch?",
|
||||
"canonical_answer": "150",
|
||||
"options": None,
|
||||
"hint": "Mehr als einhundert Punkte.",
|
||||
"question_en": "How many points is catching the Golden Snitch worth in Quidditch?",
|
||||
"answer_en": "150",
|
||||
"options_en": None,
|
||||
"hint_en": "More than one hundred points."
|
||||
},
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "In wie viele verschiedene Häuser teilt der Sprechende Hut die Schüler ein?",
|
||||
"canonical_answer": "4",
|
||||
"options": None,
|
||||
"hint": "Gryffindor, Hufflepuff, Ravenclaw und Slytherin.",
|
||||
"question_en": "Into how many different houses does the Sorting Hat divide students?",
|
||||
"answer_en": "4",
|
||||
"options_en": None,
|
||||
"hint_en": "Gryffindor, Hufflepuff, Ravenclaw, and Slytherin."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Mit welchem Zauberspruch beschwört man einen Patronus herauf?",
|
||||
"canonical_answer": "Expecto Patronum",
|
||||
"options": ["Expecto Patronum", "Expelliarmus", "Lumos Solem", "Avada Kedavra"],
|
||||
"hint": "Er erfordert eine zutiefst glückliche Erinnerung.",
|
||||
"question_en": "Which incantation conjures a Patronus guardian?",
|
||||
"answer_en": "Expecto Patronum",
|
||||
"options_en": ["Expecto Patronum", "Expelliarmus", "Lumos Solem", "Avada Kedavra"],
|
||||
"hint_en": "It requires a powerfully happy memory."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wie lautet der zweite Vorname von Harry Potter?",
|
||||
"canonical_answer": "James",
|
||||
"options": None,
|
||||
"hint": "Er trägt denselben Namen wie sein Vater.",
|
||||
"question_en": "What is Harry Potter's middle name?",
|
||||
"answer_en": "James",
|
||||
"options_en": None,
|
||||
"hint_en": "He shares this name with his father."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welcher magische Kern befindet sich im Zauberstab von Harry Potter?",
|
||||
"canonical_answer": "Phönixfeder",
|
||||
"options": ["Phönixfeder", "Drachenherzfaser", "Einhornhaar", "Veela-Haar"],
|
||||
"hint": "Die Feder stammt von Dumbledores Vogel Fawkes.",
|
||||
"question_en": "What magical core is inside Harry Potter's wand?",
|
||||
"answer_en": "Phoenix feather",
|
||||
"options_en": ["Phoenix feather", "Dragon heartstring", "Unicorn hair", "Veela hair"],
|
||||
"hint_en": "The feather was donated by Dumbledore's phoenix Fawkes."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wer war der weise Schulleiter von Hogwarts zu Harrys Schulzeit?",
|
||||
"canonical_answer": "Albus Dumbledore",
|
||||
"options": None,
|
||||
"hint": "Er trägt eine Halbmondbrille und liebt Zitronenbrausebonbons.",
|
||||
"question_en": "Who was the wise Headmaster of Hogwarts during most of Harry's schooling?",
|
||||
"answer_en": "Albus Dumbledore",
|
||||
"options_en": None,
|
||||
"hint_en": "He wears half-moon spectacles and loves Lemon Drops."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wie heißt der Zwillingsbruder von Fred Weasley?",
|
||||
"canonical_answer": "George",
|
||||
"options": None,
|
||||
"hint": "Gemeinsam eröffneten sie 'Weasleys Zauberhafte Zauberscherze'.",
|
||||
"question_en": "What is the name of Fred Weasley's twin brother?",
|
||||
"answer_en": "George",
|
||||
"options_en": None,
|
||||
"hint_en": "Together they opened 'Weasleys' Wizard Wheezes'."
|
||||
},
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welches Tier ist das Wappentier des Hauses Hufflepuff?",
|
||||
"canonical_answer": "Dachs",
|
||||
"options": ["Dachs", "Biber", "Eichhörnchen", "Otter"],
|
||||
"hint": "Ein grabendes, treues und wehrhaftes Tier.",
|
||||
"question_en": "Which animal is the emblem of House Hufflepuff?",
|
||||
"answer_en": "Badger",
|
||||
"options_en": ["Badger", "Beaver", "Squirrel", "Otter"],
|
||||
"hint_en": "A loyal, hard-working, burrowing animal."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Welcher Zauberspruch entwaffnet einen gegnerischen Zauberer?",
|
||||
"canonical_answer": "Expelliarmus",
|
||||
"options": None,
|
||||
"hint": "Harrys Markenzeichen im Duell.",
|
||||
"question_en": "Which spell is used to disarm an opponent?",
|
||||
"answer_en": "Expelliarmus",
|
||||
"options_en": None,
|
||||
"hint_en": "Harry's trademark defensive spell in duels."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "In welches Tier kann sich Sirius Black als Animagus verwandeln?",
|
||||
"canonical_answer": "Schwarzer Hund",
|
||||
"options": ["Schwarzer Hund", "Wolf", "Hirsch", "Ratte"],
|
||||
"hint": "Sein Spitzname lautet 'Tatze' (Padfoot).",
|
||||
"question_en": "Which animal does Sirius Black transform into as an Animagus?",
|
||||
"answer_en": "Black Dog",
|
||||
"options_en": ["Black Dog", "Wolf", "Stag", "Rat"],
|
||||
"hint_en": "His Marauder nickname is 'Padfoot'."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wie viele Heiligtümer des Todes gibt es laut der Legende der drei Brüder?",
|
||||
"canonical_answer": "3",
|
||||
"options": None,
|
||||
"hint": "Elderstab, Stein der Auferstehung und Tarnumhang.",
|
||||
"question_en": "How many Deathly Hallows exist according to the Tale of the Three Brothers?",
|
||||
"answer_en": "3",
|
||||
"options_en": None,
|
||||
"hint_en": "The Elder Wand, the Resurrection Stone, and the Invisibility Cloak."
|
||||
},
|
||||
|
||||
# --- MEDIUM (15-30) ---
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie heißt die beliebte Wirtin des Gasthauses 'Drei Besen' in Hogsmeade?",
|
||||
"canonical_answer": "Madam Rosmerta",
|
||||
"options": ["Madam Rosmerta", "Madam Malkin", "Madam Pince", "Madam Pomfrey"],
|
||||
"hint": "Ron Weasley hat eine kleine Schwäche für sie.",
|
||||
"question_en": "What is the name of the popular landlady of 'The Three Broomsticks' in Hogsmeade?",
|
||||
"answer_en": "Madam Rosmerta",
|
||||
"options_en": ["Madam Rosmerta", "Madam Malkin", "Madam Pince", "Madam Pomfrey"],
|
||||
"hint_en": "Ron Weasley harbors quite a crush on her."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Welche riesige Schlange hauste tief unter Hogwarts in der Kammer des Schreckens?",
|
||||
"canonical_answer": "Basilisk",
|
||||
"options": None,
|
||||
"hint": "Ihr Blick ist absolut tödlich.",
|
||||
"question_en": "Which giant serpent dwelt deep beneath Hogwarts in the Chamber of Secrets?",
|
||||
"answer_en": "Basilisk",
|
||||
"options_en": None,
|
||||
"hint_en": "Its direct stare causes instant death."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Wie heißt Albus Dumbledores treuer Phönix?",
|
||||
"canonical_answer": "Fawkes",
|
||||
"options": None,
|
||||
"hint": "Seine Tränen besitzen wundersame Heilkräfte.",
|
||||
"question_en": "What is the name of Albus Dumbledore's loyal pet phoenix?",
|
||||
"answer_en": "Fawkes",
|
||||
"options_en": None,
|
||||
"hint_en": "His tears have miraculous healing powers."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Wie heißt der mürrische alte Hauself der Familie Black am Grimmauldplatz 12?",
|
||||
"canonical_answer": "Kreacher",
|
||||
"options": None,
|
||||
"hint": "Er verehrte Regulus Black über alles.",
|
||||
"question_en": "What is the name of the grumpy house-elf serving the Black family at 12 Grimmauld Place?",
|
||||
"answer_en": "Kreacher",
|
||||
"options_en": None,
|
||||
"hint_en": "He was fiercely loyal to Regulus Black."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welchen Spitznamen trug Remus Lupin als Schöpfer der Karte des Rumtreibers?",
|
||||
"canonical_answer": "Moony",
|
||||
"options": ["Moony", "Wurmschwanz", "Tatze", "Krone"],
|
||||
"hint": "Eine Anspielung auf den Vollmond.",
|
||||
"question_en": "What was Remus Lupin's Marauder nickname?",
|
||||
"answer_en": "Moony",
|
||||
"options_en": ["Moony", "Wormtail", "Padfoot", "Prongs"],
|
||||
"hint_en": "A direct reference to the full moon."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie heißt Hermines krummbeiniger roter Kater mit Kniesel-Abstammung?",
|
||||
"canonical_answer": "Krummbein",
|
||||
"options": ["Krummbein", "Knut", "Knecht Ruprecht", "Klaue"],
|
||||
"hint": "Auf Englisch heißt er Crookshanks.",
|
||||
"question_en": "What is the name of Hermione's half-Kneazle ginger cat?",
|
||||
"answer_en": "Crookshanks",
|
||||
"options_en": ["Crookshanks", "Krummbein", "Claw", "Rusty"],
|
||||
"hint_en": "He immediately recognized Scabbers as an imposter."
|
||||
},
|
||||
{
|
||||
"category": "quidditch",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Wie viele Bälle sind insgesamt gleichzeitig bei einem regulären Quidditch-Spiel im Einsatz?",
|
||||
"canonical_answer": "4",
|
||||
"options": None,
|
||||
"hint": "Ein Quaffel, zwei Klatscher und ein Schnatz.",
|
||||
"question_en": "How many total balls are in play during a regular Quidditch match?",
|
||||
"answer_en": "4",
|
||||
"options_en": None,
|
||||
"hint_en": "One Quaffle, two Bludgers, and one Golden Snitch."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Mit welchem Spruch wird ein gestaltwandelnder Irrwicht unschädlich gemacht?",
|
||||
"canonical_answer": "Riddikulus",
|
||||
"options": ["Riddikulus", "Reducto", "Rictusempra", "Relashio"],
|
||||
"hint": "Man muss sich etwas Lächerliches und Heiteres vorstellen.",
|
||||
"question_en": "Which charm dispels a shape-shifting Boggart by turning it comical?",
|
||||
"answer_en": "Riddikulus",
|
||||
"options_en": ["Riddikulus", "Reducto", "Rictusempra", "Relashio"],
|
||||
"hint_en": "It requires turning the creature into something funny."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welcher Zauberspruch lässt Gegenstände in die Luft schweben?",
|
||||
"canonical_answer": "Wingardium Leviosa",
|
||||
"options": ["Wingardium Leviosa", "Accio", "Alohomora", "Avis"],
|
||||
"hint": "'Es heißt Leviooooosa, nicht Leviosaaaa!'",
|
||||
"question_en": "Which levitation charm makes objects fly into the air?",
|
||||
"answer_en": "Wingardium Leviosa",
|
||||
"options_en": ["Wingardium Leviosa", "Accio", "Alohomora", "Avis"],
|
||||
"hint_en": "'It's Levi-ooo-sa, not Levio-saaa!' - Hermione Granger."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Gegen welche gefährliche Drachenart musste Harry in der ersten Aufgabe des Trimagischen Turniers antreten?",
|
||||
"canonical_answer": "Ungarischer Hornschwanz",
|
||||
"options": ["Ungarischer Hornschwanz", "Schwedischer Kurzschnäuzler", "Gemeiner Walisischer Grünling", "Chinesischer Feuerball"],
|
||||
"hint": "Der gefährlichste und bösartigste aller vier Drachen.",
|
||||
"question_en": "Which fierce dragon breed did Harry face in the First Task of the Triwizard Tournament?",
|
||||
"answer_en": "Hungarian Horntail",
|
||||
"options_en": ["Hungarian Horntail", "Swedish Short-Snout", "Common Welsh Green", "Chinese Fireball"],
|
||||
"hint_en": "Considered the most vicious and dangerous dragon of all four."
|
||||
},
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welches Tier ziert das offizielle Hauswappen von Ravenclaw?",
|
||||
"canonical_answer": "Adler",
|
||||
"options": ["Adler", "Rabe", "Falke", "Eule"],
|
||||
"hint": "Entgegen dem Namensbestandteil 'Raven' ist es kein Rabe!",
|
||||
"question_en": "Which bird is the official emblem of House Ravenclaw in the books?",
|
||||
"answer_en": "Eagle",
|
||||
"options_en": ["Eagle", "Raven", "Falcon", "Owl"],
|
||||
"hint_en": "Despite the house name, it is definitely not a raven!"
|
||||
},
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie heißt der geheimnisvolle Raum im 7. Stock, der nur erscheint, wenn man ihn wirklich braucht?",
|
||||
"canonical_answer": "Raum der Wünsche",
|
||||
"options": ["Raum der Wünsche", "Raum der Rätsel", "Spiegelkammer", "Halle der Prophezeiungen"],
|
||||
"hint": "Dumbledores Armee trainierte hier heimlich.",
|
||||
"question_en": "What is the name of the magical 7th-floor room that only appears when someone is in real need of it?",
|
||||
"answer_en": "Room of Requirement",
|
||||
"options_en": ["Room of Requirement", "Chamber of Riddles", "Hall of Prophecy", "Mirror Room"],
|
||||
"hint_en": "Dumbledore's Army used it as their secret training ground."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welche Gestalt nahm der Patronus von Severus Snape an?",
|
||||
"canonical_answer": "Silberne Hirschkuh",
|
||||
"options": ["Silberne Hirschkuh", "Großer Hirsch", "Schlange", "Fledermaus"],
|
||||
"hint": "'Nach all dieser Zeit?' – 'Immer.'",
|
||||
"question_en": "What shape did Severus Snape's Patronus take?",
|
||||
"answer_en": "Silver Doe",
|
||||
"options_en": ["Silver Doe", "Stag", "Serpent", "Bat"],
|
||||
"hint_en": "'After all this time?' – 'Always.'"
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welcher Horkrux von Lord Voldemort wurde als allererstes von Harry Potter zerstört?",
|
||||
"canonical_answer": "Tom Riddles Tagebuch",
|
||||
"options": ["Tom Riddles Tagebuch", "Medaillon von Slytherin", "Ring der Gaunts", "Diadem von Ravenclaw"],
|
||||
"hint": "In der Kammer des Schreckens mit einem Basiliskenzahn.",
|
||||
"question_en": "Which of Voldemort's Horcruxes was the very first to be destroyed?",
|
||||
"answer_en": "Tom Riddle's Diary",
|
||||
"options_en": ["Tom Riddle's Diary", "Slytherin's Locket", "Gaunt's Ring", "Ravenclaw's Diadem"],
|
||||
"hint_en": "Destroyed in the Chamber of Secrets with a basilisk fang."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie lautet der vollständige Vorname des weltberühmten Zauberstabmachers Ollivander?",
|
||||
"canonical_answer": "Garrick",
|
||||
"options": ["Garrick", "Gerbold", "Gervase", "Gellert"],
|
||||
"hint": "Sein Geschäft in der Winkelgasse besteht seit 382 v. Chr.",
|
||||
"question_en": "What is the first name of the master wandmaker Mr. Ollivander?",
|
||||
"answer_en": "Garrick",
|
||||
"options_en": ["Garrick", "Gerbold", "Gervase", "Gellert"],
|
||||
"hint_en": "Makers of fine wands since 382 B.C."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Wie lautet der echte Vorname von Auror 'Mad-Eye' Moody?",
|
||||
"canonical_answer": "Alastor",
|
||||
"options": None,
|
||||
"hint": "Beginnt mit dem Buchstaben A.",
|
||||
"question_en": "What is the actual first name of Auror 'Mad-Eye' Moody?",
|
||||
"answer_en": "Alastor",
|
||||
"options_en": None,
|
||||
"hint_en": "Starts with the letter A."
|
||||
},
|
||||
|
||||
# --- HARD (31-40) ---
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wer war der Schulleiter von Hogwarts unmittelbar vor Albus Dumbledore?",
|
||||
"canonical_answer": "Armando Dippet",
|
||||
"options": ["Armando Dippet", "Phineas Nigellus Black", "Dexter Fortescue", "Dilys Derwent"],
|
||||
"hint": "Er war Schulleiter, als Tom Riddle 1943 die Kammer des Schreckens öffnete.",
|
||||
"question_en": "Who served as Headmaster of Hogwarts immediately before Albus Dumbledore?",
|
||||
"answer_en": "Armando Dippet",
|
||||
"options_en": ["Armando Dippet", "Phineas Nigellus Black", "Dexter Fortescue", "Dilys Derwent"],
|
||||
"hint_en": "He was in charge when Tom Riddle opened the Chamber of Secrets in 1943."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Wie hieß die jüngere Schwester von Albus und Aberforth Dumbledore, die tragisch verstarb?",
|
||||
"canonical_answer": "Ariana",
|
||||
"options": None,
|
||||
"hint": "Ihr unkontrollierter Zauberausbruch tötete einst ihre Mutter Kendra.",
|
||||
"question_en": "What was the name of Albus and Aberforth Dumbledore's tragic younger sister?",
|
||||
"answer_en": "Ariana",
|
||||
"options_en": None,
|
||||
"hint_en": "She was traumatized as a child and died during a three-way duel."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Wie hieß die Mutter von Tom Marvolo Riddle (Lord Voldemort) mit Vornamen?",
|
||||
"canonical_answer": "Merope",
|
||||
"options": None,
|
||||
"hint": "Merope Gaunt, eine Nachfahrin von Salazar Slytherin.",
|
||||
"question_en": "What was the first name of Tom Riddle's (Lord Voldemort's) pure-blood mother?",
|
||||
"answer_en": "Merope",
|
||||
"options_en": None,
|
||||
"hint_en": "Merope Gaunt, direct descendant of Salazar Slytherin."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "In welcher exakten Reihenfolge werden die vier Schöpfer auf der Karte des Rumtreibers genannt?",
|
||||
"canonical_answer": "Moony, Wurmschwanz, Tatze und Krone",
|
||||
"options": [
|
||||
"Moony, Wurmschwanz, Tatze und Krone",
|
||||
"Krone, Tatze, Wurmschwanz und Moony",
|
||||
"Moony, Tatze, Wurmschwanz und Krone",
|
||||
"Tatze, Moony, Wurmschwanz und Krone"
|
||||
],
|
||||
"hint": "Moony steht ganz am Anfang.",
|
||||
"question_en": "In what exact order are the four Marauders listed on the Marauder's Map?",
|
||||
"answer_en": "Moony, Wormtail, Padfoot, and Prongs",
|
||||
"options_en": [
|
||||
"Moony, Wormtail, Padfoot, and Prongs",
|
||||
"Prongs, Padfoot, Wormtail, and Moony",
|
||||
"Moony, Padfoot, Wormtail, and Prongs",
|
||||
"Padfoot, Moony, Wormtail, and Prongs"
|
||||
],
|
||||
"hint_en": "Moony is listed first, reflecting the reverse order of their deaths."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie lange muss der Vielsafttrank laut Rezeptur mindestens im Kessel köcheln?",
|
||||
"canonical_answer": "Einen Monat",
|
||||
"options": ["Einen Monat", "Zwei Wochen", "Sieben Tage", "Drei Monate"],
|
||||
"hint": "Unter anderem müssen Florfliegen 21 Tage lang gedünstet werden.",
|
||||
"question_en": "How long must the complex Polyjuice Potion simmer before it is ready to drink?",
|
||||
"answer_en": "One month",
|
||||
"options_en": ["One month", "Two weeks", "Seven days", "Three months"],
|
||||
"hint_en": "Lacewing flies alone must be stewed for 21 days."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Mit welchem lateinischstämmigen Zauber versiegelt man Türen gegen unbefugtes Betreten?",
|
||||
"canonical_answer": "Colloportus",
|
||||
"options": None,
|
||||
"hint": "Erzeugt ein eigenartiges matschiges Sauggeräusch beim Schließen.",
|
||||
"question_en": "Which locking spell seals doors securely, making an odd squelching noise?",
|
||||
"answer_en": "Colloportus",
|
||||
"options_en": None,
|
||||
"hint_en": "Frequently used by Hermione in the Department of Mysteries."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Wie lautet der Nachname der strengen Bibliothekarin der verbotenen Abteilung in Hogwarts?",
|
||||
"canonical_answer": "Madam Pince",
|
||||
"options": None,
|
||||
"hint": "Ihr Vorname ist Irma, ihr Nachname klingt wie 'zwicken' auf Englisch.",
|
||||
"question_en": "What is the surname of Hogwarts' notoriously strict head librarian?",
|
||||
"answer_en": "Madam Pince",
|
||||
"options_en": None,
|
||||
"hint_en": "Her first name is Irma; she guards the Restricted Section fiercely."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welcher Blutstatus trifft biologisch auf Lord Voldemort zu?",
|
||||
"canonical_answer": "Halbblut",
|
||||
"options": ["Halbblut", "Reinblut", "Muggelgeboren", "Squib"],
|
||||
"hint": "Sein Vater Tom Riddle Senior war ein gewöhnlicher Muggel.",
|
||||
"question_en": "What was Lord Voldemort's actual biological blood status?",
|
||||
"answer_en": "Half-blood",
|
||||
"options_en": ["Half-blood", "Pure-blood", "Muggle-born", "Squib"],
|
||||
"hint_en": "His father Tom Riddle Sr. was an ordinary Muggle."
|
||||
},
|
||||
{
|
||||
"category": "quidditch",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Auf welcher Position spielte Harrys Vater James Potter im Gryffindor-Quidditch-Team?",
|
||||
"canonical_answer": "Jäger",
|
||||
"options": ["Jäger", "Sucher", "Treiber", "Hüter"],
|
||||
"hint": "Obwohl er im Film mit einem Schnatz spielte, war er in den Büchern Jäger (Chaser).",
|
||||
"question_en": "Which position did Harry's father James Potter play on the Gryffindor Quidditch team in the books?",
|
||||
"answer_en": "Chaser",
|
||||
"options_en": ["Chaser", "Seeker", "Beater", "Keeper"],
|
||||
"hint_en": "Though often seen fiddling with a Snitch, J.K. Rowling confirmed he was a Chaser."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Was bedeutet der rückwärts gelesene Spruch auf dem Spiegel Nerhegeb ('Erised stra ehru oyt ube cafru oyt on wohsi')?",
|
||||
"canonical_answer": "Ich zeige nicht dein Gesicht, sondern deines Herzens Verlangen",
|
||||
"options": [
|
||||
"Ich zeige nicht dein Gesicht, sondern deines Herzens Verlangen",
|
||||
"Nur wer die Wahrheit sucht, wird seine Zukunft sehen",
|
||||
"Der Blick in die Vergangenheit heilt keine Wunden",
|
||||
"Wer mich erblickt, verliert den Verstand an seine Träume"
|
||||
],
|
||||
"hint": "'Nerhegeb' rückwärts gelesen ergibt 'Begehren'.",
|
||||
"question_en": "What does the reversed inscription on the Mirror of Erised mean?",
|
||||
"answer_en": "I show not your face but your heart's desire",
|
||||
"options_en": [
|
||||
"I show not your face but your heart's desire",
|
||||
"Only the pure of heart may see their future here",
|
||||
"Look not into the past lest you lose your present",
|
||||
"He who gazes within shall drown in their own dreams"
|
||||
],
|
||||
"hint_en": "'Erised' backwards spells 'Desire'."
|
||||
}
|
||||
]
|
||||
|
||||
def seed_40():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 1. Clear all existing questions as requested
|
||||
print("Clearing existing questions...")
|
||||
db.query(Question).delete()
|
||||
db.commit()
|
||||
|
||||
# 2. Insert the 40 new rich bilingual questions
|
||||
print(f"Inserting {len(QUESTIONS_DATA)} new bilingual questions...")
|
||||
for idx, q_data in enumerate(QUESTIONS_DATA, 1):
|
||||
options_json = json.dumps(q_data["options"], ensure_ascii=False) if q_data["options"] else None
|
||||
options_en_json = json.dumps(q_data["options_en"], ensure_ascii=False) if q_data["options_en"] else None
|
||||
|
||||
# Accepted answers list includes both German and English answers
|
||||
accepted = [q_data["canonical_answer"], q_data["answer_en"]]
|
||||
# Also common variations
|
||||
if q_data["canonical_answer"] == "9 3/4":
|
||||
accepted.extend(["9 3/4", "9 3/4", "9,75", "neun dreiviertel", "nine and three quarters"])
|
||||
elif q_data["canonical_answer"] == "Albus Dumbledore":
|
||||
accepted.append("Dumbledore")
|
||||
elif q_data["canonical_answer"] == "Tom Riddles Tagebuch":
|
||||
accepted.extend(["Tagebuch", "Diary", "Riddles Tagebuch"])
|
||||
|
||||
accepted_json = json.dumps(list(set(accepted)), ensure_ascii=False)
|
||||
|
||||
new_q = Question(
|
||||
id=idx,
|
||||
category=q_data["category"],
|
||||
difficulty=q_data["difficulty"],
|
||||
question_type=q_data["question_type"],
|
||||
question=q_data["question"],
|
||||
canonical_answer=q_data["canonical_answer"],
|
||||
options=options_json,
|
||||
hint=q_data["hint"],
|
||||
question_en=q_data["question_en"],
|
||||
answer_en=q_data["answer_en"],
|
||||
options_en=options_en_json,
|
||||
hint_en=q_data["hint_en"],
|
||||
accepted_answers=accepted_json,
|
||||
status="active",
|
||||
sort_order=idx,
|
||||
submitted_by="Zaubereiministerium"
|
||||
)
|
||||
db.add(new_q)
|
||||
|
||||
db.commit()
|
||||
print("Successfully seeded 40 bilingual questions across Easy, Medium, and Hard!")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
seed_40_questions = seed_40
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_40()
|
||||
@@ -83,6 +83,41 @@ body {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lang-switch-group {
|
||||
display: inline-flex;
|
||||
background: rgba(20, 22, 40, 0.85);
|
||||
border: 1px solid var(--border-gold);
|
||||
border-radius: 20px;
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 4px 9px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lang-btn:hover {
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.lang-btn.active {
|
||||
background: var(--gold);
|
||||
color: #0b0b16;
|
||||
box-shadow: 0 0 8px rgba(201, 168, 106, 0.4);
|
||||
}
|
||||
|
||||
.audio-btn {
|
||||
background: rgba(201, 168, 106, 0.15);
|
||||
border: 1px solid var(--gold);
|
||||
@@ -302,6 +337,57 @@ body {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.difficulty-selection {
|
||||
margin: 18px 0 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.difficulty-label {
|
||||
font-size: 0.95rem;
|
||||
color: var(--gold-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.difficulty-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.diff-chip {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--border-gold);
|
||||
color: var(--text-muted);
|
||||
border-radius: 20px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.88rem;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.diff-chip:hover {
|
||||
border-color: var(--gold);
|
||||
color: var(--text-main);
|
||||
transform: translateY(-1px);
|
||||
background: rgba(201, 168, 106, 0.12);
|
||||
}
|
||||
|
||||
.diff-chip.active {
|
||||
background: rgba(201, 168, 106, 0.25);
|
||||
border-color: var(--gold);
|
||||
color: var(--gold-bright);
|
||||
box-shadow: 0 0 10px rgba(201, 168, 106, 0.35);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
BUTTONS
|
||||
========================================= */
|
||||
@@ -446,6 +532,13 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
@@ -457,6 +550,30 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-diff {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.badge-diff-easy {
|
||||
background: rgba(46, 204, 113, 0.15);
|
||||
border-color: #2ecc71;
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.badge-diff-medium {
|
||||
background: rgba(243, 156, 18, 0.15);
|
||||
border-color: #f39c12;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.badge-diff-hard {
|
||||
background: rgba(231, 76, 60, 0.15);
|
||||
border-color: #e74c3c;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: rgba(243, 156, 18, 0.2);
|
||||
border-color: var(--warning);
|
||||
@@ -1055,4 +1172,14 @@ body {
|
||||
.admin-top-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lang-btn {
|
||||
padding: 4px 6px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.diff-chip {
|
||||
padding: 6px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
+166
-22
@@ -1,9 +1,72 @@
|
||||
let currentSessionId = null;
|
||||
let currentQuestionNumber = 1;
|
||||
let totalQuestions = 15;
|
||||
let totalQuestions = 10;
|
||||
let currentScore = 0;
|
||||
let attemptsUsed = 0;
|
||||
const maxAttempts = 3;
|
||||
let selectedDifficulty = "all";
|
||||
let lastQuestionData = null;
|
||||
|
||||
const UI_TEXT = {
|
||||
de: {
|
||||
pageTitle: "✨ Magisches Hogwarts-Escape-Quiz ✨",
|
||||
pageSubtitle: "10 knifflige Rätsel aus der Zauberwelt warten auf dich",
|
||||
envelopeLetter: "Hogwarts-Schule für Hexerei und Zauberei",
|
||||
introDesc: "Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?",
|
||||
diffLabel: "Schwierigkeitsgrad wählen:",
|
||||
diffAll: "Alle gemischt",
|
||||
diffEasy: "Leicht",
|
||||
diffMed: "Mittel",
|
||||
diffHard: "Schwer",
|
||||
startBtn: "📜 Siegel brechen & Brief öffnen",
|
||||
startBtnCasting: "✨ Zauber wird gewirkt...",
|
||||
pointsLabel: "🏆 Hauspunkte",
|
||||
questionLabel: "Frage",
|
||||
ofLabel: "von",
|
||||
attemptLabel: "Versuch",
|
||||
checkBtn: "✨ Zauber prüfen",
|
||||
hintBtn: "💡 Zaubertipp",
|
||||
hintHeading: "📜 Notiz aus der verbotenen Abteilung:",
|
||||
placeholder: "Deine Antwort hier eingeben...",
|
||||
noHint: "Kein Tipp vorhanden.",
|
||||
hintFailed: "Tipp konnte nicht geladen werden.",
|
||||
correctFeedback: "✨ Richtig! 1 Hauspunkt für dein Haus! ✨",
|
||||
wrongFeedback: "Leider falsch! Noch {attempts} Versuch(e).",
|
||||
exhaustedWrong: "Leider nicht richtig (3 Fehlversuche).",
|
||||
solutionLabel: "Richtige Lösung:",
|
||||
startError: "Fehler beim Starten des Quiz: ",
|
||||
networkError: "Netzwerkfehler: "
|
||||
},
|
||||
en: {
|
||||
pageTitle: "✨ Magical Hogwarts Escape Quiz ✨",
|
||||
pageSubtitle: "10 tricky wizarding world puzzles await you",
|
||||
envelopeLetter: "Hogwarts School of Witchcraft and Wizardry",
|
||||
introDesc: "An enchanted letter from Hogwarts is floating in the air before you.<br>Are you ready to take the exams and secure House Points?",
|
||||
diffLabel: "Choose difficulty level:",
|
||||
diffAll: "All mixed",
|
||||
diffEasy: "Easy",
|
||||
diffMed: "Medium",
|
||||
diffHard: "Hard",
|
||||
startBtn: "📜 Break Seal & Open Letter",
|
||||
startBtnCasting: "✨ Casting spell...",
|
||||
pointsLabel: "🏆 House Points",
|
||||
questionLabel: "Question",
|
||||
ofLabel: "of",
|
||||
attemptLabel: "Attempt",
|
||||
checkBtn: "✨ Check Spell",
|
||||
hintBtn: "💡 Magic Hint",
|
||||
hintHeading: "📜 Note from the Restricted Section:",
|
||||
placeholder: "Type your answer here...",
|
||||
noHint: "No hint available.",
|
||||
hintFailed: "Could not load hint.",
|
||||
correctFeedback: "✨ Correct! 1 House Point for your House! ✨",
|
||||
wrongFeedback: "Incorrect! {attempts} attempt(s) remaining.",
|
||||
exhaustedWrong: "Unfortunately incorrect (3 failed attempts).",
|
||||
solutionLabel: "Correct solution:",
|
||||
startError: "Error starting quiz: ",
|
||||
networkError: "Network error: "
|
||||
}
|
||||
};
|
||||
|
||||
// Web Audio API Sound Generator for magic chimes and buzzers
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
@@ -43,12 +106,63 @@ function playSound(type) {
|
||||
}
|
||||
}
|
||||
|
||||
function selectDifficulty(diff) {
|
||||
selectedDifficulty = diff;
|
||||
document.querySelectorAll(".diff-chip").forEach(chip => {
|
||||
chip.classList.toggle("active", chip.getAttribute("data-diff") === diff);
|
||||
});
|
||||
}
|
||||
|
||||
function onLanguageChange(lang) {
|
||||
applyQuizTranslations(lang);
|
||||
}
|
||||
|
||||
function applyQuizTranslations(lang) {
|
||||
const t = UI_TEXT[lang] || UI_TEXT.de;
|
||||
const pageTitle = document.getElementById("pageTitle");
|
||||
const pageSubtitle = document.getElementById("pageSubtitle");
|
||||
const envelopeLetter = document.getElementById("envelopeLetter");
|
||||
const introDesc = document.getElementById("introDesc");
|
||||
const diffLabel = document.getElementById("diffLabel");
|
||||
const diffAllText = document.getElementById("diffAllText");
|
||||
const diffEasyText = document.getElementById("diffEasyText");
|
||||
const diffMedText = document.getElementById("diffMedText");
|
||||
const diffHardText = document.getElementById("diffHardText");
|
||||
const startBtn = document.getElementById("startBtn");
|
||||
const checkBtn = document.getElementById("checkBtn");
|
||||
const hintBtn = document.getElementById("hintBtn");
|
||||
const hintBtnMC = document.getElementById("hintBtnMC");
|
||||
const hintHeading = document.getElementById("hintHeading");
|
||||
const answerInput = document.getElementById("answerInput");
|
||||
|
||||
if (pageTitle) pageTitle.innerText = t.pageTitle;
|
||||
if (pageSubtitle) pageSubtitle.innerText = t.pageSubtitle;
|
||||
if (envelopeLetter) envelopeLetter.innerText = t.envelopeLetter;
|
||||
if (introDesc) introDesc.innerHTML = t.introDesc;
|
||||
if (diffLabel) diffLabel.innerText = t.diffLabel;
|
||||
if (diffAllText) diffAllText.innerText = t.diffAll;
|
||||
if (diffEasyText) diffEasyText.innerText = t.diffEasy;
|
||||
if (diffMedText) diffMedText.innerText = t.diffMed;
|
||||
if (diffHardText) diffHardText.innerText = t.diffHard;
|
||||
if (startBtn && !startBtn.disabled) startBtn.innerText = t.startBtn;
|
||||
if (checkBtn) checkBtn.innerText = t.checkBtn;
|
||||
if (hintBtn) hintBtn.innerText = t.hintBtn;
|
||||
if (hintBtnMC) hintBtnMC.innerText = t.hintBtn;
|
||||
if (hintHeading) hintHeading.innerText = t.hintHeading;
|
||||
if (answerInput) answerInput.placeholder = t.placeholder;
|
||||
|
||||
if (lastQuestionData) {
|
||||
renderQuestion(lastQuestionData);
|
||||
}
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
const envelope = document.getElementById("envelope");
|
||||
const startBtn = document.getElementById("startBtn");
|
||||
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
||||
|
||||
startBtn.disabled = true;
|
||||
startBtn.innerText = "✨ Zauber wird gewirkt...";
|
||||
startBtn.innerText = t.startBtnCasting;
|
||||
envelope.classList.add("envelope-opened");
|
||||
|
||||
// Start background music if enabled or first click
|
||||
@@ -59,7 +173,7 @@ async function startGame() {
|
||||
const text = document.getElementById("audioText");
|
||||
if (icon && text) {
|
||||
icon.innerText = "🔊";
|
||||
text.innerText = "Musik: An";
|
||||
text.innerText = (currentLang === "de") ? "Musik: An" : "Music: On";
|
||||
}
|
||||
localStorage.setItem("bgm_enabled", "true");
|
||||
}).catch(() => {});
|
||||
@@ -68,11 +182,15 @@ async function startGame() {
|
||||
try {
|
||||
const res = await fetch("/api/game/start", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
lang: currentLang,
|
||||
difficulty: selectedDifficulty
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Konnte Spiel nicht starten.");
|
||||
throw new Error(currentLang === "en" ? "Could not start game session." : "Konnte Spiel nicht starten.");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
@@ -89,18 +207,39 @@ async function startGame() {
|
||||
}, 1100);
|
||||
|
||||
} catch (err) {
|
||||
alert("Fehler beim Starten des Quiz: " + err.message);
|
||||
alert(t.startError + err.message);
|
||||
startBtn.disabled = false;
|
||||
startBtn.innerText = "📜 Siegel brechen & Brief öffnen";
|
||||
startBtn.innerText = t.startBtn;
|
||||
envelope.classList.remove("envelope-opened");
|
||||
}
|
||||
}
|
||||
|
||||
function renderQuestion(qData) {
|
||||
document.getElementById("questionCounter").innerText = `Frage ${qData.question_number} von ${totalQuestions}`;
|
||||
document.getElementById("pointsCounter").innerText = `🏆 Hauspunkte: ${currentScore}`;
|
||||
lastQuestionData = qData;
|
||||
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
||||
|
||||
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
|
||||
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
|
||||
document.getElementById("badgeCategory").innerText = `⚡ ${qData.category.replace("_", " ").toUpperCase()}`;
|
||||
document.getElementById("attemptBadge").innerText = `Versuch ${attemptsUsed + 1} von ${maxAttempts}`;
|
||||
|
||||
// Difficulty badge
|
||||
const badgeDiff = document.getElementById("badgeDifficulty");
|
||||
if (badgeDiff) {
|
||||
const diff = qData.difficulty || "medium";
|
||||
badgeDiff.className = `badge badge-diff badge-diff-${diff}`;
|
||||
let diffText = t.diffMed;
|
||||
let diffIcon = "🟡";
|
||||
if (diff === "easy") {
|
||||
diffText = t.diffEasy;
|
||||
diffIcon = "🟢";
|
||||
} else if (diff === "hard") {
|
||||
diffText = t.diffHard;
|
||||
diffIcon = "🔴";
|
||||
}
|
||||
badgeDiff.innerText = `${diffIcon} ${diffText}`;
|
||||
}
|
||||
|
||||
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${attemptsUsed + 1} ${t.ofLabel} ${maxAttempts}`;
|
||||
document.getElementById("frageText").innerText = qData.question;
|
||||
|
||||
const textGroup = document.getElementById("textAnswerGroup");
|
||||
@@ -129,6 +268,7 @@ function renderQuestion(qData) {
|
||||
choiceGroup.classList.add("hidden");
|
||||
textGroup.classList.remove("hidden");
|
||||
input.value = "";
|
||||
input.placeholder = t.placeholder;
|
||||
input.disabled = false;
|
||||
document.getElementById("checkBtn").disabled = false;
|
||||
input.focus();
|
||||
@@ -160,6 +300,8 @@ function checkAnswer() {
|
||||
async function submitAnswer(answer, clickedBtn = null) {
|
||||
const checkBtn = document.getElementById("checkBtn");
|
||||
const input = document.getElementById("answerInput");
|
||||
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
||||
|
||||
if (checkBtn) checkBtn.disabled = true;
|
||||
if (input) input.disabled = true;
|
||||
|
||||
@@ -181,12 +323,12 @@ async function submitAnswer(answer, clickedBtn = null) {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Fehler bei der Antwortprüfung.");
|
||||
throw new Error(currentLang === "en" ? "Answer check failed." : "Fehler bei der Antwortprüfung.");
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
currentScore = result.score;
|
||||
document.getElementById("pointsCounter").innerText = `🏆 Hauspunkte: ${currentScore}`;
|
||||
document.getElementById("pointsCounter").innerText = `${t.pointsLabel}: ${currentScore}`;
|
||||
|
||||
const frageBox = document.getElementById("frageBox");
|
||||
const feedbackBox = document.getElementById("feedbackBox");
|
||||
@@ -199,7 +341,7 @@ async function submitAnswer(answer, clickedBtn = null) {
|
||||
clickedBtn.classList.add("choice-correct");
|
||||
}
|
||||
frageBox.classList.add("sparkle-effect");
|
||||
feedbackBox.innerHTML = `<span class="feedback-correct">✨ Richtig! 1 Hauspunkt für dein Haus! ✨</span>`;
|
||||
feedbackBox.innerHTML = `<span class="feedback-correct">${t.correctFeedback}</span>`;
|
||||
|
||||
setTimeout(() => {
|
||||
frageBox.classList.remove("sparkle-effect");
|
||||
@@ -223,13 +365,13 @@ async function submitAnswer(answer, clickedBtn = null) {
|
||||
setTimeout(() => frageBox.classList.remove("shake-effect"), 500);
|
||||
|
||||
attemptsUsed = result.attempts_used;
|
||||
document.getElementById("attemptBadge").innerText = `Versuch ${Math.min(attemptsUsed + 1, maxAttempts)} von ${maxAttempts}`;
|
||||
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
|
||||
|
||||
if (result.attempts_left === 0) {
|
||||
// Exhausted all attempts
|
||||
feedbackBox.innerHTML = `
|
||||
<div class="feedback-wrong">Leider nicht richtig (3 Fehlversuche).</div>
|
||||
<div class="feedback-revealed">Richtige Lösung: <strong>${result.revealed_answer}</strong></div>
|
||||
<div class="feedback-wrong">${t.exhaustedWrong}</div>
|
||||
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -244,7 +386,8 @@ async function submitAnswer(answer, clickedBtn = null) {
|
||||
|
||||
} else {
|
||||
// Still attempts remaining
|
||||
feedbackBox.innerHTML = `<span class="feedback-wrong">Leider falsch! Noch ${result.attempts_left} Versuch(e).</span>`;
|
||||
const msg = t.wrongFeedback.replace("{attempts}", result.attempts_left);
|
||||
feedbackBox.innerHTML = `<span class="feedback-wrong">${msg}</span>`;
|
||||
if (checkBtn) checkBtn.disabled = false;
|
||||
if (input) {
|
||||
input.disabled = false;
|
||||
@@ -256,7 +399,7 @@ async function submitAnswer(answer, clickedBtn = null) {
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
alert("Netzwerkfehler: " + err.message);
|
||||
alert(t.networkError + err.message);
|
||||
if (checkBtn) checkBtn.disabled = false;
|
||||
if (input) input.disabled = false;
|
||||
choiceBtns.forEach(b => b.disabled = false);
|
||||
@@ -267,22 +410,22 @@ async function requestHint() {
|
||||
if (!currentSessionId) return;
|
||||
const hintBox = document.getElementById("hintBox");
|
||||
const hintText = document.getElementById("hintText");
|
||||
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/game/hint?session_id=${encodeURIComponent(currentSessionId)}`);
|
||||
const data = await res.json();
|
||||
hintText.innerText = data.hint || "Kein Tipp vorhanden.";
|
||||
hintText.innerText = data.hint || t.noHint;
|
||||
hintBox.classList.remove("hidden");
|
||||
} catch (err) {
|
||||
hintText.innerText = "Tipp konnte nicht geladen werden.";
|
||||
hintText.innerText = t.hintFailed;
|
||||
hintBox.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function finishGame() {
|
||||
// 100% progress
|
||||
document.getElementById("progressBar").style.width = "100%";
|
||||
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}`;
|
||||
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&lang=${currentLang}`;
|
||||
}
|
||||
|
||||
// Enter key to submit
|
||||
@@ -296,4 +439,5 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
applyQuizTranslations(currentLang);
|
||||
});
|
||||
|
||||
+119
-9
@@ -1,32 +1,136 @@
|
||||
const SUGGESTION_TEXT = {
|
||||
de: {
|
||||
title: "✨ Neue Frage für das Quiz vorschlagen ✨",
|
||||
subtitle: "Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!",
|
||||
alert: "ℹ️ <strong>Hinweis zur Moderation:</strong> Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status <em>„Prüfung ausstehend“</em>. Unser Zaubereiministerium prüft und testet die Antwort, bevor die Frage freigeschaltet wird.",
|
||||
nameLabel: "Dein Name / Zauberer-Alias (optional):",
|
||||
namePlaceholder: "z. B. Hermine Granger",
|
||||
frageLabel: "Deine Frage: *",
|
||||
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
|
||||
diffLabel: "Schwierigkeitsgrad:",
|
||||
typeLabel: "Art der Frage:",
|
||||
optText: "🔤 Freitext-Antwort (Tippen mit Fehlertoleranz)",
|
||||
optMC: "🔘 Multiple Choice (Auswahl aus 4 Antwortmöglichkeiten)",
|
||||
antwortLabelText: "Richtige Antwort:",
|
||||
antwortLabelMC: "Richtige Antwort (die korrekte Option):",
|
||||
antwortHelpText: "Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.",
|
||||
antwortHelpMC: "Diese Antwort wird zusammen mit den falschen Optionen zufällig für den Spieler angeordnet.",
|
||||
wrongLabel: "Falsche Antwortmöglichkeiten (zur Auswahl für die Spieler):",
|
||||
wrongPlaceholder1: "Falsche Option 1 (z. B. Severus Snape)",
|
||||
wrongPlaceholder2: "Falsche Option 2 (z. B. Minerva McGonagall)",
|
||||
wrongPlaceholder3: "Falsche Option 3 (z. B. Rubeus Hagrid)",
|
||||
tippLabel: "Tipp / Hinweis (optional):",
|
||||
tippPlaceholder: "z. B. Er trägt eine Halbmondbrille.",
|
||||
submitBtn: "📜 Vorschlag an das Ministerium senden",
|
||||
submitBtnCasting: "✨ Vorschlag wird übertragen...",
|
||||
alertFillRequired: "Bitte gib mindestens die Frage und die richtige Antwort ein.",
|
||||
alertMCRequired: "Bitte gib bei Multiple-Choice mindestens eine falsche Antwortmöglichkeit an.",
|
||||
successHeader: "✨ Vorschlag erfolgreich übermittelt! ✨"
|
||||
},
|
||||
en: {
|
||||
title: "✨ Suggest a New Quiz Question ✨",
|
||||
subtitle: "Enrich the magical quiz with tricky Harry Potter or escape puzzles!",
|
||||
alert: "ℹ️ <strong>Moderation Note:</strong> Submitted questions are not immediately public; they automatically receive the status <em>'Pending Review'</em>. The Ministry of Magic tests and verifies the answer before approval.",
|
||||
nameLabel: "Your Name / Wizard Alias (optional):",
|
||||
namePlaceholder: "e.g. Hermione Granger",
|
||||
frageLabel: "Your Question: *",
|
||||
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
|
||||
diffLabel: "Difficulty Level:",
|
||||
typeLabel: "Question Type:",
|
||||
optText: "🔤 Free-Text Input (with typo tolerance)",
|
||||
optMC: "🔘 Multiple Choice (Selection of 4 options)",
|
||||
antwortLabelText: "Correct Answer:",
|
||||
antwortLabelMC: "Correct Answer (the winning option):",
|
||||
antwortHelpText: "Our system automatically matches minor typos and casing.",
|
||||
antwortHelpMC: "This option will be shuffled together with the false choices.",
|
||||
wrongLabel: "Wrong Answer Options (for player to choose from):",
|
||||
wrongPlaceholder1: "Wrong Option 1 (e.g. Severus Snape)",
|
||||
wrongPlaceholder2: "Wrong Option 2 (e.g. Minerva McGonagall)",
|
||||
wrongPlaceholder3: "Wrong Option 3 (e.g. Rubeus Hagrid)",
|
||||
tippLabel: "Hint / Clue (optional):",
|
||||
tippPlaceholder: "e.g. He wears half-moon spectacles.",
|
||||
submitBtn: "📜 Submit Proposal to the Ministry",
|
||||
submitBtnCasting: "✨ Transmitting proposal...",
|
||||
alertFillRequired: "Please enter at least the question and the correct answer.",
|
||||
alertMCRequired: "For Multiple Choice, please provide at least one incorrect option.",
|
||||
successHeader: "✨ Question successfully submitted! ✨"
|
||||
}
|
||||
};
|
||||
|
||||
function onLanguageChange(lang) {
|
||||
const t = SUGGESTION_TEXT[lang] || SUGGESTION_TEXT.de;
|
||||
const title = document.getElementById("suggTitle");
|
||||
const subtitle = document.getElementById("suggSubtitle");
|
||||
const alertBox = document.getElementById("suggAlert");
|
||||
const nameLabel = document.getElementById("suggNameLabel");
|
||||
const nameInput = document.getElementById("name");
|
||||
const frageLabel = document.getElementById("suggFrageLabel");
|
||||
const frageInput = document.getElementById("frage");
|
||||
const diffLabel = document.getElementById("suggDiffLabel");
|
||||
const typeLabel = document.getElementById("suggTypeLabel");
|
||||
const optText = document.getElementById("optText");
|
||||
const optMC = document.getElementById("optMC");
|
||||
const mcWrongLabel = document.getElementById("mcWrongLabel");
|
||||
const tippLabel = document.getElementById("suggTippLabel");
|
||||
const tippInput = document.getElementById("tipp");
|
||||
const submitBtn = document.getElementById("submitBtn");
|
||||
|
||||
if (title) title.innerText = t.title;
|
||||
if (subtitle) subtitle.innerText = t.subtitle;
|
||||
if (alertBox) alertBox.innerHTML = t.alert;
|
||||
if (nameLabel) nameLabel.innerText = t.nameLabel;
|
||||
if (nameInput) nameInput.placeholder = t.namePlaceholder;
|
||||
if (frageLabel) frageLabel.innerText = t.frageLabel;
|
||||
if (frageInput) frageInput.placeholder = t.fragePlaceholder;
|
||||
if (diffLabel) diffLabel.innerText = t.diffLabel;
|
||||
if (typeLabel) typeLabel.innerText = t.typeLabel;
|
||||
if (optText) optText.innerText = t.optText;
|
||||
if (optMC) optMC.innerText = t.optMC;
|
||||
if (mcWrongLabel) mcWrongLabel.innerText = t.wrongLabel;
|
||||
if (tippLabel) tippLabel.innerText = t.tippLabel;
|
||||
if (tippInput) tippInput.placeholder = t.tippPlaceholder;
|
||||
if (submitBtn && !submitBtn.disabled) submitBtn.innerText = t.submitBtn;
|
||||
|
||||
const qType = document.getElementById("q_type");
|
||||
if (qType) toggleVorschlagType(qType.value);
|
||||
}
|
||||
|
||||
function toggleVorschlagType(type) {
|
||||
const lang = (typeof currentLang !== "undefined") ? currentLang : (localStorage.getItem("quiz_lang") || "de");
|
||||
const t = SUGGESTION_TEXT[lang] || SUGGESTION_TEXT.de;
|
||||
|
||||
const mcGroup = document.getElementById("mcOptionsGroup");
|
||||
const antwortLabel = document.getElementById("antwortLabel");
|
||||
const antwortHelp = document.getElementById("antwortHelp");
|
||||
|
||||
if (type === "multiple_choice") {
|
||||
mcGroup.classList.remove("hidden");
|
||||
antwortLabel.innerText = "Richtige Antwort (die korrekte Option):";
|
||||
antwortHelp.innerText = "Diese Antwort wird zusammen mit den falschen Optionen zufällig für den Spieler angeordnet.";
|
||||
antwortLabel.innerText = t.antwortLabelMC;
|
||||
antwortHelp.innerText = t.antwortHelpMC;
|
||||
} else {
|
||||
mcGroup.classList.add("hidden");
|
||||
antwortLabel.innerText = "Richtige Antwort:";
|
||||
antwortHelp.innerText = "Unser System gleicht Tippfehler und Groß-/Kleinschreibung automatisch ab.";
|
||||
antwortLabel.innerText = t.antwortLabelText;
|
||||
antwortHelp.innerText = t.antwortHelpText;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSuggestion(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const lang = (typeof currentLang !== "undefined") ? currentLang : (localStorage.getItem("quiz_lang") || "de");
|
||||
const t = SUGGESTION_TEXT[lang] || SUGGESTION_TEXT.de;
|
||||
|
||||
const name = document.getElementById("name").value.trim();
|
||||
const frage = document.getElementById("frage").value.trim();
|
||||
const antwort = document.getElementById("antwort").value.trim();
|
||||
const tipp = document.getElementById("tipp").value.trim();
|
||||
const qType = document.getElementById("q_type").value;
|
||||
const diff = document.getElementById("diff_select") ? document.getElementById("diff_select").value : "medium";
|
||||
const submitBtn = document.getElementById("submitBtn");
|
||||
const resultBox = document.getElementById("resultMessage");
|
||||
|
||||
if (!frage || !antwort) {
|
||||
alert("Bitte gib mindestens die Frage und die richtige Antwort ein.");
|
||||
alert(t.alertFillRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,13 +142,13 @@ async function submitSuggestion(event) {
|
||||
falscheAntworten = [w1, w2, w3].filter(x => x.length > 0);
|
||||
|
||||
if (falscheAntworten.length === 0) {
|
||||
alert("Bitte gib bei Multiple-Choice mindestens eine falsche Antwortmöglichkeit an.");
|
||||
alert(t.alertMCRequired);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = "✨ Vorschlag wird übertragen...";
|
||||
submitBtn.innerText = t.submitBtnCasting;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
@@ -53,6 +157,7 @@ async function submitSuggestion(event) {
|
||||
antwort: antwort,
|
||||
tipp: tipp || null,
|
||||
question_type: qType,
|
||||
difficulty: diff,
|
||||
falsche_antworten: falscheAntworten
|
||||
};
|
||||
|
||||
@@ -67,7 +172,7 @@ async function submitSuggestion(event) {
|
||||
if (res.ok && data.success) {
|
||||
resultBox.innerHTML = `
|
||||
<div style="background: rgba(46, 204, 113, 0.2); border: 1px solid #2ecc71; color: #55efc4; padding: 15px; border-radius: 8px;">
|
||||
<h3>✨ Vorschlag erfolgreich übermittelt! ✨</h3>
|
||||
<h3>${t.successHeader}</h3>
|
||||
<p>${data.message}</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -91,6 +196,11 @@ async function submitSuggestion(event) {
|
||||
resultBox.classList.remove("hidden");
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = "📜 Vorschlag an das Ministerium senden";
|
||||
submitBtn.innerText = t.submitBtn;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const lang = (typeof currentLang !== "undefined") ? currentLang : (localStorage.getItem("quiz_lang") || "de");
|
||||
onLanguageChange(lang);
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<div class="q-header">
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<span class="badge badge-warning">⚠️ Prüfung ausstehend</span>
|
||||
<span class="badge badge-diff badge-diff-{{ q.difficulty or 'medium' }}">{{ (q.difficulty or 'medium')|capitalize }}</span>
|
||||
{% if q.question_type == 'multiple_choice' %}
|
||||
<span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span>
|
||||
{% else %}
|
||||
@@ -143,6 +144,7 @@
|
||||
<div class="q-header">
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<span class="badge badge-success">Reihenfolge #{{ q.sort_order }}</span>
|
||||
<span class="badge badge-diff badge-diff-{{ q.difficulty or 'medium' }}">{{ (q.difficulty or 'medium')|capitalize }}</span>
|
||||
{% if q.question_type == 'multiple_choice' %}
|
||||
<span class="badge" style="background: rgba(155, 89, 182, 0.2); border-color: #9b59b6; color: #d2b4de;">🔘 Multiple-Choice</span>
|
||||
{% else %}
|
||||
@@ -182,6 +184,14 @@
|
||||
<option value="multiple_choice" {% if q.question_type == 'multiple_choice' %}selected{% endif %}>🔘 Multiple Choice</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Schwierigkeitsgrad:</label>
|
||||
<select name="difficulty" class="input-text">
|
||||
<option value="easy" {% if q.difficulty == 'easy' %}selected{% endif %}>🟢 Leicht</option>
|
||||
<option value="medium" {% if q.difficulty == 'medium' or not q.difficulty %}selected{% endif %}>🟡 Mittel</option>
|
||||
<option value="hard" {% if q.difficulty == 'hard' %}selected{% endif %}>🔴 Schwer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Richtige Antwort:</label>
|
||||
<input type="text" name="canonical_answer" class="input-text" value="{{ q.canonical_answer }}" required>
|
||||
@@ -221,6 +231,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_diff">Schwierigkeitsgrad:</label>
|
||||
<select name="difficulty" id="new_diff" class="input-text">
|
||||
<option value="easy">🟢 Leicht</option>
|
||||
<option value="medium" selected>🟡 Mittel</option>
|
||||
<option value="hard">🔴 Schwer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="new_type">Fragentyp:</label>
|
||||
<select name="question_type" id="new_type" class="input-text" onchange="toggleAdminNewType(this.value)">
|
||||
|
||||
+62
-7
@@ -19,6 +19,11 @@
|
||||
</a>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="lang-switch-group" role="group" aria-label="Sprachauswahl / Language">
|
||||
<button type="button" id="langDe" class="lang-btn active" onclick="setLanguage('de')" title="Deutsch">🇩🇪 DE</button>
|
||||
<button type="button" id="langEn" class="lang-btn" onclick="setLanguage('en')" title="English">🇬🇧 EN</button>
|
||||
</div>
|
||||
|
||||
<button id="audioToggle" class="audio-btn" title="Musik an/aus" onclick="toggleAudio()">
|
||||
<span id="audioIcon">🔇</span>
|
||||
<span id="audioText" class="audio-label">Musik: Aus</span>
|
||||
@@ -30,9 +35,9 @@
|
||||
</div>
|
||||
|
||||
<nav id="navLinks" class="nav-links">
|
||||
<a href="/" class="nav-link" onclick="closeMobileMenu()">✨ Zum Quiz</a>
|
||||
<a href="/vorschlag" class="nav-link" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
|
||||
<a href="/admin" class="nav-link" onclick="closeMobileMenu()">🔒 Admin-Cockpit</a>
|
||||
<a href="/" class="nav-link" id="navQuiz" onclick="closeMobileMenu()">✨ Zum Quiz</a>
|
||||
<a href="/vorschlag" class="nav-link" id="navSuggest" onclick="closeMobileMenu()">✍️ Frage einreichen</a>
|
||||
<a href="/admin" class="nav-link" id="navAdmin" onclick="closeMobileMenu()">🔒 Admin-Cockpit</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -42,7 +47,7 @@
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p>
|
||||
<p id="footerText">⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -52,11 +57,56 @@
|
||||
const navLinks = document.getElementById("navLinks");
|
||||
const menuIcon = document.getElementById("menuIcon");
|
||||
|
||||
let currentLang = localStorage.getItem("quiz_lang") || "de";
|
||||
|
||||
function setLanguage(lang) {
|
||||
currentLang = (lang === "en") ? "en" : "de";
|
||||
localStorage.setItem("quiz_lang", currentLang);
|
||||
document.documentElement.lang = currentLang;
|
||||
updateLanguageUI();
|
||||
if (typeof onLanguageChange === "function") {
|
||||
onLanguageChange(currentLang);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLanguageUI() {
|
||||
const isDe = currentLang === "de";
|
||||
const deBtn = document.getElementById("langDe");
|
||||
const enBtn = document.getElementById("langEn");
|
||||
if (deBtn && enBtn) {
|
||||
deBtn.classList.toggle("active", isDe);
|
||||
enBtn.classList.toggle("active", !isDe);
|
||||
}
|
||||
|
||||
const navQuiz = document.getElementById("navQuiz");
|
||||
const navSuggest = document.getElementById("navSuggest");
|
||||
const navAdmin = document.getElementById("navAdmin");
|
||||
const footerText = document.getElementById("footerText");
|
||||
|
||||
if (navQuiz) navQuiz.innerText = isDe ? "✨ Zum Quiz" : "✨ To Quiz";
|
||||
if (navSuggest) navSuggest.innerText = isDe ? "✍️ Frage einreichen" : "✍️ Submit Question";
|
||||
if (navAdmin) navAdmin.innerText = isDe ? "🔒 Admin-Cockpit" : "🔒 Admin Cockpit";
|
||||
if (footerText) {
|
||||
footerText.innerText = isDe
|
||||
? "⚡ Magisches Hogwarts-Escape-Quiz · Betrieben mit Liebe & Zauberkraft ⚡"
|
||||
: "⚡ Magical Hogwarts Escape Quiz · Powered by Love & Wizardry ⚡";
|
||||
}
|
||||
if (audioText) {
|
||||
const isPlaying = bgm && !bgm.paused;
|
||||
if (isDe) {
|
||||
audioText.innerText = isPlaying ? "Musik: An" : "Musik: Aus";
|
||||
} else {
|
||||
audioText.innerText = isPlaying ? "Music: On" : "Music: Off";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAudio() {
|
||||
const isDe = currentLang === "de";
|
||||
if (bgm.paused) {
|
||||
bgm.play().then(() => {
|
||||
audioIcon.innerText = "🔊";
|
||||
audioText.innerText = "Musik: An";
|
||||
audioText.innerText = isDe ? "Musik: An" : "Music: On";
|
||||
localStorage.setItem("bgm_enabled", "true");
|
||||
}).catch(err => {
|
||||
console.log("Audio play blocked:", err);
|
||||
@@ -64,7 +114,7 @@
|
||||
} else {
|
||||
bgm.pause();
|
||||
audioIcon.innerText = "🔇";
|
||||
audioText.innerText = "Musik: Aus";
|
||||
audioText.innerText = isDe ? "Musik: Aus" : "Music: Off";
|
||||
localStorage.setItem("bgm_enabled", "false");
|
||||
}
|
||||
}
|
||||
@@ -84,11 +134,16 @@
|
||||
if (localStorage.getItem("bgm_enabled") === "true" && bgm.paused) {
|
||||
bgm.play().then(() => {
|
||||
audioIcon.innerText = "🔊";
|
||||
audioText.innerText = "Musik: An";
|
||||
audioText.innerText = (currentLang === "de") ? "Musik: An" : "Music: On";
|
||||
}).catch(() => {});
|
||||
}
|
||||
document.removeEventListener("click", initAudioOnInteraction);
|
||||
}, { once: true });
|
||||
|
||||
// Initialize UI on load
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
updateLanguageUI();
|
||||
});
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -1,35 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}🎉 Das große Finale – Magisches Quiz{% endblock %}
|
||||
{% block title %}{% if lang == 'en' %}🎉 The Grand Finale – Magical Quiz{% else %}🎉 Das große Finale – Magisches Quiz{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="quiz-wrapper">
|
||||
<div class="card finale-card">
|
||||
<div class="finale-header">
|
||||
<span class="sparkle-icon">✨</span>
|
||||
<h1 class="glow-title">🎉 Happy Birthday Schatz! 🎉</h1>
|
||||
<h1 class="glow-title" id="finaleTitle">🎉 Happy Birthday Schatz! 🎉</h1>
|
||||
<span class="sparkle-icon">✨</span>
|
||||
</div>
|
||||
|
||||
<div class="score-banner">
|
||||
<p class="score-highlight">
|
||||
<p class="score-highlight" id="finaleScoreText">
|
||||
{% if lang == 'en' %}
|
||||
You secured <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ total }}</span> possible House Points for your House!
|
||||
{% else %}
|
||||
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> möglichen Hauspunkten für dein Haus gesichert!
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="reward-box">
|
||||
<h2>💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛</h2>
|
||||
<p class="reward-description">
|
||||
<h2 id="rewardTitle">
|
||||
{% if lang == 'en' %}
|
||||
💛 Your Magical Reward: An Unforgettable Evening at Home 💛
|
||||
{% else %}
|
||||
💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛
|
||||
{% endif %}
|
||||
</h2>
|
||||
<p class="reward-description" id="rewardDesc">
|
||||
{% if lang == 'en' %}
|
||||
We'll order delicious food and make ourselves totally comfortable: relax, laugh, and enjoy quality time together.<br><br>
|
||||
Plus, we'll play a brand new <strong>Online Escape Game</strong> of your choice!
|
||||
{% else %}
|
||||
Wir holen uns lecker Essen und machen es uns so richtig gemütlich: entspannen, lachen und die gemeinsame Zeit genießen.<br><br>
|
||||
Dazu spielen wir ein brandneues <strong>Online Escape Game</strong> deiner Wahl!
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="love-note" id="loveNote">
|
||||
{% if lang == 'en' %}
|
||||
I'm so excited for our wonderful evening together! 💫
|
||||
{% else %}
|
||||
Ich freue mich riesig auf unseren gemeinsamen Abend! 💫
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="love-note">Ich freue mich riesig auf unseren gemeinsamen Abend! 💫</p>
|
||||
</div>
|
||||
|
||||
<div class="btn-group finale-actions">
|
||||
<a href="/" class="btn btn-primary">🔄 Erneut spielen</a>
|
||||
<a href="/vorschlag" class="btn btn-secondary">✍️ Eigene Frage vorschlagen</a>
|
||||
<a href="/" class="btn btn-primary" id="playAgainBtn">
|
||||
{% if lang == 'en' %}🔄 Play Again{% else %}🔄 Erneut spielen{% endif %}
|
||||
</a>
|
||||
<a href="/vorschlag" class="btn btn-secondary" id="suggestBtn">
|
||||
{% if lang == 'en' %}✍️ Suggest Your Own Question{% else %}✍️ Eigene Frage vorschlagen{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const finaleScore = {{ score }};
|
||||
const finaleTotal = {{ total }};
|
||||
|
||||
function onLanguageChange(lang) {
|
||||
const isDe = lang === "de";
|
||||
const title = document.getElementById("finaleTitle");
|
||||
const scoreText = document.getElementById("finaleScoreText");
|
||||
const rewardTitle = document.getElementById("rewardTitle");
|
||||
const rewardDesc = document.getElementById("rewardDesc");
|
||||
const loveNote = document.getElementById("loveNote");
|
||||
const playAgain = document.getElementById("playAgainBtn");
|
||||
const suggest = document.getElementById("suggestBtn");
|
||||
|
||||
if (title) title.innerText = isDe ? "🎉 Happy Birthday Schatz! 🎉" : "🎉 Happy Birthday Darling! 🎉";
|
||||
if (scoreText) {
|
||||
scoreText.innerHTML = isDe
|
||||
? `Du hast <span class="gold-text">${finaleScore}</span> von <span class="gold-text">${finaleTotal}</span> möglichen Hauspunkten für dein Haus gesichert!`
|
||||
: `You secured <span class="gold-text">${finaleScore}</span> out of <span class="gold-text">${finaleTotal}</span> possible House Points for your House!`;
|
||||
}
|
||||
if (rewardTitle) {
|
||||
rewardTitle.innerText = isDe
|
||||
? "💛 Dein magischer Gewinn: Ein unvergesslicher Abend Zuhause 💛"
|
||||
: "💛 Your Magical Reward: An Unforgettable Evening at Home 💛";
|
||||
}
|
||||
if (rewardDesc) {
|
||||
rewardDesc.innerHTML = isDe
|
||||
? "Wir holen uns lecker Essen und machen es uns so richtig gemütlich: entspannen, lachen und die gemeinsame Zeit genießen.<br><br>Dazu spielen wir ein brandneues <strong>Online Escape Game</strong> deiner Wahl!"
|
||||
: "We'll order delicious food and make ourselves totally comfortable: relax, laugh, and enjoy quality time together.<br><br>Plus, we'll play a brand new <strong>Online Escape Game</strong> of your choice!";
|
||||
}
|
||||
if (loveNote) {
|
||||
loveNote.innerText = isDe
|
||||
? "Ich freue mich riesig auf unseren gemeinsamen Abend! 💫"
|
||||
: "I'm so excited for our wonderful evening together! 💫";
|
||||
}
|
||||
if (playAgain) playAgain.innerText = isDe ? "🔄 Erneut spielen" : "🔄 Play Again";
|
||||
if (suggest) suggest.innerText = isDe ? "✍️ Eigene Frage vorschlagen" : "✍️ Suggest Your Own Question";
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
onLanguageChange(currentLang);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{% block content %}
|
||||
<div class="quiz-wrapper">
|
||||
<header class="quiz-header">
|
||||
<h1 class="glow-title">✨ Magisches Hogwarts-Escape-Quiz ✨</h1>
|
||||
<p class="subtitle">15 knifflige Rätsel aus der Zauberwelt warten auf dich</p>
|
||||
<h1 class="glow-title" id="pageTitle">✨ Magisches Hogwarts-Escape-Quiz ✨</h1>
|
||||
<p class="subtitle" id="pageSubtitle">10 knifflige Rätsel aus der Zauberwelt warten auf dich</p>
|
||||
</header>
|
||||
|
||||
<!-- Brief-Intro / Startmaske -->
|
||||
@@ -16,15 +16,35 @@
|
||||
<div class="envelope-flap"></div>
|
||||
<div class="wax-seal">H</div>
|
||||
<div class="letter-preview">
|
||||
<p class="letter-text">Hogwarts-Schule für Hexerei und Zauberei</p>
|
||||
<p class="letter-text" id="envelopeLetter">Hogwarts-Schule für Hexerei und Zauberei</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="intro-content">
|
||||
<p class="intro-text">
|
||||
<p class="intro-text" id="introDesc">
|
||||
Ein verzauberter Brief aus Hogwarts schwebt vor dir in der Luft.<br>
|
||||
Bist du bereit, die Prüfungen abzulegen und deine Hauspunkte zu sichern?
|
||||
</p>
|
||||
|
||||
<!-- Difficulty Selector Chips -->
|
||||
<div class="difficulty-selection">
|
||||
<span class="difficulty-label" id="diffLabel">Schwierigkeitsgrad wählen:</span>
|
||||
<div class="difficulty-chips" role="radiogroup" aria-label="Schwierigkeitsgrad">
|
||||
<button type="button" class="diff-chip active" data-diff="all" onclick="selectDifficulty('all')">
|
||||
<span class="diff-icon">🎲</span> <span class="diff-text" id="diffAllText">Alle gemischt</span>
|
||||
</button>
|
||||
<button type="button" class="diff-chip" data-diff="easy" onclick="selectDifficulty('easy')">
|
||||
<span class="diff-icon">🟢</span> <span class="diff-text" id="diffEasyText">Leicht</span>
|
||||
</button>
|
||||
<button type="button" class="diff-chip" data-diff="medium" onclick="selectDifficulty('medium')">
|
||||
<span class="diff-icon">🟡</span> <span class="diff-text" id="diffMedText">Mittel</span>
|
||||
</button>
|
||||
<button type="button" class="diff-chip" data-diff="hard" onclick="selectDifficulty('hard')">
|
||||
<span class="diff-icon">🔴</span> <span class="diff-text" id="diffHardText">Schwer</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="startBtn" class="btn btn-primary btn-large" onclick="startGame()">
|
||||
📜 Siegel brechen & Brief öffnen
|
||||
</button>
|
||||
@@ -36,7 +56,7 @@
|
||||
<!-- Fortschritt & Punkte -->
|
||||
<div class="status-panel">
|
||||
<div class="progress-info">
|
||||
<span id="questionCounter">Frage 1 von {{ question_count }}</span>
|
||||
<span id="questionCounter">Frage 1 von 10</span>
|
||||
<span id="pointsCounter" class="points-badge">🏆 Hauspunkte: 0</span>
|
||||
</div>
|
||||
<div class="progress-container">
|
||||
@@ -47,7 +67,10 @@
|
||||
<!-- Frage-Container -->
|
||||
<div id="frageBox" class="card question-card">
|
||||
<div class="question-meta">
|
||||
<span id="badgeCategory" class="badge">⚡ Harry Potter</span>
|
||||
<div class="meta-left">
|
||||
<span id="badgeCategory" class="badge">⚡ Harry Potter</span>
|
||||
<span id="badgeDifficulty" class="badge badge-diff">🟢 Leicht</span>
|
||||
</div>
|
||||
<span id="attemptBadge" class="attempt-badge">Versuch 1 von 3</span>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<div id="hintBox" class="hint-box hidden">
|
||||
<strong>📜 Notiz aus der verbotenen Abteilung:</strong>
|
||||
<strong id="hintHeading">📜 Notiz aus der verbotenen Abteilung:</strong>
|
||||
<p id="hintText"></p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,29 +5,38 @@
|
||||
{% block content %}
|
||||
<div class="quiz-wrapper">
|
||||
<div class="card vorschlag-card">
|
||||
<h1 class="glow-title">✨ Neue Frage für das Quiz vorschlagen ✨</h1>
|
||||
<p class="subtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p>
|
||||
<h1 class="glow-title" id="suggTitle">✨ Neue Frage für das Quiz vorschlagen ✨</h1>
|
||||
<p class="subtitle" id="suggSubtitle">Bereichere das magische Quiz mit kniffligen Harry-Potter- oder Escape-Rätseln!</p>
|
||||
|
||||
<div class="info-alert">
|
||||
<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.
|
||||
</div>
|
||||
|
||||
<form id="suggestionForm" class="magic-form" onsubmit="submitSuggestion(event)">
|
||||
<div class="form-group">
|
||||
<label for="name">Dein Name / Zauberer-Alias (optional):</label>
|
||||
<label for="name" id="suggNameLabel">Dein Name / Zauberer-Alias (optional):</label>
|
||||
<input type="text" id="name" name="name" class="input-text" placeholder="z. B. Hermine Granger">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="frage">Deine Frage: <span class="required">*</span></label>
|
||||
<label for="frage" id="suggFrageLabel">Deine Frage: <span class="required">*</span></label>
|
||||
<textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="q_type">Art der Frage:</label>
|
||||
<label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label>
|
||||
<select id="diff_select" name="difficulty" class="input-text">
|
||||
<option value="easy">🟢 Leicht / Easy</option>
|
||||
<option value="medium" selected>🟡 Mittel / Medium</option>
|
||||
<option value="hard">🔴 Schwer / Hard</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="q_type" id="suggTypeLabel">Art der Frage:</label>
|
||||
<select id="q_type" name="q_type" class="input-text" onchange="toggleVorschlagType(this.value)">
|
||||
<option value="text" selected>🔤 Freitext-Antwort (Tippen mit Fehlertoleranz)</option>
|
||||
<option value="multiple_choice">🔘 Multiple Choice (Auswahl aus 4 Antwortmöglichkeiten)</option>
|
||||
<option value="text" selected id="optText">🔤 Freitext-Antwort (Tippen mit Fehlertoleranz)</option>
|
||||
<option value="multiple_choice" id="optMC">🔘 Multiple Choice (Auswahl aus 4 Antwortmöglichkeiten)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -39,14 +48,14 @@
|
||||
|
||||
<!-- Zusätzliche Optionen für Multiple Choice -->
|
||||
<div id="mcOptionsGroup" class="form-group hidden" style="background: rgba(255,255,255,0.03); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-gold);">
|
||||
<label style="color: var(--gold);">Falsche Antwortmöglichkeiten (zur Auswahl für die Spieler):</label>
|
||||
<label id="mcWrongLabel" style="color: var(--gold);">Falsche Antwortmöglichkeiten (zur Auswahl für die Spieler):</label>
|
||||
<input type="text" id="wrong1" class="input-text" placeholder="Falsche Option 1 (z. B. Severus Snape)" style="margin-bottom: 8px;">
|
||||
<input type="text" id="wrong2" class="input-text" placeholder="Falsche Option 2 (z. B. Minerva McGonagall)" style="margin-bottom: 8px;">
|
||||
<input type="text" id="wrong3" class="input-text" placeholder="Falsche Option 3 (z. B. Rubeus Hagrid)">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tipp">Tipp / Hinweis (optional):</label>
|
||||
<label for="tipp" id="suggTippLabel">Tipp / Hinweis (optional):</label>
|
||||
<input type="text" id="tipp" name="tipp" class="input-text" placeholder="z. B. Er trägt eine Halbmondbrille.">
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from app.database import engine
|
||||
from sqlalchemy import text
|
||||
|
||||
def migrate():
|
||||
with engine.connect() as conn:
|
||||
# Questions table
|
||||
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS difficulty VARCHAR(20) DEFAULT 'medium'"))
|
||||
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS question_en TEXT NULL"))
|
||||
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS answer_en VARCHAR(255) NULL"))
|
||||
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS options_en TEXT NULL"))
|
||||
conn.execute(text("ALTER TABLE questions ADD COLUMN IF NOT EXISTS hint_en TEXT NULL"))
|
||||
|
||||
# Quiz sessions table
|
||||
conn.execute(text("ALTER TABLE quiz_sessions ADD COLUMN IF NOT EXISTS lang VARCHAR(10) DEFAULT 'de'"))
|
||||
conn.execute(text("ALTER TABLE quiz_sessions ADD COLUMN IF NOT EXISTS difficulty VARCHAR(20) DEFAULT 'all'"))
|
||||
conn.commit()
|
||||
print("Migration v3 successful: added difficulty, multilingual columns, and session preferences.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
Reference in New Issue
Block a user