136 lines
4.9 KiB
Python
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()
|