"""Deterministic Chaos tools (God Console quick-buttons). Unlike the freeform LLM god console, these apply engine-validated effects directly: beasts actually spawn, famine actually halves food nodes, and the maniac directive flows through the existing hostile-directive pipeline. Cooldowns (in ticks) follow GAME_DESIGN.md §11. """ from __future__ import annotations import random from world_simulator.domain import Beast, WorldState from world_simulator.simulation.directives import set_active_directive from world_simulator.simulation.mechanics import is_alive from world_simulator.simulation.survival import ( _log_event, _next_beast_index, _random_map_edge_position, ) CHAOS_COOLDOWNS: dict[str, int] = { "spawn_beast": 20, "beast_pack": 80, "famine": 120, "maniac": 100, } CHAOS_TOOL_LABELS: dict[str, str] = { "spawn_beast": "Spawn Beast", "beast_pack": "Beast Pack", "famine": "Famine", "maniac": "Maniac", } FAMINE_DURATION_TICKS = 180 MANIAC_DIRECTIVE = ( "Madness consumes you. Attack the nearest villager — ignore work, ignore friends." ) CHAOS_BEAST_CAP = 6 # God Console may exceed MAX_BEASTS (3) up to this. def apply_chaos_action(world: WorldState, action: str) -> list[str]: """Apply one chaos quick-button. Returns human-readable applied lines.""" if action == "spawn_beast": applied = _spawn_chaos_beasts(world, count=1) elif action == "beast_pack": applied = _spawn_chaos_beasts(world, count=3) elif action == "famine": applied = _apply_famine(world) elif action == "maniac": applied = _apply_maniac(world) else: raise ValueError(f"Unknown chaos action: {action}") if applied: world.chaos_intervention_until = max( world.chaos_intervention_until, world.tick + 100 ) return applied def _spawn_chaos_beasts(world: WorldState, *, count: int) -> list[str]: living = sum(1 for beast in world.beasts if beast.state != "dead") allowed = max(0, min(count, CHAOS_BEAST_CAP - living)) if allowed == 0: return [] rng = random.Random(f"{world.seed}:{world.tick}:chaos_beast") applied: list[str] = [] for _ in range(allowed): beast = Beast( id=f"beast_{_next_beast_index(world)}", position=_random_map_edge_position(world, rng), health=120.0, damage=15.0, ) world.beasts.append(beast) _log_event( world, "chaos_event", f"Chaos unleashed {beast.id} at the edge of the map", severity="danger", actor_id=beast.id, ) applied.append(f"{beast.id} unleashed at the map edge.") return applied def _apply_famine(world: WorldState) -> list[str]: if world.famine_until >= world.tick: return [] affected = 0 for node in world.resource_nodes: if node.resource_type != "food": continue world.famine_saved_max[node.id] = node.max_amount node.amount //= 2 node.max_amount = max(1, node.max_amount // 2) affected += 1 if affected == 0: return [] world.famine_until = world.tick + FAMINE_DURATION_TICKS _log_event( world, "chaos_event", f"Famine grips the land: {affected} food nodes halved for " f"{FAMINE_DURATION_TICKS} ticks", severity="danger", ) return [f"Famine: {affected} food nodes halved for {FAMINE_DURATION_TICKS} ticks."] def end_famine_if_due(world: WorldState) -> None: """Restore food node caps once the famine duration elapses.""" if world.famine_until < 0 or world.tick <= world.famine_until: return for node in world.resource_nodes: saved = world.famine_saved_max.get(node.id) if saved is not None: node.max_amount = saved world.famine_saved_max.clear() world.famine_until = -1 _log_event(world, "chaos_event", "The famine has ended", severity="neutral") def _apply_maniac(world: WorldState) -> list[str]: candidates = [npc for npc in world.living_npcs() if is_alive(npc)] if not candidates: return [] rng = random.Random(f"{world.seed}:{world.tick}:chaos_maniac") victim = rng.choice(candidates) set_active_directive( victim, MANIAC_DIRECTIVE, source="chaos:maniac", tick=world.tick, ttl_ticks=60, ) victim.personality = "violent maniac" _log_event( world, "chaos_event", f"{victim.name} has gone mad and turned on the village!", severity="danger", actor_id=victim.id, ) return [f"{victim.name} has gone mad and turned on the village."]