"""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 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from solve import problem_from_dict, solve, solution_to_dict INDEX_HTML = r""" Days Without Strife - Planner

Days Without Strife — Planner Optimizer

Game
Starting resources
Tradeable into (Trade Goods convert 1-for-1 for scoring)
Cities

The name is the city's unique identifier — it labels the output plan and is what an agent's "forced city" refers to. Upgrades are the ones already installed at the start; the available choices follow the city's type.

Agents

Pick a known agent; its governor behaviour is implicit and shown in the Effect column. The name identifies it and is what a forced city refers back to. All effects apply while the agent is Governor of a city (at most one city per turn).

Objective — scoring terms

Each term scores a resource (or renown) at the end of a turn (blank turn = final turn). For a log term, write a JS expression that evals to a one-argument function, e.g. (x) => Math.log2(x + 1). It is called over amounts 0..max_resource in the browser to build the lookup table.

Resource constraints

""" class Handler(BaseHTTPRequestHandler): def _send(self, code, body, content_type="application/json"): 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))) 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") 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 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}"})) def log_message(self, fmt, *args): # quieter console pass def main(): host, port = "127.0.0.1", 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()