| import gradio as gr | |
| import math | |
| treasures = [ | |
| {"name": "寶藏第1個", "lat": 24.985703, "lon": 121.288680, "question": "請問喬一老師叫什麼課?", "answer": "STEAM"}, | |
| {"name": "寶藏第2個", "lat": 24.985900, "lon": 121.289000, "question": "請問今年是何年?", "answer": "2025"}, | |
| {"name": "寶藏第3個", "lat": 24.956398, "lon": 121.297729, "question": "請問熊熊老師叫什麼課?", "answer": "balloon"}, | |
| ] | |
| def haversine(lat1, lon1, lat2, lon2): | |
| R = 6371000 | |
| phi1, phi2 = math.radians(lat1), math.radians(lat2) | |
| d_phi = math.radians(lat2 - lat1) | |
| d_lambda = math.radians(lon2 - lon1) | |
| a = math.sin(d_phi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(d_lambda/2)**2 | |
| c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) | |
| return R * c | |
| def find_nearest(lat, lon, found): | |
| nearest = None | |
| nearest_idx = None | |
| min_distance = float("inf") | |
| for idx, treasure in enumerate(treasures): | |
| if idx in found: | |
| continue | |
| dist = haversine(lat, lon, treasure["lat"], treasure["lon"]) | |
| if dist < min_distance: | |
| min_distance = dist | |
| nearest = treasure | |
| nearest_idx = idx | |
| return nearest, nearest_idx, min_distance | |
| def update_position(lat, lon, found): | |
| if not lat or not lon: | |
| return "❌ 請先取得 GPS", gr.update(visible=False), "", "", found | |
| try: | |
| lat = float(lat) | |
| lon = float(lon) | |
| except: | |
| return "❌ 座標格式錯誤", gr.update(visible=False), "", "", found | |
| nearest, nearest_idx, dist = find_nearest(lat, lon, found) | |
| if nearest is None: | |
| return "🎉 所有寶藏已完成!", gr.update(visible=False), "", "", found | |
| if dist <= 200: | |
| return f"📍 距離 {nearest['name']}:{dist:.2f} 公尺 ✅ 請回答問題:", gr.update(visible=True), nearest["question"], nearest_idx, found | |
| else: | |
| return f"📍 距離 {nearest['name']}:{dist:.2f} 公尺,請再靠近一些", gr.update(visible=False), "", "", found | |
| def check_answer(user_answer, nearest_idx, found): | |
| correct = treasures[nearest_idx]["answer"] | |
| if user_answer.strip().lower() == correct.strip().lower(): | |
| return "✅ 答對了!請上傳一張照片作為紀念!", gr.update(visible=True), nearest_idx, found | |
| else: | |
| return "❌ 答錯了,請再試一次。", gr.update(visible=False), None, found | |
| def complete_mission(photo, nearest_idx, found): | |
| if photo is not None: | |
| found.append(nearest_idx) | |
| if len(found) == len(treasures): | |
| return "🎉 所有寶藏完成!", """ | |
| <h2 style='text-align:center;'>🎉 恭喜完成全部探索任務!</h2> | |
| <img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGU4YzZhOTdjYjJmODlmZDEwZmExZDcyOTU4NGY5NGY4MzU3ZDM2YSZjdD1n/26n6WywJyh39n1pBu/giphy.gif" style="width:100%; max-height:300px;"> | |
| """, found | |
| else: | |
| return "✅ 照片上傳成功!繼續尋找下一個寶藏~", "", found | |
| else: | |
| return "❌ 請上傳照片!", "", found | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# 🗺️ GPS 探索互動尋寶遊戲") | |
| gr.Markdown("📍 自動定位,靠近寶藏觸發題目任務") | |
| with gr.Row(): | |
| lat = gr.Textbox(label="Latitude") | |
| lon = gr.Textbox(label="Longitude") | |
| found_state = gr.State([]) | |
| status = gr.Markdown() | |
| question_box = gr.Textbox(label="回答問題", visible=False) | |
| question_text = gr.Textbox(visible=False) | |
| nearest_idx_state = gr.State() | |
| photo_upload = gr.Image(type="filepath", label="上傳紀念照片", visible=False) | |
| final_message = gr.Markdown() | |
| final_celebration = gr.HTML() | |
| btn_check = gr.Button("🔍 刷新位置") | |
| btn_check.click(update_position, inputs=[lat, lon, found_state], | |
| outputs=[status, question_box, question_text, nearest_idx_state, found_state]) | |
| btn_submit_answer = gr.Button("✅ 提交回答") | |
| btn_submit_answer.click(check_answer, inputs=[question_box, nearest_idx_state, found_state], | |
| outputs=[status, photo_upload, nearest_idx_state, found_state]) | |
| btn_upload_photo = gr.Button("📸 上傳完成") | |
| btn_upload_photo.click(complete_mission, inputs=[photo_upload, nearest_idx_state, found_state], | |
| outputs=[status, final_celebration, found_state]) | |
| gr.HTML(""" | |
| <script> | |
| setInterval(() => { | |
| navigator.geolocation.getCurrentPosition( | |
| function(pos) { | |
| document.querySelectorAll('textarea')[0].value = pos.coords.latitude; | |
| document.querySelectorAll('textarea')[1].value = pos.coords.longitude; | |
| document.querySelectorAll('textarea')[0].dispatchEvent(new Event('input')); | |
| document.querySelectorAll('textarea')[1].dispatchEvent(new Event('input')); | |
| document.querySelector('button[id$="btn_check"]').click(); | |
| } | |
| ); | |
| }, 5000); | |
| </script> | |
| """) | |
| demo.launch() | |