Google OR tools hints
This commit is contained in:
parent
3f9ee57208
commit
71a428ee48
2 changed files with 60 additions and 0 deletions
12
index.html
12
index.html
|
|
@ -650,6 +650,9 @@
|
|||
placeholder: "Bearhearth, Kingsland"
|
||||
});
|
||||
const forced = el("input", {value: pairsToStr(c.forced_action), placeholder: "0:upgrade"});
|
||||
// Soft hint (not a constraint): a suggested action per turn that
|
||||
// warm-starts the search but can be overridden by the solver.
|
||||
const hinted = el("input", {value: pairsToStr(c.hint_action), placeholder: "0:collect"});
|
||||
// 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
|
||||
|
|
@ -713,6 +716,8 @@
|
|||
if (adj) o.adjacent = adj;
|
||||
const fa = parsePairs(forced.value);
|
||||
if (Object.keys(fa).length) o.forced_action = fa;
|
||||
const ha = parsePairs(hinted.value);
|
||||
if (Object.keys(ha).length) o.hint_action = ha;
|
||||
const a = arrival.value, d = departure.value;
|
||||
if (a !== "" || d !== "") {
|
||||
const lo = a !== "" ? +a : 0;
|
||||
|
|
@ -733,6 +738,7 @@
|
|||
field("Upgrades (already installed)", upWrap),
|
||||
field("Adjacent cities (csv of names)", adjacent),
|
||||
field("Forced actions (turn:action, csv)", forced),
|
||||
field("Hint actions (turn:action, csv)", hinted),
|
||||
field("Arrival turn (blank=start)", arrival),
|
||||
field("Departure turn (blank=end)", departure),
|
||||
vatGroup,
|
||||
|
|
@ -754,6 +760,9 @@
|
|||
const desc = el("span", {class: "help"});
|
||||
const bastions = num(a.bonus_trade_goods || 3, {min: 0});
|
||||
const forced = el("input", {value: pairsToStr(a.forced_city), placeholder: "0:Aridias"});
|
||||
// Soft hint (not a constraint): a suggested city to govern per turn
|
||||
// that biases the search but can be overridden by the solver.
|
||||
const hintCity = el("input", {value: pairsToStr(a.hint_city), placeholder: "0:Aridias"});
|
||||
const avail = el("input", {value: (a.available_turns || []).join(", ")});
|
||||
|
||||
const bastionsField = field("Bastions (Baron only)", bastions);
|
||||
|
|
@ -773,6 +782,8 @@
|
|||
if (type.value === "Baron") o.bonus_trade_goods = +bastions.value;
|
||||
const fc = parsePairs(forced.value, true);
|
||||
if (Object.keys(fc).length) o.forced_city = fc;
|
||||
const hc = parsePairs(hintCity.value, true);
|
||||
if (Object.keys(hc).length) o.hint_city = hc;
|
||||
const at = parseInts(avail.value);
|
||||
if (at) o.available_turns = at;
|
||||
return o;
|
||||
|
|
@ -781,6 +792,7 @@
|
|||
field("Agent", type),
|
||||
field("Effect", desc),
|
||||
field("Forced city (turn:city, csv)", forced),
|
||||
field("Hint city (turn:city, csv)", hintCity),
|
||||
field("Avail turns (csv, blank=all)", avail),
|
||||
bastionsField,
|
||||
el("div", {class: "card-actions"}, removeBtn(card)));
|
||||
|
|
|
|||
48
solve.py
48
solve.py
|
|
@ -183,6 +183,10 @@ class City:
|
|||
can_renovate: bool = True # Metropolis cannot renovate
|
||||
# Hard constraint: force a specific action on a given turn (turn -> Action).
|
||||
forced_action: dict[int, str] = field(default_factory=dict)
|
||||
# Soft hint (NOT a constraint): a suggested action on a given turn
|
||||
# (turn -> Action). Warm-starts/biases the search toward this plan, but the
|
||||
# solver may override it if a better plan exists (cf. forced_action).
|
||||
hint_action: dict[int, str] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.renown is None:
|
||||
|
|
@ -218,6 +222,10 @@ class Agent:
|
|||
available_turns: Optional[list[int]] = None
|
||||
# Hard constraint: must govern this city on this turn (turn -> city name).
|
||||
forced_city: dict[int, str] = field(default_factory=dict)
|
||||
# Soft hint (NOT a constraint): a suggested city to govern on this turn
|
||||
# (turn -> city name). Warm-starts/biases the search, but the solver may
|
||||
# override it (cf. forced_city).
|
||||
hint_city: dict[int, str] = field(default_factory=dict)
|
||||
|
||||
def is_available(self, t: int) -> bool:
|
||||
return self.available_turns is None or t in self.available_turns
|
||||
|
|
@ -543,6 +551,7 @@ class _Builder:
|
|||
self._build_renown_total()
|
||||
self._build_resource_constraints()
|
||||
self._build_objective()
|
||||
self._build_hints()
|
||||
return self.m
|
||||
|
||||
def _build_actions_and_governors(self):
|
||||
|
|
@ -629,6 +638,41 @@ class _Builder:
|
|||
# Only one overworking placement at a time is already implied by
|
||||
# the agent's "<=1 city" constraint.
|
||||
|
||||
def _build_hints(self):
|
||||
"""Apply soft solution hints via ``model.AddHint``. Unlike the forced_*
|
||||
fields these add no constraints: they only warm-start / bias the search
|
||||
toward a suggested plan, and the solver may override any of them.
|
||||
|
||||
* City.hint_action : suggest a City takes a given Action on a Turn.
|
||||
* Agent.hint_city : suggest an Agent governs a given City on a Turn.
|
||||
|
||||
Hints that can't apply are skipped: out-of-range or unavailable Turns,
|
||||
an Action the City can never take (Renovate on a Metropolis / a City
|
||||
that can't renovate), or an unknown target City."""
|
||||
m = self.m
|
||||
for ci, city in enumerate(self.p.cities):
|
||||
for t, action in city.hint_action.items():
|
||||
if not (0 <= t < self.T) or not city.is_available(t):
|
||||
continue
|
||||
a = Action(action)
|
||||
# Renovate is a fixed 0-constant for these Cities (see
|
||||
# _build_actions_and_governors); it isn't a real decision var.
|
||||
if a == Action.RENOVATE and (
|
||||
not city.can_renovate or city.type == CityType.METROPOLIS
|
||||
):
|
||||
continue
|
||||
var = self.act.get((ci, t, a))
|
||||
if var is not None:
|
||||
m.AddHint(var, 1)
|
||||
for ai, agent in enumerate(self.p.agents):
|
||||
for t, city_name in agent.hint_city.items():
|
||||
if not (0 <= t < self.T) or not agent.is_available(t):
|
||||
continue
|
||||
ci = self._city_index(city_name)
|
||||
g = self.gov.get((ai, ci, t))
|
||||
if g is not None:
|
||||
m.AddHint(g, 1)
|
||||
|
||||
def _build_adjacency(self):
|
||||
"""Build the symmetric City adjacency map (``self.adj[ci]`` -> set of
|
||||
adjacent City indices). Declaring A adjacent to B also makes B adjacent
|
||||
|
|
@ -1690,12 +1734,16 @@ def problem_from_dict(d: dict) -> Problem:
|
|||
c["type"] = CityType(c["type"])
|
||||
if "forced_action" in c:
|
||||
c["forced_action"] = {int(k): v for k, v in c["forced_action"].items()}
|
||||
if "hint_action" in c:
|
||||
c["hint_action"] = {int(k): v for k, v in c["hint_action"].items()}
|
||||
cities.append(City(**c))
|
||||
agents = []
|
||||
for a in d.get("agents", []):
|
||||
a = dict(a)
|
||||
if "forced_city" in a:
|
||||
a["forced_city"] = {int(k): v for k, v in a["forced_city"].items()}
|
||||
if "hint_city" in a:
|
||||
a["hint_city"] = {int(k): v for k, v in a["hint_city"].items()}
|
||||
agents.append(Agent(**a))
|
||||
obj_d = dict(d.get("objective", {}))
|
||||
if "terms" in obj_d:
|
||||
|
|
|
|||
Loading…
Reference in a new issue