Files
bootle/app/main.py
T

231 lines
9.0 KiB
Python
Executable File

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):
# 1: Poison
# 2: Wine
# 3: Ahead (Smallest / Dwarf - passes through black fire)
# 4: Poison
# 5: Poison
# 6: Wine
# 7: Back (Largest / Giant - passes through purple fire)
CLASSIC_SOLUTION = {
1: "poison",
2: "wine",
3: "ahead",
4: "poison",
5: "poison",
6: "wine",
7: "back"
}
def get_session_state(request: Request) -> dict:
if "escape" not in request.session:
request.session["escape"] = {
"session_id": str(uuid.uuid4()),
"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 request.session["escape"]
@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={"state": state, "lang": 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={"state": state, "lang": 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={"state": state, "lang": 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={"state": state, "lang": 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={"state": state, "lang": lang}
)
@app.get("/api/state")
async def api_get_state(request: Request):
state = get_session_state(request)
return JSONResponse(state)
@app.post("/api/reset")
async def api_reset_state(request: Request):
request.session["escape"] = {
"session_id": str(uuid.uuid4()),
"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))
request.session["escape"] = state
return JSONResponse({
"success": True,
"message": "Der Schlüssel passt ins Schloss! Die Tür zur Flammenkammer steht offen!",
"state": state
})
@app.post("/api/bottles/drink")
async def api_bottles_drink(request: Request):
"""
Check the bottle the player decided to drink to pass through the flames.
Bottle 3 is Ahead (Black fire -> Success).
Bottle 7 is Back (Purple fire -> Escaped back).
Bottles 1, 4, 5 are Poison (Fizzle/Defeat).
Bottles 2, 6 are Nettle Wine (Harmless wine).
"""
try:
data = await request.json()
except Exception:
data = {}
bottle_index = int(data.get("bottle", 0))
target_gate = data.get("gate", "ahead") # 'ahead' (black fire) or 'back' (purple fire)
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()
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,
"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."
})