diff --git a/index.html b/index.html
index 21e9899..89f2a32 100644
--- a/index.html
+++ b/index.html
@@ -518,11 +518,73 @@
if (value !== undefined && value !== null && value !== "") sel.value = String(value);
return sel;
}
+ // Plain turn-number selects (no "final" entry): a blank option (value "")
+ // labeled however the caller likes, then turn 0..count-1. Used for the
+ // cities' Arrival/Departure turns where blank means "unbounded" rather
+ // than "final".
+ const plainTurnSelects = new Set();
+ function fillPlainTurnOptions(sel) {
+ const prev = sel.value;
+ sel.innerHTML = "";
+ sel.append(el("option", {value: ""}, sel._blankLabel || ""));
+ for (let t = 0; t < turnsCount(); t++)
+ sel.append(el("option", {value: String(t)}, "turn " + t));
+ sel.value = [...sel.options].some(o => o.value === prev) ? prev : "";
+ }
+ function plainTurnSelect(value, blankLabel) {
+ const sel = el("select");
+ sel._blankLabel = blankLabel;
+ plainTurnSelects.add(sel);
+ fillPlainTurnOptions(sel);
+ if (value !== undefined && value !== null && value !== "") sel.value = String(value);
+ return sel;
+ }
+
+ // Multi-turn picker: a row of checkboxes ("final" + turn 0..count-1).
+ // _selected() returns the checked values as strings ("" = final). Registers
+ // for refresh so its options track the Turns count, preserving selections.
+ const multiTurnGroups = new Set();
+ function fillMultiTurnOptions(group) {
+ const prev = new Set((group._boxes || []).filter(b => b.checked).map(b => b.value));
+ group._boxes = [];
+ group.innerHTML = "";
+ const mk = (val, label) => {
+ const cb = el("input", {type: "checkbox"});
+ cb.value = val;
+ if (prev.has(val)) cb.checked = true;
+ group._boxes.push(cb);
+ return el("label", {style: "display:inline-block;margin-right:.6rem;font-size:.85rem"},
+ [cb, " " + label]);
+ };
+ group.append(mk("", "final"));
+ for (let t = 0; t < turnsCount(); t++) group.append(mk(String(t), "turn " + t));
+ }
+ function multiTurnSelect(values) {
+ const group = el("div", {class: "multi-turn"});
+ group._boxes = [];
+ group._selected = () => group._boxes.filter(b => b.checked).map(b => b.value);
+ multiTurnGroups.add(group);
+ fillMultiTurnOptions(group);
+ if (values && values.length) {
+ const want = new Set(values.map(v => (v === "" || v == null) ? "" : String(v)));
+ for (const b of group._boxes) if (want.has(b.value)) b.checked = true;
+ }
+ return group;
+ }
+
function refreshTurnSelects() {
for (const sel of turnSelects) {
if (!sel.isConnected) {turnSelects.delete(sel); continue;}
fillTurnOptions(sel);
}
+ for (const sel of plainTurnSelects) {
+ if (!sel.isConnected) {plainTurnSelects.delete(sel); continue;}
+ fillPlainTurnOptions(sel);
+ }
+ for (const group of multiTurnGroups) {
+ if (!group.isConnected) {multiTurnGroups.delete(group); continue;}
+ fillMultiTurnOptions(group);
+ }
}
// --- starting resources & tradeable ---
@@ -562,7 +624,16 @@
placeholder: "Bearhearth, Kingsland"
});
const forced = el("input", {value: "", placeholder: "0:upgrade"});
- const avail = el("input", {value: "", placeholder: ""});
+ // Available turns are given as an inclusive arrival..departure range and
+ // expanded into the explicit list the solver expects. Blank arrival means
+ // "from turn 0", blank departure means "through the last turn"; both blank
+ // means available on every turn.
+ const arrival = plainTurnSelect(undefined, "start");
+ const departure = plainTurnSelect(undefined, "end");
+ if (c.available_turns && c.available_turns.length) {
+ arrival.value = String(Math.min(...c.available_turns));
+ departure.value = String(Math.max(...c.available_turns));
+ }
// Upgrade checkboxes: the 3 universal upgrades plus the current type's
// type-specific upgrade. State persists across type changes (hidden boxes
@@ -616,8 +687,14 @@
if (adj) o.adjacent = adj;
const fa = parsePairs(forced.value);
if (Object.keys(fa).length) o.forced_action = fa;
- const at = parseInts(avail.value);
- if (at) o.available_turns = at;
+ const a = arrival.value, d = departure.value;
+ if (a !== "" || d !== "") {
+ const lo = a !== "" ? +a : 0;
+ const hi = d !== "" ? +d : Math.max(0, turnsCount() - 1);
+ const at = [];
+ for (let t = lo; t <= hi; t++) at.push(t);
+ if (at.length) o.available_turns = at;
+ }
return o;
};
card.append(
@@ -630,7 +707,8 @@
field("Upgrades (already installed)", upWrap),
field("Adjacent cities (csv of names)", adjacent),
field("Forced actions (turn:action, csv)", forced),
- field("Avail turns (csv, blank=all)", avail),
+ field("Arrival turn (blank=start)", arrival),
+ field("Departure turn (blank=end)", departure),
vatGroup,
el("div", {class: "card-actions"}, removeBtn(card)));
document.getElementById("cities").append(card);
@@ -798,15 +876,21 @@
const to = selectEl(RESOURCES, c.to || "capital");
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0});
const maxCount = num(c.max_count ?? 1, {min: 1});
- const turn = turnSelect(c.turn);
+ // Multiple turns may be selected; each chosen turn yields its own copy of
+ // this conversion in the problem JSON (one entry per turn).
+ const turns = multiTurnSelect(c.turn != null ? [c.turn] : []);
card._get = () => {
- const o = {
+ const base = {
from: from.value, to: to.value,
from_amount: +fromAmt.value, to_amount: +toAmt.value,
max_count: +maxCount.value,
};
- if (turn.value !== "") o.turn = +turn.value;
- return o;
+ const sel = turns._selected();
+ return (sel.length ? sel : [""]).map(tv => {
+ const o = {...base};
+ if (tv !== "") o.turn = +tv;
+ return o;
+ });
};
card.append(
field("From", from),
@@ -814,7 +898,7 @@
field("To", to),
field("To amount", toAmt),
field("Max count", maxCount),
- field("Turn (blank=final)", turn),
+ field("Turns (none=final)", turns),
el("div", {class: "card-actions"}, removeBtn(card)));
document.getElementById("optional_conversions").append(card);
}
@@ -858,7 +942,7 @@
objective: {terms: collect("terms")},
resource_constraints: collect("constraints"),
conversions: collect("conversions"),
- optional_conversions: collect("optional_conversions"),
+ optional_conversions: collect("optional_conversions").flat(),
};
}