20 層 × 5 部互聯電梯|門口呼叫互聯點亮|無請求則原地不動

Tick(ms): Auto Traffic stopped
from pyscript import document import js from pyodide.ffi import create_proxy import random FLOORS = 20 ELEVATORS = 5 # ----------------------------- # Model # ----------------------------- class Elevator: def __init__(self, eid:int): self.eid = eid self.floor = 1 self.dir = 0 # -1 down, 0 idle, +1 up self.door_open = False self.fault = False self.door_timer = 0 # ticks remaining while door open self.car_calls = set() # inside car selected floors self.pickups = [] # list of (floor, dir) assigned hall calls def has_work(self): return (len(self.car_calls) > 0) or (len(self.pickups) > 0) def all_targets(self): t = set(self.car_calls) for (f, d) in self.pickups: t.add(f) return t def next_target(self): targets = sorted(self.all_targets()) if not targets: return None if self.dir == 1: ahead = [f for f in targets if f >= self.floor] return min(ahead) if ahead else max(targets) if self.dir == -1: ahead = [f for f in targets if f <= self.floor] return max(ahead) if ahead else min(targets) return min(targets, key=lambda f: abs(f - self.floor)) def step(self, hall_calls): if self.fault: return # Door handling if self.door_open: self.door_timer -= 1 if self.door_timer <= 0: self.door_open = False return target = self.next_target() # No requests above/below => stay if target is None: self.dir = 0 return # If at target => open door, clear served calls if target == self.floor: # clear car call at this floor self.car_calls.discard(self.floor) # clear assigned pickups served at this floor self.pickups = [(f,d) for (f,d) in self.pickups if f != self.floor] # Clear global hall call lights if any call at this floor is served by stopping here (simplified) hall_calls[self.floor]["up"] = False hall_calls[self.floor]["down"] = False self.door_open = True self.door_timer = 2 # Update direction after stop target2 = self.next_target() if target2 is None: self.dir = 0 else: self.dir = 1 if target2 > self.floor else (-1 if target2 < self.floor else 0) return # Door must be closed to move if self.door_open: return # Move one floor towards target self.dir = 1 if target > self.floor else -1 self.floor += self.dir # ----------------------------- # Dispatcher # ----------------------------- def dispatch_hall_calls(hall_calls, elevators): pending = [] for f in range(1, FLOORS+1): if hall_calls[f]["up"]: pending.append((f, 1)) if hall_calls[f]["down"]: pending.append((f, -1)) assigned = set() for e in elevators: for (pf, pd) in e.pickups: assigned.add((pf, pd)) for (f, d) in pending: if (f, d) in assigned: continue best = None best_score = 10**9 for e in elevators: if e.fault: continue dist = abs(e.floor - f) stops = len(e.car_calls) + len(e.pickups) if e.dir == 0: score = dist + 0.6*stops else: on_the_way = (e.dir == d and ((d==1 and f >= e.floor) or (d==-1 and f <= e.floor))) if on_the_way: score = dist + 0.8*stops else: score = dist*2.5 + 1.2*stops if score < best_score: best_score = score best = e if best is not None: best.pickups.append((f, d)) # ----------------------------- # UI build # ----------------------------- def dir_text(d): return "▲" if d==1 else ("▼" if d==-1 else "■") def build_ui(): grid = document.getElementById("grid") grid.innerHTML = "" for eid in range(ELEVATORS): col = js.document.createElement("div") col.className = "col" col.id = f"col_{eid}" head = js.document.createElement("div") head.className = "colhead" head.innerHTML = f"""
Elevator {eid+1}
F1 DOOR OK
""" col.appendChild(head) wrap = js.document.createElement("div") wrap.className = "wrap" # Shaft shaft = js.document.createElement("div") shaft.className = "shaft" shaft.id = f"shaft_{eid}" car = js.document.createElement("div") car.className = "car" car.id = f"car_{eid}" car.innerText = "E" shaft.appendChild(car) wrap.appendChild(shaft) # Panels panels = js.document.createElement("div") panels.className = "panels" # Inside panel inside = js.document.createElement("div") inside.className = "panel" inside.innerHTML = f"""

