RFTSystems commited on
Commit
acdfbb5
·
verified ·
1 Parent(s): 294e0e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +84 -93
app.py CHANGED
@@ -7,11 +7,10 @@ import gradio as gr
7
 
8
  # ============================================================
9
  # RFT Predator Space — First-Person Observer View (Pseudo-3D)
10
- # + Unlockable maps
11
- # + Save/Load (slot + export/import) + slot dropdown auto-lists ./saves/
12
- # + Hybrid mode: AutoRun ON + AutoChase OFF => predator WANDERS autonomously (while prey flees if not player-controlled)
13
- # + Toggle control POV/inputs between Predator vs Prey (symmetric observers)
14
- # + Optional coherence overlay (subtle; off by default)
15
  # ============================================================
16
 
17
  # -----------------------------
@@ -68,10 +67,7 @@ def _slot_path(slot: str) -> str:
68
 
69
  def list_save_slots():
70
  try:
71
- files = []
72
- for fn in os.listdir(SAVE_DIR):
73
- if fn.lower().endswith(".json"):
74
- files.append(fn)
75
  files.sort()
76
  return files
77
  except Exception:
@@ -300,8 +296,8 @@ def build_state(seed, map_name, progress=None, override=None):
300
 
301
  "control": "pred", # "pred" or "prey" (view + manual inputs)
302
  "overlay": False, # coherence overlay
303
- "disturbance": 0.0, # subtle coherence metric (EWMA)
304
- "last_impulse": 0.0, # updated by actions
305
 
306
  "step": 0,
307
  "caught": False,
@@ -331,21 +327,16 @@ def serialize_state(st):
331
  "seed": int(st["seed"]),
332
  "map_name": str(st["map_name"]),
333
  "step": int(st["step"]),
334
-
335
  "pred": [int(st["pred"][0]), int(st["pred"][1])],
336
  "prey": [int(st["prey"][0]), int(st["prey"][1])],
337
-
338
  "ori": int(st["ori"]),
339
  "prey_ori": int(st.get("prey_ori", 0)),
340
-
341
  "control": str(st.get("control", "pred")),
342
  "overlay": bool(st.get("overlay", False)),
343
  "disturbance": float(st.get("disturbance", 0.0)),
344
-
345
  "caught": bool(st["caught"]),
346
  "auto_chase": bool(st["auto_chase"]),
347
  "auto_run": bool(st["auto_run"]),
348
-
349
  "catches": catches,
350
  "log_tail": st["log"][-20:],
351
  }
@@ -364,25 +355,20 @@ def deserialize_state(payload):
364
  "step": int(payload.get("step", 0)),
365
  "pred": tuple(payload.get("pred", [1, 1])),
366
  "prey": tuple(payload.get("prey", [2, 2])),
367
-
368
  "ori": int(payload.get("ori", 0)) % 4,
369
  "prey_ori": int(payload.get("prey_ori", 0)) % 4,
370
-
371
  "control": str(payload.get("control", "pred")) if str(payload.get("control", "pred")) in ("pred", "prey") else "pred",
372
  "overlay": bool(payload.get("overlay", False)),
373
  "disturbance": float(payload.get("disturbance", 0.0)),
374
  "last_impulse": 0.0,
375
-
376
  "caught": bool(payload.get("caught", False)),
377
  "auto_chase": bool(payload.get("auto_chase", False)),
378
  "auto_run": bool(payload.get("auto_run", False)),
379
-
380
  "log": (payload.get("log_tail", []) or [])[:],
381
  }
382
 
383
  st = build_state(seed, map_name, progress=progress, override=override)
384
 
385
- # validate positions (must be on empty cells)
386
  grid = st["grid"]
387
  H, W = grid.shape
388
  px, py = st["pred"]
@@ -394,7 +380,7 @@ def deserialize_state(payload):
394
  if not ok:
395
  rng = seeded_rng(seed + 777)
396
  st["pred"], st["prey"] = pick_spawn_pair(grid, rng, min_dist=8)
397
- st["log"].append("Loaded save had invalid positions for this map; respawned safely.")
398
 
399
  st["log"].append("Loaded save.")
400
  return st
@@ -491,30 +477,23 @@ def dda_raycast(grid, px, py, ray_dx, ray_dy, max_depth=MAX_DEPTH):
491
  return perp, side, map_x, map_y
492
 
493
  def _apply_coherence_overlay(img, disturbance: float):
494
- # Very subtle: faint torque lines + edge tint. Disturbance expected ~[0..~3]
495
  d = float(disturbance)
496
  if d <= 0.001:
497
  return img
498
 
