# app.py — Conscious Agent Civilization with Bootstrap Densification + Presets + Provenance import time import threading import numpy as np import gradio as gr from collections import deque import hashlib import json import os from datetime import datetime # ========================= # Globals # ========================= ENGINE_LOCK = threading.Lock() SIM = None RUNNING = False PROVENANCE_LOG = deque(maxlen=32) PROVENANCE_FILE = "provenance_log.jsonl" RUN_ID_COUNTER = 0 # ========================= # Entity codes and colors # ========================= ( EMPTY, MALE, FEMALE, BABY, WORKER, BUILDING, HOUSE, SHOP, ROAD, FOREST, FOOD, TEACHER, GUILD, SCHOOL, TOP_BUILDER, TOP_GATHERER, CITY_CENTER ) = range(17) COLORS = { EMPTY: (255, 255, 255), MALE: (30, 144, 255), # builders FEMALE: (255, 105, 180), # gatherers BABY: (255, 235, 59), WORKER: (150, 150, 255), BUILDING: (128, 128, 128), HOUSE: (180, 180, 180), SHOP: (255, 165, 0), ROAD: (0, 0, 0), FOREST: (34, 139, 34), FOOD: (144, 238, 144), TEACHER: (128, 0, 128), GUILD: (102, 51, 153), SCHOOL: (0, 191, 255), TOP_BUILDER: (0, 0, 200), TOP_GATHERER: (200, 0, 0), CITY_CENTER: (255, 0, 255) # magenta } # ========================= # Parameters # ========================= class SimParams: def __init__( self, width=64, height=64, male_init=90, female_init=90, teacher_init=3, forest_fraction=0.10, food_fraction=0.06, baby_age_ticks=8, tick_rate=6, top_builders=8, top_gatherers=8, city_scan_interval=50, # ticks between city detection city_cluster_radius=2, # half-size window city_building_threshold=6, # threshold to seed city road_connect_interval=100 # ticks between city road connections ): self.width = int(width) self.height = int(height) self.male_init = int(male_init) self.female_init = int(female_init) self.teacher_init = int(teacher_init) self.forest_fraction = float(forest_fraction) self.food_fraction = float(food_fraction) self.baby_age_ticks = int(baby_age_ticks) self.tick_rate = int(tick_rate) self.top_builders = int(top_builders) self.top_gatherers = int(top_gatherers) self.city_scan_interval = int(city_scan_interval) self.city_cluster_radius = int(city_cluster_radius) self.city_building_threshold = int(city_building_threshold) self.road_connect_interval = int(road_connect_interval) # ========================= # Provenance logging # ========================= def log_run(params: SimParams, seed): global RUN_ID_COUNTER, PROVENANCE_LOG RUN_ID_COUNTER += 1 base_record = { "run_id": RUN_ID_COUNTER, "timestamp_utc": datetime.utcnow().isoformat(timespec="seconds") + "Z", "seed": int(seed), "width": params.width, "height": params.height, "male_init": params.male_init, "female_init": params.female_init, "teacher_init": params.teacher_init, "forest_fraction": params.forest_fraction, "food_fraction": params.food_fraction, "baby_age_ticks": params.baby_age_ticks, "tick_rate": params.tick_rate, "top_builders": params.top_builders, "top_gatherers": params.top_gatherers, "city_scan_interval": params.city_scan_interval, "city_cluster_radius": params.city_cluster_radius, "city_building_threshold": params.city_building_threshold, "road_connect_interval": params.road_connect_interval, } payload = json.dumps(base_record, sort_keys=True) sha = hashlib.sha512(payload.encode("utf-8")).hexdigest() record = dict(base_record) record["sha512"] = sha PROVENANCE_LOG.append(record) try: with open(PROVENANCE_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(record) + "\n") except Exception: pass def get_provenance_summary(): if not PROVENANCE_LOG: return "No runs logged yet.", None lines = [] for r in list(PROVENANCE_LOG)[-12:][::-1]: line = ( f"#{r['run_id']} | seed={r['seed']} | " f"size={r['width']}x{r['height']} | " f"m={r['male_init']}, f={r['female_init']}, T={r['teacher_init']} | " f"tick_rate={r['tick_rate']} | " f"sha512={r['sha512'][:12]}..." ) lines.append(line) text = "\n".join(lines) file_path = PROVENANCE_FILE if os.path.exists(PROVENANCE_FILE) else None return text, file_path # ========================= # Civilization simulation # ========================= class Civilization: def __init__(self, params: SimParams, seed=7): self.params = params self.rng = np.random.default_rng(int(seed)) H, W = params.height, params.width self.grid = np.full((H, W), EMPTY, dtype=np.uint8) self.age = np.zeros_like(self.grid, dtype=np.uint16) # age for babies/structures self.food = np.zeros_like(self.grid, dtype=np.float32) # local food intensity (at cell) self.tick = 0 # City state self.city_ids = np.full((H, W), -1, dtype=np.int32) # -1 = none; else city id self.city_registry = {} # id -> dict with center, members self.next_city_id = 0 # Seed terrain 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[food_mask] = 1.0 # Seed population empties = np.argwhere(self.grid == EMPTY) self.rng.shuffle(empties) idx = 0 for (y, x) in empties[idx:idx + params.male_init]: self.grid[y, x] = MALE idx += params.male_init for (y, x) in empties[idx:idx + params.female_init]: self.grid[y, x] = FEMALE idx += params.female_init # Seed teachers for (y, x) in empties[idx:idx + params.teacher_init]: self.grid[y, x] = TEACHER idx += params.teacher_init # Seed top agents empties2 = np.argwhere(self.grid == EMPTY) self.rng.shuffle(empties2) tb = min(params.top_builders, len(empties2)) tg = min(params.top_gatherers, max(0, len(empties2) - tb)) for (y, x) in empties2[:tb]: self.grid[y, x] = TOP_BUILDER for (y, x) in empties2[tb:tb + tg]: self.grid[y, x] = TOP_GATHERER 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 yield (y + dy) % H, (x + dx) % W def _any_food_nearby(self, y, x): if self.food[y, x] > 0: return True for ny, nx in self._neighbors8(y, x): if self.food[ny, nx] > 0: return True return False def _global_counts(self): g = self.grid return { "roads": int(np.sum(g == ROAD)), "houses": int(np.sum(g == HOUSE)), "shops": int(np.sum(g == SHOP)), "buildings": int(np.sum(g == BUILDING)), "foods": int(np.sum(g == FOOD)), "forests": int(np.sum(g == FOREST)), "cities": int(np.sum(g == CITY_CENTER)), } # -------- City detection and road connection -------- def detect_cities(self): r = self.params.city_cluster_radius H, W = self.grid.shape new_centers = [] # scan for local building/builder clusters for y in range(H): y0, y1 = max(0, y - r), min(H, y + r + 1) for x in range(W): x0, x1 = max(0, x - r), min(W, x + r + 1) window = self.grid[y0:y1, x0:x1] bcount = np.sum( (window == BUILDING) | (window == SHOP) | (window == HOUSE) | (window == MALE) | (window == TOP_BUILDER) ) if bcount >= self.params.city_building_threshold and self.grid[y, x] != CITY_CENTER: new_centers.append((y, x)) # register centers for (y, x) in new_centers: cid = self.next_city_id self.next_city_id += 1 self.grid[y, x] = CITY_CENTER self.city_ids[y, x] = cid self.city_registry[cid] = { "center": (y, x), "members": set([(y, x)]), "last_connected": -9999 } # assign nearby cells to city id for influence (simple radius-based membership) for cid, meta in self.city_registry.items(): cy, cx = meta["center"] y0, y1 = max(0, cy - r), min(H, cy + r + 1) x0, x1 = max(0, cx - r), min(W, cx + r + 1) region = [(yy, xx) for yy in range(y0, y1) for xx in range(x0, x1)] for yy, xx in region: self.city_ids[yy, xx] = cid meta["members"].add((yy, xx)) def _bfs_path(self, start, goal): # BFS on toroidal grid, preferring straight paths H, W = self.grid.shape sy, sx = start gy, gx = goal q = deque([(sy, sx)]) prev = {(sy, sx): None} visited = set([(sy, sx)]) def neighbors(y, x): # 4-neighborhood to keep roads straighter return [ ((y - 1) % H, x), ((y + 1) % H, x), (y, (x - 1) % W), (y, (x + 1) % W), ] while q: y, x = q.popleft() if (y, x) == (gy, gx): # reconstruct path = [] cur = (gy, gx) while cur is not None: path.append(cur) cur = prev[cur] path.reverse() return path for ny, nx in neighbors(y, x): if (ny, nx) not in visited: visited.add((ny, nx)) prev[(ny, nx)] = (y, x) q.append((ny, nx)) return [] # should rarely happen def connect_city_roads(self): # connect nearest pairs periodically centers = [(cid, meta["center"]) for cid, meta in self.city_registry.items()] if len(centers) < 2: return for i in range(len(centers)): cid_a, (ay, ax) = centers[i] # find nearest other center d_best, j_best = 1e9, None for j in range(len(centers)): if i == j: continue cid_b, (by, bx) = centers[j] d = abs(ay - by) + abs(ax - bx) if d < d_best: d_best, j_best = d, j if j_best is None: continue cid_b, (by, bx) = centers[j_best] # throttle connections last_a = self.city_registry[cid_a]["last_connected"] last_b = self.city_registry[cid_b]["last_connected"] if (self.tick - last_a) < self.params.road_connect_interval and (self.tick - last_b) < self.params.road_connect_interval: continue path = self._bfs_path((ay, ax), (by, bx)) for (y, x) in path: if self.grid[y, x] in (EMPTY, FOREST, FOOD): self.grid[y, x] = ROAD self.age[y, x] = 0 self.city_registry[cid_a]["last_connected"] = self.tick self.city_registry[cid_b]["last_connected"] = self.tick # -------- Main step -------- def step(self): H, W = self.grid.shape new_grid = self.grid.copy() new_age = self.age.copy() new_food = self.food.copy() # Terrain regen and food diffusion-lite new_food[self.grid == FOREST] += 0.01 new_food[self.grid == FOOD] += 0.005 new_food = np.clip(new_food, 0.0, 5.0) # Global counts & bootstrap gate counts = self._global_counts() need_bootstrap = (counts["houses"] < 50) or (counts["shops"] < 20) or (counts["buildings"] < 5) allow_road_widen = counts["houses"] >= 30 # hold road expansion until some housing exists # Population consumption (Law of Sustenance) pop_mask = (self.grid == MALE) | (self.grid == FEMALE) | (self.grid == BABY) new_food[pop_mask] -= 0.01 new_food = np.clip(new_food, 0.0, 5.0) # Momentum safety valves (disable structure decay during bootstrap) base_mortality_p = 0.0005 base_decay_p = 0.0004 mortality_p = base_mortality_p decay_p = 0.0 if need_bootstrap else base_decay_p # City mechanics: detection and inter-city roads if self.tick % self.params.city_scan_interval == 0: self.detect_cities() if self.tick % self.params.road_connect_interval == 0: self.connect_city_roads() 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 = [self.grid[ny, nx] for ny, nx in nb] candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY] # Mortality opens space if cell in (MALE, FEMALE, BABY) and self.rng.random() < mortality_p: new_grid[y, x] = EMPTY new_age[y, x] = 0 continue # Infrastructure decay opens space if cell in (ROAD, HOUSE, SHOP, BUILDING, SCHOOL) and self.rng.random() < decay_p: new_grid[y, x] = EMPTY new_age[y, x] = 0 continue # LAW: Sustenance — must eat or gather if cell in (MALE, FEMALE, BABY) and not self._any_food_nearby(y, x): # Prefer turning adjacent forest into food; else plant food did_food = False for ny, nx in nb: if self.grid[ny, nx] == FOREST: new_grid[ny, nx] = FOOD new_food[ny, nx] += 0.7 new_age[ny, nx] = 0 did_food = True break if not did_food and candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = FOOD new_food[ty, tx] += 0.7 new_age[ty, tx] = 0 # Reproduction with environment bonus if cell in (MALE, FEMALE): if (MALE in nb_cells) and (FEMALE in nb_cells): env_bonus = 0.01 * sum(1 for c in nb_cells if c in (FOOD, FOREST, ROAD, HOUSE, SHOP, BUILDING)) p_baby = min(0.03 + env_bonus, 0.25) if candidates and self.rng.random() < p_baby: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = BABY new_age[ty, tx] = 0 # Babies age into adults elif cell == BABY: new_age[y, x] = self.age[y, x] + 1 if new_age[y, x] >= self.params.baby_age_ticks: # Bias babies near cities to spawn adults that support city growth if self.city_ids[y, x] >= 0 and self.rng.random() < 0.6: new_grid[y, x] = MALE if self.rng.random() < 0.55 else FEMALE else: new_grid[y, x] = MALE if self.rng.random() < 0.5 else FEMALE new_age[y, x] = 0 # Builders (men): prioritize bootstrap densification, then city growth and connections elif cell == MALE: upgraded = False # Bootstrap: must place structures first until minimum stock exists if need_bootstrap and candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] target = self.rng.choice([HOUSE, HOUSE, SHOP]) # bias to HOUSE new_grid[ty, tx] = target new_age[ty, tx] = 0 upgraded = True else: # City-aware priorities cid = self.city_ids[y, x] if cid >= 0 and candidates and self.rng.random() < 0.6: # densify around city: build houses/shops/buildings ty, tx = candidates[self.rng.integers(0, len(candidates))] target = self.rng.choice([HOUSE, SHOP, BUILDING]) new_grid[ty, tx] = target new_age[ty, tx] = 0 upgraded = True # Upgrade structures for ny, nx in nb: ncell = self.grid[ny, nx] if ncell == HOUSE and self.rng.random() < 0.08: new_grid[ny, nx] = SHOP new_age[ny, nx] = 0 upgraded = True elif ncell == SHOP and self.rng.random() < 0.07: new_grid[ny, nx] = BUILDING new_age[ny, nx] = 0 upgraded = True # Widen roads (only after some housing exists) if allow_road_widen and candidates and any(self.grid[ny, nx] == ROAD for ny, nx in nb): if self.rng.random() < 0.12: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = ROAD new_age[ty, tx] = 0 upgraded = True # Build new roads toward nearest city center if not in any if not upgraded and cid < 0 and candidates and self.city_registry: centers = [meta["center"] for meta in self.city_registry.values()] cy, cx = min(centers, key=lambda c: abs(c[0]-y)+abs(c[1]-x)) ty, tx = min(candidates, key=lambda p: abs(p[0]-cy)+abs(p[1]-cx)) new_grid[ty, tx] = ROAD new_age[ty, tx] = 0 upgraded = True # If nothing else, general infrastructure if not upgraded and candidates: target = self.rng.choice([ROAD, HOUSE, SHOP]) ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = target new_age[ty, tx] = 0 # Light forest clearing for ny, nx in nb: if self.grid[ny, nx] == FOREST and self.rng.random() < 0.02: new_grid[ny, nx] = EMPTY new_age[ny, nx] = 0 # Gatherers (women): renewal cycle — harvest and replant elif cell == FEMALE: converted = False for ny, nx in nb: if self.grid[ny, nx] == FOREST: new_grid[ny, nx] = FOOD new_food[ny, nx] += 0.6 new_age[ny, nx] = 0 converted = True break if candidates: # replant forest to maintain balance ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = FOREST new_age[ty, tx] = 0 if not converted and candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = FOOD new_food[ty, tx] += 0.5 new_age[ty, tx] = 0 # Teachers: spawn schools conservatively elif cell == TEACHER: if candidates and self.rng.random() < 0.003: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = SCHOOL new_age[ty, tx] = 0 # Top agents elif cell == TOP_BUILDER: # Bootstrap: elite guarantees density if need_bootstrap and candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = self.rng.choice([HOUSE, SHOP, BUILDING]) new_age[ty, tx] = 0 for ny, nx in nb: ncell = self.grid[ny, nx] if ncell == HOUSE: new_grid[ny, nx] = SHOP new_age[ny, nx] = 0 elif ncell == SHOP: new_grid[ny, nx] = BUILDING new_age[ny, nx] = 0 else: # City-first densification; else push roads cid = self.city_ids[y, x] if cid >= 0 and candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = self.rng.choice([HOUSE, SHOP, BUILDING]) new_age[ty, tx] = 0 elif allow_road_widen and candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = ROAD new_age[ty, tx] = 0 # Opportunistic upgrades for ny, nx in nb: ncell = self.grid[ny, nx] if ncell == HOUSE: new_grid[ny, nx] = SHOP new_age[ny, nx] = 0 elif ncell == SHOP: new_grid[ny, nx] = BUILDING new_age[ny, nx] = 0 elif cell == TOP_GATHERER: if candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] target = self.rng.choice([FOOD, FOREST]) new_grid[ty, tx] = target new_age[ty, tx] = 0 if target == FOOD: new_food[ty, tx] += 0.6 for ny, nx in nb: if self.grid[ny, nx] == FOREST: new_grid[ny, nx] = FOOD new_food[ny, nx] += 0.6 new_age[ny, nx] = 0 if candidates: ty, tx = candidates[self.rng.integers(0, len(candidates))] new_grid[ty, tx] = FOREST new_age[ty, tx] = 0 # Age structures if cell in (ROAD, HOUSE, SHOP, BUILDING, SCHOOL, CITY_CENTER): new_age[y, x] = self.age[y, x] + 1 # Commit step self.grid = new_grid self.age = new_age self.food = new_food self.tick += 1 def render_image(self, scale=4): H, W = self.grid.shape img = np.zeros((H, W, 3), dtype=np.uint8) for code, color in COLORS.items(): mask = (self.grid == code) if np.any(mask): img[mask] = np.array(color, dtype=np.uint8) return np.repeat(np.repeat(img, scale, axis=0), scale, axis=1) # ========================= # Engine controls # ========================= def start_engine(width, height, seed, male_init, female_init, teacher_init, forest_fraction, food_fraction, baby_age_ticks, tick_rate, top_builders, top_gatherers, city_scan_interval, city_cluster_radius, city_building_threshold, road_connect_interval): global SIM, RUNNING 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, tick_rate=tick_rate, top_builders=top_builders, top_gatherers=top_gatherers, city_scan_interval=city_scan_interval, city_cluster_radius=city_cluster_radius, city_building_threshold=city_building_threshold, road_connect_interval=road_connect_interval ) with ENGINE_LOCK: SIM = Civilization(params, seed=seed) RUNNING = True log_run(params, seed) def loop(): global SIM, RUNNING sleep_s = max(0.05, 1.0 / max(1, int(tick_rate))) while RUNNING: with ENGINE_LOCK: if SIM is not None: SIM.step() time.sleep(sleep_s) threading.Thread(target=loop, daemon=True).start() return f"Engine started {int(width)}x{int(height)} | tick_rate={int(tick_rate)}/s" def stop_engine(): global RUNNING RUNNING = False return "Engine stopped." def reset_engine(): stop_engine() globals()["SIM"] = None return "Reset complete." def compute_stability(pop, houses, shops, buildings, foods, schools, teachers): capacity = houses * 2 + shops * 3 + buildings * 4 + schools * 2 + foods * 1 if pop <= 0: return 0.0, "Empty" base_ratio = capacity / float(pop) stability = max(0.0, min(1.0, base_ratio / 4.0)) teacher_bonus = 1.0 + 0.03 * min(teachers, 20) stability *= teacher_bonus stability = max(0.0, min(1.0, stability)) if stability >= 0.7: label = "Stable" elif stability >= 0.4: label = "At Risk" else: label = "Fragile" return stability, label def get_grid(scale): with ENGINE_LOCK: if SIM is None: return None, "Engine not running." img = SIM.render_image(scale=int(scale)) g = SIM.grid counts = SIM._global_counts() pop = int(np.sum(np.isin(g, [MALE, FEMALE, BABY]))) roads = counts["roads"] houses = counts["houses"] shops = counts["shops"] buildings = counts["buildings"] foods = counts["foods"] forests = counts["forests"] schools = int(np.sum(g == SCHOOL)) top_b = int(np.sum(g == TOP_BUILDER)) top_g = int(np.sum(g == TOP_GATHERER)) cities = counts["cities"] teachers = int(np.sum(g == TEACHER)) guilds = int(np.sum(g == GUILD)) bootstrap = "ON" if (houses < 50 or shops < 20 or buildings < 5) else "OFF" stability, stability_label = compute_stability(pop, houses, shops, buildings, foods, schools, teachers) stats = ( f"Tick={SIM.tick} | Pop={pop} | Stability={stability:.2f} ({stability_label}) | " f"Teachers={teachers} | Guilds={guilds} | Roads={roads} | Houses={houses} | Shops={shops} | " f"Buildings={buildings} | FoodCells={foods} | Forests={forests} | Schools={schools} | " f"TopBuilders={top_b} | TopGatherers={top_g} | Cities={cities} | Bootstrap={bootstrap}" ) return img, stats # ========================= # Preset scenarios # ========================= def apply_preset(preset_name, width, height, seed, tick_rate, male_init, female_init, teacher_init, forest_fraction, food_fraction, baby_age_ticks, top_builders, top_gatherers, city_scan_interval, city_cluster_radius, city_building_threshold, road_connect_interval): # Start from current values w = width h = height s = seed tr = tick_rate m = male_init f = female_init t = teacher_init ff = forest_fraction fd = food_fraction bat = baby_age_ticks tb = top_builders tg = top_gatherers csi = city_scan_interval ccr = city_cluster_radius cbt = city_building_threshold rci = road_connect_interval if preset_name == "Balanced City Growth": w, h = 64, 64 s = 7 tr = 6 m, f, t = 120, 120, 3 ff, fd = 0.10, 0.06 bat = 8 tb, tg = 8, 8 csi, ccr, cbt, rci = 50, 2, 6, 100 elif preset_name == "No Teachers (Instinct Only)": w, h = 64, 64 s = 11 tr = 6 m, f, t = 140, 140, 0 ff, fd = 0.12, 0.06 bat = 8 tb, tg = 8, 8 csi, ccr, cbt, rci = 50, 2, 6, 100 elif preset_name == "Teacher Surge (Guidance Heavy)": w, h = 64, 64 s = 21 tr = 6 m, f, t = 110, 110, 18 ff, fd = 0.10, 0.06 bat = 8 tb, tg = 10, 10 csi, ccr, cbt, rci = 40, 2, 5, 80 elif preset_name == "Resource Shock (Sparse Food)": w, h = 64, 64 s = 33 tr = 6 m, f, t = 120, 120, 4 ff, fd = 0.06, 0.02 bat = 8 tb, tg = 8, 8 csi, ccr, cbt, rci = 60, 2, 6, 120 elif preset_name == "Dense Grid (Urban Pressure)": w, h = 96, 96 s = 5 tr = 8 m, f, t = 260, 260, 6 ff, fd = 0.08, 0.05 bat = 8 tb, tg = 12, 12 csi, ccr, cbt, rci = 40, 3, 8, 80 return ( int(w), int(h), int(s), int(tr), int(m), int(f), int(t), float(ff), float(fd), int(bat), int(tb), int(tg), int(csi), int(ccr), int(cbt), int(rci), ) # ========================= # Gradio UI # ========================= with gr.Blocks(title="Conscious Agent Civilization — Bootstrap Densification") as demo: gr.Markdown("# Conscious Agent Civilization — Bootstrap Densification, Cities, and Roads") with gr.Row(): width = gr.Slider(32, 256, value=64, step=16, label="Grid width") height = gr.Slider(32, 256, value=64, step=16, label="Grid height") seed = gr.Number(value=7, label="Seed", precision=0) tick_rate = gr.Slider(1, 30, value=6, step=1, label="Ticks per second") with gr.Row(): male_init = gr.Slider(10, 400, value=120, step=10, label="Initial males") female_init = gr.Slider(10, 400, value=120, step=10, label="Initial females") teacher_init = gr.Slider(0, 50, value=3, step=1, label="Initial teachers") forest_fraction = gr.Slider(0.0, 0.6, value=0.10, step=0.02, label="Forest fraction") food_fraction = gr.Slider(0.0, 0.3, value=0.06, step=0.01, label="Food fraction") baby_age_ticks = gr.Slider(4, 24, value=8, step=1, label="Baby→Adult ticks") with gr.Row(): top_builders = gr.Slider(0, 50, value=8, step=1, label="Top trained builders") top_gatherers = gr.Slider(0, 50, value=8, step=1, label="Top trained gatherers") with gr.Row(): preset = gr.Dropdown( ["Custom / manual", "Balanced City Growth", "No Teachers (Instinct Only)", "Teacher Surge (Guidance Heavy)", "Resource Shock (Sparse Food)", "Dense Grid (Urban Pressure)"], value="Custom / manual", label="Scenario preset" ) with gr.Accordion("City mechanics", open=True): city_scan_interval = gr.Slider(10, 500, value=50, step=10, label="City scan interval (ticks)") city_cluster_radius = gr.Slider(1, 6, value=2, step=1, label="City cluster radius") city_building_threshold = gr.Slider(3, 30, value=6, step=1, label="City building threshold") road_connect_interval = gr.Slider(20, 1000, value=100, step=20, label="Road connect interval (ticks)") run_btn = gr.Button("Run simulation", variant="primary") stop_btn = gr.Button("Stop") reset_btn = gr.Button("Reset") with gr.Accordion("Live grid", open=True): scale = gr.Slider(1, 8, value=4, step=1, label="Display scale") img_out = gr.Image(type="numpy", label="Grid") stats_out = gr.Textbox(label="Stats", lines=3) refresh_btn = gr.Button("Refresh grid") with gr.Accordion("Run provenance (config + SHA-512)", open=False): prov_text = gr.Textbox(label="Recent runs", lines=8) prov_file = gr.File(label="Provenance log (.jsonl)") prov_refresh = gr.Button("Refresh provenance") # Wiring run_btn.click( start_engine, inputs=[ width, height, seed, male_init, female_init, teacher_init, forest_fraction, food_fraction, baby_age_ticks, tick_rate, top_builders, top_gatherers, city_scan_interval, city_cluster_radius, city_building_threshold, road_connect_interval ], outputs=[] ) stop_btn.click(stop_engine, inputs=[], outputs=[]) reset_btn.click(reset_engine, inputs=[], outputs=[]) refresh_btn.click(get_grid, inputs=[scale], outputs=[img_out, stats_out]) demo.load(get_grid, inputs=[scale], outputs=[img_out, stats_out]) preset.change( apply_preset, inputs=[ preset, width, height, seed, tick_rate, male_init, female_init, teacher_init, forest_fraction, food_fraction, baby_age_ticks, top_builders, top_gatherers, city_scan_interval, city_cluster_radius, city_building_threshold, road_connect_interval ], outputs=[ width, height, seed, tick_rate, male_init, female_init, teacher_init, forest_fraction, food_fraction, baby_age_ticks, top_builders, top_gatherers, city_scan_interval, city_cluster_radius, city_building_threshold, road_connect_interval ] ) prov_refresh.click( get_provenance_summary, inputs=[], outputs=[prov_text, prov_file] ) demo.load( get_provenance_summary, inputs=[], outputs=[prov_text, prov_file] ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)