Compare commits

...

7 commits

Author SHA1 Message Date
Pagwin
47e21678df
yeet fixed choices from solve 2026-06-11 20:11:56 -04:00
Pagwin
57c2157134
feat: report prints the actually maximized objective
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 19:30:52 -04:00
Pagwin
98eeb18e3c
feat: build objective from factors dict in product or sum mode 2026-06-11 19:28:20 -04:00
Pagwin
43636d330e
feat: compute resource ceilings only for objective resources
Modify solve() to only compute ceilings (_ceiling solver calls) for
resources that appear in the objective function (where obj_factors[k] != 0).
This avoids unnecessary 20s ceiling solves for resources not contributing
to the maximize criterion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 19:26:18 -04:00
Pagwin
ead700adf4
feat: solve() accepts objective_factors and objective_mode
Add objective_factors and objective_mode as optional parameters to solve().
Resolve defaults at function entry and validate using existing _validate_objective.
Normalize objective_factors to ensure all required keys (EBSC) are present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 19:22:59 -04:00
Pagwin
8a775dd941
feat: add objective factors/mode defaults and validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 19:20:50 -04:00
Pagwin
feb5c1cb76
chore: ignore .worktrees directory 2026-06-11 19:17:46 -04:00
2 changed files with 97 additions and 53 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.serena
__pycache__
output.txt
.worktrees

147
solve.py
View file

