Initial commit: Harry Potter Escape Adventure on bootle.dev.waiswuaschd.de
This commit is contained in:
Executable
+230
@@ -0,0 +1,230 @@
|
||||
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."
|
||||
})
|
||||
Reference in New Issue
Block a user