| from __future__ import annotations |
|
|
| import random |
|
|
| from world_simulator.config import GameConfig |
| from world_simulator.domain import ( |
| Beast, |
| CountryState, |
| House, |
| MemoryEntry, |
| Npc, |
| ResourceNode, |
| Terrain, |
| TreasuryState, |
| Vec3, |
| WorldState, |
| ) |
| from world_simulator.simulation.mechanics import BLOCK_SIZE, GRID_BLOCKS |
| from world_simulator.simulation.roles import normalize_role |
|
|
| _NAMES = [ |
| "Ada", |
| "Boris", |
| "Cora", |
| "Dima", |
| "Elena", |
| "Farid", |
| "Gita", |
| "Hana", |
| "Ivan", |
| "Juno", |
| "Kira", |
| "Lena", |
| "Mira", |
| "Niko", |
| "Oleg", |
| "Pavel", |
| "Raya", |
| "Sofia", |
| "Toma", |
| "Vera", |
| ] |
|
|
| _ROLES = ["citizen"] |
|
|
| _QWEN_NAMES = ["Kira", "Lena", "Mira", "Niko", "Oleg", "Pavel"] |
|
|
| def create_world(config: GameConfig) -> WorldState: |
| rng = random.Random(config.world.seed) |
| terrain = Terrain( |
| kind=config.world.terrain, |
| width=config.world.width, |
| depth=config.world.depth, |
| ) |
| village_npcs = [ |
| _spawn_npc(index=index, config=config, rng=rng, country_id="nemotron") |
| for index in range(config.npcs.count) |
| ] |
| qwen_npcs = _spawn_qwen_npcs(config, count=config.npcs.count) if config.world.survival else [] |
| houses = _spawn_houses(config) |
|
|
| if config.world.survival: |
| _seed_survival_relationships(village_npcs + qwen_npcs) |
| _assign_starting_homes(village_npcs + qwen_npcs, houses) |
| _initialize_importance(village_npcs + qwen_npcs) |
|
|
| all_npcs = village_npcs + qwen_npcs |
| population = sum(1 for npc in all_npcs if npc.health > 0) |
| world = WorldState( |
| tick=0, |
| seed=config.world.seed, |
| terrain=terrain, |
| npcs=all_npcs, |
| resource_nodes=_spawn_resource_nodes(config), |
| beasts=_spawn_beasts(config), |
| houses=houses, |
| countries=_spawn_countries(config, all_npcs), |
| population=population, |
| population_cap=max(24, population + 8), |
| peak_population=population, |
| overseer_mode="off", |
| overseer_cycle_ticks=0, |
| ) |
| _assign_initial_rulers(world) |
| return world |
|
|
|
|
| def _spawn_npc(*, index: int, config: GameConfig, rng: random.Random, country_id: str) -> Npc: |
| half_width = config.world.width / 2 |
| half_depth = config.world.depth / 2 |
|
|
| name = _NAMES[index % len(_NAMES)] |
| role = normalize_role(_ROLES[index % len(_ROLES)]) |
| if config.world.survival: |
| |
| village_x = -(half_width * 2 / 3) |
| ring = index % 6 |
| offsets = [ |
| (0.0, 0.0), |
| (1.2, 0.8), |
| (-1.0, -0.9), |
| (3.4, 0.0), |
| (-3.4, 0.0), |
| (0.0, 3.4), |
| ] |
| dx, z = offsets[ring] |
| x = village_x + dx |
| else: |
| x = _random_block_center(rng, half_width=half_width) |
| z = _random_block_center(rng, half_width=half_depth) |
|
|
| npc = Npc( |
| id=f"npc-{index + 1:03d}", |
| name=name, |
| role=role, |
| position=Vec3(x=x, y=0.0, z=z), |
| country_id=country_id, |
| attack_damage=rng.randint(5, 15), |
| memory=[MemoryEntry(tick=0, text=f"{name} arrived as a {role}.")], |
| age=60 if config.world.survival else 0, |
| max_age=_random_max_age(config.world.seed, index), |
| ) |
|
|
| if config.world.survival: |
| |
| |
| inv_rng = random.Random(config.world.seed + index) |
| npc.hunger = 0.0 if index == 0 else 20.0 + inv_rng.randint(0, 10) |
| npc.safety = 100.0 if index == 0 else 0.0 |
| npc.inventory_food = 2 if index == 0 else inv_rng.randint(0, 2) |
| npc.inventory_herbs = inv_rng.randint(0, 1) |
| |
| |
| npc.inventory_weapon = 0 |
| if index < 2: |
| npc.inventory_wood = 5 |
|
|
| return npc |
|
|
|
|
| def _spawn_resource_nodes(config: GameConfig) -> list[ResourceNode]: |
| if not config.world.survival: |
| return [] |
| rng = random.Random(f"{config.world.seed}:initial_resources") |
| |
| |
| counts = {"food": 4, "herbs": 2, "wood": 3} |
| max_amounts = {"food": 5, "herbs": 4, "wood": 6, "weapon": 2} |
| nodes: list[ResourceNode] = [] |
|
|
| village_x = -(config.world.width / 2 * 2 / 3) |
| village_food_offsets = [ |
| (2 * BLOCK_SIZE, BLOCK_SIZE), |
| (-2 * BLOCK_SIZE, 2 * BLOCK_SIZE), |
| (BLOCK_SIZE, -2 * BLOCK_SIZE), |
| ] |
| for i, (dx, dz) in enumerate(village_food_offsets, start=1): |
| nodes.append( |
| ResourceNode( |
| id=f"res_food_village_{i}", |
| resource_type="food", |
| position=Vec3(x=round(village_x + dx, 3), y=0.0, z=round(dz, 3)), |
| amount=5, |
| max_amount=5, |
| ) |
| ) |
|
|
| for resource_type, count in counts.items(): |
| for index in range(1, count + 1): |
| max_amount = max_amounts[resource_type] |
| nodes.append( |
| ResourceNode( |
| id=f"res_{resource_type}_{index}", |
| resource_type=resource_type, |
| position=_random_world_position( |
| rng, |
| half_width=config.world.width / 2, |
| half_depth=config.world.depth / 2, |
| ), |
| amount=rng.randint(max(1, max_amount // 2), max_amount), |
| max_amount=max_amount, |
| ) |
| ) |
|
|
| |
| |
| qwen_x = config.world.width / 2 * 2 / 3 |
| weapon_max = max_amounts["weapon"] |
| weapon_z = -2 * BLOCK_SIZE |
| for index, base_x in enumerate((-qwen_x, qwen_x), start=1): |
| nodes.append( |
| ResourceNode( |
| id=f"res_weapon_{index}", |
| resource_type="weapon", |
| position=Vec3(x=round(base_x, 3), y=0.0, z=round(weapon_z, 3)), |
| amount=weapon_max, |
| max_amount=weapon_max, |
| ) |
| ) |
| return nodes |
|
|
|
|
| def _spawn_beasts(config: GameConfig) -> list[Beast]: |
| if not config.world.survival: |
| return [] |
| |
| |
| edge_x = config.world.width / 2 - BLOCK_SIZE |
| return [ |
| Beast( |
| "beast_1", |
| Vec3(x=edge_x, y=0.0, z=0.0), |
| health=60.0, |
| damage=9.0, |
| ), |
| Beast( |
| "beast_2", |
| Vec3(x=-edge_x, y=0.0, z=0.0), |
| health=60.0, |
| damage=9.0, |
| ), |
| ] |
|
|
|
|
| def _spawn_houses(config: GameConfig) -> list[House]: |
| if not config.world.survival: |
| return [] |
| village_x = -(config.world.width / 2 * 2 / 3) |
| qwen_x = config.world.width / 2 * 2 / 3 |
| return [ |
| House( |
| id="house_nemotron_001", |
| position=Vec3(x=village_x, y=0.0, z=0.0), |
| hp=60.0, |
| max_hp=60.0, |
| state="completed", |
| build_progress=10, |
| capacity=3, |
| ), |
| House( |
| id="house_qwen_001", |
| position=Vec3(x=qwen_x, y=0.0, z=0.0), |
| hp=60.0, |
| max_hp=60.0, |
| state="completed", |
| build_progress=10, |
| capacity=3, |
| ), |
| ] |
|
|
|
|
| def _seed_survival_relationships(npcs: list[Npc]) -> None: |
| for npc in npcs: |
| for other in npcs: |
| if other.id == npc.id: |
| continue |
| npc.relationships[other.id] = 0.45 |
|
|
|
|
| def _assign_starting_homes(npcs: list[Npc], houses: list[House]) -> None: |
| if not houses: |
| return |
| for npc in npcs: |
| home = min(houses, key=lambda house: abs(house.position.x - npc.position.x)) |
| npc.home_house_id = home.id |
| for house in houses: |
| citizens = [npc.id for npc in npcs if npc.home_house_id == house.id] |
| house.occupant_ids = citizens[: house.capacity] |
|
|
|
|
| def _initialize_importance(npcs: list[Npc]) -> None: |
| counts: dict[str, int] = {} |
| for npc in npcs: |
| role = normalize_role(npc.role) |
| counts[role] = counts.get(role, 0) + 1 |
| for npc in npcs: |
| role_count = max(1, counts[normalize_role(npc.role)]) |
| npc.importance = round(1.0 + 2.0 * (1 / role_count), 3) |
|
|
|
|
| def _spawn_qwen_npcs(config: GameConfig, *, count: int) -> list[Npc]: |
| half_width = config.world.width / 2 |
| qwen_x = half_width * 2 / 3 |
| offsets = [ |
| (0.0, 0.0), |
| (BLOCK_SIZE, 0.0), |
| (-BLOCK_SIZE, 0.0), |
| (0.0, BLOCK_SIZE), |
| (BLOCK_SIZE, BLOCK_SIZE), |
| (-BLOCK_SIZE, BLOCK_SIZE), |
| ] |
| npcs = [] |
| inv_rng = random.Random(f"{config.world.seed}:qwen_inventory") |
| for i in range(count): |
| dx, dz = offsets[i % len(offsets)] |
| name = _QWEN_NAMES[i % len(_QWEN_NAMES)] |
| npc = Npc( |
| id=f"qwen-{i + 1:03d}", |
| name=name, |
| role="citizen", |
| position=Vec3(x=round(qwen_x + dx, 3), y=0.0, z=round(dz, 3)), |
| attack_damage=inv_rng.randint(5, 12), |
| memory=[MemoryEntry(tick=0, text=f"{name} joined Qwen as a free spirit.")], |
| age=25, |
| max_age=550, |
| unrestricted_actions=True, |
| connector_id="qwen", |
| country_id="qwen", |
| personality="curious and resourceful", |
| hunger=15.0, |
| safety=60.0, |
| inventory_food=inv_rng.randint(1, 3), |
| inventory_herbs=inv_rng.randint(0, 1), |
| ) |
| npcs.append(npc) |
| return npcs |
|
|
|
|
| def _spawn_countries(config: GameConfig, npcs: list[Npc]) -> list[CountryState]: |
| if not config.world.survival: |
| return [] |
| half_width = config.world.width / 2 |
| nemotron_x = -(half_width * 2 / 3) |
| qwen_x = half_width * 2 / 3 |
| citizens_by_country = { |
| "nemotron": [npc.id for npc in npcs if npc.country_id == "nemotron"], |
| "qwen": [npc.id for npc in npcs if npc.country_id == "qwen"], |
| } |
| return [ |
| CountryState( |
| id="nemotron", |
| name="Nemotron", |
| color="#4f8cff", |
| badge="N", |
| citizen_ids=citizens_by_country["nemotron"], |
| treasury=TreasuryState( |
| id="treasury_nemotron", |
| position=Vec3(x=nemotron_x, y=0.0, z=BLOCK_SIZE * 1.5), |
| resources={"food": 2, "herbs": 1, "wood": 20, "coins": 25}, |
| ), |
| ), |
| CountryState( |
| id="qwen", |
| name="Qwen", |
| color="#d86bff", |
| badge="Q", |
| citizen_ids=citizens_by_country["qwen"], |
| treasury=TreasuryState( |
| id="treasury_qwen", |
| position=Vec3(x=qwen_x, y=0.0, z=BLOCK_SIZE * 1.5), |
| resources={"food": 2, "herbs": 1, "wood": 20, "coins": 25}, |
| ), |
| ), |
| ] |
|
|
|
|
| def _assign_initial_rulers(world: WorldState) -> None: |
| for country in world.countries: |
| ruler = next((npc for npc in world.living_npcs() if npc.id in country.citizen_ids), None) |
| if ruler is None: |
| continue |
| country.ruler_id = ruler.id |
| ruler.special_status = "ruler" |
|
|
|
|
| def _random_max_age(seed: int, index: int) -> int: |
| return random.Random(f"{seed}:{index}:max_age").randint(320, 480) |
|
|
|
|
| def _random_block_center(rng: random.Random, *, half_width: float) -> float: |
| first_center = -half_width + (BLOCK_SIZE / 2) |
| num_blocks = round(half_width * 2 / BLOCK_SIZE) |
| return first_center + (rng.randrange(num_blocks) * BLOCK_SIZE) |
|
|
|
|
| def _random_world_position( |
| rng: random.Random, |
| *, |
| half_width: float, |
| half_depth: float, |
| ) -> Vec3: |
| return Vec3( |
| x=round(rng.uniform(-half_width + BLOCK_SIZE, half_width - BLOCK_SIZE), 3), |
| y=0.0, |
| z=round(rng.uniform(-half_depth + BLOCK_SIZE, half_depth - BLOCK_SIZE), 3), |
| ) |
|
|