illegal amounts disallowed
This commit is contained in:
parent
dff7f995ef
commit
0c94d94936
2 changed files with 50 additions and 14 deletions
31
index.html
31
index.html
|
|
@ -53,6 +53,13 @@
|
||||||
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;
|
||||||
|
|
@ -462,6 +469,11 @@
|
||||||
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) {
|
||||||
|
|
@ -595,7 +607,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});
|
const inp = num(r === "express" || r === "trade_goods" ? 0 : 3, {min: 0, step: resStep(r)});
|
||||||
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]));
|
||||||
|
|
@ -830,7 +842,8 @@
|
||||||
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);
|
const value = num(c.value ?? 0, {step: resStep(res.value)});
|
||||||
|
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};
|
||||||
|
|
@ -852,9 +865,11 @@
|
||||||
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});
|
const fromAmt = num(c.from_amount ?? c.amount ?? 1, {min: 0, step: resStep(from.value)});
|
||||||
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});
|
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0, step: resStep(to.value)});
|
||||||
|
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 = {
|
||||||
|
|
@ -880,10 +895,12 @@
|
||||||
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});
|
const fromAmt = num(c.from_amount ?? c.amount ?? 1, {min: 0, step: resStep(from.value)});
|
||||||
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});
|
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0, step: resStep(to.value)});
|
||||||
const maxCount = num(c.max_count ?? 1, {min: 1});
|
from.onchange = () => {fromAmt.step = resStep(from.value);};
|
||||||
|
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] : []);
|
||||||
|
|
|
||||||
33
solve.py
33
solve.py
|
|
@ -451,6 +451,22 @@ 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
|
||||||
|
|
@ -1183,8 +1199,8 @@ class _Builder:
|
||||||
# precision; every other resource stays an integer.
|
# precision; every other resource stays an integer.
|
||||||
from_scale = ELECTRUM_SCALE if src == "electrum" else 1
|
from_scale = ELECTRUM_SCALE if src == "electrum" else 1
|
||||||
to_scale = ELECTRUM_SCALE if dst == "electrum" else 1
|
to_scale = ELECTRUM_SCALE if dst == "electrum" else 1
|
||||||
from_amt = int(round(raw_from * from_scale))
|
from_amt = _scaled_int(raw_from, from_scale, f"conversion from_amount ({src})")
|
||||||
to_amt = int(round(raw_to * to_scale))
|
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)
|
||||||
|
|
@ -1226,8 +1242,10 @@ class _Builder:
|
||||||
# precision; every other resource stays an integer.
|
# precision; every other resource stays an integer.
|
||||||
from_scale = ELECTRUM_SCALE if src == "electrum" else 1
|
from_scale = ELECTRUM_SCALE if src == "electrum" else 1
|
||||||
to_scale = ELECTRUM_SCALE if dst == "electrum" else 1
|
to_scale = ELECTRUM_SCALE if dst == "electrum" else 1
|
||||||
from_amt = int(round(raw_from * from_scale))
|
from_amt = _scaled_int(
|
||||||
to_amt = int(round(raw_to * to_scale))
|
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))
|
||||||
|
|
@ -1284,7 +1302,7 @@ class _Builder:
|
||||||
T = self.T
|
T = self.T
|
||||||
for r in RESOURCES:
|
for r in RESOURCES:
|
||||||
scale = ELECTRUM_SCALE if r == "electrum" else 1
|
scale = ELECTRUM_SCALE if r == "electrum" else 1
|
||||||
start = int(round(self.p.start.get(r, 0) * scale))
|
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 * scale, f"res_{r}_t{t}")
|
||||||
|
|
@ -1376,9 +1394,10 @@ 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 a decimal.
|
# Electrum is compared in tenths; its bound may carry one decimal.
|
||||||
scale = ELECTRUM_SCALE if c["resource"] == "electrum" else 1
|
scale = ELECTRUM_SCALE if c["resource"] == "electrum" else 1
|
||||||
value = int(round(float(c["value"]) * scale))
|
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 == "<=":
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue