ask4rakesh/RLTestSimulation
0
1"""2GridWorld Navigator — Q-Learning RL Edition3 • Manual play tab (keyboard / buttons)4 • RL Training tab (watch the agent learn live)5"""6 7import gymnasium as gym8from gymnasium import spaces9import numpy as np10from collections import deque11import random, threading, time, json12import gradio as gr13 14# ═══════════════════════════════════════════════════════════════════════════════15# GridWorld Environment16# ═══════════════════════════════════════════════════════════════════════════════17 18GRID_SIZE = 1019MAX_STEPS = 15020OBSTACLE_DENSITY = 0.2021 22REWARD_GOAL = 10.023REWARD_CLOSER = 0.524REWARD_FARTHER = -0.325REWARD_NEW = 0.126REWARD_WALL = -0.527REWARD_STEP = -0.0128REWARD_TIMEOUT = -2.029 30 31def manhattan(a, b):32 return abs(a[0]-b[0]) + abs(a[1]-b[1])33 34 35def is_solvable(grid, start, goal, size):36 visited, q = {start}, deque([start])37 while q:38 r, c = q.popleft()39 if (r,c) == goal: return True40 for dr, dc in [(-1,0),(0,1),(1,0),(0,-1)]:41 nr, nc = r+dr, c+dc42 if 0<=nr<size and 0<=nc<size and (nr,nc) not in visited and grid[nr][nc]!='X':43 visited.add((nr,nc)); q.append((nr,nc))44 return False45 46 47def generate_maze(size=GRID_SIZE, density=OBSTACLE_DENSITY):48 for _ in range(2000):49 grid = [['.' for _ in range(size)] for _ in range(size)]50 cells = [(r,c) for r in range(size) for c in range(size)]51 random.shuffle(cells)52 start, goal = cells[0], cells[1]53 for r,c in cells[2:]:54 if random.random() < density:55 grid[r][c] = 'X'56 grid[start[0]][start[1]] = 'S'57 grid[goal[0]][goal[1]] = 'G'58 if is_solvable(grid, start, goal, size):59 return grid, start, goal60 grid = [['.' for _ in range(size)] for _ in range(size)]61 grid[0][0]='S'; grid[size-1][size-1]='G'62 return grid, (0,0), (size-1,size-1)63 64 65class GridWorldEnv(gym.Env):66 def __init__(self, size=GRID_SIZE, fixed_maze=None):67 super().__init__()68 self.size = size69 self.fixed_maze = fixed_maze # if set, never regenerate70 self.observation_space = spaces.Box(0.,1.,shape=(4,),dtype=np.float32)71 self.action_space = spaces.Discrete(4)72 self._reset_internals()73 74 def _reset_internals(self):75 if self.fixed_maze:76 import copy77 self.grid, self.start, self.goal = copy.deepcopy(self.fixed_maze)78 else:79 self.grid, self.start, self.goal = generate_maze(self.size)80 self.agent_pos = list(self.start)81 self.steps = 082 self.total_score= 0.83 self.last_reward= 0.84 self.visited = {tuple(self.agent_pos)}85 self.done = False86 87 def _obs(self):88 ax,ay = self.agent_pos; gx,gy = self.goal; s=self.size-189 return np.array([ax/s,ay/s,gx/s,gy/s],dtype=np.float32)90 91 def reset(self,*,seed=None,options=None):92 super().reset(seed=seed); self._reset_internals()93 return self._obs(),{}94 95 def step(self,action):96 if self.done: return self._obs(),0.,True,False,{}97 dr,dc=[(-1,0),(0,1),(1,0),(0,-1)][action]98 nr,nc = self.agent_pos[0]+dr, self.agent_pos[1]+dc99 prev_dist = manhattan(self.agent_pos,self.goal)100 101 if not(0<=nr<self.size and 0<=nc<self.size) or self.grid[nr][nc]=='X':102 rew = REWARD_WALL+REWARD_STEP103 self.last_reward=rew; self.total_score+=rew; self.steps+=1104 trunc = self.steps>=MAX_STEPS105 if trunc: rew+=REWARD_TIMEOUT; self.total_score+=REWARD_TIMEOUT; self.done=True106 return self._obs(),rew,False,trunc,{}107 108 self.agent_pos=[nr,nc]; new_dist=manhattan(self.agent_pos,self.goal); self.steps+=1109 rew = REWARD_STEP110 if tuple(self.agent_pos) not in self.visited:111 rew+=REWARD_NEW; self.visited.add(tuple(self.agent_pos))112 rew += REWARD_CLOSER if new_dist<prev_dist else (REWARD_FARTHER if new_dist>prev_dist else 0)113 114 if (nr,nc)==self.goal:115 rew+=REWARD_GOAL; self.total_score+=rew; self.last_reward=rew116 self.done=True; return self._obs(),rew,True,False,{}117 118 trunc = self.steps>=MAX_STEPS119 if trunc: rew+=REWARD_TIMEOUT; self.done=True120 self.total_score+=rew; self.last_reward=rew121 return self._obs(),rew,False,trunc,{}122 123 124# ═══════════════════════════════════════════════════════════════════════════════125# Q-Learning Agent (tabular, state = (agent_row, agent_col))126# ═══════════════════════════════════════════════════════════════════════════════127 128class QAgent:129 def __init__(self, size=GRID_SIZE, lr=0.2, gamma=0.95, eps=1.0,130 eps_min=0.05, eps_decay=0.997):131 self.size=size; self.lr=lr; self.gamma=gamma132 self.eps=eps; self.eps_min=eps_min; self.eps_decay=eps_decay133 self.Q = np.zeros((size,size,4), dtype=np.float32)134 135 def act(self, pos, explore=True):136 if explore and random.random()<self.eps:137 return random.randint(0,3)138 return int(np.argmax(self.Q[pos[0],pos[1]]))139 140 def learn(self, pos, action, reward, npos, done):141 q_curr = self.Q[pos[0],pos[1],action]142 q_next = 0. if done else float(np.max(self.Q[npos[0],npos[1]]))143 self.Q[pos[0],pos[1],action] += self.lr*(reward+self.gamma*q_next-q_curr)144 145 def decay_eps(self):146 self.eps = max(self.eps_min, self.eps*self.eps_decay)147 148 def reset(self):149 self.Q[:] = 0.150 self.eps = 1.0151 152 153# ═══════════════════════════════════════════════════════════════════════════════154# Shared RL state (written by training thread, read by UI)155# ═══════════════════════════════════════════════════════════════════════════════156 157rl_lock = threading.Lock()158 159rl_state = dict(160 running = False,161 episode = 0,162 eps = 1.0,163 ep_reward = 0.,164 steps = 0,165 agent_pos = [0,0],166 goal = (0,0),167 grid = None,168 visited_trail = set(), # cells visited this episode169 success_history = [], # list of bool per episode170 reward_history = [], # cumulative reward per episode171 Q_values = None, # snapshot for heatmap172 status = "idle", # idle / training / paused / done173 msg = "Press ▶ Start Training to begin.",174 speed = 0.06, # seconds between steps175 total_episodes = 300,176)177 178rl_agent = QAgent()179rl_env = GridWorldEnv()180 181_stop_event = threading.Event()182_pause_event = threading.Event() # set = paused183 184 185def rl_train_thread(total_episodes, speed_getter):186 global rl_state, rl_agent, rl_env187 188 # Fix the maze for the whole training run so the agent learns one map189 grid, start, goal = generate_maze()190 import copy191 fixed = (copy.deepcopy(grid), start, goal)192 rl_env = GridWorldEnv(fixed_maze=fixed)193 rl_agent.reset()194 195 for ep in range(1, total_episodes+1):196 if _stop_event.is_set(): break197 198 obs, _ = rl_env.reset()199 ep_rew = 0.200 visited = {tuple(rl_env.agent_pos)}201 202 with rl_lock:203 rl_state['episode'] = ep204 rl_state['eps'] = rl_agent.eps205 rl_state['grid'] = copy.deepcopy(rl_env.grid)206 rl_state['goal'] = rl_env.goal207 rl_state['agent_pos'] = list(rl_env.agent_pos)208 rl_state['visited_trail'] = set(visited)209 rl_state['status'] = 'training'210 rl_state['msg'] = f"Episode {ep}/{total_episodes} | ε={rl_agent.eps:.3f}"211 212 while not rl_env.done:213 if _stop_event.is_set(): return214 while _pause_event.is_set():215 time.sleep(0.05)216 if _stop_event.is_set(): return217 218 pos = list(rl_env.agent_pos)219 action = rl_agent.act(pos)220 obs2, rew, term, trunc, _ = rl_env.step(action)221 npos = list(rl_env.agent_pos)222 rl_agent.learn(pos, action, rew, npos, term or trunc)223 ep_rew += rew224 visited.add(tuple(npos))225 226 with rl_lock:227 rl_state['agent_pos'] = list(npos)228 rl_state['ep_reward'] = ep_rew229 rl_state['steps'] = rl_env.steps230 rl_state['visited_trail'] = set(visited)231 rl_state['eps'] = rl_agent.eps232 rl_state['msg'] = (233 f"Episode {ep}/{total_episodes} | "234 f"step {rl_env.steps} | ε={rl_agent.eps:.3f} | "235 f"reward {ep_rew:+.1f}"236 )237 238 spd = speed_getter()239 time.sleep(max(0.001, spd))240 241 success = (tuple(rl_env.agent_pos) == rl_env.goal)242 with rl_lock:243 rl_state['success_history'].append(success)244 rl_state['reward_history'].append(ep_rew)245 rl_state['Q_values'] = rl_agent.Q.copy()246 if ep >= total_episodes:247 rl_state['status'] = 'done'248 rl_state['msg'] = f"Training complete! {total_episodes} episodes."249 250 rl_agent.decay_eps()251 252 with rl_lock:253 rl_state['status'] = 'done'254 rl_state['running'] = False255 256 257# ═══════════════════════════════════════════════════════════════════════════════258# HTML renderers259# ═══════════════════════════════════════════════════════════════════════════════260 261CELL_PX = 44262 263 264def _cell_bg(r, c, ap, goal, grid, trail, Q=None):265 pos = (r,c)266 if pos == tuple(ap):267 return 'agent'268 cell = grid[r][c]269 if cell == 'X': return 'obs'270 if cell == 'S': return 'start'271 if pos == goal: return 'goal'272 if pos in trail: return 'trail'273 return 'empty'274 275 276def render_grid_html(grid, ap, goal, trail=None, Q=None, show_q=False):277 if grid is None: return "<p style='color:#8899bb;font-family:sans-serif;padding:20px'>Press ▶ Start to begin training…</p>"278 trail = trail or set()279 size = len(grid)280 281 styles = dict(282 agent = "background:radial-gradient(circle at 38% 35%,#ff6b6b,#e84393);",283 obs = "background:#8899aa;",284 start = "background:#4a90d9;color:#fff;font-weight:700;",285 goal = "background:#27ae60;color:#fff;font-weight:700;",286 trail = "background:#c8ecd4;",287 empty = "background:#eef2ff;",288 )289 290 out = f"""<style>291.gw{{display:inline-block;border:3px solid #c0c8e0;border-radius:8px;overflow:hidden;box-shadow:0 4px 24px #a0b0c044}}292.gw table{{border-collapse:collapse;table-layout:fixed}}293.gw td{{width:{CELL_PX}px;height:{CELL_PX}px;text-align:center;vertical-align:middle;294 font-family:monospace;font-size:13px;border:1px solid #dde4f0;position:relative}}295.gw-a{{display:inline-block;width:30px;height:30px;296 background:radial-gradient(circle at 38% 35%,#ff6b6b,#e84393);297 border-radius:50%;box-shadow:0 0 14px #e84393aa;border:2px solid #ffb3d9}}298.gw-q{{font-size:9px;color:#6655aa;line-height:1.1}}299</style><div class="gw"><table>"""300 301 for r in range(size):302 out += "<tr>"303 for c in range(size):304 kind = _cell_bg(r,c,ap,goal,grid,trail,Q)305 st = styles[kind]306 307 # Q-value overlay on non-special cells308 if show_q and Q is not None and kind in ('empty','trail'):309 best_a = int(np.argmax(Q[r,c]))310 arrows = ['↑','→','↓','←']311 qv = Q[r,c,best_a]312 inner = f'<div class="gw-q">{arrows[best_a]}<br>{qv:.1f}</div>'313 # colour by best Q314 mn,mx = Q.min(), Q.max()315 t = (qv-mn)/(mx-mn+1e-9)316 r_hex = int(200+t*20); g_hex=int(220+t*35); b_hex=int(255-t*80)317 st = f"background:rgb({r_hex},{g_hex},{b_hex});"318 elif kind=='agent':319 inner = '<div class="gw-a"></div>'320 elif kind=='goal':321 inner = 'G'322 elif kind=='start':323 inner = 'S'324 else:325 inner = ''326 327 out += f'<td style="{st}">{inner}</td>'328 out += "</tr>"329 out += "</table></div>"330 return out331 332 333def render_scoreboard(ep, total_ep, eps, ep_rew, steps, agent_pos, goal,334 success_hist, reward_hist, status):335 dist = manhattan(agent_pos, goal)336 recent = success_hist[-20:] if success_hist else []337 win_rate = sum(recent)/len(recent)*100 if recent else 0.338 339 bar_len = 14340 filled = int(bar_len * (ep / max(total_ep,1)))341 prog_bar= '█'*filled + '░'*(bar_len-filled)342 343 # sparkline for last 30 rewards344 rh = reward_hist[-30:] if reward_hist else []345 if rh:346 mn,mx = min(rh),max(rh)347 rng = mx-mn or 1348 SPARKS = '▁▂▃▄▅▆▇█'349 spark = ''.join(SPARKS[min(7,int((v-mn)/rng*7))] for v in rh)350 else:351 spark = '—'352 353 status_color = {'idle':'#6677aa','training':'#7bcde0','paused':'#f0a500',354 'done':'#22dd6a'}.get(status,'#aaa')355 356 rows = [357 ("Episode", f"{ep} / {total_ep}"),358 ("Progress", f'<span style="letter-spacing:1px;color:#5566cc">{prog_bar}</span>'),359 ("ε (explore)",f"{eps:.4f}"),360 ("Ep Reward", f"{ep_rew:+.2f}"),361 ("Steps", str(steps)),362 ("Agent", f"({agent_pos[0]},{agent_pos[1]})"),363 ("Goal", f"({goal[0]},{goal[1]})"),364 ("Distance", str(dist)),365 ("Win rate", f"{win_rate:.0f}% (last 20)"),366 ("Rewards ↗", f'<span style="font-size:10px;letter-spacing:0">{spark}</span>'),367 ("Status", f'<span style="color:{status_color}">{status.upper()}</span>'),368 ]369 370 html = """<style>371.sb{font-family:monospace;background:#ffffff;border-radius:10px;372 padding:16px 20px;border:2px solid #c8d4f0;box-shadow:0 4px 20px #a0b0c030}373.sb h2{color:#5566cc;margin:0 0 12px;font-size:14px;letter-spacing:2px}374.sb-row{display:flex;justify-content:space-between;gap:12px;375 padding:4px 0;border-bottom:1px solid #e0e8f8}376.sb-row:last-child{border-bottom:none}377.sb-l{color:#778;font-size:12px}.sb-v{color:#223;font-size:12px;font-weight:700}378</style><div class="sb"><h2>📊 RL Scoreboard</h2>"""379 for lbl,val in rows:380 html += f'<div class="sb-row"><span class="sb-l">{lbl}</span><span class="sb-v">{val}</span></div>\n'381 html += "</div>"382 return html383 384 385# ═══════════════════════════════════════════════════════════════════════════════386# Manual-play singletons387# ═══════════════════════════════════════════════════════════════════════════════388 389manual_env = GridWorldEnv()390manual_env.reset()391 392 393def manual_reset():394 manual_env.reset()395 g = render_grid_html(manual_env.grid, manual_env.agent_pos, manual_env.goal)396 return g, _manual_score(), "New maze! Navigate to the green G cell."397 398 399def _manual_score():400 e = manual_env401 ax,ay=e.agent_pos; gx,gy=e.goal402 reached = e.done and (ax,ay)==e.goal403 tout = e.done and not reached404 sc = '#22863a' if reached else ('#e74c3c' if tout else '#aaa')405 st = '✅ GOAL!' if reached else ('⏰ TIMEOUT' if tout else '▶ Playing')406 rows=[("Score",f"{e.total_score:+.2f}"),("Last Reward",f"{e.last_reward:+.2f}"),407 ("Steps",f"{e.steps}/{MAX_STEPS}"),("Agent",f"({ax},{ay})"),408 ("Goal",f"({gx},{gy})"),("Distance",str(manhattan(e.agent_pos,e.goal))),409 ("Status",f'<span style="color:{sc}">{st}</span>')]410 h='<style>.msb{font-family:monospace;background:#ffffff;border-radius:10px;padding:14px 18px;border:2px solid #c8d4f0;box-shadow:0 4px 20px #a0b0c030}.msb h2{color:#5566cc;margin:0 0 10px;font-size:13px;letter-spacing:2px}.mr{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid #e0e8f8}.mr:last-child{border-bottom:none}.ml{color:#778;font-size:12px}.mv{color:#223;font-size:12px;font-weight:700}</style><div class="msb"><h2>📊 Score</h2>'411 for l,v in rows:412 h+=f'<div class="mr"><span class="ml">{l}</span><span class="mv">{v}</span></div>'413 return h+'</div>'414 415 416def manual_step(action):417 if manual_env.done:418 return render_grid_html(manual_env.grid,manual_env.agent_pos,manual_env.goal), _manual_score(), "Game over — press Reset."419 manual_env.step(action)420 names=["⬆ UP","➡ RIGHT","⬇ DOWN","⬅ LEFT"]421 msg=f"Moved {names[action]} | reward {manual_env.last_reward:+.2f}"422 if manual_env.done:423 msg="🎉 Goal reached!" if tuple(manual_env.agent_pos)==manual_env.goal else "⏰ Timed out."424 return render_grid_html(manual_env.grid,manual_env.agent_pos,manual_env.goal), _manual_score(), msg425 426 427# ═══════════════════════════════════════════════════════════════════════════════428# RL tab callbacks429# ═══════════════════════════════════════════════════════════════════════════════430 431_training_thread = None432 433def _speed_getter():434 with rl_lock: return rl_state['speed']435 436 437def rl_start(total_ep, speed_val, show_q):438 global _training_thread, rl_state439 _stop_event.clear(); _pause_event.clear()440 with rl_lock:441 rl_state['running'] = True442 rl_state['total_episodes'] = int(total_ep)443 rl_state['success_history'] = []444 rl_state['reward_history'] = []445 rl_state['episode'] = 0446 rl_state['speed'] = float(speed_val)447 rl_state['status'] = 'training'448 rl_state['msg'] = 'Starting…'449 _training_thread = threading.Thread(450 target=rl_train_thread, args=(int(total_ep), _speed_getter), daemon=True)451 _training_thread.start()452 return gr.update(interactive=False), gr.update(interactive=True), gr.update(interactive=True)453 454 455def rl_pause():456 if _pause_event.is_set():457 _pause_event.clear()458 with rl_lock: rl_state['status']='training'459 return gr.update(value="⏸ Pause")460 else:461 _pause_event.set()462 with rl_lock: rl_state['status']='paused'463 return gr.update(value="▶ Resume")464 465 466def rl_stop():467 _stop_event.set(); _pause_event.clear()468 with rl_lock:469 rl_state['running']=False; rl_state['status']='idle'470 rl_state['msg']='Stopped.'471 return gr.update(interactive=True), gr.update(interactive=False), gr.update(interactive=False, value="⏸ Pause")472 473 474def rl_update(show_q):475 with rl_lock:476 s = dict(rl_state)477 478 grid = s['grid']479 ap = s['agent_pos']480 goal = s['goal']481 trail= s['visited_trail']482 Q = s['Q_values']483 484 grid_h = render_grid_html(grid, ap, goal, trail, Q, show_q)485 score_h = render_scoreboard(486 s['episode'], s['total_episodes'], s['eps'], s['ep_reward'],487 s['steps'], ap, goal if goal else (0,0),488 s['success_history'], s['reward_history'], s['status']489 )490 return grid_h, score_h, s['msg']491 492 493def set_speed(v):494 with rl_lock: rl_state['speed'] = float(v)495 496 497# ═══════════════════════════════════════════════════════════════════════════════498# Gradio UI499# ═══════════════════════════════════════════════════════════════════════════════500 501CUSTOM_CSS = """502body,.gradio-container{background:#f4f7ff!important}503#title-md h1{font-family:'Poppins','Segoe UI',sans-serif;color:#4455cc;letter-spacing:3px;text-align:center;margin-bottom:2px}504#title-md p{color:#8899bb;text-align:center;font-family:sans-serif;font-size:12px}505.tab-nav button{font-family:sans-serif!important;letter-spacing:1px!important}506.ctrl-btn button{background:#ffffff!important;color:#4455cc!important;border:2px solid #c0cce8!important;507 border-radius:10px!important;font-size:18px!important;font-weight:700!important;508 min-width:50px!important;min-height:44px!important;box-shadow:0 2px 8px #a0b0d020!important}509.ctrl-btn button:hover{background:#eef2ff!important;border-color:#4455cc!important;box-shadow:0 4px 12px #4455cc22!important}510#start-btn button{background:linear-gradient(135deg,#4a90d9,#27ae60)!important;color:#fff!important;511 border:none!important;border-radius:10px!important;font-family:sans-serif!important;font-weight:700!important;512 box-shadow:0 4px 14px #27ae6044!important}513#stop-btn button{background:#fff0f0!important;color:#e74c3c!important;border:2px solid #f0c0c0!important;514 border-radius:10px!important;font-family:sans-serif!important;font-weight:700!important}515#pause-btn button{background:#fffbea!important;color:#d08000!important;border:2px solid #f0d880!important;516 border-radius:10px!important;font-family:sans-serif!important;font-weight:700!important}517#reset-btn-m button{background:linear-gradient(135deg,#4a90d9,#27ae60)!important;color:#fff!important;518 border:none!important;border-radius:10px!important;font-family:sans-serif!important;font-weight:700!important;519 width:100%!important;box-shadow:0 4px 14px #27ae6044!important}520.msg-box textarea{background:#ffffff!important;color:#334!important;font-family:monospace!important;521 border:1px solid #c8d4f0!important;border-radius:6px!important;font-size:12px!important}522"""523 524KEY_JS = """() => {525 if(window._gwKeys) return; window._gwKeys=true;526 document.addEventListener('keydown',(e)=>{527 const m={'ArrowUp':'btn-mu','ArrowRight':'btn-mr','ArrowDown':'btn-md','ArrowLeft':'btn-ml'};528 if(m[e.key]){e.preventDefault();document.getElementById(m[e.key])?.querySelector('button')?.click()}529 });530}"""531 532with gr.Blocks(title="GridWorld RL Navigator") as demo:533 534 gr.Markdown("# 🤖 GridWorld RL Navigator\n*Q-Learning · Gymnasium · Live Training Visualization*",535 elem_id="title-md")536 537 with gr.Tabs():538 539 # ── TAB 1 : RL Training ───────────────────────────────────────────────540 with gr.Tab("🧠 RL Training"):541 with gr.Row():542 with gr.Column(scale=3):543 rl_grid = gr.HTML(544 "<div style='color:#8899bb;font-family:sans-serif;padding:30px;text-align:center;background:#f8faff;border-radius:8px;border:2px dashed #c8d4f0'>🤖 Press ▶ Start Training to begin…</div>",545 label="")546 rl_msg = gr.Textbox(value="Configure settings and press Start Training.",547 label="", interactive=False, lines=1, elem_classes="msg-box")548 549 with gr.Column(scale=1):550 rl_score = gr.HTML("")551 552 with gr.Row():553 with gr.Column():554 total_ep_sl = gr.Slider(50, 1000, value=300, step=50,555 label="Total Episodes", interactive=True)556 speed_sl = gr.Slider(0.001, 0.3, value=0.06, step=0.005,557 label="Step delay (s) — lower = faster", interactive=True)558 show_q_cb = gr.Checkbox(label="Show Q-value arrows on grid", value=False)559 560 with gr.Column():561 start_btn = gr.Button("▶ Start Training", elem_id="start-btn", variant="primary")562 pause_btn = gr.Button("⏸ Pause", elem_id="pause-btn", interactive=False)563 stop_btn = gr.Button("⏹ Stop", elem_id="stop-btn", interactive=False)564 565 gr.HTML("""<div style="font-family:sans-serif;font-size:12px;color:#6677aa;text-align:center;margin-top:8px;padding:6px;background:#fff;border-radius:8px;border:1px solid #dde4f0">566 <span style="color:#4a90d9;font-weight:bold">■ S</span> Start 567 <span style="color:#27ae60;font-weight:bold">■ G</span> Goal 568 <span style="color:#a0c8a0">■</span> Trail 569 <span style="color:#8899aa;font-weight:bold">■ X</span> Obstacle 570 <span style="color:#e84393;font-weight:bold">● A</span> Agent571 </div>""")572 573 # Live polling timer574 rl_timer = gr.Timer(value=0.15)575 rl_timer.tick(fn=rl_update, inputs=[show_q_cb], outputs=[rl_grid, rl_score, rl_msg])576 577 speed_sl.change(fn=set_speed, inputs=[speed_sl])578 579 start_btn.click(fn=rl_start,580 inputs=[total_ep_sl, speed_sl, show_q_cb],581 outputs=[start_btn, pause_btn, stop_btn])582 pause_btn.click(fn=rl_pause, outputs=[pause_btn])583 stop_btn.click(fn=rl_stop, outputs=[start_btn, pause_btn, stop_btn])584 585 # ── TAB 2 : Manual Play ───────────────────────────────────────────────586 with gr.Tab("🕹️ Manual Play"):587 with gr.Row():588 with gr.Column(scale=3, elem_id="grid-col"):589 m_grid = gr.HTML(590 render_grid_html(manual_env.grid, manual_env.agent_pos, manual_env.goal))591 592 with gr.Row():593 gr.HTML("")594 with gr.Column(scale=1, min_width=54, elem_id="btn-mu", elem_classes="ctrl-btn"):595 mu = gr.Button("⬆")596 gr.HTML("")597 with gr.Row():598 with gr.Column(scale=1, min_width=54, elem_id="btn-ml", elem_classes="ctrl-btn"):599 ml = gr.Button("⬅")600 with gr.Column(scale=1, min_width=54, elem_id="btn-md", elem_classes="ctrl-btn"):601 md = gr.Button("⬇")602 with gr.Column(scale=1, min_width=54, elem_id="btn-mr", elem_classes="ctrl-btn"):603 mr = gr.Button("➡")604 605 with gr.Row():606 with gr.Column(elem_id="reset-btn-m"):607 mr2 = gr.Button("🔄 New Maze", variant="primary")608 609 m_msg = gr.Textbox(value="Navigate the red agent to the green goal!",610 label="", interactive=False, lines=1, elem_classes="msg-box")611 612 with gr.Column(scale=1):613 m_score = gr.HTML(_manual_score())614 615 m_outs = [m_grid, m_score, m_msg]616 mu.click(fn=lambda: manual_step(0), outputs=m_outs)617 mr.click(fn=lambda: manual_step(1), outputs=m_outs)618 md.click(fn=lambda: manual_step(2), outputs=m_outs)619 ml.click(fn=lambda: manual_step(3), outputs=m_outs)620 mr2.click(fn=manual_reset, outputs=m_outs)621 622 demo.load(fn=None, js=KEY_JS)623 624if __name__ == "__main__":625 demo.launch(css=CUSTOM_CSS)626 