RFTSystems commited on
Commit
0ed23b4
·
verified ·
1 Parent(s): 6a4435f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +542 -0
app.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import random
3
+ import numpy as np
4
+ import gradio as gr
5
+
6
+ # ============================================================
7
+ # RFT Predator Space — First-Person Observer View (Pseudo-3D)
8
+ # - Raycast wall slicing (occlusion + distance shading)
9
+ # - Prey billboard with LOS + per-column occlusion vs walls
10
+ # - Optional top-down minimap for debugging / verification
11
+ # ============================================================
12
+
13
+ # -----------------------------
14
+ # World config (grid)
15
+ # -----------------------------
16
+ GRID_W, GRID_H = 23, 23
17
+ OBSTACLE_P = 0.13
18
+
19
+ # -----------------------------
20
+ # View config (render)
21
+ # -----------------------------
22
+ VIEW_W, VIEW_H = 560, 360 # output size
23
+ RAY_W = 280 # internal ray columns (upscaled to VIEW_W)
24
+ FOV_DEG = 78
25
+ MAX_DEPTH = 18
26
+
27
+ # Movement
28
+ TURN_DEG = 20
29
+ MOVE_STEP = 1
30
+ AUTO_TICK_HZ = 8
31
+
32
+ # Colors
33
+ SKY = np.array([14, 16, 26], dtype=np.uint8)
34
+ FLOOR_NEAR = np.array([20, 22, 34], dtype=np.uint8)
35
+ FLOOR_FAR = np.array([10, 11, 18], dtype=np.uint8)
36
+
37
+ WALL_BASE = np.array([210, 210, 225], dtype=np.uint8)
38
+ WALL_SIDE = np.array([150, 150, 170], dtype=np.uint8)
39
+
40
+ PREY_COLOR = np.array([255, 140, 90], dtype=np.uint8) # “billboard”
41
+ RETICLE = np.array([120, 190, 255], dtype=np.uint8)
42
+
43
+ # 0=E,1=S,2=W,3=N
44
+ DIRS = [(1,0),(0,1),(-1,0),(0,-1)]
45
+ ORI_DEG = [0, 90, 180, 270]
46
+
47
+ def clamp(x, lo, hi):
48
+ return lo if x < lo else hi if x > hi else x
49
+
50
+ def wrap_angle_deg(a):
51
+ a = a % 360.0
52
+ if a < 0: a += 360.0
53
+ return a
54
+
55
+ def angle_diff_rad(a, b):
56
+ # minimal difference a-b in [-pi, pi]
57
+ d = (a - b + math.pi) % (2*math.pi) - math.pi
58
+ return d
59
+
60
+ def seeded_rng(seed: int):
61
+ return random.Random(int(seed) & 0xFFFFFFFF)
62
+
63
+ def make_world(seed=1):
64
+ rng = seeded_rng(seed)
65
+ grid = np.zeros((GRID_H, GRID_W), dtype=np.int8)
66
+
67
+ # borders
68
+ grid[0, :] = 1
69
+ grid[GRID_H-1, :] = 1
70
+ grid[:, 0] = 1
71
+ grid[:, GRID_W-1] = 1
72
+
73
+ # obstacles
74
+ for y in range(1, GRID_H-1):
75
+ for x in range(1, GRID_W-1):
76
+ if rng.random() < OBSTACLE_P:
77
+ grid[y, x] = 1
78
+
79
+ def empty_cell():
80
+ for _ in range(20000):
81
+ x = rng.randint(1, GRID_W-2)
82
+ y = rng.randint(1, GRID_H-2)
83
+ if grid[y, x] == 0:
84
+ return (x, y)
85
+ return (1, 1)
86
+
87
+ pred = empty_cell()
88
+ prey = empty_cell()
89
+ while prey == pred:
90
+ prey = empty_cell()
91
+
92
+ ori = rng.randint(0, 3)
93
+
94
+ st = {
95
+ "seed": int(seed),
96
+ "grid": grid,
97
+ "pred": pred,
98
+ "prey": prey,
99
+ "ori": ori,
100
+ "step": 0,
101
+ "caught": False,
102
+ "auto_chase": False,
103
+ "auto_run": False,
104
+ "log": ["Reset."]
105
+ }
106
+ return st
107
+
108
+ def los_clear(grid, a, b):
109
+ # Grid LOS using DDA in continuous space (cell centers)
110
+ ax, ay = a[0] + 0.5, a[1] + 0.5
111
+ bx, by = b[0] + 0.5, b[1] + 0.5
112
+ dx, dy = bx - ax, by - ay
113
+ dist = math.hypot(dx, dy)
114
+ if dist < 1e-6:
115
+ return True
116
+ dx /= dist
117
+ dy /= dist
118
+
119
+ x, y = ax, ay
120
+ steps = int(dist * 20) # oversample for safety
121
+ for _ in range(steps):
122
+ x += dx * (dist / steps)
123
+ y += dy * (dist / steps)
124
+ cx, cy = int(x), int(y)
125
+ cx = clamp(cx, 0, GRID_W-1)
126
+ cy = clamp(cy, 0, GRID_H-1)
127
+ if grid[cy, cx] == 1:
128
+ return False
129
+ return True
130
+
131
+ def dda_raycast(grid, px, py, ray_dx, ray_dy, max_depth=MAX_DEPTH):
132
+ """
133
+ DDA raycast in grid.
134
+ Returns: (hit_dist, hit_side, hit_cell_x, hit_cell_y)
135
+ hit_side: 0 if hit vertical wall, 1 if hit horizontal wall (used for shading)
136
+ """
137
+ map_x = int(px)
138
+ map_y = int(py)
139
+
140
+ delta_dist_x = abs(1.0 / ray_dx) if abs(ray_dx) > 1e-9 else 1e9
141
+ delta_dist_y = abs(1.0 / ray_dy) if abs(ray_dy) > 1e-9 else 1e9
142
+
143
+ if ray_dx < 0:
144
+ step_x = -1
145
+ side_dist_x = (px - map_x) * delta_dist_x
146
+ else:
147
+ step_x = 1
148
+ side_dist_x = (map_x + 1.0 - px) * delta_dist_x
149
+
150
+ if ray_dy < 0:
151
+ step_y = -1
152
+ side_dist_y = (py - map_y) * delta_dist_y
153
+ else:
154
+ step_y = 1
155
+ side_dist_y = (map_y + 1.0 - py) * delta_dist_y
156
+
157
+ hit = False
158
+ side = 0
159
+ for _ in range(max_depth * 8):
160
+ if side_dist_x < side_dist_y:
161
+ side_dist_x += delta_dist_x
162
+ map_x += step_x
163
+ side = 0
164
+ else:
165
+ side_dist_y += delta_dist_y
166
+ map_y += step_y
167
+ side = 1
168
+
169
+ if map_x < 0 or map_x >= GRID_W or map_y < 0 or map_y >= GRID_H:
170
+ break
171
+ if grid[map_y, map_x] == 1:
172
+ hit = True
173
+ break
174
+
175
+ if not hit:
176
+ return max_depth, 0, map_x, map_y
177
+
178
+ # perpendicular distance (avoid fisheye by using projection)
179
+ if side == 0:
180
+ denom = ray_dx if abs(ray_dx) > 1e-9 else 1e-9
181
+ perp = (map_x - px + (1 - step_x) / 2) / denom
182
+ else:
183
+ denom = ray_dy if abs(ray_dy) > 1e-9 else 1e-9
184
+ perp = (map_y - py + (1 - step_y) / 2) / denom
185
+
186
+ perp = abs(perp)
187
+ perp = clamp(perp, 0.0005, max_depth)
188
+ return perp, side, map_x, map_y
189
+
190
+ def render_first_person(st):
191
+ grid = st["grid"]
192
+ (cx, cy) = st["pred"]
193
+ ori = st["ori"]
194
+
195
+ px = cx + 0.5
196
+ py = cy + 0.5
197
+
198
+ fov = math.radians(FOV_DEG)
199
+ base = math.radians(ORI_DEG[ori])
200
+
201
+ img = np.zeros((VIEW_H, VIEW_W, 3), dtype=np.uint8)
202
+
203
+ # sky
204
+ img[:VIEW_H//2, :, :] = SKY
205
+
206
+ # floor gradient
207
+ for y in range(VIEW_H//2, VIEW_H):
208
+ t = (y - VIEW_H//2) / max(1, (VIEW_H//2 - 1))
209
+ col = (FLOOR_NEAR * (1 - t) + FLOOR_FAR * t).astype(np.uint8)
210
+ img[y, :, :] = col
211
+
212
+ # Raycast at lower resolution then upscale to VIEW_W
213
+ wall_dists = np.full(RAY_W, MAX_DEPTH, dtype=np.float32)
214
+
215
+ for x in range(RAY_W):
216
+ u = (x / (RAY_W - 1)) if RAY_W > 1 else 0.5
217
+ ang = base + (u - 0.5) * fov
218
+ ray_dx = math.cos(ang)
219
+ ray_dy = math.sin(ang)
220
+
221
+ dist, side, hitx, hity = dda_raycast(grid, px, py, ray_dx, ray_dy, MAX_DEPTH)
222
+ # remove fisheye: project on camera direction
223
+ dist *= math.cos(ang - base)
224
+ dist = clamp(dist, 0.001, MAX_DEPTH)
225
+ wall_dists[x] = dist
226
+
227
+ # wall slice height
228
+ slice_h = int((VIEW_H * 0.92) / dist)
229
+ slice_h = clamp(slice_h, 1, VIEW_H)
230
+ top = (VIEW_H - slice_h) // 2
231
+ bot = top + slice_h
232
+
233
+ # shading: distance + side shading
234
+ shade = 1.0 / (1.0 + dist * 0.12)
235
+ shade = clamp(shade, 0.12, 1.0)
236
+
237
+ base_col = WALL_SIDE if side == 1 else WALL_BASE
238
+
239
+ # cheap "texture" pattern by hit cell coords
240
+ checker = ((hitx + hity) & 1)
241
+ tex = 0.90 if checker == 0 else 1.05
242
+
243
+ col = np.clip(base_col.astype(np.float32) * shade * tex, 0, 255).astype(np.uint8)
244
+
245
+ # draw vertical stripe into upscaled coordinates later
246
+ # map x from RAY_W -> VIEW_W
247
+ x0 = int(x * VIEW_W / RAY_W)
248
+ x1 = int((x + 1) * VIEW_W / RAY_W)
249
+ if x1 <= x0:
250
+ x1 = x0 + 1
251
+ img[top:bot, x0:x1, :] = col
252
+
253
+ # Prey billboard: only if in FOV AND LOS AND not behind wall per-column
254
+ prey = st["prey"]
255
+ prey_vis = False
256
+ if not st["caught"]:
257
+ if los_clear(grid, st["pred"], prey):
258
+ vx = (prey[0] + 0.5) - px
259
+ vy = (prey[1] + 0.5) - py
260
+ prey_dist = math.hypot(vx, vy)
261
+ prey_ang = math.atan2(vy, vx)
262
+ rel = angle_diff_rad(prey_ang, base)
263
+ if abs(rel) <= fov * 0.5 and prey_dist < MAX_DEPTH:
264
+ prey_vis = True
265
+
266
+ # screen x in ray space
267
+ u = (rel / fov) + 0.5
268
+ sx_ray = int(u * (RAY_W - 1))
269
+ sx_ray = clamp(sx_ray, 0, RAY_W - 1)
270
+
271
+ # sprite size
272
+ sprite_h = int((VIEW_H * 0.75) / max(0.2, prey_dist))
273
+ sprite_w = int(sprite_h * 0.45)
274
+ sprite_h = clamp(sprite_h, 8, VIEW_H)
275
+ sprite_w = clamp(sprite_w, 6, VIEW_W)
276
+
277
+ # convert to VIEW coords
278
+ sx = int(sx_ray * VIEW_W / RAY_W)
279
+ sy = VIEW_H // 2
280
+
281
+ x0 = clamp(sx - sprite_w // 2, 0, VIEW_W - 1)
282
+ x1 = clamp(sx + sprite_w // 2, 0, VIEW_W - 1)
283
+ y0 = clamp(sy - sprite_h // 2, 0, VIEW_H - 1)
284
+ y1 = clamp(sy + sprite_h // 2, 0, VIEW_H - 1)
285
+
286
+ # Occlusion test: only draw columns where prey is closer than wall
287
+ # Convert view columns -> ray columns
288
+ for vxcol in range(x0, x1):
289
+ rx = int(vxcol * RAY_W / VIEW_W)
290
+ rx = clamp(rx, 0, RAY_W - 1)
291
+ if prey_dist < wall_dists[rx]:
292
+ img[y0:y1, vxcol:vxcol+1, :] = PREY_COLOR
293
+
294
+ # Reticle (crosshair)
295
+ cxh, cyh = VIEW_W // 2, VIEW_H // 2
296
+ img[cyh-1:cyh+2, cxh-12:cxh+13, :] = RETICLE
297
+ img[cyh-12:cyh+13, cxh-1:cxh+2, :] = RETICLE
298
+
299
+ # HUD strip (simple, in-image)
300
+ hud_h = 26
301
+ img[:hud_h, :, :] = np.clip(img[:hud_h, :, :].astype(np.int16) + 20, 0, 255).astype(np.uint8)
302
+
303
+ # encode tiny indicators as pixels (keeps deps minimal: no PIL)
304
+ # left corner: auto_chase / auto_run / prey_vis
305
+ def dot(x, y, c):
306
+ img[y:y+6, x:x+6, :] = c
307
+
308
+ dot(8, 10, np.array([90, 255, 140], np.uint8) if st["auto_chase"] else np.array([60, 60, 70], np.uint8))
309
+ dot(20, 10, np.array([120, 190, 255], np.uint8) if st["auto_run"] else np.array([60, 60, 70], np.uint8))
310
+ dot(32, 10, np.array([255, 140, 90], np.uint8) if prey_vis else np.array([60, 60, 70], np.uint8))
311
+
312
+ return img
313
+
314
+ def render_minimap(st, scale=14):
315
+ grid = st["grid"]
316
+ h, w = grid.shape
317
+ img = np.zeros((h*scale, w*scale, 3), dtype=np.uint8)
318
+
319
+ # base
320
+ img[:, :, :] = np.array([18, 20, 32], dtype=np.uint8)
321
+
322
+ # walls
323
+ wall = np.array([220, 220, 235], dtype=np.uint8)
324
+ for y in range(h):
325
+ for x in range(w):
326
+ if grid[y, x] == 1:
327
+ img[y*scale:(y+1)*scale, x*scale:(x+1)*scale, :] = wall
328
+
329
+ # prey / pred
330
+ pred = st["pred"]
331
+ prey = st["prey"]
332
+ px, py = pred
333
+ qx, qy = prey
334
+
335
+ img[py*scale:(py+1)*scale, px*scale:(px+1)*scale, :] = np.array([120, 190, 255], np.uint8)
336
+ img[qy*scale:(qy+1)*scale, qx*scale:(qx+1)*scale, :] = np.array([255, 140, 90], np.uint8)
337
+
338
+ # heading marker
339
+ ori = st["ori"]
340
+ dx, dy = DIRS[ori]
341
+ hx = px + dx
342
+ hy = py + dy
343
+ if 0 <= hx < w and 0 <= hy < h:
344
+ img[hy*scale:(hy+1)*scale, hx*scale:(hx+1)*scale, :] = np.array([80, 255, 160], np.uint8)
345
+
346
+ return img
347
+
348
+ def status(st):
349
+ ori_txt = ["E", "S", "W", "N"][st["ori"]]
350
+ tail = st["log"][-10:]
351
+ return (
352
+ f"Step: {st['step']} | Predator: {st['pred']} {ori_txt} | Prey: {st['prey']} | "
353
+ f"AutoChase: {st['auto_chase']} | AutoRun: {st['auto_run']} | Caught: {st['caught']}\n\n"
354
+ + "\n".join(tail)
355
+ )
356
+
357
+ def move_forward(st):
358
+ if st["caught"]:
359
+ return
360
+ x, y = st["pred"]
361
+ dx, dy = DIRS[st["ori"]]
362
+ nx, ny = x + dx * MOVE_STEP, y + dy * MOVE_STEP
363
+ if st["grid"][ny, nx] == 0:
364
+ st["pred"] = (nx, ny)
365
+ else:
366
+ st["log"].append("Bumped wall.")
367
+
368
+ def turn_left(st):
369
+ if st["caught"]:
370
+ return
371
+ st["ori"] = (st["ori"] - 1) % 4
372
+
373
+ def turn_right(st):
374
+ if st["caught"]:
375
+ return
376
+ st["ori"] = (st["ori"] + 1) % 4
377
+
378
+ def prey_step(st):
379
+ if st["caught"]:
380
+ return
381
+ rng = seeded_rng(st["seed"] + 1337 + st["step"] * 19)
382
+ px, py = st["prey"]
383
+ ax, ay = st["pred"]
384
+
385
+ options = [(0,0),(1,0),(-1,0),(0,1),(0,-1)]
386
+ scored = []
387
+ for dx, dy in options:
388
+ nx, ny = px + dx, py + dy
389
+ if st["grid"][ny, nx] == 1:
390
+ continue
391
+ dist = (nx-ax)**2 + (ny-ay)**2
392
+ scored.append((dist + rng.random()*0.1, (nx, ny)))
393
+
394
+ if scored:
395
+ scored.sort(reverse=True)
396
+ st["prey"] = scored[0][1] if rng.random() < 0.78 else rng.choice(scored)[1]
397
+
398
+ def check_catch(st):
399
+ if st["pred"] == st["prey"]:
400
+ st["caught"] = True
401
+ st["log"].append("CAUGHT the prey.")
402
+
403
+ def auto_chase_policy(st):
404
+ if st["caught"]:
405
+ return
406
+ # If prey visible + in FOV, turn toward it; else drift forward avoiding walls.
407
+ grid = st["grid"]
408
+ px = st["pred"][0] + 0.5
409
+ py = st["pred"][1] + 0.5
410
+ base = math.radians(ORI_DEG[st["ori"]])
411
+ fov = math.radians(FOV_DEG)
412
+
413
+ prey = st["prey"]
414
+ if los_clear(grid, st["pred"], prey):
415
+ vx = (prey[0] + 0.5) - px
416
+ vy = (prey[1] + 0.5) - py
417
+ ang = math.atan2(vy, vx)
418
+ rel = angle_diff_rad(ang, base)
419
+ if abs(rel) <= fov * 0.5:
420
+ if rel < -0.10:
421
+ turn_left(st); st["log"].append("AutoChase: turn left.")
422
+ elif rel > 0.10:
423
+ turn_right(st); st["log"].append("AutoChase: turn right.")
424
+ else:
425
+ move_forward(st); st["log"].append("AutoChase: forward.")
426
+ return
427
+
428
+ # avoid walls: look one step ahead
429
+ x, y = st["pred"]
430
+ dx, dy = DIRS[st["ori"]]
431
+ if st["grid"][y+dy, x+dx] == 1:
432
+ if random.random() < 0.5:
433
+ turn_left(st); st["log"].append("AutoChase: avoid left.")
434
+ else:
435
+ turn_right(st); st["log"].append("AutoChase: avoid right.")
436
+ else:
437
+ move_forward(st); st["log"].append("AutoChase: forward roam.")
438
+
439
+ def tick(st):
440
+ if st["caught"]:
441
+ return
442
+ st["step"] += 1
443
+
444
+ if st["auto_chase"]:
445
+ auto_chase_policy(st)
446
+
447
+ prey_step(st)
448
+ check_catch(st)
449
+
450
+ if st["step"] >= 600:
451
+ st["caught"] = True
452
+ st["log"].append("Max steps reached (freeze).")
453
+
454
+ # -----------------------------
455
+ # Gradio actions
456
+ # -----------------------------
457
+ def ui_reset(seed):
458
+ st = make_world(int(seed))
459
+ return st, render_first_person(st), render_minimap(st), status(st)
460
+
461
+ def ui_left(st):
462
+ turn_left(st); st["log"].append("Turn left.")
463
+ tick(st)
464
+ return st, render_first_person(st), render_minimap(st), status(st)
465
+
466
+ def ui_right(st):
467
+ turn_right(st); st["log"].append("Turn right.")
468
+ tick(st)
469
+ return st, render_first_person(st), render_minimap(st), status(st)
470
+
471
+ def ui_forward(st):
472
+ move_forward(st); st["log"].append("Forward.")
473
+ check_catch(st)
474
+ tick(st)
475
+ return st, render_first_person(st), render_minimap(st), status(st)
476
+
477
+ def ui_toggle_chase(st):
478
+ st["auto_chase"] = not st["auto_chase"]
479
+ st["log"].append(f"AutoChase set to {st['auto_chase']}.")
480
+ return st, render_first_person(st), render_minimap(st), status(st)
481
+
482
+ def ui_toggle_run(st):
483
+ st["auto_run"] = not st["auto_run"]
484
+ st["log"].append(f"AutoRun set to {st['auto_run']}.")
485
+ return st, render_first_person(st), render_minimap(st), status(st)
486
+
487
+ def ui_tick(st):
488
+ tick(st)
489
+ return st, render_first_person(st), render_minimap(st), status(st)
490
+
491
+ def ui_timer(st):
492
+ # Timer-driven tick when AutoRun is enabled
493
+ if st["auto_run"] and not st["caught"]:
494
+ tick(st)
495
+ return st, render_first_person(st), render_minimap(st), status(st)
496
+
497
+ # -----------------------------
498
+ # App
499
+ # -----------------------------
500
+ with gr.Blocks(title="RFT Predator Space — First Person Observer") as demo:
501
+ gr.Markdown(
502
+ "## Experience reality through an RFT observer agent’s perspective\n"
503
+ "This view shows **only what the predator can perceive**: a pseudo-3D raycast viewport with occlusion and LOS-correct prey visibility.\n"
504
+ "**Dots:** AutoChase / AutoRun / PreyVisible (top-left)."
505
+ )
506
+
507
+ st = gr.State(make_world(1))
508
+
509
+ with gr.Row():
510
+ seed = gr.Number(label="Seed", value=1, precision=0)
511
+ btn_reset = gr.Button("Reset")
512
+ btn_chase = gr.Button("Toggle AutoChase")
513
+ btn_run = gr.Button("Toggle AutoRun")
514
+ btn_tick = gr.Button("Tick")
515
+
516
+ with gr.Row():
517
+ btn_left = gr.Button("Turn Left")
518
+ btn_fwd = gr.Button("Forward")
519
+ btn_right = gr.Button("Turn Right")
520
+
521
+ with gr.Row():
522
+ view = gr.Image(label="First-person observer view", type="numpy")
523
+ mini = gr.Image(label="Minimap (debug)", type="numpy")
524
+
525
+ info = gr.Textbox(label="Run log", lines=12)
526
+
527
+ demo.load(lambda: (st.value, render_first_person(st.value), render_minimap(st.value), status(st.value)),
528
+ outputs=[st, view, mini, info])
529
+
530
+ btn_reset.click(ui_reset, inputs=[seed], outputs=[st, view, mini, info])
531
+ btn_left.click(ui_left, inputs=[st], outputs=[st, view, mini, info])
532
+ btn_right.click(ui_right, inputs=[st], outputs=[st, view, mini, info])
533
+ btn_fwd.click(ui_forward, inputs=[st], outputs=[st, view, mini, info])
534
+ btn_chase.click(ui_toggle_chase, inputs=[st], outputs=[st, view, mini, info])
535
+ btn_run.click(ui_toggle_run, inputs=[st], outputs=[st, view, mini, info])
536
+ btn_tick.click(ui_tick, inputs=[st], outputs=[st, view, mini, info])
537
+
538
+ # Timer auto-run (if supported by your Gradio build)
539
+ if hasattr(gr, "Timer"):
540
+ gr.Timer(1.0 / AUTO_TICK_HZ).tick(ui_timer, inputs=[st], outputs=[st, view, mini, info])
541
+
542
+ demo.launch()