awacke1 commited on
Commit
70ffaae
·
verified ·
1 Parent(s): ff18811

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +23 -391
app.py CHANGED
@@ -1,35 +1,24 @@
1
- # app.py 🎭
2
- # One file, two personalities:
3
- # - RUN_MODE=api -> FastAPI JSON multiplayer relay
4
- # - RUN_MODE=admin -> Streamlit admin console
5
- #
6
- # Shared state uses SQLite so both processes agree on reality.
7
- # (In-memory is where bugs go to reproduce. 🐛💞)
8
-
9
  import os, time, json, secrets, string, sqlite3
10
- from typing import Any, Dict, Optional, List
11
 
12
- DB_PATH = os.getenv("GAME_DB_PATH", "/data/game.sqlite") # HF persistent storage uses /data 💾
13
  CORS_ORIGINS = os.getenv("CORS_ORIGINS", "https://allaiinc.org").split(",")
14
 
15
- def now_i() -> int:
16
- return int(time.time())
17
 
18
- def rid(n=6) -> str:
19
  alphabet = string.ascii_uppercase + string.digits
20
  return "".join(secrets.choice(alphabet) for _ in range(n))
21
 
22
- def tok() -> str:
23
- return secrets.token_urlsafe(24)
24
 
25
- def db() -> sqlite3.Connection:
26
- # check_same_thread=False because Streamlit/FastAPI like to multitask 🤹
27
- conn = sqlite3.connect(DB_PATH, check_same_thread=False)
28
- conn.row_factory = sqlite3.Row
29
- return conn
30
 
31
  def init_db():
32
- os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
33
  con = db()
34
  cur = con.cursor()
