File size: 6,961 Bytes
e328272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f5ffdc
e328272
 
 
 
 
 
 
 
 
5f5ffdc
 
 
 
e328272
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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,
    }