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
|
.serena
|
||||||
__pycache__
|
__pycache__
|
||||||
output.txt
|
output.txt
|
||||||
|
.worktrees
|
||||||
|
|
|
||||||
149
solve.py
149
solve.py
|
|
@ -36,30 +36,7 @@ INITIAL = (30, 30, 30, 30)
|
||||||
#
|
#
|
||||||
# None for nothing
|
# None for nothing
|
||||||
# FIXED_CHOICES = None
|
# FIXED_CHOICES = None
|
||||||
FIXED_CHOICES = {
|
FIXED_CHOICES = {"actions": {}}
|
||||||
"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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Resource constraints: list of callables that receive a dict with keys E, B, S, C
|
# 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
|
# 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
|
# lambda res: res["E"][3] >= 50 # Ensure at least 50 E at step 3
|
||||||
RESOURCE_CONSTRAINTS = []
|
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
|
# 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.
|
# at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument.
|
||||||
# Arriving cities act that same step.
|
# Arriving cities act that same step.
|
||||||
|
|
@ -133,7 +137,7 @@ AGENT_AVAILABILITY = {
|
||||||
"metallurgist": [],
|
"metallurgist": [],
|
||||||
"builder": [],
|
"builder": [],
|
||||||
"courier": [],
|
"courier": [],
|
||||||
"planner": [],
|
"planner": [1, 2, 3, 4, 5],
|
||||||
"fence": [],
|
"fence": [],
|
||||||
"foreman": [],
|
"foreman": [],
|
||||||
"industrialist": [],
|
"industrialist": [],
|
||||||
|
|
@ -989,8 +993,18 @@ def solve(
|
||||||
verbose=True,
|
verbose=True,
|
||||||
fixed_choices=FIXED_CHOICES,
|
fixed_choices=FIXED_CHOICES,
|
||||||
resource_constraints=None,
|
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 -----------------------------------------
|
# ---- build the city list -----------------------------------------
|
||||||
# cities: list[tuple[int, dict]] where the dict has keys "type" and
|
# cities: list[tuple[int, dict]] where the dict has keys "type" and
|
||||||
# "adjacent_to". TODO: adjacency unused; for Industrialist agent.
|
# "adjacent_to". TODO: adjacency unused; for Industrialist agent.
|
||||||
|
|
@ -1546,36 +1560,37 @@ def solve(
|
||||||
else max_res
|
else max_res
|
||||||
)
|
)
|
||||||
|
|
||||||
capE = _ceiling(finalE)
|
finals = {"E": finalE, "B": finalB, "S": finalS, "C": C[NUM_STEPS + 1]}
|
||||||
capB = _ceiling(finalB)
|
|
||||||
capS = _ceiling(finalS)
|
# Ceilings only for resources that appear in the objective: each
|
||||||
m.Add(finalE <= capE)
|
# ceiling solve costs up to 20s, and only objective resources need
|
||||||
m.Add(finalB <= capB)
|
# bounds (product mode) / benefit from the redundant cap constraint.
|
||||||
m.Add(finalS <= capS)
|
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
|
# OBJECTIVE IS SET HERE
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
def Eprod(v):
|
if objective_mode == "sum":
|
||||||
return v * v
|
# Linear: CP-SAT takes weighted sums (negative weights included)
|
||||||
|
# directly, no auxiliary variables needed.
|
||||||
def Bprod(v):
|
m.Maximize(sum(f * finals[k] for k, f in obj_factors.items() if f))
|
||||||
return v
|
else:
|
||||||
|
# Product: maximize prod(finals[k] ** obj_factors[k]). Expand the
|
||||||
def Sprod(v):
|
# exponents into a flat factor list and fold pairwise, carrying a
|
||||||
return v * v
|
# running upper bound from the Phase-1 caps.
|
||||||
|
factor_keys = [k for k, f in obj_factors.items() for _ in range(f)]
|
||||||
prodEE = m.NewIntVar(0, Eprod(capE), "prodEE")
|
obj = finals[factor_keys[0]]
|
||||||
m.AddMultiplicationEquality(prodEE, [finalE, finalE])
|
bound = caps[factor_keys[0]]
|
||||||
prodSS = m.NewIntVar(0, Sprod(capS), "prodSS")
|
for k in factor_keys[1:]:
|
||||||
m.AddMultiplicationEquality(prodSS, [finalS, finalS])
|
bound *= caps[k]
|
||||||
prodBB = m.NewIntVar(0, Bprod(capB), "prodBB")
|
nxt = m.NewIntVar(0, bound, "")
|
||||||
m.AddMultiplicationEquality(prodBB, [finalB])
|
m.AddMultiplicationEquality(nxt, [obj, finals[k]])
|
||||||
prodEB = m.NewIntVar(0, Eprod(capE) * Bprod(capB), "prodEB")
|
obj = nxt
|
||||||
m.AddMultiplicationEquality(prodEB, [prodEE, prodBB])
|
m.Maximize(obj)
|
||||||
obj = m.NewIntVar(0, Eprod(capE) * Bprod(capB) * Sprod(capS), "obj")
|
|
||||||
m.AddMultiplicationEquality(obj, [prodEB, prodSS])
|
|
||||||
m.Maximize(obj)
|
|
||||||
|
|
||||||
# ---- Phase 2: solve the product to optimality ----
|
# ---- Phase 2: solve the product to optimality ----
|
||||||
solver = cp_model.CpSolver()
|
solver = cp_model.CpSolver()
|
||||||
|
|
@ -1593,7 +1608,8 @@ def solve(
|
||||||
status = solver.Solve(m)
|
status = solver.Solve(m)
|
||||||
|
|
||||||
if verbose:
|
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(
|
_report(
|
||||||
solver,
|
solver,
|
||||||
status,
|
status,
|
||||||
|
|
@ -1644,6 +1660,8 @@ def solve(
|
||||||
capital_spent,
|
capital_spent,
|
||||||
fence_deposits,
|
fence_deposits,
|
||||||
baron_deposits,
|
baron_deposits,
|
||||||
|
obj_factors=obj_factors,
|
||||||
|
obj_mode=objective_mode,
|
||||||
)
|
)
|
||||||
return solver, status
|
return solver, status
|
||||||
|
|
||||||
|
|
@ -1698,6 +1716,8 @@ def _report(
|
||||||
capital_spent=None,
|
capital_spent=None,
|
||||||
fence_deposits=None,
|
fence_deposits=None,
|
||||||
baron_deposits=None,
|
baron_deposits=None,
|
||||||
|
obj_factors=None,
|
||||||
|
obj_mode=None,
|
||||||
):
|
):
|
||||||
print("status:", solver.StatusName(status))
|
print("status:", solver.StatusName(status))
|
||||||
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
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)
|
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(
|
print(
|
||||||
f"\nFINAL E={fe / 10:.1f} B={fb / 10:.1f} S={fs / 10:.1f} "
|
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