499
- alpha = clamp(d * 0.06, 0.0, 0.22) # keep subtle
500
  h, w, _ = img.shape
501
  cx, cy = w // 2, h // 2
502
 
503
- # edge tint
504
  edge = int(min(w, h) * 0.08)
505
  if edge >= 2:
506
- tint = np.array([22, 8, 18], dtype=np.float32) # slight magenta heat
507
- # top
508
  img[:edge, :, :] = np.clip(img[:edge, :, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
509
- # bottom
510
  img[h-edge:, :, :] = np.clip(img[h-edge:, :, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
511
- # left
512
  img[:, :edge, :] = np.clip(img[:, :edge, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
513
- # right
514
  img[:, w-edge:, :] = np.clip(img[:, w-edge:, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
515
 
516
- # torque lines near center
517
- line_col = np.array([180, 70, 160], dtype=np.float32) # muted
518
  for i in range(-40, 41):
519
  x = cx + i
520
  y = cy + int(i * 0.35)
@@ -530,7 +509,6 @@ def _apply_coherence_overlay(img, disturbance: float):
530
  def render_first_person(st):
531
  grid = st["grid"]
532
 
533
- # viewer is whichever agent is currently controlled
534
  if st["control"] == "prey":
535
  view_cell = st["prey"]
536
  view_ori = st["prey_ori"]
@@ -586,7 +564,6 @@ def render_first_person(st):
586
  x1 = x0 + 1
587
  img[top:bot, x0:x1, :] = col
588
 
589
- # Other-agent billboard (LOS + per-column occlusion)
590
  other_vis = False
591
  if not st["caught"] and los_clear(grid, view_cell, other_cell):
592
  vx = (other_cell[0] + 0.5) - px
@@ -619,19 +596,13 @@ def render_first_person(st):
619
  if other_dist < wall_dists[rx]:
620
  img[y0:y1, vxcol:vxcol+1, :] = AGENT_OTHER_COLOR
621
 
622
- # Reticle
623
  cxh, cyh = VIEW_W // 2, VIEW_H // 2
624
  img[cyh-1:cyh+2, cxh-12:cxh+13, :] = RETICLE
625
  img[cyh-12:cyh+13, cxh-1:cxh+2, :] = RETICLE
626
 
627
- # HUD strip
628
  hud_h = 26
629
  img[:hud_h, :, :] = np.clip(img[:hud_h, :, :].astype(np.int16) + 20, 0, 255).astype(np.uint8)
630
 
631
- # Indicator dots:
632
- # - AutoChase
633
- # - AutoRun
634
- # - OtherVisible
635
  def dot(x, y, c):
636
  img[y:y+6, x:x+6, :] = c
637
 
@@ -639,7 +610,6 @@ def render_first_person(st):
639
  dot(20, 10, np.array([120, 190, 255], np.uint8) if st["auto_run"] else np.array([60, 60, 70], np.uint8))
640
  dot(32, 10, np.array([255, 140, 90], np.uint8) if other_vis else np.array([60, 60, 70], np.uint8))
641
 
642
- # Optional coherence overlay
643
  if st.get("overlay", False):
644
  img = _apply_coherence_overlay(img, st.get("disturbance", 0.0))
645
 
@@ -659,14 +629,12 @@ def render_minimap(st, scale=14):
659
 
660
  px, py = st["pred"]
661
  qx, qy = st["prey"]
662
-
663
  pred_col = np.array([120, 190, 255], np.uint8)
664
  prey_col = np.array([255, 140, 90], np.uint8)
665
 
666
  img[py*scale:(py+1)*scale, px*scale:(px+1)*scale, :] = pred_col
667
  img[qy*scale:(qy+1)*scale, qx*scale:(qx+1)*scale, :] = prey_col
668
 
669
- # headings
670
  dx, dy = DIRS[st["ori"]]
671
  hx, hy = px + dx, py + dy
672
  if 0 <= hx < W and 0 <= hy < H:
@@ -677,7 +645,6 @@ def render_minimap(st, scale=14):
677
  if 0 <= hx2 < W and 0 <= hy2 < H:
678
  img[hy2*scale:(hy2+1)*scale, hx2*scale:(hx2+1)*scale, :] = np.array([255, 220, 120], np.uint8)
679
 
680
- # highlight controlled agent with a bright ring (simple border)
681
  if st["control"] == "pred":
682
  x0, y0 = px*scale, py*scale
683
  else:
@@ -708,12 +675,11 @@ def status(st):
708
  catches = st["progress"]["catches"]
709
  current = st["map_name"]
710
 
711
- # interpret hybrid clearly
712
  mode = "Manual"
713
  if st["auto_run"] and st["auto_chase"]:
714
- mode = "AutoRun+AutoChase (pred chases autonomously)"
715
  elif st["auto_run"] and not st["auto_chase"]:
716
- mode = "Hybrid AutoRun (pred wanders autonomously)"
717
 
718
  ctrl = "Predator" if st["control"] == "pred" else "Prey"
719
  coh = st.get("disturbance", 0.0)
@@ -731,7 +697,6 @@ def _add_impulse(st, x):
731
  st["last_impulse"] = float(st.get("last_impulse", 0.0)) + float(x)
732
 
733
  def _step_disturbance(st):
734
- # EWMA, keeps it subtle. Decays naturally.
735
  d = float(st.get("disturbance", 0.0))
736
  imp = float(st.get("last_impulse", 0.0))
737
  st["disturbance"] = 0.92 * d + imp
@@ -750,13 +715,13 @@ def _set_agent_pos_ori(st, who, pos=None, ori=None):
750
  if pos is not None: st["pred"] = pos
751
  if ori is not None: st["ori"] = int(ori) % 4
752
 
753
- def _turn(st, who, direction): # direction = -1 left, +1 right
754
  if st["caught"]:
755
  return
756
- pos, ori = _agent_pos_ori(st, who)
757
  ori = (ori + direction) % 4
758
  _set_agent_pos_ori(st, who, ori=ori)
759
- _add_impulse(st, 0.9) # turning = higher disturbance
760
 
761
  def _forward(st, who):
762
  if st["caught"]:
@@ -779,9 +744,10 @@ def _check_catch_and_unlock(st):
779
  st["progress"]["unlocked"] = compute_unlocks(st["progress"]["catches"])
780
  st["log"].append(f"Catches now {st['progress']['catches']}. Unlocks updated.")
781
  _add_impulse(st, 1.2)
 
 
782
 
783
  def prey_flee_step(st):
784
- # prey flees predator (if prey is not player-controlled)
785
  if st["caught"]:
786
  return
787
  rng = seeded_rng(st["seed"] + 1337 + st["step"] * 19)
@@ -806,7 +772,6 @@ def prey_flee_step(st):
806
  st["prey_ori"] = DIR_TO_ORI[(dx, dy)]
807
 
808
  def predator_wander_step(st):
809
- # Hybrid mode: predator wanders autonomously (if predator is not player-controlled)
810
  if st["caught"]:
811
  return
812
  rng = seeded_rng(st["seed"] + 4242 + st["step"] * 23)
@@ -815,7 +780,6 @@ def predator_wander_step(st):
815
  dx, dy = DIRS[ori]
816
  front_blocked = (st["grid"][y+dy, x+dx] == 1)
817
 
818
- # If blocked, turn deterministically-ish. Else random preference: forward with occasional turns.
819
  r = rng.random()
820
  if front_blocked:
821
  if r < 0.5:
@@ -831,7 +795,6 @@ def predator_wander_step(st):
831
  _turn(st, "pred", +1); st["log"].append("AutoWander: turn right.")
832
 
833
  def predator_chase_step(st):
834
- # AutoChase mode: predator turns/moves toward prey when in LOS+FOV; else roams with wall avoid
835
  if st["caught"]:
836
  return
837
  grid = st["grid"]
@@ -854,34 +817,51 @@ def predator_chase_step(st):
854
  else:
855
  _forward(st, "pred"); st["log"].append("AutoChase: forward.")
856
  return
857
-
858
- # fallback: wander-ish
859
  predator_wander_step(st)
860
 
 
 
 
 
 
861
  def tick(st):
862
  if st["caught"]:
863
- return
864
 
865
  st["step"] += 1
 
866
 
867
- # Autonomous predator step ONLY when predator is not player-controlled and AutoRun is enabled.
868
- if st["auto_run"] and st["control"] != "pred":
 
 
 
 
 
 
 
 
 
 
869
  if st["auto_chase"]:
870
  predator_chase_step(st)
871
  else:
872
  predator_wander_step(st)
873
 
874
- # Autonomous prey flee ONLY when prey is not player-controlled.
875
  if st["control"] != "prey":
876
  prey_flee_step(st)
877
 
878
- _check_catch_and_unlock(st)
 
 
879
  _step_disturbance(st)
880
 
881
  if st["step"] >= 600:
882
  st["caught"] = True
883
  st["log"].append("Max steps reached (freeze).")
884
 
 
 
885
  # -----------------------------
886
  # Gradio handlers
887
  # -----------------------------
@@ -899,7 +879,6 @@ def ui_reset(seed, map_choice, st=None):
899
  if map_choice not in progress["unlocked"]:
900
  map_choice = "Training Bay"
901
  new_st = build_state(seed, map_choice, progress=progress)
902
- # keep user prefs
903
  if st:
904
  new_st["control"] = st.get("control", "pred")
905
  new_st["overlay"] = st.get("overlay", False)
@@ -915,61 +894,69 @@ def ui_turn_left(st):
915
  who = st["control"]
916
  _turn(st, who, -1)
917
  st["log"].append(f"{'Prey' if who=='prey' else 'Predator'} turn left.")
918
- tick(st)
919
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
 
 
920
 
921
  def ui_turn_right(st):
922
  who = st["control"]
923
  _turn(st, who, +1)
924
  st["log"].append(f"{'Prey' if who=='prey' else 'Predator'} turn right.")
925
- tick(st)
926
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
 
 
927
 
928
  def ui_forward(st):
929
  who = st["control"]
930
  _forward(st, who)
931
  st["log"].append(f"{'Prey' if who=='prey' else 'Predator'} forward.")
932
- _check_catch_and_unlock(st)
933
- tick(st)
934
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
 
935
 
936
  def ui_toggle_chase(st):
937
  st["auto_chase"] = not st["auto_chase"]
938
  st["log"].append(f"AutoChase set to {st['auto_chase']}.")
939
  _add_impulse(st, 0.10)
940
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
941
 
942
  def ui_toggle_run(st):
943
  st["auto_run"] = not st["auto_run"]
944
  st["log"].append(f"AutoRun set to {st['auto_run']}.")
945
  _add_impulse(st, 0.10)
946
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
947
 
948
  def ui_toggle_overlay(st):
949
  st["overlay"] = not st.get("overlay", False)
950
  st["log"].append(f"Overlay set to {st['overlay']}.")
951
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
952
 
953
  def ui_tick(st):
954
- tick(st)
955
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
 
 
956
 
957
  def ui_timer(st):
 
958
  if st["auto_run"] and not st["caught"]:
959
- tick(st)
960
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
961
 
962
  def ui_swap_roles(st):
963
- # Optional: swap predator <-> prey positions/orientations (symmetry hammer)
964
  if st["caught"]:
965
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
966
-
967
  st["pred"], st["prey"] = st["prey"], st["pred"]
968
  st["ori"], st["prey_ori"] = st["prey_ori"], st["ori"]
969
  st["log"].append("Swapped roles (Predator ⇄ Prey).")
970
  _add_impulse(st, 0.35)
971
- _check_catch_and_unlock(st)
972
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
 
 
973
 
974
  # ---- Save/load UI handlers ----
975
  def ui_save_slot(st, slot_name):
@@ -990,28 +977,28 @@ def ui_load_slot(st, selected_slot):
990
  if not os.path.exists(path):
991
  st["log"].append(f"No save found at {path}")
992
  dd = ui_refresh_slots(selected_slot)
993
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st), None, dd
994
  loaded = load_from_path(path)
995
  dd = ui_refresh_slots(os.path.basename(path))
996
  return loaded, render_first_person(loaded), render_minimap(loaded), status(loaded), unlock_summary(loaded), None, dd
997
  except Exception as e:
998
  st["log"].append(f"Load failed: {e}")
999
  dd = ui_refresh_slots(selected_slot)
1000
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st), None, dd
1001
 
1002
  def ui_import_save(st, uploaded_file):
1003
  try:
1004
  if uploaded_file is None:
1005
  st["log"].append("Import: no file provided.")
1006
  dd = ui_refresh_slots()
1007
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st), None, dd
1008
  loaded = load_from_path(uploaded_file)
1009
  dd = ui_refresh_slots()
1010
  return loaded, render_first_person(loaded), render_minimap(loaded), status(loaded), unlock_summary(loaded), None, dd
1011
  except Exception as e:
1012
  st["log"].append(f"Import failed: {e}")
1013
  dd = ui_refresh_slots()
1014
- return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st), None, dd
1015
 
1016
  # -----------------------------
1017
  # App
@@ -1026,9 +1013,8 @@ initial_slot_value = initial_slots[0] if initial_slots else "slot1.json"
1026
  with gr.Blocks(title="RFT Predator Space — Symmetric Observers") as demo:
1027
  gr.Markdown(
1028
  "## Experience reality through an RFT observer agent’s perspective\n"
1029
- "Two symmetric observers share the same frame.\n\n"
1030
- "**Hybrid mode:** AutoRun ON + AutoChase OFF ⇒ predator wanders autonomously while prey flees (unless you control it).\n"
1031
- "**Symmetry:** Toggle control to view/drive either observer."
1032
  )
1033
 
1034
  st = gr.State(initial_state)
@@ -1081,7 +1067,6 @@ with gr.Blocks(title="RFT Predator Space — Symmetric Observers") as demo:
1081
  )
1082
 
1083
  btn_reset.click(ui_reset, inputs=[seed, map_choice, st], outputs=[st, view, mini, info, unlocks])
1084
-
1085
  btn_control.click(ui_toggle_control, inputs=[st], outputs=[st, view, mini, info, unlocks])
1086
 
1087
  btn_left.click(ui_turn_left, inputs=[st], outputs=[st, view, mini, info, unlocks])
@@ -1115,7 +1100,13 @@ with gr.Blocks(title="RFT Predator Space — Symmetric Observers") as demo:
1115
  outputs=[st, view, mini, info, unlocks, export_file, slot_pick]
1116
  )
1117
 
 
1118
  if hasattr(gr, "Timer"):
1119
- gr.Timer(1.0 / AUTO_TICK_HZ).tick(ui_timer, inputs=[st], outputs=[st, view, mini, info, unlocks])
1120
-
1121
- demo.launch()
 
 
 
 
 
 
7
 
8
  # ============================================================
9
  # RFT Predator Space — First-Person Observer View (Pseudo-3D)
10
+ # FIXES:
11
+ # 1) NO flashing progression panel (do NOT update it on timer ticks)
12
+ # 2) AutoRun works in first-person POV (autopilot moves current POV agent)
13
+ # 3) queue() enabled for reliable timer updates on Spaces
 
14
  # ============================================================
15
 
16
  # -----------------------------
 
67
 
68
  def list_save_slots():
69
  try:
70
+ files = [fn for fn in os.listdir(SAVE_DIR) if fn.lower().endswith(".json")]
 
 
 
71
  files.sort()
72
  return files
73
  except Exception:
 
296
 
297
  "control": "pred", # "pred" or "prey" (view + manual inputs)
298
  "overlay": False, # coherence overlay
299
+ "disturbance": 0.0,
300
+ "last_impulse": 0.0,
301
 
302
  "step": 0,
303
  "caught": False,
 
327
  "seed": int(st["seed"]),
328
  "map_name": str(st["map_name"]),
329
  "step": int(st["step"]),
 
330
  "pred": [int(st["pred"][0]), int(st["pred"][1])],
331
  "prey": [int(st["prey"][0]), int(st["prey"][1])],
 
332
  "ori": int(st["ori"]),
333
  "prey_ori": int(st.get("prey_ori", 0)),
 
334
  "control": str(st.get("control", "pred")),
335
  "overlay": bool(st.get("overlay", False)),
336
  "disturbance": float(st.get("disturbance", 0.0)),
 
337
  "caught": bool(st["caught"]),
338
  "auto_chase": bool(st["auto_chase"]),
339
  "auto_run": bool(st["auto_run"]),
 
340
  "catches": catches,
341
  "log_tail": st["log"][-20:],
342
  }
 
355
  "step": int(payload.get("step", 0)),
356
  "pred": tuple(payload.get("pred", [1, 1])),
357
  "prey": tuple(payload.get("prey", [2, 2])),
 
358
  "ori": int(payload.get("ori", 0)) % 4,
359
  "prey_ori": int(payload.get("prey_ori", 0)) % 4,
 
360
  "control": str(payload.get("control", "pred")) if str(payload.get("control", "pred")) in ("pred", "prey") else "pred",
361
  "overlay": bool(payload.get("overlay", False)),
362
  "disturbance": float(payload.get("disturbance", 0.0)),
363
  "last_impulse": 0.0,
 
364
  "caught": bool(payload.get("caught", False)),
365
  "auto_chase": bool(payload.get("auto_chase", False)),
366
  "auto_run": bool(payload.get("auto_run", False)),
 
367
  "log": (payload.get("log_tail", []) or [])[:],
368
  }
