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:
2026-04-07 10:17:15 +01:00
commit e465b1cf71
29 changed files with 3515 additions and 0 deletions

34
backend/database.py Normal file
View 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
View 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)