RFTSystems's picture
Create app.py
eb32092 verified
raw
history blame
18.5 kB
import gradio as gr
import numpy as np
import time
import json
import hashlib
from dataclasses import dataclass
# =========================
# Artifact metadata
# =========================
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()
# =========================
# Color palette (RGB)
# =========================
COLORS = {
"EMPTY": (255, 255, 255), # white
"MALE": (30, 144, 255), # blue
"FEMALE": (255, 105, 180), # pink
"BABY": (255, 235, 59), # yellow
"WORKER": (150, 150, 255), # light blue
"BUILDING": (128, 128, 128), # gray
"SHOP": (255, 165, 0), # orange
"ROAD": (0, 0, 0), # black
"FOREST": (34, 139, 34), # green
"FOOD": (144, 238, 144), # lime
"TEACHER": (128, 0, 128), # purple
"GUILD": (102, 51, 153), # gray-purple (school)
}
# =========================
# Entity codes
# =========================
EMPTY, MALE, FEMALE, BABY, WORKER, BUILDING, SHOP, ROAD, FOREST, FOOD, TEACHER, GUILD = range(12)
# =========================
# Parameters & state
# =========================
@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
# Seed forests and food
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
# Seed buildings
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
# Seed shops near buildings
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
# Seed males & females
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
# Seed teacher agents
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):
# Collapse pressure: low food + high density; teachers reduce pressure by raising override willingness
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:
# 20% chance to choose override even if base threshold not met
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()
# regenerate forests slightly
forest_regen = (self.grid == FOREST)
new_food[forest_regen] += 0.01
# consume food by population
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)
# Conscious override decision
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 # teachers boost pairing
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
# Optional: teacher-led immediate shelter/food placement under override
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:
# ageing into worker (babies near teachers age faster)
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:
# workers build roads, buildings, or harvest food
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:
# shops emerge near buildings over time
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:
# convert surplus food to prosperity -> attracts agents
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:
# harvesting creates food slots; excessive pressure reduces 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:
# roads extend toward population centers
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:
# Teachers can spawn guild buildings
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:
# Guilds stabilize local economy
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
# Guilds also increase chance of buildings emerging nearby
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
# starvation collapse (if local food is zero and high density)
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
):
# Initialize the sim
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)
# Stats tracking
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):
# Advance a few internal ticks per UI update for visible growth
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)
# Seal & log at the end of the stream
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
# =========================
# Gradio UI (single screen)
# =========================
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()