From b1d88da6818a51d4f8883bb45e916316fa562e63 Mon Sep 17 00:00:00 2001 From: kobold-ai Date: Mon, 21 Sep 2026 14:23:08 +0200 Subject: [PATCH] Feature: Leaderboard difficulty isolation, proposal category select, numeric answer matcher, 25 new questions (65 total) --- app/main.py | 17 +- app/matcher.py | 67 +++++- app/schemas.py | 1 + app/seed_expand.py | 396 +++++++++++++++++++++++++++++++++++ app/static/js/quiz.js | 2 +- app/static/js/vorschlag.js | 37 ++++ app/templates/admin.html | 2 +- app/templates/base.html | 2 +- app/templates/finale.html | 6 +- app/templates/index.html | 2 +- app/templates/vorschlag.html | 15 +- 11 files changed, 526 insertions(+), 21 deletions(-) create mode 100644 app/seed_expand.py diff --git a/app/main.py b/app/main.py index ae32850..1623fe2 100755 --- a/app/main.py +++ b/app/main.py @@ -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." } diff --git a/app/matcher.py b/app/matcher.py index d2b9916..a751234 100755 --- a/app/matcher.py +++ b/app/matcher.py @@ -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 diff --git a/app/schemas.py b/app/schemas.py index 86fc535..94fd538 100755 --- a/app/schemas.py +++ b/app/schemas.py @@ -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" diff --git a/app/seed_expand.py b/app/seed_expand.py new file mode 100644 index 0000000..709f310 --- /dev/null +++ b/app/seed_expand.py @@ -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() diff --git a/app/static/js/quiz.js b/app/static/js/quiz.js index b32ba60..5bc06df 100755 --- a/app/static/js/quiz.js +++ b/app/static/js/quiz.js @@ -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 diff --git a/app/static/js/vorschlag.js b/app/static/js/vorschlag.js index 5c5f26e..b689332 100755 --- a/app/static/js/vorschlag.js +++ b/app/static/js/vorschlag.js @@ -5,6 +5,14 @@ const SUGGESTION_TEXT = { alert: "ℹ️ Hinweis zur Moderation: Eingereichte Fragen werden nicht sofort öffentlich sichtbar, sondern erhalten automatisch den Status „Prüfung ausstehend“. 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: "ℹ️ Moderation Note: Submitted questions are not immediately public; they automatically receive the status 'Pending Review'. 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, diff --git a/app/templates/admin.html b/app/templates/admin.html index 868306d..5b2ec1f 100755 --- a/app/templates/admin.html +++ b/app/templates/admin.html @@ -309,5 +309,5 @@ {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 0dbce72..7bb8b0e 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -5,7 +5,7 @@ {% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %} - + {% block head %}{% endblock %} diff --git a/app/templates/finale.html b/app/templates/finale.html index 65507d8..844bafb 100755 --- a/app/templates/finale.html +++ b/app/templates/finale.html @@ -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"); diff --git a/app/templates/index.html b/app/templates/index.html index 4db80b3..710a6ac 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -113,5 +113,5 @@ {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/app/templates/vorschlag.html b/app/templates/vorschlag.html index bfb7623..f8d854e 100755 --- a/app/templates/vorschlag.html +++ b/app/templates/vorschlag.html @@ -23,6 +23,19 @@ +
+ + +
+