import os import uuid import json import datetime from fastapi import FastAPI, Request, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware from dotenv import load_dotenv load_dotenv() app = FastAPI(title="Hogwarts Escape Adventure - Die Kammern des Steins der Weisen") SECRET_KEY = os.getenv("SECRET_KEY", "alohomora_bootle_potion_secret_2026") app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="escape_session") BASE_DIR = os.path.dirname(os.path.abspath(__file__)) app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static") templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates")) # Standard Classic Riddle Solution (Book Accurate): CLASSIC_SOLUTION = { 1: "poison", 2: "wine", 3: "ahead", 4: "poison", 5: "poison", 6: "wine", 7: "back" } ALL_ITEMS = { "wand": { "id": "wand", "name": "Zauberstab", "icon": "🪄", "desc": "Stechpalme und Phönixfeder, 11 Zoll. Liegt warm in deiner Hand.", "action": "lumos", "action_label": "Lumos / Licht 💡" }, "broom": { "id": "broom", "name": "Nimbus 2000", "icon": "🧹", "desc": "Ein pfeilschneller Rennbesen. Perfekt balanciert für Wendemanöver im Schlüsselraum.", "action": "broom_polish", "action_label": "Besen polieren ✨" }, "snape_scroll": { "id": "snape_scroll", "name": "Snapes Logik-Rolle", "icon": "📜", "desc": "Ein vergilbtes Pergament mit Snapes Vierzeilern zur Flammenkammer.", "action": "read_scroll", "action_label": "Rätsel-Verse lesen 📖" }, "silver_key": { "id": "silver_key", "name": "Geknickter Silberschlüssel", "icon": "🗝️", "desc": "Der schwere antike Schlüssel aus der Schlüsselkammer. Sein geknickter Flügel verrät ihn.", "action": "examine_key", "action_label": "Schloss prüfen 🔍" }, "philosophers_stone": { "id": "philosophers_stone", "name": "Stein der Weisen", "icon": "💎", "desc": "Der legendäre blutrote Alchemistenstein. Er schenkt unsterbliches Leben und pure Weisheit!", "action": "feel_power", "action_label": "Macht spüren ⚡" } } HOUSES = { "gryffindor": {"id": "gryffindor", "name": "Gryffindor", "icon": "🦁", "color": "#740001", "accent": "#d3a625"}, "slytherin": {"id": "slytherin", "name": "Slytherin", "icon": "🐍", "color": "#1a472a", "accent": "#aaaaaa"}, "ravenclaw": {"id": "ravenclaw", "name": "Ravenclaw", "icon": "🦅", "color": "#0e1a40", "accent": "#946b2d"}, "hufflepuff": {"id": "hufflepuff", "name": "Hufflepuff", "icon": "🦡", "color": "#ecb939", "accent": "#372e29"} } def get_session_state(request: Request) -> dict: if "escape" not in request.session: request.session["escape"] = { "session_id": str(uuid.uuid4()), "house": "gryffindor", "inventory": ["wand", "broom", "snape_scroll"], "broom_solved": False, "broom_score": 0, "broom_time": 0, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), "completed_at": None, "lang": "de" } else: # Ensure new fields exist for existing sessions state = request.session["escape"] if "inventory" not in state: state["inventory"] = ["wand", "broom", "snape_scroll"] if state.get("broom_solved"): state["inventory"].append("silver_key") if state.get("bottles_solved"): state["inventory"].append("philosophers_stone") if "house" not in state: state["house"] = "gryffindor" request.session["escape"] = state return request.session["escape"] def get_template_context(request: Request, state: dict, lang: str) -> dict: inv_ids = state.get("inventory", ["wand", "broom", "snape_scroll"]) inv_items = [ALL_ITEMS[i] for i in inv_ids if i in ALL_ITEMS] house_info = HOUSES.get(state.get("house", "gryffindor"), HOUSES["gryffindor"]) return { "state": state, "lang": lang, "inventory_items": inv_items, "all_items": ALL_ITEMS, "house": house_info, "houses": HOUSES } @app.get("/", response_class=HTMLResponse) async def page_index(request: Request): state = get_session_state(request) lang = request.query_params.get("lang", state.get("lang", "de")) state["lang"] = lang request.session["escape"] = state return templates.TemplateResponse( request=request, name="index.html", context=get_template_context(request, state, lang) ) @app.get("/broom", response_class=HTMLResponse) async def page_broom(request: Request): state = get_session_state(request) lang = request.query_params.get("lang", state.get("lang", "de")) state["lang"] = lang request.session["escape"] = state return templates.TemplateResponse( request=request, name="broom.html", context=get_template_context(request, state, lang) ) @app.get("/bottles", response_class=HTMLResponse) async def page_bottles(request: Request): state = get_session_state(request) lang = request.query_params.get("lang", state.get("lang", "de")) state["lang"] = lang request.session["escape"] = state return templates.TemplateResponse( request=request, name="bottles.html", context=get_template_context(request, state, lang) ) @app.get("/sliding", response_class=HTMLResponse) async def page_sliding(request: Request): state = get_session_state(request) lang = request.query_params.get("lang", state.get("lang", "de")) state["lang"] = lang request.session["escape"] = state return templates.TemplateResponse( request=request, name="sliding.html", context=get_template_context(request, state, lang) ) @app.get("/finale", response_class=HTMLResponse) async def page_finale(request: Request): state = get_session_state(request) lang = request.query_params.get("lang", state.get("lang", "de")) state["lang"] = lang request.session["escape"] = state return templates.TemplateResponse( request=request, name="finale.html", context=get_template_context(request, state, lang) ) @app.get("/api/state") async def api_get_state(request: Request): state = get_session_state(request) return JSONResponse(state) @app.get("/api/inventory") async def api_get_inventory(request: Request): state = get_session_state(request) inv_ids = state.get("inventory", ["wand", "broom", "snape_scroll"]) inv_items = [ALL_ITEMS[i] for i in inv_ids if i in ALL_ITEMS] house_info = HOUSES.get(state.get("house", "gryffindor"), HOUSES["gryffindor"]) return JSONResponse({ "inventory": inv_items, "house": house_info, "score": state.get("broom_score", 0), "broom_solved": state.get("broom_solved", False), "bottles_solved": state.get("bottles_solved", False), "sliding_solved": state.get("sliding_solved", False) }) @app.post("/api/player/house") async def api_set_house(request: Request): try: data = await request.json() except Exception: data = {} house_id = data.get("house", "gryffindor").lower() if house_id not in HOUSES: raise HTTPException(status_code=400, detail="Ungültiges Hogwarts-Haus.") state = get_session_state(request) state["house"] = house_id request.session["escape"] = state return JSONResponse({"success": True, "house": HOUSES[house_id]}) @app.post("/api/reset") async def api_reset_state(request: Request): request.session["escape"] = { "session_id": str(uuid.uuid4()), "house": "gryffindor", "inventory": ["wand", "broom", "snape_scroll"], "broom_solved": False, "broom_score": 0, "broom_time": 0, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), "completed_at": None, "lang": "de" } return JSONResponse({"success": True, "message": "Escape progress reset."}) @app.post("/api/broom/complete") async def api_broom_complete(request: Request): try: data = await request.json() except Exception: data = {} state = get_session_state(request) state["broom_solved"] = True state["broom_score"] = int(data.get("score", 100)) state["broom_time"] = int(data.get("time_taken", 30)) # Add Silver Key to Inventory! if "inventory" not in state: state["inventory"] = ["wand", "broom", "snape_scroll"] if "silver_key" not in state["inventory"]: state["inventory"].append("silver_key") request.session["escape"] = state return JSONResponse({ "success": True, "message": "Der Schlüssel passt ins Schloss! Die Tür zur Flammenkammer steht offen!", "new_item": ALL_ITEMS["silver_key"], "state": state }) @app.post("/api/bottles/drink") async def api_bottles_drink(request: Request): try: data = await request.json() except Exception: data = {} bottle_index = int(data.get("bottle", 0)) target_gate = data.get("gate", "ahead") lang = data.get("lang", "de") if bottle_index not in CLASSIC_SOLUTION: raise HTTPException(status_code=400, detail="Ungültige Flaschennummer.") content = CLASSIC_SOLUTION[bottle_index] state = get_session_state(request) if content == "ahead": if target_gate == "ahead": state["bottles_solved"] = True if state.get("broom_solved"): state["completed_at"] = datetime.datetime.utcnow().isoformat() if "inventory" not in state: state["inventory"] = ["wand", "broom", "snape_scroll"] if "philosophers_stone" not in state["inventory"]: state["inventory"].append("philosophers_stone") request.session["escape"] = state msg_de = "Ein eisiger Schauer durchströmt dich! Das schwarze Feuer weicht vor dir zurück. Du trittst siegreich in die Schatzkammer des Steins der Weisen!" msg_en = "An icy chill flows through you! The black flames retreat. You step victoriously into the chamber of the Philosopher's Stone!" return JSONResponse({ "success": True, "result": "success_ahead", "content": content, "message": msg_de if lang == "de" else msg_en, "new_item": ALL_ITEMS["philosophers_stone"], "next_url": "/finale" }) else: msg_de = "Dieser Trank schützt dich nur vor dem schwarzen Feuer, nicht vor dem violetten!" msg_en = "This potion only protects against black fire, not purple fire!" return JSONResponse({"success": False, "result": "wrong_gate", "content": content, "message": msg_de if lang == "de" else msg_en}) elif content == "back": if target_gate == "back": msg_de = "Ein wohliger Kälteschleier hüllt dich ein. Du kannst sicher durch die violetten Flammen zurückweichen." msg_en = "A protective cold mist envelops you. You can safely retreat through the purple flames." return JSONResponse({ "success": True, "result": "success_back", "content": content, "message": msg_de if lang == "de" else msg_en, "next_url": "/broom" }) else: msg_de = "Dieser Trank schützt vor den violetten Flammen, führt dich aber nicht vorwärts durch das schwarze Feuer!" msg_en = "This potion protects against purple flames, but does not allow forward passage through black fire!" return JSONResponse({"success": False, "result": "wrong_gate", "content": content, "message": msg_de if lang == "de" else msg_en}) elif content == "poison": msg_de = "💀 Gift! Dein Hals brennt wie Feuer. Ein Zauber bricht die Wirkung gerade noch rechtzeitig ab, doch du musst es erneut versuchen!" msg_en = "💀 Poison! Your throat burns. A counter-curse breaks the effect just in time, but you must try again!" return JSONResponse({"success": False, "result": "poison", "content": content, "message": msg_de if lang == "de" else msg_en}) elif content == "wine": msg_de = "🍷 Milder Brennnesselwein! Köstlich erfrischend, aber ohne jegliche magische Schutzwirkung gegen die verzehrenden Flammen." msg_en = "🍷 Mild nettle wine! Deliciously refreshing, but offering zero magical protection against the consuming flames." return JSONResponse({"success": False, "result": "wine", "content": content, "message": msg_de if lang == "de" else msg_en}) @app.post("/api/sliding/complete") async def api_sliding_complete(request: Request): state = get_session_state(request) state["sliding_solved"] = True request.session["escape"] = state return JSONResponse({ "success": True, "message": "Das Alchemie-Schiebesiegel ist gelöst! Die geheime Essenz wurde freigegeben." })