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"""
"""
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()