369
 
370
  st = build_state(seed, map_name, progress=progress, override=override)
371
 
 
372
  grid = st["grid"]
373
  H, W = grid.shape
374
  px, py = st["pred"]
 
380
  if not ok:
381
  rng = seeded_rng(seed + 777)
382
  st["pred"], st["prey"] = pick_spawn_pair(grid, rng, min_dist=8)
383
+ st["log"].append("Loaded save had invalid positions; respawned safely.")
384
 
385
  st["log"].append("Loaded save.")
386
  return st
 
477
  return perp, side, map_x, map_y
478
 
479
  def _apply_coherence_overlay(img, disturbance: float):
 
480
  d = float(disturbance)
481
  if d <= 0.001:
482
  return img
483
 
484
+ alpha = clamp(d * 0.06, 0.0, 0.22) # subtle
485
  h, w, _ = img.shape
486
  cx, cy = w // 2, h // 2
487
 
 
488
  edge = int(min(w, h) * 0.08)
489
  if edge >= 2:
490
+ tint = np.array([22, 8, 18], dtype=np.float32)
 
491
  img[:edge, :, :] = np.clip(img[:edge, :, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
 
492
  img[h-edge:, :, :] = np.clip(img[h-edge:, :, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
 
493
  img[:, :edge, :] = np.clip(img[:, :edge, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
 
494
  img[:, w-edge:, :] = np.clip(img[:, w-edge:, :].astype(np.float32) + tint * alpha, 0, 255).astype(np.uint8)
495
 
496
+ line_col = np.array([180, 70, 160], dtype=np.float32)
 
497
  for i in range(-40, 41):
498
  x = cx + i
499
  y = cy + int(i * 0.35)
 
509
  def render_first_person(st):
510
  grid = st["grid"]
511
 
 
512
  if st["control"] == "prey":
513
  view_cell = st["prey"]
514
  view_ori = st["prey_ori"]
 
564
  x1 = x0 + 1
565
  img[top:bot, x0:x1, :] = col
566
 
 
567
  other_vis = False
568
  if not st["caught"] and los_clear(grid, view_cell, other_cell):
569
  vx = (other_cell[0] + 0.5) - px
 
596
  if other_dist < wall_dists[rx]:
597
  img[y0:y1, vxcol:vxcol+1, :] = AGENT_OTHER_COLOR
598
 
 
599
  cxh, cyh = VIEW_W // 2, VIEW_H // 2
600
  img[cyh-1:cyh+2, cxh-12:cxh+13, :] = RETICLE
601
  img[cyh-12:cyh+13, cxh-1:cxh+2, :] = RETICLE
602
 
 
603
  hud_h = 26
604
  img[:hud_h, :, :] = np.clip(img[:hud_h, :, :].astype(np.int16) + 20, 0, 255).astype(np.uint8)
605
 
 
 
 
 
606
  def dot(x, y, c):
607
  img[y:y+6, x:x+6, :] = c
608
 
 
610
  dot(20, 10, np.array([120, 190, 255], np.uint8) if st["auto_run"] else np.array([60, 60, 70], np.uint8))
611
  dot(32, 10, np.array([255, 140, 90], np.uint8) if other_vis else np.array([60, 60, 70], np.uint8))
612
 
 
613
  if st.get("overlay", False):
614
  img = _apply_coherence_overlay(img, st.get("disturbance", 0.0))
615
 
 
629
 
630
  px, py = st["pred"]
631
  qx, qy = st["prey"]
 
632
  pred_col = np.array([120, 190, 255], np.uint8)
633
  prey_col = np.array([255, 140, 90], np.uint8)
634
 
635
  img[py*scale:(py+1)*scale, px*scale:(px+1)*scale, :] = pred_col
636
  img[qy*scale:(qy+1)*scale, qx*scale:(qx+1)*scale, :] = prey_col
637
 
 
638
  dx, dy = DIRS[st["ori"]]
639
  hx, hy = px + dx, py + dy
640
  if 0 <= hx < W and 0 <= hy < H:
 
645
  if 0 <= hx2 < W and 0 <= hy2 < H:
646
  img[hy2*scale:(hy2+1)*scale, hx2*scale:(hx2+1)*scale, :] = np.array([255, 220, 120], np.uint8)
647
 
 
648
  if st["control"] == "pred":
649
  x0, y0 = px*scale, py*scale
650
  else:
 
675
  catches = st["progress"]["catches"]
676
  current = st["map_name"]
677
 
 
678
  mode = "Manual"
679
  if st["auto_run"] and st["auto_chase"]:
680
+ mode = "AutoRun+AutoChase"
681
  elif st["auto_run"] and not st["auto_chase"]:
682
+ mode = "Hybrid AutoRun (wander)"
683
 
684
  ctrl = "Predator" if st["control"] == "pred" else "Prey"
685
  coh = st.get("disturbance", 0.0)
 
697
  st["last_impulse"] = float(st.get("last_impulse", 0.0)) + float(x)
698
 
699
  def _step_disturbance(st):
 
700
  d = float(st.get("disturbance", 0.0))
701
  imp = float(st.get("last_impulse", 0.0))
702
  st["disturbance"] = 0.92 * d + imp
 
715
  if pos is not None: st["pred"] = pos
716
  if ori is not None: st["ori"] = int(ori) % 4
717
 
718
+ def _turn(st, who, direction):
719
  if st["caught"]:
720
  return
721
+ _, ori = _agent_pos_ori(st, who)
722
  ori = (ori + direction) % 4
723
  _set_agent_pos_ori(st, who, ori=ori)
724
+ _add_impulse(st, 0.9)
725
 
726
  def _forward(st, who):
727
  if st["caught"]:
 
744
  st["progress"]["unlocked"] = compute_unlocks(st["progress"]["catches"])
745
  st["log"].append(f"Catches now {st['progress']['catches']}. Unlocks updated.")
746
  _add_impulse(st, 1.2)
747
+ return True
748
+ return False
749
 
750
  def prey_flee_step(st):
 
751
  if st["caught"]:
752
  return
753
  rng = seeded_rng(st["seed"] + 1337 + st["step"] * 19)
 
772
  st["prey_ori"] = DIR_TO_ORI[(dx, dy)]
773
 
774
  def predator_wander_step(st):
 
775
  if st["caught"]:
776
  return
777
  rng = seeded_rng(st["seed"] + 4242 + st["step"] * 23)
 
780
  dx, dy = DIRS[ori]
781
  front_blocked = (st["grid"][y+dy, x+dx] == 1)
782
 
 
783
  r = rng.random()
784
  if front_blocked:
785
  if r < 0.5:
 
795
  _turn(st, "pred", +1); st["log"].append("AutoWander: turn right.")
796
 
797
  def predator_chase_step(st):
 
798
  if st["caught"]:
799
  return
800
  grid = st["grid"]
 
817
  else:
818
  _forward(st, "pred"); st["log"].append("AutoChase: forward.")
819
  return
 
 
820
  predator_wander_step(st)
821
 
822
+ def prey_autopilot_step(st):
823
+ # if user is viewing prey and AutoRun is on, prey should still behave like prey (flee)
824
+ prey_flee_step(st)
825
+ st["log"].append("AutoPrey: flee.")
826
+
827
  def tick(st):
828
  if st["caught"]:
829
+ return False # unlock did not change
830
 
831
  st["step"] += 1
832
+ unlock_changed = False
833
 
834
+ # AutoRun = autopilot for the currently viewed observer
835
+ if st["auto_run"]:
836
+ if st["control"] == "pred":
837
+ if st["auto_chase"]:
838
+ predator_chase_step(st)
839
+ else:
840
+ predator_wander_step(st)
841
+ else:
842
+ prey_autopilot_step(st)
843
+
844
+ # The non-controlled agent still runs its own policy each step
845
+ if st["control"] != "pred":
846
  if st["auto_chase"]:
847
  predator_chase_step(st)
848
  else:
849
  predator_wander_step(st)
850
 
 
851
  if st["control"] != "prey":
852
  prey_flee_step(st)
853
 
854
+ # capture + unlock
855
+ unlock_changed = _check_catch_and_unlock(st)
856
+
857
  _step_disturbance(st)
858
 
859
  if st["step"] >= 600:
860
  st["caught"] = True
861
  st["log"].append("Max steps reached (freeze).")
862
 
863
+ return unlock_changed
864
+
865
  # -----------------------------
866
  # Gradio handlers
867
  # -----------------------------
 
879
  if map_choice not in progress["unlocked"]:
880
  map_choice = "Training Bay"
881
  new_st = build_state(seed, map_choice, progress=progress)
 
882
  if st:
883
  new_st["control"] = st.get("control", "pred")
884
  new_st["overlay"] = st.get("overlay", False)
 
894
  who = st["control"]
895
  _turn(st, who, -1)
896
  st["log"].append(f"{'Prey' if who=='prey' else 'Predator'} turn left.")
897
+ unlock_changed = tick(st)
898
+ if unlock_changed:
899
+ return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
900
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update() # avoid re-render if unchanged
901
 
902
  def ui_turn_right(st):
903
  who = st["control"]
904
  _turn(st, who, +1)
905
  st["log"].append(f"{'Prey' if who=='prey' else 'Predator'} turn right.")
906
+ unlock_changed = tick(st)
907
+ if unlock_changed:
908
+ return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
909
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
910
 
911
  def ui_forward(st):
912
  who = st["control"]
913
  _forward(st, who)
914
  st["log"].append(f"{'Prey' if who=='prey' else 'Predator'} forward.")
915
+ unlock_changed = tick(st)
916
+ if unlock_changed:
917
+ return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
918
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
919
 
920
  def ui_toggle_chase(st):
921
  st["auto_chase"] = not st["auto_chase"]
922
  st["log"].append(f"AutoChase set to {st['auto_chase']}.")
923
  _add_impulse(st, 0.10)
924
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
925
 
926
  def ui_toggle_run(st):
927
  st["auto_run"] = not st["auto_run"]
928
  st["log"].append(f"AutoRun set to {st['auto_run']}.")
929
  _add_impulse(st, 0.10)
930
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
931
 
932
  def ui_toggle_overlay(st):
933
  st["overlay"] = not st.get("overlay", False)
934
  st["log"].append(f"Overlay set to {st['overlay']}.")
935
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
936
 
937
  def ui_tick(st):
938
+ unlock_changed = tick(st)
939
+ if unlock_changed:
940
+ return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
941
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
942
 
943
  def ui_timer(st):
944
+ # IMPORTANT: do NOT update unlock markdown here (prevents flashing)
945
  if st["auto_run"] and not st["caught"]:
946
+ _ = tick(st)
947
+ return st, render_first_person(st), render_minimap(st), status(st)
948
 
949
  def ui_swap_roles(st):
 
950
  if st["caught"]:
951
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
 
952
  st["pred"], st["prey"] = st["prey"], st["pred"]
953
  st["ori"], st["prey_ori"] = st["prey_ori"], st["ori"]
954
  st["log"].append("Swapped roles (Predator ⇄ Prey).")
955
  _add_impulse(st, 0.35)
956
+ changed = _check_catch_and_unlock(st)
957
+ if changed:
958
+ return st, render_first_person(st), render_minimap(st), status(st), unlock_summary(st)
959
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update()
960
 
961
  # ---- Save/load UI handlers ----
962
  def ui_save_slot(st, slot_name):
 
977
  if not os.path.exists(path):
978
  st["log"].append(f"No save found at {path}")
979
  dd = ui_refresh_slots(selected_slot)
980
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update(), None, dd
981
  loaded = load_from_path(path)
982
  dd = ui_refresh_slots(os.path.basename(path))
983
  return loaded, render_first_person(loaded), render_minimap(loaded), status(loaded), unlock_summary(loaded), None, dd
984
  except Exception as e:
985
  st["log"].append(f"Load failed: {e}")
986
  dd = ui_refresh_slots(selected_slot)
987
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update(), None, dd
988
 
989
  def ui_import_save(st, uploaded_file):
990
  try:
991
  if uploaded_file is None:
992
  st["log"].append("Import: no file provided.")
993
  dd = ui_refresh_slots()
994
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update(), None, dd
995
  loaded = load_from_path(uploaded_file)
996
  dd = ui_refresh_slots()
997
  return loaded, render_first_person(loaded), render_minimap(loaded), status(loaded), unlock_summary(loaded), None, dd
998
  except Exception as e:
999
  st["log"].append(f"Import failed: {e}")
1000
  dd = ui_refresh_slots()
1001
+ return st, render_first_person(st), render_minimap(st), status(st), gr.update(), None, dd
1002
 
1003
  # -----------------------------
1004
  # App
 
1013
  with gr.Blocks(title="RFT Predator Space — Symmetric Observers") as demo:
1014
  gr.Markdown(
1015
  "## Experience reality through an RFT observer agent’s perspective\n"
1016
+ "**Accessibility note:** the progression panel is now event-driven (no flashing).\n\n"
1017
+ "**AutoRun:** moves the current POV observer (first-person autopilot)."
 
1018
  )
1019
 
1020
  st = gr.State(initial_state)
 
1067
  )
1068
 
1069
  btn_reset.click(ui_reset, inputs=[seed, map_choice, st], outputs=[st, view, mini, info, unlocks])
 
1070
  btn_control.click(ui_toggle_control, inputs=[st], outputs=[st, view, mini, info, unlocks])
1071
 
1072
  btn_left.click(ui_turn_left, inputs=[st], outputs=[st, view, mini, info, unlocks])
 
1100
  outputs=[st, view, mini, info, unlocks, export_file, slot_pick]
1101
  )
1102
 
1103
+ # Timer outputs DO NOT include unlocks (prevents flashing)
1104
  if hasattr(gr, "Timer"):
1105
+ gr.Timer(1.0 / AUTO_TICK_HZ).tick(
1106
+ ui_timer,
1107
+ inputs=[st],
1108
+ outputs=[st, view, mini, info]
1109
+ )
1110
+
1111
+ # queue() helps Timer behave reliably in Spaces
1112
+ demo.queue().launch()