2025-09-03 03:34:30 -07:00

135 lines
5.0 KiB
Python

# policy.py — resume-safe, no module state, overlap-aware
#
# Key fixes for your stuck run:
# • Uses a *derived lower bound* for the required (german_speaker ∩ international)
# overlap: GI_MIN = max(0, minG + minI - CAP); current GI >= max(0, G + I - admitted).
# This avoids freezing when swapping policies mid-game.
# • Hard gates:
# (1) helper-only, (2) per-attribute feasibility, (3) GI-overlap feasibility
# using the lower-bound (safe, never overestimates what's required).
# • Pacing guard: while any attribute is behind its on-track pace, only admit
# candidates that help at least one behind attribute.
# • Must-take guard: if S <= need[a] for any attribute a, accept anyone who has a.
#
# Drop-in compatible with the Berghain runner. Flip DEBUG=True for reasoning logs.
import sys
from math import ceil
from typing import Dict, List
DEBUG = False
# Attribute names for Scenario 3
U = "underground_veteran"
I = "international"
F = "fashion_forward"
Q = "queer_friendly"
V = "vinyl_collector"
G = "german_speaker"
# Small slack so we don't thrash when exactly on pace
PACE_SLACK = 0
def _log(msg: str) -> None:
if DEBUG:
print(msg, file=sys.stderr, flush=True)
def _need(mins: Dict[str, int], tallies: Dict[str, int]) -> Dict[str, int]:
return {a: max(0, mins[a] - tallies.get(a, 0)) for a in mins}
def _helpers(attrs: Dict[str, bool], need: Dict[str, int]) -> List[str]:
return [a for a, n in need.items() if n > 0 and attrs.get(a, False)]
def _feasible_per_attr(attrs: Dict[str, bool], need: Dict[str, int], S: int) -> bool:
# After admitting, S_after seats must cover all remaining shortfalls individually.
S_after = S - 1
for a, n in need.items():
rem = n - (1 if attrs.get(a, False) else 0)
if rem < 0:
rem = 0
if S_after < rem:
return False
return True
def _gi_both_lb(tallies: Dict[str, int], admitted_count: int) -> int:
# Lower bound on how many admitted are both German and International
g = tallies.get(G, 0)
i = tallies.get(I, 0)
return max(0, g + i - admitted_count)
def _feasible_gi_overlap(attrs: Dict[str, bool],
mins: Dict[str, int],
tallies: Dict[str, int],
admitted_count: int,
venue_cap: int,
S: int) -> bool:
# Enforce feasibility for the *required* G∩I overlap using the *lower bound* so we never over-block.
gi_min = max(0, mins.get(G, 0) + mins.get(I, 0) - venue_cap)
if gi_min <= 0:
return True
lb_now = _gi_both_lb(tallies, admitted_count)
lb_after = lb_now + (1 if (attrs.get(G, False) and attrs.get(I, False)) else 0)
need_after = max(0, gi_min - lb_after)
S_after = S - 1
return S_after >= need_after
def _pace_behind(mins: Dict[str, int], tallies: Dict[str, int], admitted_count: int, cap: int) -> List[str]:
# Attributes behind linear pace: ceil(min * admitted / cap)
behind = []
for a, m in mins.items():
want = ceil(m * admitted_count / max(1, cap))
have = tallies.get(a, 0)
if have + PACE_SLACK < want:
behind.append(a)
return behind
def decide(attributes: Dict[str, bool],
tallies: Dict[str, int],
mins: Dict[str, int],
admitted_count: int,
venue_cap: int) -> bool:
S = venue_cap - admitted_count # remaining seats
need = _need(mins, tallies)
total_need = sum(need.values())
# If all minima are satisfied, admit everyone.
if total_need == 0:
_log("All minima met -> admit")
return True
# Must help at least one unmet attribute.
helps = _helpers(attributes, need)
if not helps:
_log("Reject: non-helper")
return False
# Hard feasibility gates
if not _feasible_per_attr(attributes, need, S):
_log("Reject: per-attribute feasibility would break")
return False
if not _feasible_gi_overlap(attributes, mins, tallies, admitted_count, venue_cap, S):
_log("Reject: GI-overlap feasibility (lower-bound) would break")
return False
# Must-take guard: if S <= need[a] for any attribute a, we must accept anyone who has a.
must_take = [a for a, n in need.items() if n > 0 and S <= n]
if must_take and any(attributes.get(a, False) for a in must_take):
_log(f"Accept: must-take for {must_take}")
return True
# Pacing guard: while any attribute is behind its on-track count, only take helpers for those behind.
behind = _pace_behind(mins, tallies, admitted_count, venue_cap)
if behind:
if any(a in behind for a in helps):
_log(f"Accept: pacing helper for behind={behind}")
return True
else:
_log(f"Reject: pacing guard (behind={behind}); candidate helps {helps} none behind")
return False
# On pace: admit any feasible helper (GI overlap protected by the hard gate).
_log(f"Accept: on-pace helper {helps}")
return True