轎厢內 (Car Panel)

""" panels.appendChild(inside) # Hall panel (per elevator entrance, but interconnected) hall = js.document.createElement("div") hall.className = "panel" hall.innerHTML = f"""

門口呼叫 (此欄入口,按下後 5 欄同步點亮)

註:燈亮代表該樓層呼叫尚未被服務(任一電梯停靠開門後清除;目前清除規則為同層上下皆清)。
""" panels.appendChild(hall) wrap.appendChild(panels) col.appendChild(wrap) grid.appendChild(col) # Build inside buttons 1..20 ig = document.getElementById(f"inside_grid_{eid}") for f in range(1, FLOORS+1): b = js.document.createElement("button") b.className = "kbtn" b.id = f"carbtn_{eid}_{f}" b.innerText = str(f) b.addEventListener("click", create_proxy(lambda evt, ee=eid, ff=f: on_car_call(ee, ff))) ig.appendChild(b) # Inside row buttons: open/close/alarm row = document.getElementById(f"inside_row_{eid}") bopen = js.document.createElement("button") bopen.className = "kbtn blue" bopen.innerText = "開門" bopen.addEventListener("click", create_proxy(lambda evt, ee=eid: on_open_door(ee))) row.appendChild(bopen) bclose = js.document.createElement("button") bclose.className = "kbtn blue" bclose.innerText = "關門" bclose.addEventListener("click", create_proxy(lambda evt, ee=eid: on_close_door(ee))) row.appendChild(bclose) balarm = js.document.createElement("button") balarm.className = "kbtn" balarm.innerText = "警報/故障" balarm.addEventListener("click", create_proxy(lambda evt, ee=eid: on_toggle_fault(ee))) row.appendChild(balarm) # Hall list (20 floors, up/down buttons) hl = document.getElementById(f"hall_list_{eid}") for f in range(FLOORS, 0, -1): tag = js.document.createElement("div") tag.className = "floor_tag" tag.innerText = f"F{f}" hl.appendChild(tag) bup = js.document.createElement("button") bup.className = "hbtn up" bup.id = f"hallup_{eid}_{f}" bup.innerText = "▲" bup.disabled = (f == FLOORS) bup.addEventListener("click", create_proxy(lambda evt, ff=f: on_hall_call(ff, "up"))) hl.appendChild(bup) bdn = js.document.createElement("button") bdn.className = "hbtn" bdn.id = f"halldn_{eid}_{f}" bdn.innerText = "▼" bdn.disabled = (f == 1) bdn.addEventListener("click", create_proxy(lambda evt, ff=f: on_hall_call(ff, "down"))) hl.appendChild(bdn) # ----------------------------- # Global state # ----------------------------- elevators = [Elevator(i) for i in range(ELEVATORS)] hall_calls = {f: {"up": False, "down": False} for f in range(1, FLOORS+1)} running = False timer_id = None tick_ms = 260 auto_on = False auto_timer_id = None # ----------------------------- # Handlers # ----------------------------- def on_hall_call(floor:int, which:str): hall_calls[floor][which] = True render() def on_car_call(eid:int, floor:int): e = elevators[eid] if e.fault: return e.car_calls.add(floor) render() def on_open_door(eid:int): e = elevators[eid] if e.fault: return e.door_open = True e.door_timer = 2 render() def on_close_door(eid:int): e = elevators[eid] if e.fault: return e.door_open = False e.door_timer = 0 render() def on_toggle_fault(eid:int): e = elevators[eid] e.fault = not e.fault if e.fault: e.dir = 0 e.door_open = False e.door_timer = 0 render() # ----------------------------- # Simulation tick # ----------------------------- def tick(): dispatch_hall_calls(hall_calls, elevators) for e in elevators: e.step(hall_calls) render() # ----------------------------- # Auto traffic generator # ----------------------------- def auto_step(): if random.random() < 0.55: f = random.randint(1, FLOORS) if f == 1: hall_calls[f]["up"] = True elif f == FLOORS: hall_calls[f]["down"] = True else: if random.random() < 0.5: hall_calls[f]["up"] = True else: hall_calls[f]["down"] = True if random.random() < 0.35: eid = random.randint(0, ELEVATORS-1) if not elevators[eid].fault: elevators[eid].car_calls.add(random.randint(1, FLOORS)) render() # ----------------------------- # Render # ----------------------------- def render(): for e in elevators: eid = e.eid bd = document.getElementById(f"badge_dir_{eid}") bf = document.getElementById(f"badge_floor_{eid}") bdoor = document.getElementById(f"badge_door_{eid}") bfault = document.getElementById(f"badge_fault_{eid}") bd.innerText = dir_text(e.dir) bd.className = "badge" + (" on" if e.dir != 0 else "") bf.innerText = f"F{e.floor}" if e.door_open: bdoor.innerText = "OPEN" bdoor.className = "badge ok" else: bdoor.innerText = "CLOSED" bdoor.className = "badge" if e.fault: bfault.innerText = "FAULT" bfault.className = "badge danger" else: bfault.innerText = "OK" bfault.className = "badge" car = document.getElementById(f"car_{eid}") shaft_h = 560 car_h = 22 step_h = (shaft_h - car_h - 12) / (FLOORS - 1) y = (FLOORS - e.floor) * step_h + 6 car.style.top = f"{y}px" car.innerText = f"F{e.floor}{dir_text(e.dir)}" car.className = "car" + (" open" if e.door_open else "") + (" fault" if e.fault else "") # inside button highlight for f in range(1, FLOORS+1): btn = document.getElementById(f"carbtn_{eid}_{f}") if f in e.car_calls: btn.classList.add("active") else: btn.classList.remove("active") btn.disabled = bool(e.fault) # hall button lights (interconnected) for f in range(1, FLOORS+1): up = document.getElementById(f"hallup_{eid}_{f}") dn = document.getElementById(f"halldn_{eid}_{f}") if hall_calls[f]["up"]: up.classList.add("on") else: up.classList.remove("on") if hall_calls[f]["down"]: dn.classList.add("on") else: dn.classList.remove("on") up.disabled = (f == FLOORS) dn.disabled = (f == 1) info = document.getElementById("info") pending = sum((1 if hall_calls[f]["up"] else 0) + (1 if hall_calls[f]["down"] else 0) for f in range(1, FLOORS+1)) info.innerText = ("running" if running else "stopped") + f" | pending hall calls={pending} | auto={'on' if auto_on else 'off'}" # ----------------------------- # Controls # ----------------------------- def start(): global running, timer_id if running: return running = True timer_id = js.setInterval(create_proxy(lambda: tick()), int(tick_ms)) render() def stop(): global running, timer_id if not running: return running = False if timer_id is not None: js.clearInterval(timer_id) timer_id = None render() def reset(): global elevators, hall_calls stop() elevators = [Elevator(i) for i in range(ELEVATORS)] hall_calls = {f: {"up": False, "down": False} for f in range(1, FLOORS+1)} render() def apply_tick(): global tick_ms v = document.getElementById("tickMs").value try: ms = int(v) except: ms = 260 ms = max(60, ms) tick_ms = ms if running: stop() start() render() def auto_on_fn(): global auto_on, auto_timer_id if auto_on: return auto_on = True auto_timer_id = js.setInterval(create_proxy(lambda: auto_step()), 650) render() def auto_off_fn(): global auto_on, auto_timer_id auto_on = False if auto_timer_id is not None: js.clearInterval(auto_timer_id) auto_timer_id = None render() # Bind toolbar document.getElementById("btnStart").addEventListener("click", create_proxy(lambda evt: start())) document.getElementById("btnStop").addEventListener("click", create_proxy(lambda evt: stop())) document.getElementById("btnReset").addEventListener("click", create_proxy(lambda evt: reset())) document.getElementById("btnApplyTick").addEventListener("click", create_proxy(lambda evt: apply_tick())) document.getElementById("btnAutoOn").addEventListener("click", create_proxy(lambda evt: auto_on_fn())) document.getElementById("btnAutoOff").addEventListener("click", create_proxy(lambda evt: auto_off_fn())) # init UI build_ui() render()