35
  cur.executescript("""
@@ -64,386 +53,29 @@ def init_db():
64
  con.commit()
65
  con.close()
66
 
67
- def room_get(con, room: str):
68
- r = con.execute("SELECT * FROM rooms WHERE room=?", (room,)).fetchone()
69
- return r
70
-
71
- def event_append(con, room: str, kind: str, data: Dict[str, Any]) -> int:
72
- r = room_get(con, room)
73
- if not r:
74
- raise ValueError("room not found")
75
- seq = int(r["seq"]) + 1
76
- con.execute("UPDATE rooms SET seq=? WHERE room=?", (seq, room))
77
- con.execute(
78
- "INSERT INTO events(room, seq, t, kind, data_json) VALUES(?,?,?,?,?)",
79
- (room, seq, now_i(), kind, json.dumps(data, ensure_ascii=False))
80
- )
81
- return seq
82
-
83
- def players_roster(con, room: str) -> List[Dict[str, Any]]:
84
- rows = con.execute("SELECT seat, name, last_seen FROM players WHERE room=? ORDER BY seat", (room,)).fetchall()
85
- return [dict(r) for r in rows]
86
 
87
- def player_by_token(con, token: str):
88
- return con.execute("SELECT * FROM players WHERE token=?", (token,)).fetchone()
89
-
90
- def public_get(con, room: str) -> Dict[str, Any]:
91
- r = room_get(con, room)
92
- if not r:
93
- raise KeyError("room")
94
- return json.loads(r["public_json"])
95
-
96
- def public_set(con, room: str, obj: Dict[str, Any]):
97
  con.execute("UPDATE rooms SET public_json=? WHERE room=?", (json.dumps(obj, ensure_ascii=False), room))
98
 
99
- def private_get(con, room: str, seat: int) -> Dict[str, Any]:
100
  r = con.execute("SELECT private_json FROM private_state WHERE room=? AND seat=?", (room, seat)).fetchone()
101
  return json.loads(r["private_json"]) if r else {}
102
 
103
- def private_set(con, room: str, seat: int, obj: Dict[str, Any]):
104
  con.execute(
105
  "INSERT INTO private_state(room, seat, private_json) VALUES(?,?,?) "
106
  "ON CONFLICT(room, seat) DO UPDATE SET private_json=excluded.private_json",
107
- (room, seat, json.dumps(obj, ensure_ascii=False))
108
  )
109
 
110
- def cleanup_inactive(con, room: str, ttl: int = 180):
111
- # “If you haven’t pinged me in 3 minutes, I assume you went to get snacks.” 🍪
112
- cutoff = now_i() - ttl
113
- gone = con.execute("SELECT seat, name FROM players WHERE room=? AND last_seen < ?", (room, cutoff)).fetchall()
114
- if gone:
115
- con.execute("DELETE FROM players WHERE room=? AND last_seen < ?", (room, cutoff))
116
- for g in gone:
117
- event_append(con, room, "leave", {"seat": int(g["seat"]), "name": g["name"], "why": "timeout"})
118
-
119
- # -----------------------------
120
- # FastAPI mode 🧠⚡
121
- # -----------------------------
122
- if os.getenv("RUN_MODE", "api") == "api":
123
- from fastapi import FastAPI, HTTPException
124
- from fastapi.middleware.cors import CORSMiddleware
125
- from pydantic import BaseModel
126
-
127
- init_db()
128
- api = FastAPI(title="AllAIINC Multiplayer Relay", version="0.1")
129
-
130
- api.add_middleware(
131
- CORSMiddleware,
132
- allow_origins=[o.strip() for o in CORS_ORIGINS if o.strip()],
133
- allow_credentials=True,
134
- allow_methods=["*"],
135
- allow_headers=["*"],
136
- )
137
-
138
- class CreateRoomIn(BaseModel):
139
- name: str = "Host"
140
-
141
- class JoinRoomIn(BaseModel):
142
- room: str
143
- name: str = "Player"
144
-
145
- class CmdIn(BaseModel):
146
- room: str
147
- token: str
148
- cmd: str
149
-
150
- class ActionIn(BaseModel):
151
- room: str
152
- token: str
153
- kind: str
154
- payload: Dict[str, Any] = {}
155
-
156
- # --- “Easy words” multiplayer language ---
157
- # verbs: say, put, get, add, del, who, list, ping, clear
158
- # all stored as events + public kv (plus private kv per player if needed)
159
-
160
- def parse_cmd(cmd: str):
161
- cmd = (cmd or "").strip()
162
- parts = cmd.split()
163
- verb = parts[0].lower() if parts else ""
164
- rest = parts[1:]
165
- return verb, rest
166
-
167
- @api.get("/api/health")
168
- def health():
169
- return {"ok": True, "t": now_i(), "db": DB_PATH}
170
-
171
- @api.post("/api/room/create")
172
- def room_create(body: CreateRoomIn):
173
- con = db()
174
- try:
175
- room = rid()
176
- token_ = tok()
177
- public = {"turnSeat": 0, "kv": {}, "joke": "Why did the packet cross the road? To get ACK’d. 🐔📨"}
178
- con.execute(
179
- "INSERT INTO rooms(room, created, seq, public_json) VALUES(?,?,?,?)",
180
- (room, now_i(), 0, json.dumps(public, ensure_ascii=False))
181
- )
182
- con.execute(
183
- "INSERT INTO players(token, room, seat, name, last_seen) VALUES(?,?,?,?,?)",
184
- (token_, room, 0, body.name[:24], now_i())
185
- )
186
- private_set(con, room, 0, {"kv": {}, "note": "host private pocket 🧥"})
187
- event_append(con, room, "join", {"seat": 0, "name": body.name[:24]})
188
- con.commit()
189
- return {"room": room, "token": token_, "seat": 0}
190
- finally:
191
- con.close()
192
-
193
- @api.post("/api/room/join")
194
- def room_join(body: JoinRoomIn):
195
- con = db()
196
- try:
197
- if not room_get(con, body.room):
198
- raise HTTPException(404, "Room not found")
199
- cleanup_inactive(con, body.room)
200
-
201
- # seat = smallest unused
202
- used = {int(r["seat"]) for r in con.execute("SELECT seat FROM players WHERE room=?", (body.room,)).fetchall()}
203
- seat = 0
204
- while seat in used:
205
- seat += 1
206
-
207
- token_ = tok()
208
- con.execute(
209
- "INSERT INTO players(token, room, seat, name, last_seen) VALUES(?,?,?,?,?)",
210
- (token_, body.room, seat, body.name[:24], now_i())
211
- )
212
- private_set(con, body.room, seat, {"kv": {}, "hand": []})
213
- seq = event_append(con, body.room, "join", {"seat": seat, "name": body.name[:24]})
214
- con.commit()
215
- return {"room": body.room, "token": token_, "seat": seat, "seq": seq}
216
- finally:
217
- con.close()
218
-
219
- @api.get("/api/state")
220
- def state(room: str, token: str, since: int = 0):
221
- con = db()
222
- try:
223
- if not room_get(con, room):
224
- raise HTTPException(404, "Room not found")
225
- p = player_by_token(con, token)
226
- if not p or p["room"] != room:
227
- raise HTTPException(401, "Bad token")
228
- con.execute("UPDATE players SET last_seen=? WHERE token=?", (now_i(), token))
229
- cleanup_inactive(con, room)
230
-
231
- pub = public_get(con, room)
232
- seat = int(p["seat"])
233
- priv = private_get(con, room, seat)
234
-
235
- evs = con.execute(
236
- "SELECT seq, t, kind, data_json FROM events WHERE room=? AND seq > ? ORDER BY seq",
237
- (room, since)
238
- ).fetchall()
239
-
240
- return {
241
- "seq": int(room_get(con, room)["seq"]),
242
- "you": {"seat": seat, "name": p["name"]},
243
- "roster": players_roster(con, room),
244
- "public": pub,
245
- "private": priv,
246
- "events": [{"seq": int(e["seq"]), "t": int(e["t"]), "kind": e["kind"], "data": json.loads(e["data_json"])} for e in evs],
247
- }
248
- finally:
249
- con.close()
250
-
251
- @api.post("/api/cmd")
252
- def cmd_run(body: CmdIn):
253
- con = db()
254
- try:
255
- if not room_get(con, body.room):
256
- raise HTTPException(404, "Room not found")
257
- p = player_by_token(con, body.token)
258
- if not p or p["room"] != body.room:
259
- raise HTTPException(401, "Bad token")
260
-
261
- seat = int(p["seat"])
262
- name = p["name"]
263
- con.execute("UPDATE players SET last_seen=? WHERE token=?", (now_i(), body.token))
264
- cleanup_inactive(con, body.room)
265
-
266
- pub = public_get(con, body.room)
267
- pub.setdefault("kv", {})
268
- priv = private_get(con, body.room, seat)
269
- priv.setdefault("kv", {})
270
-
271
- verb, rest = parse_cmd(body.cmd)
272
-
273
- if verb in ("help", ""):
274
- return {"ok": True, "msg": "verbs: join, say, put, get, add, del, who, list, ping, clear"}
275
 
276
- if verb == "ping":
277
- seq = event_append(con, body.room, "ping", {"seat": seat, "name": name})
278
- con.commit()
279
- return {"ok": True, "msg": "pong 🏓", "seq": seq}
280
-
281
- if verb == "who":
282
- return {"ok": True, "players": players_roster(con, body.room)}
283
-
284
- if verb == "list":
285
- return {"ok": True, "keys": sorted(pub["kv"].keys())}
286
-
287
- if verb == "say":
288
- msg = " ".join(rest).strip()[:240]
289
- if not msg:
290
- raise HTTPException(400, "Usage: say <message>")
291
- seq = event_append(con, body.room, "say", {"seat": seat, "name": name, "text": msg})
292
- con.commit()
293
- return {"ok": True, "msg": "sent 🗣️", "seq": seq}
294
-
295
- if verb == "put":
296
- if len(rest) < 2:
297
- raise HTTPException(400, "Usage: put <key> <value>")
298
- k = rest[0]
299
- v = " ".join(rest[1:])[:240]
300
- pub["kv"][k] = v
301
- public_set(con, body.room, pub)
302
- seq = event_append(con, body.room, "put", {"seat": seat, "key": k, "value": v})
303
- con.commit()
304
- return {"ok": True, "msg": f"ok ✅ stored {k}", "seq": seq}
305
-
306
- if verb == "get":
307
- if len(rest) != 1:
308
- raise HTTPException(400, "Usage: get <key>")
309
- k = rest[0]
310
- return {"ok": True, "key": k, "value": pub["kv"].get(k)}
311
-
312
- if verb == "add":
313
- if len(rest) < 2:
314
- raise HTTPException(400, "Usage: add <key> <number>")
315
- k = rest[0]
316
- try:
317
- amt = float(rest[1])
318
- except:
319
- raise HTTPException(400, "add needs a number, e.g. add score 5")
320
- cur = pub["kv"].get(k, 0)
321
- try:
322
- cur = float(cur)
323
- except:
324
- cur = 0.0
325
- pub["kv"][k] = cur + amt
326
- public_set(con, body.room, pub)
327
- seq = event_append(con, body.room, "add", {"seat": seat, "key": k, "amt": amt, "new": pub["kv"][k]})
328
- con.commit()
329
- return {"ok": True, "msg": f"{k} -> {pub['kv'][k]} 📈", "seq": seq}
330
-
331
- if verb == "del":
332
- if len(rest) != 1:
333
- raise HTTPException(400, "Usage: del <key>")
334
- k = rest[0]
335
- pub["kv"].pop(k, None)
336
- public_set(con, body.room, pub)
337
- seq = event_append(con, body.room, "del", {"seat": seat, "key": k})
338
- con.commit()
339
- return {"ok": True, "msg": f"deleted {k} 🧹", "seq": seq}
340
-
341
- if verb == "clear":
342
- if seat != 0:
343
- raise HTTPException(403, "Only seat 0 can clear")
344
- pub["kv"] = {}
345
- public_set(con, body.room, pub)
346
- seq = event_append(con, body.room, "clear", {"by": name})
347
- con.commit()
348
- return {"ok": True, "msg": "cleared 🧼", "seq": seq}
349
-
350
- raise HTTPException(400, f"Unknown verb: {verb}")
351
- finally:
352
- con.close()
353
-
354
- @api.post("/api/action")
355
- def action(body: ActionIn):
356
- # “action” is for structured game moves (turn-based etc.)
357
- con = db()
358
- try:
359
- if not room_get(con, body.room):
360
- raise HTTPException(404, "Room not found")
361
- p = player_by_token(con, body.token)
362
- if not p or p["room"] != body.room:
363
- raise HTTPException(401, "Bad token")
364
-
365
- seat = int(p["seat"])
366
- name = p["name"]
367
- con.execute("UPDATE players SET last_seen=? WHERE token=?", (now_i(), body.token))
368
- cleanup_inactive(con, body.room)
369
-
370
- pub = public_get(con, body.room)
371
-
372
- # ultra-minimal example: a shared “pot” with turn enforcement
373
- pub.setdefault("turnSeat", 0)
374
- pub.setdefault("pot", 0)
375
-
376
- if seat != int(pub["turnSeat"]):
377
- raise HTTPException(409, "Not your turn")
378
-
379
- if body.kind == "add_to_pot":
380
- amt = int(body.payload.get("amt", 1))
381
- pub["pot"] += max(0, amt)
382
- else:
383
- raise HTTPException(400, "Unknown kind")
384
-
385
- # advance turn
386
- seats = [r["seat"] for r in players_roster(con, body.room)]
387
- seats = sorted(int(s) for s in seats)
388
- i = seats.index(seat)
389
- pub["turnSeat"] = seats[(i+1) % len(seats)]
390
-
391
- public_set(con, body.room, pub)
392
- seq = event_append(con, body.room, "action", {"seat": seat, "name": name, "kind": body.kind, "payload": body.payload})
393
- con.commit()
394
- return {"ok": True, "seq": seq, "public": pub}
395
- finally:
396
- con.close()
397
-
398
- # -----------------------------
399
- # Streamlit admin mode 🧑‍🔧🧠
400
- # -----------------------------
401
- else:
402
- import streamlit as st
403
- import pandas as pd
404
- import requests
405
-
406
- init_db()
407
-
408
- st.set_page_config(page_title="Admin — Multiplayer Relay", layout="wide")
409
- st.title("🧑‍✈️ Admin Console — Multiplayer Relay")
410
- st.caption("If you can read this, nginx routing worked. If you can’t, blame gremlins. 👹")
411
-
412
- con = db()
413
- rooms = con.execute("SELECT room, created, seq FROM rooms ORDER BY created DESC").fetchall()
414
- con.close()
415
-
416
- if not rooms:
417
- st.info("No rooms yet. Create one via /api/room/create from your web client.")
418
- st.stop()
419
-
420
- df_rooms = pd.DataFrame([dict(r) for r in rooms])
421
- st.dataframe(df_rooms, use_container_width=True, hide_index=True)
422
-
423
- room_id = st.selectbox("Pick a room", df_rooms["room"].tolist())
424
- col1, col2, col3 = st.columns([1,1,1])
425
-
426
- if col1.button("🔄 Refresh"):
427
- st.rerun()
428
-
429
- # Show room details
430
- con = db()
431
- room = con.execute("SELECT * FROM rooms WHERE room=?", (room_id,)).fetchone()
432
- roster = con.execute("SELECT seat, name, last_seen FROM players WHERE room=? ORDER BY seat", (room_id,)).fetchall()
433
- events = con.execute("SELECT seq, t, kind, data_json FROM events WHERE room=? ORDER BY seq DESC LIMIT 80", (room_id,)).fetchall()
434
- con.close()
435
-
436
- st.subheader(f"🧩 Room `{room_id}`")
437
- st.write("**Public**")
438
- st.json(json.loads(room["public_json"]))
439
-
440
- st.write("**Players**")
441
- st.dataframe(pd.DataFrame([dict(r) for r in roster]), use_container_width=True, hide_index=True)
442
-
443
- st.write("**Recent events**")
444
- ev_df = pd.DataFrame([{
445
- "seq": int(e["seq"]), "t": int(e["t"]), "kind": e["kind"], "data": json.loads(e["data_json"])
446
- } for e in events])
447
- st.dataframe(ev_df, use_container_width=True, hide_index=True)
448
 
449
- st.caption("Joke: A SQL query walks into a bar and says: ‘Can I join you?’ 🍻🧾")
 
 
 
 
 
 
 
 
 
 
 
1
  import os, time, json, secrets, string, sqlite3
2
+ from typing import Any, Dict, List
3
 
4
+ DB_PATH = os.getenv("GAME_DB_PATH", "/data/game.sqlite") # /data if HF persistent storage is enabled
5
  CORS_ORIGINS = os.getenv("CORS_ORIGINS", "https://allaiinc.org").split(",")
6
 
7
+ def now_i(): return int(time.time())
 
8
 
9
+ def rid(n=6):
10
  alphabet = string.ascii_uppercase + string.digits
11
  return "".join(secrets.choice(alphabet) for _ in range(n))
12
 
13
+ def tok(): return secrets.token_urlsafe(24)
 
14
 
15
+ def db():
16
+ os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
17
+ con = sqlite3.connect(DB_PATH, check_same_thread=False)
18
+ con.row_factory = sqlite3.Row
19
+ return con
20
 
21
  def init_db():
 
22
  con = db()
23
  cur = con.cursor()
24
  cur.executescript("""
 
53
  con.commit()
54
  con.close()
55
 
56
+ def room_get(con, room): return con.execute("SELECT * FROM rooms WHERE room=?", (room,)).fetchone()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ def public_get(con, room): return json.loads(room_get(con, room)["public_json"])
59
+ def public_set(con, room, obj):
 
 
 
 
 
 
 
 
60
  con.execute("UPDATE rooms SET public_json=? WHERE room=?", (json.dumps(obj, ensure_ascii=False), room))
61
 
62
+ def private_get(con, room, seat):
63
  r = con.execute("SELECT private_json FROM private_state WHERE room=? AND seat=?", (room, seat)).fetchone()
64
  return json.loads(r["private_json"]) if r else {}
65
 
66
+ def private_set(con, room, seat, obj):
67
  con.execute(
68
  "INSERT INTO private_state(room, seat, private_json) VALUES(?,?,?) "
69
  "ON CONFLICT(room, seat) DO UPDATE SET private_json=excluded.private_json",
70
+ (room, seat, json.dumps(obj, ensure_ascii=False)),
71
  )
72
 
73
+ def player_by_token(con, token): return con.execute("SELECT * FROM players WHERE token=?", (token,)).fetchone()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ def roster(con, room) -> List[Dict[str, Any]]:
76
+ rows = con.execute("SELECT seat, name, last_seen FROM players WHERE room=? ORDER BY seat", (room,)).fetchall()
77
+ return [dict(r) for r in rows]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ def event_append(con, room, kind, data) -> int:
80
+ r = room_get(con, room)
81
+ seq = int(r["seq"]) +