build-small-hackathon/aube-nova
1
1"""2Two types of injectable events:3- CRISES: degrade resources, create urgency4- OPTIMISM (Beginning of Infinity mechanic): generate permanent breakthroughs5 inspired by Deutsch's idea that all problems are soluble given good explanations.6"""7 8import random9 10from simulation.colonist import ALL_TRAITS11from simulation.world import Breakthrough, WorldState12 13CRISIS_DEFS = [14 {15 "id": "oxygen_failure",16 "trigger": lambda s: s.resources["oxygen"] < 22,17 "announcement": "⚠️ OXYGEN PLANT FAILURE: Primary reactor offline. Reserves dropping.",18 "resource_delta": {"oxygen": -18.0},19 },20 {21 "id": "food_blight",22 "trigger": lambda s: random.random() < 0.006,23 "announcement": "🌱 GREENHOUSE BLIGHT: Fungal infection detected. Food output cut by 40%.",24 "resource_delta": {"food": -12.0},25 "building_effect": ("Greenhouse", "strained"),26 },27 {28 "id": "solar_storm",29 "trigger": lambda s: s.tick > 0 and s.tick % 143 == 0, # every ~2.75 years30 "announcement": "☀️ SOLAR STORM: All surface operations suspended. Energy and credits hit.",31 "resource_delta": {"energy": -22.0, "credits": -12.0},32 },33 {34 "id": "credit_crisis",35 "trigger": lambda s: s.resources["credits"] < 8,36 "announcement": "💳 CREDIT CRISIS: Colony economy collapsing. Trade suspended.",37 "resource_delta": {"credits": -5.0},38 },39]40 41# Breakthroughs are Deutsch-inspired: permanent knowledge gains from collective reasoning.42# Each has a title, description, which building it improves, and by how much.43BREAKTHROUGH_DEFS = [44 {45 "title": "Closed-Loop Oxygen Recycling",46 "description": "A researcher proposed that CO2 scrubbers could be re-integrated with "47 "the habitat's water system. The colony adopted the explanation and built it. "48 "Habitat oxygen efficiency improved permanently by 20%.",49 "building_name": "Habitat",50 "bonus": 0.20,51 "required_job": "researcher",52 },53 {54 "title": "Hydroponic Yield Optimization",55 "description": "By studying root zone temperature gradients, the farming team devised "56 "a new planting schedule. Food production from the Greenhouse improved "57 "permanently by 20%.",58 "building_name": "Greenhouse",59 "bonus": 0.20,60 "required_job": "farmer",61 },62 {63 "title": "Reactor Load Balancing",64 "description": "An engineer discovered that staggering equipment power cycles reduced "65 "peak draw by 25%. Energy system efficiency improved permanently.",66 "building_name": "Reactor",67 "bonus": 0.25,68 "required_job": "engineer",69 },70 {71 "title": "Cooperative Labor Networks",72 "description": "Colonists proposed rotating specializations during crises. "73 "All buildings temporarily gain resilience against future failures.",74 "building_name": "Habitat",75 "bonus": 0.10,76 "required_job": None, # any colonist can propose this77 },78]79 80 81def check_and_fire_crises(state: WorldState) -> list[str]:82 events = []83 for crisis in CRISIS_DEFS:84 if state.crisis_active == crisis["id"]:85 continue86 if crisis["trigger"](state):87 events.append(crisis["announcement"])88 for res, delta in crisis.get("resource_delta", {}).items():89 state.resources[res] = max(0.0, state.resources[res] + delta)90 if "building_effect" in crisis:91 bname, status = crisis["building_effect"]92 for b in state.buildings:93 if b.name == bname:94 b.status = status95 state.crisis_active = crisis["id"]96 state.crisis_duration_remaining = 1097 state.event_timeline.append(98 {99 "tick": state.tick,100 "year": state.year,101 "type": "crisis",102 "label": crisis["announcement"][:40],103 }104 )105 return events106 107 108def inject_manual_crisis(state: WorldState) -> str:109 """Called by the UI crisis button — randomly picks from all crisis types."""110 # Prefer crises not already active to avoid repetition111 available = [c for c in CRISIS_DEFS if state.crisis_active != c["id"]]112 if not available:113 available = list(CRISIS_DEFS)114 crisis = random.choice(available)115 for res, delta in crisis.get("resource_delta", {}).items():116 state.resources[res] = max(0.0, state.resources[res] + delta)117 if "building_effect" in crisis:118 bname, status = crisis["building_effect"]119 for b in state.buildings:120 if b.name == bname:121 b.status = status122 state.crisis_active = crisis["id"]123 state.crisis_duration_remaining = 10124 state.event_timeline.append(125 {126 "tick": state.tick,127 "year": state.year,128 "type": "crisis",129 "label": crisis["announcement"][:40],130 }131 )132 return crisis["announcement"]133 134 135def inject_optimism(state: WorldState) -> tuple[str, dict | None]:136 """137 Beginning of Infinity mechanic: inject a period of explanatory progress.138 Returns (announcement, breakthrough_dict_or_None).139 140 Philosophy: problems are inevitable, but so are solutions — given good explanations.141 The colony's knowledge grows permanently, not just its resources.142 """143 # Don't fire if already active144 if state.optimism_active:145 return "💡 A breakthrough is already underway.", None146 147 # Find an applicable breakthrough not yet discovered148 discovered_titles = {b.title for b in state.breakthroughs}149 available = [b for b in BREAKTHROUGH_DEFS if b["title"] not in discovered_titles]150 151 if not available:152 return (153 "🌟 The colony has achieved all known breakthroughs. A new era begins.",154 None,155 )156 157 # Prefer breakthroughs matching current colonist jobs158 living_jobs = {c.job for c in state.living}159 relevant = [160 b161 for b in available162 if b.get("required_job") in living_jobs or not b.get("required_job")163 ]164 chosen_def = random.choice(relevant or available)165 166 # Apply to the right building167 for building in state.buildings:168 if building.name == chosen_def["building_name"]:169 building.efficiency_bonus += chosen_def["bonus"]170 break171 172 bt = Breakthrough(173 year=state.year_int,174 title=chosen_def["title"],175 description=chosen_def["description"],176 building_id=chosen_def["building_name"],177 bonus=chosen_def["bonus"],178 )179 state.breakthroughs.append(bt)180 state.event_timeline.append(181 {182 "tick": state.tick,183 "year": state.year,184 "type": "breakthrough",185 "label": chosen_def["title"],186 }187 )188 state.optimism_active = True189 190 # Optimism fades after 8 ticks (2 months)191 state._optimism_ticks_remaining = 8192 193 announcement = (194 f"💡 EXPLANATORY BREAKTHROUGH: {chosen_def['title']}\n"195 f"{chosen_def['description']}\n"196 f"Production bonus: +{chosen_def['bonus'] * 100:.0f}% for {chosen_def['building_name']} permanently."197 )198 199 # Track proposer's notable action200 for c in state.living:201 if c.job == chosen_def.get("required_job") or not chosen_def.get(202 "required_job"203 ):204 c.add_notable_action(f"Year {state.year:.1f}: contributed to '{bt.title}'")205 break206 207 return announcement, bt208 209 210def tick_cultural_drift(state: WorldState) -> None:211 """212 Silently drift colonist traits each tick based on colony conditions.213 ~2% chance per living colonist per tick — creates gradual cultural evolution214 visible in the Cultural DNA bars without flooding the event feed.215 """216 if not state.living:217 return218 219 # Context-weighted trait pools220 scarce = (221 state.resources.get("oxygen", 100) < 25 or state.resources.get("food", 100) < 25222 )223 if scarce:224 drift_pool = ["stubborn", "fearful", "pragmatic", "resourceful", "risk-averse"]225 elif state.optimism_active:226 drift_pool = [227 "optimistic",228 "inventive",229 "cooperative",230 "ambitious",231 "scientific",232 ]233 elif state.crisis_active:234 drift_pool = ["risk-averse", "loyal", "pragmatic", "stubborn", "empathetic"]235 else:236 drift_pool = ALL_TRAITS # random drift in stable times237 238 for c in state.living:239 if random.random() > 0.02:240 continue241 candidates = [t for t in drift_pool if t not in c.traits]242 if not candidates:243 continue244 new_trait = random.choice(candidates)245 old_trait = random.choice(c.traits)246 c.traits = [new_trait if t == old_trait else t for t in c.traits]247 248 249def tick_optimism(state: WorldState) -> None:250 """Call each tick to manage optimism duration."""251 if state.optimism_active:252 remaining = getattr(state, "_optimism_ticks_remaining", 0) - 1253 state._optimism_ticks_remaining = remaining254 if remaining <= 0:255 state.optimism_active = False256 