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"
|
placeholder: "Bearhearth, Kingsland"
|
||||||
});
|
});
|
||||||
const forced = el("input", {value: pairsToStr(c.forced_action), placeholder: "0:upgrade"});
|
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
|
// Available turns are given as an inclusive arrival..departure range and
|
||||||
// expanded into the explicit list the solver expects. Blank arrival means
|
// expanded into the explicit list the solver expects. Blank arrival means
|
||||||
// "from turn 0", blank departure means "through the last turn"; both blank
|
// "from turn 0", blank departure means "through the last turn"; both blank
|
||||||
|
|
@ -713,6 +716,8 @@
|
||||||
if (adj) o.adjacent = adj;
|
if (adj) o.adjacent = adj;
|
||||||
const fa = parsePairs(forced.value);
|
const fa = parsePairs(forced.value);
|
||||||
if (Object.keys(fa).length) o.forced_action = fa;
|
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;
|
const a = arrival.value, d = departure.value;
|
||||||
if (a !== "" || d !== "") {
|
if (a !== "" || d !== "") {
|
||||||
const lo = a !== "" ? +a : 0;
|
const lo = a !== "" ? +a : 0;
|
||||||
|
|
@ -733,6 +738,7 @@
|
||||||
field("Upgrades (already installed)", upWrap),
|
field("Upgrades (already installed)", upWrap),
|
||||||
field("Adjacent cities (csv of names)", adjacent),
|
field("Adjacent cities (csv of names)", adjacent),
|
||||||
field("Forced actions (turn:action, csv)", forced),
|
field("Forced actions (turn:action, csv)", forced),
|
||||||
|
field("Hint actions (turn:action, csv)", hinted),
|
||||||
field("Arrival turn (blank=start)", arrival),
|
field("Arrival turn (blank=start)", arrival),
|
||||||
field("Departure turn (blank=end)", departure),
|
field("Departure turn (blank=end)", departure),
|
||||||
vatGroup,
|
vatGroup,
|
||||||
|
|
@ -754,6 +760,9 @@
|
||||||
const desc = el("span", {class: "help"});
|
const desc = el("span", {class: "help"});
|
||||||
const bastions = num(a.bonus_trade_goods || 3, {min: 0});
|
const bastions = num(a.bonus_trade_goods || 3, {min: 0});
|
||||||
const forced = el("input", {value: pairsToStr(a.forced_city), placeholder: "0:Aridias"});
|
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 avail = el("input", {value: (a.available_turns || []).join(", ")});
|
||||||
|
|
||||||
const bastionsField = field("Bastions (Baron only)", bastions);
|
const bastionsField = field("Bastions (Baron only)", bastions);
|
||||||
|
|
@ -773,6 +782,8 @@
|
||||||
if (type.value === "Baron") o.bonus_trade_goods = +bastions.value;
|
if (type.value === "Baron") o.bonus_trade_goods = +bastions.value;
|
||||||
const fc = parsePairs(forced.value, true);
|
const fc = parsePairs(forced.value, true);
|
||||||
if (Object.keys(fc).length) o.forced_city = fc;
|
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);
|
const at = parseInts(avail.value);
|
||||||
if (at) o.available_turns = at;
|
if (at) o.available_turns = at;
|
||||||
return o;
|
return o;
|
||||||
|
|
@ -781,6 +792,7 @@
|
||||||
field("Agent", type),
|
field("Agent", type),
|
||||||
field("Effect", desc),
|
field("Effect", desc),
|
||||||
field("Forced city (turn:city, csv)", forced),
|
field("Forced city (turn:city, csv)", forced),
|
||||||
|
field("Hint city (turn:city, csv)", hintCity),
|
||||||
field("Avail turns (csv, blank=all)", avail),
|
field("Avail turns (csv, blank=all)", avail),
|
||||||
bastionsField,
|
bastionsField,
|
||||||
el("div", {class: "card-actions"}, removeBtn(card)));
|
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
|
can_renovate: bool = True # Metropolis cannot renovate
|
||||||
# Hard constraint: force a specific action on a given turn (turn -> Action).
|
# Hard constraint: force a specific action on a given turn (turn -> Action).
|
||||||
forced_action: dict[int, str] = field(default_factory=dict)
|
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):
|
def __post_init__(self):
|
||||||
if self.renown is None:
|
if self.renown is None:
|
||||||
|
|
@ -218,6 +222,10 @@ class Agent:
|
||||||
available_turns: Optional[list[int]] = None
|
available_turns: Optional[list[int]] = None
|
||||||
# Hard constraint: must govern this city on this turn (turn -> city name).
|
# Hard constraint: must govern this city on this turn (turn -> city name).
|
||||||
forced_city: dict[int, str] = field(default_factory=dict)
|
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:
|
def is_available(self, t: int) -> bool:
|
||||||
return self.available_turns is None or t in self.available_turns
|
return self.available_turns is None or t in self.available_turns
|
||||||
|
|
@ -543,6 +551,7 @@ class _Builder:
|
||||||
self._build_renown_total()
|
self._build_renown_total()
|
||||||
self._build_resource_constraints()
|
self._build_resource_constraints()
|
||||||
self._build_objective()
|
self._build_objective()
|
||||||
|
self._build_hints()
|
||||||
return self.m
|
return self.m
|
||||||
|
|
||||||
def _build_actions_and_governors(self):
|
def _build_actions_and_governors(self):
|
||||||
|
|
@ -629,6 +638,41 @@ class _Builder:
|
||||||
# Only one overworking placement at a time is already implied by
|
# Only one overworking placement at a time is already implied by
|
||||||
# the agent's "<=1 city" constraint.
|
# 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):
|
def _build_adjacency(self):
|
||||||
"""Build the symmetric City adjacency map (``self.adj[ci]`` -> set of
|
"""Build the symmetric City adjacency map (``self.adj[ci]`` -> set of
|
||||||
adjacent City indices). Declaring A adjacent to B also makes B adjacent
|
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"])
|
c["type"] = CityType(c["type"])
|
||||||
if "forced_action" in c:
|
if "forced_action" in c:
|
||||||
c["forced_action"] = {int(k): v for k, v in c["forced_action"].items()}
|
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))
|
cities.append(City(**c))
|
||||||
agents = []
|
agents = []
|
||||||
for a in d.get("agents", []):
|
for a in d.get("agents", []):
|
||||||
a = dict(a)
|
a = dict(a)
|
||||||
if "forced_city" in a:
|
if "forced_city" in a:
|
||||||
a["forced_city"] = {int(k): v for k, v in a["forced_city"].items()}
|
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))
|
agents.append(Agent(**a))
|
||||||
obj_d = dict(d.get("objective", {}))
|
obj_d = dict(d.get("objective", {}))
|
||||||
if "terms" in obj_d:
|
if "terms" in obj_d:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue