# 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