@ -36,30 +36,7 @@ INITIAL = (30, 30, 30, 30)
#
# None for nothing
# FIXED_CHOICES = None
FIXED_CHOICES = {
"actions": {
(0, 1): "renovate_h",
(1, 1): "renovate_h",
(2, 1): "renovate_h",
(3, 1): "overwork",
(0, 2): "collect",
(1, 2): "collect",
(2, 2): "overwork",
(3, 2): "upgrade_a",
(0, 3): "collect",
(1, 3): "collect",
(2, 3): "upgrade_a",
(3, 3): "overwork",
(0, 4): "collect",
(1, 4): "collect",
(2, 4): "overwork",
(3, 4): "upgrade_b",
(0, 5): "collect",
(1, 5): "collect",
(2, 5): "upgrade_b",
(3, 5): "overwork",
}
}
FIXED_CHOICES = {"actions": {}}
# Resource constraints: list of callables that receive a dict with keys E, B, S, C
# mapping to resource variables indexed by step. Each callable returns a constraint
@ -67,6 +44,33 @@ FIXED_CHOICES = {
# lambda res: res["E"][3] >= 50 # Ensure at least 50 E at step 3
RESOURCE_CONSTRAINTS = []
# Maximize criteria for solve(). Factor = exponent in "product" mode,
# weight in "sum" mode. Keys: E, B, S, C; missing keys = 0 (resource
# excluded from the objective — it is NOT forced to zero). Negative
# factors are allowed only in "sum" mode (a negative exponent would mean
# division, which integer CP-SAT cannot express).
OBJECTIVE_FACTORS = {"E": 2, "B": 1, "S": 2}
OBJECTIVE_MODE = "product" # "product" or "sum"
def _validate_objective(factors, mode):
if mode not in ("product", "sum"):
raise ValueError(f"objective_mode must be 'product' or 'sum', got {mode!r}")
unknown = set(factors) - {"E", "B", "S", "C"}
if unknown:
raise ValueError(f"unknown objective_factors keys: {sorted(unknown)}")
for k, v in factors.items():
if not isinstance(v, int) or isinstance(v, bool):
raise ValueError(f"objective factor {k}={v!r} must be an int")
if mode == "product" and v < 0:
raise ValueError(
f"objective factor {k}={v} is negative; negative exponents "
"are not expressible in product mode"
)
if not any(factors.values()):
raise ValueError("objective_factors needs at least one nonzero factor")
# Arrival schedule. Key = step (1..5), value = list of city types that arrive
# at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument.
# Arriving cities act that same step.
@ -133,7 +137,7 @@ AGENT_AVAILABILITY = {
"metallurgist": [],
"builder": [],
"courier": [],
"planner": [],
"planner": [1, 2, 3, 4, 5],
"fence": [],
"foreman": [],
"industrialist": [],
@ -989,8 +993,18 @@ def solve(
verbose=True,
fixed_choices=FIXED_CHOICES,
resource_constraints=None,
objective_factors=None,
objective_mode=None,
):
if objective_factors is None:
objective_factors = OBJECTIVE_FACTORS
if objective_mode is None:
objective_mode = OBJECTIVE_MODE
_validate_objective(objective_factors, objective_mode)
# Normalized copy: every key present, missing keys = 0.
obj_factors = {k: objective_factors.get(k, 0) for k in "EBSC"}
# ---- build the city list -----------------------------------------
# cities: list[tuple[int, dict]] where the dict has keys "type" and
# "adjacent_to". TODO: adjacency unused; for Industrialist agent.
@ -1546,35 +1560,36 @@ def solve(
else max_res
)
capE = _ceiling(finalE)
capB = _ceiling(finalB)
capS = _ceiling(finalS)
m.Add(finalE <= capE)
m.Add(finalB <= capB)
m.Add(finalS <= capS)
finals = {"E": finalE, "B": finalB, "S": finalS, "C": C[NUM_STEPS + 1]}
# Ceilings only for resources that appear in the objective: each
# ceiling solve costs up to 20s, and only objective resources need
# bounds (product mode) / benefit from the redundant cap constraint.
caps = {}
for k, var in finals.items():
if obj_factors[k] != 0:
caps[k] = _ceiling(var)
m.Add(var <= caps[k])
# ======================================================================
# OBJECTIVE IS SET HERE
# ======================================================================
def Eprod(v):
return v * v
def Bprod(v):
return v
def Sprod(v):
return v * v
prodEE = m.NewIntVar(0, Eprod(capE), "prodEE")
m.AddMultiplicationEquality(prodEE, [finalE, finalE])
prodSS = m.NewIntVar(0, Sprod(capS), "prodSS")
m.AddMultiplicationEquality(prodSS, [finalS, finalS])
prodBB = m.NewIntVar(0, Bprod(capB), "prodBB")
m.AddMultiplicationEquality(prodBB, [finalB])
prodEB = m.NewIntVar(0, Eprod(capE) * Bprod(capB), "prodEB")
m.AddMultiplicationEquality(prodEB, [prodEE, prodBB])
obj = m.NewIntVar(0, Eprod(capE) * Bprod(capB) * Sprod(capS), "obj")
m.AddMultiplicationEquality(obj, [prodEB, prodSS])
if objective_mode == "sum":
# Linear: CP-SAT takes weighted sums (negative weights included)
# directly, no auxiliary variables needed.
m.Maximize(sum(f * finals[k] for k, f in obj_factors.items() if f))
else:
# Product: maximize prod(finals[k] ** obj_factors[k]). Expand the
# exponents into a flat factor list and fold pairwise, carrying a
# running upper bound from the Phase-1 caps.
factor_keys = [k for k, f in obj_factors.items() for _ in range(f)]
obj = finals[factor_keys[0]]
bound = caps[factor_keys[0]]
for k in factor_keys[1:]:
bound *= caps[k]
nxt = m.NewIntVar(0, bound, "")
m.AddMultiplicationEquality(nxt, [obj, finals[k]])
obj = nxt
m.Maximize(obj)
# ---- Phase 2: solve the product to optimality ----
@ -1593,7 +1608,8 @@ def solve(
status = solver.Solve(m)
if verbose:
print(f"(resource ceilings used: E<={capE} B<={capB} S<={capS})")
caps_str = " ".join(f"{k}<={v}" for k, v in caps.items())
print(f"(resource ceilings used: {caps_str})")
_report(
solver,
status,
@ -1644,6 +1660,8 @@ def solve(
capital_spent,
fence_deposits,
baron_deposits,
obj_factors=obj_factors,
obj_mode=objective_mode,
)
return solver, status
@ -1698,6 +1716,8 @@ def _report(
capital_spent=None,
fence_deposits=None,
baron_deposits=None,
obj_factors=None,
obj_mode=None,
):
print("status:", solver.StatusName(status))
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
@ -1852,9 +1872,32 @@ def _report(
)
fe, fb, fs = solver.Value(finalE), solver.Value(finalB), solver.Value(finalS)
vals = {"E": fe, "B": fb, "S": fs, "C": solver.Value(C[NUM_STEPS + 1])}
if obj_factors is None:
# Legacy fallback: old hardcoded E*B*S display.
obj_str = f"product(scaled) = {fe * fb * fs / 1000}"
elif obj_mode == "sum":
terms = [
f"{f}{k}" if abs(f) != 1 else (k if f > 0 else f"-{k}")
for k, f in obj_factors.items()
if f
]
expr = " + ".join(terms).replace("+ -", "- ")
raw = sum(f * vals[k] for k, f in obj_factors.items())
obj_str = f"objective {expr} = {raw / 10:.1f}"
else:
expr = "*".join(
k if f == 1 else f"{k}^{f}" for k, f in obj_factors.items() if f
)
raw, n = 1, 0
for k, f in obj_factors.items():
raw *= vals[k] ** f
n += f
# Resource values are x10-scaled, so descale by 10^(sum of exponents).
obj_str = f"objective {expr} = {raw / 10**n}"
print(
f"\nFINAL E={fe / 10:.1f} B={fb / 10:.1f} S={fs / 10:.1f} "
f"product(scaled) = {fe * fb * fs / 1000} sum = {(fe + fb + fs) / 10:.1f}"
f"{obj_str} sum = {(fe + fb + fs) / 10:.1f}"
)