from __future__ import annotations from dataclasses import asdict import numpy as np from ..config import WorldConfig from ..domain.person import Person from ..domain.resources import ResourcePool from ..domain.soul import Soul from ..domain.world import World def _clamp(x: float, lo: float, hi: float) -> float: return lo if x < lo else hi if x > hi else x def _make_initial_world(cfg: WorldConfig) -> World: rng = np.random.default_rng(cfg.seed) souls = {i: Soul(soul_id=i, karma=0.0) for i in range(cfg.num_souls)} pool = ResourcePool( capacity=float(cfg.resource_capacity), amount=float(cfg.resource_start), replenish_rate=float(cfg.resource_replenish_rate), ) moral_bias = rng.normal(cfg.initial_moral_bias_mean, cfg.initial_moral_bias_std, size=cfg.num_souls) moral_bias = np.clip(moral_bias, -1.0, 1.0) max_ages = rng.integers(cfg.min_max_age, cfg.max_max_age + 1, size=cfg.num_souls) people: list[Person] = [] for i in range(cfg.num_souls): people.append( Person( soul_id=i, age=int(rng.integers(0, 18)), max_age=int(max_ages[i]), health=float(rng.uniform(0.7, 1.0)), wellbeing=float(rng.uniform(0.4, 0.9)), moral_bias=float(moral_bias[i]), consumption_need=float(cfg.baseline_consumption_need), alive=True, ) ) return World(time=0, resource_pool=pool, people=people, souls=souls) def _rebirth_person(cfg: WorldConfig, rng: np.random.Generator, soul: Soul) -> Person: # Karma nudges next-life moral bias (simple, V1). bias_shift = _clamp(cfg.rebirth_influence_strength * np.tanh(soul.karma / 10.0), -0.5, 0.5) moral_bias = float(np.clip(rng.normal(cfg.initial_moral_bias_mean + bias_shift, cfg.initial_moral_bias_std), -1.0, 1.0)) max_age = int(rng.integers(cfg.min_max_age, cfg.max_max_age + 1)) return Person( soul_id=soul.soul_id, age=0, max_age=max_age, health=float(rng.uniform(0.7, 1.0)), wellbeing=float(rng.uniform(0.4, 0.9)), moral_bias=moral_bias, consumption_need=float(cfg.baseline_consumption_need), alive=True, ) def step_world(world: World, cfg: WorldConfig, rng: np.random.Generator) -> dict[str, float]: world.resource_pool.replenish() alive_people = [p for p in world.people if p.alive] n_alive = len(alive_people) # If everyone died (shouldn’t happen with fixed souls, but keep it robust): if n_alive == 0: return { "time": float(world.time), "alive": 0.0, "resource": float(world.resource_pool.amount), "mean_karma": float(np.mean([s.karma for s in world.souls.values()])), "mean_wellbeing": 0.0, "mean_health": 0.0, "events": 0.0, } # “Good” people share a bit; “bad” people hoard a bit (very simple V1). # This indirectly affects average consumption outcomes under scarcity. share_pool = 0.0 events_count = 0 # First pass: take resources for baseline needs (plus hoarding). for p in alive_people: hoard_factor = max(0.0, -p.moral_bias) * 0.5 # bad bias → hoard requested = p.consumption_need * (1.0 + hoard_factor) taken = world.resource_pool.take(requested) # Surplus slightly increases wellbeing. surplus = max(0.0, taken - p.consumption_need) p.wellbeing = _clamp(p.wellbeing + cfg.wellbeing_from_surplus * surplus, 0.0, 1.0) # Shortfall hurts health and wellbeing. shortfall = max(0.0, p.consumption_need - taken) if shortfall > 0: p.health = _clamp(p.health - cfg.starvation_health_penalty * shortfall, 0.0, 1.0) p.wellbeing = _clamp(p.wellbeing - 0.05 * shortfall, 0.0, 1.0) # Good bias contributes a bit to a share pool. share_contrib = max(0.0, p.moral_bias) * 0.1 * taken share_pool += share_contrib # Karma update: good actions add karma, harmful/hoarding tendencies reduce. world.souls[p.soul_id].karma += 0.02 * p.moral_bias # Random life event. if rng.random() < cfg.event_rate: events_count += 1 # Events can be good or bad; moral bias slightly changes expected impact. sign = 1.0 if rng.random() < (0.5 + 0.15 * p.moral_bias) else -1.0 p.health = _clamp(p.health + sign * cfg.event_health_impact * rng.random(), 0.0, 1.0) p.wellbeing = _clamp(p.wellbeing + sign * cfg.event_wellbeing_impact * rng.random(), 0.0, 1.0) world.souls[p.soul_id].karma += 0.05 * sign * (0.5 + 0.5 * p.moral_bias) # Second pass: redistribute share_pool evenly (very simple V1 “social safety net”). if share_pool > 0 and n_alive > 0: per = share_pool / n_alive for p in alive_people: # Add wellbeing from shared support; keep it modest. p.wellbeing = _clamp(p.wellbeing + 0.01 * per, 0.0, 1.0) # Aging and death, then rebirth. for p in alive_people: p.age += 1 # natural aging p.health = _clamp(p.health - 0.005, 0.0, 1.0) if p.age >= p.max_age or p.health <= 0.0: p.alive = False # Rebirth: replace dead persons with newborns for the same souls. # Keep population size constant. new_people: list[Person] = [] for p in world.people: if p.alive: new_people.append(p) else: soul = world.souls[p.soul_id] if cfg.karma_decay_per_tick > 0: soul.karma *= (1.0 - cfg.karma_decay_per_tick) new_people.append(_rebirth_person(cfg, rng, soul)) world.people = new_people world.time += 1 karma = np.array([s.karma for s in world.souls.values()], dtype=float) wellbeing = np.array([p.wellbeing for p in world.people], dtype=float) health = np.array([p.health for p in world.people], dtype=float) return { "time": float(world.time), "alive": float(len([p for p in world.people if p.alive])), "resource": float(world.resource_pool.amount), "mean_karma": float(np.mean(karma)), "mean_wellbeing": float(np.mean(wellbeing)), "mean_health": float(np.mean(health)), "events": float(events_count), "collapsed": len(alive_people) == 0 or world.resource_pool.amount <= 0 or abs(np.mean(karma)) < 1e-6 } def run_simulation(cfg: WorldConfig) -> dict[str, object]: rng = np.random.default_rng(cfg.seed) world = _make_initial_world(cfg) series: list[dict[str, float]] = [] for _ in range(int(cfg.ticks)): step_data = step_world(world, cfg, rng) series.append(step_data) if step_data.get("collapsed"): break return { "config": asdict(cfg), "series": series, }