Files
OBS-Overlays/blackout-bingo/server.py
T

136 lines
4.9 KiB
Python

#!/usr/bin/env python3
import json
import os
import random
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse
DATA_FILE = Path(os.environ.get("BINGO_DATA_FILE", Path(__file__).with_name("bingo.json"))).expanduser()
MAX_TASKS = 100
BOARD_SIZE = 25
def default_data():
return {"players": ["Player 1", "Player 2"], "tasks": [], "board": []}
def load_data():
if not DATA_FILE.exists():
save_data(default_data())
with DATA_FILE.open("r", encoding="utf-8") as fh:
data = json.load(fh)
data.setdefault("players", ["Player 1", "Player 2"])
data.setdefault("tasks", [])
data.setdefault("board", [])
return data
def clean_data(data):
tasks = [str(t).strip() for t in data.get("tasks", []) if str(t).strip()]
if len(tasks) > MAX_TASKS:
raise ValueError("You can save up to 100 tasks.")
players = data.get("players") or ["Player 1", "Player 2"]
players = [str(p).strip() or f"Player {i + 1}" for i, p in enumerate(players[:2])]
while len(players) < 2:
players.append(f"Player {len(players) + 1}")
board = []
for i, cell in enumerate(data.get("board", [])[:BOARD_SIZE]):
task = str(cell.get("task", "")).strip()
if not task:
continue
completed = []
for player in cell.get("completedBy", []):
player = str(player).strip()
if player and player in players and player not in completed:
completed.append(player)
board.append({"id": int(cell.get("id", i)), "task": task, "completedBy": completed})
return {"players": players, "tasks": tasks, "board": board}
def save_data(data):
clean = clean_data(data)
DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp = DATA_FILE.with_suffix(".json.tmp")
tmp.write_text(json.dumps(clean, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
tmp.replace(DATA_FILE)
return clean
def generate_board(seed=None):
data = load_data()
tasks = list(dict.fromkeys([t.strip() for t in data.get("tasks", []) if t.strip()]))
if len(tasks) < BOARD_SIZE:
raise ValueError("Add at least 25 unique tasks before generating a board.")
rng = random.Random(seed) if seed else random.SystemRandom()
selected = rng.sample(tasks, BOARD_SIZE)
data["board"] = [{"id": i, "task": task, "completedBy": []} for i, task in enumerate(selected)]
return save_data(data)
def toggle_mark(cell_id, player):
data = load_data()
players = data.get("players", ["Player 1", "Player 2"])
if player not in players:
raise ValueError("Unknown player.")
for cell in data.get("board", []):
if int(cell.get("id", -1)) == int(cell_id):
completed = cell.setdefault("completedBy", [])
if player in completed:
completed.remove(player)
else:
completed.append(player)
return save_data(data)
raise ValueError("Unknown board cell.")
class Handler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cache-Control", "no-store")
super().end_headers()
def read_json(self):
length = int(self.headers.get("Content-Length", "0") or 0)
raw = self.rfile.read(length).decode("utf-8") if length else "{}"
return json.loads(raw or "{}")
def send_json(self, status, payload):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if urlparse(self.path).path == "/api/bingo":
self.send_json(200, {"ok": True, "data": load_data()})
return
return super().do_GET()
def do_POST(self):
path = urlparse(self.path).path
try:
payload = self.read_json()
if path == "/api/bingo":
self.send_json(200, {"ok": True, "data": save_data(payload)})
return
if path == "/api/generate":
self.send_json(200, {"ok": True, "data": generate_board(payload.get("seed"))})
return
if path == "/api/mark":
self.send_json(200, {"ok": True, "data": toggle_mark(payload.get("cellId"), payload.get("player"))})
return
self.send_json(404, {"ok": False, "error": "Not found"})
except Exception as exc:
self.send_json(400, {"ok": False, "error": str(exc)})
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8780
print(f"Bingo overlay server running at http://localhost:{port}/")
print(f"Editor: http://localhost:{port}/editor.html")
print(f"Overlay: http://localhost:{port}/overlay.html")
ThreadingHTTPServer(("", port), Handler).serve_forever()