Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import time | |
| import math | |
| from collections import deque, defaultdict | |
| # --- 設定參數 --- | |
| MAX_RECENT_EVENTS = 1000 # 暫存區大小 (達到此數量觸發壓縮) | |
| GRID_PRECISION = 4 # 網格精度 (4小數點約=11公尺) | |
| MAX_GRID_INTENSITY = 20.0 # 單一網格最大亮度 (避免單點過曝) | |
| # --- 資料結構 --- | |
| # 1. 近期事件 (Delta Queue): 用於快速同步 | |
| # 格式: {'t': time, 'u': user_id, 'l': [lat, lng], 's': speed} | |
| recent_events = deque() | |
| # 2. 歷史網格 (Snapshot Grid): 儲存壓縮後的長期記憶 | |
| # Key: (lat_round, lng_round), Value: intensity | |
| archived_grid = defaultdict(float) | |
| # 3. 狀態版本控制 | |
| snapshot_version = 0 | |
| last_snapshot_time = time.time() | |
| def get_intensity_by_speed(speed_kmh): | |
| """ | |
| 根據速度決定迷霧驅散強度 (亮度) | |
| < 5 km/h (步行/停留): 強度 1.0 | |
| < 20 km/h (跑步/腳踏車): 強度 0.5 | |
| > 20 km/h (車輛): 強度 0.1 | |
| """ | |
| if speed_kmh is None: return 0.5 | |
| if speed_kmh < 5: return 1.0 | |
| if speed_kmh < 20: return 0.5 | |
| return 0.1 | |
| def compress_events_to_grid(events_list): | |
| """將事件列表壓縮進網格""" | |
| global archived_grid | |
| for event in events_list: | |
| lat, lng = event['l'] | |
| speed = event['s'] | |
| # 網格化座標 | |
| grid_key = (round(lat, GRID_PRECISION), round(lng, GRID_PRECISION)) | |
| # 累加亮度 | |
| intensity = get_intensity_by_speed(speed) | |
| archived_grid[grid_key] += intensity | |
| # 限制最大亮度 (防止單點無限疊加) | |
| if archived_grid[grid_key] > MAX_GRID_INTENSITY: | |
| archived_grid[grid_key] = MAX_GRID_INTENSITY | |
| def trigger_snapshot(): | |
| """檢查並執行快照壓縮""" | |
| global recent_events, snapshot_version, last_snapshot_time | |
| # 如果暫存區超過閾值,將最舊的一半資料壓縮進歷史網格 | |
| if len(recent_events) > MAX_RECENT_EVENTS: | |
| events_to_archive = [] | |
| count_to_remove = int(MAX_RECENT_EVENTS / 2) | |
| for _ in range(count_to_remove): | |
| events_to_archive.append(recent_events.popleft()) | |
| compress_events_to_grid(events_to_archive) | |
| snapshot_version += 1 | |
| last_snapshot_time = time.time() | |
| print(f"[Server] Snapshot Updated. Ver: {snapshot_version}, Grid Points: {len(archived_grid)}") | |
| def sync_game_state(client_last_time, client_ver, user_id, lat, lng, speed): | |
| """ | |
| 主 API: 處理客戶端同步請求 | |
| """ | |
| current_time = time.time() | |
| # 1. 記錄用戶上傳的位置 (如果有) | |
| if lat is not None and lng is not None: | |
| # 過濾異常數據 (例如 GPS 飄移瞬間移動 > 150km/h) | |
| if speed is None or speed < 150: | |
| new_event = { | |
| "t": current_time, | |
| "u": user_id, | |
| "l": [lat, lng], | |
| "s": speed | |
| } | |
| recent_events.append(new_event) | |
| # 2. 檢查是否需要觸發壓縮 | |
| trigger_snapshot() | |
| # 3. 準備回傳資料 | |
| response = { | |
| "server_time": current_time, | |
| "snapshot_version": snapshot_version, | |
| "sync_type": "delta", | |
| "payload": [] | |
| } | |
| # 判斷同步策略 | |
| # 如果客戶端版本落後 (需要全量更新) 或 客戶端剛加入 (ver = -1) | |
| # if client_ver < snapshot_version: | |
| if client_ver < 0: | |
| response["sync_type"] = "snapshot" | |
| # 回傳壓縮後的網格數據: [[lat, lng, intensity], ...] | |
| response["payload"] = [[k[0], k[1], v] for k, v in archived_grid.items()] | |
| # 同時附上還沒被壓縮的近期事件,確保數據不出現斷層 | |
| response["recent_events"] = list(recent_events) | |
| else: | |
| # 增量更新: 只回傳 client_last_time 之後發生的事件 | |
| # 注意: 這裡回傳的是未壓縮的原始事件 | |
| delta = [e for e in recent_events if e['t'] > client_last_time] | |
| response["payload"] = delta | |
| return response | |
| # --- Gradio 介面設定 --- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("## City Scout Game API Server") | |
| gr.Markdown("此 Space 為遊戲後端,請使用前端 HTML 連接。") | |
| with gr.Row(visible=False): # 隱藏 UI,僅作為 API 使用 | |
| # Inputs | |
| in_time = gr.Number(label="Client Last Time") | |
| in_ver = gr.Number(label="Client Snapshot Version") | |
| in_uid = gr.Textbox(label="User ID") | |
| in_lat = gr.Number(label="Lat") | |
| in_lng = gr.Number(label="Lng") | |
| in_speed = gr.Number(label="Speed (km/h)") | |
| # Outputs | |
| out_json = gr.JSON(label="Sync Response") | |
| # API 按鈕 | |
| btn_sync = gr.Button("Sync") | |
| btn_sync.click( | |
| fn=sync_game_state, | |
| inputs=[in_time, in_ver, in_uid, in_lat, in_lng, in_speed], | |
| outputs=out_json, | |
| api_name="sync" # 重要: 前端 client.predict("/sync") 用這個名字 | |
| ) | |
| # 啟動並允許跨域 (CORS) | |
| demo.queue(max_size=50).launch(cors_allowed_origins=["*"]) |