Spaces:
Runtime error
Runtime error
| 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 is anchored on the left side of the map. | |
| 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: | |
| # Independent per-NPC RNG so survival inventory never perturbs the shared | |
| # position/damage stream (keeps non-survival spawning deterministic). | |
| 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) | |
| # Weapons stay scarce: citizens start unarmed and must gather them from the | |
| # (limited) weapon nodes during play. | |
| 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") | |
| # Weapons are NOT placed randomly: one node is mirrored near each base so both | |
| # nations have equal, but still limited, access to arming up (see below). | |
| 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, | |
| ) | |
| ) | |
| # One weapon node per flank, mirrored across the x-axis so neither nation is | |
| # favoured. Offset off the base centre so it does not sit on the home/treasury. | |
| 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 [] | |
| # One beast on each flank so both nations face an equal threat: beast_1 near | |
| # the Qwen side (+x), beast_2 mirrored near the Nemotron side (-x). | |
| 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), | |
| ) | |