10-level progressive game teaching ARM assembly basics: registers, arithmetic, bitwise ops, memory, branching, loops. Vue 3 + FastAPI + SQLite with K8s deployment.
120 lines
3.2 KiB
Python
120 lines
3.2 KiB
Python
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
|
|
from .database import get_db, init_db
|
|
|
|
FRONTEND_DIR = os.environ.get("FRONTEND_DIR", "../frontend/dist")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await init_db()
|
|
yield
|
|
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
|
|
|
|
class PlayerCreate(BaseModel):
|
|
name: str
|
|
|
|
|
|
class ProgressSave(BaseModel):
|
|
player_id: int
|
|
level_id: int
|
|
stars: int
|
|
code: str = ""
|
|
|
|
|
|
@app.get("/api/health")
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.post("/api/players")
|
|
async def create_or_get_player(data: PlayerCreate):
|
|
name = data.name.strip()
|
|
if not name:
|
|
raise HTTPException(400, "名字不能为空")
|
|
db = await get_db()
|
|
try:
|
|
row = await db.execute_fetchall(
|
|
"SELECT id, name FROM players WHERE name = ?", (name,)
|
|
)
|
|
if row:
|
|
player_id = row[0][0]
|
|
else:
|
|
cursor = await db.execute(
|
|
"INSERT INTO players (name) VALUES (?)", (name,)
|
|
)
|
|
await db.commit()
|
|
player_id = cursor.lastrowid
|
|
|
|
progress = await db.execute_fetchall(
|
|
"SELECT level_id, stars, code FROM progress WHERE player_id = ?",
|
|
(player_id,),
|
|
)
|
|
progress_dict = {
|
|
r[0]: {"stars": r[1], "code": r[2], "completed": True}
|
|
for r in progress
|
|
}
|
|
return {"id": player_id, "name": name, "progress": progress_dict}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
@app.post("/api/progress")
|
|
async def save_progress(data: ProgressSave):
|
|
db = await get_db()
|
|
try:
|
|
await db.execute(
|
|
"""INSERT INTO progress (player_id, level_id, stars, code)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT (player_id, level_id)
|
|
DO UPDATE SET stars = MAX(stars, excluded.stars),
|
|
code = excluded.code,
|
|
completed_at = CURRENT_TIMESTAMP""",
|
|
(data.player_id, data.level_id, data.stars, data.code),
|
|
)
|
|
await db.commit()
|
|
return {"success": True}
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
@app.get("/api/leaderboard")
|
|
async def leaderboard():
|
|
db = await get_db()
|
|
try:
|
|
rows = await db.execute_fetchall("""
|
|
SELECT p.name, COALESCE(SUM(pr.stars), 0) as total_stars,
|
|
COUNT(pr.id) as levels_completed
|
|
FROM players p
|
|
LEFT JOIN progress pr ON p.id = pr.player_id
|
|
GROUP BY p.id
|
|
ORDER BY total_stars DESC, levels_completed DESC
|
|
LIMIT 50
|
|
""")
|
|
return [
|
|
{"name": r[0], "total_stars": r[1], "levels_completed": r[2]}
|
|
for r in rows
|
|
]
|
|
finally:
|
|
await db.close()
|
|
|
|
|
|
# Serve frontend static files
|
|
if os.path.isdir(FRONTEND_DIR):
|
|
@app.get("/{full_path:path}")
|
|
async def serve_spa(full_path: str):
|
|
file_path = os.path.join(FRONTEND_DIR, full_path)
|
|
if full_path and os.path.isfile(file_path):
|
|
return FileResponse(file_path)
|
|
index = os.path.join(FRONTEND_DIR, "index.html")
|
|
return FileResponse(index)
|