Compare commits
No commits in common. "0c94d9493601c31705bc5087c630953e95951c64" and "2207cf2fbcb2e01d88327e86a538785a2f75aeaf" have entirely different histories.
0c94d94936
...
2207cf2fbc
2 changed files with 48 additions and 151 deletions
51
index.html
51
index.html
|
|
@ -53,13 +53,6 @@
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Flag an amount that's too precise for the resource (e.g. a fractional
|
|
||||||
non-Electrum amount, or finer than a tenth of Electrum): the step
|
|
||||||
attribute makes it :invalid and the server rejects it too. */
|
|
||||||
input:invalid {
|
|
||||||
outline: 2px solid #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
padding: .3rem .7rem;
|
padding: .3rem .7rem;
|
||||||
|
|
@ -413,10 +406,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const RESOURCES = ["capital", "luxuries", "steel", "brass", "electrum", "trade_goods", "express"];
|
const RESOURCES = ["capital", "luxuries", "steel", "brass", "electrum", "trade_goods", "express"];
|
||||||
// Electrum is the one resource the solver tracks in tenths (it takes
|
|
||||||
// fractional amounts); its log tables are therefore sampled per tenth.
|
|
||||||
// Must match ELECTRUM_SCALE in solve.py.
|
|
||||||
const ELECTRUM_SCALE = 10;
|
|
||||||
const SCORE_KEYS = RESOURCES.concat(["renown"]);
|
const SCORE_KEYS = RESOURCES.concat(["renown"]);
|
||||||
const CITY_TYPES = ["hub", "foundry", "monument", "metropolis"];
|
const CITY_TYPES = ["hub", "foundry", "monument", "metropolis"];
|
||||||
const ACTIONS = ["idle", "collect", "renovate", "upgrade", "launch"];
|
const ACTIONS = ["idle", "collect", "renovate", "upgrade", "launch"];
|
||||||
|
|
@ -469,11 +458,6 @@
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
function num(value, attrs = {}) {return el("input", {type: "number", value, ...attrs});}
|
function num(value, attrs = {}) {return el("input", {type: "number", value, ...attrs});}
|
||||||
// Smallest amount a resource can be specified in: Electrum is tracked in
|
|
||||||
// tenths (0.1 steps), every other resource is whole-unit (integers). Used
|
|
||||||
// to set the <input step> so over-precise amounts read as :invalid; the
|
|
||||||
// server enforces the same rule. "renown" (constraints) is whole-unit too.
|
|
||||||
function resStep(resource) {return resource === "electrum" ? "0.1" : "1";}
|
|
||||||
function selectEl(opts, value) {
|
function selectEl(opts, value) {
|
||||||
const s = el("select");
|
const s = el("select");
|
||||||
for (const o of opts) {
|
for (const o of opts) {
|
||||||
|
|
@ -607,7 +591,7 @@
|
||||||
const startInputs = {};
|
const startInputs = {};
|
||||||
const tradeInputs = {};
|
const tradeInputs = {};
|
||||||
for (const r of RESOURCES) {
|
for (const r of RESOURCES) {
|
||||||
const inp = num(r === "express" || r === "trade_goods" ? 0 : 3, {min: 0, step: resStep(r)});
|
const inp = num(r === "express" || r === "trade_goods" ? 0 : 3, {min: 0});
|
||||||
startInputs[r] = inp;
|
startInputs[r] = inp;
|
||||||
document.getElementById("start").append(
|
document.getElementById("start").append(
|
||||||
el("label", {class: "field"}, [el("span", {}, r), inp]));
|
el("label", {class: "field"}, [el("span", {}, r), inp]));
|
||||||
|
|
@ -793,7 +777,7 @@
|
||||||
card._get = () => {
|
card._get = () => {
|
||||||
const o = {resource: res.value, scalar: +scalar.value};
|
const o = {resource: res.value, scalar: +scalar.value};
|
||||||
if (turn.value !== "") o.turn = +turn.value;
|
if (turn.value !== "") o.turn = +turn.value;
|
||||||
if (isLog.checked) o.log_mapping = buildLogTable(expr.value, res.value);
|
if (isLog.checked) o.log_mapping = buildLogTable(expr.value);
|
||||||
return o;
|
return o;
|
||||||
};
|
};
|
||||||
const resF = field("Resource", res); resF.classList.add("linear-only");
|
const resF = field("Resource", res); resF.classList.add("linear-only");
|
||||||
|
|
@ -813,12 +797,9 @@
|
||||||
addTerm({resource, scalar}));
|
addTerm({resource, scalar}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eval the expression ONCE into a function, then sample it over the
|
// Eval the expression ONCE into a function, then call it over the amounts
|
||||||
// amounts the lookup table needs (0..max_resource). The solver indexes the
|
// the lookup table needs (0..max_resource).
|
||||||
// table by the resource's internal amount, so Electrum (tracked in tenths)
|
function buildLogTable(exprStr) {
|
||||||
// is sampled per tenth -- entry k is the score at k/ELECTRUM_SCALE -- while
|
|
||||||
// every other resource gets one entry per whole unit.
|
|
||||||
function buildLogTable(exprStr, resource) {
|
|
||||||
let fn;
|
let fn;
|
||||||
try {
|
try {
|
||||||
fn = eval(exprStr);
|
fn = eval(exprStr);
|
||||||
|
|
@ -828,10 +809,9 @@
|
||||||
if (typeof fn !== "function")
|
if (typeof fn !== "function")
|
||||||
throw new Error("Log expression must eval to a function, got " + typeof fn);
|
throw new Error("Log expression must eval to a function, got " + typeof fn);
|
||||||
const max = +document.getElementById("max_resource").value || 0;
|
const max = +document.getElementById("max_resource").value || 0;
|
||||||
const scale = resource === "electrum" ? ELECTRUM_SCALE : 1;
|
|
||||||
const table = [];
|
const table = [];
|
||||||
for (let i = 0; i <= max * scale; i++) {
|
for (let x = 0; x <= max; x++) {
|
||||||
const v = Number(fn(i / scale));
|
const v = Number(fn(x));
|
||||||
table.push(Number.isFinite(v) ? v : 0);
|
table.push(Number.isFinite(v) ? v : 0);
|
||||||
}
|
}
|
||||||
return table;
|
return table;
|
||||||
|
|
@ -842,8 +822,7 @@
|
||||||
const card = el("div", {class: "card"});
|
const card = el("div", {class: "card"});
|
||||||
const res = selectEl(SCORE_KEYS, c.resource || "capital");
|
const res = selectEl(SCORE_KEYS, c.resource || "capital");
|
||||||
const op = selectEl([">=", "<=", "=="], c.op || ">=");
|
const op = selectEl([">=", "<=", "=="], c.op || ">=");
|
||||||
const value = num(c.value ?? 0, {step: resStep(res.value)});
|
const value = num(c.value ?? 0);
|
||||||
res.onchange = () => {value.step = resStep(res.value);};
|
|
||||||
const turn = turnSelect(c.turn);
|
const turn = turnSelect(c.turn);
|
||||||
card._get = () => {
|
card._get = () => {
|
||||||
const o = {resource: res.value, op: op.value, value: +value.value};
|
const o = {resource: res.value, op: op.value, value: +value.value};
|
||||||
|
|
@ -865,11 +844,9 @@
|
||||||
function addConversion(c = {}) {
|
function addConversion(c = {}) {
|
||||||
const card = el("div", {class: "card"});
|
const card = el("div", {class: "card"});
|
||||||
const from = selectEl(RESOURCES, c.from || "trade_goods");
|
const from = selectEl(RESOURCES, c.from || "trade_goods");
|
||||||
const fromAmt = num(c.from_amount ?? c.amount ?? 1, {min: 0, step: resStep(from.value)});
|
const fromAmt = num(c.from_amount ?? c.amount ?? 1, {min: 0});
|
||||||
const to = selectEl(RESOURCES, c.to || "capital");
|
const to = selectEl(RESOURCES, c.to || "capital");
|
||||||
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0, step: resStep(to.value)});
|
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0});
|
||||||
from.onchange = () => {fromAmt.step = resStep(from.value);};
|
|
||||||
to.onchange = () => {toAmt.step = resStep(to.value);};
|
|
||||||
const turn = turnSelect(c.turn);
|
const turn = turnSelect(c.turn);
|
||||||
card._get = () => {
|
card._get = () => {
|
||||||
const o = {
|
const o = {
|
||||||
|
|
@ -895,12 +872,10 @@
|
||||||
function addOptionalConversion(c = {}) {
|
function addOptionalConversion(c = {}) {
|
||||||
const card = el("div", {class: "card"});
|
const card = el("div", {class: "card"});
|
||||||
const from = selectEl(RESOURCES, c.from || "trade_goods");
|
const from = selectEl(RESOURCES, c.from || "trade_goods");
|
||||||
const fromAmt = num(c.from_amount ?? c.amount ?? 1, {min: 0, step: resStep(from.value)});
|
const fromAmt = num(c.from_amount ?? c.amount ?? 1, {min: 0});
|
||||||
const to = selectEl(RESOURCES, c.to || "capital");
|
const to = selectEl(RESOURCES, c.to || "capital");
|
||||||
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0, step: resStep(to.value)});
|
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0});
|
||||||
from.onchange = () => {fromAmt.step = resStep(from.value);};
|
const maxCount = num(c.max_count ?? 1, {min: 1});
|
||||||
to.onchange = () => {toAmt.step = resStep(to.value);};
|
|
||||||
const maxCount = num(c.max_count ?? 1, {min: 1, step: "1"});
|
|
||||||
// Multiple turns may be selected; each chosen turn yields its own copy of
|
// Multiple turns may be selected; each chosen turn yields its own copy of
|
||||||
// this conversion in the problem JSON (one entry per turn).
|
// this conversion in the problem JSON (one entry per turn).
|
||||||
const turns = multiTurnSelect(c.turn != null ? [c.turn] : []);
|
const turns = multiTurnSelect(c.turn != null ? [c.turn] : []);
|
||||||
|
|
|
||||||
146
solve.py
146
solve.py
|
|
@ -96,14 +96,6 @@ RESOURCES = [
|
||||||
# values so the CP-SAT objective stays integral.
|
# values so the CP-SAT objective stays integral.
|
||||||
OBJ_SCALE = 1000
|
OBJ_SCALE = 1000
|
||||||
|
|
||||||
# Electrum is the one stockpiled resource that takes fractional amounts (the
|
|
||||||
# Provisioner's +1.5/Turn, "awful" 2.5-each Trade Good buys, etc.). CP-SAT only
|
|
||||||
# holds integers, so the Electrum pool is tracked internally in *tenths*: a
|
|
||||||
# stored value of 25 means 2.5 Electrum. Every Electrum amount entering the
|
|
||||||
# model is multiplied by this scale, and every Electrum amount read back out is
|
|
||||||
# divided by it. All other resources stay 1:1 integers.
|
|
||||||
ELECTRUM_SCALE = 10
|
|
||||||
|
|
||||||
# Upper bound used to bound resource accumulators (and AddElement domains in
|
# Upper bound used to bound resource accumulators (and AddElement domains in
|
||||||
# log mode). Override via Problem.max_resource if your game runs hotter.
|
# log mode). Override via Problem.max_resource if your game runs hotter.
|
||||||
DEFAULT_MAX_RESOURCE = 300
|
DEFAULT_MAX_RESOURCE = 300
|
||||||
|
|
@ -319,12 +311,6 @@ class ScoreTerm:
|
||||||
``resource`` is a stockpiled resource name or "renown". Because per-Turn
|
``resource`` is a stockpiled resource name or "renown". Because per-Turn
|
||||||
Renown is not tracked, a "renown" term must target the final Turn
|
Renown is not tracked, a "renown" term must target the final Turn
|
||||||
(``turn is None``).
|
(``turn is None``).
|
||||||
|
|
||||||
For a log term, ``log_mapping`` is indexed by the resource amount in its own
|
|
||||||
internal units. That is one entry per whole unit for every resource except
|
|
||||||
Electrum, which is tracked in tenths (see ELECTRUM_SCALE): an Electrum table
|
|
||||||
must be sampled per tenth, so ``log_mapping[k]`` is the score at
|
|
||||||
``k / ELECTRUM_SCALE`` Electrum (the web UI builds it this way).
|
|
||||||
"""
|
"""
|
||||||
resource: str
|
resource: str
|
||||||
scalar: float = 1.0
|
scalar: float = 1.0
|
||||||
|
|
@ -451,22 +437,6 @@ class Solution:
|
||||||
# Model construction
|
# Model construction
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
def _scaled_int(value: float, scale: int, what: str) -> int:
|
|
||||||
"""Represent ``value`` as the integer the model stores for it, where ``scale``
|
|
||||||
is the resource's internal sub-unit count (1 for whole-unit resources,
|
|
||||||
ELECTRUM_SCALE for Electrum's tenths).
|
|
||||||
|
|
||||||
Rather than silently rounding, reject any amount too precise to represent
|
|
||||||
exactly: a non-integer for a whole-unit resource, or finer than a tenth for
|
|
||||||
Electrum. ``what`` names the field for the error message."""
|
|
||||||
scaled = value * scale
|
|
||||||
nearest = round(scaled)
|
|
||||||
if abs(scaled - nearest) > 1e-6:
|
|
||||||
unit = "a whole number" if scale == 1 else f"a multiple of {1 / scale:g}"
|
|
||||||
raise ValueError(f"{what} must be {unit}; got {value}")
|
|
||||||
return int(nearest)
|
|
||||||
|
|
||||||
|
|
||||||
class _Builder:
|
class _Builder:
|
||||||
def __init__(self, problem: Problem):
|
def __init__(self, problem: Problem):
|
||||||
self.p = problem
|
self.p = problem
|
||||||
|
|
@ -1092,9 +1062,6 @@ class _Builder:
|
||||||
picked_level = self._gate_expr(vat[r][t], pick[r])
|
picked_level = self._gate_expr(vat[r][t], pick[r])
|
||||||
gain_base = picked_level + self._mul_bool(harv, pick[r], 1)
|
gain_base = picked_level + self._mul_bool(harv, pick[r], 1)
|
||||||
gain_total = gain_base + self._mul_bool(gain_base, ow, self.MAXR)
|
gain_total = gain_base + self._mul_bool(gain_base, ow, self.MAXR)
|
||||||
# Vats hold whole units; the Electrum pool is tracked in tenths.
|
|
||||||
if r == "electrum":
|
|
||||||
gain_total = ELECTRUM_SCALE * gain_total
|
|
||||||
self._add_delta(r, t, gain_total)
|
self._add_delta(r, t, gain_total)
|
||||||
|
|
||||||
# transition to next turn's vat levels
|
# transition to next turn's vat levels
|
||||||
|
|
@ -1167,9 +1134,7 @@ class _Builder:
|
||||||
for r in targets:
|
for r in targets:
|
||||||
c = m.NewIntVar(0, self.MAXR, f"tgconv_{r}_t{t}")
|
c = m.NewIntVar(0, self.MAXR, f"tgconv_{r}_t{t}")
|
||||||
self.trade_conv[(r, t)] = c
|
self.trade_conv[(r, t)] = c
|
||||||
# 1 Trade Good -> 1 target unit; Electrum is stored in tenths.
|
self._add_delta(r, t, c) # +1 target resource
|
||||||
gain = ELECTRUM_SCALE * c if r == "electrum" else c
|
|
||||||
self._add_delta(r, t, gain) # +1 target resource
|
|
||||||
self._add_delta("trade_goods", t, -c) # -1 Trade Good
|
self._add_delta("trade_goods", t, -c) # -1 Trade Good
|
||||||
|
|
||||||
def _build_conversions(self):
|
def _build_conversions(self):
|
||||||
|
|
@ -1193,22 +1158,15 @@ class _Builder:
|
||||||
if dst not in RESOURCES:
|
if dst not in RESOURCES:
|
||||||
raise ValueError(f"conversion 'to' unknown resource: {dst!r}")
|
raise ValueError(f"conversion 'to' unknown resource: {dst!r}")
|
||||||
amount = c.get("amount")
|
amount = c.get("amount")
|
||||||
raw_from = c.get("from_amount", amount if amount is not None else 0)
|
from_amt = int(round(c.get("from_amount", amount if amount is not None else 0)))
|
||||||
raw_to = c.get("to_amount", amount if amount is not None else raw_from)
|
to_amt = int(round(c.get("to_amount", amount if amount is not None else from_amt)))
|
||||||
# Electrum is tracked in tenths, so its amounts keep one decimal of
|
|
||||||
# precision; every other resource stays an integer.
|
|
||||||
from_scale = ELECTRUM_SCALE if src == "electrum" else 1
|
|
||||||
to_scale = ELECTRUM_SCALE if dst == "electrum" else 1
|
|
||||||
from_amt = _scaled_int(raw_from, from_scale, f"conversion from_amount ({src})")
|
|
||||||
to_amt = _scaled_int(raw_to, to_scale, f"conversion to_amount ({dst})")
|
|
||||||
if from_amt < 0 or to_amt < 0:
|
if from_amt < 0 or to_amt < 0:
|
||||||
raise ValueError("conversion amounts must be non-negative")
|
raise ValueError("conversion amounts must be non-negative")
|
||||||
self._add_delta(src, t, -from_amt)
|
self._add_delta(src, t, -from_amt)
|
||||||
self._add_delta(dst, t, to_amt)
|
self._add_delta(dst, t, to_amt)
|
||||||
self.conversions.append({
|
self.conversions.append({
|
||||||
"turn": t, "from": src, "to": dst,
|
"turn": t, "from": src, "to": dst,
|
||||||
"from_amount": from_amt / from_scale if from_scale != 1 else from_amt,
|
"from_amount": from_amt, "to_amount": to_amt,
|
||||||
"to_amount": to_amt / to_scale if to_scale != 1 else to_amt,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def _build_optional_conversions(self):
|
def _build_optional_conversions(self):
|
||||||
|
|
@ -1236,16 +1194,8 @@ class _Builder:
|
||||||
if dst not in RESOURCES:
|
if dst not in RESOURCES:
|
||||||
raise ValueError(f"optional conversion 'to' unknown resource: {dst!r}")
|
raise ValueError(f"optional conversion 'to' unknown resource: {dst!r}")
|
||||||
amount = c.get("amount")
|
amount = c.get("amount")
|
||||||
raw_from = c.get("from_amount", amount if amount is not None else 0)
|
from_amt = int(round(c.get("from_amount", amount if amount is not None else 0)))
|
||||||
raw_to = c.get("to_amount", amount if amount is not None else raw_from)
|
to_amt = int(round(c.get("to_amount", amount if amount is not None else from_amt)))
|
||||||
# Electrum is tracked in tenths, so its amounts keep one decimal of
|
|
||||||
# precision; every other resource stays an integer.
|
|
||||||
from_scale = ELECTRUM_SCALE if src == "electrum" else 1
|
|
||||||
to_scale = ELECTRUM_SCALE if dst == "electrum" else 1
|
|
||||||
from_amt = _scaled_int(
|
|
||||||
raw_from, from_scale, f"optional conversion from_amount ({src})")
|
|
||||||
to_amt = _scaled_int(
|
|
||||||
raw_to, to_scale, f"optional conversion to_amount ({dst})")
|
|
||||||
if from_amt < 0 or to_amt < 0:
|
if from_amt < 0 or to_amt < 0:
|
||||||
raise ValueError("optional conversion amounts must be non-negative")
|
raise ValueError("optional conversion amounts must be non-negative")
|
||||||
max_count = int(c.get("max_count", 1))
|
max_count = int(c.get("max_count", 1))
|
||||||
|
|
@ -1256,9 +1206,7 @@ class _Builder:
|
||||||
self._add_delta(dst, t, to_amt * n)
|
self._add_delta(dst, t, to_amt * n)
|
||||||
meta = {
|
meta = {
|
||||||
"turn": t, "from": src, "to": dst,
|
"turn": t, "from": src, "to": dst,
|
||||||
"from_amount": from_amt / from_scale if from_scale != 1 else from_amt,
|
"from_amount": from_amt, "to_amount": to_amt, "max_count": max_count,
|
||||||
"to_amount": to_amt / to_scale if to_scale != 1 else to_amt,
|
|
||||||
"max_count": max_count,
|
|
||||||
}
|
}
|
||||||
self.optional_conversions.append(meta)
|
self.optional_conversions.append(meta)
|
||||||
self._opt_conv_vars.append((meta, n))
|
self._opt_conv_vars.append((meta, n))
|
||||||
|
|
@ -1294,18 +1242,16 @@ class _Builder:
|
||||||
m.Add(first >= gov_any[t] - ever_before)
|
m.Add(first >= gov_any[t] - ever_before)
|
||||||
for r, amt in a.onetime_governor_bonus.items():
|
for r, amt in a.onetime_governor_bonus.items():
|
||||||
if r in RESOURCES and amt:
|
if r in RESOURCES and amt:
|
||||||
scaled_amt = amt * ELECTRUM_SCALE if r == "electrum" else amt
|
self._add_delta(r, t, amt * first)
|
||||||
self._add_delta(r, t, scaled_amt * first)
|
|
||||||
|
|
||||||
def _build_resource_balance(self):
|
def _build_resource_balance(self):
|
||||||
m = self.m
|
m = self.m
|
||||||
T = self.T
|
T = self.T
|
||||||
for r in RESOURCES:
|
for r in RESOURCES:
|
||||||
scale = ELECTRUM_SCALE if r == "electrum" else 1
|
start = int(round(self.p.start.get(r, 0)))
|
||||||
start = _scaled_int(self.p.start.get(r, 0), scale, f"starting {r}")
|
|
||||||
self.res[r] = []
|
self.res[r] = []
|
||||||
for t in range(T):
|
for t in range(T):
|
||||||
v = m.NewIntVar(0, self.MAXR * scale, f"res_{r}_t{t}")
|
v = m.NewIntVar(0, self.MAXR, f"res_{r}_t{t}")
|
||||||
self.res[r].append(v)
|
self.res[r].append(v)
|
||||||
prev = self.res[r][t - 1] if t > 0 else start
|
prev = self.res[r][t - 1] if t > 0 else start
|
||||||
deltas = self.delta.get((r, t), [])
|
deltas = self.delta.get((r, t), [])
|
||||||
|
|
@ -1316,39 +1262,40 @@ class _Builder:
|
||||||
"""Provisioner: +1.5 Electrum each Turn it governs a City.
|
"""Provisioner: +1.5 Electrum each Turn it governs a City.
|
||||||
|
|
||||||
Electrum is never spent in this model (only accumulated for scoring),
|
Electrum is never spent in this model (only accumulated for scoring),
|
||||||
so the bonus is layered on top of the Electrum pool as an
|
so the bonus is layered on top of the integer Electrum pool as an
|
||||||
``electrum_eff[t]`` amount used by scoring and constraints. Both the
|
``electrum_eff[t]`` amount used by scoring and constraints. The gain is
|
||||||
pool and this overlay are in tenths (see ELECTRUM_SCALE), so the 1.5
|
tracked in half-units (1.5 = 3 half-units); a partial trailing 0.5 from
|
||||||
gain is an exact 15-tenths integer with no rounding. ``governor_
|
an odd number of governing Turns is floored."""
|
||||||
electrum_half`` is in half-units (1 half = 0.5 = 5 tenths)."""
|
|
||||||
m = self.m
|
m = self.m
|
||||||
T = self.T
|
T = self.T
|
||||||
ncities = len(self.p.cities)
|
ncities = len(self.p.cities)
|
||||||
# default: no Provisioner effect, effective Electrum == the pool (tenths).
|
# default: no Provisioner effect, effective Electrum == the pool.
|
||||||
self.electrum_eff = list(self.res["electrum"])
|
self.electrum_eff = list(self.res["electrum"])
|
||||||
tenth_terms = {t: [] for t in range(T)}
|
half_terms = {t: [] for t in range(T)}
|
||||||
any_tenths = False
|
any_half = False
|
||||||
for ai, a in enumerate(self.p.agents):
|
for ai, a in enumerate(self.p.agents):
|
||||||
if not a.governor_electrum_half:
|
if not a.governor_electrum_half:
|
||||||
continue
|
continue
|
||||||
for ci in range(ncities):
|
for ci in range(ncities):
|
||||||
for t in range(T):
|
for t in range(T):
|
||||||
if (ai, ci, t) in self.gov:
|
if (ai, ci, t) in self.gov:
|
||||||
tenth_terms[t].append(
|
half_terms[t].append(
|
||||||
a.governor_electrum_half * 5 * self.gov[(ai, ci, t)])
|
a.governor_electrum_half * self.gov[(ai, ci, t)])
|
||||||
any_tenths = True
|
any_half = True
|
||||||
if not any_tenths:
|
if not any_half:
|
||||||
return
|
return
|
||||||
per_turn_max = sum(a.governor_electrum_half for a in self.p.agents) * 5
|
per_turn_max = sum(a.governor_electrum_half for a in self.p.agents)
|
||||||
bound = per_turn_max * T
|
bound = per_turn_max * T + 2
|
||||||
eff = []
|
eff = []
|
||||||
cum_prev = 0
|
cum_prev = 0
|
||||||
for t in range(T):
|
for t in range(T):
|
||||||
cum = m.NewIntVar(0, bound, f"electenth_cum_t{t}")
|
cum = m.NewIntVar(0, bound, f"elechalf_cum_t{t}")
|
||||||
m.Add(cum == cum_prev + sum(tenth_terms[t]))
|
m.Add(cum == cum_prev + sum(half_terms[t]))
|
||||||
cum_prev = cum
|
cum_prev = cum
|
||||||
e = m.NewIntVar(0, self.MAXR * ELECTRUM_SCALE + bound, f"electrum_eff_t{t}")
|
half = m.NewIntVar(0, bound, f"elechalf_t{t}")
|
||||||
m.Add(e == self.res["electrum"][t] + cum)
|
m.AddDivisionEquality(half, cum, 2)
|
||||||
|
e = m.NewIntVar(0, self.MAXR + bound, f"electrum_eff_t{t}")
|
||||||
|
m.Add(e == self.res["electrum"][t] + half)
|
||||||
eff.append(e)
|
eff.append(e)
|
||||||
self.electrum_eff = eff
|
self.electrum_eff = eff
|
||||||
|
|
||||||
|
|
@ -1394,10 +1341,7 @@ class _Builder:
|
||||||
if op not in self._OPS:
|
if op not in self._OPS:
|
||||||
raise ValueError(f"resource_constraints op must be one of {self._OPS}")
|
raise ValueError(f"resource_constraints op must be one of {self._OPS}")
|
||||||
var = self._resource_at(c["resource"], c.get("turn"))
|
var = self._resource_at(c["resource"], c.get("turn"))
|
||||||
# Electrum is compared in tenths; its bound may carry one decimal.
|
value = int(c["value"])
|
||||||
scale = ELECTRUM_SCALE if c["resource"] == "electrum" else 1
|
|
||||||
value = _scaled_int(
|
|
||||||
float(c["value"]), scale, f"constraint value ({c['resource']})")
|
|
||||||
if op == ">=":
|
if op == ">=":
|
||||||
m.Add(var >= value)
|
m.Add(var >= value)
|
||||||
elif op == "<=":
|
elif op == "<=":
|
||||||
|
|
@ -1425,24 +1369,11 @@ class _Builder:
|
||||||
if not term.scalar:
|
if not term.scalar:
|
||||||
continue
|
continue
|
||||||
amt_var = self._resource_at(term.resource, term.turn)
|
amt_var = self._resource_at(term.resource, term.turn)
|
||||||
# Electrum's amount var is in tenths; the table/scalar are stated per
|
|
||||||
# whole Electrum, so account for the scale on the way into the score.
|
|
||||||
is_elec = term.resource == "electrum"
|
|
||||||
if term.log_mapping is None:
|
if term.log_mapping is None:
|
||||||
if is_elec:
|
|
||||||
# scalar * (amt/ELECTRUM_SCALE), kept integral via OBJ_SCALE.
|
|
||||||
coeff = int(round(term.scalar * OBJ_SCALE / ELECTRUM_SCALE))
|
|
||||||
terms.append(coeff * amt_var)
|
|
||||||
else:
|
|
||||||
terms.append(scaled(term.scalar) * amt_var)
|
terms.append(scaled(term.scalar) * amt_var)
|
||||||
else:
|
else:
|
||||||
table = term.log_mapping
|
table = term.log_mapping
|
||||||
# value = table[min(amt, len-1)]; scale table values to ints.
|
# value = table[min(amt, len-1)]; scale table values to ints.
|
||||||
# The table is indexed in the resource's own internal units, so
|
|
||||||
# ``amt_var`` indexes it directly: whole units for most resources,
|
|
||||||
# but tenths for Electrum (see ELECTRUM_SCALE) -- callers must
|
|
||||||
# therefore supply an Electrum table sampled per tenth (entry k is
|
|
||||||
# the score at k/ELECTRUM_SCALE Electrum), which the UI does.
|
|
||||||
vals = [scaled(v) for v in table]
|
vals = [scaled(v) for v in table]
|
||||||
tag = f"{term.resource}_t{term.turn if term.turn is not None else T - 1}"
|
tag = f"{term.resource}_t{term.turn if term.turn is not None else T - 1}"
|
||||||
idx = m.NewIntVar(0, len(table) - 1, f"idx_{tag}")
|
idx = m.NewIntVar(0, len(table) - 1, f"idx_{tag}")
|
||||||
|
|
@ -1580,9 +1511,7 @@ def _extract(problem: Problem, b: _Builder, solver, status_name: str) -> Solutio
|
||||||
for expr in b.city_delta.get((ci, r, t), []):
|
for expr in b.city_delta.get((ci, r, t), []):
|
||||||
total += expr if isinstance(expr, int) else solver.Value(expr)
|
total += expr if isinstance(expr, int) else solver.Value(expr)
|
||||||
if total:
|
if total:
|
||||||
# Electrum deltas are accumulated in tenths.
|
deltas[r] = float(total)
|
||||||
deltas[r] = (float(total) / ELECTRUM_SCALE
|
|
||||||
if r == "electrum" else float(total))
|
|
||||||
for r, amt in extra.get((ci, t), {}).items():
|
for r, amt in extra.get((ci, t), {}).items():
|
||||||
deltas[r] = deltas.get(r, 0.0) + amt
|
deltas[r] = deltas.get(r, 0.0) + amt
|
||||||
if deltas[r] == 0:
|
if deltas[r] == 0:
|
||||||
|
|
@ -1593,12 +1522,8 @@ def _extract(problem: Problem, b: _Builder, solver, status_name: str) -> Solutio
|
||||||
deltas=deltas,
|
deltas=deltas,
|
||||||
))
|
))
|
||||||
|
|
||||||
def _unscale(r, raw):
|
|
||||||
# The Electrum pool is stored in tenths; everything else is 1:1.
|
|
||||||
return float(raw) / ELECTRUM_SCALE if r == "electrum" else float(raw)
|
|
||||||
|
|
||||||
final_resources = {
|
final_resources = {
|
||||||
r: _unscale(r, solver.Value(b.final_amt[r])) for r in RESOURCES
|
r: float(solver.Value(b.final_amt[r])) for r in RESOURCES
|
||||||
}
|
}
|
||||||
conversions = []
|
conversions = []
|
||||||
for (r, t), var in sorted(b.trade_conv.items(), key=lambda kv: (kv[0][1], kv[0][0])):
|
for (r, t), var in sorted(b.trade_conv.items(), key=lambda kv: (kv[0][1], kv[0][0])):
|
||||||
|
|
@ -1618,17 +1543,14 @@ def _extract(problem: Problem, b: _Builder, solver, status_name: str) -> Solutio
|
||||||
for t in range(T):
|
for t in range(T):
|
||||||
row = {"turn": t}
|
row = {"turn": t}
|
||||||
for r in RESOURCES:
|
for r in RESOURCES:
|
||||||
row[r] = _unscale(r, solver.Value(b._resource_at(r, t)))
|
row[r] = float(solver.Value(b._resource_at(r, t)))
|
||||||
resources_by_turn.append(row)
|
resources_by_turn.append(row)
|
||||||
return Solution(
|
return Solution(
|
||||||
status=status_name,
|
status=status_name,
|
||||||
objective_value=solver.ObjectiveValue() / OBJ_SCALE,
|
objective_value=solver.ObjectiveValue() / OBJ_SCALE,
|
||||||
final_resources=final_resources,
|
final_resources=final_resources,
|
||||||
final_renown_total=int(solver.Value(b.renown_total)),
|
final_renown_total=int(solver.Value(b.renown_total)),
|
||||||
start_resources={
|
start_resources={r: float(int(round(problem.start.get(r, 0)))) for r in RESOURCES},
|
||||||
r: (round(problem.start.get(r, 0) * ELECTRUM_SCALE) / ELECTRUM_SCALE
|
|
||||||
if r == "electrum" else float(int(round(problem.start.get(r, 0)))))
|
|
||||||
for r in RESOURCES},
|
|
||||||
plan=sorted(plan, key=lambda p: (p.turn, p.city)),
|
plan=sorted(plan, key=lambda p: (p.turn, p.city)),
|
||||||
trade_conversions=conversions,
|
trade_conversions=conversions,
|
||||||
forced_conversions=list(b.conversions),
|
forced_conversions=list(b.conversions),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue