Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,11 +7,10 @@ import gradio as gr
|
|
| 7 |
|
| 8 |
# ============================================================
|
| 9 |
# RFT Predator Space — First-Person Observer View (Pseudo-3D)
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
#
|
| 13 |
-
#
|
| 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,
|
| 304 |
-
"last_impulse": 0.0,
|
| 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
|
| 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) #
|
| 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)
|
| 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 |
-
|
| 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
|
| 715 |
elif st["auto_run"] and not st["auto_chase"]:
|
| 716 |
-
mode = "Hybrid AutoRun (
|
| 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):
|
| 754 |
if st["caught"]:
|
| 755 |
return
|
| 756 |
-
|
| 757 |
ori = (ori + direction) % 4
|
| 758 |
_set_agent_pos_ori(st, who, ori=ori)
|
| 759 |
-
_add_impulse(st, 0.9)
|
| 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 |
-
#
|
| 868 |
-
if st["auto_run"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 933 |
-
|
| 934 |
-
|
|
|
|
| 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),
|
| 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),
|
| 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),
|
| 952 |
|
| 953 |
def ui_tick(st):
|
| 954 |
-
tick(st)
|
| 955 |
-
|
|
|
|
|
|
|
| 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)
|
| 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),
|
| 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 |
-
|
|
|
|
|
|
|
| 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),
|
| 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),
|
| 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),
|
| 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),
|
| 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 |
-
"
|
| 1030 |
-
"**
|
| 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(
|
| 1120 |
-
|
| 1121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|