stuff
This commit is contained in:
parent
9711aa5bac
commit
7229124bb5
2 changed files with 217 additions and 122 deletions
142
main.py
142
main.py
|
|
@ -27,15 +27,18 @@ INITIAL = (30, 30, 30, 30)
|
|||
# at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument.
|
||||
# Total cities across all steps must be <= 7. Arriving cities act that same step.
|
||||
# Each entry may be either a string shorthand (e.g. "H") or a dict, e.g.
|
||||
# {"type": "F", "adjacent_to": [city_idx, ...]}
|
||||
# {"type": "F", "adjacent_to": [city_idx, ...], "vats": {"E": 1, "B": 1, "S": 1},
|
||||
# "departure_step": NUM_STEPS + 1}
|
||||
# `adjacent_to` is currently parsed but unused; see normalize_city().
|
||||
# Optional `vats` overrides initial vat values at arrival (each defaults to 1).
|
||||
# Optional `departure_step` is the first step at which the city is ABSENT;
|
||||
# default NUM_STEPS+1 means the city stays through the whole simulation.
|
||||
ARRIVALS = {
|
||||
1: [
|
||||
{"type": "F", "adjacent_to": []},
|
||||
{"type": "F", "adjacent_to": []},
|
||||
{"type": "F", "adjacent_to": [], "departure_step": 6},
|
||||
{"type": "H", "adjacent_to": []},
|
||||
{"type": "H", "adjacent_to": []},
|
||||
{"type": "N", "adjacent_to": []},
|
||||
],
|
||||
2: [],
|
||||
3: [],
|
||||
|
|
@ -46,8 +49,19 @@ ARRIVALS = {
|
|||
|
||||
def normalize_city(c):
|
||||
if isinstance(c, str):
|
||||
return {"type": c, "adjacent_to": []}
|
||||
return c
|
||||
c = {"type": c}
|
||||
vats_in = c.get("vats", {}) or {}
|
||||
vats = {
|
||||
"E": vats_in.get("E", 1),
|
||||
"B": vats_in.get("B", 1),
|
||||
"S": vats_in.get("S", 1),
|
||||
}
|
||||
return {
|
||||
"type": c["type"],
|
||||
"adjacent_to": c.get("adjacent_to", []),
|
||||
"vats": vats,
|
||||
"departure_step": c.get("departure_step", NUM_STEPS + 1),
|
||||
}
|
||||
|
||||
|
||||
# Collect Bonus (b) adds +1 to whatever a Collect gives. On a foundry that is
|
||||
|
|
@ -73,7 +87,7 @@ AGENT_AVAILABILITY = {
|
|||
"prodigy": [],
|
||||
"provisioner": [],
|
||||
"metallurgist": [2, 3, 4, 5],
|
||||
"builder": [],
|
||||
"builder": [2, 3, 4],
|
||||
"courier": [],
|
||||
"planner": [1, 2, 3, 4, 5],
|
||||
"fence": [],
|
||||
|
|
@ -94,6 +108,7 @@ ENABLED_ACTIONS = {
|
|||
"overwork": True,
|
||||
"renovate_h": True,
|
||||
"renovate_f": True,
|
||||
"noop": True,
|
||||
# "renovate_m": False, # currently always disabled in main.py
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +169,40 @@ class Action:
|
|||
# ======================================================================
|
||||
|
||||
|
||||
class NoOpAction(Action):
|
||||
def __init__(self):
|
||||
super().__init__("noop")
|
||||
|
||||
def declare_vars(self, model, i, t, ctx):
|
||||
noop = model.NewBoolVar(f"noop_{i}_{t}")
|
||||
ctx["action_vars"]["noop"][(i, t)] = noop
|
||||
ctx["noop"][i, t] = noop
|
||||
|
||||
# only allowed after overwork to avoid more things needing to be checked
|
||||
def add_constraints(self, model, i, t, ctx):
|
||||
noop = ctx["noop"][i, t]
|
||||
|
||||
if t == ctx["arrival_step"][i]:
|
||||
# Can't noop at arrival (no prior step)
|
||||
model.Add(noop == 0)
|
||||
else:
|
||||
# noop ≤ ow[i, t-1]: noop can be 1 only if overwork was 1 last step
|
||||
model.Add(noop <= ctx["ow"][i, t - 1])
|
||||
|
||||
def add_global_constraints(self, model, t, ctx):
|
||||
pass
|
||||
|
||||
def contributes_gains_costs(self, model, i, t, ctx):
|
||||
# No gains or costs
|
||||
pass
|
||||
|
||||
def transition_contrib(self, model, i, t, ctx):
|
||||
pass
|
||||
|
||||
def describe(self, solver, i, t, ctx):
|
||||
return "Rest (no action)"
|
||||
|
||||
|
||||
class CollectAction(Action):
|
||||
def __init__(self):
|
||||
super().__init__("collect")
|
||||
|
|
@ -466,9 +515,9 @@ class OverworkAction(Action):
|
|||
m = model
|
||||
ow = ctx["ow"]
|
||||
N = ctx["N"]
|
||||
# no planner = no overworks
|
||||
if "planner" not in AGENT_AVAILABILITY:
|
||||
# At most 1 city overworks per step
|
||||
m.Add(sum(ow[i, t] for i in range(N)) <= 1)
|
||||
m.Add(sum(ow[i, t] for i in range(N)) <= 0)
|
||||
# else: gating handled per-city by Planner
|
||||
|
||||
def contributes_gains_costs(self, model, i, t, ctx):
|
||||
|
|
@ -693,6 +742,7 @@ class Baron(Agent):
|
|||
m.Add(deposit == amt).OnlyEnforceIf(both)
|
||||
m.Add(deposit == 0).OnlyEnforceIf(both.Not())
|
||||
ctx["tg_pool"][t].append(deposit)
|
||||
ctx.setdefault("baron_deposits", {})[t] = deposit
|
||||
|
||||
|
||||
class Prodigy(Agent):
|
||||
|
|
@ -856,7 +906,9 @@ class Fence(EventAgent):
|
|||
"""Each available step, deposits +20 (=2 trade goods x10) into tg_pool[t]."""
|
||||
|
||||
def apply_event(self, model, t, ctx):
|
||||
ctx["tg_pool"][t].append(model.NewConstant(20))
|
||||
deposit = model.NewConstant(20)
|
||||
ctx["tg_pool"][t].append(deposit)
|
||||
ctx.setdefault("fence_deposits", {})[t] = 20
|
||||
|
||||
|
||||
# ======================================================================
|
||||
|
|
@ -899,6 +951,7 @@ def solve(
|
|||
# action variables (sparse, only created for enabled actions)
|
||||
col, ua, ub, ud = {}, {}, {}, {}
|
||||
ow = {}
|
||||
noop = {}
|
||||
rH, rF, rM = {}, {}, {}
|
||||
cvE, cvB, cvS = {}, {}, {}
|
||||
owcvE, owcvB, owcvS = {}, {}, {}
|
||||
|
|
@ -908,6 +961,7 @@ def solve(
|
|||
for i in range(N):
|
||||
a_step, a_city = cities[i]
|
||||
a_type = a_city["type"]
|
||||
d_step = a_city["departure_step"]
|
||||
for t in range(1, NUM_STEPS + 2):
|
||||
isH[i, t] = m.NewBoolVar(f"isH_{i}_{t}")
|
||||
isF[i, t] = m.NewBoolVar(f"isF_{i}_{t}")
|
||||
|
|
@ -921,8 +975,8 @@ def solve(
|
|||
vB[i, t] = m.NewIntVar(0, max_vat, f"vB_{i}_{t}")
|
||||
vS[i, t] = m.NewIntVar(0, max_vat, f"vS_{i}_{t}")
|
||||
|
||||
# presence: present from arrival step onward, persists
|
||||
m.Add(present[i, t] == (1 if t >= a_step else 0))
|
||||
# presence: present from arrival step until departure step (exclusive)
|
||||
m.Add(present[i, t] == (1 if a_step <= t < d_step else 0))
|
||||
# exactly one type iff present
|
||||
m.Add(isH[i, t] + isF[i, t] + isM[i, t] + isMon[i, t] == present[i, t])
|
||||
|
||||
|
|
@ -962,6 +1016,8 @@ def solve(
|
|||
action_registry.append(RenovateAction("renovate_h", "H"))
|
||||
if ENABLED_ACTIONS.get("renovate_f", True):
|
||||
action_registry.append(RenovateAction("renovate_f", "F"))
|
||||
if ENABLED_ACTIONS.get("noop", True):
|
||||
action_registry.append(NoOpAction())
|
||||
|
||||
# ---- Governor variable matrix ----
|
||||
# governor[(i, t, name)] = BoolVar (or constant 0) for whether agent `name`
|
||||
|
|
@ -1002,7 +1058,6 @@ def solve(
|
|||
"planner": Planner,
|
||||
"foreman": Foreman,
|
||||
"industrialist": Industrialist,
|
||||
"economist": Economist,
|
||||
}
|
||||
for name, cls in agent_classes.items():
|
||||
if name in AGENT_AVAILABILITY:
|
||||
|
|
@ -1019,6 +1074,8 @@ def solve(
|
|||
builder_fire_map = {}
|
||||
|
||||
# One-shot agent declarations.
|
||||
fence_deposits = {}
|
||||
baron_deposits = {}
|
||||
_agent_decl_ctx = {
|
||||
"model": m,
|
||||
"N": N,
|
||||
|
|
@ -1032,6 +1089,8 @@ def solve(
|
|||
"cost_C": cost_C,
|
||||
"cost_S": cost_S,
|
||||
"builder_fire": builder_fire_map,
|
||||
"fence_deposits": fence_deposits,
|
||||
"baron_deposits": baron_deposits,
|
||||
}
|
||||
for agent in agent_registry:
|
||||
agent.declare_vars(m, _agent_decl_ctx)
|
||||
|
|
@ -1051,21 +1110,28 @@ def solve(
|
|||
m.Add(hasB[i, a_step] == 0)
|
||||
m.Add(hasD[i, a_step] == 0)
|
||||
# vats at arrival
|
||||
m.Add(vE[i, a_step] == 1)
|
||||
m.Add(vB[i, a_step] == 1)
|
||||
m.Add(vS[i, a_step] == 1)
|
||||
m.Add(vE[i, a_step] == a_city["vats"]["E"])
|
||||
m.Add(vB[i, a_step] == a_city["vats"]["B"])
|
||||
m.Add(vS[i, a_step] == a_city["vats"]["S"])
|
||||
|
||||
# before arrival: everything zero
|
||||
for t in range(1, a_step):
|
||||
for v in (isH, isF, isM, isMon, hasA, hasB, hasD, vE, vB, vS):
|
||||
m.Add(v[i, t] == 0)
|
||||
|
||||
# after departure: everything zero from d_step onward
|
||||
d_step = a_city["departure_step"]
|
||||
for t in range(d_step, NUM_STEPS + 2):
|
||||
for v in (isH, isF, isM, isMon, hasA, hasB, hasD, vE, vB, vS):
|
||||
m.Add(v[i, t] == 0)
|
||||
|
||||
# action + transition logic for active steps
|
||||
for t in range(a_step, NUM_STEPS + 1):
|
||||
for t in range(a_step, min(d_step, NUM_STEPS + 1)):
|
||||
P = present[i, t]
|
||||
|
||||
# Build context for action methods
|
||||
ctx = {
|
||||
"noop": noop,
|
||||
"model": m,
|
||||
"i": i,
|
||||
"t": t,
|
||||
|
|
@ -1118,6 +1184,7 @@ def solve(
|
|||
"renovations": {},
|
||||
"vat_next": {},
|
||||
"builder_fire": builder_fire_map,
|
||||
"baron_deposits": baron_deposits,
|
||||
}
|
||||
|
||||
# Declare all action variables
|
||||
|
|
@ -1165,7 +1232,8 @@ def solve(
|
|||
for agent in agent_registry:
|
||||
agent.apply(m, i, t, ctx)
|
||||
|
||||
# Upgrade transitions
|
||||
# Upgrade transitions (skip propagation past last present step)
|
||||
if t + 1 < d_step:
|
||||
m.AddMaxEquality(
|
||||
hasA[i, t + 1], [hasA[i, t], ua.get((i, t), m.NewConstant(0))]
|
||||
)
|
||||
|
|
@ -1185,6 +1253,7 @@ def solve(
|
|||
for action in action_registry:
|
||||
action.contributes_gains_costs(m, i, t, ctx)
|
||||
|
||||
if t + 1 < d_step:
|
||||
# Transition contributions (including vats)
|
||||
for action in action_registry:
|
||||
action.transition_contrib(m, i, t, ctx)
|
||||
|
|
@ -1223,8 +1292,10 @@ def solve(
|
|||
metallurgist_g = governor.get((i, t, "metallurgist"))
|
||||
if metallurgist_g is not None and "metallurgist" in AGENT_AVAILABILITY:
|
||||
metal_active = m.NewBoolVar("")
|
||||
m.AddMultiplicationEquality(metal_active, [metallurgist_g, isF[i, t]])
|
||||
m.Add(inc == 1 + hasD[i, t] + metal_active)
|
||||
m.AddMultiplicationEquality(
|
||||
metal_active, [metallurgist_g, isF[i, t]]
|
||||
)
|
||||
m.Add(inc == 1 + metal_active)
|
||||
else:
|
||||
m.Add(inc == 1 + hasD[i, t])
|
||||
|
||||
|
|
@ -1280,8 +1351,8 @@ def solve(
|
|||
m.Add(vS[i, t + 1] == 0).OnlyEnforceIf(not_F_next)
|
||||
|
||||
# ---- Builder one-shot: at most one fire across all (i, t) ----
|
||||
if builder_fire_map:
|
||||
m.Add(sum(builder_fire_map.values()) <= 1)
|
||||
# if builder_fire_map:
|
||||
# m.Add(sum(builder_fire_map.values()) <= 1)
|
||||
|
||||
# ---- global constraints (per action type, per step) ----
|
||||
for t in range(1, NUM_STEPS + 1):
|
||||
|
|
@ -1302,6 +1373,8 @@ def solve(
|
|||
"gain_C": gain_C,
|
||||
"cost_C": cost_C,
|
||||
"cost_S": cost_S,
|
||||
"fence_deposits": fence_deposits,
|
||||
"baron_deposits": baron_deposits,
|
||||
}
|
||||
for t in range(1, NUM_STEPS + 1):
|
||||
for agent in event_agent_registry:
|
||||
|
|
@ -1324,6 +1397,10 @@ def solve(
|
|||
tg_S = m.NewIntVar(0, max_res, f"tg_S_{t}")
|
||||
tg_C = m.NewIntVar(0, max_res, f"tg_C_{t}")
|
||||
m.Add(tg_E + tg_B + tg_S + tg_C == pool_total)
|
||||
# Each trade good (10 scaled units) must be allocated entirely to one resource
|
||||
m.AddModuloEquality(m.NewIntVar(0, 0, ""), tg_E, 10)
|
||||
m.AddModuloEquality(m.NewIntVar(0, 0, ""), tg_B, 10)
|
||||
m.AddModuloEquality(m.NewIntVar(0, 0, ""), tg_S, 10)
|
||||
gain_E[t].append(tg_E)
|
||||
gain_B[t].append(tg_B)
|
||||
gain_S[t].append(tg_S)
|
||||
|
|
@ -1398,9 +1475,9 @@ def solve(
|
|||
solver.parameters.num_search_workers = num_workers
|
||||
status = solver.Solve(
|
||||
m,
|
||||
printer.IntermediateSolutionPrinter(
|
||||
{"electrum": finalE, "brass": finalB, "steel": finalS}, scale=0.01
|
||||
),
|
||||
# printer.IntermediateSolutionPrinter(
|
||||
# {"electrum": finalE, "brass": finalB, "steel": finalS}, scale=0.1
|
||||
# ),
|
||||
)
|
||||
|
||||
if verbose:
|
||||
|
|
@ -1453,6 +1530,8 @@ def solve(
|
|||
governor,
|
||||
agent_registry,
|
||||
capital_spent,
|
||||
fence_deposits,
|
||||
baron_deposits,
|
||||
)
|
||||
return solver, status
|
||||
|
||||
|
|
@ -1505,6 +1584,8 @@ def _report(
|
|||
governor=None,
|
||||
agent_registry=None,
|
||||
capital_spent=None,
|
||||
fence_deposits=None,
|
||||
baron_deposits=None,
|
||||
):
|
||||
print("status:", solver.StatusName(status))
|
||||
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
||||
|
|
@ -1605,6 +1686,19 @@ def _report(
|
|||
spent = sum(solver.Value(x) for x in capital_spent[t]) / 10
|
||||
print(f" step {t}: {spent:6.1f}")
|
||||
|
||||
if fence_deposits:
|
||||
print("\n Fence deposits (trade goods):")
|
||||
for t in fence_deposits:
|
||||
print(
|
||||
f" step {t}: {fence_deposits[t] / 10:.1f} (scaled: {fence_deposits[t]})"
|
||||
)
|
||||
|
||||
if baron_deposits:
|
||||
print("\n Baron deposits (trade goods):")
|
||||
for t in sorted(baron_deposits.keys()):
|
||||
amt = solver.Value(baron_deposits[t])
|
||||
print(f" step {t}: {amt / 10:.1f} (scaled: {amt})")
|
||||
|
||||
# Resolve governor assignments for display
|
||||
def gov_for(i, t):
|
||||
if not governor or not agent_registry:
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ class IntermediateSolutionPrinter(cp_model.CpSolverSolutionCallback):
|
|||
cp_model.CpSolverSolutionCallback.__init__(self)
|
||||
self._variables = variables
|
||||
self._solution_count = 0
|
||||
self.scale = scale
|
||||
|
||||
def on_solution_callback(self):
|
||||
"""Called each time an improving solution is found."""
|
||||
print("\n--- Solution ---")
|
||||
for name, var in self._variables.items():
|
||||
print(f"{name} = {self.Value(var)}")
|
||||
print(f"{name} = {self.scale * self.Value(var)}")
|
||||
|
||||
@property
|
||||
def solution_count(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue