# VARK Simple Parking Grid Demo # Paste this file as app.py in a Hugging Face Space (Gradio SDK) import time from dataclasses import dataclass, field from typing import Dict, List, Optional, Tuple import gradio as gr import matplotlib.pyplot as plt import matplotlib.patches as patches # ----------------------------- # Config # ----------------------------- ROWS = 4 COLS = 5 PLATFORM_COUNT = 16 # Two external cells (22 total cells shown) DROP_POINT = (-1, 0) # outside the 20-cell grid, top-left PICK_POINT = (ROWS, COLS - 1) # outside the 20-cell grid, bottom-right STEP_DELAY = 0.08 # ----------------------------- # Data models # ----------------------------- @dataclass class Platform: pid: int r: int c: int car_id: Optional[int] = None home: Tuple[int, int] = (0, 0) @dataclass class State: platforms: Dict[int, Platform] = field(default_factory=dict) occupancy: Dict[Tuple[int, int], Optional[int]] = field(default_factory=dict) cars: Dict[int, int] = field(default_factory=dict) # car_id -> platform_id log: List[str] = field(default_factory=list) # animation overlays moving_pid: Optional[int] = None moving_dir: Optional[str] = None # 'U'|'D'|'L'|'R' # ----------------------------- # Helpers # ----------------------------- def in_main_grid(r: int, c: int) -> bool: return 0 <= r < ROWS and 0 <= c < COLS def in_extended(r: int, c: int) -> bool: return in_main_grid(r, c) or (r, c) == DROP_POINT or (r, c) == PICK_POINT def manhattan(a: Tuple[int, int], b: Tuple[int, int]) -> int: return abs(a[0] - b[0]) + abs(a[1] - b[1]) def arrow_for_dir(d: Optional[str]) -> str: return { 'U': '↑', 'D': '↓', 'L': '←', 'R': '→', None: '' }.get(d, '') # ----------------------------- # Init # ----------------------------- def init_state() -> State: st = State() # four empty cells inside the 20-cell grid empty = {(0, 4), (1, 2), (3, 0), (3, 4)} # occupancy for all displayed cells (20 + 2) for r in range(-1, ROWS + 1): for c in range(-1, COLS + 1): if in_extended(r, c): st.occupancy[(r, c)] = None pid = 1 for r in range(ROWS): for c in range(COLS): if (r, c) in empty: continue if pid > PLATFORM_COUNT: continue p = Platform(pid=pid, r=r, c=c, home=(r, c)) st.platforms[pid] = p st.occupancy[(r, c)] = pid pid += 1 st.log.append("System initialised: 4x5 grid (20) + 2 external cells = 22 total") st.log.append(f"DROP point at {DROP_POINT} (outside top-left)") st.log.append(f"PICK point at {PICK_POINT} (outside bottom-right)") return st # ----------------------------- # Rendering (frame-by-frame) # ----------------------------- def render(st: State): fig, ax = plt.subplots(figsize=(8, 6)) def xy_for_cell(cell: Tuple[int, int]) -> Tuple[float, float]: if cell == DROP_POINT: # place above (0,0) return (0, ROWS) if cell == PICK_POINT: # place below bottom-right main cell return (COLS - 1, -1) r, c = cell return (c, ROWS - 1 - r) # Draw main grid for r in range(ROWS): for c in range(COLS): x, y = xy_for_cell((r, c)) ax.add_patch(patches.Rectangle((x, y), 1, 1, fill=False, linewidth=1.2)) # Draw external cells for cell, label in [(DROP_POINT, "DROP"), (PICK_POINT, "PICK")]: x, y = xy_for_cell(cell) ax.add_patch(patches.Rectangle((x, y), 1, 1, fill=False, linewidth=2.6)) ax.text(x + 0.5, y + 0.86, label, ha="center", va="center", fontsize=10) # Blue fill when carrying a car for p in st.platforms.values(): if p.car_id is None: continue x, y = xy_for_cell((p.r, p.c)) ax.add_patch(patches.Rectangle((x, y), 1, 1, fill=True, alpha=0.25)) # Platform labels + motion arrow (direction of next step) for p in st.platforms.values(): x, y = xy_for_cell((p.r, p.c)) moving_arrow = arrow_for_dir(st.moving_dir) if st.moving_pid == p.pid else '' label = f"P{p.pid}" if p.car_id is not None: label += f"\nCar {p.car_id}" if moving_arrow: label = f"{moving_arrow}\n{label}" ax.text(x + 0.5, y + 0.5, label, ha="center", va="center", fontsize=9) # bounds include external cells ax.set_xlim(-0.2, COLS + 0.2) ax.set_ylim(-1.2, ROWS + 1.2) ax.set_aspect("equal") ax.axis("off") return fig # ----------------------------- # Movement logic (4-direction only) # ----------------------------- def step_towards(src: Tuple[int, int], dst: Tuple[int, int]) -> Tuple[int, int]: r, c = src tr, tc = dst # deterministic: move row first then col if r < tr: return (r + 1, c) if r > tr: return (r - 1, c) if c < tc: return (r, c + 1) if c > tc: return (r, c - 1) return (r, c) def direction_from_to(a: Tuple[int, int], b: Tuple[int, int]) -> Optional[str]: ar, ac = a br, bc = b if br == ar - 1 and bc == ac: return 'U' if br == ar + 1 and bc == ac: return 'D' if br == ar and bc == ac - 1: return 'L' if br == ar and bc == ac + 1: return 'R' return None def move_one(st: State, pid: int, target: Tuple[int, int]): p = st.platforms[pid] cur = (p.r, p.c) if not in_extended(*target): return if target not in st.occupancy: st.occupancy[target] = None occ = st.occupancy[target] if occ is None: st.occupancy[cur] = None p.r, p.c = target st.occupancy[target] = pid else: # swap to avoid overlap (simple demo rule) other = st.platforms[occ] p.r, other.r = other.r, p.r p.c, other.c = other.c, p.c st.occupancy[(p.r, p.c)] = pid st.occupancy[(other.r, other.c)] = occ def animate_move(st: State, pid: int, dst: Tuple[int, int]): p = st.platforms[pid] st.moving_pid = pid while (p.r, p.c) != dst: cur = (p.r, p.c) nxt = step_towards(cur, dst) st.moving_dir = direction_from_to(cur, nxt) move_one(st, pid, nxt) yield st time.sleep(STEP_DELAY) st.moving_dir = None st.moving_pid = None # ----------------------------- # Business logic # ----------------------------- def free_platform(st: State) -> Optional[int]: free = [p for p in st.platforms.values() if p.car_id is None] if not free: return None free.sort(key=lambda p: (manhattan((p.r, p.c), DROP_POINT), p.pid)) return free[0].pid def drop_off(st: State, car_id: int): if car_id in st.cars: st.log.append(f"Drop rejected: Car {car_id} already parked") yield st return if len(st.cars) >= PLATFORM_COUNT: st.log.append("Drop rejected: capacity full (16 cars)") yield st return pid = free_platform(st) if pid is None: st.log.append("Drop rejected: no empty platform available") yield st return st.log.append(f"Dispatch P{pid} to DROP for Car {car_id}") for _ in animate_move(st, pid, DROP_POINT): yield st st.platforms[pid].car_id = car_id st.cars[car_id] = pid st.log.append(f"Car {car_id} loaded onto P{pid}") home = st.platforms[pid].home st.log.append(f"Store Car {car_id}: return P{pid} to {home}") for _ in animate_move(st, pid, home): yield st st.log.append(f"Drop complete: Car {car_id} stored") yield st def pick_up(st: State, car_id: int): if car_id not in st.cars: st.log.append(f"Pick rejected: Car {car_id} not found") yield st return pid = st.cars[car_id] st.log.append(f"Dispatch P{pid} to PICK for Car {car_id}") for _ in animate_move(st, pid, PICK_POINT): yield st st.platforms[pid].car_id = None del st.cars[car_id] st.log.append(f"Car {car_id} unloaded at PICK") home = st.platforms[pid].home st.log.append(f"Return P{pid} to {home}") for _ in animate_move(st, pid, home): yield st st.log.append("Pick complete") yield st # ----------------------------- # UI handlers # ----------------------------- def reset_ui(): st = init_state() return st, render(st), "\n".join(st.log[-16:]), f"{len(st.cars)} / 16" def ui_drop(st: State, car: str): try: cid = int(car) if cid <= 0: raise ValueError except Exception: st.log.append("Drop rejected: Car ID must be a positive integer") yield st, render(st), "\n".join(st.log[-16:]), f"{len(st.cars)} / 16" return for s in drop_off(st, cid): yield s, render(s), "\n".join(s.log[-16:]), f"{len(s.cars)} / 16" def ui_pick(st: State, car: str): try: cid = int(car) if cid <= 0: raise ValueError except Exception: st.log.append("Pick rejected: Car ID must be a positive integer") yield st, render(st), "\n".join(st.log[-16:]), f"{len(st.cars)} / 16" return for s in pick_up(st, cid): yield s, render(s), "\n".join(s.log[-16:]), f"{len(s.cars)} / 16" # ----------------------------- # Gradio App # ----------------------------- with gr.Blocks(title="VARK Parking Grid Demo (Animated)") as demo: state = gr.State(init_state()) gr.Markdown( """ VARK grid demo (animated, frame-by-frame) - Main grid: 4 x 5 = 20 cells - External cells: DROP (outside top-left) and PICK (outside bottom-right) - Total shown: 22 cells - Platforms move only up / down / left / right - Moving platform shows an arrow for direction each frame - Platform cell turns blue when carrying a car """.strip() ) with gr.Row(): car_id = gr.Textbox(label="Car ID", value="101") capacity = gr.Textbox(label="Capacity", value="0 / 16", interactive=False) with gr.Row(): drop_btn = gr.Button("DROP OFF") pick_btn = gr.Button("PICK UP") reset_btn = gr.Button("RESET") plot = gr.Plot(label="Grid") log = gr.Textbox(label="Log", lines=16, interactive=False) plot.value = render(state.value) log.value = "\n".join(state.value.log[-16:]) reset_btn.click(reset_ui, outputs=[state, plot, log, capacity]) drop_btn.click(ui_drop, inputs=[state, car_id], outputs=[state, plot, log, capacity]) pick_btn.click(ui_pick, inputs=[state, car_id], outputs=[state, plot, log, capacity]) demo.queue().launch()