Feature: Leaderboard difficulty isolation, proposal category select, numeric answer matcher, 25 new questions (65 total)
This commit is contained in:
+14
-3
@@ -386,7 +386,7 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
|
||||
return {"hint": hint_text}
|
||||
|
||||
@app.get("/finale", response_class=HTMLResponse)
|
||||
async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de"):
|
||||
async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de", diff: str = "all"):
|
||||
max_score = 1000 if (total <= 10 and score > 10) else (total * 100 if total <= 10 else total)
|
||||
tier = get_tier_evaluation(score, total, lang)
|
||||
if time >= 60:
|
||||
@@ -396,6 +396,8 @@ async def finale(request: Request, score: int = 0, total: int = 10, time: int =
|
||||
else:
|
||||
time_str = f"{time} Sek." if lang != "en" else f"{time} sec."
|
||||
|
||||
clean_diff = diff if diff in ["easy", "medium", "hard"] else "all"
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="finale.html",
|
||||
@@ -406,6 +408,7 @@ async def finale(request: Request, score: int = 0, total: int = 10, time: int =
|
||||
"time_seconds": time,
|
||||
"time_formatted": time_str,
|
||||
"lang": lang,
|
||||
"difficulty": clean_diff,
|
||||
"tier": tier
|
||||
}
|
||||
)
|
||||
@@ -460,7 +463,11 @@ async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = De
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
|
||||
better_count = db.query(LeaderboardEntry).filter(
|
||||
better_query = db.query(LeaderboardEntry)
|
||||
if entry.difficulty and entry.difficulty != "all":
|
||||
better_query = better_query.filter(LeaderboardEntry.difficulty == entry.difficulty)
|
||||
|
||||
better_count = better_query.filter(
|
||||
(LeaderboardEntry.score > entry.score) |
|
||||
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds < entry.time_seconds)) |
|
||||
((LeaderboardEntry.score == entry.score) & (LeaderboardEntry.time_seconds == entry.time_seconds) & (LeaderboardEntry.id < entry.id))
|
||||
@@ -469,6 +476,7 @@ async def submit_leaderboard(payload: LeaderboardSubmitRequest, db: Session = De
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"id": entry.id,
|
||||
"entry_id": entry.id,
|
||||
"rank": rank,
|
||||
"nickname": entry.nickname,
|
||||
@@ -530,10 +538,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
||||
opts.append(opt.strip())
|
||||
options_json = json.dumps(opts, ensure_ascii=False)
|
||||
|
||||
chosen_cat = payload.category if payload.category in ["hogwarts", "characters", "spells", "creatures", "quidditch", "lore", "general"] else "general"
|
||||
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
|
||||
|
||||
new_suggestion = Question(
|
||||
category="community",
|
||||
category=chosen_cat,
|
||||
question=payload.frage.strip(),
|
||||
canonical_answer=payload.antwort.strip(),
|
||||
hint=payload.tipp.strip() if payload.tipp else None,
|
||||
@@ -546,9 +555,11 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
||||
)
|
||||
db.add(new_suggestion)
|
||||
db.commit()
|
||||
db.refresh(new_suggestion)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"id": new_suggestion.id,
|
||||
"message": "Vielen Dank! Dein Vorschlag wurde eingereicht und wird von uns geprüft, bevor er im Quiz erscheint."
|
||||
}
|
||||
|
||||
|
||||
+56
-11
@@ -33,11 +33,47 @@ def levenshtein_distance(s1: str, s2: str) -> int:
|
||||
previous_row = current_row
|
||||
return previous_row[-1]
|
||||
|
||||
WORD_TO_NUM = {
|
||||
"null": "0", "zero": "0",
|
||||
"eins": "1", "ein": "1", "one": "1",
|
||||
"zwei": "2", "two": "2",
|
||||
"drei": "3", "three": "3",
|
||||
"vier": "4", "four": "4",
|
||||
"fuenf": "5", "five": "5",
|
||||
"sechs": "6", "six": "6",
|
||||
"sieben": "7", "seven": "7",
|
||||
"acht": "8", "eight": "8",
|
||||
"neun": "9", "nine": "9",
|
||||
"zehn": "10", "ten": "10",
|
||||
"elf": "11", "eleven": "11",
|
||||
"zwoelf": "12", "twelve": "12",
|
||||
"dreizehn": "13", "thirteen": "13",
|
||||
"vierzehn": "14", "fourteen": "14",
|
||||
"fuenfzehn": "15", "fifteen": "15",
|
||||
"sechzehn": "16", "sixteen": "16",
|
||||
"siebzehn": "17", "seventeen": "17",
|
||||
"achtzehn": "18", "eighteen": "18",
|
||||
"neunzehn": "19", "nineteen": "19",
|
||||
"zwanzig": "20", "twenty": "20",
|
||||
"fuenfzig": "50", "fifty": "50",
|
||||
"hundert": "100", "einhundert": "100", "one hundred": "100",
|
||||
"einhundertfuenfzig": "150", "one hundred fifty": "150", "one hundred and fifty": "150"
|
||||
}
|
||||
|
||||
NUM_TO_WORDS = {}
|
||||
for w, n in WORD_TO_NUM.items():
|
||||
NUM_TO_WORDS.setdefault(n, []).append(w)
|
||||
|
||||
def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None, canonical_en: str | None = None) -> bool:
|
||||
clean_user = normalize_text(user_input)
|
||||
if not clean_user:
|
||||
return False
|
||||
|
||||
# Also consider digit conversion of user input if a word was typed
|
||||
user_variants = [clean_user]
|
||||
if clean_user in WORD_TO_NUM:
|
||||
user_variants.append(WORD_TO_NUM[clean_user])
|
||||
|
||||
targets = [normalize_text(canonical)]
|
||||
if canonical_en:
|
||||
clean_en = normalize_text(canonical_en)
|
||||
@@ -56,18 +92,27 @@ def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None
|
||||
if part_clean:
|
||||
targets.append(part_clean)
|
||||
|
||||
for target in targets:
|
||||
# Expand any numeric target with corresponding number words and vice-versa
|
||||
expanded_targets = list(targets)
|
||||
for t in targets:
|
||||
if t in NUM_TO_WORDS:
|
||||
expanded_targets.extend(NUM_TO_WORDS[t])
|
||||
if t in WORD_TO_NUM:
|
||||
expanded_targets.append(WORD_TO_NUM[t])
|
||||
|
||||
for target in expanded_targets:
|
||||
if not target:
|
||||
continue
|
||||
if clean_user == target:
|
||||
return True
|
||||
# For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match
|
||||
if len(target) <= 3:
|
||||
continue
|
||||
# For medium words (4-6 chars), allow max 1 typo
|
||||
max_allowed = 1 if len(target) <= 6 else 2
|
||||
dist = levenshtein_distance(clean_user, target)
|
||||
if dist <= max_allowed:
|
||||
return True
|
||||
for u in user_variants:
|
||||
if u == target:
|
||||
return True
|
||||
# For short answers (1-3 chars, e.g. numbers or abbreviations), require exact match
|
||||
if len(target) <= 3 or len(u) <= 3:
|
||||
continue
|
||||
# For medium words (4-6 chars), allow max 1 typo
|
||||
max_allowed = 1 if len(target) <= 6 else 2
|
||||
dist = levenshtein_distance(u, target)
|
||||
if dist <= max_allowed:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -38,6 +38,7 @@ class GameCheckResponse(BaseModel):
|
||||
class SuggestionCreateRequest(BaseModel):
|
||||
frage: str
|
||||
antwort: str
|
||||
category: Optional[str] = "general"
|
||||
tipp: Optional[str] = None
|
||||
name: Optional[str] = "Gast"
|
||||
question_type: Optional[str] = "text"
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from sqlalchemy import text
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from app.database import engine, SessionLocal
|
||||
from app.models import Question
|
||||
|
||||
NEW_QUESTIONS_DATA = [
|
||||
# --- EASY (1-8) ---
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie heißt der gutmütige Wildhüter von Hogwarts und treue Freund von Harry?",
|
||||
"canonical_answer": "Rubeus Hagrid",
|
||||
"options": ["Rubeus Hagrid", "Argus Filch", "Garrick Ollivander", "Cornelius Fudge"],
|
||||
"hint": "Er liebt gefährliche magische Kreaturen wie Drachen und Riesenspinnen.",
|
||||
"question_en": "What is the name of the kind Hogwarts gamekeeper and loyal friend of Harry?",
|
||||
"answer_en": "Rubeus Hagrid",
|
||||
"options_en": ["Rubeus Hagrid", "Argus Filch", "Garrick Ollivander", "Cornelius Fudge"],
|
||||
"hint_en": "He loves dangerous magical creatures like dragons and giant spiders."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wie viele Jahre dauert die reguläre Schulzeit eines Schülers in Hogwarts von der 1. bis zur Abschlussklasse? (Bitte Zahl eingeben)",
|
||||
"canonical_answer": "7",
|
||||
"accepted_answers": json.dumps(["7", "sieben", "seven"]),
|
||||
"options": None,
|
||||
"hint": "Man startet mit 11 Jahren und legt im fünften Jahr die Z.A.G.-Prüfungen ab.",
|
||||
"question_en": "How many years does a student typically attend Hogwarts from first year to final graduation? (Please enter as a number)",
|
||||
"answer_en": "7",
|
||||
"options_en": None,
|
||||
"hint_en": "Students start at age 11 and take their N.E.W.T.s in their final year."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welcher Zauberspruch erzeugt helles Licht an der Spitze des Zauberstabs?",
|
||||
"canonical_answer": "Lumos",
|
||||
"options": ["Lumos", "Nox", "Alohomora", "Accio"],
|
||||
"hint": "Der Gegenfluch zum Erlöschen des Lichts lautet Nox.",
|
||||
"question_en": "Which wand-lighting charm produces bright light at the tip of the wand?",
|
||||
"answer_en": "Lumos",
|
||||
"options_en": ["Lumos", "Nox", "Alohomora", "Accio"],
|
||||
"hint_en": "Its counter-curse to extinguish the light is Nox."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welche Art von magischem Wesen bewacht die Falltür zum Stein der Weisen und hört auf den Namen Fluffy?",
|
||||
"canonical_answer": "Dreiköpfiger Hund",
|
||||
"options": ["Dreiköpfiger Hund", "Basilisken-Schlange", "Ungarischer Hornschwanz", "Zentaur"],
|
||||
"hint": "Er schläft sofort ein, sobald er Musik hört.",
|
||||
"question_en": "What kind of magical beast guards the trapdoor to the Philosopher's Stone and answers to the name Fluffy?",
|
||||
"answer_en": "Three-headed dog",
|
||||
"options_en": ["Three-headed dog", "Basilisk serpent", "Hungarian Horntail", "Centaur"],
|
||||
"hint_en": "He instantly falls fast asleep the moment music plays."
|
||||
},
|
||||
{
|
||||
"category": "quidditch",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welchen berühmten Besen erhält Harry in seinem ersten Schuljahr von Professor McGonagall geschenkt?",
|
||||
"canonical_answer": "Nimbus 2000",
|
||||
"options": ["Nimbus 2000", "Feuerblitz", "Sauberwisch 7", "Komet 260"],
|
||||
"hint": "Das damals schnellste Rennbesenmodell der Zauberwelt.",
|
||||
"question_en": "Which famous broomstick does Harry receive in his first year as a gift from Professor McGonagall?",
|
||||
"answer_en": "Nimbus 2000",
|
||||
"options_en": ["Nimbus 2000", "Firebolt", "Cleansweep Seven", "Comet 260"],
|
||||
"hint_en": "At the time, the fastest racing broom in the wizarding world."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wie heißt die berühmte Einkaufsstraße für Zauberbedarf im Herzen von London?",
|
||||
"canonical_answer": "Winkelgasse",
|
||||
"accepted_answers": json.dumps(["Winkelgasse", "Diagon Alley"]),
|
||||
"options": None,
|
||||
"hint": "Man erreicht sie durch das Klopfen an einer Backsteinmauer im Hinterhof des Tropfenden Kessels.",
|
||||
"question_en": "What is the name of the famous wizarding shopping alley located in London?",
|
||||
"answer_en": "Diagon Alley",
|
||||
"options_en": None,
|
||||
"hint_en": "Accessed by tapping bricks behind the Leaky Cauldron pub."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "easy",
|
||||
"question_type": "text",
|
||||
"question": "Wie lautet der Rufname von Harrys rothaarigem bestem Schulfreund?",
|
||||
"canonical_answer": "Ron",
|
||||
"accepted_answers": json.dumps(["Ron", "Ronald", "Ron Weasley", "Ronald Weasley"]),
|
||||
"options": None,
|
||||
"hint": "Vollständiger Name: Ronald Bilius Weasley.",
|
||||
"question_en": "What is the first name of Harry's red-haired best school friend?",
|
||||
"answer_en": "Ron",
|
||||
"options_en": None,
|
||||
"hint_en": "Full name: Ronald Bilius Weasley."
|
||||
},
|
||||
{
|
||||
"category": "general",
|
||||
"difficulty": "easy",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welches magische Transportmittel bringt die Schüler alljährlich am 1. September nach Hogwarts?",
|
||||
"canonical_answer": "Hogwarts-Express",
|
||||
"options": ["Hogwarts-Express", "Fliegender Ford Anglia", "Flohpulver-Netzwerk", "Nachtbus"],
|
||||
"hint": "Ein scharlachroter Dampfzug von Gleis 9 3/4.",
|
||||
"question_en": "Which magical transport takes students to Hogwarts every year on September 1st?",
|
||||
"answer_en": "Hogwarts Express",
|
||||
"options_en": ["Hogwarts Express", "Flying Ford Anglia", "Floo Powder Network", "Knight Bus"],
|
||||
"hint_en": "A scarlet steam engine leaving Platform 9 3/4."
|
||||
},
|
||||
|
||||
# --- MEDIUM (9-18) ---
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "In welchem Schuljahr müssen Hogwarts-Schüler ihre Z.A.G.-Prüfungen ablegen? (Bitte Zahl eingeben)",
|
||||
"canonical_answer": "5",
|
||||
"accepted_answers": json.dumps(["5", "fuenf", "five", "fünf", "5."]),
|
||||
"options": None,
|
||||
"hint": "In diesem Jahr treibt Großinquisitorin Dolores Umbridge ihr Unwesen an der Schule.",
|
||||
"question_en": "In which school year do Hogwarts students sit their O.W.L. exams? (Please enter as a number)",
|
||||
"answer_en": "5",
|
||||
"options_en": None,
|
||||
"hint_en": "The school year during which Dolores Umbridge torments Hogwarts."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Mit welchem Zauberspruch kann man verschlossene Schlösser, Türen und Fenster magisch entriegeln?",
|
||||
"canonical_answer": "Alohomora",
|
||||
"accepted_answers": json.dumps(["Alohomora"]),
|
||||
"options": None,
|
||||
"hint": "Hermine nutzt diesen Spruch im ersten Band, um dem Hausmeister Filch zu entkommen.",
|
||||
"question_en": "Which unlocking charm opens locked doors and windows?",
|
||||
"answer_en": "Alohomora",
|
||||
"options_en": None,
|
||||
"hint_en": "Hermione uses this spell in the first book to escape caretaker Filch."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welche Spezies magischer Wesen betreibt und bewacht die Zaubererbank Gringotts?",
|
||||
"canonical_answer": "Kobolde",
|
||||
"options": ["Kobolde", "Zwerge", "Hauselfen", "Zentauren"],
|
||||
"hint": "Griphook gehört diesem stolzen und scharfsinnigen Volk an.",
|
||||
"question_en": "Which species of magical beings operates and guards the wizarding bank Gringotts?",
|
||||
"answer_en": "Goblins",
|
||||
"options_en": ["Goblins", "Dwarves", "House-elves", "Centaurs"],
|
||||
"hint_en": "Griphook belongs to this clever and proud species."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welcher verzauberte Gegenstand erlaubte es Hermine im 3. Schuljahr, mehrere Unterrichtsstunden gleichzeitig zu besuchen?",
|
||||
"canonical_answer": "Zeitumkehrer",
|
||||
"options": ["Zeitumkehrer", "Denkarium", "Tarnumhang", "Spickoskop"],
|
||||
"hint": "Eine kleine Sanduhr an einer goldenen Halskette, vergeben von Professor McGonagall.",
|
||||
"question_en": "Which enchanted object allowed Hermione to attend multiple classes at the same time in her 3rd year?",
|
||||
"answer_en": "Time-Turner",
|
||||
"options_en": ["Time-Turner", "Pensieve", "Invisibility Cloak", "Sneakoscope"],
|
||||
"hint_en": "A miniature hourglass necklace authorized by Professor McGonagall."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welche Gestalt nimmt der mächtige Patronus von Harry Potter an?",
|
||||
"canonical_answer": "Hirsch",
|
||||
"options": ["Hirsch", "Silberne Hirschkuh", "Otter", "Phönix"],
|
||||
"hint": "Prongs – das Animagus-Tier seines Vaters James Potter.",
|
||||
"question_en": "What form does Harry Potter's corporeal Patronus take?",
|
||||
"answer_en": "Stag",
|
||||
"options_en": ["Stag", "Silver Doe", "Otter", "Phoenix"],
|
||||
"hint_en": "Prongs – the Animagus form of his father James Potter."
|
||||
},
|
||||
{
|
||||
"category": "quidditch",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Wie viele Spieler stehen insgesamt pro Quidditch-Mannschaft auf dem Spielfeld? (Bitte Zahl eingeben)",
|
||||
"canonical_answer": "7",
|
||||
"accepted_answers": json.dumps(["7", "sieben", "seven"]),
|
||||
"options": None,
|
||||
"hint": "3 Jäger, 2 Treiber, 1 Hüter und 1 Sucher.",
|
||||
"question_en": "How many players are on the pitch for each Quidditch team? (Please enter as a number)",
|
||||
"answer_en": "7",
|
||||
"options_en": None,
|
||||
"hint_en": "3 Chasers, 2 Beaters, 1 Keeper, and 1 Seeker."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welche Gestalt nimmt ein Irrwicht an, wenn er Professor Remus Lupin gegenübersteht?",
|
||||
"canonical_answer": "Vollmond",
|
||||
"accepted_answers": json.dumps(["Vollmond", "Mond", "Full Moon", "Moon"]),
|
||||
"options": ["Vollmond", "Silberner Dolch", "Dementor", "Korsett"],
|
||||
"hint": "Wegen seiner Lykanthropie fürchtet Lupin das Licht des Mondes.",
|
||||
"question_en": "What shape does a boggart take when facing Professor Remus Lupin?",
|
||||
"answer_en": "Full moon",
|
||||
"options_en": ["Full moon", "Silver dagger", "Dementor", "Corset"],
|
||||
"hint_en": "Because of his lycanthropy, Lupin fears the light of the moon."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "medium",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welcher dieser Zaubersprüche gehört NICHT zu den drei verbotenen Unverzeihlichen Flüchen?",
|
||||
"canonical_answer": "Sectumsempra",
|
||||
"options": ["Sectumsempra", "Crucio", "Imperio", "Avada Kedavra"],
|
||||
"hint": "Diesen finsteren Fluch erfand der Halbblutprinz für seine Feinde.",
|
||||
"question_en": "Which of these dark spells is NOT one of the three Unforgivable Curses?",
|
||||
"answer_en": "Sectumsempra",
|
||||
"options_en": ["Sectumsempra", "Crucio", "Imperio", "Avada Kedavra"],
|
||||
"hint_en": "Invented by the Half-Blood Prince 'for enemies'."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Wie heißt der stolze Hippogreif, der von Hagrid unterrichtet und später von Harry und Hermine gerettet wird?",
|
||||
"canonical_answer": "Seidenschnabel",
|
||||
"accepted_answers": json.dumps(["Seidenschnabel", "Buckbeak", "Witherwings"]),
|
||||
"options": None,
|
||||
"hint": "Vor seiner Berührung muss man ihm fest in die Augen blicken und sich verbeugen.",
|
||||
"question_en": "What is the name of the majestic Hippogriff introduced by Hagrid and later rescued by Harry and Hermione?",
|
||||
"answer_en": "Buckbeak",
|
||||
"options_en": None,
|
||||
"hint_en": "One must maintain eye contact and bow respectfully before approaching him."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "medium",
|
||||
"question_type": "text",
|
||||
"question": "Aus welchem Holz und magischem Kern besteht Harrys originaler Zauberstab?",
|
||||
"canonical_answer": "Stechpalme und Phönixfeder",
|
||||
"accepted_answers": json.dumps(["Stechpalme und Phönixfeder", "Stechpalme", "Phönixfeder", "Holly and phoenix feather", "Holly"]),
|
||||
"options": None,
|
||||
"hint": "Der Phönix Fawkes spendete genau zwei Federn – eine für Harry, eine für Voldemort.",
|
||||
"question_en": "What wood and core make up Harry Potter's original wand from Ollivanders?",
|
||||
"answer_en": "Holly and Phoenix Feather",
|
||||
"options_en": None,
|
||||
"hint_en": "The phoenix Fawkes gave exactly two feathers – one to Harry's wand and one to Voldemort's."
|
||||
},
|
||||
|
||||
# --- HARD (19-25) ---
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Wie alt muss eine Hexe oder ein Zauberer in Großbritannien sein, um die magische Volljährigkeit zu erreichen? (Bitte Zahl eingeben)",
|
||||
"canonical_answer": "17",
|
||||
"accepted_answers": json.dumps(["17", "siebzehn", "seventeen"]),
|
||||
"options": None,
|
||||
"hint": "Ab diesem Alter erlischt auch die magische 'Spur' des Zaubereiministeriums.",
|
||||
"question_en": "At what age does a witch or wizard in Britain legally come of age in the magical world? (Please enter as a number)",
|
||||
"answer_en": "17",
|
||||
"options_en": None,
|
||||
"hint_en": "At this age, the Ministry of Magic's 'Trace' breaks."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Wie lautet der vollständige Mädchenname von Lord Voldemorts unglücklicher Mutter?",
|
||||
"canonical_answer": "Merope Gaunt",
|
||||
"options": ["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Helena Ravenclaw"],
|
||||
"hint": "Sie verliebte sich in den Muggel Tom Riddle Senior und stammte von Salazar Slytherin ab.",
|
||||
"question_en": "What is the full maiden name of Lord Voldemort's tragic mother?",
|
||||
"answer_en": "Merope Gaunt",
|
||||
"options_en": ["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Helena Ravenclaw"],
|
||||
"hint_en": "A direct descendant of Salazar Slytherin who bewitched Tom Riddle Sr."
|
||||
},
|
||||
{
|
||||
"category": "creatures",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "Welche geflügelten, skelettartigen Wesen ziehen die scheinbar herrenlosen Kutschen von Hogsmeade zum Schloss?",
|
||||
"canonical_answer": "Thestrale",
|
||||
"options": ["Thestrale", "Abraxaner", "Hippogreife", "Gryffins"],
|
||||
"hint": "Sie sind unsichtbar für alle, die noch nie den Tod eines Menschen miterlebt haben.",
|
||||
"question_en": "Which winged, skeletal creatures pull the carriages from Hogsmeade to Hogwarts Castle?",
|
||||
"answer_en": "Thestrals",
|
||||
"options_en": ["Thestrals", "Abraxans", "Hippogriffs", "Griffins"],
|
||||
"hint_en": "They can only be seen by those who have witnessed death."
|
||||
},
|
||||
{
|
||||
"category": "lore",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Wie heißt das steinerne Becken im Büro des Schulleiters, in dem man Gedanken und Erinnerungen ablegen und betrachten kann?",
|
||||
"canonical_answer": "Denkarium",
|
||||
"accepted_answers": json.dumps(["Denkarium", "Pensieve"]),
|
||||
"options": None,
|
||||
"hint": "Auf Englisch wird es Pensieve genannt.",
|
||||
"question_en": "What is the name of the stone basin in the Headmaster's office used to view stored memories?",
|
||||
"answer_en": "Pensieve",
|
||||
"options_en": None,
|
||||
"hint_en": "In German it is known as Denkarium."
|
||||
},
|
||||
{
|
||||
"category": "spells",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Welcher Zauberspruch wird verwendet, um Erinnerungen aus dem Gedächtnis einer Person zu löschen oder zu verändern?",
|
||||
"canonical_answer": "Obliviate",
|
||||
"accepted_answers": json.dumps(["Obliviate", "Vergisszauber"]),
|
||||
"options": None,
|
||||
"hint": "Gilderoy Lockharts Spezialgebiet, der sich damit am Ende selbst das Gedächtnis raubte.",
|
||||
"question_en": "Which memory charm is used to erase or modify specific memories from a person's mind?",
|
||||
"answer_en": "Obliviate",
|
||||
"options_en": None,
|
||||
"hint_en": "Gilderoy Lockhart's specialty, which ultimately backfired on him."
|
||||
},
|
||||
{
|
||||
"category": "characters",
|
||||
"difficulty": "hard",
|
||||
"question_type": "multiple_choice",
|
||||
"question": "In welches winzige Insekt konnte sich die Klatschreporterin Rita Kimmkorn als unregistrierter Animagus verwandeln?",
|
||||
"canonical_answer": "Käfer",
|
||||
"options": ["Käfer", "Fliege", "Spinne", "Wespe"],
|
||||
"hint": "Hermine fängt sie in einem unzerbrechlichen Glasgefäß ein.",
|
||||
"question_en": "Into which tiny insect could gossip reporter Rita Skeeter transform as an unregistered Animagus?",
|
||||
"answer_en": "Beetle",
|
||||
"options_en": ["Beetle", "Fly", "Spider", "Wasp"],
|
||||
"hint_en": "Hermione captures her inside an unbreakable glass jar."
|
||||
},
|
||||
{
|
||||
"category": "hogwarts",
|
||||
"difficulty": "hard",
|
||||
"question_type": "text",
|
||||
"question": "Wie heißt der Poltergeist von Hogwarts, der Schülern Streiche spielt und nur vor dem Blutigen Baron Respekt hat?",
|
||||
"canonical_answer": "Peeves",
|
||||
"accepted_answers": json.dumps(["Peeves", "Peeves der Poltergeist"]),
|
||||
"options": None,
|
||||
"hint": "Er wirft gerne mit Kreide, Kandelabern und Stöcken nach Passanten.",
|
||||
"question_en": "What is the name of the Hogwarts poltergeist who terrorizes students and only fears the Bloody Baron?",
|
||||
"answer_en": "Peeves",
|
||||
"options_en": None,
|
||||
"hint_en": "He loves dropping chandeliers, throwing chalk, and singing mocking rhymes."
|
||||
}
|
||||
]
|
||||
|
||||
def seed_expand():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
inserted = 0
|
||||
for item in NEW_QUESTIONS_DATA:
|
||||
# Check if question already exists by question text
|
||||
existing = db.query(Question).filter(Question.question == item["question"]).first()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
opts_json = json.dumps(item["options"], ensure_ascii=False) if item.get("options") else None
|
||||
opts_en_json = json.dumps(item["options_en"], ensure_ascii=False) if item.get("options_en") else None
|
||||
|
||||
q = Question(
|
||||
category=item["category"],
|
||||
difficulty=item["difficulty"],
|
||||
question_type=item["question_type"],
|
||||
question=item["question"],
|
||||
canonical_answer=item["canonical_answer"],
|
||||
accepted_answers=item.get("accepted_answers"),
|
||||
options=opts_json,
|
||||
hint=item.get("hint"),
|
||||
question_en=item.get("question_en"),
|
||||
answer_en=item.get("answer_en"),
|
||||
options_en=opts_en_json,
|
||||
hint_en=item.get("hint_en"),
|
||||
status="active",
|
||||
sort_order=0
|
||||
)
|
||||
db.add(q)
|
||||
inserted += 1
|
||||
|
||||
db.commit()
|
||||
total_count = db.query(Question).filter(Question.status == "active").count()
|
||||
print(f"Successfully inserted {inserted} new questions! Total active questions: {total_count}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_expand()
|
||||
@@ -525,7 +525,7 @@ function finishGame() {
|
||||
if (timerInterval) clearInterval(timerInterval);
|
||||
saveRecentQuestions(sessionQuestionIds);
|
||||
document.getElementById("progressBar").style.width = "100%";
|
||||
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}`;
|
||||
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}&diff=${encodeURIComponent(selectedDifficulty)}`;
|
||||
}
|
||||
|
||||
// Enter key to submit
|
||||
|
||||
@@ -5,6 +5,14 @@ const SUGGESTION_TEXT = {
|
||||
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",
|
||||
catLabel: "Kategorie / Themenbereich:",
|
||||
catHogwarts: "🏰 Hogwarts & Schulgelände",
|
||||
catCharacters: "🧙 Charaktere & Personen",
|
||||
catSpells: "⚡ Zaubersprüche & Magie",
|
||||
catCreatures: "🐉 Magische Tierwesen",
|
||||
catQuidditch: "🧹 Quidditch & Sport",
|
||||
catLore: "🔮 Artefakte & Geschichte",
|
||||
catGeneral: "📜 Allgemeines Zauberwissen",
|
||||
frageLabel: "Deine Frage: *",
|
||||
fragePlaceholder: "z. B. Wie heißt der Schulleiter von Hogwarts?",
|
||||
diffLabel: "Schwierigkeitsgrad:",
|
||||
@@ -33,6 +41,14 @@ const SUGGESTION_TEXT = {
|
||||
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",
|
||||
catLabel: "Category / Topic Area:",
|
||||
catHogwarts: "🏰 Hogwarts & School Grounds",
|
||||
catCharacters: "🧙 Characters & People",
|
||||
catSpells: "⚡ Spells & Magic",
|
||||
catCreatures: "🐉 Magical Creatures",
|
||||
catQuidditch: "🧹 Quidditch & Sport",
|
||||
catLore: "🔮 Artifacts & Lore",
|
||||
catGeneral: "📜 General Wizarding Lore",
|
||||
frageLabel: "Your Question: *",
|
||||
fragePlaceholder: "e.g. Who is the Headmaster of Hogwarts?",
|
||||
diffLabel: "Difficulty Level:",
|
||||
@@ -75,11 +91,28 @@ function onLanguageChange(lang) {
|
||||
const tippInput = document.getElementById("tipp");
|
||||
const submitBtn = document.getElementById("submitBtn");
|
||||
|
||||
const catLabel = document.getElementById("suggCatLabel");
|
||||
const catHogwarts = document.getElementById("catHogwarts");
|
||||
const catCharacters = document.getElementById("catCharacters");
|
||||
const catSpells = document.getElementById("catSpells");
|
||||
const catCreatures = document.getElementById("catCreatures");
|
||||
const catQuidditch = document.getElementById("catQuidditch");
|
||||
const catLore = document.getElementById("catLore");
|
||||
const catGeneral = document.getElementById("catGeneral");
|
||||
|
||||
if (title) title.innerText = t.title;
|
||||
if (subtitle) subtitle.innerText = t.subtitle;
|
||||
if (alertBox) alertBox.innerHTML = t.alert;
|
||||
if (nameLabel) nameLabel.innerText = t.nameLabel;
|
||||
if (nameInput) nameInput.placeholder = t.namePlaceholder;
|
||||
if (catLabel) catLabel.innerText = t.catLabel;
|
||||
if (catHogwarts) catHogwarts.innerText = t.catHogwarts;
|
||||
if (catCharacters) catCharacters.innerText = t.catCharacters;
|
||||
if (catSpells) catSpells.innerText = t.catSpells;
|
||||
if (catCreatures) catCreatures.innerText = t.catCreatures;
|
||||
if (catQuidditch) catQuidditch.innerText = t.catQuidditch;
|
||||
if (catLore) catLore.innerText = t.catLore;
|
||||
if (catGeneral) catGeneral.innerText = t.catGeneral;
|
||||
if (frageLabel) frageLabel.innerText = t.frageLabel;
|
||||
if (frageInput) frageInput.placeholder = t.fragePlaceholder;
|
||||
if (diffLabel) diffLabel.innerText = t.diffLabel;
|
||||
@@ -151,8 +184,12 @@ async function submitSuggestion(event) {
|
||||
submitBtn.innerText = t.submitBtnCasting;
|
||||
|
||||
try {
|
||||
const catElem = document.getElementById("kategorie");
|
||||
const category = catElem ? catElem.value : "general";
|
||||
|
||||
const payload = {
|
||||
name: name || "Gast",
|
||||
category: category,
|
||||
frage: frage,
|
||||
antwort: antwort,
|
||||
tipp: tipp || null,
|
||||
|
||||
@@ -309,5 +309,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/admin.js?v=2.2"></script>
|
||||
<script src="/static/js/admin.js?v=2.3"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
||||
<title>{% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %}</title>
|
||||
<link rel="icon" href="/static/favicon.ico">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=2.2">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=2.3">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
const total = {{ total }};
|
||||
const maxScore = {{ max_score }};
|
||||
const timeSec = {{ time_seconds }};
|
||||
const difficulty = "{{ difficulty }}";
|
||||
let scoreAlreadySubmitted = false;
|
||||
|
||||
const WIZARD_NAMES = {
|
||||
@@ -350,7 +351,7 @@
|
||||
score: score,
|
||||
total_questions: total,
|
||||
time_seconds: timeSec,
|
||||
difficulty: "all",
|
||||
difficulty: difficulty,
|
||||
lang: currentLang
|
||||
})
|
||||
});
|
||||
@@ -386,7 +387,8 @@
|
||||
|
||||
async function loadMiniLeaderboard() {
|
||||
try {
|
||||
const res = await fetch("/api/leaderboard?limit=5");
|
||||
const url = "/api/leaderboard?limit=5" + (difficulty && difficulty !== "all" ? `&difficulty=${encodeURIComponent(difficulty)}` : "");
|
||||
const res = await fetch(url);
|
||||
const entries = await res.json();
|
||||
const body = document.getElementById("miniLbBody");
|
||||
const container = document.getElementById("miniLbContainer");
|
||||
|
||||
@@ -113,5 +113,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/quiz.js?v=2.2"></script>
|
||||
<script src="/static/js/quiz.js?v=2.3"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -23,6 +23,19 @@
|
||||
<textarea id="frage" name="frage" class="textarea" rows="3" placeholder="z. B. Wie heißt der Schulleiter von Hogwarts?" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="kategorie" id="suggCatLabel">Kategorie / Themenbereich:</label>
|
||||
<select id="kategorie" name="category" class="input-text">
|
||||
<option value="hogwarts" id="catHogwarts">🏰 Hogwarts & Schulgelände</option>
|
||||
<option value="characters" id="catCharacters" selected>🧙 Charaktere & Personen</option>
|
||||
<option value="spells" id="catSpells">⚡ Zaubersprüche & Magie</option>
|
||||
<option value="creatures" id="catCreatures">🐉 Magische Tierwesen</option>
|
||||
<option value="quidditch" id="catQuidditch">🧹 Quidditch & Sport</option>
|
||||
<option value="lore" id="catLore">🔮 Artefakte & Geschichte</option>
|
||||
<option value="general" id="catGeneral">📜 Allgemeines Zauberwissen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="diff_select" id="suggDiffLabel">Schwierigkeitsgrad:</label>
|
||||
<select id="diff_select" name="difficulty" class="input-text">
|
||||
@@ -70,5 +83,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/vorschlag.js?v=2.2"></script>
|
||||
<script src="/static/js/vorschlag.js?v=2.3"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user