Feature: Leaderboard difficulty isolation, proposal category select, numeric answer matcher, 25 new questions (65 total)

This commit is contained in:
kobold-ai
2026-09-21 14:23:08 +02:00
parent a938123271
commit b1d88da681
11 changed files with 526 additions and 21 deletions
+14 -3
View File
@@ -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
View File
@@ -33,11 +33,47 @@ def levenshtein_distance(s1: str, s2: str) -> int:
previous_row = current_row
return previous_row[-1]
WORD_TO_NUM = {
"null": "0", "zero": "0",
"eins": "1", "ein": "1", "one": "1",
"zwei": "2", "two": "2",
"drei": "3", "three": "3",
"vier": "4", "four": "4",
"fuenf": "5", "five": "5",
"sechs": "6", "six": "6",
"sieben": "7", "seven": "7",
"acht": "8", "eight": "8",
"neun": "9", "nine": "9",
"zehn": "10", "ten": "10",
"elf": "11", "eleven": "11",
"zwoelf": "12", "twelve": "12",
"dreizehn": "13", "thirteen": "13",
"vierzehn": "14", "fourteen": "14",
"fuenfzehn": "15", "fifteen": "15",
"sechzehn": "16", "sixteen": "16",
"siebzehn": "17", "seventeen": "17",
"achtzehn": "18", "eighteen": "18",
"neunzehn": "19", "nineteen": "19",
"zwanzig": "20", "twenty": "20",
"fuenfzig": "50", "fifty": "50",
"hundert": "100", "einhundert": "100", "one hundred": "100",
"einhundertfuenfzig": "150", "one hundred fifty": "150", "one hundred and fifty": "150"
}
NUM_TO_WORDS = {}
for w, n in WORD_TO_NUM.items():
NUM_TO_WORDS.setdefault(n, []).append(w)
def is_answer_correct(user_input: str, canonical: str, accepted_raw: str | None = None, canonical_en: str | None = None) -> bool:
clean_user = normalize_text(user_input)
if not clean_user:
return False
# Also consider digit conversion of user input if a word was typed
user_variants = [clean_user]
if clean_user in WORD_TO_NUM:
user_variants.append(WORD_TO_NUM[clean_user])
targets = [normalize_text(canonical)]
if canonical_en:
clean_en = normalize_text(canonical_en)
@@ -56,18 +92,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
+1
View File
@@ -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"
+396
View File
@@ -0,0 +1,396 @@
import json
import os
import sys
from sqlalchemy import text
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.database import engine, SessionLocal
from app.models import Question
NEW_QUESTIONS_DATA = [
# --- EASY (1-8) ---
{
"category": "characters",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Wie heißt der gutmütige Wildhüter von Hogwarts und treue Freund von Harry?",
"canonical_answer": "Rubeus Hagrid",
"options": ["Rubeus Hagrid", "Argus Filch", "Garrick Ollivander", "Cornelius Fudge"],
"hint": "Er liebt gefährliche magische Kreaturen wie Drachen und Riesenspinnen.",
"question_en": "What is the name of the kind Hogwarts gamekeeper and loyal friend of Harry?",
"answer_en": "Rubeus Hagrid",
"options_en": ["Rubeus Hagrid", "Argus Filch", "Garrick Ollivander", "Cornelius Fudge"],
"hint_en": "He loves dangerous magical creatures like dragons and giant spiders."
},
{
"category": "lore",
"difficulty": "easy",
"question_type": "text",
"question": "Wie viele Jahre dauert die reguläre Schulzeit eines Schülers in Hogwarts von der 1. bis zur Abschlussklasse? (Bitte Zahl eingeben)",
"canonical_answer": "7",
"accepted_answers": json.dumps(["7", "sieben", "seven"]),
"options": None,
"hint": "Man startet mit 11 Jahren und legt im fünften Jahr die Z.A.G.-Prüfungen ab.",
"question_en": "How many years does a student typically attend Hogwarts from first year to final graduation? (Please enter as a number)",
"answer_en": "7",
"options_en": None,
"hint_en": "Students start at age 11 and take their N.E.W.T.s in their final year."
},
{
"category": "spells",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welcher Zauberspruch erzeugt helles Licht an der Spitze des Zauberstabs?",
"canonical_answer": "Lumos",
"options": ["Lumos", "Nox", "Alohomora", "Accio"],
"hint": "Der Gegenfluch zum Erlöschen des Lichts lautet Nox.",
"question_en": "Which wand-lighting charm produces bright light at the tip of the wand?",
"answer_en": "Lumos",
"options_en": ["Lumos", "Nox", "Alohomora", "Accio"],
"hint_en": "Its counter-curse to extinguish the light is Nox."
},
{
"category": "creatures",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welche Art von magischem Wesen bewacht die Falltür zum Stein der Weisen und hört auf den Namen Fluffy?",
"canonical_answer": "Dreiköpfiger Hund",
"options": ["Dreiköpfiger Hund", "Basilisken-Schlange", "Ungarischer Hornschwanz", "Zentaur"],
"hint": "Er schläft sofort ein, sobald er Musik hört.",
"question_en": "What kind of magical beast guards the trapdoor to the Philosopher's Stone and answers to the name Fluffy?",
"answer_en": "Three-headed dog",
"options_en": ["Three-headed dog", "Basilisk serpent", "Hungarian Horntail", "Centaur"],
"hint_en": "He instantly falls fast asleep the moment music plays."
},
{
"category": "quidditch",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welchen berühmten Besen erhält Harry in seinem ersten Schuljahr von Professor McGonagall geschenkt?",
"canonical_answer": "Nimbus 2000",
"options": ["Nimbus 2000", "Feuerblitz", "Sauberwisch 7", "Komet 260"],
"hint": "Das damals schnellste Rennbesenmodell der Zauberwelt.",
"question_en": "Which famous broomstick does Harry receive in his first year as a gift from Professor McGonagall?",
"answer_en": "Nimbus 2000",
"options_en": ["Nimbus 2000", "Firebolt", "Cleansweep Seven", "Comet 260"],
"hint_en": "At the time, the fastest racing broom in the wizarding world."
},
{
"category": "lore",
"difficulty": "easy",
"question_type": "text",
"question": "Wie heißt die berühmte Einkaufsstraße für Zauberbedarf im Herzen von London?",
"canonical_answer": "Winkelgasse",
"accepted_answers": json.dumps(["Winkelgasse", "Diagon Alley"]),
"options": None,
"hint": "Man erreicht sie durch das Klopfen an einer Backsteinmauer im Hinterhof des Tropfenden Kessels.",
"question_en": "What is the name of the famous wizarding shopping alley located in London?",
"answer_en": "Diagon Alley",
"options_en": None,
"hint_en": "Accessed by tapping bricks behind the Leaky Cauldron pub."
},
{
"category": "characters",
"difficulty": "easy",
"question_type": "text",
"question": "Wie lautet der Rufname von Harrys rothaarigem bestem Schulfreund?",
"canonical_answer": "Ron",
"accepted_answers": json.dumps(["Ron", "Ronald", "Ron Weasley", "Ronald Weasley"]),
"options": None,
"hint": "Vollständiger Name: Ronald Bilius Weasley.",
"question_en": "What is the first name of Harry's red-haired best school friend?",
"answer_en": "Ron",
"options_en": None,
"hint_en": "Full name: Ronald Bilius Weasley."
},
{
"category": "general",
"difficulty": "easy",
"question_type": "multiple_choice",
"question": "Welches magische Transportmittel bringt die Schüler alljährlich am 1. September nach Hogwarts?",
"canonical_answer": "Hogwarts-Express",
"options": ["Hogwarts-Express", "Fliegender Ford Anglia", "Flohpulver-Netzwerk", "Nachtbus"],
"hint": "Ein scharlachroter Dampfzug von Gleis 9 3/4.",
"question_en": "Which magical transport takes students to Hogwarts every year on September 1st?",
"answer_en": "Hogwarts Express",
"options_en": ["Hogwarts Express", "Flying Ford Anglia", "Floo Powder Network", "Knight Bus"],
"hint_en": "A scarlet steam engine leaving Platform 9 3/4."
},
# --- MEDIUM (9-18) ---
{
"category": "hogwarts",
"difficulty": "medium",
"question_type": "text",
"question": "In welchem Schuljahr müssen Hogwarts-Schüler ihre Z.A.G.-Prüfungen ablegen? (Bitte Zahl eingeben)",
"canonical_answer": "5",
"accepted_answers": json.dumps(["5", "fuenf", "five", "fünf", "5."]),
"options": None,
"hint": "In diesem Jahr treibt Großinquisitorin Dolores Umbridge ihr Unwesen an der Schule.",
"question_en": "In which school year do Hogwarts students sit their O.W.L. exams? (Please enter as a number)",
"answer_en": "5",
"options_en": None,
"hint_en": "The school year during which Dolores Umbridge torments Hogwarts."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "text",
"question": "Mit welchem Zauberspruch kann man verschlossene Schlösser, Türen und Fenster magisch entriegeln?",
"canonical_answer": "Alohomora",
"accepted_answers": json.dumps(["Alohomora"]),
"options": None,
"hint": "Hermine nutzt diesen Spruch im ersten Band, um dem Hausmeister Filch zu entkommen.",
"question_en": "Which unlocking charm opens locked doors and windows?",
"answer_en": "Alohomora",
"options_en": None,
"hint_en": "Hermione uses this spell in the first book to escape caretaker Filch."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche Spezies magischer Wesen betreibt und bewacht die Zaubererbank Gringotts?",
"canonical_answer": "Kobolde",
"options": ["Kobolde", "Zwerge", "Hauselfen", "Zentauren"],
"hint": "Griphook gehört diesem stolzen und scharfsinnigen Volk an.",
"question_en": "Which species of magical beings operates and guards the wizarding bank Gringotts?",
"answer_en": "Goblins",
"options_en": ["Goblins", "Dwarves", "House-elves", "Centaurs"],
"hint_en": "Griphook belongs to this clever and proud species."
},
{
"category": "lore",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher verzauberte Gegenstand erlaubte es Hermine im 3. Schuljahr, mehrere Unterrichtsstunden gleichzeitig zu besuchen?",
"canonical_answer": "Zeitumkehrer",
"options": ["Zeitumkehrer", "Denkarium", "Tarnumhang", "Spickoskop"],
"hint": "Eine kleine Sanduhr an einer goldenen Halskette, vergeben von Professor McGonagall.",
"question_en": "Which enchanted object allowed Hermione to attend multiple classes at the same time in her 3rd year?",
"answer_en": "Time-Turner",
"options_en": ["Time-Turner", "Pensieve", "Invisibility Cloak", "Sneakoscope"],
"hint_en": "A miniature hourglass necklace authorized by Professor McGonagall."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche Gestalt nimmt der mächtige Patronus von Harry Potter an?",
"canonical_answer": "Hirsch",
"options": ["Hirsch", "Silberne Hirschkuh", "Otter", "Phönix"],
"hint": "Prongs – das Animagus-Tier seines Vaters James Potter.",
"question_en": "What form does Harry Potter's corporeal Patronus take?",
"answer_en": "Stag",
"options_en": ["Stag", "Silver Doe", "Otter", "Phoenix"],
"hint_en": "Prongs – the Animagus form of his father James Potter."
},
{
"category": "quidditch",
"difficulty": "medium",
"question_type": "text",
"question": "Wie viele Spieler stehen insgesamt pro Quidditch-Mannschaft auf dem Spielfeld? (Bitte Zahl eingeben)",
"canonical_answer": "7",
"accepted_answers": json.dumps(["7", "sieben", "seven"]),
"options": None,
"hint": "3 Jäger, 2 Treiber, 1 Hüter und 1 Sucher.",
"question_en": "How many players are on the pitch for each Quidditch team? (Please enter as a number)",
"answer_en": "7",
"options_en": None,
"hint_en": "3 Chasers, 2 Beaters, 1 Keeper, and 1 Seeker."
},
{
"category": "characters",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welche Gestalt nimmt ein Irrwicht an, wenn er Professor Remus Lupin gegenübersteht?",
"canonical_answer": "Vollmond",
"accepted_answers": json.dumps(["Vollmond", "Mond", "Full Moon", "Moon"]),
"options": ["Vollmond", "Silberner Dolch", "Dementor", "Korsett"],
"hint": "Wegen seiner Lykanthropie fürchtet Lupin das Licht des Mondes.",
"question_en": "What shape does a boggart take when facing Professor Remus Lupin?",
"answer_en": "Full moon",
"options_en": ["Full moon", "Silver dagger", "Dementor", "Corset"],
"hint_en": "Because of his lycanthropy, Lupin fears the light of the moon."
},
{
"category": "spells",
"difficulty": "medium",
"question_type": "multiple_choice",
"question": "Welcher dieser Zaubersprüche gehört NICHT zu den drei verbotenen Unverzeihlichen Flüchen?",
"canonical_answer": "Sectumsempra",
"options": ["Sectumsempra", "Crucio", "Imperio", "Avada Kedavra"],
"hint": "Diesen finsteren Fluch erfand der Halbblutprinz für seine Feinde.",
"question_en": "Which of these dark spells is NOT one of the three Unforgivable Curses?",
"answer_en": "Sectumsempra",
"options_en": ["Sectumsempra", "Crucio", "Imperio", "Avada Kedavra"],
"hint_en": "Invented by the Half-Blood Prince 'for enemies'."
},
{
"category": "creatures",
"difficulty": "medium",
"question_type": "text",
"question": "Wie heißt der stolze Hippogreif, der von Hagrid unterrichtet und später von Harry und Hermine gerettet wird?",
"canonical_answer": "Seidenschnabel",
"accepted_answers": json.dumps(["Seidenschnabel", "Buckbeak", "Witherwings"]),
"options": None,
"hint": "Vor seiner Berührung muss man ihm fest in die Augen blicken und sich verbeugen.",
"question_en": "What is the name of the majestic Hippogriff introduced by Hagrid and later rescued by Harry and Hermione?",
"answer_en": "Buckbeak",
"options_en": None,
"hint_en": "One must maintain eye contact and bow respectfully before approaching him."
},
{
"category": "lore",
"difficulty": "medium",
"question_type": "text",
"question": "Aus welchem Holz und magischem Kern besteht Harrys originaler Zauberstab?",
"canonical_answer": "Stechpalme und Phönixfeder",
"accepted_answers": json.dumps(["Stechpalme und Phönixfeder", "Stechpalme", "Phönixfeder", "Holly and phoenix feather", "Holly"]),
"options": None,
"hint": "Der Phönix Fawkes spendete genau zwei Federn – eine für Harry, eine für Voldemort.",
"question_en": "What wood and core make up Harry Potter's original wand from Ollivanders?",
"answer_en": "Holly and Phoenix Feather",
"options_en": None,
"hint_en": "The phoenix Fawkes gave exactly two feathers – one to Harry's wand and one to Voldemort's."
},
# --- HARD (19-25) ---
{
"category": "lore",
"difficulty": "hard",
"question_type": "text",
"question": "Wie alt muss eine Hexe oder ein Zauberer in Großbritannien sein, um die magische Volljährigkeit zu erreichen? (Bitte Zahl eingeben)",
"canonical_answer": "17",
"accepted_answers": json.dumps(["17", "siebzehn", "seventeen"]),
"options": None,
"hint": "Ab diesem Alter erlischt auch die magische 'Spur' des Zaubereiministeriums.",
"question_en": "At what age does a witch or wizard in Britain legally come of age in the magical world? (Please enter as a number)",
"answer_en": "17",
"options_en": None,
"hint_en": "At this age, the Ministry of Magic's 'Trace' breaks."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Wie lautet der vollständige Mädchenname von Lord Voldemorts unglücklicher Mutter?",
"canonical_answer": "Merope Gaunt",
"options": ["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Helena Ravenclaw"],
"hint": "Sie verliebte sich in den Muggel Tom Riddle Senior und stammte von Salazar Slytherin ab.",
"question_en": "What is the full maiden name of Lord Voldemort's tragic mother?",
"answer_en": "Merope Gaunt",
"options_en": ["Merope Gaunt", "Morfin Gaunt", "Kendra Dumbledore", "Helena Ravenclaw"],
"hint_en": "A direct descendant of Salazar Slytherin who bewitched Tom Riddle Sr."
},
{
"category": "creatures",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "Welche geflügelten, skelettartigen Wesen ziehen die scheinbar herrenlosen Kutschen von Hogsmeade zum Schloss?",
"canonical_answer": "Thestrale",
"options": ["Thestrale", "Abraxaner", "Hippogreife", "Gryffins"],
"hint": "Sie sind unsichtbar für alle, die noch nie den Tod eines Menschen miterlebt haben.",
"question_en": "Which winged, skeletal creatures pull the carriages from Hogsmeade to Hogwarts Castle?",
"answer_en": "Thestrals",
"options_en": ["Thestrals", "Abraxans", "Hippogriffs", "Griffins"],
"hint_en": "They can only be seen by those who have witnessed death."
},
{
"category": "lore",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt das steinerne Becken im Büro des Schulleiters, in dem man Gedanken und Erinnerungen ablegen und betrachten kann?",
"canonical_answer": "Denkarium",
"accepted_answers": json.dumps(["Denkarium", "Pensieve"]),
"options": None,
"hint": "Auf Englisch wird es Pensieve genannt.",
"question_en": "What is the name of the stone basin in the Headmaster's office used to view stored memories?",
"answer_en": "Pensieve",
"options_en": None,
"hint_en": "In German it is known as Denkarium."
},
{
"category": "spells",
"difficulty": "hard",
"question_type": "text",
"question": "Welcher Zauberspruch wird verwendet, um Erinnerungen aus dem Gedächtnis einer Person zu löschen oder zu verändern?",
"canonical_answer": "Obliviate",
"accepted_answers": json.dumps(["Obliviate", "Vergisszauber"]),
"options": None,
"hint": "Gilderoy Lockharts Spezialgebiet, der sich damit am Ende selbst das Gedächtnis raubte.",
"question_en": "Which memory charm is used to erase or modify specific memories from a person's mind?",
"answer_en": "Obliviate",
"options_en": None,
"hint_en": "Gilderoy Lockhart's specialty, which ultimately backfired on him."
},
{
"category": "characters",
"difficulty": "hard",
"question_type": "multiple_choice",
"question": "In welches winzige Insekt konnte sich die Klatschreporterin Rita Kimmkorn als unregistrierter Animagus verwandeln?",
"canonical_answer": "Käfer",
"options": ["Käfer", "Fliege", "Spinne", "Wespe"],
"hint": "Hermine fängt sie in einem unzerbrechlichen Glasgefäß ein.",
"question_en": "Into which tiny insect could gossip reporter Rita Skeeter transform as an unregistered Animagus?",
"answer_en": "Beetle",
"options_en": ["Beetle", "Fly", "Spider", "Wasp"],
"hint_en": "Hermione captures her inside an unbreakable glass jar."
},
{
"category": "hogwarts",
"difficulty": "hard",
"question_type": "text",
"question": "Wie heißt der Poltergeist von Hogwarts, der Schülern Streiche spielt und nur vor dem Blutigen Baron Respekt hat?",
"canonical_answer": "Peeves",
"accepted_answers": json.dumps(["Peeves", "Peeves der Poltergeist"]),
"options": None,
"hint": "Er wirft gerne mit Kreide, Kandelabern und Stöcken nach Passanten.",
"question_en": "What is the name of the Hogwarts poltergeist who terrorizes students and only fears the Bloody Baron?",
"answer_en": "Peeves",
"options_en": None,
"hint_en": "He loves dropping chandeliers, throwing chalk, and singing mocking rhymes."
}
]
def seed_expand():
db = SessionLocal()
try:
inserted = 0
for item in NEW_QUESTIONS_DATA:
# Check if question already exists by question text
existing = db.query(Question).filter(Question.question == item["question"]).first()
if existing:
continue
opts_json = json.dumps(item["options"], ensure_ascii=False) if item.get("options") else None
opts_en_json = json.dumps(item["options_en"], ensure_ascii=False) if item.get("options_en") else None
q = Question(
category=item["category"],
difficulty=item["difficulty"],
question_type=item["question_type"],
question=item["question"],
canonical_answer=item["canonical_answer"],
accepted_answers=item.get("accepted_answers"),
options=opts_json,
hint=item.get("hint"),
question_en=item.get("question_en"),
answer_en=item.get("answer_en"),
options_en=opts_en_json,
hint_en=item.get("hint_en"),
status="active",
sort_order=0
)
db.add(q)
inserted += 1
db.commit()
total_count = db.query(Question).filter(Question.status == "active").count()
print(f"Successfully inserted {inserted} new questions! Total active questions: {total_count}")
finally:
db.close()
if __name__ == "__main__":
seed_expand()
+1 -1
View File
@@ -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
+37
View File
@@ -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,
+1 -1
View File
@@ -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 %}
+1 -1
View File
@@ -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>
+4 -2
View File
@@ -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");
+1 -1
View File
@@ -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 %}
+14 -1
View File
@@ -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 %}