import itertools
import random
import math
import time
from collections import deque
# ==========================================
# CONFIGURATION & INPUTS
# ==========================================
# 1. ENTER YOUR CURRENT RESOURCES HERE
INVENTORY = {
"carts": 6,
"wagons": 3,
"wells": 3,
"sumps": 13,
"pipes": 19
}
# 2. CURRENT LAYOUT (New Section)
# Enter the Node IDs where your structures are currently built.
# Leave lists empty [] if you have nothing built.
# Pipe Format: "NodeA-NodeB" (e.g. "V6H1-V6H2")
CURRENT_LAYOUT = {
"wells": ["V1H3", "V3H2"],
"sumps": ["V1H1", "V1H2", "V2H1", "V2H2", "V3H1", "SPLIT_D2_T", "SPLIT_D2_B", "V4H1", "V4H2", "V5H0", "V5H1", "V5H2", "V5H3"],
"pipes": ["V1H1-V2H1", "V1H2-V2H2", "V2H1-V3H1", "V2H1-V2H2", "V3H1-SPLIT_D2_T", "SPLIT_D2_T-V4H1", "SPLIT_D2_B-V4H2", "V4H1-V5H1", "V4H2-V5H2", "V5H1-V6H1", "V5H0-V5H1", "V5H1-V5H2", "V5H2-V5H3"]
}
# 3. SUMP EFFICIENCY CONFIGURATION
SUMP_CONFIG = {
"HIGH_OUTPUT": 11, # Rightmost 3 streets + D2 Split
"MID_OUTPUT": 11, # Middle 2 streets
"LOW_OUTPUT": 11 # Leftmost 2 streets + A2 Split
}
# 4. ENTER WATER DEMAND FOR EACH HOUSE
DEMANDS = {
"A1": 24, "B1": 25, "C1": 20, "D1": 21, "E1": 19, "F1": 20,
"A2T": 22, "B2": 22, "C2": 23, "D2L": 19, "E2": 25, "F2": 23,
"A2B": 22, "D2R": 21,
"A3": 21, "B3": 22, "C3": 21, "D3": 17, "E3": 22, "F3": 24
}
# ==========================================
# GAME LOGIC & MAPPING
# ==========================================
HOUSES = list(DEMANDS.keys())
HOUSE_INDICES = {h: i for i, h in enumerate(HOUSES)}
HOUSE_NEIGHBORS = {
"A1": ["B1", "A2T"],
"B1": ["A1", "C1", "B2"],
"C1": ["B1", "D1", "C2"],
"D1": ["C1", "E1", "D2L", "D2R"],
"E1": ["D1", "F1", "E2"],
"F1": ["E1", "F2"],
"A2T": ["A1", "A2B", "B2"],
"A2B": ["A2T", "A3", "B2"],
"B2": ["B1", "B3", "C2", "A2T", "A2B"],
"C2": ["C1", "C3", "B2", "D2L"],
"D2L": ["D1", "D3", "C2", "D2R"],
"D2R": ["D1", "D3", "E2", "D2L"],
"E2": ["E1", "E3", "F2", "D2R"],
"F2": ["F1", "F3", "E2"],
"A3": ["B3", "A2B"],
"B3": ["A3", "C3", "B2"],
"C3": ["B3", "D3", "C2"],
"D3": ["C3", "E3", "D2L", "D2R"],
"E3": ["D3", "F3", "E2"],
"F3": ["E3", "F2"]
}
# Pre-compute neighbor indices for faster lookup
HOUSE_NEIGHBOR_INDICES = [
[HOUSE_INDICES[n] for n in HOUSE_NEIGHBORS[h]]
for h in HOUSES
]
NODES = []
NODE_ADJACENCY = {}
NODE_SUMP_VALUE = {}
NODE_NEIGHBORS = {}
def setup_grid():
# 1. Standard Intersections
for v in range(7):
for h in range(4):
node_id = f"V{v}H{h}"
NODES.append(node_id)
if v >= 4: val = SUMP_CONFIG["HIGH_OUTPUT"]
elif v >= 2: val = SUMP_CONFIG["MID_OUTPUT"]
else: val = SUMP_CONFIG["LOW_OUTPUT"]
NODE_SUMP_VALUE[node_id] = val
adj = []
def get_house(c, r):
if c < 0 or c > 5 or r < 0 or r > 2: return None
if c == 0 and r == 1: return ["A2T", "A2B"]
if c == 3 and r == 1: return ["D2L", "D2R"]
cols = "ABCDEF"
return [f"{cols[c]}{r+1}"]
# Quadrant checks
hl = get_house(v-1, h-1)
if hl:
for hx in hl:
if hx == "A2T" and h == 2: continue
if hx == "A2B" and h == 1: continue
if hx == "D2L" and v == 4: continue
if hx == "D2R" and v == 3: continue
adj.append(hx)
hr = get_house(v, h-1)
if hr:
for hx in hr:
if hx == "A2T" and h == 2: continue
if hx == "A2B" and h == 1: continue
if hx == "D2L" and v == 4: continue
if hx == "D2R" and v == 3: continue
adj.append(hx)
bl = get_house(v-1, h)
if bl:
for hx in bl:
if hx == "A2T" and h == 2: continue
if hx == "A2B" and h == 1: continue
if hx == "D2L" and v == 4: continue
if hx == "D2R" and v == 3: continue
adj.append(hx)
br = get_house(v, h)
if br:
for hx in br:
if hx == "A2T" and h == 2: continue
if hx == "A2B" and h == 1: continue
if hx == "D2L" and v == 4: continue
if hx == "D2R" and v == 3: continue
adj.append(hx)
NODE_ADJACENCY[node_id] = list(set(adj))
# 2. Special Split Nodes
nid = "SPLIT_A2_L"
NODES.append(nid)
NODE_SUMP_VALUE[nid] = SUMP_CONFIG["LOW_OUTPUT"]
NODE_ADJACENCY[nid] = ["A2T", "A2B"]
nid = "SPLIT_A2_R"
NODES.append(nid)
NODE_SUMP_VALUE[nid] = SUMP_CONFIG["LOW_OUTPUT"]
NODE_ADJACENCY[nid] = ["A2T", "A2B", "B2"]
nid = "SPLIT_D2_T"
NODES.append(nid)
NODE_SUMP_VALUE[nid] = SUMP_CONFIG["HIGH_OUTPUT"]
NODE_ADJACENCY[nid] = ["D1", "D2L", "D2R"]
nid = "SPLIT_D2_B"
NODES.append(nid)
NODE_SUMP_VALUE[nid] = SUMP_CONFIG["HIGH_OUTPUT"]
NODE_ADJACENCY[nid] = ["D3", "D2L", "D2R"]
# 3. Graph Connections
for n in NODES:
NODE_NEIGHBORS[n] = []
def connect(n1, n2):
if n1 in NODES and n2 in NODES:
NODE_NEIGHBORS[n1].append(n2)
NODE_NEIGHBORS[n2].append(n1)
for v in range(7):
for h in range(4):
curr = f"V{v}H{h}"
if v < 6: connect(curr, f"V{v+1}H{h}")
if h < 3: connect(curr, f"V{v}H{h+1}")
connect("SPLIT_A2_L", "V0H1")
connect("SPLIT_A2_L", "V0H2")
connect("SPLIT_A2_L", "SPLIT_A2_R")
connect("SPLIT_A2_R", "V1H1")
connect("SPLIT_A2_R", "V1H2")
connect("SPLIT_D2_T", "V3H1")
connect("SPLIT_D2_T", "V4H1")
connect("SPLIT_D2_T", "SPLIT_D2_B")
connect("SPLIT_D2_B", "V3H2")
connect("SPLIT_D2_B", "V4H2")
setup_grid()
MILL_NODE = "V6H1"
# ==========================================
# VALIDATION UTILS
# ==========================================
def get_connected_sumps_from_layout(layout):
if not layout['sumps'] or not layout['pipes']:
return []
adj = {n: [] for n in NODES}
for p in layout['pipes']:
parts = p.split('-')
if len(parts) != 2: continue
u, v = parts[0].strip(), parts[1].strip()
if u in adj and v in adj:
adj[u].append(v)
adj[v].append(u)
connected = set()
queue = deque([MILL_NODE])
visited = {MILL_NODE}
while queue:
curr = queue.popleft()
connected.add(curr)
for n in adj[curr]:
if n not in visited:
visited.add(n)
queue.append(n)
active_sumps = [s for s in layout['sumps'] if s in connected]
inactive_sumps = [s for s in layout['sumps'] if s not in connected]
if len(inactive_sumps) > 0:
print(f" ! Warning: {inactive_sumps} not connected to Mill.")
return active_sumps
def check_theoretical_pipe_validity(sump_nodes, available_pipes):
if not sump_nodes: return True
connected_nodes = {MILL_NODE}
target_sumps = set(sump_nodes)
if MILL_NODE in target_sumps: target_sumps.remove(MILL_NODE)
pipes_used = 0
while target_sumps:
best_path = []
found = False
queue = deque([(n, []) for n in connected_nodes])
visited = set(connected_nodes)
while queue:
curr, path = queue.popleft()
if curr in target_sumps:
best_path = path
chosen_sump = curr
found = True
break
for neighbor in NODE_NEIGHBORS[curr]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
if not found: return False
pipes_used += len(best_path)
for node in best_path: connected_nodes.add(node)
target_sumps.remove(chosen_sump)
return pipes_used <= available_pipes
# ==========================================
# SOLVER ENGINE
# ==========================================
def calculate_supply_from_structures(active_structures):
"""Calculate base supply from wells/sumps only."""
supply = [0] * len(HOUSES)
for node, s_type in active_structures.items():
if s_type == 'well': amt = 2
else: amt = NODE_SUMP_VALUE[node]
for house_str in NODE_ADJACENCY[node]:
idx = HOUSE_INDICES[house_str]
supply[idx] += amt
return supply
def add_employee_supply(base_supply, cart_locs, wagon_locs):
"""
Adds employee contribution to a base supply list.
Accepts lists of locations (e.g. ['A1', 'A1', 'B2'])
"""
# Copy strictly required? depends on context.
# In loops we usually copy before calling this or modify a temp array.
supply = list(base_supply)
# Wagons
for h_idx in wagon_locs:
supply[h_idx] += 8
for n_idx in HOUSE_NEIGHBOR_INDICES[h_idx]:
supply[n_idx] += 6
# Carts
for h_idx in cart_locs:
supply[h_idx] += 3
for n_idx in HOUSE_NEIGHBOR_INDICES[h_idx]:
supply[n_idx] += 2
return supply
def get_score_from_supply_array(supply, demand_array):
inf = 0
shortfall = 0
for i in range(len(supply)):
diff = supply[i] - demand_array[i]
if diff >= 0: inf += 1
elif diff == -1: pass # +0
else: inf -= 2
if diff < 0: shortfall += abs(diff)
return inf, shortfall
def calculate_full_score(assignments, active_structures, demands_dict):
"""
assignments: {'carts': ['A1',...], 'wagons': ['B2',...]}
"""
# Build Demand Array
dem_arr = [demands_dict[h] for h in HOUSES]
# 1. Structure Supply
base_supply = calculate_supply_from_structures(active_structures)
# 2. Employee Supply
# Convert house strings to indices
c_idxs = [HOUSE_INDICES[h] for h in assignments['carts']]
w_idxs = [HOUSE_INDICES[h] for h in assignments['wagons']]
final_supply = add_employee_supply(base_supply, c_idxs, w_idxs)
# 3. Score
inf, short = get_score_from_supply_array(final_supply, dem_arr)
# Convert back to dict for display
supply_dict = {h: final_supply[i] for i, h in enumerate(HOUSES)}
return inf, short, supply_dict
# ==========================================
# OPTIMIZATION PHASES
# ==========================================
def solve_phase_fixed_structures(demands, n_carts, n_wagons, fixed_structures):
"""
Brute Force with Unlimited Stacking (Combinations with Replacement).
Optimized to run fast by pre-calculating structure supply.
"""
print(" (Calculating combinations... this might take a moment)")
# Pre-calc base
base_supply = calculate_supply_from_structures(fixed_structures)
demand_arr = [demands[h] for h in HOUSES]
house_indices = list(range(len(HOUSES)))
best_inf = -999
best_shortfall = 999
best_assignment = {'carts': [], 'wagons': []}
# 1. Iterate Wagons (Combinations with replacement allows stacking)
# Using indices for speed
for wagon_indices in itertools.combinations_with_replacement(house_indices, n_wagons):
# Calculate supply just from wagons + base
current_supply_w = list(base_supply)
for w_idx in wagon_indices:
current_supply_w[w_idx] += 8
for n_idx in HOUSE_NEIGHBOR_INDICES[w_idx]:
current_supply_w[n_idx] += 6
# 2. Iterate Carts
for cart_indices in itertools.combinations_with_replacement(house_indices, n_carts):
# Add carts to the wagon+base supply
final_supply = list(current_supply_w)
for c_idx in cart_indices:
final_supply[c_idx] += 3
for n_idx in HOUSE_NEIGHBOR_INDICES[c_idx]:
final_supply[n_idx] += 2
# Score inline
inf = 0
shortfall = 0
possible_max = 20
# Unrolling loop slightly for speed/fail-fast?
# Not easy in Python, but let's just do standard iteration
for i in range(20):
diff = final_supply[i] - demand_arr[i]
if diff >= 0:
inf += 1
elif diff == -1:
possible_max -= 1 # Can't reach 20
else:
inf -= 2
possible_max -= 3 # Lost 1 potential point plus 2 penalty
if diff < 0: shortfall += abs(diff)
# Pruning: If we can't beat current best, stop checking this combo
# (Simple pruning logic is hard here without calculating full score)
if inf > best_inf or (inf == best_inf and shortfall < best_shortfall):
best_inf = inf
best_shortfall = shortfall
best_assignment = {
'wagons': [HOUSES[i] for i in wagon_indices],
'carts': [HOUSES[i] for i in cart_indices]
}
if best_inf == 20:
return best_assignment, best_inf
return best_assignment, best_inf
def solve_phase_sa(demands, n_carts, n_wagons, n_wells, n_sumps, n_pipes,
mode='move_wells', fixed_sumps=[]):
"""
Simulated Annealing handling Multi-Unit Stacking.
modes: 'move_wells', 'move_all'
"""
msg = "Sumps & Pipes" if mode == 'move_all' else "Wells (Fixed Sumps)"
print(f"--- Phase SA: Relocating {msg} ---")
# Init State
# Randomly assign units (allowing stacking)
current_assign = {'carts': [], 'wagons': []}
for _ in range(n_carts): current_assign['carts'].append(random.choice(HOUSES))
for _ in range(n_wagons): current_assign['wagons'].append(random.choice(HOUSES))
# Init Structures
current_structs = {}
if mode == 'move_wells':
for s in fixed_sumps: current_structs[s] = 'sump'
avail = [n for n in NODES if n not in current_structs]
random.shuffle(avail)
for _ in range(n_wells):
if avail: current_structs[avail.pop()] = 'well'
else:
avail = NODES[:]
random.shuffle(avail)
for _ in range(n_wells):
if avail: current_structs[avail.pop()] = 'well'
for _ in range(n_sumps):
if avail: current_structs[avail.pop()] = 'sump'
def get_neighbor(assign, struct):
new_a = {'carts': assign['carts'][:], 'wagons': assign['wagons'][:]}
new_s = struct.copy()
r = random.random()
# Mutation 1: Move a Unit
# Pick a type, pick an index, change location
if r < 0.4:
u_type = 'carts' if (new_a['carts'] and random.random()<0.5) else 'wagons'
if not new_a['carts'] and new_a['wagons']: u_type = 'wagons'
if new_a['carts'] and not new_a['wagons']: u_type = 'carts'
if new_a[u_type]:
idx = random.randrange(len(new_a[u_type]))
new_a[u_type][idx] = random.choice(HOUSES) # Move to ANY house (stacking allowed)
# Mutation 2: Move/Swap Structure
elif r < 0.9:
if new_s:
n_from = random.choice(list(new_s.keys()))
# If mode is move_wells, we cannot move fixed sumps
if mode == 'move_wells' and n_from in fixed_sumps:
pass # Skip
else:
sType = new_s.pop(n_from)
# Find empty spot
occupied = set(new_s.keys())
if mode == 'move_wells': occupied.update(fixed_sumps)
empty = [n for n in NODES if n not in occupied]
if empty:
new_s[random.choice(empty)] = sType
else:
new_s[n_from] = sType # Revert if full
return new_a, new_s
# Initial Score
curr_inf, _, _ = calculate_full_score(current_assign, current_structs, demands)
if mode == 'move_all':
sumps = [n for n, t in current_structs.items() if t == 'sump']
if not check_theoretical_pipe_validity(sumps, n_pipes): curr_inf -= 10
best_inf = -999
best_res = (current_assign, current_structs)
temp = 10.0
cooling = 0.995
steps = 8000 if mode == 'move_wells' else 12000
for _ in range(steps):
na, ns = get_neighbor(current_assign, current_structs)
ni, _, _ = calculate_full_score(na, ns, demands)
valid = True
if mode == 'move_all':
sumps = [n for n, t in ns.items() if t == 'sump']
if not check_theoretical_pipe_validity(sumps, n_pipes):
valid = False
ni -= 10
delta = ni - curr_inf
if delta > 0 or random.random() < math.exp(delta/temp):
current_assign = na
current_structs = ns
curr_inf = ni
if valid and ni > best_inf:
best_inf = ni
best_res = ({'carts': na['carts'][:], 'wagons': na['wagons'][:]}, ns.copy())
if best_inf == 20: break
temp *= cooling
return best_res[0], best_res[1], best_inf
# ==========================================
# MAIN EXECUTION
# ==========================================
def print_solution(assign, struct, demands, supply_details, phase_msg):
print("\n" + "="*50)
print(f"SOLUTION FOUND: {phase_msg}")
print("="*50)
inf, _, supply = calculate_full_score(assign, struct, demands)
print(f"Projected Influence: {inf}/20")
print("\n--- Employee Assignments (Stacked) ---")
# Group by house for cleaner printing
# e.g. A1: 2 Carts, 1 Wagon
grouped = {h: {'carts': 0, 'wagons': 0} for h in HOUSES}
for c in assign['carts']: grouped[c]['carts'] += 1
for w in assign['wagons']: grouped[w]['wagons'] += 1
has_units = False
for h in sorted(HOUSES):
c = grouped[h]['carts']
w = grouped[h]['wagons']
if c > 0 or w > 0:
has_units = True
parts = []
if w > 0: parts.append(f"{w} Wagon(s)")
if c > 0: parts.append(f"{c} Cart(s)")
print(f" {h}: " + ", ".join(parts))
if not has_units: print(" None")
print("\n--- Structure Placements ---")
if not struct: print(" None")
for n, sType in struct.items():
val = NODE_SUMP_VALUE[n] if sType=='sump' else 2
print(f" {sType.upper().ljust(6)} -> {n} (Water: {val})")
print("\n--- House Status ---")
print(f" {'House':<6} {'Dem':<5} {'Sup':<5} {'Diff':<5} {'Infl'}")
print("-" * 35)
total_inf = 0
ids = sorted(HOUSES)
for h in ids:
d = demands[h]
s = supply[h]
diff = s - d
if diff >= 0: i = 1
elif diff == -1: i = 0
else: i = -2
total_inf += i
print(f" {h:<6} {d:<5} {s:<5} {diff:<5} {i}")
print("-" * 35)
print(f" TOTAL INFLUENCE: {total_inf}")
def main():
print("Starting Water Supply Optimization...")
start_time = time.time()
# ----------------------------------------------------
# PHASE 1: CURRENT SETUP + EMPLOYEE OPTIMIZATION
# ----------------------------------------------------
print("--- Phase 1: Checking Current Structure Setup ---")
active_sumps = get_connected_sumps_from_layout(CURRENT_LAYOUT)
fixed_structs = {}
for w in CURRENT_LAYOUT['wells']: fixed_structs[w] = 'well'
for s in active_sumps: fixed_structs[s] = 'sump'
if len(active_sumps) < len(CURRENT_LAYOUT['sumps']):
print(f" ! Warning: Only {len(active_sumps)} of {len(CURRENT_LAYOUT['sumps'])} sumps are connected to Mill.")
assign, inf = solve_phase_fixed_structures(DEMANDS, INVENTORY['carts'], INVENTORY['wagons'], fixed_structs)
final_structs = fixed_structs
solution_phase = "Phase 1 (Current Setup)"
# ----------------------------------------------------
# PHASE 2: MOVE WELLS (Fixed Sumps)
# ----------------------------------------------------
if inf < 20 and INVENTORY['wells'] > 0:
print(f" Result: {inf}/20. Attempting to relocate Wells...")
assign_2, struct_2, inf_2 = solve_phase_sa(
DEMANDS, INVENTORY['carts'], INVENTORY['wagons'], INVENTORY['wells'], 0, 0,
mode='move_wells', fixed_sumps=active_sumps
)
if inf_2 > inf:
assign = assign_2
final_structs = struct_2
inf = inf_2
solution_phase = "Phase 2 (Relocated Wells)"
# ----------------------------------------------------
# PHASE 3: MOVE EVERYTHING
# ----------------------------------------------------
if inf < 20 and INVENTORY['sumps'] > 0 and INVENTORY['pipes'] > 0:
print(f" Result: {inf}/20. Attempting to relocate Sumps & Pipes...")
assign_3, struct_3, inf_3 = solve_phase_sa(
DEMANDS, INVENTORY['carts'], INVENTORY['wagons'], INVENTORY['wells'], INVENTORY['sumps'], INVENTORY['pipes'],
mode='move_all'
)
if inf_3 > inf:
assign = assign_3
final_structs = struct_3
inf = inf_3
solution_phase = "Phase 3 (New Layout)"
print(f"\nCalculation finished in {round(time.time() - start_time, 2)} seconds.")
_, _, final_supply = calculate_full_score(assign, final_structs, DEMANDS)
print_solution(assign, final_structs, DEMANDS, final_supply, solution_phase)
if __name__ == "__main__":
main()