RFTSystems commited on
Commit
eb32092
·
verified ·
1 Parent(s): 2a0c641

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +412 -0
app.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import time
4
+ import json
5
+ import hashlib
6
+ from dataclasses import dataclass
7
+
8
+ # =========================
9
+ # Artifact metadata
10
+ # =========================
11
+ ARTIFACT = "RFT_Conscious_Civilization_Simulator_Teachers_v1"
12
+ AUTHOR = "Liam Grinstead"
13
+ REGISTRY = "Codex_Consciousness + Civilization Ledger"
14
+
15
+ def sha512_str(s: str) -> str:
16
+ return hashlib.sha512(s.encode("utf-8")).hexdigest()
17
+
18
+ # =========================
19
+ # Color palette (RGB)
20
+ # =========================
21
+ COLORS = {
22
+ "EMPTY": (255, 255, 255), # white
23
+ "MALE": (30, 144, 255), # blue
24
+ "FEMALE": (255, 105, 180), # pink
25
+ "BABY": (255, 235, 59), # yellow
26
+ "WORKER": (150, 150, 255), # light blue
27
+ "BUILDING": (128, 128, 128), # gray
28
+ "SHOP": (255, 165, 0), # orange
29
+ "ROAD": (0, 0, 0), # black
30
+ "FOREST": (34, 139, 34), # green
31
+ "FOOD": (144, 238, 144), # lime
32
+ "TEACHER": (128, 0, 128), # purple
33
+ "GUILD": (102, 51, 153), # gray-purple (school)
34
+ }
35
+
36
+ # =========================
37
+ # Entity codes
38
+ # =========================
39
+ EMPTY, MALE, FEMALE, BABY, WORKER, BUILDING, SHOP, ROAD, FOREST, FOOD, TEACHER, GUILD = range(12)
40
+
41
+ # =========================
42
+ # Parameters & state
43
+ # =========================
44
+ @dataclass
45
+ class SimParams:
46
+ width: int = 64
47
+ height: int = 64
48
+ male_init: int = 120
49
+ female_init: int = 120
50
+ teacher_init: int = 6
51
+ forest_fraction: float = 0.20
52
+ food_fraction: float = 0.05
53
+ shop_ratio: float = 0.06
54
+ building_seed: int = 6
55
+ road_bias: float = 0.40
56
+ baby_age_ticks: int = 8
57
+ pair_prob_base: float = 0.02
58
+ worker_productivity: float = 0.25
59
+ food_need_per_capita: float = 0.005
60
+ override_sensitivity: float = 0.60
61
+ teacher_radius: int = 6
62
+
63
+ class Civilization:
64
+ def __init__(self, params: SimParams, seed: int = 7):
65
+ self.params = params
66
+ self.rng = np.random.default_rng(seed)
67
+ self.grid = np.full((params.height, params.width), EMPTY, dtype=np.uint8)
68
+ self.age_grid = np.zeros_like(self.grid, dtype=np.uint16)
69
+ self.food_stock = np.zeros_like(self.grid, dtype=np.float32)
70
+ self.tick = 0
71
+
72
+ # Seed forests and food
73
+ forest_mask = self.rng.random(self.grid.shape) < params.forest_fraction
74
+ self.grid[forest_mask] = FOREST
75
+ food_mask = (self.rng.random(self.grid.shape) < params.food_fraction) & (self.grid == EMPTY)
76
+ self.grid[food_mask] = FOOD
77
+ self.food_stock[food_mask] = 1.0
78
+
79
+ # Seed buildings
80
+ for _ in range(params.building_seed):
81
+ y = self.rng.integers(0, params.height)
82
+ x = self.rng.integers(0, params.width)
83
+ self.grid[y, x] = BUILDING
84
+
85
+ # Seed shops near buildings
86
+ shop_spots = np.argwhere(self.grid == BUILDING)
87
+ for (y, x) in shop_spots[:max(1, int(len(shop_spots) * params.shop_ratio))]:
88
+ for ny, nx in self._neighbors8(y, x):
89
+ if self.grid[ny, nx] == EMPTY and self.rng.random() < 0.5:
90
+ self.grid[ny, nx] = SHOP
91
+
92
+ # Seed males & females
93
+ empties = np.argwhere(self.grid == EMPTY)
94
+ self.rng.shuffle(empties)
95
+ for (y, x) in empties[:params.male_init]:
96
+ self.grid[y, x] = MALE
97
+ for (y, x) in empties[params.male_init:params.male_init + params.female_init]:
98
+ self.grid[y, x] = FEMALE
99
+
100
+ # Seed teacher agents
101
+ remaining = [tuple(p) for p in empties[params.male_init + params.female_init:]]
102
+ self.rng.shuffle(remaining)
103
+ for (y, x) in remaining[:params.teacher_init]:
104
+ self.grid[y, x] = TEACHER
105
+
106
+ def _neighbors8(self, y, x):
107
+ H, W = self.grid.shape
108
+ for dy in (-1, 0, 1):
109
+ for dx in (-1, 0, 1):
110
+ if dy == 0 and dx == 0:
111
+ continue
112
+ ny, nx = (y + dy) % H, (x + dx) % W
113
+ yield ny, nx
114
+
115
+ def _majority(self, cells):
116
+ vals, counts = np.unique(cells, return_counts=True)
117
+ return int(vals[np.argmax(counts)])
118
+
119
+ def teacher_influence(self, y, x, radius=None):
120
+ if radius is None:
121
+ radius = self.params.teacher_radius
122
+ H, W = self.grid.shape
123
+ y0, y1 = max(0, y - radius), min(H, y + radius + 1)
124
+ x0, x1 = max(0, x - radius), min(W, x + radius + 1)
125
+ block = self.grid[y0:y1, x0:x1]
126
+ return int(np.sum((block == TEACHER) | (block == GUILD)))
127
+
128
+ def conscious_override(self, local_food, local_density, local_buildings, teacher_influence):
129
+ # Collapse pressure: low food + high density; teachers reduce pressure by raising override willingness
130
+ pressure = (1.0 - min(1.0, local_food)) * 0.6 + local_density * 0.4
131
+ base = pressure > self.params.override_sensitivity or (local_buildings == 0 and local_density > 0.5)
132
+ if teacher_influence > 0:
133
+ # 20% chance to choose override even if base threshold not met
134
+ base = base or (self.rng.random() < 0.2)
135
+ return base
136
+
137
+ def step(self):
138
+ H, W = self.grid.shape
139
+ new_grid = self.grid.copy()
140
+ new_age = self.age_grid.copy()
141
+ new_food = self.food_stock.copy()
142
+
143
+ # regenerate forests slightly
144
+ forest_regen = (self.grid == FOREST)
145
+ new_food[forest_regen] += 0.01
146
+
147
+ # consume food by population
148
+ pop_mask = (self.grid == MALE) | (self.grid == FEMALE) | (self.grid == BABY) | (self.grid == WORKER)
149
+ new_food[pop_mask] -= self.params.food_need_per_capita
150
+ new_food = np.clip(new_food, 0.0, 5.0)
151
+
152
+ for y in range(H):
153
+ for x in range(W):
154
+ cell = self.grid[y, x]
155
+ nb = [(ny, nx) for ny, nx in self._neighbors8(y, x)]
156
+ nb_cells = np.array([self.grid[ny, nx] for ny, nx in nb], dtype=np.uint8)
157
+ nb_food = float(np.mean([self.food_stock[ny, nx] for ny, nx in nb]))
158
+ 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]))
159
+ nb_buildings = int(np.sum(nb_cells == BUILDING))
160
+ teacher_inf = self.teacher_influence(y, x)
161
+
162
+ # Conscious override decision
163
+ override = self.conscious_override(local_food=nb_food, local_density=nb_density, local_buildings=nb_buildings, teacher_influence=teacher_inf)
164
+
165
+ if cell in (MALE, FEMALE):
166
+ has_pair = (MALE in nb_cells and FEMALE in nb_cells)
167
+ pair_prob = self.params.pair_prob_base * (1.0 + min(1.0, nb_food)) * (1.0 + 0.25 * nb_buildings)
168
+ if teacher_inf > 0:
169
+ pair_prob *= 1.5 # teachers boost pairing
170
+ if has_pair and self.rng.random() < pair_prob:
171
+ empties = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
172
+ if empties:
173
+ ty, tx = empties[self.rng.integers(0, len(empties))]
174
+ new_grid[ty, tx] = BABY
175
+ new_age[ty, tx] = 0
176
+ # Optional: teacher-led immediate shelter/food placement under override
177
+ if override and self.rng.random() < 0.12:
178
+ target_type = FOOD if nb_food < 0.5 else BUILDING
179
+ candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
180
+ if candidates:
181
+ ty, tx = candidates[self.rng.integers(0, len(candidates))]
182
+ new_grid[ty, tx] = target_type
183
+ if target_type == FOOD:
184
+ new_food[ty, tx] += 0.4
185
+
186
+ elif cell == BABY:
187
+ # ageing into worker (babies near teachers age faster)
188
+ inc = 1 + (1 if teacher_inf > 0 else 0)
189
+ new_age[y, x] = self.age_grid[y, x] + inc
190
+ if new_age[y, x] >= self.params.baby_age_ticks:
191
+ new_grid[y, x] = WORKER
192
+
193
+ elif cell == WORKER:
194
+ # workers build roads, buildings, or harvest food
195
+ choice = self.rng.random()
196
+ if override:
197
+ if nb_food < 0.5 and choice < 0.6:
198
+ new_grid[y, x] = FOOD
199
+ new_food[y, x] += 0.3
200
+ elif choice < 0.9:
201
+ new_grid[y, x] = ROAD
202
+ else:
203
+ new_grid[y, x] = BUILDING
204
+ else:
205
+ if choice < 0.33:
206
+ new_grid[y, x] = BUILDING
207
+ elif choice < 0.66:
208
+ new_grid[y, x] = ROAD
209
+ else:
210
+ new_grid[y, x] = FOOD
211
+ new_food[y, x] += 0.2
212
+
213
+ elif cell == BUILDING:
214
+ # shops emerge near buildings over time
215
+ if self.rng.random() < 0.02:
216
+ candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
217
+ if candidates:
218
+ ty, tx = candidates[self.rng.integers(0, len(candidates))]
219
+ new_grid[ty, tx] = SHOP
220
+
221
+ elif cell == SHOP:
222
+ # convert surplus food to prosperity -> attracts agents
223
+ if nb_food > 0.8 and self.rng.random() < 0.05:
224
+ candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
225
+ if candidates:
226
+ ty, tx = candidates[self.rng.integers(0, len(candidates))]
227
+ new_grid[ty, tx] = MALE if self.rng.random() < 0.5 else FEMALE
228
+
229
+ elif cell == FOREST:
230
+ # harvesting creates food slots; excessive pressure reduces forest
231
+ if nb_density > 0.4 and nb_food < 0.5 and self.rng.random() < 0.06:
232
+ new_grid[y, x] = FOOD
233
+ new_food[y, x] += 0.4
234
+
235
+ elif cell == ROAD:
236
+ # roads extend toward population centers
237
+ if self.rng.random() < self.params.road_bias:
238
+ tgt = self._majority(nb_cells)
239
+ if tgt in (BUILDING, SHOP, MALE, FEMALE, WORKER, BABY) and self.rng.random() < 0.4:
240
+ candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
241
+ if candidates:
242
+ ty, tx = candidates[self.rng.integers(0, len(candidates))]
243
+ new_grid[ty, tx] = ROAD
244
+
245
+ elif cell == TEACHER:
246
+ # Teachers can spawn guild buildings
247
+ if self.rng.random() < 0.01:
248
+ candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
249
+ if candidates:
250
+ ty, tx = candidates[self.rng.integers(0, len(candidates))]
251
+ new_grid[ty, tx] = GUILD
252
+
253
+ elif cell == GUILD:
254
+ # Guilds stabilize local economy
255
+ if nb_food < 0.5 and self.rng.random() < 0.05:
256
+ candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
257
+ if candidates:
258
+ ty, tx = candidates[self.rng.integers(0, len(candidates))]
259
+ new_grid[ty, tx] = FOOD
260
+ new_food[ty, tx] += 0.5
261
+ # Guilds also increase chance of buildings emerging nearby
262
+ if self.rng.random() < 0.02:
263
+ candidates = [(ny, nx) for (ny, nx), c in zip(nb, nb_cells) if c == EMPTY]
264
+ if candidates:
265
+ ty, tx = candidates[self.rng.integers(0, len(candidates))]
266
+ new_grid[ty, tx] = BUILDING
267
+
268
+ # starvation collapse (if local food is zero and high density)
269
+ if (cell in (MALE, FEMALE, BABY, WORKER)) and nb_food <= 0.0 and nb_density > 0.6 and self.rng.random() < 0.05:
270
+ new_grid[y, x] = EMPTY
271
+
272
+ self.grid = new_grid
273
+ self.age_grid = new_age
274
+ self.food_stock = new_food
275
+ self.tick += 1
276
+
277
+ def render_image(self):
278
+ H, W = self.grid.shape
279
+ img = np.zeros((H, W, 3), dtype=np.uint8)
280
+ mapping = {
281
+ EMPTY: "EMPTY", MALE: "MALE", FEMALE: "FEMALE", BABY: "BABY", WORKER: "WORKER",
282
+ BUILDING: "BUILDING", SHOP: "SHOP", ROAD: "ROAD", FOREST: "FOREST", FOOD: "FOOD",
283
+ TEACHER: "TEACHER", GUILD: "GUILD"
284
+ }
285
+ for val, name in mapping.items():
286
+ rgb = COLORS[name]
287
+ mask = (self.grid == val)
288
+ img[mask] = np.array(rgb, dtype=np.uint8)
289
+ return img
290
+
291
+ def stream_sim(
292
+ width=64, height=64, seed=7,
293
+ male_init=120, female_init=120, teacher_init=6,
294
+ forest_fraction=0.20, food_fraction=0.05,
295
+ baby_age_ticks=8, override_sensitivity=0.60,
296
+ update_interval_sec=10, total_minutes=2
297
+ ):
298
+ # Initialize the sim
299
+ params = SimParams(
300
+ width=width, height=height,
301
+ male_init=male_init, female_init=female_init, teacher_init=teacher_init,
302
+ forest_fraction=forest_fraction, food_fraction=food_fraction,
303
+ baby_age_ticks=baby_age_ticks, override_sensitivity=override_sensitivity
304
+ )
305
+ sim = Civilization(params, seed=seed)
306
+
307
+ # Stats tracking
308
+ pop_series, build_series, road_series, shop_series, food_series = [], [], [], [], []
309
+ total_ticks = int((60 // max(1, update_interval_sec)) * total_minutes)
310
+
311
+ legend = "Blue=Male, Pink=Female, Yellow=Baby, LightBlue=Worker, Gray=Building, Orange=Shop, Black=Road, Green=Forest, Lime=Food, Purple=Teacher, Gray-Purple=Guild"
312
+
313
+ for _ in range(total_ticks):
314
+ # Advance a few internal ticks per UI update for visible growth
315
+ inner_ticks = max(1, update_interval_sec // 2)
316
+ for __ in range(inner_ticks):
317
+ sim.step()
318
+
319
+ img = sim.render_image()
320
+ g = sim.grid
321
+ pop_series.append(int(np.sum(np.isin(g, [MALE, FEMALE, BABY, WORKER]))))
322
+ build_series.append(int(np.sum(g == BUILDING)))
323
+ road_series.append(int(np.sum(g == ROAD)))
324
+ shop_series.append(int(np.sum(g == SHOP)))
325
+ food_series.append(int(np.sum(g == FOOD)))
326
+
327
+ text = (
328
+ 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"
329
+ f"Artifact={ARTIFACT} | Author={AUTHOR}\n"
330
+ f"Legend: {legend}"
331
+ )
332
+ yield img, text
333
+ time.sleep(update_interval_sec)
334
+
335
+ # Seal & log at the end of the stream
336
+ summary = {
337
+ "tick_final": sim.tick,
338
+ "pop_final": pop_series[-1] if pop_series else 0,
339
+ "buildings_final": build_series[-1] if build_series else 0,
340
+ "roads_final": road_series[-1] if road_series else 0,
341
+ "shops_final": shop_series[-1] if shop_series else 0,
342
+ "food_cells_final": food_series[-1] if food_series else 0,
343
+ "width": int(width), "height": int(height),
344
+ "teacher_init": int(teacher_init),
345
+ "update_interval_sec": int(update_interval_sec),
346
+ "total_minutes": int(total_minutes),
347
+ }
348
+ seal = sha512_str(json.dumps(summary, sort_keys=True))
349
+ prov = {
350
+ "artifact": ARTIFACT,
351
+ "author": AUTHOR,
352
+ "registry": REGISTRY,
353
+ "timestamp": int(time.time()),
354
+ "params": {
355
+ "width": width, "height": height, "seed": seed,
356
+ "male_init": male_init, "female_init": female_init, "teacher_init": teacher_init,
357
+ "forest_fraction": forest_fraction, "food_fraction": food_fraction,
358
+ "baby_age_ticks": baby_age_ticks, "override_sensitivity": override_sensitivity,
359
+ "update_interval_sec": update_interval_sec, "total_minutes": total_minutes
360
+ },
361
+ "summary": summary,
362
+ "sha512": seal
363
+ }
364
+ try:
365
+ with open("provenance.jsonl", "a") as f:
366
+ f.write(json.dumps(prov) + "\n")
367
+ except Exception:
368
+ pass
369
+
370
+ # =========================
371
+ # Gradio UI (single screen)
372
+ # =========================
373
+ with gr.Blocks(title="RFT Conscious Civilization Simulator (Teachers)") as demo:
374
+ gr.Markdown(
375
+ "# RFT Conscious Civilization Simulator\n"
376
+ "Single-screen live grid. Conscious math rules (male+female=baby → worker → building/road/food → shops) grow villages into cities.\n"
377
+ "Purple Teacher agents guide life, spawn guilds, and stabilize growth.\n"
378
+ "Authored by Liam Grinstead. Each session is sealed for falsifiability."
379
+ )
380
+
381
+ with gr.Row():
382
+ width = gr.Slider(32, 128, value=64, step=8, label="Grid width")
383
+ height = gr.Slider(32, 128, value=64, step=8, label="Grid height")
384
+ seed = gr.Number(value=7, label="Seed", precision=0)
385
+
386
+ with gr.Row():
387
+ male_init = gr.Slider(20, 300, value=120, step=10, label="Initial males")
388
+ female_init = gr.Slider(20, 300, value=120, step=10, label="Initial females")
389
+ teacher_init = gr.Slider(1, 30, value=6, step=1, label="Initial teachers")
390
+
391
+ with gr.Row():
392
+ forest_fraction = gr.Slider(0.0, 0.6, value=0.20, step=0.02, label="Forest fraction")
393
+ food_fraction = gr.Slider(0.0, 0.3, value=0.05, step=0.01, label="Food fraction")
394
+ baby_age_ticks = gr.Slider(4, 20, value=8, step=1, label="Baby→Worker ticks")
395
+ override_sensitivity = gr.Slider(0.3, 0.9, value=0.60, step=0.05, label="Conscious override sensitivity")
396
+
397
+ with gr.Row():
398
+ update_interval_sec = gr.Slider(2, 20, value=10, step=1, label="Update cadence (seconds)")
399
+ total_minutes = gr.Slider(1, 10, value=2, step=1, label="Session duration (minutes)")
400
+
401
+ start_btn = gr.Button("Start live civilization")
402
+ img_out = gr.Image(type="numpy", label="Live grid", streaming=True)
403
+ text_out = gr.Textbox(label="Summary (streaming)", lines=4)
404
+
405
+ start_btn.click(
406
+ stream_sim,
407
+ inputs=[width, height, seed, male_init, female_init, teacher_init, forest_fraction, food_fraction, baby_age_ticks, override_sensitivity, update_interval_sec, total_minutes],
408
+ outputs=[img_out, text_out]
409
+ )
410
+
411
+ if __name__ == "__main__":
412
+ demo.launch()