Initial commit: Simple ASM - ARM assembly learning game
10-level progressive game teaching ARM assembly basics: registers, arithmetic, bitwise ops, memory, branching, loops. Vue 3 + FastAPI + SQLite with K8s deployment.
This commit is contained in:
34
backend/database.py
Normal file
34
backend/database.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import aiosqlite
|
||||
import os
|
||||
|
||||
DB_PATH = os.environ.get("DB_PATH", "app.db")
|
||||
|
||||
|
||||
async def get_db():
|
||||
db = await aiosqlite.connect(DB_PATH)
|
||||
db.row_factory = aiosqlite.Row
|
||||
await db.execute("PRAGMA journal_mode=WAL")
|
||||
return db
|
||||
|
||||
|
||||
async def init_db():
|
||||
db = await get_db()
|
||||
await db.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS progress (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
level_id INTEGER NOT NULL,
|
||||
stars INTEGER NOT NULL DEFAULT 0,
|
||||
code TEXT,
|
||||
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (player_id) REFERENCES players(id),
|
||||
UNIQUE (player_id, level_id)
|
||||
);
|
||||
""")
|
||||
await db.commit()
|
||||
await db.close()
|
||||
119
backend/main.py
Normal file
119
backend/main.py
Normal file
@@ -0,0 +1,119 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user