RFTSystems's picture
Update app.py
deab6c0 verified
# 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)