From 2db8ff63c757a332d5f791db11f5aa300067405d Mon Sep 17 00:00:00 2001 From: Pagwin Date: Fri, 5 Jun 2026 13:57:00 -0400 Subject: [PATCH] added intermediate printing --- .gitignore | 3 + main.py | 174 +++++++++++++++++++++++++++++++++++++++++++++++++---- printer.py | 20 ++++++ 3 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100644 printer.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7154c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.serena +__pycache__ +output.txt diff --git a/main.py b/main.py index 89ff5ff..af49d63 100644 --- a/main.py +++ b/main.py @@ -11,7 +11,7 @@ LP/MIP solver. Read the comments at: """ from ortools.sat.python import cp_model - +import printer # ====================================================================== # PARAMETERS -- edit these @@ -24,10 +24,10 @@ INITIAL = (3, 3, 3, 3) # 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. ARRIVALS = { - 1: ["H", "F", "H", "N", "F"], - 2: ["M"], + 1: ["H", "F", "H"], + 2: ["F"], 3: [], - 4: [], + 4: ["N"], 5: [], } @@ -81,9 +81,12 @@ def solve( # action variables, t in 1..NUM_STEPS col, ua, ub, ud = {}, {}, {}, {} # collect / upgrade a,b,d + ow = {} # overwork (global limit: 1 per step) rH, rF, rM = {}, {}, {} # renovate -> Hub/Foundry/Metro - cvE, cvB, cvS = {}, {}, {} # foundry: which vat collected + cvE, cvB, cvS = {}, {}, {} # foundry: which vat collected (normal collect) + owcvE, owcvB, owcvS = {}, {}, {} # foundry: which vat collected (overwork) mE, mB, mS, mC = {}, {}, {}, {} # metropolis: resources picked (+1 each) + owmE, owmB, owmS, owmC = {}, {}, {}, {} # metropolis: resources picked (overwork) for i in range(N): a_step, a_type = cities[i] @@ -110,16 +113,24 @@ def solve( ua[i, t] = m.NewBoolVar(f"ua_{i}_{t}") ub[i, t] = m.NewBoolVar(f"ub_{i}_{t}") ud[i, t] = m.NewBoolVar(f"ud_{i}_{t}") + ow[i, t] = m.NewBoolVar(f"ow_{i}_{t}") rH[i, t] = m.NewBoolVar(f"rH_{i}_{t}") rF[i, t] = m.NewBoolVar(f"rF_{i}_{t}") rM[i, t] = m.NewBoolVar(f"rM_{i}_{t}") cvE[i, t] = m.NewBoolVar(f"cvE_{i}_{t}") cvB[i, t] = m.NewBoolVar(f"cvB_{i}_{t}") cvS[i, t] = m.NewBoolVar(f"cvS_{i}_{t}") + owcvE[i, t] = m.NewBoolVar(f"owcvE_{i}_{t}") + owcvB[i, t] = m.NewBoolVar(f"owcvB_{i}_{t}") + owcvS[i, t] = m.NewBoolVar(f"owcvS_{i}_{t}") mE[i, t] = m.NewIntVar(0, 3, f"mE_{i}_{t}") mB[i, t] = m.NewIntVar(0, 3, f"mB_{i}_{t}") mS[i, t] = m.NewIntVar(0, 3, f"mS_{i}_{t}") mC[i, t] = m.NewIntVar(0, 3, f"mC_{i}_{t}") + owmE[i, t] = m.NewIntVar(0, 6, f"owmE_{i}_{t}") + owmB[i, t] = m.NewIntVar(0, 6, f"owmB_{i}_{t}") + owmS[i, t] = m.NewIntVar(0, 6, f"owmS_{i}_{t}") + owmC[i, t] = m.NewIntVar(0, 6, f"owmC_{i}_{t}") # ---- per-step gain/cost accumulators (linear expressions) --------- gain_E = {t: [] for t in range(1, NUM_STEPS + 1)} @@ -150,7 +161,30 @@ def solve( for v in (isH, isF, isM, isMon, hasA, hasB, hasD, vE, vB, vS): m.Add(v[i, t] == 0) if t <= NUM_STEPS: - for v in (col, ua, ub, ud, rH, rF, rM, cvE, cvB, cvS, mE, mB, mS, mC): + for v in ( + col, + ua, + ub, + ud, + ow, + rH, + rF, + rM, + cvE, + cvB, + cvS, + owcvE, + owcvB, + owcvS, + mE, + mB, + mS, + mC, + owmE, + owmB, + owmS, + owmC, + ): m.Add(v[i, t] == 0) # action + transition logic for active steps @@ -167,6 +201,7 @@ def solve( + ua[i, t] + ub[i, t] + ud[i, t] + + ow[i, t] + rH[i, t] + rF[i, t] + rM[i, t] @@ -192,6 +227,12 @@ def solve( m.Add(ua[i, t] == 0).OnlyEnforceIf(isMon[i, t]) m.Add(ub[i, t] == 0).OnlyEnforceIf(isMon[i, t]) m.Add(ud[i, t] == 0).OnlyEnforceIf(isMon[i, t]) + m.Add(ow[i, t] == 0).OnlyEnforceIf(isMon[i, t]) + + # overwork cooldown: city that overworked last step can't collect or overwork + if t > a_step: + m.Add(col[i, t] == 0).OnlyEnforceIf(ow[i, t - 1]) + m.Add(ow[i, t] == 0).OnlyEnforceIf(ow[i, t - 1]) # ---- type transition t -> t+1 ---- m.Add(isH[i, t + 1] == isH[i, t]).OnlyEnforceIf(no_ren) @@ -272,6 +313,61 @@ def solve( gain_B[t].append(AND(cvB[i, t], hasB[i, t])) gain_S[t].append(AND(cvS[i, t], hasB[i, t])) + # ---- overwork sub-choices (double-collect; no Capital cost) ---- + fow = AND(isF[i, t], ow[i, t]) + mow = AND(isM[i, t], ow[i, t]) + how = AND(isH[i, t], ow[i, t]) + + # foundry overwork: exactly one vat chosen iff foundry overworks + m.Add(owcvE[i, t] + owcvB[i, t] + owcvS[i, t] == fow) + + # metropolis overwork: pick 2*(2 + hasB) resources; else nothing + ow_npick = m.NewIntVar(0, 6, "") + m.Add(ow_npick == 2 * (2 + hasB[i, t])).OnlyEnforceIf(mow) + m.Add(ow_npick == 0).OnlyEnforceIf(mow.Not()) + m.Add(owmE[i, t] + owmB[i, t] + owmS[i, t] + owmC[i, t] == ow_npick) + + # ================= OVERWORK GAINS (2x normal collect) ================= + # Hub overwork: 2*(2 + hasB) Capital + hub_ow_gain = m.NewIntVar(0, 6, "") + m.Add(hub_ow_gain == 2 * (2 + hasB[i, t])).OnlyEnforceIf(how) + m.Add(hub_ow_gain == 0).OnlyEnforceIf(how.Not()) + gain_C[t].append(hub_ow_gain) + + # Metropolis overwork: +1 per pick (picks are already doubled via ow_npick) + gain_E[t].append(owmE[i, t]) + gain_B[t].append(owmB[i, t]) + gain_S[t].append(owmS[i, t]) + gain_C[t].append(owmC[i, t]) + + # Foundry overwork: 2 * chosen vat's value + owgEf = m.NewIntVar(0, 2 * max_vat, "") + owgBf = m.NewIntVar(0, 2 * max_vat, "") + owgSf = m.NewIntVar(0, 2 * max_vat, "") + _owE = m.NewIntVar(0, max_vat, "") + _owB = m.NewIntVar(0, max_vat, "") + _owS = m.NewIntVar(0, max_vat, "") + m.AddMultiplicationEquality(_owE, [owcvE[i, t], vE[i, t]]) + m.AddMultiplicationEquality(_owB, [owcvB[i, t], vB[i, t]]) + m.AddMultiplicationEquality(_owS, [owcvS[i, t], vS[i, t]]) + m.Add(owgEf == 2 * _owE) + m.Add(owgBf == 2 * _owB) + m.Add(owgSf == 2 * _owS) + gain_E[t].append(owgEf) + gain_B[t].append(owgBf) + gain_S[t].append(owgSf) + + # Collect Bonus (b) for foundry overwork: 2 * (+1) = +2 of that resource + ow_be = AND(owcvE[i, t], hasB[i, t]) + ow_bb = AND(owcvB[i, t], hasB[i, t]) + ow_bs = AND(owcvS[i, t], hasB[i, t]) + gain_E[t].append(ow_be) + gain_E[t].append(ow_be) # added twice == *2 + gain_B[t].append(ow_bb) + gain_B[t].append(ow_bb) + gain_S[t].append(ow_bs) + gain_S[t].append(ow_bs) + # ---- vat update producing vat[i, t+1] ---- # increment added to the two non-collected vats (1, or 2 with upgrade d) inc = m.NewIntVar(1, 2, "") @@ -293,11 +389,24 @@ def solve( m.Add(vSn == 0).OnlyEnforceIf(cvS[i, t]) m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(cvS[i, t]) m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(cvS[i, t]) - # foundry but not collecting (upgrade / renovate-away): vats unchanged - f_noncollect = AND(isF[i, t], col[i, t].Not()) - m.Add(vEn == vE[i, t]).OnlyEnforceIf(f_noncollect) - m.Add(vBn == vB[i, t]).OnlyEnforceIf(f_noncollect) - m.Add(vSn == vS[i, t]).OnlyEnforceIf(f_noncollect) + # overwork vat transitions: same reset/increment as normal collect + m.Add(vEn == 0).OnlyEnforceIf(owcvE[i, t]) + m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(owcvE[i, t]) + m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(owcvE[i, t]) + m.Add(vBn == 0).OnlyEnforceIf(owcvB[i, t]) + m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(owcvB[i, t]) + m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(owcvB[i, t]) + m.Add(vSn == 0).OnlyEnforceIf(owcvS[i, t]) + m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(owcvS[i, t]) + m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(owcvS[i, t]) + + # foundry but neither collecting nor overworking: vats unchanged + # f_noncollect_noow = isF AND NOT fcol AND NOT fow = isF - fcol - fow + f_noncollect_noow = m.NewBoolVar("") + m.Add(f_noncollect_noow == isF[i, t] - fcol - fow) + m.Add(vEn == vE[i, t]).OnlyEnforceIf(f_noncollect_noow) + m.Add(vBn == vB[i, t]).OnlyEnforceIf(f_noncollect_noow) + m.Add(vSn == vS[i, t]).OnlyEnforceIf(f_noncollect_noow) # assign vat[i, t+1]: # renovate-to-foundry -> reset to 1 @@ -319,6 +428,10 @@ def solve( m.Add(vB[i, t + 1] == 0).OnlyEnforceIf(not_F_next) m.Add(vS[i, t + 1] == 0).OnlyEnforceIf(not_F_next) + # ---- global overwork limit: at most 1 city overworks per step ---- + for t in range(1, NUM_STEPS + 1): + m.Add(sum(ow[i, t] for i in range(N)) <= 1) + # ---- resource pool recursion -------------------------------------- E = {1: m.NewIntVar(initial[0], initial[0], "E1")} B = {1: m.NewIntVar(initial[1], initial[1], "B1")} @@ -382,7 +495,12 @@ def solve( solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = time_limit solver.parameters.num_search_workers = num_workers - status = solver.Solve(m) + status = solver.Solve( + m, + printer.IntermediateSolutionPrinter( + {"electrum": finalE, "brass": finalB, "steel": finalS} + ), + ) if verbose: print(f"(resource ceilings used: E<={capE} B<={capB} S<={capS})") @@ -399,16 +517,24 @@ def solve( ua, ub, ud, + ow, rH, rF, rM, cvE, cvB, cvS, + owcvE, + owcvB, + owcvS, mE, mB, mS, mC, + owmE, + owmB, + owmS, + owmC, hasA, hasB, hasD, @@ -439,16 +565,24 @@ def _report( ua, ub, ud, + ow, rH, rF, rM, cvE, cvB, cvS, + owcvE, + owcvB, + owcvS, mE, mB, mS, mC, + owmE, + owmB, + owmS, + owmC, hasA, hasB, hasD, @@ -497,6 +631,22 @@ def _report( picks += [nm] * solver.Value(var[i, t]) return "Collect {" + ",".join(picks) + "}" return "Collect (+Capital)" + if solver.Value(ow[i, t]): + if typ_name[i, t] == "Foundry": + v = ( + "E" + if solver.Value(owcvE[i, t]) + else "B" + if solver.Value(owcvB[i, t]) + else "S" + ) + return f"Overwork vat {v} (2x)" + if typ_name[i, t] == "Metro": + picks = [] + for nm, var in (("E", owmE), ("B", owmB), ("S", owmS), ("C", owmC)): + picks += [nm] * solver.Value(var[i, t]) + return "Overwork {" + ",".join(picks) + "} (2x)" + return "Overwork (+Capital 2x)" if solver.Value(ua[i, t]): return "Upgrade a (cost-reduction)" if solver.Value(ub[i, t]): diff --git a/printer.py b/printer.py new file mode 100644 index 0000000..8580275 --- /dev/null +++ b/printer.py @@ -0,0 +1,20 @@ +from ortools.sat.python import cp_model + + +class IntermediateSolutionPrinter(cp_model.CpSolverSolutionCallback): + """Callback that prints intermediate solutions.""" + + def __init__(self, variables): + cp_model.CpSolverSolutionCallback.__init__(self) + self._variables = variables + self._solution_count = 0 + + 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)}") + + @property + def solution_count(self): + return self._solution_count