web server with docker container
This commit is contained in:
parent
b1dba083a0
commit
3e604324ad
7 changed files with 1194 additions and 59 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"]
|
||||
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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
35
solve.py
35
solve.py
|
|
@ -63,7 +63,7 @@ FIXED_CHOICES = {
|
|||
|
||||
# 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.
|
||||
# Arriving cities act that same step.
|
||||
# Each entry may be either a string shorthand (e.g. "H") or a dict, e.g.
|
||||
# {"type": "F", "adjacent_to": [city_idx, ...], "vats": {"E": 1, "B": 1, "S": 1},
|
||||
# "departure_step": NUM_STEPS + 1}
|
||||
|
|
@ -930,14 +930,33 @@ class Foreman(Agent):
|
|||
|
||||
|
||||
class Industrialist(Agent):
|
||||
"""TODO: Network ability — grants Infrastructure to governed city and its
|
||||
adjacent cities when appointed governor. Requires the city's adjacency list
|
||||
(city_dict['adjacent_to']) to be populated and an Infrastructure mechanic
|
||||
in the model. Neither is implemented yet.
|
||||
"""
|
||||
"""Network ability — grants Infrastructure (hasA) to governed city and its
|
||||
adjacent cities when appointed governor."""
|
||||
|
||||
def apply(self, model, i, t, ctx):
|
||||
pass
|
||||
m = model
|
||||
g = ctx["governor"].get((i, t, self.name))
|
||||
if g is None:
|
||||
return
|
||||
|
||||
cities = ctx.get("cities")
|
||||
hasA = ctx.get("hasA")
|
||||
NUM_STEPS = ctx.get("NUM_STEPS", 5)
|
||||
|
||||
if cities is None or hasA is None:
|
||||
return
|
||||
|
||||
a_step, a_city = cities[i]
|
||||
adjacent_indices = a_city.get("adjacent_to", [])
|
||||
|
||||
# When Industrialist governs city i at step t, grant hasA to city i and adjacent cities at t+1
|
||||
if t + 1 <= NUM_STEPS:
|
||||
# Grant to governed city
|
||||
m.Add(hasA[i, t + 1] == 1).OnlyEnforceIf(g)
|
||||
|
||||
# Grant to adjacent cities
|
||||
for adj_i in adjacent_indices:
|
||||
m.Add(hasA[adj_i, t + 1] == 1).OnlyEnforceIf(g)
|
||||
|
||||
|
||||
class Fence(EventAgent):
|
||||
|
|
@ -1230,6 +1249,8 @@ def solve(
|
|||
"vat_next": {},
|
||||
"builder_fire": builder_fire_map,
|
||||
"baron_deposits": baron_deposits,
|
||||
"cities": cities,
|
||||
"NUM_STEPS": NUM_STEPS,
|
||||
}
|
||||
|
||||
# Declare all action variables
|
||||
|
|
|
|||
779
templates/solver.html
Normal file
779
templates/solver.html
Normal file
|
|
@ -0,0 +1,779 @@
|
|||
<!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>
|
||||
|
||||
<!-- 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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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" },
|
||||
]
|
||||
|
|
|
|||
162
web_solve.py
162
web_solve.py
|
|
@ -0,0 +1,162 @@
|
|||
"""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
|
||||
|
||||
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
|
||||
|
||||
# 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,
|
||||
time_limit=time_limit,
|
||||
num_workers=8,
|
||||
verbose=verbose,
|
||||
fixed_choices=fixed_choices,
|
||||
)
|
||||
|
||||
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