feat: expose objective_factors and objective_mode in web UI

The solver form gains an Objective section with a product/sum mode
select and per-resource factor inputs (E/B/S/C, zero = excluded).
The /solve handler parses both fields and forwards them to solve(),
falling back to solve.py defaults when omitted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Pagwin 2026-06-11 21:48:42 -04:00
parent 47e21678df
commit 24f58765ac
No known key found for this signature in database
GPG key ID: 81137023740CA260
2 changed files with 68 additions and 0 deletions

View file

@ -414,6 +414,47 @@
</div>
</div>
<!-- Objective Section -->
<div class="form-section">
<h2>Objective</h2>
<p style="color: #666; font-size: 13px; margin-bottom: 15px;">
Factor = exponent in product mode, weight in sum mode. A factor of 0 excludes that
resource from the objective (it is not forced to zero). Negative factors are only
allowed in sum mode. Factors are unaffected by the internal x10 resource scaling.
</p>
<div class="form-group">
<label for="objective_mode">Mode</label>
<select id="objective_mode" name="objective_mode">
<option value="product" {% if objective_mode=="product" %}selected{% endif %}>Product
(maximize E^a &times; B^b &times; ...)</option>
<option value="sum" {% if objective_mode=="sum" %}selected{% endif %}>Sum
(maximize a&middot;E + b&middot;B + ...)</option>
</select>
</div>
<div class="resource-inputs">
<div class="form-group">
<label for="objective_factor_E">Electrum Factor</label>
<input type="number" id="objective_factor_E" name="objective_factor_E"
value="{{ objective_factors.get('E', 0) }}" step="1">
</div>
<div class="form-group">
<label for="objective_factor_B">Brass Factor</label>
<input type="number" id="objective_factor_B" name="objective_factor_B"
value="{{ objective_factors.get('B', 0) }}" step="1">
</div>
<div class="form-group">
<label for="objective_factor_S">Steel Factor</label>
<input type="number" id="objective_factor_S" name="objective_factor_S"
value="{{ objective_factors.get('S', 0) }}" step="1">
</div>
<div class="form-group">
<label for="objective_factor_C">Capital Factor</label>
<input type="number" id="objective_factor_C" name="objective_factor_C"
value="{{ objective_factors.get('C', 0) }}" step="1">
</div>
</div>
</div>
<!-- Solver Settings Section -->
<div class="form-section">
<h2>Solver Settings</h2>
@ -699,6 +740,17 @@
data[input.name] = input.checked;
});
// Collect objective mode and factors (only nonzero factors; missing = excluded)
data.objective_mode = document.getElementById('objective_mode').value;
const objectiveFactors = {};
['E', 'B', 'S', 'C'].forEach(r => {
const factor = parseInt(document.getElementById(`objective_factor_${r}`).value);
if (factor) {
objectiveFactors[r] = factor;
}
});
data.objective_factors = objectiveFactors;
// Collect fixed action constraints
const fixedActions = {};
form.querySelectorAll('[id^="action-constraint-"]').forEach(row => {

View file

@ -21,6 +21,8 @@ def index():
actions=solve.ENABLED_ACTIONS,
agents=solve.AGENT_AVAILABILITY,
num_steps=solve.NUM_STEPS,
objective_factors=solve.OBJECTIVE_FACTORS,
objective_mode=solve.OBJECTIVE_MODE,
)
@ -138,6 +140,18 @@ def solve_handler():
city, step = int(city), int(step)
fixed_choices["governors"][(city, step, agent)] = enabled
# Parse objective (factor = exponent in "product" mode, weight in
# "sum" mode; missing keys = resource excluded). Factors are NOT x10
# scaled — they apply to the scaled resource totals and the report
# descales the objective. None falls back to solve.py's defaults.
objective_mode = data.get("objective_mode") or None
objective_factors = None
objective_factors_data = data.get("objective_factors")
if objective_factors_data is not None:
# All-zero dict passes through so solve()'s validation rejects it
# with a clear error instead of silently using the defaults.
objective_factors = {r: int(f) for r, f in objective_factors_data.items()}
# Parse resource constraints
resource_constraints = None
resource_constraints_data = data.get("resource_constraints", [])
@ -165,6 +179,8 @@ def solve_handler():
verbose=verbose,
fixed_choices=fixed_choices,
resource_constraints=resource_constraints,
objective_factors=objective_factors,
objective_mode=objective_mode,
)
output = output_buffer.getvalue()