| """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 |
|
|
|
|
| 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."] |
|
|