Compare commits
7 commits
652126acf9
...
47e21678df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47e21678df | ||
|
|
57c2157134 | ||
|
|
98eeb18e3c | ||
|
|
43636d330e | ||
|
|
ead700adf4 | ||
|
|
8a775dd941 | ||
|
|
feb5c1cb76 |
2 changed files with 97 additions and 53 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.serena
|
||||
__pycache__
|
||||
output.txt
|
||||
.worktrees
|
||||
|
|
|
|||
147
solve.py
147
solve.py
|
|
@ -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}"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue