# app.py ๐ŸŽญ # One file, two personalities: # - RUN_MODE=api -> FastAPI JSON multiplayer relay # - RUN_MODE=admin -> Streamlit admin console # # Shared state uses SQLite so both processes agree on reality. # (In-memory is where bugs go to reproduce. ๐Ÿ›๐Ÿ’ž) import os, time, json, secrets, string, sqlite3 from typing import Any, Dict, Optional, List DB_PATH = os.getenv("GAME_DB_PATH", "/data/game.sqlite") # HF persistent storage uses /data ๐Ÿ’พ CORS_ORIGINS = os.getenv("CORS_ORIGINS", "https://allaiinc.org").split(",") def now_i() -> int: return int(time.time()) def rid(n=6) -> str: alphabet = string.ascii_uppercase + string.digits return "".join(secrets.choice(alphabet) for _ in range(n)) def tok() -> str: return secrets.token_urlsafe(24) def db() -> sqlite3.Connection: # check_same_thread=False because Streamlit/FastAPI like to multitask ๐Ÿคน conn = sqlite3.connect(DB_PATH, check_same_thread=False) conn.row_factory = sqlite3.Row return conn def init_db(): os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) con = db() cur = con.cursor() cur.executescript(""" CREATE TABLE IF NOT EXISTS rooms( room TEXT PRIMARY KEY, created INTEGER NOT NULL, seq INTEGER NOT NULL, public_json TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS players( token TEXT PRIMARY KEY, room TEXT NOT NULL, seat INTEGER NOT NULL, name TEXT NOT NULL, last_seen INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS private_state( room TEXT NOT NULL, seat INTEGER NOT NULL, private_json TEXT NOT NULL, PRIMARY KEY(room, seat) ); CREATE TABLE IF NOT EXISTS events( room TEXT NOT NULL, seq INTEGER NOT NULL, t INTEGER NOT NULL, kind TEXT NOT NULL, data_json TEXT NOT NULL, PRIMARY KEY(room, seq) ); """) con.commit() con.close() def room_get(con, room: str): r = con.execute("SELECT * FROM rooms WHERE room=?", (room,)).fetchone() return r def event_append(con, room: str, kind: str, data: Dict[str, Any]) -> int: r = room_get(con, room) if not r: raise ValueError("room not found") seq = int(r["seq"]) + 1 con.execute("UPDATE rooms SET seq=? WHERE room=?", (seq, room)) con.execute( "INSERT INTO events(room, seq, t, kind, data_json) VALUES(?,?,?,?,?)", (room, seq, now_i(), kind, json.dumps(data, ensure_ascii=False)) ) return seq def players_roster(con, room: str) -> List[Dict[str, Any]]: rows = con.execute("SELECT seat, name, last_seen FROM players WHERE room=? ORDER BY seat", (room,)).fetchall() return [dict(r) for r in rows] def player_by_token(con, token: str): return con.execute("SELECT * FROM players WHERE token=?", (token,)).fetchone() def public_get(con, room: str) -> Dict[str, Any]: r = room_get(con, room) if not r: raise KeyError("room") return json.loads(r["public_json"]) def public_set(con, room: str, obj: Dict[str, Any]): con.execute("UPDATE rooms SET public_json=? WHERE room=?", (json.dumps(obj, ensure_ascii=False), room)) def private_get(con, room: str, seat: int) -> Dict[str, Any]: r = con.execute("SELECT private_json FROM private_state WHERE room=? AND seat=?", (room, seat)).fetchone() return json.loads(r["private_json"]) if r else {} def private_set(con, room: str, seat: int, obj: Dict[str, Any]): con.execute( "INSERT INTO private_state(room, seat, private_json) VALUES(?,?,?) " "ON CONFLICT(room, seat) DO UPDATE SET private_json=excluded.private_json", (room, seat, json.dumps(obj, ensure_ascii=False)) ) def cleanup_inactive(con, room: str, ttl: int = 180): # โ€œIf you havenโ€™t pinged me in 3 minutes, I assume you went to get snacks.โ€ ๐Ÿช cutoff = now_i() - ttl gone = con.execute("SELECT seat, name FROM players WHERE room=? AND last_seen < ?", (room, cutoff)).fetchall() if gone: con.execute("DELETE FROM players WHERE room=? AND last_seen < ?", (room, cutoff)) for g in gone: event_append(con, room, "leave", {"seat": int(g["seat"]), "name": g["name"], "why": "timeout"}) # ----------------------------- # FastAPI mode ๐Ÿง โšก # ----------------------------- if os.getenv("RUN_MODE", "api") == "api": from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel init_db() api = FastAPI(title="AllAIINC Multiplayer Relay", version="0.1") api.add_middleware( CORSMiddleware, allow_origins=[o.strip() for o in CORS_ORIGINS if o.strip()], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) class CreateRoomIn(BaseModel): name: str = "Host" class JoinRoomIn(BaseModel): room: str name: str = "Player" class CmdIn(BaseModel): room: str token: str cmd: str class ActionIn(BaseModel): room: str token: str kind: str payload: Dict[str, Any] = {} # --- โ€œEasy wordsโ€ multiplayer language --- # verbs: say, put, get, add, del, who, list, ping, clear # all stored as events + public kv (plus private kv per player if needed) def parse_cmd(cmd: str): cmd = (cmd or "").strip() parts = cmd.split() verb = parts[0].lower() if parts else "" rest = parts[1:] return verb, rest @api.get("/api/health") def health(): return {"ok": True, "t": now_i(), "db": DB_PATH} @api.post("/api/room/create") def room_create(body: CreateRoomIn): con = db() try: room = rid() token_ = tok() public = {"turnSeat": 0, "kv": {}, "joke": "Why did the packet cross the road? To get ACKโ€™d. ๐Ÿ”๐Ÿ“จ"} con.execute( "INSERT INTO rooms(room, created, seq, public_json) VALUES(?,?,?,?)", (room, now_i(), 0, json.dumps(public, ensure_ascii=False)) ) con.execute( "INSERT INTO players(token, room, seat, name, last_seen) VALUES(?,?,?,?,?)", (token_, room, 0, body.name[:24], now_i()) ) private_set(con, room, 0, {"kv": {}, "note": "host private pocket ๐Ÿงฅ"}) event_append(con, room, "join", {"seat": 0, "name": body.name[:24]}) con.commit() return {"room": room, "token": token_, "seat": 0} finally: con.close() @api.post("/api/room/join") def room_join(body: JoinRoomIn): con = db() try: if not room_get(con, body.room): raise HTTPException(404, "Room not found") cleanup_inactive(con, body.room) # seat = smallest unused used = {int(r["seat"]) for r in con.execute("SELECT seat FROM players WHERE room=?", (body.room,)).fetchall()} seat = 0 while seat in used: seat += 1 token_ = tok() con.execute( "INSERT INTO players(token, room, seat, name, last_seen) VALUES(?,?,?,?,?)", (token_, body.room, seat, body.name[:24], now_i()) ) private_set(con, body.room, seat, {"kv": {}, "hand": []}) seq = event_append(con, body.room, "join", {"seat": seat, "name": body.name[:24]}) con.commit() return {"room": body.room, "token": token_, "seat": seat, "seq": seq} finally: con.close() @api.get("/api/state") def state(room: str, token: str, since: int = 0): con = db() try: if not room_get(con, room): raise HTTPException(404, "Room not found") p = player_by_token(con, token) if not p or p["room"] != room: raise HTTPException(401, "Bad token") con.execute("UPDATE players SET last_seen=? WHERE token=?", (now_i(), token)) cleanup_inactive(con, room) pub = public_get(con, room) seat = int(p["seat"]) priv = private_get(con, room, seat) evs = con.execute( "SELECT seq, t, kind, data_json FROM events WHERE room=? AND seq > ? ORDER BY seq", (room, since) ).fetchall() return { "seq": int(room_get(con, room)["seq"]), "you": {"seat": seat, "name": p["name"]}, "roster": players_roster(con, room), "public": pub, "private": priv, "events": [{"seq": int(e["seq"]), "t": int(e["t"]), "kind": e["kind"], "data": json.loads(e["data_json"])} for e in evs], } finally: con.close() @api.post("/api/cmd") def cmd_run(body: CmdIn): con = db() try: if not room_get(con, body.room): raise HTTPException(404, "Room not found") p = player_by_token(con, body.token) if not p or p["room"] != body.room: raise HTTPException(401, "Bad token") seat = int(p["seat"]) name = p["name"] con.execute("UPDATE players SET last_seen=? WHERE token=?", (now_i(), body.token)) cleanup_inactive(con, body.room) pub = public_get(con, body.room) pub.setdefault("kv", {}) priv = private_get(con, body.room, seat) priv.setdefault("kv", {}) verb, rest = parse_cmd(body.cmd) if verb in ("help", ""): return {"ok": True, "msg": "verbs: join, say, put, get, add, del, who, list, ping, clear"} if verb == "ping": seq = event_append(con, body.room, "ping", {"seat": seat, "name": name}) con.commit() return {"ok": True, "msg": "pong ๐Ÿ“", "seq": seq} if verb == "who": return {"ok": True, "players": players_roster(con, body.room)} if verb == "list": return {"ok": True, "keys": sorted(pub["kv"].keys())} if verb == "say": msg = " ".join(rest).strip()[:240] if not msg: raise HTTPException(400, "Usage: say ") seq = event_append(con, body.room, "say", {"seat": seat, "name": name, "text": msg}) con.commit() return {"ok": True, "msg": "sent ๐Ÿ—ฃ๏ธ", "seq": seq} if verb == "put": if len(rest) < 2: raise HTTPException(400, "Usage: put ") k = rest[0] v = " ".join(rest[1:])[:240] pub["kv"][k] = v public_set(con, body.room, pub) seq = event_append(con, body.room, "put", {"seat": seat, "key": k, "value": v}) con.commit() return {"ok": True, "msg": f"ok โœ… stored {k}", "seq": seq} if verb == "get": if len(rest) != 1: raise HTTPException(400, "Usage: get ") k = rest[0] return {"ok": True, "key": k, "value": pub["kv"].get(k)} if verb == "add": if len(rest) < 2: raise HTTPException(400, "Usage: add ") k = rest[0] try: amt = float(rest[1]) except: raise HTTPException(400, "add needs a number, e.g. add score 5") cur = pub["kv"].get(k, 0) try: cur = float(cur) except: cur = 0.0 pub["kv"][k] = cur + amt public_set(con, body.room, pub) seq = event_append(con, body.room, "add", {"seat": seat, "key": k, "amt": amt, "new": pub["kv"][k]}) con.commit() return {"ok": True, "msg": f"{k} -> {pub['kv'][k]} ๐Ÿ“ˆ", "seq": seq} if verb == "del": if len(rest) != 1: raise HTTPException(400, "Usage: del ") k = rest[0] pub["kv"].pop(k, None) public_set(con, body.room, pub) seq = event_append(con, body.room, "del", {"seat": seat, "key": k}) con.commit() return {"ok": True, "msg": f"deleted {k} ๐Ÿงน", "seq": seq} if verb == "clear": if seat != 0: raise HTTPException(403, "Only seat 0 can clear") pub["kv"] = {} public_set(con, body.room, pub) seq = event_append(con, body.room, "clear", {"by": name}) con.commit() return {"ok": True, "msg": "cleared ๐Ÿงผ", "seq": seq} raise HTTPException(400, f"Unknown verb: {verb}") finally: con.close() @api.post("/api/action") def action(body: ActionIn): # โ€œactionโ€ is for structured game moves (turn-based etc.) con = db() try: if not room_get(con, body.room): raise HTTPException(404, "Room not found") p = player_by_token(con, body.token) if not p or p["room"] != body.room: raise HTTPException(401, "Bad token") seat = int(p["seat"]) name = p["name"] con.execute("UPDATE players SET last_seen=? WHERE token=?", (now_i(), body.token)) cleanup_inactive(con, body.room) pub = public_get(con, body.room) # ultra-minimal example: a shared โ€œpotโ€ with turn enforcement pub.setdefault("turnSeat", 0) pub.setdefault("pot", 0) if seat != int(pub["turnSeat"]): raise HTTPException(409, "Not your turn") if body.kind == "add_to_pot": amt = int(body.payload.get("amt", 1)) pub["pot"] += max(0, amt) else: raise HTTPException(400, "Unknown kind") # advance turn seats = [r["seat"] for r in players_roster(con, body.room)] seats = sorted(int(s) for s in seats) i = seats.index(seat) pub["turnSeat"] = seats[(i+1) % len(seats)] public_set(con, body.room, pub) seq = event_append(con, body.room, "action", {"seat": seat, "name": name, "kind": body.kind, "payload": body.payload}) con.commit() return {"ok": True, "seq": seq, "public": pub} finally: con.close() # ----------------------------- # Streamlit admin mode ๐Ÿง‘โ€๐Ÿ”ง๐Ÿง  # ----------------------------- else: import streamlit as st import pandas as pd import requests init_db() st.set_page_config(page_title="Admin โ€” Multiplayer Relay", layout="wide") st.title("๐Ÿง‘โ€โœˆ๏ธ Admin Console โ€” Multiplayer Relay") st.caption("If you can read this, nginx routing worked. If you canโ€™t, blame gremlins. ๐Ÿ‘น") con = db() rooms = con.execute("SELECT room, created, seq FROM rooms ORDER BY created DESC").fetchall() con.close() if not rooms: st.info("No rooms yet. Create one via /api/room/create from your web client.") st.stop() df_rooms = pd.DataFrame([dict(r) for r in rooms]) st.dataframe(df_rooms, use_container_width=True, hide_index=True) room_id = st.selectbox("Pick a room", df_rooms["room"].tolist()) col1, col2, col3 = st.columns([1,1,1]) if col1.button("๐Ÿ”„ Refresh"): st.rerun() # Show room details con = db() room = con.execute("SELECT * FROM rooms WHERE room=?", (room_id,)).fetchone() roster = con.execute("SELECT seat, name, last_seen FROM players WHERE room=? ORDER BY seat", (room_id,)).fetchall() events = con.execute("SELECT seq, t, kind, data_json FROM events WHERE room=? ORDER BY seq DESC LIMIT 80", (room_id,)).fetchall() con.close() st.subheader(f"๐Ÿงฉ Room `{room_id}`") st.write("**Public**") st.json(json.loads(room["public_json"])) st.write("**Players**") st.dataframe(pd.DataFrame([dict(r) for r in roster]), use_container_width=True, hide_index=True) st.write("**Recent events**") ev_df = pd.DataFrame([{ "seq": int(e["seq"]), "t": int(e["t"]), "kind": e["kind"], "data": json.loads(e["data_json"]) } for e in events]) st.dataframe(ev_df, use_container_width=True, hide_index=True) st.caption("Joke: A SQL query walks into a bar and says: โ€˜Can I join you?โ€™ ๐Ÿป๐Ÿงพ")