Compare commits
10 commits
2556352193
...
4f9a980cfb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f9a980cfb | ||
|
|
0900bbaabd | ||
|
|
17020b857c | ||
|
|
3e604324ad | ||
|
|
b1dba083a0 | ||
|
|
041bf9febc | ||
|
|
7229124bb5 | ||
|
|
9711aa5bac | ||
|
|
d17ffbdb45 | ||
|
|
8bb6877e1b |
10 changed files with 3065 additions and 728 deletions
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install uv
|
||||
RUN pip install --no-cache-dir uv
|
||||
|
||||
# Copy project files
|
||||
COPY pyproject.toml uv.lock ./
|
||||
|
||||
# Install dependencies with uv
|
||||
RUN uv sync --no-editable
|
||||
|
||||
# Copy source code
|
||||
COPY solve.py web_solve.py ./
|
||||
COPY templates/ templates/
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=web_solve.py
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Run Flask app
|
||||
CMD ["uv", "run", "python", "web_solve.py"]
|
||||
25
constraint_validator.py
Normal file
25
constraint_validator.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""Create constraint lambdas from expressions."""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def make_constraint_lambda(expr_str):
|
||||
"""Create a lambda function from a constraint expression.
|
||||
|
||||
Transforms resource identifiers to res dict access and creates a lambda
|
||||
with restricted builtins.
|
||||
|
||||
Args:
|
||||
expr_str: Constraint expression like "E[3] >= 50" or "B[2] + S[2] >= 100"
|
||||
|
||||
Returns:
|
||||
A lambda function: lambda res: <evaluated_expression>
|
||||
|
||||
Raises:
|
||||
Exception: If expression fails to eval
|
||||
"""
|
||||
# Transform identifiers to res subscript access: E -> res["E"], B -> res["B"], etc.
|
||||
expr = re.sub(r'\b([A-Z])\b', r'res["\1"]', expr_str)
|
||||
|
||||
# Create lambda with restricted builtins (no imports, no dangerous functions)
|
||||
return eval(f"lambda res: {expr}", {"__builtins__": {}})
|
||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- FLASK_ENV=development
|
||||
- FLASK_DEBUG=1
|
||||
volumes:
|
||||
- .:/app
|
||||
command: uv run python web_solve.py
|
||||
725
main.py
725
main.py
|
|
@ -1,725 +0,0 @@
|
|||
"""
|
||||
City Resource Optimization -> CP-SAT (Google OR-Tools)
|
||||
|
||||
Maximise Electrum * Brass * Steel after the gains of step 5.
|
||||
|
||||
The objective is a product of three variables, so this is a constraint-
|
||||
programming / nonlinear problem, hence CP-SAT (cp_model) rather than the
|
||||
LP/MIP solver. Read the comments at:
|
||||
- PARAMETERS (initial pools + arrival schedule + bonus mode)
|
||||
- "OBJECTIVE IS SET HERE" (the product being maximised -- tweak freely)
|
||||
"""
|
||||
|
||||
from ortools.sat.python import cp_model
|
||||
import printer
|
||||
|
||||
# ======================================================================
|
||||
# PARAMETERS -- edit these
|
||||
# ======================================================================
|
||||
|
||||
# Starting resource pools at the start of step 1: (Electrum, Brass, Steel, Capital)
|
||||
INITIAL = (3, 3, 3, 3)
|
||||
|
||||
# Arrival schedule. Key = step (1..5), value = list of city types that arrive
|
||||
# at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument.
|
||||
# Total cities across all steps must be <= 7. Arriving cities act that same step.
|
||||
ARRIVALS = {
|
||||
1: ["H", "F", "H", "H", "N"],
|
||||
2: [],
|
||||
3: [],
|
||||
4: [],
|
||||
5: [],
|
||||
}
|
||||
|
||||
# Collect Bonus (b) adds +1 to whatever a Collect gives. On a foundry that is
|
||||
# +1 of the resource collected (i.e. the chosen vat's value + 1). This is the
|
||||
# same uniform rule as Hub (+1 Capital) and Metropolis (+1 resource pick).
|
||||
|
||||
NUM_STEPS = 5
|
||||
MAX_RES = (
|
||||
200 # upper bound on any resource pool (raise if you expect more; affects speed)
|
||||
)
|
||||
MAX_VAT = 15 # upper bound on a foundry vat value (1 + 2*nsteps is plenty)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# MODEL
|
||||
# ======================================================================
|
||||
|
||||
|
||||
def solve(
|
||||
initial=INITIAL,
|
||||
arrivals=ARRIVALS,
|
||||
max_res=MAX_RES,
|
||||
max_vat=MAX_VAT,
|
||||
time_limit=60.0,
|
||||
num_workers=8,
|
||||
verbose=True,
|
||||
):
|
||||
|
||||
# ---- build the city list -----------------------------------------
|
||||
cities = [] # list of (arrival_step, arrival_type)
|
||||
for s in range(1, NUM_STEPS + 1):
|
||||
for typ in arrivals.get(s, []):
|
||||
cities.append((s, typ))
|
||||
N = len(cities)
|
||||
# assert N <= 7, f"At most 7 cities allowed, got {N}"
|
||||
|
||||
m = cp_model.CpModel()
|
||||
|
||||
def AND(a, b):
|
||||
"""Boolean AND of two 0/1 vars, returned as a new 0/1 var."""
|
||||
c = m.NewBoolVar("")
|
||||
m.AddMultiplicationEquality(c, [a, b])
|
||||
return c
|
||||
|
||||
# ---- state variables ---------------------------------------------
|
||||
# Indexed [i][t]; t in 1..NUM_STEPS+1 for state (t=NUM_STEPS+1 == "after step 5").
|
||||
isH, isF, isM, isMon, present = {}, {}, {}, {}, {}
|
||||
hasA, hasB, hasD = {}, {}, {}
|
||||
vE, vB, vS = {}, {}, {} # foundry vats at start of step t
|
||||
|
||||
# action variables, t in 1..NUM_STEPS
|
||||
col, ua, ub, ud = {}, {}, {}, {} # collect / upgrade a,b,d
|
||||
ow = {} # overwork (global limit: 1 per step)
|
||||
rH, rF, rM = {}, {}, {} # renovate -> Hub/Foundry/Metro
|
||||
cvE, cvB, cvS = {}, {}, {} # foundry: which vat collected (normal collect)
|
||||
owcvE, owcvB, owcvS = {}, {}, {} # foundry: which vat collected (overwork)
|
||||
mE, mB, mS, mC = {}, {}, {}, {} # metropolis: resources picked (+1 each)
|
||||
owmE, owmB, owmS, owmC = {}, {}, {}, {} # metropolis: resources picked (overwork)
|
||||
|
||||
for i in range(N):
|
||||
a_step, a_type = cities[i]
|
||||
for t in range(1, NUM_STEPS + 2):
|
||||
isH[i, t] = m.NewBoolVar(f"isH_{i}_{t}")
|
||||
isF[i, t] = m.NewBoolVar(f"isF_{i}_{t}")
|
||||
isM[i, t] = m.NewBoolVar(f"isM_{i}_{t}")
|
||||
isMon[i, t] = m.NewBoolVar(f"isMon_{i}_{t}")
|
||||
present[i, t] = m.NewBoolVar(f"present_{i}_{t}")
|
||||
hasA[i, t] = m.NewBoolVar(f"hasA_{i}_{t}")
|
||||
hasB[i, t] = m.NewBoolVar(f"hasB_{i}_{t}")
|
||||
hasD[i, t] = m.NewBoolVar(f"hasD_{i}_{t}")
|
||||
vE[i, t] = m.NewIntVar(0, max_vat, f"vE_{i}_{t}")
|
||||
vB[i, t] = m.NewIntVar(0, max_vat, f"vB_{i}_{t}")
|
||||
vS[i, t] = m.NewIntVar(0, max_vat, f"vS_{i}_{t}")
|
||||
|
||||
# presence: present from arrival step onward, persists
|
||||
m.Add(present[i, t] == (1 if t >= a_step else 0))
|
||||
# exactly one type iff present
|
||||
m.Add(isH[i, t] + isF[i, t] + isM[i, t] + isMon[i, t] == present[i, t])
|
||||
|
||||
for t in range(1, NUM_STEPS + 1):
|
||||
col[i, t] = m.NewBoolVar(f"col_{i}_{t}")
|
||||
ua[i, t] = m.NewBoolVar(f"ua_{i}_{t}")
|
||||
ub[i, t] = m.NewBoolVar(f"ub_{i}_{t}")
|
||||
ud[i, t] = m.NewBoolVar(f"ud_{i}_{t}")
|
||||
ow[i, t] = m.NewBoolVar(f"ow_{i}_{t}")
|
||||
rH[i, t] = m.NewBoolVar(f"rH_{i}_{t}")
|
||||
rF[i, t] = m.NewBoolVar(f"rF_{i}_{t}")
|
||||
rM[i, t] = m.NewBoolVar(f"rM_{i}_{t}")
|
||||
cvE[i, t] = m.NewBoolVar(f"cvE_{i}_{t}")
|
||||
cvB[i, t] = m.NewBoolVar(f"cvB_{i}_{t}")
|
||||
cvS[i, t] = m.NewBoolVar(f"cvS_{i}_{t}")
|
||||
owcvE[i, t] = m.NewBoolVar(f"owcvE_{i}_{t}")
|
||||
owcvB[i, t] = m.NewBoolVar(f"owcvB_{i}_{t}")
|
||||
owcvS[i, t] = m.NewBoolVar(f"owcvS_{i}_{t}")
|
||||
mE[i, t] = m.NewIntVar(0, 3, f"mE_{i}_{t}")
|
||||
mB[i, t] = m.NewIntVar(0, 3, f"mB_{i}_{t}")
|
||||
mS[i, t] = m.NewIntVar(0, 3, f"mS_{i}_{t}")
|
||||
mC[i, t] = m.NewIntVar(0, 3, f"mC_{i}_{t}")
|
||||
owmE[i, t] = m.NewIntVar(0, 6, f"owmE_{i}_{t}")
|
||||
owmB[i, t] = m.NewIntVar(0, 6, f"owmB_{i}_{t}")
|
||||
owmS[i, t] = m.NewIntVar(0, 6, f"owmS_{i}_{t}")
|
||||
owmC[i, t] = m.NewIntVar(0, 6, f"owmC_{i}_{t}")
|
||||
|
||||
# ---- per-step gain/cost accumulators (linear expressions) ---------
|
||||
gain_E = {t: [] for t in range(1, NUM_STEPS + 1)}
|
||||
gain_B = {t: [] for t in range(1, NUM_STEPS + 1)}
|
||||
gain_S = {t: [] for t in range(1, NUM_STEPS + 1)}
|
||||
gain_C = {t: [] for t in range(1, NUM_STEPS + 1)}
|
||||
cost_C = {t: [] for t in range(1, NUM_STEPS + 1)} # capital cost (collects)
|
||||
cost_S = {t: [] for t in range(1, NUM_STEPS + 1)} # steel cost (upgrades b,d)
|
||||
|
||||
# ---- per-city logic -----------------------------------------------
|
||||
for i in range(N):
|
||||
a_step, a_type = cities[i]
|
||||
|
||||
# initial type at arrival step
|
||||
init = {"H": isH, "F": isF, "M": isM, "N": isMon}
|
||||
m.Add(init[a_type][i, a_step] == 1)
|
||||
# no upgrades at arrival
|
||||
m.Add(hasA[i, a_step] == 0)
|
||||
m.Add(hasB[i, a_step] == 0)
|
||||
m.Add(hasD[i, a_step] == 0)
|
||||
# vats at arrival
|
||||
m.Add(vE[i, a_step] == 1)
|
||||
m.Add(vB[i, a_step] == 1)
|
||||
m.Add(vS[i, a_step] == 1)
|
||||
|
||||
# before arrival: everything zero
|
||||
for t in range(1, a_step):
|
||||
for v in (isH, isF, isM, isMon, hasA, hasB, hasD, vE, vB, vS):
|
||||
m.Add(v[i, t] == 0)
|
||||
if t <= NUM_STEPS:
|
||||
for v in (
|
||||
col,
|
||||
ua,
|
||||
ub,
|
||||
ud,
|
||||
ow,
|
||||
rH,
|
||||
rF,
|
||||
rM,
|
||||
cvE,
|
||||
cvB,
|
||||
cvS,
|
||||
owcvE,
|
||||
owcvB,
|
||||
owcvS,
|
||||
mE,
|
||||
mB,
|
||||
mS,
|
||||
mC,
|
||||
owmE,
|
||||
owmB,
|
||||
owmS,
|
||||
owmC,
|
||||
):
|
||||
m.Add(v[i, t] == 0)
|
||||
|
||||
# action + transition logic for active steps
|
||||
for t in range(a_step, NUM_STEPS + 1):
|
||||
P = present[i, t]
|
||||
ren = m.NewBoolVar("") # any renovation this step
|
||||
m.Add(ren == rH[i, t] + rF[i, t] + rM[i, t])
|
||||
no_ren = m.NewBoolVar("")
|
||||
m.Add(no_ren == 1 - ren)
|
||||
|
||||
# exactly one action while present
|
||||
m.Add(
|
||||
col[i, t]
|
||||
+ ua[i, t]
|
||||
+ ub[i, t]
|
||||
+ ud[i, t]
|
||||
+ ow[i, t]
|
||||
+ rH[i, t]
|
||||
+ rF[i, t]
|
||||
+ rM[i, t]
|
||||
== P
|
||||
)
|
||||
|
||||
# renovation must change type (renovate-to-X requires not-X now)
|
||||
m.Add(isH[i, t] == 0).OnlyEnforceIf(rH[i, t])
|
||||
m.Add(isF[i, t] == 0).OnlyEnforceIf(rF[i, t])
|
||||
m.Add(isM[i, t] == 0).OnlyEnforceIf(rM[i, t])
|
||||
|
||||
# upgrade legality
|
||||
m.Add(hasA[i, t] == 0).OnlyEnforceIf(ua[i, t]) # can't re-acquire
|
||||
m.Add(hasB[i, t] == 0).OnlyEnforceIf(ub[i, t])
|
||||
m.Add(hasD[i, t] == 0).OnlyEnforceIf(ud[i, t])
|
||||
m.Add(isF[i, t] == 1).OnlyEnforceIf(ud[i, t]) # d is foundry-only
|
||||
|
||||
# LLM allowed renovation into metropolis, need to prevent that now
|
||||
m.Add(rM[i, t] == 0)
|
||||
|
||||
# Monument constraints due to maximizing resources and not wanting enemy to get free upgrades
|
||||
m.Add(col[i, t] == 0).OnlyEnforceIf(isMon[i, t])
|
||||
m.Add(ua[i, t] == 0).OnlyEnforceIf(isMon[i, t])
|
||||
m.Add(ub[i, t] == 0).OnlyEnforceIf(isMon[i, t])
|
||||
m.Add(ud[i, t] == 0).OnlyEnforceIf(isMon[i, t])
|
||||
m.Add(ow[i, t] == 0).OnlyEnforceIf(isMon[i, t])
|
||||
|
||||
# overwork cooldown: city that overworked last step can't collect or overwork
|
||||
if t > a_step:
|
||||
m.Add(col[i, t] == 0).OnlyEnforceIf(ow[i, t - 1])
|
||||
m.Add(ow[i, t] == 0).OnlyEnforceIf(ow[i, t - 1])
|
||||
|
||||
# ---- type transition t -> t+1 ----
|
||||
m.Add(isH[i, t + 1] == isH[i, t]).OnlyEnforceIf(no_ren)
|
||||
m.Add(isF[i, t + 1] == isF[i, t]).OnlyEnforceIf(no_ren)
|
||||
m.Add(isM[i, t + 1] == isM[i, t]).OnlyEnforceIf(no_ren)
|
||||
for r, target in ((rH[i, t], isH), (rF[i, t], isF), (rM[i, t], isM)):
|
||||
m.Add(target[i, t + 1] == 1).OnlyEnforceIf(r)
|
||||
m.Add(isF[i, t + 1] == 0).OnlyEnforceIf(rH[i, t])
|
||||
m.Add(isM[i, t + 1] == 0).OnlyEnforceIf(rH[i, t])
|
||||
m.Add(isH[i, t + 1] == 0).OnlyEnforceIf(rF[i, t])
|
||||
m.Add(isM[i, t + 1] == 0).OnlyEnforceIf(rF[i, t])
|
||||
m.Add(isH[i, t + 1] == 0).OnlyEnforceIf(rM[i, t])
|
||||
m.Add(isF[i, t + 1] == 0).OnlyEnforceIf(rM[i, t])
|
||||
|
||||
# ---- upgrade transition t -> t+1 ----
|
||||
# a, b survive renovation and are monotone
|
||||
m.AddMaxEquality(hasA[i, t + 1], [hasA[i, t], ua[i, t]])
|
||||
m.AddMaxEquality(hasB[i, t + 1], [hasB[i, t], ub[i, t]])
|
||||
# d is stripped on renovation, else monotone
|
||||
m.Add(hasD[i, t + 1] == 0).OnlyEnforceIf(ren)
|
||||
d_keep = m.NewBoolVar("")
|
||||
m.AddMaxEquality(d_keep, [hasD[i, t], ud[i, t]])
|
||||
m.Add(hasD[i, t + 1] == d_keep).OnlyEnforceIf(no_ren)
|
||||
|
||||
# ---- collect sub-choices ----
|
||||
fcol = AND(isF[i, t], col[i, t])
|
||||
mcol = AND(isM[i, t], col[i, t])
|
||||
hcol = AND(isH[i, t], col[i, t])
|
||||
# foundry: exactly one vat chosen iff foundry collects
|
||||
m.Add(cvE[i, t] + cvB[i, t] + cvS[i, t] == fcol)
|
||||
# metropolis: pick (2 + hasB) resources (+1 each); else nothing
|
||||
npick = m.NewIntVar(0, 3, "")
|
||||
m.Add(npick == 2 + hasB[i, t]).OnlyEnforceIf(mcol)
|
||||
m.Add(npick == 0).OnlyEnforceIf(mcol.Not())
|
||||
m.Add(mE[i, t] + mB[i, t] + mS[i, t] + mC[i, t] == npick)
|
||||
|
||||
# ================= COSTS =================
|
||||
# foundry & metropolis collect each cost 1 Capital
|
||||
cost_C[t].append(fcol)
|
||||
cost_C[t].append(mcol)
|
||||
# upgrades b,d cost 2 Steel, reduced by 1 if cost-reduction (a) already held
|
||||
for upg in (ub[i, t], ud[i, t]):
|
||||
c = m.NewIntVar(0, 2, "")
|
||||
both = AND(upg, hasA[i, t])
|
||||
m.Add(c == 2).OnlyEnforceIf(upg, hasA[i, t].Not())
|
||||
m.Add(c == 1).OnlyEnforceIf(both)
|
||||
m.Add(c == 0).OnlyEnforceIf(upg.Not())
|
||||
cost_S[t].append(c)
|
||||
|
||||
# ================= GAINS =================
|
||||
# Hub collect: +2 Capital (+1 more if collect-bonus b)
|
||||
hub_gain = m.NewIntVar(0, 3, "")
|
||||
m.Add(hub_gain == 2 + hasB[i, t]).OnlyEnforceIf(hcol)
|
||||
m.Add(hub_gain == 0).OnlyEnforceIf(hcol.Not())
|
||||
gain_C[t].append(hub_gain)
|
||||
|
||||
# Metropolis collect: +1 per pick
|
||||
gain_E[t].append(mE[i, t])
|
||||
gain_B[t].append(mB[i, t])
|
||||
gain_S[t].append(mS[i, t])
|
||||
gain_C[t].append(mC[i, t])
|
||||
|
||||
# Foundry collect: gain chosen vat's value as that resource
|
||||
gEf = m.NewIntVar(0, max_vat, "")
|
||||
gBf = m.NewIntVar(0, max_vat, "")
|
||||
gSf = m.NewIntVar(0, max_vat, "")
|
||||
m.AddMultiplicationEquality(gEf, [cvE[i, t], vE[i, t]])
|
||||
m.AddMultiplicationEquality(gBf, [cvB[i, t], vB[i, t]])
|
||||
m.AddMultiplicationEquality(gSf, [cvS[i, t], vS[i, t]])
|
||||
gain_E[t].append(gEf)
|
||||
gain_B[t].append(gBf)
|
||||
gain_S[t].append(gSf)
|
||||
|
||||
# Collect Bonus (b): adds +1 to the amount a Collect gives. For a
|
||||
# foundry that means +1 of the collected resource (vat value + 1),
|
||||
# the same uniform "+1 to what Collect gives" rule as Hub/Metro.
|
||||
gain_E[t].append(AND(cvE[i, t], hasB[i, t]))
|
||||
gain_B[t].append(AND(cvB[i, t], hasB[i, t]))
|
||||
gain_S[t].append(AND(cvS[i, t], hasB[i, t]))
|
||||
|
||||
# ---- overwork sub-choices (double-collect; no Capital cost) ----
|
||||
fow = AND(isF[i, t], ow[i, t])
|
||||
mow = AND(isM[i, t], ow[i, t])
|
||||
how = AND(isH[i, t], ow[i, t])
|
||||
|
||||
# foundry overwork: exactly one vat chosen iff foundry overworks
|
||||
m.Add(owcvE[i, t] + owcvB[i, t] + owcvS[i, t] == fow)
|
||||
|
||||
# metropolis overwork: pick 2*(2 + hasB) resources; else nothing
|
||||
ow_npick = m.NewIntVar(0, 6, "")
|
||||
m.Add(ow_npick == 2 * (2 + hasB[i, t])).OnlyEnforceIf(mow)
|
||||
m.Add(ow_npick == 0).OnlyEnforceIf(mow.Not())
|
||||
m.Add(owmE[i, t] + owmB[i, t] + owmS[i, t] + owmC[i, t] == ow_npick)
|
||||
|
||||
# ================= OVERWORK GAINS (2x normal collect) =================
|
||||
# Hub overwork: 2*(2 + hasB) Capital
|
||||
hub_ow_gain = m.NewIntVar(0, 6, "")
|
||||
m.Add(hub_ow_gain == 2 * (2 + hasB[i, t])).OnlyEnforceIf(how)
|
||||
m.Add(hub_ow_gain == 0).OnlyEnforceIf(how.Not())
|
||||
gain_C[t].append(hub_ow_gain)
|
||||
|
||||
# Metropolis overwork: +1 per pick (picks are already doubled via ow_npick)
|
||||
gain_E[t].append(owmE[i, t])
|
||||
gain_B[t].append(owmB[i, t])
|
||||
gain_S[t].append(owmS[i, t])
|
||||
gain_C[t].append(owmC[i, t])
|
||||
|
||||
# Foundry overwork: 2 * chosen vat's value
|
||||
owgEf = m.NewIntVar(0, 2 * max_vat, "")
|
||||
owgBf = m.NewIntVar(0, 2 * max_vat, "")
|
||||
owgSf = m.NewIntVar(0, 2 * max_vat, "")
|
||||
_owE = m.NewIntVar(0, max_vat, "")
|
||||
_owB = m.NewIntVar(0, max_vat, "")
|
||||
_owS = m.NewIntVar(0, max_vat, "")
|
||||
m.AddMultiplicationEquality(_owE, [owcvE[i, t], vE[i, t]])
|
||||
m.AddMultiplicationEquality(_owB, [owcvB[i, t], vB[i, t]])
|
||||
m.AddMultiplicationEquality(_owS, [owcvS[i, t], vS[i, t]])
|
||||
m.Add(owgEf == 2 * _owE)
|
||||
m.Add(owgBf == 2 * _owB)
|
||||
m.Add(owgSf == 2 * _owS)
|
||||
gain_E[t].append(owgEf)
|
||||
gain_B[t].append(owgBf)
|
||||
gain_S[t].append(owgSf)
|
||||
|
||||
# Collect Bonus (b) for foundry overwork: 2 * (+1) = +2 of that resource
|
||||
ow_be = AND(owcvE[i, t], hasB[i, t])
|
||||
ow_bb = AND(owcvB[i, t], hasB[i, t])
|
||||
ow_bs = AND(owcvS[i, t], hasB[i, t])
|
||||
gain_E[t].append(ow_be)
|
||||
gain_E[t].append(ow_be) # added twice == *2
|
||||
gain_B[t].append(ow_bb)
|
||||
gain_B[t].append(ow_bb)
|
||||
gain_S[t].append(ow_bs)
|
||||
gain_S[t].append(ow_bs)
|
||||
|
||||
# ---- vat update producing vat[i, t+1] ----
|
||||
# increment added to the two non-collected vats (1, or 2 with upgrade d)
|
||||
inc = m.NewIntVar(1, 2, "")
|
||||
m.Add(inc == 1 + hasD[i, t])
|
||||
|
||||
# vat_next = result of this step's action (only meaningful if foundry)
|
||||
vEn = m.NewIntVar(0, max_vat, "")
|
||||
vBn = m.NewIntVar(0, max_vat, "")
|
||||
vSn = m.NewIntVar(0, max_vat, "")
|
||||
# collect E: E->0, B,S += inc
|
||||
m.Add(vEn == 0).OnlyEnforceIf(cvE[i, t])
|
||||
m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(cvE[i, t])
|
||||
m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(cvE[i, t])
|
||||
# collect B
|
||||
m.Add(vBn == 0).OnlyEnforceIf(cvB[i, t])
|
||||
m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(cvB[i, t])
|
||||
m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(cvB[i, t])
|
||||
# collect S
|
||||
m.Add(vSn == 0).OnlyEnforceIf(cvS[i, t])
|
||||
m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(cvS[i, t])
|
||||
m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(cvS[i, t])
|
||||
# overwork vat transitions: same reset/increment as normal collect
|
||||
m.Add(vEn == 0).OnlyEnforceIf(owcvE[i, t])
|
||||
m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(owcvE[i, t])
|
||||
m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(owcvE[i, t])
|
||||
m.Add(vBn == 0).OnlyEnforceIf(owcvB[i, t])
|
||||
m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(owcvB[i, t])
|
||||
m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(owcvB[i, t])
|
||||
m.Add(vSn == 0).OnlyEnforceIf(owcvS[i, t])
|
||||
m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(owcvS[i, t])
|
||||
m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(owcvS[i, t])
|
||||
|
||||
# foundry but neither collecting nor overworking: vats unchanged
|
||||
# f_noncollect_noow = isF AND NOT fcol AND NOT fow = isF - fcol - fow
|
||||
f_noncollect_noow = m.NewBoolVar("")
|
||||
m.Add(f_noncollect_noow == isF[i, t] - fcol - fow)
|
||||
m.Add(vEn == vE[i, t]).OnlyEnforceIf(f_noncollect_noow)
|
||||
m.Add(vBn == vB[i, t]).OnlyEnforceIf(f_noncollect_noow)
|
||||
m.Add(vSn == vS[i, t]).OnlyEnforceIf(f_noncollect_noow)
|
||||
|
||||
# assign vat[i, t+1]:
|
||||
# renovate-to-foundry -> reset to 1
|
||||
# continuing foundry -> vat_next
|
||||
# otherwise (not foundry next) -> 0
|
||||
cont_F = AND(isF[i, t], no_ren) # stays a foundry next step
|
||||
for vnext, vn in (
|
||||
(vE[i, t + 1], vEn),
|
||||
(vB[i, t + 1], vBn),
|
||||
(vS[i, t + 1], vSn),
|
||||
):
|
||||
m.Add(vnext == 1).OnlyEnforceIf(rF[i, t])
|
||||
m.Add(vnext == vn).OnlyEnforceIf(cont_F)
|
||||
m.Add(isF[i, t + 1] == 0).OnlyEnforceIf(
|
||||
rF[i, t].Not(), cont_F.Not()
|
||||
) # tautology guard
|
||||
not_F_next = isF[i, t + 1].Not()
|
||||
m.Add(vE[i, t + 1] == 0).OnlyEnforceIf(not_F_next)
|
||||
m.Add(vB[i, t + 1] == 0).OnlyEnforceIf(not_F_next)
|
||||
m.Add(vS[i, t + 1] == 0).OnlyEnforceIf(not_F_next)
|
||||
|
||||
# ---- global overwork limit: at most 1 city overworks per step ----
|
||||
for t in range(1, NUM_STEPS + 1):
|
||||
m.Add(sum(ow[i, t] for i in range(N)) <= 1)
|
||||
|
||||
# ---- resource pool recursion --------------------------------------
|
||||
E = {1: m.NewIntVar(initial[0], initial[0], "E1")}
|
||||
B = {1: m.NewIntVar(initial[1], initial[1], "B1")}
|
||||
S = {1: m.NewIntVar(initial[2], initial[2], "S1")}
|
||||
C = {1: m.NewIntVar(initial[3], initial[3], "C1")}
|
||||
for t in range(1, NUM_STEPS + 1):
|
||||
E[t + 1] = m.NewIntVar(0, max_res, f"E_{t + 1}")
|
||||
B[t + 1] = m.NewIntVar(0, max_res, f"B_{t + 1}")
|
||||
S[t + 1] = m.NewIntVar(0, max_res, f"S_{t + 1}")
|
||||
C[t + 1] = m.NewIntVar(0, max_res, f"C_{t + 1}")
|
||||
# costs are paid from the START-of-step pool (must work out before gains)
|
||||
m.Add(C[t] - sum(cost_C[t]) >= 0)
|
||||
m.Add(S[t] - sum(cost_S[t]) >= 0)
|
||||
# next pool = start - costs + gains
|
||||
m.Add(E[t + 1] == E[t] + sum(gain_E[t]))
|
||||
m.Add(B[t + 1] == B[t] + sum(gain_B[t]))
|
||||
m.Add(S[t + 1] == S[t] - sum(cost_S[t]) + sum(gain_S[t]))
|
||||
m.Add(C[t + 1] == C[t] - sum(cost_C[t]) + sum(gain_C[t]))
|
||||
|
||||
finalE, finalB, finalS = E[NUM_STEPS + 1], B[NUM_STEPS + 1], S[NUM_STEPS + 1]
|
||||
|
||||
# ---- Phase 1: real per-resource ceilings (fast LINEAR maximisations) ----
|
||||
# The triple product E*B*S has a very loose relaxation, so proving optimality
|
||||
# directly is slow. We first find the true max each resource can reach on its
|
||||
# own (a linear objective, solved to optimality quickly), then clamp the final
|
||||
# pools to those ceilings. This tightens the product's propagation enough to
|
||||
# prove optimality, and is valid because no feasible solution can exceed an
|
||||
# individual resource's standalone maximum.
|
||||
def _ceiling(var):
|
||||
s = cp_model.CpSolver()
|
||||
s.parameters.max_time_in_seconds = 20.0
|
||||
s.parameters.num_search_workers = num_workers
|
||||
m.Maximize(var)
|
||||
st = s.Solve(m)
|
||||
return (
|
||||
int(s.ObjectiveValue())
|
||||
if st in (cp_model.OPTIMAL, cp_model.FEASIBLE)
|
||||
else max_res
|
||||
)
|
||||
|
||||
capE = _ceiling(finalE)
|
||||
capB = _ceiling(finalB)
|
||||
capS = _ceiling(finalS)
|
||||
m.Add(finalE <= capE)
|
||||
m.Add(finalB <= capB)
|
||||
m.Add(finalS <= capS)
|
||||
|
||||
# ======================================================================
|
||||
# OBJECTIVE IS SET HERE -- maximise Electrum * Brass * Steel (post step 5)
|
||||
# To change the objective, edit the three "final" pools and/or the product
|
||||
# below. (finalE/finalB/finalS are the pools after step 5's gains.)
|
||||
# ======================================================================
|
||||
## NOTE: product can be changed here
|
||||
# prodEB = m.NewIntVar(0, capE * capB, "prodEB")
|
||||
# m.AddMultiplicationEquality(prodEB, [finalE, finalB])
|
||||
# obj = m.NewIntVar(0, capE * capB * capS, "obj")
|
||||
# m.AddMultiplicationEquality(obj, [prodEB, finalS])
|
||||
# m.Maximize(obj)
|
||||
|
||||
# Linear objective instead (it sucks)
|
||||
# m.Maximize(finalE + finalB + finalS)
|
||||
|
||||
# New Product
|
||||
def Eprod(v):
|
||||
return v * v
|
||||
|
||||
def Bprod(v):
|
||||
return v
|
||||
|
||||
def Sprod(v):
|
||||
return v * v
|
||||
|
||||
prodEE = m.NewIntVar(0, Eprod(capE), "prodEE")
|
||||
m.AddMultiplicationEquality(prodEE, [finalE])
|
||||
prodSS = m.NewIntVar(0, Sprod(capS), "prodSS")
|
||||
m.AddMultiplicationEquality(prodSS, [finalS])
|
||||
prodBB = m.NewIntVar(0, Bprod(capB), "prodBB")
|
||||
m.AddMultiplicationEquality(prodBB, [finalB])
|
||||
prodEB = m.NewIntVar(0, Eprod(capE) * Bprod(capB), "prodEB")
|
||||
m.AddMultiplicationEquality(prodEB, [prodEE, prodBB])
|
||||
obj = m.NewIntVar(0, Eprod(capE) * Bprod(capB) * Sprod(capS), "obj")
|
||||
m.AddMultiplicationEquality(obj, [prodEB, prodSS])
|
||||
m.Maximize(obj)
|
||||
|
||||
# ---- Phase 2: solve the product to optimality ----
|
||||
solver = cp_model.CpSolver()
|
||||
solver.parameters.max_time_in_seconds = time_limit
|
||||
solver.parameters.num_search_workers = num_workers
|
||||
status = solver.Solve(
|
||||
m,
|
||||
printer.IntermediateSolutionPrinter(
|
||||
{"electrum": finalE, "brass": finalB, "steel": finalS}
|
||||
),
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print(f"(resource ceilings used: E<={capE} B<={capB} S<={capS})")
|
||||
_report(
|
||||
solver,
|
||||
status,
|
||||
cities,
|
||||
N,
|
||||
isH,
|
||||
isF,
|
||||
isM,
|
||||
isMon,
|
||||
col,
|
||||
ua,
|
||||
ub,
|
||||
ud,
|
||||
ow,
|
||||
rH,
|
||||
rF,
|
||||
rM,
|
||||
cvE,
|
||||
cvB,
|
||||
cvS,
|
||||
owcvE,
|
||||
owcvB,
|
||||
owcvS,
|
||||
mE,
|
||||
mB,
|
||||
mS,
|
||||
mC,
|
||||
owmE,
|
||||
owmB,
|
||||
owmS,
|
||||
owmC,
|
||||
hasA,
|
||||
hasB,
|
||||
hasD,
|
||||
vE,
|
||||
vB,
|
||||
vS,
|
||||
E,
|
||||
B,
|
||||
S,
|
||||
C,
|
||||
finalE,
|
||||
finalB,
|
||||
finalS,
|
||||
)
|
||||
return solver, status
|
||||
|
||||
|
||||
def _report(
|
||||
solver,
|
||||
status,
|
||||
cities,
|
||||
N,
|
||||
isH,
|
||||
isF,
|
||||
isM,
|
||||
isMon,
|
||||
col,
|
||||
ua,
|
||||
ub,
|
||||
ud,
|
||||
ow,
|
||||
rH,
|
||||
rF,
|
||||
rM,
|
||||
cvE,
|
||||
cvB,
|
||||
cvS,
|
||||
owcvE,
|
||||
owcvB,
|
||||
owcvS,
|
||||
mE,
|
||||
mB,
|
||||
mS,
|
||||
mC,
|
||||
owmE,
|
||||
owmB,
|
||||
owmS,
|
||||
owmC,
|
||||
hasA,
|
||||
hasB,
|
||||
hasD,
|
||||
vE,
|
||||
vB,
|
||||
vS,
|
||||
E,
|
||||
B,
|
||||
S,
|
||||
C,
|
||||
finalE,
|
||||
finalB,
|
||||
finalS,
|
||||
):
|
||||
print("status:", solver.StatusName(status))
|
||||
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||||
return
|
||||
typ_name = {}
|
||||
for i in range(N):
|
||||
for t in range(1, NUM_STEPS + 1):
|
||||
if solver.Value(isH[i, t]):
|
||||
typ_name[i, t] = "Hub"
|
||||
elif solver.Value(isF[i, t]):
|
||||
typ_name[i, t] = "Foundry"
|
||||
elif solver.Value(isM[i, t]):
|
||||
typ_name[i, t] = "Metro"
|
||||
elif solver.Value(isMon[i, t]):
|
||||
typ_name[i, t] = "Monument"
|
||||
else:
|
||||
typ_name[i, t] = "-"
|
||||
|
||||
def action_str(i, t):
|
||||
if solver.Value(col[i, t]):
|
||||
if typ_name[i, t] == "Foundry":
|
||||
v = (
|
||||
"E"
|
||||
if solver.Value(cvE[i, t])
|
||||
else "B"
|
||||
if solver.Value(cvB[i, t])
|
||||
else "S"
|
||||
)
|
||||
return f"Collect vat {v}"
|
||||
if typ_name[i, t] == "Metro":
|
||||
picks = []
|
||||
for nm, var in (("E", mE), ("B", mB), ("S", mS), ("C", mC)):
|
||||
picks += [nm] * solver.Value(var[i, t])
|
||||
return "Collect {" + ",".join(picks) + "}"
|
||||
return "Collect (+Capital)"
|
||||
if solver.Value(ow[i, t]):
|
||||
if typ_name[i, t] == "Foundry":
|
||||
v = (
|
||||
"E"
|
||||
if solver.Value(owcvE[i, t])
|
||||
else "B"
|
||||
if solver.Value(owcvB[i, t])
|
||||
else "S"
|
||||
)
|
||||
return f"Overwork vat {v} (2x)"
|
||||
if typ_name[i, t] == "Metro":
|
||||
picks = []
|
||||
for nm, var in (("E", owmE), ("B", owmB), ("S", owmS), ("C", owmC)):
|
||||
picks += [nm] * solver.Value(var[i, t])
|
||||
return "Overwork {" + ",".join(picks) + "} (2x)"
|
||||
return "Overwork (+Capital 2x)"
|
||||
if solver.Value(ua[i, t]):
|
||||
return "Upgrade a (cost-reduction)"
|
||||
if solver.Value(ub[i, t]):
|
||||
return "Upgrade b (collect-bonus)"
|
||||
if solver.Value(ud[i, t]):
|
||||
return "Upgrade d (vat-increment)"
|
||||
if solver.Value(rH[i, t]):
|
||||
return "Renovate -> Hub"
|
||||
if solver.Value(rF[i, t]):
|
||||
return "Renovate -> Foundry"
|
||||
if solver.Value(rM[i, t]):
|
||||
return "Renovate -> Metro"
|
||||
return "(inactive)"
|
||||
|
||||
print("\nPer-step pools (start of step):")
|
||||
print(" step: E B S C")
|
||||
for t in range(1, NUM_STEPS + 2):
|
||||
label = f"after5" if t == NUM_STEPS + 1 else f"start {t}"
|
||||
print(
|
||||
f" {label:>8}: {solver.Value(E[t]):3} {solver.Value(B[t]):3} "
|
||||
f"{solver.Value(S[t]):3} {solver.Value(C[t]):3}"
|
||||
)
|
||||
|
||||
print("\nActions:")
|
||||
for i in range(N):
|
||||
a_step, a_type = cities[i]
|
||||
print(f" City {i} (arrives step {a_step} as {a_type}):")
|
||||
for t in range(a_step, NUM_STEPS + 1):
|
||||
ups = "".join(
|
||||
n
|
||||
for n, h in (("a", hasA), ("b", hasB), ("d", hasD))
|
||||
if solver.Value(h[i, t])
|
||||
)
|
||||
extra = (
|
||||
f" vats(E{solver.Value(vE[i, t])},B{solver.Value(vB[i, t])},S{solver.Value(vS[i, t])})"
|
||||
if typ_name[i, t] == "Foundry"
|
||||
else ""
|
||||
)
|
||||
print(
|
||||
f" step {t}: [{typ_name[i, t]:7}] {action_str(i, t):28}"
|
||||
f" upg[{ups}]{extra}"
|
||||
)
|
||||
|
||||
fe, fb, fs = solver.Value(finalE), solver.Value(finalB), solver.Value(finalS)
|
||||
print(
|
||||
f"\nFINAL E={fe} B={fb} S={fs} product = {fe * fb * fs} sum = {fe + fb + fs}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
solve()
|
||||
|
|
@ -4,16 +4,17 @@ from ortools.sat.python import cp_model
|
|||
class IntermediateSolutionPrinter(cp_model.CpSolverSolutionCallback):
|
||||
"""Callback that prints intermediate solutions."""
|
||||
|
||||
def __init__(self, variables):
|
||||
def __init__(self, variables, *, scale=1.0):
|
||||
cp_model.CpSolverSolutionCallback.__init__(self)
|
||||
self._variables = variables
|
||||
self._solution_count = 0
|
||||
self.scale = scale
|
||||
|
||||
def on_solution_callback(self):
|
||||
"""Called each time an improving solution is found."""
|
||||
print("\n--- Solution ---")
|
||||
for name, var in self._variables.items():
|
||||
print(f"{name} = {self.Value(var)}")
|
||||
print(f"{name} = {self.scale * self.Value(var)}")
|
||||
|
||||
@property
|
||||
def solution_count(self):
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ description = "Add your description here"
|
|||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"flask>=3.1.3",
|
||||
"ortools>=9.15.6755",
|
||||
]
|
||||
|
|
|
|||
823
templates/solver.html
Normal file
823
templates/solver.html
Normal file
|
|
@ -0,0 +1,823 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>City Resource Optimization Solver</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="number"],
|
||||
input[type="text"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="number"]:focus,
|
||||
input[type="text"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.resource-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.arrivals-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.step-arrivals {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.step-arrivals h3 {
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.cities-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actions-grid,
|
||||
.agents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-solve {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.btn-solve:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-solve:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.btn-solve:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
display: none;
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.output-section.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.output-section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.agent-steps-input {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.constraint-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.constraint-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr auto;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.constraint-row.governor {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr auto;
|
||||
}
|
||||
|
||||
.constraint-row select,
|
||||
.constraint-row input {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ddd;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
details>summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details>summary:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🏛️ City Resource Optimization</h1>
|
||||
<p class="subtitle">Solve for optimal resource distribution across cities</p>
|
||||
|
||||
<form id="solverForm">
|
||||
<!-- Initial Resources Section -->
|
||||
<div class="form-section">
|
||||
<h2>Initial Resources (Step 1 Start)</h2>
|
||||
<div class="resource-inputs">
|
||||
<div class="form-group">
|
||||
<label for="initial_E">Electrum</label>
|
||||
<input type="number" id="initial_E" name="initial_E" value="{{ (initial[0] / 10) | round(1) }}"
|
||||
min="0" step="0.1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="initial_B">Brass</label>
|
||||
<input type="number" id="initial_B" name="initial_B" value="{{ initial[1] // 10 }}" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="initial_S">Steel</label>
|
||||
<input type="number" id="initial_S" name="initial_S" value="{{ initial[2] // 10 }}" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="initial_C">Capital</label>
|
||||
<input type="number" id="initial_C" name="initial_C" value="{{ initial[3] // 10 }}" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrivals Schedule Section -->
|
||||
<div class="form-section">
|
||||
<h2>Cities</h2>
|
||||
<div class="arrivals-grid" id="citiesContainer"></div>
|
||||
<button type="button" class="btn-add" onclick="addCity()">+ Add City</button>
|
||||
</div>
|
||||
|
||||
<!-- Enabled Actions Section -->
|
||||
<div class="form-section">
|
||||
<h2>Available Actions</h2>
|
||||
<div class="actions-grid">
|
||||
{% for action_name in actions.keys() | sort %}
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="action_{{ action_name }}" name="action_{{ action_name }}" {% if
|
||||
actions[action_name] %}checked{% endif %}>
|
||||
<label for="action_{{ action_name }}">{{ action_name }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Availability Section -->
|
||||
<div class="form-section">
|
||||
<h2>Agent Availability</h2>
|
||||
<p style="color: #666; font-size: 13px; margin-bottom: 15px;">
|
||||
Specify steps where each agent is available (comma-separated, e.g., "1,2,3,4,5")
|
||||
</p>
|
||||
<div class="agents-grid">
|
||||
{% for agent_name in agents.keys() | sort %}
|
||||
<div class="form-group">
|
||||
<label for="agent_{{ agent_name }}_steps">{{ agent_name }}</label>
|
||||
<input type="text" id="agent_{{ agent_name }}_steps" name="agent_{{ agent_name }}_steps"
|
||||
placeholder="e.g., 1,2,3" class="agent-steps-input" {% if agents[agent_name]
|
||||
%}value="{{ agents[agent_name] | join(',') }}" {% endif %}>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Solver Settings Section -->
|
||||
<div class="form-section">
|
||||
<h2>Solver Settings</h2>
|
||||
<div class="form-group">
|
||||
<label for="time_limit">Time Limit (seconds)</label>
|
||||
<input type="number" id="time_limit" name="time_limit" value="60" min="1">
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="verbose" name="verbose">
|
||||
<label for="verbose">Verbose Output</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Actions Section -->
|
||||
<div class="form-section">
|
||||
<h2>Fixed Actions (Optional)</h2>
|
||||
<p style="color: #666; font-size: 13px; margin-bottom: 15px;">
|
||||
Force specific actions for cities at specific steps
|
||||
</p>
|
||||
<div class="constraint-rows" id="actionConstraints"></div>
|
||||
<button type="button" class="btn-add" onclick="addActionConstraint()">+ Add Action Constraint</button>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Governors Section -->
|
||||
<div class="form-section">
|
||||
<h2>Fixed Governors (Optional)</h2>
|
||||
<p style="color: #666; font-size: 13px; margin-bottom: 15px;">
|
||||
Force specific agents to govern cities at specific steps
|
||||
</p>
|
||||
<div class="constraint-rows" id="governorConstraints"></div>
|
||||
<button type="button" class="btn-add" onclick="addGovernorConstraint()">+ Add Governor
|
||||
Constraint</button>
|
||||
</div>
|
||||
|
||||
<!-- Resource Constraints Section -->
|
||||
<div class="form-section">
|
||||
<h2>Resource Constraints (Optional)</h2>
|
||||
<p style="color: #666; font-size: 13px; margin-bottom: 15px;">
|
||||
Enforce minimum resource levels at specific steps. Examples: <code
|
||||
style="background: #f0f0f0; padding: 2px 4px;">E[3] >= 50</code>, <code
|
||||
style="background: #f0f0f0; padding: 2px 4px;">B[2] + S[2] >= 100</code>
|
||||
</p>
|
||||
<p style="color: red; font-size:13px;">Warning: all resources are multiplied by 10 internally so
|
||||
constraints should also be multiplied by 10 e.g. <code>E[2]>=10</code> checks if electrum is
|
||||
greater or equal than 1 not 10 on day 2</p>
|
||||
<div class="constraint-rows" id="resourceConstraints"></div>
|
||||
<button type="button" class="btn-add" onclick="addResourceConstraint()">+ Add Resource
|
||||
Constraint</button>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="button-group">
|
||||
<button type="submit" class="btn-solve">Solve</button>
|
||||
<button type="reset" class="btn-reset">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Solving... This may take a moment.</p>
|
||||
</div>
|
||||
|
||||
<!-- Output Section -->
|
||||
<div class="output-section" id="outputSection">
|
||||
<h2>Results</h2>
|
||||
<div id="outputContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let cityCount = 0;
|
||||
let actionConstraintCount = 0;
|
||||
let governorConstraintCount = 0;
|
||||
let resourceConstraintCount = 0;
|
||||
|
||||
const AVAILABLE_ACTIONS = {{actions | tojson }};
|
||||
const AVAILABLE_AGENTS = {{agents | tojson }};
|
||||
const NUM_STEPS = {{num_steps}};
|
||||
|
||||
function updateDepartureOptions(cityId) {
|
||||
const arrivalSelect = document.getElementById(`city_${cityId}_arrival_step`);
|
||||
const departureSelect = document.getElementById(`city_${cityId}_departure_step`);
|
||||
|
||||
if (!arrivalSelect || !departureSelect) return;
|
||||
|
||||
const arrivalStep = parseInt(arrivalSelect.value);
|
||||
const currentDeparture = departureSelect.value;
|
||||
|
||||
// Clear departure options
|
||||
departureSelect.innerHTML = '';
|
||||
|
||||
// Add Game End option
|
||||
const gameEndOption = document.createElement('option');
|
||||
gameEndOption.value = NUM_STEPS + 1;
|
||||
gameEndOption.textContent = 'Game End';
|
||||
departureSelect.appendChild(gameEndOption);
|
||||
|
||||
// Add step options only for steps after arrival
|
||||
for (let step = arrivalStep + 1; step <= NUM_STEPS; step++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = step;
|
||||
option.textContent = `Step ${step}`;
|
||||
departureSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Set departure to Game End if current value is no longer valid
|
||||
if (currentDeparture <= arrivalStep) {
|
||||
departureSelect.value = NUM_STEPS + 1;
|
||||
} else if (departureSelect.querySelector(`option[value="${currentDeparture}"]`)) {
|
||||
departureSelect.value = currentDeparture;
|
||||
}
|
||||
}
|
||||
|
||||
function updateVatInputs(cityId) {
|
||||
const typeSelect = document.getElementById(`city_${cityId}_type`);
|
||||
const vatsContainer = document.getElementById(`city_${cityId}_vats`);
|
||||
if (!typeSelect || !vatsContainer) return;
|
||||
|
||||
if (typeSelect.value === 'F') {
|
||||
vatsContainer.style.display = 'block';
|
||||
} else {
|
||||
vatsContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function addCity() {
|
||||
const container = document.getElementById('citiesContainer');
|
||||
const id = cityCount++;
|
||||
|
||||
const cityDiv = document.createElement('div');
|
||||
cityDiv.className = 'step-arrivals';
|
||||
cityDiv.id = `city-${id}`;
|
||||
cityDiv.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||||
<h3 style="margin: 0;">City ${id}</h3>
|
||||
<button type="button" class="btn-remove" style="margin: 0;" onclick="document.getElementById('city-${id}').remove()">Remove</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city_${id}_type">Type</label>
|
||||
<select id="city_${id}_type" name="city_${id}_type" onchange="updateVatInputs(${id})">
|
||||
<option value="H">Hub</option>
|
||||
<option value="F">Foundry</option>
|
||||
<option value="M">Metropolis</option>
|
||||
<option value="N">Monument</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city_${id}_arrival_step">Arrival Step</label>
|
||||
<select id="city_${id}_arrival_step" name="city_${id}_arrival_step" onchange="updateDepartureOptions(${id})">
|
||||
${Array.from({length: NUM_STEPS}, (_, i) => `<option value="${i + 1}">Step ${i + 1}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city_${id}_departure_step">Departure Step</label>
|
||||
<select id="city_${id}_departure_step" name="city_${id}_departure_step">
|
||||
<option value="${NUM_STEPS + 1}">Game End</option>
|
||||
${Array.from({length: NUM_STEPS}, (_, i) => `<option value="${i + 1}">Step ${i + 1}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city_${id}_adjacent">Adjacent Cities</label>
|
||||
<input type="text" id="city_${id}_adjacent" name="city_${id}_adjacent" placeholder="e.g., 0,2,3" class="agent-steps-input">
|
||||
</div>
|
||||
<div id="city_${id}_vats" style="display: none;">
|
||||
<p style="color: #666; font-size: 12px; margin-bottom: 10px; margin-top: 10px;">Foundry Vat Values (defaults to 1):</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label for="city_${id}_vat_E">Electrum</label>
|
||||
<input type="number" id="city_${id}_vat_E" name="city_${id}_vat_E" value="1" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city_${id}_vat_B">Brass</label>
|
||||
<input type="number" id="city_${id}_vat_B" name="city_${id}_vat_B" value="1" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city_${id}_vat_S">Steel</label>
|
||||
<input type="number" id="city_${id}_vat_S" name="city_${id}_vat_S" value="1" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(cityDiv);
|
||||
|
||||
// Initialize departure options based on default arrival step (1)
|
||||
updateDepartureOptions(id);
|
||||
}
|
||||
|
||||
function addActionConstraint() {
|
||||
const container = document.getElementById('actionConstraints');
|
||||
const id = actionConstraintCount++;
|
||||
|
||||
// Get existing city indices
|
||||
const existingCities = Array.from(document.querySelectorAll('[id^="city-"]')).map(el =>
|
||||
parseInt(el.id.split('-')[1])
|
||||
);
|
||||
const maxCity = existingCities.length > 0 ? Math.max(...existingCities) + 1 : 1;
|
||||
const cityOptions = Array.from({length: maxCity}, (_, i) =>
|
||||
`<option value="${i}">${i}</option>`
|
||||
).join('');
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'constraint-row';
|
||||
row.id = `action-constraint-${id}`;
|
||||
row.innerHTML = `
|
||||
<select name="action_city_${id}" class="action-city">
|
||||
${cityOptions}
|
||||
</select>
|
||||
<select name="action_step_${id}">
|
||||
${Array.from({length: NUM_STEPS}, (_, i) => `<option value="${i + 1}">Step ${i + 1}</option>`).join('')}
|
||||
</select>
|
||||
<select name="action_action_${id}">
|
||||
${Object.keys(AVAILABLE_ACTIONS).map(a => `<option value="${a}">${a}</option>`).join('')}
|
||||
</select>
|
||||
<button type="button" class="btn-remove" onclick="document.getElementById('action-constraint-${id}').remove()">Remove</button>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function addGovernorConstraint() {
|
||||
const container = document.getElementById('governorConstraints');
|
||||
const id = governorConstraintCount++;
|
||||
|
||||
// Get existing city indices
|
||||
const existingCities = Array.from(document.querySelectorAll('[id^="city-"]')).map(el =>
|
||||
parseInt(el.id.split('-')[1])
|
||||
);
|
||||
const maxCity = existingCities.length > 0 ? Math.max(...existingCities) + 1 : 1;
|
||||
const cityOptions = Array.from({length: maxCity}, (_, i) =>
|
||||
`<option value="${i}">${i}</option>`
|
||||
).join('');
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'constraint-row governor';
|
||||
row.id = `governor-constraint-${id}`;
|
||||
row.innerHTML = `
|
||||
<select name="gov_city_${id}">
|
||||
${cityOptions}
|
||||
</select>
|
||||
<select name="gov_step_${id}">
|
||||
${Array.from({length: NUM_STEPS}, (_, i) => `<option value="${i + 1}">Step ${i + 1}</option>`).join('')}
|
||||
</select>
|
||||
<select name="gov_agent_${id}">
|
||||
${Object.keys(AVAILABLE_AGENTS).map(a => `<option value="${a}">${a}</option>`).join('')}
|
||||
</select>
|
||||
<div class="checkbox-group" style="margin: 0; gap: 4px;">
|
||||
<input type="checkbox" name="gov_enabled_${id}" id="gov_enabled_${id}" checked>
|
||||
<label for="gov_enabled_${id}" style="margin: 0;">Enabled</label>
|
||||
</div>
|
||||
<button type="button" class="btn-remove" onclick="document.getElementById('governor-constraint-${id}').remove()">Remove</button>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function addResourceConstraint() {
|
||||
const container = document.getElementById('resourceConstraints');
|
||||
const id = resourceConstraintCount++;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'constraint-row';
|
||||
row.id = `resource-constraint-${id}`;
|
||||
row.style.gridTemplateColumns = '1fr auto';
|
||||
row.innerHTML = `
|
||||
<input type="text" name="resource_expr_${id}" placeholder="e.g., E[3] >= 50 or B[2] + S[2] >= 100" style="flex: 1;">
|
||||
<button type="button" class="btn-remove" onclick="document.getElementById('resource-constraint-${id}').remove()">Remove</button>
|
||||
`;
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
document.getElementById('solverForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
// Convert verbose checkbox to boolean
|
||||
data['verbose'] = document.getElementById('verbose').checked;
|
||||
|
||||
// Convert checked checkboxes to true/false for all action enable/disable inputs
|
||||
const enableCheckboxes = form.querySelectorAll('input[name^="action_"][type="checkbox"]');
|
||||
enableCheckboxes.forEach(input => {
|
||||
data[input.name] = input.checked;
|
||||
});
|
||||
|
||||
// Collect fixed action constraints
|
||||
const fixedActions = {};
|
||||
form.querySelectorAll('[id^="action-constraint-"]').forEach(row => {
|
||||
const city = parseInt(row.querySelector('[name^="action_city_"]').value);
|
||||
const step = parseInt(row.querySelector('[name^="action_step_"]').value);
|
||||
const action = row.querySelector('[name^="action_action_"]').value;
|
||||
fixedActions[`${city},${step}`] = action;
|
||||
});
|
||||
|
||||
// Collect fixed governor constraints
|
||||
const fixedGovernors = {};
|
||||
form.querySelectorAll('[id^="governor-constraint-"]').forEach(row => {
|
||||
const city = parseInt(row.querySelector('[name^="gov_city_"]').value);
|
||||
const step = parseInt(row.querySelector('[name^="gov_step_"]').value);
|
||||
const agent = row.querySelector('[name^="gov_agent_"]').value;
|
||||
const enabled = row.querySelector('[name^="gov_enabled_"]').checked;
|
||||
fixedGovernors[`${city},${step},${agent}`] = enabled;
|
||||
});
|
||||
|
||||
// Collect resource constraints
|
||||
const resourceConstraints = [];
|
||||
form.querySelectorAll('[id^="resource-constraint-"]').forEach(row => {
|
||||
const expr = row.querySelector('[name^="resource_expr_"]').value.trim();
|
||||
if (expr) {
|
||||
resourceConstraints.push(expr);
|
||||
}
|
||||
});
|
||||
|
||||
// Add constraints to data if any exist
|
||||
if (Object.keys(fixedActions).length > 0) {
|
||||
data.fixed_actions = fixedActions;
|
||||
}
|
||||
if (Object.keys(fixedGovernors).length > 0) {
|
||||
data.fixed_governors = fixedGovernors;
|
||||
}
|
||||
if (resourceConstraints.length > 0) {
|
||||
data.resource_constraints = resourceConstraints;
|
||||
}
|
||||
|
||||
document.getElementById('loading').classList.add('show');
|
||||
document.getElementById('outputSection').classList.remove('show');
|
||||
|
||||
try {
|
||||
const response = await fetch('/solve', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
document.getElementById('loading').classList.remove('show');
|
||||
document.getElementById('outputSection').classList.add('show');
|
||||
|
||||
// Generate summary of configuration
|
||||
const numCities = Array.from(document.querySelectorAll('[id^="city-"]')).filter(
|
||||
el => document.getElementById(`city_${el.id.split('-')[1]}_type`).value !== 'none'
|
||||
).length;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const summaryText = `${timestamp} • ${numCities} city(cities) • ${result.status}`;
|
||||
|
||||
if (result.success) {
|
||||
// Create new result section
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.style.marginBottom = '20px';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="status success" style="margin-bottom: 10px;">✓ ${summaryText}</div>
|
||||
<details open>
|
||||
<summary>Solver Output</summary>
|
||||
<pre>${result.output}</pre>
|
||||
</details>
|
||||
`;
|
||||
|
||||
// Prepend to output container (newest first)
|
||||
const container = document.getElementById('outputContainer');
|
||||
container.insertBefore(resultDiv, container.firstChild);
|
||||
} else {
|
||||
// Create new error section
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.style.marginBottom = '20px';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="status error" style="margin-bottom: 10px;">✗ ${summaryText}</div>
|
||||
<details open>
|
||||
<summary>Error Details</summary>
|
||||
<pre>${result.error}</pre>
|
||||
</details>
|
||||
`;
|
||||
|
||||
const container = document.getElementById('outputContainer');
|
||||
container.insertBefore(resultDiv, container.firstChild);
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('loading').classList.remove('show');
|
||||
document.getElementById('outputSection').classList.add('show');
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.style.marginBottom = '20px';
|
||||
resultDiv.innerHTML = `
|
||||
<div class="status error" style="margin-bottom: 10px;">✗ ${timestamp} • Request failed</div>
|
||||
<details open>
|
||||
<summary>Error Details</summary>
|
||||
<pre>${error.message}</pre>
|
||||
</details>
|
||||
`;
|
||||
|
||||
const container = document.getElementById('outputContainer');
|
||||
container.insertBefore(resultDiv, container.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize with one city on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
addCity();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
138
uv.lock
138
uv.lock
|
|
@ -19,6 +19,53 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
{ name = "click" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "immutabledict"
|
||||
version = "4.3.1"
|
||||
|
|
@ -28,6 +75,79 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a3/ce/f9018bf69ae91b273b6391a095e7c93fa5e1617f25b6ba81ad4b20c9df10/immutabledict-4.3.1-py3-none-any.whl", hash = "sha256:c9facdc0ff30fdb8e35bd16532026cac472a549e182c94fa201b51b25e4bf7bf", size = 5000, upload-time = "2026-02-15T10:32:33.672Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.6"
|
||||
|
|
@ -192,11 +312,15 @@ name = "solve"
|
|||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "ortools" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "ortools", specifier = ">=9.15.6755" }]
|
||||
requires-dist = [
|
||||
{ name = "flask", specifier = ">=3.1.3" },
|
||||
{ name = "ortools", specifier = ">=9.15.6755" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
|
|
@ -215,3 +339,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35c
|
|||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
|
||||
]
|
||||
|
|
|
|||
177
web_solve.py
Normal file
177
web_solve.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"""Web interface for City Resource Optimization solver."""
|
||||
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from ortools.sat.python import cp_model
|
||||
import json
|
||||
import solve
|
||||
import io
|
||||
import sys
|
||||
from contextlib import redirect_stdout
|
||||
from constraint_validator import make_constraint_lambda
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""Serve the solver form."""
|
||||
return render_template(
|
||||
"solver.html",
|
||||
initial=solve.INITIAL,
|
||||
actions=solve.ENABLED_ACTIONS,
|
||||
agents=solve.AGENT_AVAILABILITY,
|
||||
num_steps=solve.NUM_STEPS,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/solve", methods=["POST"])
|
||||
def solve_handler():
|
||||
"""Handle solver requests from the form."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
# Parse initial resources (divide by 10 since UI shows unscaled values)
|
||||
initial = tuple(
|
||||
int(float(data.get(f"initial_{r}", 3)) * 10) for r in ["E", "B", "S", "C"]
|
||||
)
|
||||
|
||||
# Parse per-city arrivals and departures (dynamic number of cities)
|
||||
arrivals = {}
|
||||
for step in range(1, solve.NUM_STEPS + 1):
|
||||
arrivals[step] = []
|
||||
|
||||
# Find all cities submitted (city_0_type, city_1_type, etc.)
|
||||
city_indices = set()
|
||||
for key in data.keys():
|
||||
if key.startswith("city_") and key.endswith("_type"):
|
||||
city_idx = int(key.split("_")[1])
|
||||
city_indices.add(city_idx)
|
||||
|
||||
for city_idx in sorted(city_indices):
|
||||
city_type = data.get(f"city_{city_idx}_type")
|
||||
arrival_step_str = data.get(f"city_{city_idx}_arrival_step")
|
||||
departure_step_str = data.get(f"city_{city_idx}_departure_step")
|
||||
adjacent_str = data.get(f"city_{city_idx}_adjacent", "")
|
||||
|
||||
if city_type and city_type != "none" and arrival_step_str:
|
||||
arrival_step = int(arrival_step_str)
|
||||
departure_step = (
|
||||
int(departure_step_str)
|
||||
if departure_step_str
|
||||
else solve.NUM_STEPS + 1
|
||||
)
|
||||
|
||||
# Parse adjacent cities (comma-separated, whitespace ignored)
|
||||
adjacent_to = []
|
||||
if adjacent_str:
|
||||
adjacent_to = [
|
||||
int(idx.strip())
|
||||
for idx in adjacent_str.split(",")
|
||||
if idx.strip()
|
||||
]
|
||||
|
||||
city_data = {
|
||||
"type": city_type,
|
||||
"adjacent_to": adjacent_to,
|
||||
"departure_step": departure_step,
|
||||
}
|
||||
|
||||
# Parse vat values for foundries (if specified, use them; otherwise let normalize_city default to 1)
|
||||
if city_type == "F":
|
||||
vat_e = data.get(f"city_{city_idx}_vat_E")
|
||||
vat_b = data.get(f"city_{city_idx}_vat_B")
|
||||
vat_s = data.get(f"city_{city_idx}_vat_S")
|
||||
vats = {}
|
||||
if vat_e:
|
||||
vats["E"] = int(vat_e)
|
||||
if vat_b:
|
||||
vats["B"] = int(vat_b)
|
||||
if vat_s:
|
||||
vats["S"] = int(vat_s)
|
||||
if vats:
|
||||
city_data["vats"] = vats
|
||||
|
||||
arrivals[arrival_step].append(city_data)
|
||||
|
||||
# Parse enabled actions
|
||||
enabled_actions = {}
|
||||
for action_name in solve.ENABLED_ACTIONS.keys():
|
||||
enabled_actions[action_name] = data.get(f"action_{action_name}", False)
|
||||
|
||||
# Parse agent availability
|
||||
agent_availability = {}
|
||||
for agent_name in solve.AGENT_AVAILABILITY.keys():
|
||||
steps_str = data.get(f"agent_{agent_name}_steps", "")
|
||||
if steps_str:
|
||||
agent_availability[agent_name] = [
|
||||
int(s) for s in steps_str.split(",") if s
|
||||
]
|
||||
else:
|
||||
agent_availability[agent_name] = []
|
||||
|
||||
# Parse time limit
|
||||
time_limit = float(data.get("time_limit", 60.0))
|
||||
|
||||
# Parse verbose flag
|
||||
verbose = data.get("verbose", True)
|
||||
|
||||
# Parse fixed constraints
|
||||
fixed_choices = None
|
||||
fixed_actions_data = data.get("fixed_actions", {})
|
||||
fixed_governors_data = data.get("fixed_governors", {})
|
||||
|
||||
if fixed_actions_data or fixed_governors_data:
|
||||
fixed_choices = {}
|
||||
|
||||
if fixed_actions_data:
|
||||
fixed_choices["actions"] = {}
|
||||
for key, action in fixed_actions_data.items():
|
||||
city, step = map(int, key.split(","))
|
||||
fixed_choices["actions"][(city, step)] = action
|
||||
|
||||
if fixed_governors_data:
|
||||
fixed_choices["governors"] = {}
|
||||
for key, enabled in fixed_governors_data.items():
|
||||
city, step, agent = key.rsplit(",", 2)
|
||||
city, step = int(city), int(step)
|
||||
fixed_choices["governors"][(city, step, agent)] = enabled
|
||||
|
||||
# Parse resource constraints
|
||||
resource_constraints = None
|
||||
resource_constraints_data = data.get("resource_constraints", [])
|
||||
if resource_constraints_data:
|
||||
resource_constraints = []
|
||||
for expr in resource_constraints_data:
|
||||
try:
|
||||
constraint_lambda = make_constraint_lambda(expr)
|
||||
resource_constraints.append(constraint_lambda)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid resource constraint '{expr}': {e}")
|
||||
|
||||
# Capture solver output
|
||||
output_buffer = io.StringIO()
|
||||
with redirect_stdout(output_buffer):
|
||||
solver, status = solve.solve(
|
||||
initial=initial,
|
||||
arrivals=arrivals,
|
||||
max_res=solve.MAX_RES,
|
||||
max_vat=solve.MAX_VAT,
|
||||
# min to avoid bricking stuff
|
||||
time_limit=min(time_limit, 60.0),
|
||||
num_workers=1,
|
||||
verbose=verbose,
|
||||
fixed_choices=fixed_choices,
|
||||
resource_constraints=resource_constraints,
|
||||
)
|
||||
|
||||
output = output_buffer.getvalue()
|
||||
status_name = solver.StatusName(status) if status else "UNKNOWN"
|
||||
|
||||
return jsonify({"success": True, "status": status_name, "output": output})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=False, host="0.0.0.0", port=5000)
|
||||
Loading…
Reference in a new issue