dws-city-res-solve/main.py
Pagwin 727cd03f65 Conversions and mutually exclusive solving
Conversions now are a thing to model trading

exclusive solving is in so my teammates can't kill the $5 VPS
2026-06-17 20:55:01 -04:00

90 lines
3.4 KiB
Python

"""Web UI for the Days Without Strife planner (see solve.py).
A dependency-free (stdlib only) HTTP server that exposes every input the
solver accepts: turns, starting resources, cities, agents, scoring terms
(linear or log), resource constraints and the misc Problem knobs.
For log-scored terms the user supplies a JavaScript expression that evals into
a single-argument function (e.g. ``(x) => Math.log2(x)``). The browser evals it
once, then calls it over the amounts it needs (0..max_resource) to build the
``log_mapping`` lookup table, which is sent to the server as a plain array.
"""
from __future__ import annotations
import json
import os
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from solve import problem_from_dict, solve, solution_to_dict
# The UI is a single static page served from index.html next to this module.
INDEX_HTML = (Path(__file__).resolve().parent / "index.html").read_text(encoding="utf-8")
# Only one solve may run at a time across all viewers. While it's held, other
# clients get an immediate 429 from /solve and disable their button (they learn
# the busy state by polling /status).
_solve_lock = threading.Lock()
class Handler(BaseHTTPRequestHandler):
def _send(self, code, body, content_type="application/json", no_cache=False):
if isinstance(body, str):
body = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
if no_cache:
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path in ("/", "/index.html"):
self._send(200, INDEX_HTML, "text/html; charset=utf-8")
elif self.path == "/status":
self._send(200, json.dumps({"solving": _solve_lock.locked()}), no_cache=True)
else:
self._send(404, json.dumps({"error": "not found"}))
def do_POST(self):
if self.path != "/solve":
self._send(404, json.dumps({"error": "not found"}))
return
# Reject immediately if another viewer is already solving.
if not _solve_lock.acquire(blocking=False):
self._send(429, json.dumps({"error": "Another solve is already in progress"}))
return
try:
length = int(self.headers.get("Content-Length", 0))
payload = json.loads(self.rfile.read(length) or b"{}")
problem = problem_from_dict(payload.get("problem", {}))
max_time = float(payload.get("max_time_seconds", 30.0))
sol = solve(problem, max_time_seconds=max_time)
self._send(200, json.dumps(solution_to_dict(sol)))
except Exception as exc: # surface errors to the browser
self._send(400, json.dumps({"error": f"{type(exc).__name__}: {exc}"}))
finally:
_solve_lock.release()
def log_message(self, fmt, *args): # quieter console
pass
def main():
host = os.environ.get("HOST", "127.0.0.1")
port = int(os.environ.get("PORT", "8000"))
server = ThreadingHTTPServer((host, port), Handler)
print(f"Days Without Strife planner UI: http://{host}:{port}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nshutting down")
server.shutdown()
if __name__ == "__main__":
main()