| import gradio as gr |
| import numpy as np |
| import time |
| import json |
| import hashlib |
| from dataclasses import dataclass |
|
|
| |
| |
| |
| ARTIFACT = "RFT_Conscious_Civilization_Simulator_Teachers_v1" |
| AUTHOR = "Liam Grinstead" |
| REGISTRY = "Codex_Consciousness + Civilization Ledger" |
|
|
| def sha512_str(s: str) -> str: |
| return hashlib.sha512(s.encode("utf-8")).hexdigest() |
|
|
| |
| |
| |
| COLORS = { |
| "EMPTY": (255, 255, 255), |
| "MALE": (30, 144, 255), |
| "FEMALE": (255, 105, 180), |
| "BABY": (255, 235, 59), |
| "WORKER": (150, 150, 255), |
| "BUILDING": (128, 128, 128), |
| "SHOP": (255, 165, 0), |
| "ROAD": (0, 0, 0), |
| "FOREST": (34, 139, 34), |
| "FOOD": (144, 238, 144), |
| "TEACHER": (128, 0, 128), |
| "GUILD": (102, 51, 153), |
| } |
|
|
| |
| |
| |
| EMPTY, MALE, FEMALE, BABY, WORKER, BUILDING, SHOP, ROAD, FOREST, FOOD, TEACHER, GUILD = range(12) |
|
|
| |
| |
| |
| @dataclass |
| class SimParams: |
| width: int = 64 |
| height: int = 64 |
| male_init: int = 120 |
| female_init: int = 120 |
| teacher_init: int = 6 |
| forest_fraction: float = 0.20 |
| food_fraction: float = 0.05 |
| shop_ratio: float = 0.06 |
| building_seed: int = 6 |
| road_bias: float = 0.40 |
| baby_age_ticks: int = 8 |
| pair_prob_base: float = 0.02 |
| worker_productivity: float = 0.25 |
| food_need_per_capita: float = 0.005 |
| override_sensitivity: float = 0.60 |
| teacher_radius: int = 6 |
|
|
| class Civilization: |
| def __init__(self, params: SimParams, seed: int = 7): |
| self.params = params |
| self.rng = np.random.default_rng(seed) |
| self.grid = np.full((params.height, params.width), EMPTY, dtype=np.uint8) |
| self.age_grid = np.zeros_like(self.grid, dtype=np.uint16) |
| self.food_stock = np.zeros_like(self.grid, dtype=np.float32) |
| self.tick = 0 |
|
|
| |
| forest_mask = self.rng.random(self.grid.shape) < params.forest_fraction |
| self.grid[forest_mask] = FOREST |
| food_mask = (self.rng.random(self.grid.shape) < params.food_fraction) & (self.grid == EMPTY) |
| self.grid[food_mask] = FOOD |
| self.food_stock[food_mask] = 1.0 |
|
|
| |
| for _ in range(params.building_seed): |
| y = self.rng.integers(0, params.height) |
| x = self.rng.integers(0, params.width) |
| self.grid[y, x] = BUILDING |
|
|
| |
| shop_spots = np.argwhere(self.grid == BUILDING) |
| for (y, x) in shop_spots[:max(1, int(len(shop_spots) * params.shop_ratio))]: |
| for ny, nx in self._neighbors8(y, x): |
| if self.grid[ny, nx] == EMPTY and self.rng.random() < 0.5: |
| self.grid[ny, nx] = SHOP |
|
|
| |
| empties = np.argwhere(self.grid == EMPTY) |
| self.rng.shuffle(empties) |
| for (y, x) in empties[:params.male_init]: |
| self.grid[y, x] = MALE |
| for (y, x) in empties[params.male_init:params.male_init + params.female_init]: |
| self.grid[y, x] = FEMALE |
|
|
| |
| remaining = [tuple(p) for p in empties[params.male_init + params.female_init:]] |
| self.rng.shuffle(remaining) |
| for (y, x) in remaining[:params.teacher_init]: |
| self.grid[y, x] = TEACHER |
|
|
| def _neighbors8(self, y, x): |
| H, W = self.grid.shape |
| for dy in (-1, 0, 1): |
| for dx in (-1, 0, 1): |
| if dy == 0 and dx == 0: |
| continue |
| ny, nx = (y + dy) % H, (x + dx) % W |
| yield ny, nx |
|
|
| def _majority(self, cells): |
| vals, counts = np.unique(cells, return_counts=True) |
| return int(vals[np.argmax(counts)]) |
|
|
| def teacher_influence(self, y, x, radius=None): |
| if radius is None: |
| radius = self.params.teacher_radius |
| H, W = self.grid.shape |
| y0, y1 = max(0, y - radius), min(H, y + radius + 1) |
| x0, x1 = max(0, x - radius), min(W, x + radius + 1) |
| block = self.grid[y0:y1, x0:x1] |
| return int(np.sum((block == TEACHER) | (block == GUILD))) |
|
|
| def conscious_override(self, local_food, local_density, local_buildings, teacher_influence): |
| |
| pressure = (1.0 - min(1.0, local_food)) * 0.6 + local_density * 0.4 |
| base = pressure > self.params.override_sensitivity or (local_buildings == 0 and local_density > 0.5) |
| if teacher_influence > 0: |
| |
| base = base or (self.rng.random() < 0.2) |
| return base |
|
|
| def step(self): |
| H, W = self.grid.shape |
| new_grid = self.grid.copy() |
| new_age = self.age_grid.copy() |
| new_food = self.food_stock.copy() |
|
|
| |
| forest_regen = (self.grid == FOREST) |
| new_food[forest_regen] += 0.01 |
|
|
| |
| pop_mask = (self.grid == MALE) | (self.grid == FEMALE) | (self.grid == BABY) | (self.grid == WORKER) |
| new_food[pop_mask] -= self.params.food_need_per_capita |
| new_food = np.clip(new_food, 0.0, 5.0) |
|
|
| for y in range(H): |
| for x in range(W): |
| cell = self.grid[y, x] |
| nb = [(ny, nx) for ny, nx in self._neighbors8(y, x)] |
| nb_cells = np.array([self.grid[ny, nx] for ny, nx in nb], dtype=np.uint8) |
| nb_food = float(np.mean([self.food_stock[ny, nx] for ny, nx in nb])) |
| nb_density = float(np.mean([1.0 if self.grid[ny, nx] in (MALE, FEMALE, BABY, WORKER) else 0.0 for ny, nx in nb])) |
| nb_buildings = int(np.sum(nb_cells == BUILDING)) |
| teacher_inf = self.teacher_influence(y, x) |
|
|
| |
| override = self.conscious_override(local_food=nb_food, local_density=nb_density, local_buildings=nb_buildings, teacher_influence=teacher_inf) |
|
|
| if cell in (MALE, FEMALE): |
| has_pair = (MALE in nb_cells and FEMALE in nb_cells) |
| pair_prob = self.params.pair_prob_base * (1.0 + min(1.0, nb_food)) * (1.0 + 0.25 * nb_buildings) |
| if teacher_inf > 0: |
| pair_prob *= 1.5 |
| if has_pair and self.rng.random() < pair_prob: |
| empties = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if empties: |
| ty, tx = empties[self.rng.integers(0, len(empties))] |
| new_grid[ty, tx] = BABY |
| new_age[ty, tx] = 0 |
| |
| if override and self.rng.random() < 0.12: |
| target_type = FOOD if nb_food < 0.5 else BUILDING |
| candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if candidates: |
| ty, tx = candidates[self.rng.integers(0, len(candidates))] |
| new_grid[ty, tx] = target_type |
| if target_type == FOOD: |
| new_food[ty, tx] += 0.4 |
|
|
| elif cell == BABY: |
| |
| inc = 1 + (1 if teacher_inf > 0 else 0) |
| new_age[y, x] = self.age_grid[y, x] + inc |
| if new_age[y, x] >= self.params.baby_age_ticks: |
| new_grid[y, x] = WORKER |
|
|
| elif cell == WORKER: |
| |
| choice = self.rng.random() |
| if override: |
| if nb_food < 0.5 and choice < 0.6: |
| new_grid[y, x] = FOOD |
| new_food[y, x] += 0.3 |
| elif choice < 0.9: |
| new_grid[y, x] = ROAD |
| else: |
| new_grid[y, x] = BUILDING |
| else: |
| if choice < 0.33: |
| new_grid[y, x] = BUILDING |
| elif choice < 0.66: |
| new_grid[y, x] = ROAD |
| else: |
| new_grid[y, x] = FOOD |
| new_food[y, x] += 0.2 |
|
|
| elif cell == BUILDING: |
| |
| if self.rng.random() < 0.02: |
| candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if candidates: |
| ty, tx = candidates[self.rng.integers(0, len(candidates))] |
| new_grid[ty, tx] = SHOP |
|
|
| elif cell == SHOP: |
| |
| if nb_food > 0.8 and self.rng.random() < 0.05: |
| candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if candidates: |
| ty, tx = candidates[self.rng.integers(0, len(candidates))] |
| new_grid[ty, tx] = MALE if self.rng.random() < 0.5 else FEMALE |
|
|
| elif cell == FOREST: |
| |
| if nb_density > 0.4 and nb_food < 0.5 and self.rng.random() < 0.06: |
| new_grid[y, x] = FOOD |
| new_food[y, x] += 0.4 |
|
|
| elif cell == ROAD: |
| |
| if self.rng.random() < self.params.road_bias: |
| tgt = self._majority(nb_cells) |
| if tgt in (BUILDING, SHOP, MALE, FEMALE, WORKER, BABY) and self.rng.random() < 0.4: |
| candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if candidates: |
| ty, tx = candidates[self.rng.integers(0, len(candidates))] |
| new_grid[ty, tx] = ROAD |
|
|
| elif cell == TEACHER: |
| |
| if self.rng.random() < 0.01: |
| candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if candidates: |
| ty, tx = candidates[self.rng.integers(0, len(candidates))] |
| new_grid[ty, tx] = GUILD |
|
|
| elif cell == GUILD: |
| |
| if nb_food < 0.5 and self.rng.random() < 0.05: |
| candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if candidates: |
| ty, tx = candidates[self.rng.integers(0, len(candidates))] |
| new_grid[ty, tx] = FOOD |
| new_food[ty, tx] += 0.5 |
| |
| if self.rng.random() < 0.02: |
| candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] |
| if candidates: |
| ty, tx = candidates[self.rng.integers(0, len(candidates))] |
| new_grid[ty, tx] = BUILDING |
|
|
| |
| if (cell in (MALE, FEMALE, BABY, WORKER)) and nb_food <= 0.0 and nb_density > 0.6 and self.rng.random() < 0.05: |
| new_grid[y, x] = EMPTY |
|
|
| self.grid = new_grid |
| self.age_grid = new_age |
| self.food_stock = new_food |
| self.tick += 1 |
|
|
| def render_image(self): |
| H, W = self.grid.shape |
| img = np.zeros((H, W, 3), dtype=np.uint8) |
| mapping = { |
| EMPTY: "EMPTY", MALE: "MALE", FEMALE: "FEMALE", BABY: "BABY", WORKER: "WORKER", |
| BUILDING: "BUILDING", SHOP: "SHOP", ROAD: "ROAD", FOREST: "FOREST", FOOD: "FOOD", |
| TEACHER: "TEACHER", GUILD: "GUILD" |
| } |
| for val, name in mapping.items(): |
| rgb = COLORS[name] |
| mask = (self.grid == val) |
| img[mask] = np.array(rgb, dtype=np.uint8) |
| return img |
|
|
| def stream_sim( |
| width=64, height=64, seed=7, |
| male_init=120, female_init=120, teacher_init=6, |
| forest_fraction=0.20, food_fraction=0.05, |
| baby_age_ticks=8, override_sensitivity=0.60, |
| update_interval_sec=10, total_minutes=2 |
| ): |
| |
| params = SimParams( |
| width=width, height=height, |
| male_init=male_init, female_init=female_init, teacher_init=teacher_init, |
| forest_fraction=forest_fraction, food_fraction=food_fraction, |
| baby_age_ticks=baby_age_ticks, override_sensitivity=override_sensitivity |
| ) |
| sim = Civilization(params, seed=seed) |
|
|
| |
| pop_series, build_series, road_series, shop_series, food_series = [], [], [], [], [] |
| total_ticks = int((60 // max(1, update_interval_sec)) * total_minutes) |
|
|
| legend = "Blue=Male, Pink=Female, Yellow=Baby, LightBlue=Worker, Gray=Building, Orange=Shop, Black=Road, Green=Forest, Lime=Food, Purple=Teacher, Gray-Purple=Guild" |
|
|
| for _ in range(total_ticks): |
| |
| inner_ticks = max(1, update_interval_sec // 2) |
| for __ in range(inner_ticks): |
| sim.step() |
|
|
| img = sim.render_image() |
| g = sim.grid |
| pop_series.append(int(np.sum(np.isin(g, [MALE, FEMALE, BABY, WORKER])))) |
| build_series.append(int(np.sum(g == BUILDING))) |
| road_series.append(int(np.sum(g == ROAD))) |
| shop_series.append(int(np.sum(g == SHOP))) |
| food_series.append(int(np.sum(g == FOOD))) |
|
|
| text = ( |
| f"Tick={sim.tick} | Pop={pop_series[-1]} | Buildings={build_series[-1]} | Roads={road_series[-1]} | Shops={shop_series[-1]} | FoodCells={food_series[-1]}\n" |
| f"Artifact={ARTIFACT} | Author={AUTHOR}\n" |
| f"Legend: {legend}" |
| ) |
| yield img, text |
| time.sleep(update_interval_sec) |
|
|
| |
| summary = { |
| "tick_final": sim.tick, |
| "pop_final": pop_series[-1] if pop_series else 0, |
| "buildings_final": build_series[-1] if build_series else 0, |
| "roads_final": road_series[-1] if road_series else 0, |
| "shops_final": shop_series[-1] if shop_series else 0, |
| "food_cells_final": food_series[-1] if food_series else 0, |
| "width": int(width), "height": int(height), |
| "teacher_init": int(teacher_init), |
| "update_interval_sec": int(update_interval_sec), |
| "total_minutes": int(total_minutes), |
| } |
| seal = sha512_str(json.dumps(summary, sort_keys=True)) |
| prov = { |
| "artifact": ARTIFACT, |
| "author": AUTHOR, |
| "registry": REGISTRY, |
| "timestamp": int(time.time()), |
| "params": { |
| "width": width, "height": height, "seed": seed, |
| "male_init": male_init, "female_init": female_init, "teacher_init": teacher_init, |
| "forest_fraction": forest_fraction, "food_fraction": food_fraction, |
| "baby_age_ticks": baby_age_ticks, "override_sensitivity": override_sensitivity, |
| "update_interval_sec": update_interval_sec, "total_minutes": total_minutes |
| }, |
| "summary": summary, |
| "sha512": seal |
| } |
| try: |
| with open("provenance.jsonl", "a") as f: |
| f.write(json.dumps(prov) + "\n") |
| except Exception: |
| pass |
|
|
| |
| |
| |
| with gr.Blocks(title="RFT Conscious Civilization Simulator (Teachers)") as demo: |
| gr.Markdown( |
| "# RFT Conscious Civilization Simulator\n" |
| "Single-screen live grid. Conscious math rules (male+female=baby → worker → building/road/food → shops) grow villages into cities.\n" |
| "Purple Teacher agents guide life, spawn guilds, and stabilize growth.\n" |
| "Authored by Liam Grinstead. Each session is sealed for falsifiability." |
| ) |
|
|
| with gr.Row(): |
| width = gr.Slider(32, 128, value=64, step=8, label="Grid width") |
| height = gr.Slider(32, 128, value=64, step=8, label="Grid height") |
| seed = gr.Number(value=7, label="Seed", precision=0) |
|
|
| with gr.Row(): |
| male_init = gr.Slider(20, 300, value=120, step=10, label="Initial males") |
| female_init = gr.Slider(20, 300, value=120, step=10, label="Initial females") |
| teacher_init = gr.Slider(1, 30, value=6, step=1, label="Initial teachers") |
|
|
| with gr.Row(): |
| forest_fraction = gr.Slider(0.0, 0.6, value=0.20, step=0.02, label="Forest fraction") |
| food_fraction = gr.Slider(0.0, 0.3, value=0.05, step=0.01, label="Food fraction") |
| baby_age_ticks = gr.Slider(4, 20, value=8, step=1, label="Baby→Worker ticks") |
| override_sensitivity = gr.Slider(0.3, 0.9, value=0.60, step=0.05, label="Conscious override sensitivity") |
|
|
| with gr.Row(): |
| update_interval_sec = gr.Slider(2, 20, value=10, step=1, label="Update cadence (seconds)") |
| total_minutes = gr.Slider(1, 10, value=2, step=1, label="Session duration (minutes)") |
|
|
| start_btn = gr.Button("Start live civilization") |
| img_out = gr.Image(type="numpy", label="Live grid", streaming=True) |
| text_out = gr.Textbox(label="Summary (streaming)", lines=4) |
|
|
| start_btn.click( |
| stream_sim, |
| inputs=[width, height, seed, male_init, female_init, teacher_init, forest_fraction, food_fraction, baby_age_ticks, override_sensitivity, update_interval_sec, total_minutes], |
| outputs=[img_out, text_out] |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|