Spaces:
Running
Running
Commit ·
9d97138
1
Parent(s): 9b337e8
Deploy Telearn Project Demo Showcase with premium brand style and interactive terminal input
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +34 -0
- projects/.DS_Store +0 -0
- projects/2026project_class/.DS_Store +0 -0
- projects/2026project_class/doc/16週課程計畫.md +113 -0
- projects/2026project_class/doc/w1.md +79 -0
- projects/2026project_class/doc/w10.md +83 -0
- projects/2026project_class/doc/w11.md +86 -0
- projects/2026project_class/doc/w12.md +68 -0
- projects/2026project_class/doc/w13.md +70 -0
- projects/2026project_class/doc/w14.md +58 -0
- projects/2026project_class/doc/w15.md +61 -0
- projects/2026project_class/doc/w16.md +65 -0
- projects/2026project_class/doc/w2.md +95 -0
- projects/2026project_class/doc/w3.md +87 -0
- projects/2026project_class/doc/w4.md +75 -0
- projects/2026project_class/doc/w5.md +94 -0
- projects/2026project_class/doc/w6.md +84 -0
- projects/2026project_class/doc/w7.md +75 -0
- projects/2026project_class/doc/w8.md +68 -0
- projects/2026project_class/doc/w9.md +93 -0
- projects/2026project_class/examples/.DS_Store +0 -0
- projects/2026project_class/examples/background.jpg +3 -0
- projects/2026project_class/examples/extra_apis/advice_slip_api.py +31 -0
- projects/2026project_class/examples/extra_apis/agify_api.py +34 -0
- projects/2026project_class/examples/extra_apis/dog_api.py +29 -0
- projects/2026project_class/examples/extra_apis/numbers_api.py +34 -0
- projects/2026project_class/examples/extra_apis/poke_api.py +42 -0
- projects/2026project_class/examples/extra_apis/random_user_api.py +41 -0
- projects/2026project_class/examples/face_landmarker.task +3 -0
- projects/2026project_class/examples/hand_landmarker.task +3 -0
- projects/2026project_class/examples/hit.wav +3 -0
- projects/2026project_class/examples/shoot.wav +3 -0
- projects/2026project_class/examples/test_mp.py +14 -0
- projects/2026project_class/examples/w10_pinch_complete.py +179 -0
- projects/2026project_class/examples/w10_pinch_template.py +146 -0
- projects/2026project_class/examples/w10_step1_distance.py +53 -0
- projects/2026project_class/examples/w10_step2_threshold.py +59 -0
- projects/2026project_class/examples/w11_classifier_complete.py +212 -0
- projects/2026project_class/examples/w11_classifier_template.py +139 -0
- projects/2026project_class/examples/w12_eyeguard_complete.py +260 -0
- projects/2026project_class/examples/w12_eyeguard_template.py +153 -0
- projects/2026project_class/examples/w1_1_basic_window.py +35 -0
- projects/2026project_class/examples/w1_2_challenge_follower.py +49 -0
- projects/2026project_class/examples/w2_1_basic_api.py +26 -0
- projects/2026project_class/examples/w2_2_challenge_joke_api.py +42 -0
- projects/2026project_class/examples/w4_dashboard_template.py +238 -0
- projects/2026project_class/examples/w4_dashboard_weather.py +233 -0
- projects/2026project_class/examples/w5_udp_chat.py +48 -0
- projects/2026project_class/examples/w5_udp_receiver.py +16 -0
- projects/2026project_class/examples/w5_udp_sender.py +20 -0
Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Install system dependencies required by OpenCV headless and Pygame
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
+
libglib2.0-0 \
|
| 6 |
+
libgl1-mesa-glx \
|
| 7 |
+
libgomp1 \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Set up environment variables
|
| 11 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 12 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 13 |
+
SDL_VIDEODRIVER=dummy \
|
| 14 |
+
SDL_AUDIODRIVER=dummy \
|
| 15 |
+
PORT=8000
|
| 16 |
+
|
| 17 |
+
# Create working directory and copy dependencies
|
| 18 |
+
WORKDIR /app
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
|
| 21 |
+
# Install dependencies
|
| 22 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# Copy application files
|
| 25 |
+
COPY . .
|
| 26 |
+
|
| 27 |
+
# Create directory for projects if it doesn't exist
|
| 28 |
+
RUN mkdir -p /app/projects
|
| 29 |
+
|
| 30 |
+
# Set permissions for Hugging Face Spaces compatibility (runs as UID 1000)
|
| 31 |
+
RUN chmod -R 777 /app
|
| 32 |
+
|
| 33 |
+
# Run server.py on port specified by environment
|
| 34 |
+
CMD uvicorn server:app --host 0.0.0.0 --port $PORT
|
projects/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
projects/2026project_class/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
projects/2026project_class/doc/16週課程計畫.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **Python AI 智慧工具與網路互動專案:16週完整教學計畫**
|
| 2 |
+
|
| 3 |
+
**適用對象**:國小六年級以上至國中(具備 Python 基礎與 Pygame 基本經驗)
|
| 4 |
+
|
| 5 |
+
**教學時長**:每週 1 堂,每堂 120 分鐘
|
| 6 |
+
|
| 7 |
+
**核心理念**:讓程式具備「連結世界」與「視覺感知」的能力,從單機遊戲轉向解決真實生活問題的智慧工具。
|
| 8 |
+
|
| 9 |
+
## **第一階段:數據與介面 —— 實體數據看板 (第 1 \- 4 週)**
|
| 10 |
+
|
| 11 |
+
**核心目標**:學習如何抓取網路上的真實數據,並將其視覺化呈現。
|
| 12 |
+
|
| 13 |
+
### **第 1 週:引擎重啟 —— 從線性邏輯到即時循環系統**
|
| 14 |
+
|
| 15 |
+
* **教學重點 (45 min)**:
|
| 16 |
+
* **線性 vs. 循環**:複習一般 Python 腳本(執行完畢即結束)與遊戲引擎(每秒 60 次不斷循環)的差異。
|
| 17 |
+
* **Pygame 四大架構**:深入解析「初始化、事件處理、邏輯運算、畫面重繪」的執行順序。
|
| 18 |
+
* **實作任務 (60 min)**:製作一個「靈魂追隨者」,圓形會延遲追蹤滑鼠位置,並在點擊時產生顏色漸變。
|
| 19 |
+
* **討論與總結 (15 min)**:為什麼遊戲如果不清空畫面 (screen.fill),物體移動會留下尾巴?
|
| 20 |
+
|
| 21 |
+
### **第 2 週:數位服務生 —— requests 庫與網路 API**
|
| 22 |
+
|
| 23 |
+
* **教學重點 (40 min)**:
|
| 24 |
+
* **什麼是 API?**:用餐廳點餐比喻 API (服務生)、伺服器 (廚房) 與客戶端 (你)。
|
| 25 |
+
* **JSON 格式解析**:學習如何從像字典一樣的 JSON 數據中提取我們需要的內容。
|
| 26 |
+
* **實作任務 (65 min)**:使用 requests 抓取真實的 API (如天氣、冷笑話、寶可夢圖鑑、姓名猜測年齡),並在終端機輸出。
|
| 27 |
+
* **進階任務 (15 min)**:學習處理錯誤代碼(例如 404 代表找不到,500 代表伺服器罷工)。
|
| 28 |
+
|
| 29 |
+
### **第 3 週:數據視覺化 —— 打造個人智慧看板**
|
| 30 |
+
|
| 31 |
+
* **教學重點 (40 min)**:
|
| 32 |
+
* **文字渲染技術**:如何在 Pygame 中加載中文字型並顯示動態變數。
|
| 33 |
+
* **數據與圖像聯動**:例如當天氣是「雨天」時,程式自動加載下雨的圖片。
|
| 34 |
+
* **實作任務 (80 min)**:將上週抓取的數據顯示在畫面上,製作一個能顯示「今日氣溫、濕度、建議穿著」的視覺化視窗。
|
| 35 |
+
|
| 36 |
+
### **第 4 週:專案實作 (一) —— 生活數據助理發表**
|
| 37 |
+
|
| 38 |
+
* **專案開發 (100 min)**:學生自主選題(股市走勢、空氣品質、星座運勢),獨立開發一個具備實用功能的看板。
|
| 39 |
+
* **成果分享 (20 min)**:演示自己的看板,並說明數據來源。
|
| 40 |
+
|
| 41 |
+
## **第二階段:社交與競爭 —— 區域網路連線互動 (第 5 \- 8 週)**
|
| 42 |
+
|
| 43 |
+
**核心目標**:理解網路通訊,讓兩台電腦不再孤立,實現同步互動。
|
| 44 |
+
|
| 45 |
+
### **第 5 週:哈囉鄰居!Socket 通訊與 IP 基礎**
|
| 46 |
+
|
| 47 |
+
* **教學重點 (45 min)**:
|
| 48 |
+
* **住址與門牌**:理解 IP 地址 (電腦的位置) 與 Port 端口 (程式的門牌)。
|
| 49 |
+
* **連線協定**:簡單說明 TCP (安全點名) 與 UDP (快速廣播) 的差別。
|
| 50 |
+
* **實作任務 (60 min)**:編寫一個「班級秘密傳訊機」,讓 A 同學傳送文字,B 同學的視窗即時跳出訊息。
|
| 51 |
+
|
| 52 |
+
### **第 6 週:數據的包裹 —— JSON 序列化與同步**
|
| 53 |
+
|
| 54 |
+
* **教學重點 (45 min)**:
|
| 55 |
+
* **數據打包**:如何將座標 (x, y) 變成一個包裹 (JSON String) 寄給對方。
|
| 56 |
+
* **延遲與補償**:討論為什麼網路遊戲有時候會「閃現」或「Lag」。
|
| 57 |
+
* **實作任務 (75 min)**:雙人同步移動練習——我在我的螢幕動,你的螢幕會同步出現我的影子。
|
| 58 |
+
|
| 59 |
+
### **第 7 週:對戰邏輯 —— 誰打中了誰?**
|
| 60 |
+
|
| 61 |
+
* **教學重點 (40 min)**:
|
| 62 |
+
* **碰撞判定同步**:學習如何在兩台電腦上達成共識(誰先被打中?)。
|
| 63 |
+
* **狀態管理**:分數與血量在網路間的即時更新。
|
| 64 |
+
* **實作任務 (80 min)**:開發「網路版坦克對戰」雛形,實現發射子彈與受傷判定。
|
| 65 |
+
|
| 66 |
+
### **第 8 週:專案實作 (二) —— 班級網路競技場**
|
| 67 |
+
|
| 68 |
+
* **對戰賽事 (120 min)**:全班分組優化自己的遊戲,並進行連線測試與小組 PK,觀察網路通訊的穩定性。
|
| 69 |
+
|
| 70 |
+
## **第三階段:智慧互動 —— AI 視覺核心應用 (第 9 \- 12 週)**
|
| 71 |
+
|
| 72 |
+
**核心目標**:整合電腦視覺模型,實現「非接觸式」的新一代人機互動。
|
| 73 |
+
|
| 74 |
+
### **第 9 週:數位眼睛 —— MediaPipe 視覺辨識入門**
|
| 75 |
+
|
| 76 |
+
* **教學重點 (45 min)**:
|
| 77 |
+
* **座標的魔力**:介紹 AI 如何在 0.01 秒內找出手掌上的 21 個關鍵點。
|
| 78 |
+
* **影像與矩陣**:簡單說明攝影機畫面如何轉化為 Python 可以運算的數字。
|
| 79 |
+
* **實作任務 (65 min)**:啟動鏡頭,並在視窗中將食指尖標示出一個「雷射紅點」。
|
| 80 |
+
|
| 81 |
+
### **第 10 週:魔法手勢 —— 從座標到控制邏輯**
|
| 82 |
+
|
| 83 |
+
* **教學重點 (45 min)**:
|
| 84 |
+
* **距離計算**:利用勾股定理計算大拇指與食指的距���,判斷「抓取」動作。
|
| 85 |
+
* **邏輯觸發**:將手勢與第一階段的看板結合,揮揮手就能翻頁。
|
| 86 |
+
* **實作任務 (75 min)**:開發「虛擬畫板」,用手尖在空中畫畫,或用掌心控制視窗物件。
|
| 87 |
+
|
| 88 |
+
### **第 11 週:訓練你的大腦 —— Teachable Machine 實戰**
|
| 89 |
+
|
| 90 |
+
* **教學重點 (40 min)**:
|
| 91 |
+
* **機器學習流程**:蒐集數據、訓練模型、測試準確率。
|
| 92 |
+
* **模型匯出與匯入**:將訓練好的檔案放進 Python 中執行。
|
| 93 |
+
* **實作任務 (80 min)**:訓練一個「資源回收分類器」,拿著空瓶給鏡頭看,程式會告訴你該丟哪一類。
|
| 94 |
+
|
| 95 |
+
### **第 12 週:專案實作 (三) —— AI 生活工具箱**
|
| 96 |
+
|
| 97 |
+
* **獨立開發 (120 min)**:製作一個具備 AI 功能的小工具(如:坐姿提醒器、聲控小管家、手勢操作的音量旋鈕)。
|
| 98 |
+
|
| 99 |
+
## **第四階段:綜合整合 —— 終極專題發表 (第 13 \- 16 週)**
|
| 100 |
+
|
| 101 |
+
**核心目標**:解決真實痛點,展示軟體工程的完整思維。
|
| 102 |
+
|
| 103 |
+
### **第 13 週:創意發想與架構圖繪製**
|
| 104 |
+
|
| 105 |
+
* **任務**:分組討論「生活中還有什麼不方便?」,繪製系統流程圖,決定要用到 API、網路還是 AI。
|
| 106 |
+
|
| 107 |
+
### **第 14 \- 15 週:開發衝刺與問題排解**
|
| 108 |
+
|
| 109 |
+
* **任務**:密集的程式開發。學習團隊分工(有人寫介面、有人調校 AI、有人處理數據)。
|
| 110 |
+
|
| 111 |
+
### **第 16 週:Demo Day 成果發表**
|
| 112 |
+
|
| 113 |
+
* **活動**:佈置展位,邀請其他班級或老師來試玩,學生練習說明「我的程式解決了什麼問題」。
|
projects/2026project_class/doc/w1.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第一週:引擎重啟——從線性邏輯到即時系統 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 了解未來 16 週的課程藍圖,看見 Python 結合網路與 AI 所能實現的專案威力。
|
| 6 |
+
2. 理解「線性腳本」與「即時循環系統」的本質差異。
|
| 7 |
+
3. 掌握 Pygame 的四大核心架構(初始化、事件、邏輯、繪製)。
|
| 8 |
+
4. 成功建立開發環境並將程式拆解為多個步驟,執行第一個互動畫布。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[30 min] 歡迎與願景**:16 週藍圖介紹與未來學習里程碑 Demo 展示。
|
| 13 |
+
2. **[20 min] 思維大改造**:線性執行 vs. 即時系統(互動討論)。
|
| 14 |
+
3. **[25 min] 構造大拆解**:Pygame 四大支柱與程式碼結構解析。
|
| 15 |
+
4. **[25 min] 第一手練習**:按部就班建立遊戲主迴圈與事件監聽。
|
| 16 |
+
5. **[20 min] 挑戰任務**:變色追隨者(結合 Python 滑鼠與按鍵偵測邏輯)。
|
| 17 |
+
|
| 18 |
+
## **課程詳解內容**
|
| 19 |
+
|
| 20 |
+
### **第一部分:歡迎與願景 —— 16週藍圖與魔法展示 (30 min)**
|
| 21 |
+
|
| 22 |
+
* **投影片:我們這學期要做什麼?**
|
| 23 |
+
* 介紹課程的四大階段:
|
| 24 |
+
* **階段一 (第1-4週):** 實體數據看板(抓取 API 網路真實數據並畫出來)。
|
| 25 |
+
* **階段二 (第5-8週):** 區域網路連線互動(讓兩台不同電腦的程式互相傳送子彈與座標)。
|
| 26 |
+
* **階段三 (第9-12週):** 最新 AI 視覺應用(啟動鏡頭,讓電腦看懂你的手勢和動作)。
|
| 27 |
+
* **階段四 (第13-16週):** 終極專題發表(解決真實痛點的軟體開發)。
|
| 28 |
+
* **【重要實作】展示魔法時刻 (Demo Session)**
|
| 29 |
+
* 不要只用嘴巴說!老師請在此刻開啟預先寫好的三個小專案,展示給學生看(但不要秀出程式碼,保持神秘感):
|
| 30 |
+
1. **Demo 1:天氣看板**。展示一個能根據真實網路天氣數據改變背景的視窗。
|
| 31 |
+
2. **Demo 2:雙人連線方塊**。老師開兩個視窗互相送封包,展示「網路」的概念。
|
| 32 |
+
3. **Demo 3:AI 魔術手**。老師開鏡頭,用食指在畫面上移動 Pygame 的方塊。
|
| 33 |
+
* **目的**:讓學生在第一堂課就對「16 週後我會變得多強」有具體、視覺化的強烈期待。
|
| 34 |
+
|
| 35 |
+
### **第二部分:思維大改造 (20 min)**
|
| 36 |
+
|
| 37 |
+
* **投影片:你的程式「活」著嗎?**
|
| 38 |
+
* **線性程式 (Linear)**:像自動販賣機。開始 -> 輸入 -> 運算 -> 輸出 -> 結束。
|
| 39 |
+
* **即時系統 (Real-time)**:遊戲核心是一個無限循環(心跳)!一秒鐘跑 60 次,不斷檢查有沒有人按鍵盤。
|
| 40 |
+
* **互動實驗**:
|
| 41 |
+
* 讓學生想像:如果遊戲不是「無限循環」,當你不按按鍵時,遊戲畫面會發生什麼事?(畫面會靜止,甚至程式會結束)。
|
| 42 |
+
* **核心概念**:遊戲引擎是一秒鐘跑 60 次的 `while True` 迴圈。
|
| 43 |
+
|
| 44 |
+
### **第三部分:構造大拆解 (25 min)**
|
| 45 |
+
|
| 46 |
+
* **投影片:Pygame 的四大支柱 (骨架)**
|
| 47 |
+
1. **初始化 (Init)**:準備工具箱(聲音、影像、字體)。告訴電腦我們要借用顯示卡。
|
| 48 |
+
2. **事件監聽 (Events)**:感官系統。隨時檢查「排隊區」:有人按鍵嗎?滑鼠動了嗎?想關門了嗎?
|
| 49 |
+
3. **邏輯更新 (Logic)**:大腦思考。計算座標 `x = x + 5`,判斷有沒有撞到牆。
|
| 50 |
+
4. **畫面渲染 (Drawing)**:視覺呈現。把計算結果畫出來。
|
| 51 |
+
* **重點比喻解析**:
|
| 52 |
+
* **為何要 `screen.fill()`?**:把舊畫面擦掉。就像白板如果沒擦掉就畫下一幀,會變成一團亂塗鴉。
|
| 53 |
+
* **為何要 `pygame.display.flip()`?**:畫畫在小房間畫好,才翻過來給觀眾看,避免玩家看到畫一半的圖。
|
| 54 |
+
|
| 55 |
+
### **第四部分:第一手練習 (25 min)**
|
| 56 |
+
|
| 57 |
+
* **實作:一步步打造畫布**
|
| 58 |
+
* 帶領學生打開編輯器,確認 `pip install pygame`。
|
| 59 |
+
* **步驟拆解**(不直接給整包 Boilerplate,而是帶著打):
|
| 60 |
+
1. **建立畫布**:輸入 `pygame.init()` 與設定長寬。
|
| 61 |
+
2. **建立心跳**:寫出 `while True:` 與 `clock.tick(60)`。
|
| 62 |
+
3. **加入防禦**:寫出 `pygame.event.get()` 並且設定處理 `QUIT` 事件(告訴學生不寫這個視窗會死當)。
|
| 63 |
+
4. **繪出實體**:在迴圈最後填滿背景色 `fill`、畫圓形 `draw.circle`、並 `flip` 更新畫面。
|
| 64 |
+
* 這 25 分鐘重點在於學生跟著老師「逐段」讓視窗從死當 $\rightarrow$ 黑畫面 $\rightarrow$ 出現紅色圓形。
|
| 65 |
+
|
| 66 |
+
### **第五部分:挑戰任務:變色追隨者 (20 min)**
|
| 67 |
+
|
| 68 |
+
* **題目:製作一個「聽話的圈圈」**
|
| 69 |
+
* **基本要求 (Level 1)**:
|
| 70 |
+
1. 使用 `pygame.mouse.get_pos()` 讓圓形座標隨著滑鼠移動。
|
| 71 |
+
2. 當按下鍵盤 SPACE 鍵時,利用 `random` 模組讓圓形隨機變成另一種顏色。
|
| 72 |
+
* **進階要求 (PRO)**:
|
| 73 |
+
1. 讓圓形不要「瞬間」移動到滑鼠位置,而是緩慢地「追隨」滑鼠(計算距離並除以速度系數)。
|
| 74 |
+
2. 限制圓形不能跑出螢幕邊界。
|
| 75 |
+
|
| 76 |
+
## **教師筆記 (Teacher's Note)**
|
| 77 |
+
|
| 78 |
+
* **第一堂課的節奏**:這堂課的 120 分鐘非常緊湊。30 分鐘的 Demo 非常關鍵,這是往後 15 週學生遇到 Bug 願意撐下去的動力來源。
|
| 79 |
+
* **避免一次梭哈**:在「第一手練習」中,強烈建議老師在台上「刻意犯錯」。例如:不要寫 `event.get()` 就執行,讓全班看你的電腦當機;不要寫 `screen.fill()` 讓圓形畫出貪食蛇般的詭異軌跡。這比直接給他們完美程式碼的教學效果好上十倍。
|
projects/2026project_class/doc/w10.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第十週:魔法手勢——從座標到控制邏輯 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 將 MediaPipe 的各個指尖點位數據,利用幾何學的「距離運算」轉化為手指的絕對互動邏輯(如:掐合動作)。
|
| 6 |
+
2. 在 Pygame 遊戲主迴圈中穩定整合串接辨識模型所吐出的運算結果。
|
| 7 |
+
3. 實作「手掌游標」與基本的人機互動功能。
|
| 8 |
+
4. **【解決運算延遲問題】**:建立 Frame Skip (跳幀) 的概念以避免 AI 運算卡住畫面更新。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[20 min] 魔法的代價**:勾股定理與兩點距離。
|
| 13 |
+
2. **[40 min] 手印與勾結**:寫出抓取手勢判定 (Pinch Detection)。
|
| 14 |
+
3. **[40 min] 觸發!連動第一階段的介面**:手勢翻頁,甚至控制方塊與物件。
|
| 15 |
+
4. **[20 min] 挑戰任務**:設定動作冷卻機制 (Cooldown Time),解決重複觸發的問題。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:魔法的代價 (20 min)**
|
| 20 |
+
|
| 21 |
+
* **投影片:數學是魔法界的通用語**
|
| 22 |
+
* 在 2D 螢幕上,要怎麼知道兩根手指(比如大拇指 4 和 食指 8)是不是靠在一起?
|
| 23 |
+
* 複習 / 預習國中數學「勾股定理」:$\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$。
|
| 24 |
+
* **實戰程式法**:介紹 Python 的 `math.hypot(x2 - x1, y2 - y1)` 一行解。數學其實不可怕,因為有內建函數。
|
| 25 |
+
|
| 26 |
+
### **第二部分:手印與勾結 (40 min)**
|
| 27 |
+
|
| 28 |
+
* **尋找距離的設定值 (Threshold)**
|
| 29 |
+
* 回到上週的迴圈中,獲取食指 `(cx8, cy8)` 和大拇指 `(cx4, cy4)` 的視窗座標。
|
| 30 |
+
* **寫出距離公式印在終端機**:
|
| 31 |
+
```python
|
| 32 |
+
import math
|
| 33 |
+
dist = math.hypot(cx8 - cx4, cy8 - cy4)
|
| 34 |
+
print(dist)
|
| 35 |
+
```
|
| 36 |
+
* **觀察與實驗**:請同學張開手,再把兩指捏起來,紀錄終端機的數字變化,並設定一個 Threshold(比如距離小於 40)。
|
| 37 |
+
* 邏輯撰寫:
|
| 38 |
+
```python
|
| 39 |
+
if dist < 40:
|
| 40 |
+
is_pinching = True
|
| 41 |
+
pygame.draw.circle(screen, (0,255,0), (cx8, cy8), 15) # 捏起顯示綠色
|
| 42 |
+
else:
|
| 43 |
+
is_pinching = False
|
| 44 |
+
pygame.draw.circle(screen, (255,0,0), (cx8, cy8), 15) # 放開顯示紅色
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### **第三部分:觸發!連動第一階段的介面 (40 min)**
|
| 48 |
+
|
| 49 |
+
* **動作與物件碰撞的交響樂**
|
| 50 |
+
* 還記得 Pygame 的 `colliderect` 嗎?這次我們不用發射出來的子彈,我們用「手」!
|
| 51 |
+
* 結合第一階段的天氣看板,或者在畫面上畫出一個方形按鈕。
|
| 52 |
+
* **複合條件:**
|
| 53 |
+
```python
|
| 54 |
+
finger_rect = pygame.Rect(cx8, cy8, 10, 10)
|
| 55 |
+
# 如果手指在按鈕上,而且「捏起」了!
|
| 56 |
+
if finger_rect.colliderect(button_rect) and is_pinching:
|
| 57 |
+
print("魔法點擊按鈕成功!")
|
| 58 |
+
# 觸發天氣變更、背景顏色轉變等事件
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### **第四部分:挑戰任務與防呆冷卻 (20 min)**
|
| 62 |
+
|
| 63 |
+
* **題目:魔法冷卻時間**
|
| 64 |
+
* **問題**:如果在 `while` 迴圈中一旦「捏」下去,程式會在一秒內印出 60 次「魔法點擊」。
|
| 65 |
+
* **防呆解決**:教導如何使用 `pygame.time.get_ticks()` 或設立一個「開關 Boolean (Flag)」:
|
| 66 |
+
- 必需是「前一幀沒捏」且「這一幀捏了」才算是點擊一次。
|
| 67 |
+
這叫做**邊緣觸發判定 (Edge Detection)**,對未來開發遊戲和 UI 控制極為重要。
|
| 68 |
+
|
| 69 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 70 |
+
|
| 71 |
+
### **關鍵術語表 (Glossary)**
|
| 72 |
+
|
| 73 |
+
* **Threshold (閾值)**:觸發某個狀態判定的一條「界線數值」,如超過多少距離才算分開。
|
| 74 |
+
* **Hypotenuse (斜邊)**:直角三角形的斜邊,用來推演平面上任意兩點的直線距離。
|
| 75 |
+
|
| 76 |
+
### **常見問題解答 (FAQ)**
|
| 77 |
+
|
| 78 |
+
* **Q:我的手越靠近鏡頭,稍微伸出一點就觸發了,但手伸遠卻要捏緊才行?**
|
| 79 |
+
A:因為你在算的是「畫面上的絕對長度(Pixel)」!如果你的手掌離鏡頭近,那 `(cx8, cy8)` 隨便動就有很多像素;離得遠就很少。未來的進階作法是:以你手掌的寬度(點 0 到點 5 的長度)當作基準尺!
|
| 80 |
+
|
| 81 |
+
## **教師筆記 (Teacher's Note)**
|
| 82 |
+
|
| 83 |
+
* **【優化防呆再次強烈建議】**:這堂課如果使用視訊鏡頭而導致 FPS 嚴重降低至 5 或直接當掉畫面。請直接開大牌,在 `cv2.imshow` 和 `cap.read()` 這部分進行跳幀處理(例如:每 Pygame 三次 Update 畫面才請鏡頭抓一次),保護好同學好不容易開發出來的 Pygame 主畫布。
|
projects/2026project_class/doc/w11.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第十一週:訓練你的大腦——Teachable Machine 實戰 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 了解機器學習 (Machine Learning) 的三大核心步驟:資料蒐集、神經網路訓練、模型預測。
|
| 6 |
+
2. 認識以圖搜圖與圖像分類。
|
| 7 |
+
3. 把模型打包 (Keras .h5 / TFLite),匯入至 Python 中,使自己的 Python 看懂萬物。
|
| 8 |
+
4. 體驗「準備優質訓練資料」的重要性。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[20 min] 白痴與天才**:機器學習如何靠「看很多張圖」來歸納世界。
|
| 13 |
+
2. **[40 min] Teachable Machine 遊樂場**:實作專屬你的三個分類模型。
|
| 14 |
+
3. **[40 min] 接通神經網路**:把訓練成果放進 Python。
|
| 15 |
+
4. **[20 min] 挑戰任務**:剪刀、石頭、布!或者猜猜看誰在喝水。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:白痴與天才 (20 min)**
|
| 20 |
+
|
| 21 |
+
* **投影片:一隻貓是怎麼被定義的?**
|
| 22 |
+
* 如果叫我們寫 if-else 來判斷影像中是不是貓(if 有兩隻尖尖的耳朵 and 有鬍鬚...)。幾乎是不可能寫出來的,因為不同光源和角度千變萬化。
|
| 23 |
+
* **現代解法:倒因為果**。我不給你 if-else,我給你十萬張印著貓的圖片,五十萬張不是貓的,然後你自己去裡面找出那該死的數學規律。這就是 Deep Learning!
|
| 24 |
+
|
| 25 |
+
### **第二部分:Teachable Machine 遊樂場 (40 min)**
|
| 26 |
+
|
| 27 |
+
* **線上神裝使用**
|
| 28 |
+
* 登入 Google Teachable Machine 網站選擇 `Image Project` > `Standard Image Model`。
|
| 29 |
+
* **搜集高品質資料 (Data Collection)**
|
| 30 |
+
* **Class 1 (背景或無意義動作)**:告訴學生,一定要有一個類別是「沒在做事」的,這超級重要!沒有教它什麼是背景,它看見空白牆壁會以為是一隻貓的近拍!
|
| 31 |
+
* **Class 2 (你要辨識的動作)**:手拿水瓶喝水、剪刀手、比讚。
|
| 32 |
+
* 拍攝每類超過 200 張。
|
| 33 |
+
* **按一鍵就完成訓練 (Train) 與輸出 (Export)**:選擇匯出為 TensorFlow (Keras) `.h5` 檔案。
|
| 34 |
+
|
| 35 |
+
### **第三部分:接通神經網路 (40 min)**
|
| 36 |
+
|
| 37 |
+
* **檔案安置與庫載入**
|
| 38 |
+
* 將下載下來的 `keras_model.h5` 和 `labels.txt` 放進今天專案的根目錄。
|
| 39 |
+
* 備註:如果是較舊的電腦,可教導匯出 OpenCV 較好處理的 TFLite。
|
| 40 |
+
* **讀取與推論 (Inference)**:
|
| 41 |
+
```python
|
| 42 |
+
from keras.models import load_model # (或是其它匯入方式依當下環境)
|
| 43 |
+
import cv2
|
| 44 |
+
import numpy as np
|
| 45 |
+
|
| 46 |
+
model = load_model("keras_model.h5", compile=False)
|
| 47 |
+
labels = open("labels.txt", "r").readlines()
|
| 48 |
+
|
| 49 |
+
# ... 省略攝影機啟動程式 (cap.read) ...
|
| 50 |
+
|
| 51 |
+
# Keras 要求特定的影像處理 (Resize to 224x224 & Normalize)
|
| 52 |
+
img_resized = cv2.resize(img, (224, 224))
|
| 53 |
+
img_array = np.asarray(img_resized, dtype=np.float32).reshape(1, 224, 224, 3)
|
| 54 |
+
img_array = (img_array / 127.5) - 1
|
| 55 |
+
|
| 56 |
+
# 前饋推論,得到有每一個類別機率的數字陣列
|
| 57 |
+
prediction = model.predict(img_array)
|
| 58 |
+
index = np.argmax(prediction) # 找最大機率的那一項標籤!
|
| 59 |
+
print(labels[index])
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### **第四部分:挑戰任務 (20 min)**
|
| 63 |
+
|
| 64 |
+
* **題目:AI 聲音播放器**
|
| 65 |
+
* 當類別 1 "喝水中" 的機率大於 90% 的時候,立刻觸發 Pygame 寫好的音效播放,放出氣泡咕嚕聲。
|
| 66 |
+
* 問題思考:如果在 `while` 的連續迴圈下,一偵測到喝水就播音效,音軌會被不斷覆蓋而鬼畜。
|
| 67 |
+
* **引導修復**:利用上週學到的 Boolean 狀態標記,讓音效「只在狀態剛變成喝水的那一瞬間」播放,之後不管再高機率都不重新播,直到狀態變為背景再重置。
|
| 68 |
+
|
| 69 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 70 |
+
|
| 71 |
+
### **關鍵術語表 (Glossary)**
|
| 72 |
+
|
| 73 |
+
* **Model Training (模型訓練)**:提供正確答案與資料集,讓神經網路不斷修改內部幾百萬個變數以降低預測誤差。
|
| 74 |
+
* **Inference (推論)**:將訓練好的「大腦迴路結果 (\.h5 等)」丟給未知的影像去「猜」出這是什麼。我們用 Python 就是在做這階段。
|
| 75 |
+
* **Labels (標籤)**:這是水、那是杯子、那是背景。人為賦予的各類別名字。
|
| 76 |
+
|
| 77 |
+
### **常見問題解答 (FAQ)**
|
| 78 |
+
|
| 79 |
+
* **Q:為什麼我的模型在網站上很準,進到 Python 裡就變白痴?**
|
| 80 |
+
A:有兩個可能。第一,你的 Python 影像(如矩形變正方型)變形度過大,或是 BGR 沒有轉 RGB!Keras 模型只接受訓練當時同樣的處理法。
|
| 81 |
+
* **Q:為什麼只要一跑到 `model.predict` 我的畫面就會大卡?**
|
| 82 |
+
A:神經網路本來就不是給一般 CPU 玩的好嗎!不要每一幀都預測!你可以加一個 `if counter % 5 == 0:` 每五格影像,再把畫面丟去判斷一次就可以解決。
|
| 83 |
+
|
| 84 |
+
## **教師筆記 (Teacher's Note)**
|
| 85 |
+
|
| 86 |
+
* **降低環境挫折**:Keras 及 Tensorflow 動輒佔據好幾 GB 空間,很多防毒軟體甚至覺得 `load_model` 是木馬行徑。請事前統一叫同學裝好。並一定要幫學生將範本代碼與 `cv2.resize` 那三行死背的預處理程式碼準備好,否則他們花兩小時都生不出一行前饋。
|
projects/2026project_class/doc/w12.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第十二週:專案實作 (三)——AI 生活工具箱 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 將 MediaPipe 或者 Teachable Machine 訓練好的 AI 技術,獨立連動並包裝進使用者的具體解決方案中。
|
| 6 |
+
2. 認識生活與數位結合的創新點子。
|
| 7 |
+
3. **【優化項目】深化 PBL 敏捷開發 Scrum 站會**:針對學生易產生的「點子過大與迷幻發散」狀況進行控管與限制。
|
| 8 |
+
4. **【優化項目】跨電腦 QA 找碴**:訓練抗壓除錯能力與軟體穩定度。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[15 min] 專案啟動與進階 Scrum 站會**:生活痛點定義與 MVP 設定。
|
| 13 |
+
2. **[65 min] 開發衝刺 (Sprint)**:實作工具箱的 AI 分析與介面回饋。
|
| 14 |
+
3. **[20 min] 破壞者大會 (QA 測試)**:請同學坐在你位子上嘗試用奇葩動作破壞你的程式。
|
| 15 |
+
4. **[20 min] Demo 分享會**:快速演練如何用三十秒推銷 AI 生活小道具。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:進階 Scrum 站會 (15 min)**
|
| 20 |
+
|
| 21 |
+
* **痛點收斂與技術決選**
|
| 22 |
+
* 第九至第十一週,我們學了 AI 是怎麼運作的。現在請全班分組。
|
| 23 |
+
* 每個人快速提出:「日常生活中,我都做不到什麼?或者是覺得有什麼很煩的例行公事可以被自動化的?」
|
| 24 |
+
* 例子A:我常常打電動打到坐姿很差或靠到電腦銀幕上。
|
| 25 |
+
* 例子B:音樂太大聲,手又髒髒的不想碰鍵盤去調整。
|
| 26 |
+
* **技術抉擇:**
|
| 27 |
+
* 要選 MediaPipe(需要抓手點數)還是 Teachable Machine(直接餵畫面)?
|
| 28 |
+
* 定義好你的 MVP 到底要做出什麼!
|
| 29 |
+
|
| 30 |
+
### **第二部分:開發衝刺 Sprint (65 min)**
|
| 31 |
+
|
| 32 |
+
* **沉浸開發與老師巡迴打壓:**
|
| 33 |
+
* 同學 A:「我要用 Teachable Machine 做一個能夠偵測到我有沒有打掃房間,沒有的話它會叫的鬧鐘。」
|
| 34 |
+
* 老師:「很棒的點子!但你今天要怎麼拿出一個剛好能表現『沒打掃』跟『有打掃』的示範?而且只有 60 分鐘!請給我一個拿在你手上的水瓶有沒有蓋好蓋子分類器就好。」
|
| 35 |
+
* **將「偵測到」化作「互動行為」**:
|
| 36 |
+
* AI 抓出結果後,要在 Pygame 原先建立好的畫面上,切換提示圖文字(如「危險!坐姿太近!」)。
|
| 37 |
+
* 若要播放警報音,要處理延遲與重複播放。
|
| 38 |
+
|
| 39 |
+
### **第三部分:破壞者大會 (跨電腦 QA 防呆測試) (20 min)**
|
| 40 |
+
|
| 41 |
+
* **【優化實踐】QA 找碴的軟體工程**
|
| 42 |
+
* 過去 AI 的辨識都是同學用自己特有的習慣與角度在拍攝。
|
| 43 |
+
* 「現在,請左邊的同學坐上你的位子,給你的 AI 看看他的手、他的臉,或是拿同樣的東西給他看!」
|
| 44 |
+
* 紀錄系統有沒有發瘋?同學把手伸到畫面外面時,會不會噴出 Index Out of Bounds 或者直接閃退?
|
| 45 |
+
* 修復他!加上 `try-except` 或更多的防呆判定。不要把未處理的異常 (Unhandled Exception) 留給使用者。
|
| 46 |
+
|
| 47 |
+
### **第四部分:Demo 分享會 (20 min)**
|
| 48 |
+
|
| 49 |
+
* **銷售你的 AI 道具**
|
| 50 |
+
* 「這款名為【護眼大師】的小怪獸,在你的臉離螢幕低於 XX 距離(由兩眼 Landmark 的大小決定),就會尖叫並暫停你聽的音樂。只要我們有電腦與鏡頭,這就是免費的硬體!」
|
| 51 |
+
* 教師引導台下同學思考:它的辨識有盲點嗎?什麼環境下會破功?
|
| 52 |
+
|
| 53 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 54 |
+
|
| 55 |
+
### **關鍵術語表 (Glossary)**
|
| 56 |
+
|
| 57 |
+
* **Edge Case (邊緣案例)**:因為開發者沒想過而引發的錯誤,比如在手勢識別時,畫面中出現「兩隻手」,或者「根本沒有手」,會導致不正常的程式執行。
|
| 58 |
+
* **False Positive (偽陽性)**:明明沒有做出壞坐姿,卻硬是報警,表示把不該抓的結果給誤抓了。對使用者體驗影響極大。
|
| 59 |
+
|
| 60 |
+
### **常見問題解答 (FAQ)**
|
| 61 |
+
|
| 62 |
+
* **Q:別人一來玩我的工具,它的 Teachable Machine 就一直辨識錯類別?**
|
| 63 |
+
A:代表你上次訓練時,把「你專屬的衣服顏色、你的房間光線」也當成了判斷的特徵。你需要蒐集更多在不同房間不同人出現的豐富度資料,才能避免過度擬合 (Overfitting)。
|
| 64 |
+
|
| 65 |
+
## **教師筆記 (Teacher's Note)**
|
| 66 |
+
|
| 67 |
+
* **防禦性程式開發**:這週是驗證 AI 防呆最完美的體驗。由於 AI 輸出的數值隨時在無序的浮動(甚至突然消失成為 `None`),這是培養他們加上 `if results.multi_hand_landmarks is not None:` 最棒的時段。
|
| 68 |
+
* **PBL 痛點**:幫助學生理解到,即使是最頂尖的工程師與模型,也會對「手在鏡頭外」這等常識無能為力。一切都建立在開發者是否足夠「防禦」。
|
projects/2026project_class/doc/w13.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第十三週:創意發想與架構圖繪製——系統分析 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 從程式編寫員步入「軟體系統分析師 (System Analyst)」,面對一個模糊的社會生活痛點,構建能以軟體解決的可行方案。
|
| 6 |
+
2. 透過繪製具體的架構與流程圖,訓練邏輯拆解與團隊分工能力。
|
| 7 |
+
3. **【優化項目】MVP ( Minimum Viable Product) 收斂與防雷控管**:避免過度野心導致後三週發生災難。
|
| 8 |
+
4. 複習前三階段的所有功能:生活 API、連線網路對戰、AI 電腦視覺。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[20 min] 重點回放與痛點挖掘**:複習兵器譜,發現社會問題。
|
| 13 |
+
2. **[30 min] 專題 Ideas Pitch (點子審查會)**:發表點子與殘酷可行性評估。
|
| 14 |
+
3. **[40 min] 系統拆分與架構繪製**:UI 長怎樣?資料怎麼流動?
|
| 15 |
+
4. **[30 min] 專案資料夾建立與備份設定**:技術基底的初始建置。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:重點回放與痛點挖掘 (20 min)**
|
| 20 |
+
|
| 21 |
+
* **投影片:回顧我們的軍火庫**
|
| 22 |
+
* 階段一:API。我們可以拿到全台灣公車的現在位置。
|
| 23 |
+
* 階段二:Socket 區網。我們可以把 A 電腦發生的一點小事傳給 B 的螢幕顯示。
|
| 24 |
+
* 階段三:電腦視覺。我們可以讓電腦看懂一切我們塞給它的動作或圖形。
|
| 25 |
+
* **分組腦力激盪**:
|
| 26 |
+
* 給每個小組三張白紙,寫下這世界(或學校裡)「很蠢」、「很麻煩」、「常常造成糾紛」的幾個問題。
|
| 27 |
+
|
| 28 |
+
### **第二部分:專題 Ideas Pitch 點子審查 (30 min)**
|
| 29 |
+
|
| 30 |
+
* **上台發表你的狂想**:
|
| 31 |
+
* "Pitch" 在矽谷是向投資人推銷的概念。告訴大家:「我們想解決中午買午餐大排長龍的問題,所以要設計一個教室座位點餐連線系統,附帶 AI 手勢決定便當口味。」
|
| 32 |
+
* **【優化實踐】老師的殘酷 MVP 剃刀**:
|
| 33 |
+
* **野心過大**:「你要怎麼在學校裡連通所有教室的內網?而且你兩週內還要把菜單畫到畫面上?而且 AI 辨識雞腿排還是排骨根本是天方夜譚!」
|
| 34 |
+
* **收斂與打磨**:「你們的 MVP 應該是:用區網傳遞 JSON 到同一個螢幕上的 Pygame UI,AI 不要有,只要按鍵盤就是了。API 試著加一個冷笑話讓排隊的畫面不無聊就好。」
|
| 35 |
+
|
| 36 |
+
### **第三部分:系統拆分與架構繪製 (40 min)**
|
| 37 |
+
|
| 38 |
+
* **軟體工程的大圖紙**
|
| 39 |
+
* 給予全開壁報紙,請他們畫出這個期末專案的:
|
| 40 |
+
1. **UI 簡圖**:包含所有的「主畫面」、「設定畫面」或「連線配對區」。有哪些按鈕?
|
| 41 |
+
2. **數據流程流 (Data Flow)**:標示哪一塊要跟外部伺服器發 Request?哪一塊要用 Socket 發給隔壁對手?
|
| 42 |
+
3. **AI 辨識迴圈**:有沒有專門處理 `cv2.read()` 與推論的地方?
|
| 43 |
+
* **團隊職責分配**:
|
| 44 |
+
* 「誰當 UI 工程師?」(負責畫 Pygame 畫面)。
|
| 45 |
+
* 「誰當邏輯分析師?」(負責接收封包處理、修補防呆或 AI 模型訓練)。
|
| 46 |
+
* 組長負責整合所有的變數!
|
| 47 |
+
|
| 48 |
+
### **第四部分:專案建立與版本控制宣導 (30 min)**
|
| 49 |
+
|
| 50 |
+
* **【優化實踐】專案的起源資料夾**
|
| 51 |
+
* 大家在電腦桌面上建立一個名為 `FinalProject_Group1_v1` 的資料夾。
|
| 52 |
+
* 放進老師發放的最乾淨的 Pygame 樣板 `app.py`。
|
| 53 |
+
* **宣導版本控制鐵律**:「當你們要把某段寫好的模組 (例如網路通訊),複製貼進這個 `app.py` 之前,請務必先把資料夾複製改名成 `FinalProject_Group1_v2`!我不想在那天聽到哭聲。」
|
| 54 |
+
|
| 55 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 56 |
+
|
| 57 |
+
### **關鍵術語表 (Glossary)**
|
| 58 |
+
|
| 59 |
+
* **MVP (最小可行性產品)**:先用最低成本只做出最核心、最痛點解決功能的產品,用來測試大家會不會想用。
|
| 60 |
+
* **Wireframe (線框圖)**:一種低保真度的 UI 設計草圖,不考慮顏色美觀,只規劃按鈕、數據和各種狀態字串的螢幕排版位置。
|
| 61 |
+
|
| 62 |
+
### **常見問題解答 (FAQ)**
|
| 63 |
+
|
| 64 |
+
* **Q:老師,我們什麼都不會畫,畫面超級醜怎麼辦?**
|
| 65 |
+
A:不要忘了 Python 是拿來解決自動化與資料問題的!只要 UI 清楚整齊,能秀出你對程式脈絡的理解(如抓取 API 與 Socket 傳輸),那就是滿分。不用比美術大賽!
|
| 66 |
+
|
| 67 |
+
## **教師筆記 (Teacher's Note)**
|
| 68 |
+
|
| 69 |
+
* **這是一堂「文科 / 管理」重於「寫扣」的課**。很多國中/小學生對這節很不在乎,想直接打開 IDE 劈哩啪啦打起程式。
|
| 70 |
+
* 老師要嚴格執行**「沒有畫出架構圖,沒有給老師確認過 MVP 範圍的組別,不准打開電腦寫程式」**。這才是 PBL (Project-Based Learning) 教導的「規劃」能力之精髓。
|
projects/2026project_class/doc/w14.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第十四週:開發衝刺 (一)——核心邏輯建構 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 建立專案第一版最小可行性產品 (MVP) 的核心技術打底。
|
| 6 |
+
2. 進行模組化程式撰寫,學習將大專案切分為:純邏輯處理與純介面顯示的兩個層次。
|
| 7 |
+
3. **【優化項目】PBL 敏捷開發 Scrum 站會**:培養專注力,設定當日具體產出目標。
|
| 8 |
+
4. **【優化項目】版本控制與除錯心法**:養成隨時備份版本的習慣,避免程式改壞全毀的慘劇。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[10 min] Scrum 站立會議**:每組宣告今天的最高開發目標。
|
| 13 |
+
2. **[90 min] 深潛開發 (Deep Work)**:無干擾的密集程式撰寫。
|
| 14 |
+
3. **[20 min] 版本備份與進度盤點**:教導並執行檔案多重備份與版本命名法。
|
| 15 |
+
|
| 16 |
+
## **課程詳解內容**
|
| 17 |
+
|
| 18 |
+
### **第一部分:Scrum 站立會議 (10 min)**
|
| 19 |
+
|
| 20 |
+
* **目標宣誓**
|
| 21 |
+
* 在開發開始前,所有人起立,由組長發言:
|
| 22 |
+
* 「我們這週的 MVP 是:不用 AI,只要按鍵盤能送出 API,並且畫面會動就好!」
|
| 23 |
+
* 老師在一旁再次擔任「把關者」,告訴他們「畫面不要畫、字體不要換,先讓它『動起來』!」
|
| 24 |
+
|
| 25 |
+
### **第二部分:深潛開發 (Deep Work) (90 min)**
|
| 26 |
+
|
| 27 |
+
* **本週的開發準則 (Golden Rules)**:
|
| 28 |
+
1. **拒絕美觀**:這個禮拜如果有人在用 Photoshop 去背圖片,或是修改字體大小,請嚴厲喝止。這週是「邏輯」的週次。
|
| 29 |
+
2. **除錯自救第一步**:遇到紅字 Error,第一時間不是叫老師,而是用 Google 或翻譯軟體將最後一句的 `TypeError` 或 `KeyError` 看懂。
|
| 30 |
+
* **教師輔導策略**:
|
| 31 |
+
* 在教室走動,主要回答「架構性」問題(例如:我的 while 迴圈為什麼一直卡死?),而不幫忙修「打錯字」或「縮排錯誤」這種初階問題,培養獨立除錯能力。
|
| 32 |
+
|
| 33 |
+
### **第三部分:版本控制與除錯心法 (20 min)**
|
| 34 |
+
|
| 35 |
+
* **【優化實踐】專案的守護神——備份命名**
|
| 36 |
+
* 打鐘前 20 分鐘,強制全班停下手邊工作。
|
| 37 |
+
* **狀況發生與預防**:
|
| 38 |
+
* 「有沒有人剛剛改了一行想要加新功能,結果本來會動的突然不會動,而且你也按不了 Ctrl+Z 倒退回去了?」
|
| 39 |
+
* **強制實作:**
|
| 40 |
+
* 將剛才運作正常的 `app_v1.py` 另存新檔為 `app_v2_add_network.py`。
|
| 41 |
+
* 建立資料夾觀念:將所有失敗的舊程式集中在一個叫做 `Old_Versions` 的廢件區資料夾,專案根目錄永遠只有一個主程式 `app.py`。
|
| 42 |
+
|
| 43 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 44 |
+
|
| 45 |
+
### **關鍵術語表 (Glossary)**
|
| 46 |
+
|
| 47 |
+
* **Deep Work (深潛工作)**:在無干擾的情況下專注於高認知要求的工作狀態。這對於寫出不會有蟲的程式碼非常重要。
|
| 48 |
+
* **Modularity (模組化)**:把複雜的系統拆解成互相獨立的小模塊(比如 A 同學寫 API 檔案,B 同學寫 Pygame 介面檔,然後匯入 `app.py` 組合,而非全部擠在同一個檔案內)。
|
| 49 |
+
|
| 50 |
+
### **常見問題解答 (FAQ)**
|
| 51 |
+
|
| 52 |
+
* **Q:我的程式在自己的電腦上會跑,移到隔壁同學做測試的時候就一直報找不到檔案的錯?**
|
| 53 |
+
A:你的檔案路徑寫成了「絕對路徑(如:C槽/桌面)」。請一律使用「相對路徑(只寫 `background.png`)」,並確保圖片與 `app.py` 在同一個階層,讓資料夾具有「可移植性」。
|
| 54 |
+
|
| 55 |
+
## **教師筆記 (Teacher's Note)**
|
| 56 |
+
|
| 57 |
+
* 這是一段無聲但壓力極大的課堂。很多組別會因為「什麼都做不出來」而開始聊天放棄。老師必須在巡場時給予**「微小的成功體驗」**:只要他們能成功 `print` 出一個字串,就大力讚賞他們解決了核心的一大半,然後再鼓勵他們將字串接到畫面上。
|
| 58 |
+
* 老師的心態也要穩住:不要有「一定要每組都做出完整功能」的執念。程式不是魔法,會動與不會動就是一行防呆的距離。陪伴他們度過這段陣痛期即可。
|
projects/2026project_class/doc/w15.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第十五週:開發衝刺 (二)——體驗優化與除錯 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 透過 UI/UX 打磨,將專案從「可以用」的原始人狀態,提升到「好用漂亮」的現代軟體。
|
| 6 |
+
2. 完整應用之前所學之字體渲染、音樂/音效匯入與背景圖片縮放技術。
|
| 7 |
+
3. **【優化項目】跨組 QA 測試 (交叉找碴)**:理解使用者永遠是最大的 Bug 製造機,訓練從開發者轉為品保者的視野。
|
| 8 |
+
4. 完成最終除錯與封包(Package),使專案能一鍵獨立穩定運行。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[10 min] Scrum 站立會議**:最後衝刺的目標宣告。
|
| 13 |
+
2. **[50 min] 介面優化開發**:放飛美感,加入所有華麗的視覺設計。
|
| 14 |
+
3. **[40 min] 破壞性 QA 測試**:交換試玩並填寫 Bug 紀錄單。
|
| 15 |
+
4. **[20 min] 最終除錯與封裝**:除蟲收尾,一鍵點開即玩設定。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:Scrum 站立會議 (10 min)**
|
| 20 |
+
|
| 21 |
+
* 這是最後一次的站會。組長報告:「我們已經可以動了,我們今天要把黑底白字變成精緻的介面,還要加入音效跟防呆跳出視窗!」
|
| 22 |
+
|
| 23 |
+
### **第二部分:介面優化開發 (50 min)**
|
| 24 |
+
|
| 25 |
+
* **UX/UI 體驗升級**:
|
| 26 |
+
* **美觀**:套用去背好的角色圖片、高解析度的 Pygame 背景 (`pygame.transform.scale`)、好看的 TTF 字型。
|
| 27 |
+
* **回饋感**:加入 `pygame.mixer`。按下任何按鈕都要有「滴」聲,AI 觸發時要有特定的特效音。
|
| 28 |
+
* **互動提示 (Tooltip / Indicator)**:如果正在抓取 API,畫面上不是死機,而是要有一串字閃爍:「網路讀取中...」。如果沒接上 AI 鏡頭,要會印出:「請接上攝影機」而非直接報錯閃退。
|
| 29 |
+
|
| 30 |
+
### **第三部分:破壞性 QA 測試 (40 min)**
|
| 31 |
+
|
| 32 |
+
* **【優化實踐】使用者是來搞砸你的程式的**:
|
| 33 |
+
* **跨組交換**:全班站起來,向右移動一組的座位。
|
| 34 |
+
* **賦予任務**:「請盡量用力弄壞別人的程式!包含:亂按按鍵、拔掉網路線、把手遮住鏡頭、把視窗縮小又放大、輸入框亂打中文字。」
|
| 35 |
+
* **Bug 紀錄單 (Bug Report)**:
|
| 36 |
+
* 要求試玩者寫下一張小紙條:「當我把網路關掉時,發生了什麼事?(如:程式停住或彈出亂碼)」
|
| 37 |
+
* **為什麼要做這件事?**:自己寫的程式永遠有盲點。這也是業界最重要的 Quality Assurance 流程。
|
| 38 |
+
|
| 39 |
+
### **第四部分:最終除錯與封裝 (20 min)**
|
| 40 |
+
|
| 41 |
+
* **最後 20 分鐘防呆與除錯**:
|
| 42 |
+
* 拿著剛剛別人幫你寫的 Bug 紀錄單,透過 `try-except` 或更多的 `if` 判斷修復這些崩潰點。
|
| 43 |
+
* **封裝獨立執行**:
|
| 44 |
+
* 確保能在終端機打 `python app.py` 就順利進入並零報錯離開。
|
| 45 |
+
|
| 46 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 47 |
+
|
| 48 |
+
### **關鍵術語表 (Glossary)**
|
| 49 |
+
|
| 50 |
+
* **UX (User Experience / 使用者體驗)**:包含速度、聲音回饋、防呆提示等能讓使用者感到舒服的心理學設計。
|
| 51 |
+
* **QA (Quality Assurance / 軟體品質保證)**:專門找程式毛病的一門學問。優秀的 QA 工程師價值不會低於負責開發的軟體工程師。
|
| 52 |
+
* **Unhandled Exception (未處理異常)**:當防呆沒有寫好,一旦發生例外情況,程式直接中斷閃退,這是 UX 的災難。
|
| 53 |
+
|
| 54 |
+
### **常見問題解答 (FAQ)**
|
| 55 |
+
|
| 56 |
+
* **Q:別人發現我的遊戲只要網路稍微不穩,人物就會瞬移到奇怪的地方?**
|
| 57 |
+
A:如果是使用之前提過的 UDP,掉包在所難免。你可以實作「畫面插值(如果收不到封包,就假設對方還在繼續往前走)」來欺騙眼睛。
|
| 58 |
+
|
| 59 |
+
## **教師筆記 (Teacher's Note)**
|
| 60 |
+
|
| 61 |
+
* **QA 文化的建立**:在別人程式找碴的時候,很容易引起兩組吵架(「你幹嘛亂按破壞我的程式!!」)。老師必須在過程中強力倡導這才是業界幫對方省下「產品上線直接倒閉」救命錢的偉大行為,將衝突轉化為專業技術評估的樂趣。
|
projects/2026project_class/doc/w16.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第十六週:Demo Day 成果發表 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 完成 16 週的學習歷程,透過 Demo Day (軟體成果展) 體驗科技創作者向社會展示產品的成就感。
|
| 6 |
+
2. 磨練面對大眾的技術表達、痛點分析與溝通說服能力。
|
| 7 |
+
3. 建立對自己完成之全端(介面+網路+AI邏輯)軟體作品的自信,並回顧過去幾週的成長。
|
| 8 |
+
|
| 9 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 10 |
+
|
| 11 |
+
1. **[20 min] 教室佈置與攤位最後確認**:如同小型的黑客松或展位準備。
|
| 12 |
+
2. **[60 min] 博覽會時間 (Demo & Pitch)**:自由體驗與解說時段。
|
| 13 |
+
3. **[25 min] 榮耀時刻**:頒獎典禮與評審(或同儕)講評反饋。
|
| 14 |
+
4. **[15 min] 總結與未來展望**:帶著超能力離開教室。
|
| 15 |
+
|
| 16 |
+
## **課程詳解內容**
|
| 17 |
+
|
| 18 |
+
### **第一部分:教室佈置與攤位最後確認 (20 min)**
|
| 19 |
+
|
| 20 |
+
* **環境設定**
|
| 21 |
+
* 各組將電腦轉向對外,打開程式為「全螢幕且已經順暢執行」的狀態。
|
| 22 |
+
* 桌上可擺放一張 A4 紙,寫有自己的專案名稱與簡單標語:「用 AI ___ 拯救你生活中的 ___ 痛點!」
|
| 23 |
+
* **演練與深呼吸**:
|
| 24 |
+
* 給予最後 5 分鐘,請各組互相練習「30 秒產品電梯簡報 (Elevator Pitch)」。
|
| 25 |
+
|
| 26 |
+
### **第二部分:博覽會時間 (Demo & Pitch) (60 min)**
|
| 27 |
+
|
| 28 |
+
* **闖關與自由試玩體驗**
|
| 29 |
+
* **【優化實踐】外部觀眾邀約**:如果校方允許,強烈建議邀請其他班級學生、班導師甚至家長入場。陌生的觀眾更能激發開發者的腎上腺素與成就感。
|
| 30 |
+
* **作者講解**:當有人來到攤位前試玩,開發學生必須站在旁邊指導並講解以下三點:
|
| 31 |
+
1. 「這是為了解決什麼問題而開發的?」
|
| 32 |
+
2. 「這背後用到了什麼技術?(API 抓資料、AI 看手勢、區域網路連線)」
|
| 33 |
+
3. 「如果未來還有時間,我最想加什麼功能進去?」
|
| 34 |
+
|
| 35 |
+
### **第三部分:榮耀時刻與頒獎 (25 min)**
|
| 36 |
+
|
| 37 |
+
* **同儕票選與老師講評**:
|
| 38 |
+
* 剛才互相走動體驗完畢後,發放三張貼紙給每個人,請他們貼在最欣賞的專案海報或黑板上。
|
| 39 |
+
* **頒發專屬獎項 (非名次制)**:鼓勵多元發展,頒發如「最強防呆 UI 獎」、「最具商業價值點子獎」、「最瘋狂 AI 技術獎」。不要只頒發第一二三名,以免抹煞不同特質的學生。
|
| 40 |
+
* **心路歷程短講**:
|
| 41 |
+
* 抽點一位技術曾經最卡關,但最後撐過來的同學分享:「你這 16 週到底經歷了什麼可怕(好笑)的地獄?」
|
| 42 |
+
|
| 43 |
+
### **第四部分:總結與未來展望 (15 min)**
|
| 44 |
+
|
| 45 |
+
* **老師做最溫暖的閉幕演講**
|
| 46 |
+
* 「十六週前,你們認為 Python 就是拿來印字串或者解數學題的無聊考試。」
|
| 47 |
+
* 「今天,你們能從網路上抓全世界的數據(API)、能讓兩台電腦互相溝通(Socket)、還能讓自己的鏡頭看懂人類的手勢(AI Vision),最後做成一個精美的介面!」
|
| 48 |
+
* 「這不是魔法,這是邏輯加上你們一次又一次 Try-Except 修好 Bug 換來的『超能力』。」
|
| 49 |
+
* 「從這裡走出去後,如果遇到生活中覺得不順眼、不方便、很浪費時間的事,大聲說:『這個,可以用我的 Python 寫支程式搞定它!』」
|
| 50 |
+
|
| 51 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 52 |
+
|
| 53 |
+
### **關鍵術語表 (Glossary)**
|
| 54 |
+
|
| 55 |
+
* **Demo Day (演示日)**:新創團隊或培訓營的結業日,向大眾或投資人展現自家軟體能如何運作、有什麼價值的高光時刻。
|
| 56 |
+
* **Elevator Pitch (電梯簡報)**:假設你在電梯裡遇到大老闆,要在這 30 秒抵達樓層的時間內,把你的專案核心精華講出來並且吸引到他。
|
| 57 |
+
|
| 58 |
+
### **常見問題解答 (FAQ)**
|
| 59 |
+
|
| 60 |
+
* **無** (這週不找 Bug,只享受成果的榮耀)。
|
| 61 |
+
|
| 62 |
+
## **教師筆記 (Teacher's Note)**
|
| 63 |
+
|
| 64 |
+
* **錄下這一刻**:強烈建議老師在博覽會時間拿手機錄下學生**充滿自信向觀眾講解自己程式原理**的畫面。這部短短 30 秒的影片,將會成為他們未來推甄或升學時最具說服力的「專案實作能力」證明,遠比任何紙上的分數都有價值。
|
| 65 |
+
* **情緒高點的散場**:這堂課請不要再講任何技術的原理與對錯,完全浸泡在「成就感、歡樂與未來希望」中結束,讓學生的學習曲線帶著最高昂的尾韻離開教室。
|
projects/2026project_class/doc/w2.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第二週:數位服務生——requests 庫與網路 API (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 理解「API (應用程式介面)」的概念與運作原理。
|
| 6 |
+
2. 認識 JSON 資料格式,並能透過 Python 字典與列表提取所需數據。
|
| 7 |
+
3. 學習使用 `requests` 庫向網路伺服器發送請求並獲取回應。
|
| 8 |
+
4. **【優化項目】強化除錯與防呆機制**:學習 HTTP 狀態碼與基本的 `try-except` 錯誤處理,避免程式因網路問題崩潰。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[15 min] 概念比喻**:API 餐廳點餐理論與網路運作基礎。
|
| 13 |
+
2. **[25 min] JSON 大解密**:認識 JSON 格式,解析字典(Dict)與列表(List)的混合體。
|
| 14 |
+
3. **[30 min] 第一手練習**:安裝 `requests` 庫,寫出第一支網路抓取程式。
|
| 15 |
+
4. **[35 min] 挑戰任務**:擷取趣味 API (冷笑話、寶可夢、找狗狗等),在終端機漂亮地印出。
|
| 16 |
+
5. **[15 min] 防呆與除錯教學**:認識 404/500 錯誤代碼,實作連線失敗時的溫柔提示。
|
| 17 |
+
|
| 18 |
+
## **課程詳解內容**
|
| 19 |
+
|
| 20 |
+
### **第一部分:概念比喻 (15 min)**
|
| 21 |
+
|
| 22 |
+
* **投影片:誰是網路世界的服務生?**
|
| 23 |
+
* **角色扮演**:你(客戶端/Python) $\rightarrow$ 服務生(API) $\rightarrow$ 廚房(伺服器)。
|
| 24 |
+
* **為什麼需要 API?**:我們不能直接衝進別人的資料庫(廚房)拿資料,必須透過規定好的菜單(API 文件)請服務生幫忙送出請求(Request)並拿回餐點(Response)。
|
| 25 |
+
* **生活實例**:手機上的天氣 APP、公車動態,背後全都是 API 在運作。
|
| 26 |
+
|
| 27 |
+
### **第二部分:JSON 大解密 (25 min)**
|
| 28 |
+
|
| 29 |
+
* **投影片:資料的通用語言——JSON**
|
| 30 |
+
* **長得像誰?**:JSON 在 Python 裡看起來就是「字典 (Dictionary)」跟「列表 (List)」的超級組合包。
|
| 31 |
+
* **互動練習:俄羅斯娃娃找資料**
|
| 32 |
+
* 給予一段簡單的 JSON 字串,例如 `{"records": [{"city": "Taipei", "weather": "Rain"}]}`。
|
| 33 |
+
* 老師帶領拆解:要拿到 "Rain",得先開 `"records"` 的箱子(得到列表),拿出第 0 個元素,再開 `"weather"` 的箱子。也就是 `data["records"][0]["weather"]`。
|
| 34 |
+
|
| 35 |
+
### **第三部分:第一手練習 (30 min)**
|
| 36 |
+
|
| 37 |
+
* **實作:我的第一支爬蟲**
|
| 38 |
+
1. **環境確認**:終端機執行 `pip install requests`。
|
| 39 |
+
2. **基礎語法**:
|
| 40 |
+
```python
|
| 41 |
+
import requests
|
| 42 |
+
url = "https://api.ipify.org?format=json" # 獲取自己IP的簡單API
|
| 43 |
+
response = requests.get(url)
|
| 44 |
+
data = response.json()
|
| 45 |
+
print("我的 IP 是:", data["ip"])
|
| 46 |
+
```
|
| 47 |
+
3. **觀察**:帶領學生理解 `requests.get()` 的運作,以及 `.json()` 方法如何把文字變成 Python 字典。
|
| 48 |
+
|
| 49 |
+
### **第四部分:挑戰任務 (35 min)**
|
| 50 |
+
|
| 51 |
+
* **題目:今日冷笑話產生器 / 天氣播報員**
|
| 52 |
+
* **基本要求**:老師提供一個免費且免金鑰的 API 網址(例如:官方冷笑話、Dog API 隨機小狗、PokeAPI 寶可夢、Agify 姓名年齡探測等)。學生需用 `requests` 抓取資料,並挑選出特定的欄位印在終端機上。
|
| 53 |
+
* **進階要求**:讓程式在 while 迴圈中,每次按下 Enter 鍵就抓取一個新的笑話。
|
| 54 |
+
|
| 55 |
+
### **第五部分:防呆與除錯教學 (15 min)**
|
| 56 |
+
|
| 57 |
+
* **投影片:如果服務生跌倒了怎麼辦?**
|
| 58 |
+
* **HTTP 狀態碼 (StatusCode)**:200 (成功)、404 (找不到網頁)、500 (伺服器當機)。
|
| 59 |
+
* **實作防呆機制**:
|
| 60 |
+
* **優化前**:沒網路時,Python 直接噴出紅字錯誤然後閃退。
|
| 61 |
+
* **優化後 (try-except)**:
|
| 62 |
+
```python
|
| 63 |
+
try:
|
| 64 |
+
response = requests.get(url, timeout=3)
|
| 65 |
+
if response.status_code == 200:
|
| 66 |
+
print(response.json())
|
| 67 |
+
else:
|
| 68 |
+
print(f"哎呀!伺服器回應錯誤碼:{response.status_code}")
|
| 69 |
+
except requests.exceptions.RequestException as e:
|
| 70 |
+
print("連線失敗!請檢查您的網路或等一下再試。")
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 74 |
+
|
| 75 |
+
### **關鍵術語表 (Glossary)**
|
| 76 |
+
|
| 77 |
+
* **API (Application Programming Interface)**:應用程式介面,軟體之間溝通的橋樑。
|
| 78 |
+
* **JSON (JavaScript Object Notation)**:輕量級的資料交換格式,易於人閱讀和編寫。
|
| 79 |
+
* **Request / Response**:請求與回應,網頁通訊的基本一來一往模式。
|
| 80 |
+
* **Exception Handling (例外處理)**:使用 `try-except` 提早攔截可能發生的錯誤,避免程式無心崩潰。
|
| 81 |
+
|
| 82 |
+
### **常見問題解答 (FAQ)**
|
| 83 |
+
|
| 84 |
+
* **Q:為什麼我的 `data["name"]` 會報錯 `KeyError`?**
|
| 85 |
+
A:這代表你跟 API 點的菜(Key)不存在它的菜單裡。請先 `print(data)` 看一下服務生到底送來了什麼格式的東西!
|
| 86 |
+
* **Q:程式卡住不動好久才報錯?**
|
| 87 |
+
A:因為預設 `requests` 會無止盡地等待回應。加個 `timeout=3` 可以讓它等 3 秒就放棄。
|
| 88 |
+
|
| 89 |
+
## **教師筆記 (Teacher's Note)**
|
| 90 |
+
|
| 91 |
+
* **重點觀察**:學生讀取 JSON 的「中���號索引」非常容易寫錯或少寫。建議在黑板上用顏色標記字典(大括號 `{}`)跟列表(中括號 `[]`)的差別。
|
| 92 |
+
* **教學心法**:故意把網址打錯一個字,讓全班看看 `404` 長什麼樣子;或是請大家把電腦的 Wi-Fi 關掉,體驗程式閃退的挫折,再教 `try-except`,他們會更有感「防呆」的重要性。
|
| 93 |
+
|
| 94 |
+
* **教學建議 (API 實作區塊)**:
|
| 95 |
+
* 透過讓學生在介面上修改變數(如輸入皮卡丘或座號),能幫助他們把「程式碼中的字串串接」與「網址運作機制」在腦袋中完美結合。
|
projects/2026project_class/doc/w3.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第三週:數據視覺化——打造個人智慧看板 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 掌握 Pygame 中的文字渲染 (Text Rendering) 技術。
|
| 6 |
+
2. 了解如何處理與載入中文字型 (`.ttf` 檔案) 以解決亂碼或方塊字問題。
|
| 7 |
+
3. 將網路 API 獲取的動態數據,與 Pygame 的視覺元素(文字、圖像、背景)進行互動綁定。
|
| 8 |
+
4. 培養 UI (使用者介面) 排版的基本美感與座標規劃能力。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[20 min] 文字變身術**:Pygame 字體加載機制與中文字體的處理。
|
| 13 |
+
2. **[30 min] 動態數據綁定**:結合上週的 API 變數,將文字動態繪製至畫布。
|
| 14 |
+
3. **[40 min] 圖像與邏輯聯動**:實作情境判斷,依據 API 狀態變更背景或圖片。
|
| 15 |
+
4. **[30 min] 挑戰任務**:自動排版挑戰——打造專屬天氣/生活看板。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:文字變身術 (20 min)**
|
| 20 |
+
|
| 21 |
+
* **投影片:如何在遊戲裡印字?**
|
| 22 |
+
* 在終端機裡,印字只需要 `print()`。但在 Pygame 裡,文字其實是一張「圖片」!
|
| 23 |
+
* **三步成圖**:建立字型物件 (Font) $\rightarrow$ 渲染成圖片表面 (Render) $\rightarrow$ 貼到畫面上 (Blit)。
|
| 24 |
+
* **實作:消滅中文字方塊**
|
| 25 |
+
* 預設字體不支援中文,印出中文會變 `[ ]`。
|
| 26 |
+
* 教師發放 `思源黑體.ttf` 或請學生下載字型檔放進專案資料夾。
|
| 27 |
+
* `font = pygame.font.Font("NotoSansTC-Regular.ttf", 36)`
|
| 28 |
+
|
| 29 |
+
### **第二部分:動態數據綁定 (30 min)**
|
| 30 |
+
|
| 31 |
+
* **投影片:讓文字活起來**
|
| 32 |
+
* 文字圖片一旦渲染就是死的,如果要讓數字跳動,必須在 `while` 迴圈中「每幀重新渲染」。
|
| 33 |
+
* **實作:結合 API 數據**
|
| 34 |
+
* 複習第 2 週的 code,將抓到的溫度存進變數 `temp`。
|
| 35 |
+
* 在主迴圈中:
|
| 36 |
+
```python
|
| 37 |
+
# 必須先擦乾淨黑板!
|
| 38 |
+
screen.fill((255, 255, 255))
|
| 39 |
+
|
| 40 |
+
# 1. 將變數變成字串,2. 渲染成圖,3. 貼到指定座標
|
| 41 |
+
text_surface = font.render(f"今日氣溫:{temp} 度", True, (0, 0, 0))
|
| 42 |
+
screen.blit(text_surface, (50, 100))
|
| 43 |
+
pygame.display.flip()
|
| 44 |
+
```
|
| 45 |
+
* **重點討論**:少寫 `screen.fill()` 會怎樣?(字會疊加變成黑色的一團墨跡)。
|
| 46 |
+
|
| 47 |
+
### **第三部分:圖像與邏輯聯動 (40 min)**
|
| 48 |
+
|
| 49 |
+
* **投影片:如果下雨,請給我雨傘**
|
| 50 |
+
* If-Else 的視覺化應用。當變數改變時,畫面上的圖片也該跟著換。
|
| 51 |
+
* **實作練習:狀態圖示切換**
|
| 52 |
+
* 準備兩三張小圖示:太陽、雨傘、雲朵 (`sun.png`, `rain.png`...)。
|
| 53 |
+
* 使用 `pygame.image.load()` 載入圖片。
|
| 54 |
+
* 加入邏輯判斷:
|
| 55 |
+
```python
|
| 56 |
+
if weather == "Rain":
|
| 57 |
+
screen.blit(rain_img, (300, 100))
|
| 58 |
+
elif weather == "Clear":
|
| 59 |
+
screen.blit(sun_img, (300, 100))
|
| 60 |
+
```
|
| 61 |
+
* 測試階段:可手動將 `weather` 變數改為不同值,測試圖片是否成功切換。
|
| 62 |
+
|
| 63 |
+
### **第四部分:挑戰任務 (30 min)**
|
| 64 |
+
|
| 65 |
+
* **題目:排版達人——個人智慧看板**
|
| 66 |
+
* **基本要求**:將 API 獲取的至少三項資訊(例如:溫度、濕度、地區 / 或是三個笑話),整齊排列在 Pygame 視窗中。
|
| 67 |
+
* **進階要求**:加入「顏色意義」。例如:氣溫高於 30 度時,溫度數字變成紅色;低於 20 度變成藍色。
|
| 68 |
+
|
| 69 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 70 |
+
|
| 71 |
+
### **關鍵術語表 (Glossary)**
|
| 72 |
+
|
| 73 |
+
* **Render (渲染)**:將抽象的資料(如字串)轉換為可見圖像的過程。
|
| 74 |
+
* **Blit (位元塊傳輸)**:將一張圖片的像素資料「貼」到另一張圖片(或螢幕表面)上的動作。
|
| 75 |
+
* **Anti-aliasing (反鋸齒)**:文字渲染時讓邊緣變得平滑的技術,在 `font.render()` 的第二個參數 (通常設為 True)。
|
| 76 |
+
|
| 77 |
+
### **常見問題解答 (FAQ)**
|
| 78 |
+
|
| 79 |
+
* **Q:為什麼我的字破圖或是跑到視窗外面?**
|
| 80 |
+
A:你的座標 (x, y) 指的是文字的「左上角」。如果要置中,可以用 `text_surface.get_rect(center=(寬/2, 高/2))` 來對齊。
|
| 81 |
+
* **Q:程式跑好慢,而且電腦風扇一直轉?**
|
| 82 |
+
A:你有沒有把 `pygame.image.load()` 或者是 `font.Font()` 寫在 While 迴圈裡面?這些是很消耗資源的動作,只需在「迴圈前」做一次就好,迴圈裡只負責畫 (blit)!
|
| 83 |
+
|
| 84 |
+
## **教師筆記 (Teacher's Note)**
|
| 85 |
+
|
| 86 |
+
* **資源準備**:強烈建議課前準備一個資源包 ZIP(內含字體檔、幾張天氣小圖示),統一發給學生,避免把時間浪費在網路上找圖與去背。
|
| 87 |
+
* **效能觀念**:學生常犯的錯就是把初始化的載入方法放進 While True 也就是每秒讀取硬碟 60 次。這是一個很好的機會教育,讓他們了解 RAM (記憶體) 與硬碟讀取速度的差異。
|
projects/2026project_class/doc/w4.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第四週:專案實作(一)——生活數據助理發表 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 整合階段一 (Week 1-3) 的知識,獨立完成一個兼具 API 數據與視覺化介面的 Pygame 專案。
|
| 6 |
+
2. **【優化項目】導入敏捷開發 (Agile) 精神**:學習 Scrum 站立會議,掌握專案進度管理。
|
| 7 |
+
3. **【優化項目】建立版本控制觀念**:學習備份重要程式碼,避免修改過程導致專案崩毀。
|
| 8 |
+
4. 體驗產品展示 (Demo),訓練將技術邏輯轉化為使用者語言的表達能力。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[10 min] 專案啟動與 Scrum 站會**:規格說明與個人目標宣告。
|
| 13 |
+
2. **[65 min] 開發衝刺 (Sprint)**:學生自主選題並實作功能。
|
| 14 |
+
3. **[15 min] 版本控制防呆與除錯互相檢驗**:備份檔案練習與同儕測試。
|
| 15 |
+
4. **[30 min] Demo 分享會**:每組/每人 2 分鐘展示看板。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:專案啟動與 Scrum 站會 (10 min)**
|
| 20 |
+
|
| 21 |
+
* **專案規格說明**:
|
| 22 |
+
* 必須要有 Pygame 視窗介面。
|
| 23 |
+
* 必須至少串接一個網路 API(如:星座、匯率、公車站牌、地震資訊)。
|
| 24 |
+
* 遇到沒有網路或 API 失效時,不能直接當機(需包含 Try-Except)。
|
| 25 |
+
* **【優化實踐】微型 Scrum 站立會議**:
|
| 26 |
+
* 全班起立。
|
| 27 |
+
* 每個人用 15 秒鐘大聲說:「我今天要做的看板主題是 ______,我目前最大的挑戰可能會是 ______。」
|
| 28 |
+
* 幫助學生集中精神,確立今天 2 小時內的最高目標。
|
| 29 |
+
|
| 30 |
+
### **第二部分:開發衝刺 Sprint (65 min)**
|
| 31 |
+
|
| 32 |
+
* **自主實作階段**:
|
| 33 |
+
* **第一步**:從老師提供的「API 清單」中挑選一組,寫一段測試程式確定能抓到資料。
|
| 34 |
+
* **第二步**:建立 Pygame 視窗,將資料轉化為畫面上的文字。
|
| 35 |
+
* **第三步**:加入美化元素(背景圖、顏色判斷邏輯、排版)。
|
| 36 |
+
* **老師的角色**:
|
| 37 |
+
* 不主動幫學生寫 Code。當學生報錯時,引導他們看終端機的最後一行紅字(Traceback),並問:「你知道這行紅字是什麼意思嗎?」
|
| 38 |
+
|
| 39 |
+
### **第三部分:版本控制與除錯 (15 min)**
|
| 40 |
+
|
| 41 |
+
* **【優化實踐】拯救崩潰的專案:版本備份**
|
| 42 |
+
* **情境**:「老師,我剛剛加了個新功能,結果本來會動的程式現在整個壞掉了,怎麼辦?」
|
| 43 |
+
* **教學**:在程式從「會動」要邁向「加新功能」前,教導學生將 `app.py` 複製並改名為 `app_v1_stable.py`。
|
| 44 |
+
* **觀念建立**:軟體工程師永遠不會只在一個檔案上改到死。「備份」就是給自己的後悔藥。(為日後高中學習 Git 鋪路)。
|
| 45 |
+
* **QA 除錯互測**:
|
| 46 |
+
* 請右邊的同學來操作你的程式。
|
| 47 |
+
* 測試如果「斷網」(關閉 Wi-Fi),你的程式會閃退,還是溫馨提示?
|
| 48 |
+
|
| 49 |
+
### **第四部分:Demo 分享會 (30 min)**
|
| 50 |
+
|
| 51 |
+
* **成果展現**:
|
| 52 |
+
* 將畫面投影到大螢幕。
|
| 53 |
+
* **演講架構(強制規定)**:
|
| 54 |
+
1. 「我的看板是做什麼用的?為誰設計的?」
|
| 55 |
+
2. 「我抓了哪裡的 API 資料?」
|
| 56 |
+
3. 「我在開發中遇到最大的 Bug 是什麼?我怎麼修好它的?」
|
| 57 |
+
* **回饋環節**:老師與台下同學給予掌聲與一項具體的正向回饋(例如:你的排版顏色配得很好看)。
|
| 58 |
+
|
| 59 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 60 |
+
|
| 61 |
+
### **關鍵術語表 (Glossary)**
|
| 62 |
+
|
| 63 |
+
* **Scrum**:一種敏捷軟體開發的架構,強調短週期、快速迭代與團隊溝通。
|
| 64 |
+
* **備份 / Versioning (版本號)**:軟體開發中將穩定版本單獨儲存的行為,如 v1.0, v1.1。
|
| 65 |
+
* **MVP (Minimum Viable Product)**:最小可行性產品。先把核心功能(抓資料印出來)做好,再去想華麗的額外功能(動畫、音效)。
|
| 66 |
+
|
| 67 |
+
### **常見問題解答 (FAQ)**
|
| 68 |
+
|
| 69 |
+
* **Q:我的 API 突然抓不到東西了,是不是我程式壞了?**
|
| 70 |
+
A:免費 API 常常有「呼叫次數限制 (Rate Limit)」。如果你把它放在 while 迴圈裡每秒抓 60 次,你的 IP 可能被伺服器暫時封鎖了!記住,網路請求應該要「定時」或是「按鍵觸發」,而不是每幀狂抓。
|
| 71 |
+
|
| 72 |
+
## **教師筆記 (Teacher's Note)**
|
| 73 |
+
|
| 74 |
+
* **鷹架策略**:對於程式能力較弱的學生,老師應備好一份「半完成」的樣板檔案(已有 Pygame 架構和一個天氣 API 框架),讓他們只要填寫少數邏輯就能產出結果,建立自信心。
|
| 75 |
+
* **慶祝里程碑**:這是課程第一次的成果發表。完成後可請學生截圖自己的畫面,建立學習歷程檔案。這是一個重要的情緒高點。
|
projects/2026project_class/doc/w5.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第五週:哈囉鄰居!Socket 通訊與 IP 基礎 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 現實對照:理解 IP 地址(電腦的位置)與 Port 端口(程式的門牌)的運作關聯。
|
| 6 |
+
2. 認識 UDP 通訊協定的特性,並了解為什麼遊戲偏好使用 UDP。
|
| 7 |
+
3. 實作第一隻 Socket 網路傳輸程式,讓兩台不同電腦能互相發送訊息。
|
| 8 |
+
4. **【優化項目】防呆機制擴展**:處理網路連線的潛在錯誤與例外狀況。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[20 min] 網路世界的地址**:IP、Port 與 Socket 的生活比喻。
|
| 13 |
+
2. **[25 min] TCP vs UDP**:安全點名與廣播喇叭的差異。
|
| 14 |
+
3. **[35 min] 第一手練習**:發送包裹 (Send) 與接收包裹 (Receive)。
|
| 15 |
+
4. **[25 min] 挑戰任務**:班級匿名廣播電台(學生互相傳訊)。
|
| 16 |
+
5. **[15 min] 網路除錯教學**:ipconfig / ifconfig 的使用,與連線失敗的防呆。
|
| 17 |
+
|
| 18 |
+
## **課程詳解內容**
|
| 19 |
+
|
| 20 |
+
### **第一部分:網路世界的地址 (20 min)**
|
| 21 |
+
|
| 22 |
+
* **投影片:一封信是怎麼寄到的?**
|
| 23 |
+
* **IP Address (伺服器位址)**:就像「台北市信義區XX路XX號」。指出這台機器在網路上的位置(如 `192.168.1.100`)。
|
| 24 |
+
* **Port (端口號)**:打開大門後,指定的「收發室分機」。一台電腦同時有瀏覽器(Port 80)、遊戲(Port 25565)、你的程式(指定 Port 5000)。
|
| 25 |
+
* **Socket (通訊端點)**:IP + Port,一根專屬的連線水管。
|
| 26 |
+
|
| 27 |
+
### **第二部分:TCP vs UDP (25 min)**
|
| 28 |
+
|
| 29 |
+
* **投影片:遊戲為什麼要用 UDP?**
|
| 30 |
+
* **TCP (掛號信)**:送出 $\rightarrow$ 確認收到 $\rightarrow$ 再送下一個。絕對安全,但很慢(會卡頓)。適用於:傳檔案、載入網頁。
|
| 31 |
+
* **UDP (丟傳單)**:我一直瘋狂丟!你沒接到就算了!速度極快。適用於:射擊遊戲、視訊對話、FPS座標同步。
|
| 32 |
+
* **互動問答**:「在傳送玩家 x, y 座標的時候,如果不小心漏失了第 50 幀的資料,我們需要停下來重傳嗎?」 $\rightarrow$ 不用,因為第 51 幀的資料馬上就來了!
|
| 33 |
+
|
| 34 |
+
### **第三部分:第一手練習 (35 min)**
|
| 35 |
+
|
| 36 |
+
* **實作:用 Python 打造無線電**
|
| 37 |
+
1. 匯入內建模組:`import socket`。
|
| 38 |
+
2. **建立 UDP Socket**:
|
| 39 |
+
```python
|
| 40 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 41 |
+
```
|
| 42 |
+
3. **編碼與發送 (encode)**:網路水管只能傳輸位元組 `bytes`。
|
| 43 |
+
```python
|
| 44 |
+
message = "Hello 前方同學!".encode('utf-8')
|
| 45 |
+
target_ip = "192.168.X.X" # 修改為同學的 IP
|
| 46 |
+
sock.sendto(message, (target_ip, 5000))
|
| 47 |
+
```
|
| 48 |
+
4. **建立接收端 (bind)**:另一台電腦必須監聽該 Port。
|
| 49 |
+
```python
|
| 50 |
+
sock.bind(("0.0.0.0", 5000))
|
| 51 |
+
data, addr = sock.recvfrom(1024)
|
| 52 |
+
print(f"收到來自 {addr} 的訊息:", data.decode('utf-8'))
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### **第四部分:挑戰任務 (25 min)**
|
| 56 |
+
|
| 57 |
+
* **題目:班級廣播電台 / 座位傳訊機**
|
| 58 |
+
* 雙人一組,A 同學寫發送端程式,B 同學寫接收端程式。
|
| 59 |
+
* 成功後互相角色互換。
|
| 60 |
+
* 結合 Python `input()`,讓使用者可以在終端機打字,按下 Enter 傳給指定 IP 的同學。
|
| 61 |
+
|
| 62 |
+
### **第五部分:網路除錯與防呆 (15 min)**
|
| 63 |
+
|
| 64 |
+
* **實用技能:找自己的 IP**
|
| 65 |
+
* Windows:打開 cmd 輸入 `ipconfig`,尋找 IPv4 位址。
|
| 66 |
+
* Mac:打開終端機輸入 `ifconfig` 或查系統設定。
|
| 67 |
+
* **【優化實踐】網路錯誤處理**:
|
| 68 |
+
* 當我們試圖傳給一個不存在的 IP,或是網路根本斷線時,加上 `try-except` 防止閃退:
|
| 69 |
+
```python
|
| 70 |
+
try:
|
| 71 |
+
sock.sendto(msg, (ip, port))
|
| 72 |
+
except OSError as e:
|
| 73 |
+
print("網路傳輸失敗,請檢查 Wi-Fi 是否連線或 IP 格式是否正確!")
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 77 |
+
|
| 78 |
+
### **關鍵術語表 (Glossary)**
|
| 79 |
+
|
| 80 |
+
* **Localhost (127.0.0.1)**:本機地址。自己傳給自己測試時使用的安全 IP。
|
| 81 |
+
* **Socket**:插座/通訊端點,是 OS 提供給程式設計師連接網路的 API 工具。
|
| 82 |
+
* **Encode / Decode**:字串與位元組 (bytes) 之間的轉換,網路傳輸一律需要 encode 成二進位。
|
| 83 |
+
|
| 84 |
+
### **常見問題解答 (FAQ)**
|
| 85 |
+
|
| 86 |
+
* **Q:為什麼我的程式一直卡在 `recvfrom()` 不會動?**
|
| 87 |
+
A:因為接收是「阻塞的 (Blocking)」。它在等信箱掉進信件前,會讓整個程式停下來發呆。我們下週會教如何把它變成「不等待(Non-blocking)」!
|
| 88 |
+
* **Q:IP 打對了,但對方完全收不到?**
|
| 89 |
+
A:通常是被 Windows Defender 防火牆或防毒軟體擋住了。若跳出「是否允許存取網路」請務必選「允許」。如果還是不行,可能是學校區網禁止設備互連 (AP Isolation)。
|
| 90 |
+
|
| 91 |
+
## **教師筆記 (Teacher's Note)**
|
| 92 |
+
|
| 93 |
+
* **開課前必要確認!!!**:強烈建議課外時間親自使用教室的網路環境測試 UDP 連線。許多學校的無線網路為了安全,會開啟 "Client Isolation" 阻擋電腦互連。若現場遇到此狀況,請立即切換至備案:讓學生用 `127.0.0.1` 開兩個 VSCode 視窗假裝是兩台電腦互傳。
|
| 94 |
+
* **分組策略**:網路連線很容易有「挫折感」,因為一個人錯,兩個人的程式都不會動。建議老師把配對 IP 的清單寫在黑板上,明確規定誰跟誰一組。
|
projects/2026project_class/doc/w6.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第六週:數據的包裹——JSON 序列化與同步 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 了解如何將複雜狀態(如座標、分數、玩家名稱)打包為 `JSON` 格式的短字串。
|
| 6 |
+
2. 理解遊戲主迴圈中的「阻塞 (Blocking)」與「非阻塞 (Non-blocking)」概念。
|
| 7 |
+
3. 實作「即時座標同步」,讓自己的操作能在另一台電腦上顯示。
|
| 8 |
+
4. **【核心難點克服】**:學習處理網路延遲造成的丟包異常,透過 `try-except (BlockingIOError, ConnectionResetError)` 讓程式保持順暢。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[20 min] 資料的任意門**:打包寄送 (dumps) 與拆箱讀取 (loads)。
|
| 13 |
+
2. **[30 min] 處理空白時光**:Blocking vs Non-blocking 的觀念解析。
|
| 14 |
+
3. **[40 min] 即時座標同步實作**:將玩家方塊 `(x, y)` 化為 JSON 封包互傳。
|
| 15 |
+
4. **[30 min] 挑戰任務**:影子雙子星——A動B動的跟隨遊戲。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:資料的任意門 (20 min)**
|
| 20 |
+
|
| 21 |
+
* **投影片:遊戲伺服器怎麼知道你在哪裡?**
|
| 22 |
+
* **序列化 (Serialization)**:也就是打包行李。
|
| 23 |
+
* **反序列化 (Deserialization)**:拆開行李並把衣服拿出來。
|
| 24 |
+
* 我們不能直接把 Python 的 `[100, 200]` 用水管傳出去。必須把它變成通用語言 (JSON String) $\rightarrow$ 編碼 (Bytes) $\rightarrow$ 網路層 $\rightarrow$ 解碼 (String) $\rightarrow$ 解析 (Python 列表)。
|
| 25 |
+
* **短暫練習**:
|
| 26 |
+
```python
|
| 27 |
+
import json
|
| 28 |
+
package = {"id": "player1", "x": 100, "y": 200, "hp": 5}
|
| 29 |
+
string = json.dumps(package) # 打包成字串
|
| 30 |
+
original = json.loads(string) # 拆開來
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### **第二部分:處理空白時光 (30 min)**
|
| 34 |
+
|
| 35 |
+
* **核心問題探討**:
|
| 36 |
+
* 上週的程式碼會卡在 `recvfrom()`,直到收到信為止。但是遊戲每一秒要跑 60 次,如果這 1/60 秒我沒有收到信,畫面就不畫了嗎?
|
| 37 |
+
* **解決方案:非阻塞模式 (Non-blocking)**:
|
| 38 |
+
```python
|
| 39 |
+
sock.setblocking(False) # 告訴水管:不用等!沒信就跟我說沒信,不要讓我傻站著。
|
| 40 |
+
```
|
| 41 |
+
* **防呆機制**:
|
| 42 |
+
* 當沒有信件時,會跳出 `BlockingIOError` (Windows 下可能是 `ConnectionResetError`)。我們必須**優雅地忽略它**。
|
| 43 |
+
```python
|
| 44 |
+
try:
|
| 45 |
+
data, addr = sock.recvfrom(1024)
|
| 46 |
+
# 處理收到資料的邏輯
|
| 47 |
+
except (BlockingIOError, ConnectionResetError):
|
| 48 |
+
pass # 沒信也無所謂,裝沒事!
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### **第三部分:即時座標同步實作 (40 min)**
|
| 52 |
+
|
| 53 |
+
* **步驟拆解與編程**:
|
| 54 |
+
1. 初始化玩家 `x`, `y` 為 100, 100。對手 `ax`, `ay` 為 200, 200。
|
| 55 |
+
2. 加入按鍵偵測,控制自己 `(x, y)` 移動。
|
| 56 |
+
3. **送出資料**:每次 `while` 迴圈結束前,將 `{'x': x, 'y': y}` 的字串送給對方。
|
| 57 |
+
4. **接收資料**:每次迴圈開頭,用 `try-except` 接收對手的封包。如果收到,更新對手 `(ax, ay)` 的座標。
|
| 58 |
+
5. 畫出兩個不同顏色的方塊。
|
| 59 |
+
|
| 60 |
+
### **第四部分:挑戰任務 (30 min)**
|
| 61 |
+
|
| 62 |
+
* **題目:影子跟隨賽**
|
| 63 |
+
* 在同一個畫面中,藍色是自己,紅色是對手。
|
| 64 |
+
* 當自己按空白鍵,螢幕背景隨機變一個顏色;並且,對手螢幕的背景也要**同時**變成該顏色。
|
| 65 |
+
* **考驗**:你要怎麼把「顏色」的資訊也寫進打包好的 JSON 裡面?(e.g., `{'x': 100, 'y': 200, 'color': 'red'}`)。
|
| 66 |
+
|
| 67 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 68 |
+
|
| 69 |
+
### **關鍵術語表 (Glossary)**
|
| 70 |
+
|
| 71 |
+
* **Blocking / Non-blocking**:程式語言中是否無窮盡地等待某個 I/O (讀寫) 動作的屬性設定。
|
| 72 |
+
* **Serialization (序列化)**:將記憶體中的複雜資料結構,轉換為可以儲存成檔案或在網路傳輸的位元組序列的過程。
|
| 73 |
+
|
| 74 |
+
### **常見問題解答 (FAQ)**
|
| 75 |
+
|
| 76 |
+
* **Q:為什麼我的對手走起來一卡一卡(Lag)的?**
|
| 77 |
+
A:可能是一次發出去的資料包太多擠在緩衝區,或者網路的延遲。在高級遊戲開發中,通常會加入「平滑插值 (Interpolation)」算法來讓動作看起來更滑順。
|
| 78 |
+
* **Q:我加了 `setblocking(False)` 結果還是閃退了?**
|
| 79 |
+
A:如果用錯例外名稱(如寫成一般的 `Exception` 或者沒寫 `pass`),你的程式還是會崩潰。這是極度精密的一行防護網。
|
| 80 |
+
|
| 81 |
+
## **教師筆記 (Teacher's Note)**
|
| 82 |
+
|
| 83 |
+
* **網路除錯**:同學常在這裡徹底搞混自己是發送方還是接收方。建議兩兩互助,讓 A 專心寫發送、B 專心寫接收。如果是雙向互傳,一定要將兩個 Python 檔案的送出 IP 正確對應,因為如果寫錯,很可能變成不小心傳給隔壁組的慘劇。
|
| 84 |
+
* **物理具象化**:在講解「阻塞/非阻塞」時,可以請一個同學當郵差,如果在門口等主人(程式)開門拿信,郵差後面的工作都會停擺;如果主人設了一個信箱(非阻塞),郵差丟了就走,主人才不管有信沒信都繼續看電視。
|
projects/2026project_class/doc/w7.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第七週:對戰邏輯——誰打中了誰? (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 了解網路遊戲中的「客戶端」與「伺服器端」的信任機制與狀態同步。
|
| 6 |
+
2. 實作事件觸發的 JSON 封包傳輸(例如:按下空白鍵發射子彈)。
|
| 7 |
+
3. 使用 `pygame.Rect.colliderect` 實現在分散式系統中的碰撞判定與分數增減。
|
| 8 |
+
4. 培養「各自判定、更新結果」的基本連線對戰思維。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[20 min] 邏輯的衝突**:當兩邊同時開槍,誰先被爆頭?從 P2P 探討信任機制。
|
| 13 |
+
2. **[30 min] 發射!不只是座標**:增加 "action" 事件型的網路傳遞。
|
| 14 |
+
3. **[40 min] 碰撞判定**:用 Rects 來判斷子彈是否擊中對方方塊。
|
| 15 |
+
4. **[30 min] 挑戰任務**:加入「重生成為新獵人」與血條系統。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:邏輯的衝突 (20 min)**
|
| 20 |
+
|
| 21 |
+
* **觀點問題**:「如果我的螢幕上顯示我打中你了,但你的螢幕上顯示你閃過了,到底聽誰的?」
|
| 22 |
+
* **兩種網路遊戲架構科普**:
|
| 23 |
+
* **主從架構 (Client-Server)**:有一個權威伺服器(上帝)說了算。CS:GO, 英雄聯盟。防外掛能力強,但建置困難。
|
| 24 |
+
* **對等網路 (P2P)**:我們互相溝通,自己判斷自己有沒有受傷。我們這週要實作的版本。如果你覺得你被打中,你就扣血並通知我。
|
| 25 |
+
|
| 26 |
+
### **第二部分:發射!不只是座標 (30 min)**
|
| 27 |
+
|
| 28 |
+
* **核心改變:打包不同類型的包裹**
|
| 29 |
+
* 原本每幀狂送的封包是持續性資料:`{'type': 'pos', 'x': 100, 'y': 200}`
|
| 30 |
+
* 按下發射時,立刻送出單發事件封包:`{'type': 'shoot', 'x': 100, 'y': 200, 'dir': 'up'}`
|
| 31 |
+
* **實作技巧**:
|
| 32 |
+
* 在接收端,讀出 `data['type']` 後,用 `if-elif` 進行不同的處理。如果是 `'pos'` 就覆蓋對方影像座標;如果是 `'shoot'` 就產生一顆對方的子彈畫在螢幕上。
|
| 33 |
+
|
| 34 |
+
### **第三部分:碰撞判定 (40 min)**
|
| 35 |
+
|
| 36 |
+
* **投影片:隱形的碰撞框 (Rect)**
|
| 37 |
+
* **程式碼實戰:扣血與回報**
|
| 38 |
+
* 使用 pygame 強大的碰撞函數:
|
| 39 |
+
```python
|
| 40 |
+
my_rect = pygame.Rect(x, y, 50, 50)
|
| 41 |
+
bullet_rect = pygame.Rect(bx, by, 10, 10)
|
| 42 |
+
|
| 43 |
+
# 【P2P 邏輯】:只有我能判斷我自己的身體有沒有被 對方 的子彈打到
|
| 44 |
+
if my_rect.colliderect(bullet_rect):
|
| 45 |
+
hp -= 1
|
| 46 |
+
print("我被打中了!目前血量:", hp)
|
| 47 |
+
# 通知對方:我少了一滴血
|
| 48 |
+
sock.sendto(json.dumps({'type': 'hit', 'hp': hp}).encode(), (ip, port))
|
| 49 |
+
```
|
| 50 |
+
* 同步在畫面上畫出 `hp` 的數值字串。
|
| 51 |
+
|
| 52 |
+
### **第四部分:挑戰任務 (30 min)**
|
| 53 |
+
|
| 54 |
+
* **題目:網路復仇戰**
|
| 55 |
+
* **基本要求**:血量 (hp) 歸零時,自己的角色會瞬間移動到螢幕中央,血量恢復到 10。同時通知對方自己的血量。
|
| 56 |
+
* **進階要求**:在畫面的最上方用兩個變數 (`my_score`, `enemy_score`) 記錄雙方的得分擊殺數。
|
| 57 |
+
|
| 58 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 59 |
+
|
| 60 |
+
### **關鍵術語表 (Glossary)**
|
| 61 |
+
|
| 62 |
+
* **Client / Server 架構**:多數網路遊戲採取的設計模式,Server 是大腦與驗證中心,Client 只是螢幕顯示器與遙控器搖桿。
|
| 63 |
+
* **Hitbox 碰撞箱**:包裹在角色外圍的透明隱形框框,用於純邏輯的重疊計算,可以大於或小於真實精美的角色圖片。
|
| 64 |
+
|
| 65 |
+
### **常見問題解答 (FAQ)**
|
| 66 |
+
|
| 67 |
+
* **Q:我的子彈飛一飛突然消失了?**
|
| 68 |
+
A:你的子彈可能在每一幀都被重新定義,或是儲存子彈的串列 (`bullet_list`) 在重新接收封包時被清空了。將子彈設定為持續移動的實體物件!
|
| 69 |
+
* **Q:對手送過來的血量 `hp` 是字串還是數字?**
|
| 70 |
+
A:JSON 原生支援數字(如果在 JSON 裡面沒有加雙引號)。所以 `data['hp'] - 1` 是可以直接計算的。不要跟 `pygame` 的字串搞混。
|
| 71 |
+
|
| 72 |
+
## **教師筆記 (Teacher's Note)**
|
| 73 |
+
|
| 74 |
+
* **除錯難度極高**:這節課是全期課程最痛的一節課,因為每個 Bug 都可能是「自己沒送出」、「自己送錯格式」、「對方沒收到」或「對方收到沒更新畫面」等組合問題。
|
| 75 |
+
* **最佳實踐**:強烈建議提供一個「可運作的網路子彈模板」,讓學生專注在修改 `colliderect` 這部分的邏輯,而不是在那邊對水管丟封包浪費了 40 分鐘。在程式架構中先定義一個乾淨的 `update_network_events()` 函式將網路與遊戲繪圖剝離。
|
projects/2026project_class/doc/w8.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第八週:專案實作(二)——班級網路競技場(2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 將第六、七週的散碎技巧整合為一款完整的雙人連線遊戲雛形。
|
| 6 |
+
2. **【優化項目】PBL 敏捷開發 Scrum 站會**:運用站立會議追蹤衝刺進度,明確訂立最小達標規格 (MVP)。
|
| 7 |
+
3. 在真實多設備連網環境下進行「壓力測試」,學習找尋網路異常的 Bug。
|
| 8 |
+
4. **【優化項目】課前硬體建置**:在課堂的最後 15 分鐘,提早全班完成 Python `OpenCV` 及 `MediaPipe` 環境安裝,並拍攝/儲存確認攝影機能否抓取到畫面。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[10 min] 專案啟動與 Scrum 站會**:團隊分工與目標宣告。
|
| 13 |
+
2. **[50 min] 遊戲打磨與除錯衝刺**:完善計分系統、音效與畫面。
|
| 14 |
+
3. **[45 min] 班級競技互聯**:小組對調試玩,測試雙方的「延遲感」與防呆。
|
| 15 |
+
4. **[15 min] 面對未來:AI 環境課前測試**:全班統一安裝並試跑 cv2 鏡頭畫面。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:專案啟動與 Scrum 站會 (10 min)**
|
| 20 |
+
|
| 21 |
+
* **MVP 規格**:
|
| 22 |
+
* 按下空白鍵時雙方能互射。
|
| 23 |
+
* 血量減少,一方死掉後會有簡單的新回合刷新機制或勝負介面。
|
| 24 |
+
* 不會因為沒收到網路封包而卡頓。
|
| 25 |
+
* **Scrum 站會**:
|
| 26 |
+
* 以兩人一組為主體,站立回報:「我們目前的對戰遊戲還缺什麼?今天誰要寫血量,誰要寫網路傳遞?」
|
| 27 |
+
* 督促他們互相溝通傳送的資料格式(例如約定包裹長這樣:`{"t": "mv", "x": 10, "y": 20}` 越短越快)。
|
| 28 |
+
|
| 29 |
+
### **第二部分:遊戲打磨與除錯衝刺 (50 min)**
|
| 30 |
+
|
| 31 |
+
* **重點實作**:
|
| 32 |
+
1. **背景佈置**:透過 Pygame 載入並縮放背景(太空、荒野等)。
|
| 33 |
+
2. **加入音效**:使用 `pygame.mixer.Sound` 加入開槍「砰」、受傷「啊」的聲音。(音效放在本機端播放,不透過網路傳輸音檔!)
|
| 34 |
+
3. **結算畫面**:當某方血量歸零時,`screen.fill()` 畫上全螢幕的黑色與大字「YOU DIED」。
|
| 35 |
+
* **老師輔導重點**:專注巡視是否發生「Blocking」導致程式死當。一旦發現,引導加上 `try-except` 或 `setblocking(False)`。
|
| 36 |
+
|
| 37 |
+
### **第三部分:班級競技大亂鬥 (45 min)**
|
| 38 |
+
|
| 39 |
+
* **體驗與互測 (QA Testing)**:
|
| 40 |
+
* 「這款網路遊戲的開發已經完成!現在開放全場自由挑戰!」
|
| 41 |
+
* 鼓勵 A 組去連線挑戰 B 組的 IP。這考驗他們是否能快速在程式碼中修改 Target IP 並重新啟動(若程式包裝得好,一開場就能輸入欲連線的 IP)。
|
| 42 |
+
* 觀察與記錄:誰的連線特別順?誰的對戰會「瞬移」或「子彈消失」?原因分析討論。
|
| 43 |
+
|
| 44 |
+
### **第四部分:【優化項目】AI 環境課前測試 (15 min)**
|
| 45 |
+
|
| 46 |
+
* **為下週(Week 9)的神奇魔術做準備**:
|
| 47 |
+
* **安裝三項神器**:打開命令提示字元,輸入:
|
| 48 |
+
`pip install opencv-python mediapipe numpy`
|
| 49 |
+
* **測試腳本**:老師提供一份 10 行腳本,只做一件事:用 `cv2.VideoCapture(0)` 讀取畫面並顯示。
|
| 50 |
+
* **環境防呆盤點**:筆電鏡頭沒反應、權限被拒絕、桌機沒鏡頭。
|
| 51 |
+
* **硬體備案說明**:如果硬體有問題,請教師將「手部揮動預錄影片 (.mp4)」拷貝給學生備用,下週直接將其取代 `(0)` 即可繼續上課。
|
| 52 |
+
|
| 53 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 54 |
+
|
| 55 |
+
### **關鍵術語表 (Glossary)**
|
| 56 |
+
|
| 57 |
+
* **Stress Test (壓力測試)**:在極端或多人同時使用的情境下,測試系統會不會崩潰的過程。
|
| 58 |
+
* **Quality Assurance (QA)**:確保軟體上線沒有大問題的品管階段,互相找 Bug 的樂趣所在。
|
| 59 |
+
|
| 60 |
+
### **常見問題解答 (FAQ)**
|
| 61 |
+
|
| 62 |
+
* **Q:老師,我們不同組寫的遊戲可以互連嗎?**
|
| 63 |
+
A:如果兩組對封包的「約定格式」完全一樣,就可以!但只要有一組是用 `{"bullet_x": 10}`,另一組是用 `{"bx": 10}`,連線就會立刻引發 `KeyError` 崩潰崩潰。這就是為什麼制定共同的 API 標準這麼重要!
|
| 64 |
+
|
| 65 |
+
## **教師筆記 (Teacher's Note)**
|
| 66 |
+
|
| 67 |
+
* **心態指引**:網路對戰幾乎沒有一次能做完美的。學生很有可能會對 Bug 感到沮喪。此時提醒他們:「你們現在碰到的問題,包含英雄聯盟和暴雪等大公司也都每天在面對。這就是『分散式系統』的浪漫。」
|
| 68 |
+
* **提早測試鏡頭非常重要**:這能把第 9 週當「維修工人」的時間轉移到這堂課最後。若沒做到這點,下週會是一場因為硬體阻礙導致教學進度全線崩潰的災難。
|
projects/2026project_class/doc/w9.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **第九週:數位眼睛——MediaPipe 視覺辨識入門 (2小時完整教案)**
|
| 2 |
+
|
| 3 |
+
## **課程目標**
|
| 4 |
+
|
| 5 |
+
1. 了解電腦視覺 (Computer Vision) 與機器學習模型將影像轉換成數學矩陣的概念。
|
| 6 |
+
2. 學習讀取攝影機或影片來源 (OpenCV),並透過 MediaPipe 套件取得手部關鍵點資訊。
|
| 7 |
+
3. 將抽象的 3D 手勢座標點 (Landmarks 0~20) 轉換並映射至 Pygame 的 2D 畫布。
|
| 8 |
+
4. **【優化項目】環境備案整合**:確保每一位學生在無硬體鏡頭的情況下也能流暢跟隨教學。
|
| 9 |
+
|
| 10 |
+
## **課程時間分配 (總計 120 分鐘)**
|
| 11 |
+
|
| 12 |
+
1. **[15 min] 從矩陣到眼睛**:電腦怎麼看得見畫面的?
|
| 13 |
+
2. **[20 min] OpenCV 第一步**:抓取影像影格 (Frame) 並顯示(或載入備用影片防呆)。
|
| 14 |
+
3. **[45 min] MediaPipe 的魔幻洗禮**:尋找在 0.01 秒內捕捉手骨架的秘密,提取並列印食指座標。
|
| 15 |
+
4. **[40 min] 挑戰任務:空中雷射筆**:將 MediaPipe 座標連動回 Pygame 的視窗繪製點位。
|
| 16 |
+
|
| 17 |
+
## **課程詳解內容**
|
| 18 |
+
|
| 19 |
+
### **第一部分:從矩陣到眼睛 (15 min)**
|
| 20 |
+
|
| 21 |
+
* **投影片:一秒 30 張圖的魔法**
|
| 22 |
+
* **電腦不看臉,只看數字**:說明影像本質就是高度 $x$ 寬度 $x$ 3(RGB 色彩通道) 的立體數字陣列 (Numpy Array)。
|
| 23 |
+
* **強大的預錄模型 (Pre-trained Model)**:介紹 Google 如何餵給電腦數十萬張不同膚色、角度的手的圖片,讓這包名為 MediaPipe 的套件自動找出 21 個固定的骨節點。
|
| 24 |
+
|
| 25 |
+
### **第二部分:OpenCV 第一步 (20 min)**
|
| 26 |
+
|
| 27 |
+
* **實戰:抓取影像流**
|
| 28 |
+
* 上週已有安裝測試基礎,這次進行正式讀取並加上反轉 (鏡面)。
|
| 29 |
+
```python
|
| 30 |
+
import cv2
|
| 31 |
+
|
| 32 |
+
# 【優化防呆】預錄影片方案:cap = cv2.VideoCapture("test_hand.mp4")
|
| 33 |
+
cap = cv2.VideoCapture(0)
|
| 34 |
+
|
| 35 |
+
while True:
|
| 36 |
+
success, img = cap.read()
|
| 37 |
+
if not success:
|
| 38 |
+
break # 或者在讀影片完畢時重設到開頭
|
| 39 |
+
|
| 40 |
+
img = cv2.flip(img, 1) # 變成鏡面,直覺才對
|
| 41 |
+
cv2.imshow("Hand Tracking", img)
|
| 42 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 43 |
+
break
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### **第三部分:MediaPipe 的魔幻洗禮 (45 min)**
|
| 47 |
+
|
| 48 |
+
* **引入魔法模組**:
|
| 49 |
+
```python
|
| 50 |
+
import mediapipe as mp
|
| 51 |
+
mp_hands = mp.solutions.hands
|
| 52 |
+
hands = mp_hands.Hands(max_num_hands=1)
|
| 53 |
+
```
|
| 54 |
+
* **運算與提取:解析 Landmarks**
|
| 55 |
+
* 在迴圈中將 BGR 圖片轉換為 RGB 傳給 `hands.process(img_rgb)`。
|
| 56 |
+
* 教學重點:拿出一張寫著「0~20點」的手勢解剖圖圖示,告訴學生食指尖 (Index Finger Tip) 是第 8 點。
|
| 57 |
+
* **提取資料**:
|
| 58 |
+
```python
|
| 59 |
+
if results.multi_hand_landmarks:
|
| 60 |
+
for hand_lms in results.multi_hand_landmarks:
|
| 61 |
+
lm8 = hand_lms.landmark[8]
|
| 62 |
+
# 痛點:MediaPipe 回傳的只是比例 (0~1),不是像素!
|
| 63 |
+
h, w, c = img.shape
|
| 64 |
+
cx, cy = int(lm8.x * w), int(lm8.y * h)
|
| 65 |
+
print(f"食指座標找到了:({cx}, {cy})")
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### **第四部分:挑戰任務 - 空中雷射筆 (40 min)**
|
| 69 |
+
|
| 70 |
+
* **題目:把 AI 裝進遊戲引擎裡**
|
| 71 |
+
* 跳脫純粹的黑底 OpenCV 視窗。我們要把 `cx`, `cy` 變成剛才教過了那麼多週的 Pygame 移動變數 `x, y`!
|
| 72 |
+
* **架構挑戰**:讓 Pygame 負責 `screen.fill()` 跟畫圈圈,而 OpenCV 的 `cap.read()` 放進 Pygame 的主迴圈裡。
|
| 73 |
+
* 當你的手指在鏡頭前移動時,螢幕上有個 Pygame 畫出的紅色圖形要像是「雷射筆」一樣黏在你的食指上。
|
| 74 |
+
|
| 75 |
+
## **知識擴充 (NotebookLM 推薦素材)**
|
| 76 |
+
|
| 77 |
+
### **關鍵術語表 (Glossary)**
|
| 78 |
+
|
| 79 |
+
* **Computer Vision (CV)**:使電腦能從數位影像或影片的高維度資料中獲取高層級理解的科學。
|
| 80 |
+
* **Landmarks (地標 / 關鍵點)**:在臉部或人體骨架辨識中,標記特定特徵(如鼻尖、指尖、手腕)的數字座標。
|
| 81 |
+
* **BGR vs RGB**:OpenCV 歷史遺留的問題,預設讀出來的藍紅通道是相反的,丟給強迫症發作的 AI 之前一定要做 色彩空間轉換 (`cv2.cvtColor`)。
|
| 82 |
+
|
| 83 |
+
### **常見問題解答 (FAQ)**
|
| 84 |
+
|
| 85 |
+
* **Q:老師,我的影像讀出來完全是灰的或綠色的?**
|
| 86 |
+
A:這代表你很有可能是忘了做 BGR 轉 RGB 的動作,導致 MediaPipe 這個「外國人」看嘸你傳來的心情不好的藍綠色手!
|
| 87 |
+
* **Q:畫面跑得超級無敵慢,連三秒鐘前揮的手現在才出來?**
|
| 88 |
+
A:你的電腦算力不足。請幫忙加上 `cv2.resize()` 把輸入影像等比例變小,比如 640x480 的一半,能大幅減輕 CPU 的地獄高溫負擔。
|
| 89 |
+
|
| 90 |
+
## **教師筆記 (Teacher's Note)**
|
| 91 |
+
|
| 92 |
+
* **【優化防呆實體化】**:請在教室分享資料夾或群組直接丟好一支叫做 `backup_hand_video.mp4` 的影片檔,並強調:「跑不動的、攝影機出奧步的同學,請把 0 換成檔名,學習是不會因為硬體而停下腳步的。」這不僅安撫學生心情,也傳達了程式開發具備「解耦輸入源測試」的軟體工程意涵。
|
| 93 |
+
* **從抽象到具體**:當在終端機瘋狂印出 `cx, cy` 座標,再讓他們親自手動乘上視窗大小寬度時,學生會因為親手把這數學概念轉換為可以移動物體的神力而興奮不已。這是體驗 AI 威力的第一個「魔幻時刻」。
|
projects/2026project_class/examples/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
projects/2026project_class/examples/background.jpg
ADDED
|
Git LFS Details
|
projects/2026project_class/examples/extra_apis/advice_slip_api.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
url = "https://api.adviceslip.com/advice"
|
| 5 |
+
|
| 6 |
+
print("==============================")
|
| 7 |
+
print(" 🔮 終端機人生解答書")
|
| 8 |
+
print("==============================\n")
|
| 9 |
+
|
| 10 |
+
while True:
|
| 11 |
+
user_input = input("心中默念一個問題,然後按 Enter 獲得指引 (輸入 q 退出):")
|
| 12 |
+
|
| 13 |
+
if user_input.lower() == 'q':
|
| 14 |
+
break
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
print("宇宙正在為你尋找答案...\n")
|
| 18 |
+
time.sleep(1) # 製造一點神秘的懸疑感
|
| 19 |
+
|
| 20 |
+
response = requests.get(url, timeout=3)
|
| 21 |
+
if response.status_code == 200:
|
| 22 |
+
data = response.json()
|
| 23 |
+
# 這個 API 的結構是 {"slip": { "id": 1, "advice": "..." }}
|
| 24 |
+
advice = data["slip"]["advice"]
|
| 25 |
+
|
| 26 |
+
print("------------------------------")
|
| 27 |
+
print(f"✨ 宇宙的指引: {advice}")
|
| 28 |
+
print("------------------------------\n")
|
| 29 |
+
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print("宇宙暫時沒有回應,請稍後再試。")
|
projects/2026project_class/examples/extra_apis/agify_api.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
print("==============================")
|
| 4 |
+
print(" 🕵️♂️ 姓名年齡探測器")
|
| 5 |
+
print("==============================\n")
|
| 6 |
+
print("這個神秘的系統分析了全球千萬筆戶政資料,能透過你的『英文名字』猜出你的年齡!\n")
|
| 7 |
+
|
| 8 |
+
while True:
|
| 9 |
+
name = input("請輸入一個英文名字 (例如 Steve, Kevin, Emma,輸入 q 退出):")
|
| 10 |
+
|
| 11 |
+
if name.lower() == 'q':
|
| 12 |
+
break
|
| 13 |
+
|
| 14 |
+
url = f"https://api.agify.io?name={name}"
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
response = requests.get(url, timeout=3)
|
| 18 |
+
if response.status_code == 200:
|
| 19 |
+
data = response.json()
|
| 20 |
+
|
| 21 |
+
estimated_age = data.get("age")
|
| 22 |
+
count = data.get("count")
|
| 23 |
+
|
| 24 |
+
print("\n------------------------------")
|
| 25 |
+
if estimated_age:
|
| 26 |
+
print(f"名字: {name.capitalize()}")
|
| 27 |
+
print(f"預估年齡: {estimated_age} 歲")
|
| 28 |
+
print(f"(根據全球 {count} 位名叫 {name.capitalize()} 的統計數據)")
|
| 29 |
+
else:
|
| 30 |
+
print(f"資料庫太小了,猜不出 {name} 的年齡。")
|
| 31 |
+
print("------------------------------\n")
|
| 32 |
+
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print("連線錯誤,無法連接至大數據智庫。")
|
projects/2026project_class/examples/extra_apis/dog_api.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
url = "https://dog.ceo/api/breeds/image/random"
|
| 4 |
+
|
| 5 |
+
print("==============================")
|
| 6 |
+
print(" 🐶 隨機狗狗圖片網址產生器")
|
| 7 |
+
print("==============================\n")
|
| 8 |
+
|
| 9 |
+
while True:
|
| 10 |
+
user_input = input("按 Enter 看狗狗 (輸入 q 退出):")
|
| 11 |
+
|
| 12 |
+
if user_input.lower() == 'q':
|
| 13 |
+
break
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
response = requests.get(url, timeout=3)
|
| 17 |
+
if response.status_code == 200:
|
| 18 |
+
data = response.json()
|
| 19 |
+
image_url = data["message"]
|
| 20 |
+
status = data["status"]
|
| 21 |
+
|
| 22 |
+
if status == "success":
|
| 23 |
+
print("\n------------------------------")
|
| 24 |
+
print("🐾 成功獲得狗狗!請按住 Ctrl (或 Cmd) 並用滑鼠點擊下方網址:")
|
| 25 |
+
print(f"🔗 {image_url}")
|
| 26 |
+
print("------------------------------\n")
|
| 27 |
+
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print("狗狗在睡覺,網路連線失敗。")
|
projects/2026project_class/examples/extra_apis/numbers_api.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
print("==============================")
|
| 4 |
+
print(" 🔢 數字冷知識圖書館")
|
| 5 |
+
print("==============================\n")
|
| 6 |
+
|
| 7 |
+
while True:
|
| 8 |
+
number_str = input("請輸入任意一個數字 (例如 42 或 100,輸入 q 退出):")
|
| 9 |
+
|
| 10 |
+
if number_str.lower() == 'q':
|
| 11 |
+
break
|
| 12 |
+
|
| 13 |
+
# 檢查輸入的是不是數字
|
| 14 |
+
if not number_str.isdigit():
|
| 15 |
+
print("⚠️ 只能輸入數字喔!\n")
|
| 16 |
+
continue
|
| 17 |
+
|
| 18 |
+
# 要求 JSON 格式回傳 (?json)
|
| 19 |
+
url = f"http://numbersapi.com/{number_str}?json"
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
response = requests.get(url, timeout=3)
|
| 23 |
+
if response.status_code == 200:
|
| 24 |
+
data = response.json()
|
| 25 |
+
|
| 26 |
+
fact = data["text"]
|
| 27 |
+
|
| 28 |
+
print("\n------------------------------")
|
| 29 |
+
print(f"📚 關於數字 {number_str} 的冷知識:")
|
| 30 |
+
print(f"英文: {fact}")
|
| 31 |
+
print("------------------------------\n")
|
| 32 |
+
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print("無法連線至圖書館。")
|
projects/2026project_class/examples/extra_apis/poke_api.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
# PokeAPI: 全球最知名的開源 Pokemon 資料庫
|
| 4 |
+
print("==============================")
|
| 5 |
+
print(" 🔴 寶可夢圖鑑查詢器")
|
| 6 |
+
print("==============================\n")
|
| 7 |
+
|
| 8 |
+
while True:
|
| 9 |
+
pokemon_name = input("請輸入英文寶可夢名稱或編號 (例如 pikachu 或 25,輸入 q 退出):").lower()
|
| 10 |
+
|
| 11 |
+
if pokemon_name == 'q':
|
| 12 |
+
break
|
| 13 |
+
|
| 14 |
+
url = f"https://pokeapi.co/api/v2/pokemon/{pokemon_name}"
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
print(f"正在連線到洛托姆圖鑑獲取 {pokemon_name} 的資料...")
|
| 18 |
+
response = requests.get(url, timeout=3)
|
| 19 |
+
|
| 20 |
+
if response.status_code == 200:
|
| 21 |
+
data = response.json()
|
| 22 |
+
|
| 23 |
+
name = data["name"].capitalize()
|
| 24 |
+
height = data["height"] / 10 # API 回傳的單位是公寸
|
| 25 |
+
weight = data["weight"] / 10 # API 回傳的單位是百克
|
| 26 |
+
|
| 27 |
+
# 獲取屬性列表 (這是一個陣列,我們可以用 list comprehension 取出來)
|
| 28 |
+
types = [t["type"]["name"] for t in data["types"]]
|
| 29 |
+
type_str = ", ".join(types)
|
| 30 |
+
|
| 31 |
+
print("\n------------------------------")
|
| 32 |
+
print(f"名稱: {name}")
|
| 33 |
+
print(f"屬性: {type_str}")
|
| 34 |
+
print(f"身高: {height} m")
|
| 35 |
+
print(f"體重: {weight} kg")
|
| 36 |
+
print("------------------------------\n")
|
| 37 |
+
|
| 38 |
+
elif response.status_code == 404:
|
| 39 |
+
print(f"\n❌ 找不到名為 '{pokemon_name}' 的寶可夢,請檢查拼字!\n")
|
| 40 |
+
|
| 41 |
+
except Exception as e:
|
| 42 |
+
print("網路連線失敗,無法連接到圖鑑伺服器。")
|
projects/2026project_class/examples/extra_apis/random_user_api.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
url = "https://randomuser.me/api/"
|
| 4 |
+
|
| 5 |
+
print("==============================")
|
| 6 |
+
print(" 🎭 假身分製造機")
|
| 7 |
+
print("==============================\n")
|
| 8 |
+
print("需要寫小說或是註冊不想用的網站嗎?按下 Enter 產生一個隨機的虛擬外國身分。\n")
|
| 9 |
+
|
| 10 |
+
while True:
|
| 11 |
+
user_input = input("按 Enter 取得新身分 (輸入 q 退出):")
|
| 12 |
+
|
| 13 |
+
if user_input.lower() == 'q':
|
| 14 |
+
break
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
# 發送網路請求,獲取假身分
|
| 18 |
+
response = requests.get(url, timeout=3)
|
| 19 |
+
if response.status_code == 200:
|
| 20 |
+
data = response.json()
|
| 21 |
+
|
| 22 |
+
# 這個 API 的資料藏得比較深,需要一層一層撥開
|
| 23 |
+
# 資料結構長這樣: {"results": [ {"name": {"first": "...", "last": "..."}}, ... ] }
|
| 24 |
+
person = data["results"][0]
|
| 25 |
+
|
| 26 |
+
first_name = person["name"]["first"]
|
| 27 |
+
last_name = person["name"]["last"]
|
| 28 |
+
gender = "男" if person["gender"] == "male" else "女"
|
| 29 |
+
country = person["location"]["country"]
|
| 30 |
+
email = person["email"]
|
| 31 |
+
age = person["dob"]["age"]
|
| 32 |
+
|
| 33 |
+
print("\n------------------------------")
|
| 34 |
+
print(f"👤 姓名: {first_name} {last_name} ({gender})")
|
| 35 |
+
print(f"🎂 年齡: {age} 歲")
|
| 36 |
+
print(f"🌍 國家: {country}")
|
| 37 |
+
print(f"📧 信箱: {email}")
|
| 38 |
+
print("------------------------------\n")
|
| 39 |
+
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print("身分偽造局暫時下線,請稍後再試。")
|
projects/2026project_class/examples/face_landmarker.task
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff
|
| 3 |
+
size 3758596
|
projects/2026project_class/examples/hand_landmarker.task
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1
|
| 3 |
+
size 7819105
|
projects/2026project_class/examples/hit.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:55440a99e00bb9de3a53bcaa709c83d412001f78dcf7103036ec02a5057c2fdd
|
| 3 |
+
size 13274
|
projects/2026project_class/examples/shoot.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:091481354d97c5469a2cd40f16d8fad63904eb4676992c34c1f208fc87a8173c
|
| 3 |
+
size 17684
|
projects/2026project_class/examples/test_mp.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import mediapipe as mp
|
| 3 |
+
from mediapipe.tasks import python
|
| 4 |
+
from mediapipe.tasks.python import vision
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'hand_landmarker.task'))
|
| 8 |
+
options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 9 |
+
detector = vision.HandLandmarker.create_from_options(options)
|
| 10 |
+
|
| 11 |
+
img = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 12 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img)
|
| 13 |
+
result = detector.detect(mp_image)
|
| 14 |
+
print("Hand detected?", bool(result.hand_landmarks))
|
projects/2026project_class/examples/w10_pinch_complete.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (Complete 完整版)
|
| 3 |
+
=============================================================
|
| 4 |
+
本週目標:
|
| 5 |
+
1. 用勾股定理計算大拇指 (4) 與食指 (8) 的距離
|
| 6 |
+
2. 實作 Pinch (掐合) 偵測:距離 < Threshold 就是捏起來了
|
| 7 |
+
3. 用手指碰觸 Pygame 畫面上的按鈕 + 掐合 = 魔法點擊
|
| 8 |
+
4. 使用邊緣觸發 (Edge Detection) 防止重複觸發
|
| 9 |
+
|
| 10 |
+
操作方式:
|
| 11 |
+
- 食指移動到按鈕上方 + 大拇指與食指捏合 = 點擊按鈕
|
| 12 |
+
- 顏色會隨點擊切換
|
| 13 |
+
|
| 14 |
+
注意:
|
| 15 |
+
- 確保 hand_landmarker.task 檔案和這個腳本放在同一個資料夾。
|
| 16 |
+
"""
|
| 17 |
+
import pygame
|
| 18 |
+
import cv2
|
| 19 |
+
import mediapipe as mp
|
| 20 |
+
from mediapipe.tasks import python
|
| 21 |
+
from mediapipe.tasks.python import vision
|
| 22 |
+
import math
|
| 23 |
+
import sys
|
| 24 |
+
import os
|
| 25 |
+
|
| 26 |
+
# ============================================
|
| 27 |
+
# 1. 初始化
|
| 28 |
+
# ============================================
|
| 29 |
+
pygame.init()
|
| 30 |
+
WIDTH, HEIGHT = 800, 600
|
| 31 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 32 |
+
pygame.display.set_caption("Week 10 - 魔法手勢按鈕 🖐️✨")
|
| 33 |
+
clock = pygame.time.Clock()
|
| 34 |
+
|
| 35 |
+
# 攝影機
|
| 36 |
+
cap = cv2.VideoCapture(0)
|
| 37 |
+
if not cap.isOpened():
|
| 38 |
+
print("❌ 無法開啟攝影機,請改用影片檔案")
|
| 39 |
+
pygame.quit()
|
| 40 |
+
sys.exit()
|
| 41 |
+
|
| 42 |
+
# MediaPipe (Tasks API)
|
| 43 |
+
try:
|
| 44 |
+
base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'hand_landmarker.task'))
|
| 45 |
+
options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 46 |
+
detector = vision.HandLandmarker.create_from_options(options)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"❌ 錯誤:無法載入 MediaPipe 模型:{e}")
|
| 49 |
+
pygame.quit()
|
| 50 |
+
sys.exit()
|
| 51 |
+
|
| 52 |
+
# ============================================
|
| 53 |
+
# 2. 遊戲變數
|
| 54 |
+
# ============================================
|
| 55 |
+
# 手指座標
|
| 56 |
+
finger_x, finger_y = 400, 300
|
| 57 |
+
thumb_x, thumb_y = 400, 300
|
| 58 |
+
|
| 59 |
+
# Pinch 變數
|
| 60 |
+
is_pinching = False
|
| 61 |
+
was_pinching = False # 邊緣觸發:紀錄上一幀的狀態
|
| 62 |
+
PINCH_THRESHOLD = 40 # 距離小於此值就算「捏起來」
|
| 63 |
+
dist = 999 # 兩指距離
|
| 64 |
+
|
| 65 |
+
# 按鈕設定
|
| 66 |
+
button_rect = pygame.Rect(300, 250, 200, 100)
|
| 67 |
+
button_colors = [(100, 100, 220), (220, 100, 100), (100, 220, 100), (220, 180, 50)]
|
| 68 |
+
current_color_index = 0
|
| 69 |
+
|
| 70 |
+
# 點擊計數器
|
| 71 |
+
click_count = 0
|
| 72 |
+
hand_detected = False
|
| 73 |
+
|
| 74 |
+
# ============================================
|
| 75 |
+
# 3. 主迴圈
|
| 76 |
+
# ============================================
|
| 77 |
+
running = True
|
| 78 |
+
while running:
|
| 79 |
+
for event in pygame.event.get():
|
| 80 |
+
if event.type == pygame.QUIT:
|
| 81 |
+
running = False
|
| 82 |
+
|
| 83 |
+
# --- 讀取攝影機 ---
|
| 84 |
+
success, img = cap.read()
|
| 85 |
+
if success:
|
| 86 |
+
img = cv2.flip(img, 1)
|
| 87 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 88 |
+
|
| 89 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 90 |
+
results = detector.detect(mp_image)
|
| 91 |
+
|
| 92 |
+
if results.hand_landmarks:
|
| 93 |
+
hand_detected = True
|
| 94 |
+
for hand_lms in results.hand_landmarks:
|
| 95 |
+
# 取得食指尖 (8) 座標
|
| 96 |
+
lm8 = hand_lms[8]
|
| 97 |
+
finger_x = int(lm8.x * WIDTH)
|
| 98 |
+
finger_y = int(lm8.y * HEIGHT)
|
| 99 |
+
|
| 100 |
+
# 取得大拇指尖 (4) 座標
|
| 101 |
+
lm4 = hand_lms[4]
|
| 102 |
+
thumb_x = int(lm4.x * WIDTH)
|
| 103 |
+
thumb_y = int(lm4.y * HEIGHT)
|
| 104 |
+
|
| 105 |
+
# 用 math.hypot 計算兩指之間的距離
|
| 106 |
+
dist = math.hypot(finger_x - thumb_x, finger_y - thumb_y)
|
| 107 |
+
|
| 108 |
+
# 判斷是否掐合
|
| 109 |
+
is_pinching = dist < PINCH_THRESHOLD
|
| 110 |
+
else:
|
| 111 |
+
hand_detected = False
|
| 112 |
+
is_pinching = False
|
| 113 |
+
|
| 114 |
+
# --- 邊緣觸發判定 ---
|
| 115 |
+
# 只有「前一幀沒捏 + 這一幀捏了」才算一次點擊
|
| 116 |
+
if is_pinching and not was_pinching:
|
| 117 |
+
# 檢查手指是否在按鈕範圍內
|
| 118 |
+
finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10)
|
| 119 |
+
if finger_rect.colliderect(button_rect):
|
| 120 |
+
click_count += 1
|
| 121 |
+
current_color_index = (current_color_index + 1) % len(button_colors)
|
| 122 |
+
print(f"✨ 魔法點擊!第 {click_count} 次")
|
| 123 |
+
|
| 124 |
+
# 更新上一幀的狀態
|
| 125 |
+
was_pinching = is_pinching
|
| 126 |
+
|
| 127 |
+
# --- 繪製畫面 ---
|
| 128 |
+
screen.fill((20, 20, 35))
|
| 129 |
+
|
| 130 |
+
# 畫按鈕
|
| 131 |
+
btn_color = button_colors[current_color_index]
|
| 132 |
+
pygame.draw.rect(screen, btn_color, button_rect, border_radius=16)
|
| 133 |
+
|
| 134 |
+
font = pygame.font.SysFont(None, 32)
|
| 135 |
+
btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255))
|
| 136 |
+
screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2,
|
| 137 |
+
button_rect.centery - btn_text.get_height() // 2))
|
| 138 |
+
|
| 139 |
+
# 畫提示文字在按鈕下方
|
| 140 |
+
hint_font = pygame.font.SysFont(None, 22)
|
| 141 |
+
hint = hint_font.render("Pinch on button to click!", True, (120, 120, 150))
|
| 142 |
+
screen.blit(hint, (button_rect.centerx - hint.get_width() // 2, button_rect.bottom + 12))
|
| 143 |
+
|
| 144 |
+
if hand_detected:
|
| 145 |
+
# 畫大拇指位置(紫色小點)
|
| 146 |
+
pygame.draw.circle(screen, (139, 92, 246), (thumb_x, thumb_y), 8)
|
| 147 |
+
|
| 148 |
+
# 畫兩指之間的連線
|
| 149 |
+
line_color = (50, 255, 50) if is_pinching else (255, 50, 50)
|
| 150 |
+
pygame.draw.line(screen, line_color, (thumb_x, thumb_y), (finger_x, finger_y), 2)
|
| 151 |
+
|
| 152 |
+
# 畫手指游標(根據 Pinch 狀態改變顏色)
|
| 153 |
+
if is_pinching:
|
| 154 |
+
cursor_color = (50, 255, 50) # 捏起來 → 綠色
|
| 155 |
+
else:
|
| 156 |
+
cursor_color = (255, 50, 50) # 放開 → 紅色
|
| 157 |
+
pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12)
|
| 158 |
+
|
| 159 |
+
# 狀態文字
|
| 160 |
+
small_font = pygame.font.SysFont(None, 24)
|
| 161 |
+
coord_text = small_font.render(f"Index(8): ({finger_x}, {finger_y}) Thumb(4): ({thumb_x}, {thumb_y})", True, (150, 150, 150))
|
| 162 |
+
screen.blit(coord_text, (10, 10))
|
| 163 |
+
|
| 164 |
+
dist_text = small_font.render(f"Distance: {dist:.0f} px | Threshold: {PINCH_THRESHOLD}", True, (150, 150, 200))
|
| 165 |
+
screen.blit(dist_text, (10, 35))
|
| 166 |
+
|
| 167 |
+
pinch_text = small_font.render(f"Pinching: {'YES ✅' if is_pinching else 'NO ❌'}", True,
|
| 168 |
+
(50, 255, 50) if is_pinching else (255, 100, 100))
|
| 169 |
+
screen.blit(pinch_text, (10, 60))
|
| 170 |
+
else:
|
| 171 |
+
no_hand = font.render("Show your hand to the camera!", True, (255, 200, 0))
|
| 172 |
+
screen.blit(no_hand, (WIDTH // 2 - no_hand.get_width() // 2, 50))
|
| 173 |
+
|
| 174 |
+
pygame.display.update()
|
| 175 |
+
clock.tick(30)
|
| 176 |
+
|
| 177 |
+
cap.release()
|
| 178 |
+
pygame.quit()
|
| 179 |
+
sys.exit()
|
projects/2026project_class/examples/w10_pinch_template.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (Template 填空版)
|
| 3 |
+
=============================================================
|
| 4 |
+
本週目標:
|
| 5 |
+
1. 用勾股定理計算大拇指 (4) 與食指 (8) 的距離
|
| 6 |
+
2. 實作 Pinch (掐合) 偵測:距離 < Threshold 就是捏起來了
|
| 7 |
+
3. 用手指碰觸 Pygame 畫面上的按鈕 + 掐合 = 魔法點擊
|
| 8 |
+
4. 使用邊緣觸發 (Edge Detection) 防止重複觸發
|
| 9 |
+
|
| 10 |
+
操作方式:
|
| 11 |
+
- 食指移動到按鈕上方 + 大拇指與食指捏合 = 點擊按鈕
|
| 12 |
+
- 顏色會隨點擊切換
|
| 13 |
+
|
| 14 |
+
注意:
|
| 15 |
+
- 確保 hand_landmarker.task 檔案和這個腳本放在同一個資料夾。
|
| 16 |
+
"""
|
| 17 |
+
import pygame
|
| 18 |
+
import cv2
|
| 19 |
+
import mediapipe as mp
|
| 20 |
+
from mediapipe.tasks import python
|
| 21 |
+
from mediapipe.tasks.python import vision
|
| 22 |
+
import math
|
| 23 |
+
import sys
|
| 24 |
+
import os
|
| 25 |
+
|
| 26 |
+
# ============================================
|
| 27 |
+
# 1. 初始化
|
| 28 |
+
# ============================================
|
| 29 |
+
pygame.init()
|
| 30 |
+
WIDTH, HEIGHT = 800, 600
|
| 31 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 32 |
+
pygame.display.set_caption("Week 10 - 魔法手勢按鈕 🖐️✨")
|
| 33 |
+
clock = pygame.time.Clock()
|
| 34 |
+
|
| 35 |
+
# 攝影機
|
| 36 |
+
cap = cv2.VideoCapture(0)
|
| 37 |
+
# 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_hand_video.mp4")
|
| 38 |
+
|
| 39 |
+
# MediaPipe (Tasks API)
|
| 40 |
+
# base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'hand_landmarker.task'))
|
| 41 |
+
# options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 42 |
+
# detector = vision.HandLandmarker.create_from_options(options)
|
| 43 |
+
|
| 44 |
+
# ============================================
|
| 45 |
+
# 2. 遊戲變數
|
| 46 |
+
# ============================================
|
| 47 |
+
# 手指座標
|
| 48 |
+
finger_x, finger_y = 400, 300
|
| 49 |
+
|
| 50 |
+
# 【TODO 1】宣告 Pinch 相關變數
|
| 51 |
+
# is_pinching = False
|
| 52 |
+
# was_pinching = False # 邊緣觸發:紀錄上一幀的狀態
|
| 53 |
+
# PINCH_THRESHOLD = 40 # 距離小於此值就算「捏起來」
|
| 54 |
+
|
| 55 |
+
# 按鈕設定
|
| 56 |
+
button_rect = pygame.Rect(300, 250, 200, 100)
|
| 57 |
+
button_colors = [(100, 100, 220), (220, 100, 100), (100, 220, 100), (220, 180, 50)]
|
| 58 |
+
current_color_index = 0
|
| 59 |
+
|
| 60 |
+
# 點擊計數器
|
| 61 |
+
click_count = 0
|
| 62 |
+
|
| 63 |
+
# ============================================
|
| 64 |
+
# 3. 主迴圈
|
| 65 |
+
# ============================================
|
| 66 |
+
running = True
|
| 67 |
+
while running:
|
| 68 |
+
for event in pygame.event.get():
|
| 69 |
+
if event.type == pygame.QUIT:
|
| 70 |
+
running = False
|
| 71 |
+
|
| 72 |
+
# --- 讀取攝影機 ---
|
| 73 |
+
success, img = cap.read()
|
| 74 |
+
if success:
|
| 75 |
+
img = cv2.flip(img, 1)
|
| 76 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 77 |
+
|
| 78 |
+
# mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 79 |
+
# results = detector.detect(mp_image)
|
| 80 |
+
|
| 81 |
+
# if results.hand_landmarks:
|
| 82 |
+
# for hand_lms in results.hand_landmarks:
|
| 83 |
+
# # 取得食指尖 (8) 座標
|
| 84 |
+
# lm8 = hand_lms[8]
|
| 85 |
+
# finger_x = int(lm8.x * WIDTH)
|
| 86 |
+
# finger_y = int(lm8.y * HEIGHT)
|
| 87 |
+
#
|
| 88 |
+
# # 【TODO 2】取得大拇指尖 (4) 座標
|
| 89 |
+
# # lm4 = hand_lms[4]
|
| 90 |
+
# # thumb_x = int(lm4.x * WIDTH)
|
| 91 |
+
# # thumb_y = int(lm4.y * HEIGHT)
|
| 92 |
+
#
|
| 93 |
+
# # 【TODO 3】用 math.hypot 計算兩指之間的距離
|
| 94 |
+
# # dist = math.hypot(finger_x - thumb_x, finger_y - thumb_y)
|
| 95 |
+
#
|
| 96 |
+
# # 【TODO 4】判斷是否掐合(距離 < PINCH_THRESHOLD)
|
| 97 |
+
# # is_pinching = dist < PINCH_THRESHOLD
|
| 98 |
+
pass
|
| 99 |
+
|
| 100 |
+
# 【TODO 5】邊緣觸發:只有「前一幀沒捏 + 這一幀捏了」才算一次點擊
|
| 101 |
+
# if is_pinching and not was_pinching:
|
| 102 |
+
# # 檢查手指是否在按鈕範圍內
|
| 103 |
+
# finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10)
|
| 104 |
+
# if finger_rect.colliderect(button_rect):
|
| 105 |
+
# click_count += 1
|
| 106 |
+
# current_color_index = (current_color_index + 1) % len(button_colors)
|
| 107 |
+
# print(f"✨ 魔法點擊!第 {click_count} 次")
|
| 108 |
+
|
| 109 |
+
# 【TODO 6】更新 was_pinching
|
| 110 |
+
# was_pinching = is_pinching
|
| 111 |
+
|
| 112 |
+
# --- 繪製畫面 ---
|
| 113 |
+
screen.fill((20, 20, 35))
|
| 114 |
+
|
| 115 |
+
# 畫按鈕
|
| 116 |
+
btn_color = button_colors[current_color_index]
|
| 117 |
+
pygame.draw.rect(screen, btn_color, button_rect, border_radius=16)
|
| 118 |
+
|
| 119 |
+
font = pygame.font.SysFont(None, 32)
|
| 120 |
+
btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255))
|
| 121 |
+
screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2,
|
| 122 |
+
button_rect.centery - btn_text.get_height() // 2))
|
| 123 |
+
|
| 124 |
+
# 畫手指游標
|
| 125 |
+
# 【TODO 7】根據 is_pinching 狀態改變顏色(捏起=綠色,放開=紅色)
|
| 126 |
+
cursor_color = (255, 50, 50) # 預設紅色
|
| 127 |
+
# if is_pinching:
|
| 128 |
+
# cursor_color = (50, 255, 50) # 捏起來變綠色
|
| 129 |
+
|
| 130 |
+
pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12)
|
| 131 |
+
|
| 132 |
+
# 狀態文字
|
| 133 |
+
small_font = pygame.font.SysFont(None, 24)
|
| 134 |
+
status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150))
|
| 135 |
+
screen.blit(status, (10, 10))
|
| 136 |
+
|
| 137 |
+
# 【TODO 8】顯示距離數值(dist)在畫面上
|
| 138 |
+
# dist_text = small_font.render(f"Dist: {dist:.0f} px", True, (150, 150, 200))
|
| 139 |
+
# screen.blit(dist_text, (10, 35))
|
| 140 |
+
|
| 141 |
+
pygame.display.update()
|
| 142 |
+
clock.tick(30)
|
| 143 |
+
|
| 144 |
+
# cap.release()
|
| 145 |
+
pygame.quit()
|
| 146 |
+
sys.exit()
|
projects/2026project_class/examples/w10_step1_distance.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import mediapipe as mp
|
| 3 |
+
from mediapipe.tasks import python
|
| 4 |
+
from mediapipe.tasks.python import vision
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# ============================================
|
| 9 |
+
# Week 10 - Step 1: 計算兩指距離
|
| 10 |
+
# ============================================
|
| 11 |
+
|
| 12 |
+
model_path = os.path.join(os.path.dirname(__file__), 'hand_landmarker.task')
|
| 13 |
+
base_options = python.BaseOptions(model_asset_path=model_path)
|
| 14 |
+
options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 15 |
+
detector = vision.HandLandmarker.create_from_options(options)
|
| 16 |
+
|
| 17 |
+
cap = cv2.VideoCapture(0)
|
| 18 |
+
|
| 19 |
+
while True:
|
| 20 |
+
success, img = cap.read()
|
| 21 |
+
if not success:
|
| 22 |
+
break
|
| 23 |
+
|
| 24 |
+
img = cv2.flip(img, 1)
|
| 25 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 26 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 27 |
+
results = detector.detect(mp_image)
|
| 28 |
+
|
| 29 |
+
if results.hand_landmarks:
|
| 30 |
+
for hand_lms in results.hand_landmarks:
|
| 31 |
+
lm4 = hand_lms[4] # 大拇指尖
|
| 32 |
+
lm8 = hand_lms[8] # 食指尖
|
| 33 |
+
|
| 34 |
+
h, w, c = img.shape
|
| 35 |
+
cx4, cy4 = int(lm4.x * w), int(lm4.y * h)
|
| 36 |
+
cx8, cy8 = int(lm8.x * w), int(lm8.y * h)
|
| 37 |
+
|
| 38 |
+
# 使用 math.hypot 計算兩點之間的直線距離
|
| 39 |
+
dist = math.hypot(cx8 - cx4, cy8 - cy4)
|
| 40 |
+
print(f"距離:{dist:.1f}")
|
| 41 |
+
|
| 42 |
+
# 在影格上畫線連接兩指
|
| 43 |
+
cv2.line(img, (cx4, cy4), (cx8, cy8), (255, 255, 255), 2)
|
| 44 |
+
cv2.circle(img, (cx4, cy4), 8, (255, 0, 255), -1)
|
| 45 |
+
cv2.circle(img, (cx8, cy8), 8, (255, 0, 255), -1)
|
| 46 |
+
|
| 47 |
+
cv2.imshow("Distance Calc", img)
|
| 48 |
+
|
| 49 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 50 |
+
break
|
| 51 |
+
|
| 52 |
+
cap.release()
|
| 53 |
+
cv2.destroyAllWindows()
|
projects/2026project_class/examples/w10_step2_threshold.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import mediapipe as mp
|
| 3 |
+
from mediapipe.tasks import python
|
| 4 |
+
from mediapipe.tasks.python import vision
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# ============================================
|
| 9 |
+
# Week 10 - Step 2: Threshold 臨界值判斷
|
| 10 |
+
# ============================================
|
| 11 |
+
|
| 12 |
+
model_path = os.path.join(os.path.dirname(__file__), 'hand_landmarker.task')
|
| 13 |
+
base_options = python.BaseOptions(model_asset_path=model_path)
|
| 14 |
+
options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 15 |
+
detector = vision.HandLandmarker.create_from_options(options)
|
| 16 |
+
|
| 17 |
+
cap = cv2.VideoCapture(0)
|
| 18 |
+
|
| 19 |
+
while True:
|
| 20 |
+
success, img = cap.read()
|
| 21 |
+
if not success:
|
| 22 |
+
break
|
| 23 |
+
|
| 24 |
+
img = cv2.flip(img, 1)
|
| 25 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 26 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 27 |
+
results = detector.detect(mp_image)
|
| 28 |
+
|
| 29 |
+
if results.hand_landmarks:
|
| 30 |
+
for hand_lms in results.hand_landmarks:
|
| 31 |
+
lm4 = hand_lms[4]
|
| 32 |
+
lm8 = hand_lms[8]
|
| 33 |
+
|
| 34 |
+
h, w, c = img.shape
|
| 35 |
+
cx4, cy4 = int(lm4.x * w), int(lm4.y * h)
|
| 36 |
+
cx8, cy8 = int(lm8.x * w), int(lm8.y * h)
|
| 37 |
+
|
| 38 |
+
dist = math.hypot(cx8 - cx4, cy8 - cy4)
|
| 39 |
+
|
| 40 |
+
# Threshold 判斷:距離小於 40 視為「捏起來」
|
| 41 |
+
if dist < 40:
|
| 42 |
+
is_pinching = True
|
| 43 |
+
# 捏起來 → 顯示綠色
|
| 44 |
+
cv2.circle(img, (cx8, cy8), 20, (0, 255, 0), -1)
|
| 45 |
+
cv2.putText(img, "Pinching!", (cx8 - 40, cy8 - 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
| 46 |
+
else:
|
| 47 |
+
is_pinching = False
|
| 48 |
+
# 放開 → 顯示紅色
|
| 49 |
+
cv2.circle(img, (cx8, cy8), 20, (0, 0, 255), -1)
|
| 50 |
+
|
| 51 |
+
cv2.line(img, (cx4, cy4), (cx8, cy8), (255, 255, 255), 2)
|
| 52 |
+
|
| 53 |
+
cv2.imshow("Pinch Threshold", img)
|
| 54 |
+
|
| 55 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 56 |
+
break
|
| 57 |
+
|
| 58 |
+
cap.release()
|
| 59 |
+
cv2.destroyAllWindows()
|
projects/2026project_class/examples/w11_classifier_complete.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 11 - Teachable Machine 影像分類 + AI 音效觸發 (Complete 完整版)
|
| 3 |
+
=====================================================================
|
| 4 |
+
本週目標:
|
| 5 |
+
1. 載入 Teachable Machine 訓練好的 Keras 模型 (.h5)
|
| 6 |
+
2. 使用攝影機即時推論,辨識畫面屬於哪一個類別
|
| 7 |
+
3. 當偵測到特定類別 (如「喝水中」) 時觸發 Pygame 音效
|
| 8 |
+
4. 使用 Boolean Flag 防止音效重複播放(邊緣觸發)
|
| 9 |
+
|
| 10 |
+
前置準備:
|
| 11 |
+
- 從 Teachable Machine 匯出 keras_model.h5 和 labels.txt
|
| 12 |
+
- 把兩個檔案放在跟這個 .py 檔案同一個資料夾裡
|
| 13 |
+
- pip install tensorflow opencv-python numpy pygame
|
| 14 |
+
"""
|
| 15 |
+
import pygame
|
| 16 |
+
import cv2
|
| 17 |
+
import numpy as np
|
| 18 |
+
import sys
|
| 19 |
+
|
| 20 |
+
from keras.models import load_model
|
| 21 |
+
|
| 22 |
+
# ============================================
|
| 23 |
+
# 1. 初始化 Pygame
|
| 24 |
+
# ============================================
|
| 25 |
+
pygame.init()
|
| 26 |
+
pygame.mixer.init()
|
| 27 |
+
WIDTH, HEIGHT = 800, 600
|
| 28 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 29 |
+
pygame.display.set_caption("Week 11 - AI 影像分類器 🧠🔮")
|
| 30 |
+
clock = pygame.time.Clock()
|
| 31 |
+
|
| 32 |
+
font = pygame.font.SysFont(None, 48)
|
| 33 |
+
small_font = pygame.font.SysFont(None, 28)
|
| 34 |
+
bar_font = pygame.font.SysFont(None, 22)
|
| 35 |
+
|
| 36 |
+
# ============================================
|
| 37 |
+
# 2. 載入模型與標籤
|
| 38 |
+
# ============================================
|
| 39 |
+
try:
|
| 40 |
+
model = load_model("keras_model.h5", compile=False)
|
| 41 |
+
print("✅ 模型載入成功!")
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"❌ 模型載入失敗:{e}")
|
| 44 |
+
print(" 請確認 keras_model.h5 是否在同一個資料夾中")
|
| 45 |
+
pygame.quit()
|
| 46 |
+
sys.exit()
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
labels = open("labels.txt", "r").readlines()
|
| 50 |
+
print(f"✅ 載入 {len(labels)} 個分類標籤:{[l.strip() for l in labels]}")
|
| 51 |
+
except FileNotFoundError:
|
| 52 |
+
print("❌ 找不到 labels.txt!請確認檔案是否在同一個資料夾中")
|
| 53 |
+
pygame.quit()
|
| 54 |
+
sys.exit()
|
| 55 |
+
|
| 56 |
+
# ============================================
|
| 57 |
+
# 3. 攝影機
|
| 58 |
+
# ============================================
|
| 59 |
+
cap = cv2.VideoCapture(0)
|
| 60 |
+
if not cap.isOpened():
|
| 61 |
+
print("❌ 無法開啟攝影機,請改用影片檔案")
|
| 62 |
+
pygame.quit()
|
| 63 |
+
sys.exit()
|
| 64 |
+
|
| 65 |
+
# ============================================
|
| 66 |
+
# 4. 遊戲變數
|
| 67 |
+
# ============================================
|
| 68 |
+
current_class = "等待分析..."
|
| 69 |
+
confidence = 0.0
|
| 70 |
+
prediction = None
|
| 71 |
+
frame_counter = 0
|
| 72 |
+
|
| 73 |
+
# 邊緣觸發:紀錄上一個狀態
|
| 74 |
+
is_drinking = False # 根據你的類別修改
|
| 75 |
+
|
| 76 |
+
# 載入音效
|
| 77 |
+
try:
|
| 78 |
+
trigger_sound = pygame.mixer.Sound("bubble.wav")
|
| 79 |
+
print("✅ 音效載入成功")
|
| 80 |
+
except FileNotFoundError:
|
| 81 |
+
trigger_sound = None
|
| 82 |
+
print("⚠️ 找不到音效檔案 bubble.wav,將不播放音效")
|
| 83 |
+
|
| 84 |
+
# 背景顏色(根據辨識結果切換)
|
| 85 |
+
bg_color = (20, 20, 35)
|
| 86 |
+
trigger_flash = 0 # 觸發時的閃爍計數器
|
| 87 |
+
|
| 88 |
+
# 信心度條形圖的顏色
|
| 89 |
+
bar_colors = [
|
| 90 |
+
(34, 197, 94), # 綠色
|
| 91 |
+
(59, 130, 246), # 藍色
|
| 92 |
+
(245, 158, 11), # 黃色
|
| 93 |
+
(239, 68, 68), # 紅色
|
| 94 |
+
(139, 92, 246), # 紫色
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
# ============================================
|
| 98 |
+
# 5. 主迴圈
|
| 99 |
+
# ============================================
|
| 100 |
+
running = True
|
| 101 |
+
while running:
|
| 102 |
+
for event in pygame.event.get():
|
| 103 |
+
if event.type == pygame.QUIT:
|
| 104 |
+
running = False
|
| 105 |
+
|
| 106 |
+
# --- 讀取攝影機 ---
|
| 107 |
+
success, img = cap.read()
|
| 108 |
+
if not success:
|
| 109 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
img = cv2.flip(img, 1)
|
| 113 |
+
|
| 114 |
+
# --- 每 5 幀做一次推論(避免畫面大卡)---
|
| 115 |
+
frame_counter += 1
|
| 116 |
+
if frame_counter % 5 == 0:
|
| 117 |
+
try:
|
| 118 |
+
# Keras 要求的影像預處理(這三行請死背!)
|
| 119 |
+
img_resized = cv2.resize(img, (224, 224))
|
| 120 |
+
img_array = np.asarray(img_resized, dtype=np.float32).reshape(1, 224, 224, 3)
|
| 121 |
+
img_array = (img_array / 127.5) - 1
|
| 122 |
+
|
| 123 |
+
# 前饋推論:取得每個類別的機率
|
| 124 |
+
prediction = model.predict(img_array, verbose=0)
|
| 125 |
+
index = np.argmax(prediction)
|
| 126 |
+
current_class = labels[index].strip()
|
| 127 |
+
confidence = prediction[0][index]
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f"推論出錯:{e}")
|
| 130 |
+
|
| 131 |
+
# --- 邊緣觸發 + 動作回饋 ---
|
| 132 |
+
# 這裡假設第一個非背景的類別叫做「喝水中」
|
| 133 |
+
# 請根據你自己的 labels.txt 修改判斷條件!
|
| 134 |
+
if current_class != labels[0].strip() and confidence > 0.90:
|
| 135 |
+
if not is_drinking:
|
| 136 |
+
# 狀態剛從「背景」切換到「動作」的瞬間
|
| 137 |
+
if trigger_sound:
|
| 138 |
+
trigger_sound.play()
|
| 139 |
+
trigger_flash = 15 # 觸發閃爍 15 幀
|
| 140 |
+
print(f"🥤 偵測到動作:{current_class}!信心度: {confidence:.1%}")
|
| 141 |
+
is_drinking = True
|
| 142 |
+
bg_color = (20, 50, 35) # 深綠背景
|
| 143 |
+
else:
|
| 144 |
+
is_drinking = False
|
| 145 |
+
bg_color = (20, 20, 35) # 回到預設
|
| 146 |
+
|
| 147 |
+
# 閃爍效果倒數
|
| 148 |
+
if trigger_flash > 0:
|
| 149 |
+
trigger_flash -= 1
|
| 150 |
+
|
| 151 |
+
# --- 繪製畫面 ---
|
| 152 |
+
# 背景(觸發時閃爍)
|
| 153 |
+
if trigger_flash > 0 and trigger_flash % 2 == 0:
|
| 154 |
+
screen.fill((40, 80, 50))
|
| 155 |
+
else:
|
| 156 |
+
screen.fill(bg_color)
|
| 157 |
+
|
| 158 |
+
# 辨識結果大字
|
| 159 |
+
result_color = (50, 255, 50) if is_drinking else (255, 255, 255)
|
| 160 |
+
class_text = font.render(f"{current_class}", True, result_color)
|
| 161 |
+
screen.blit(class_text, (WIDTH // 2 - class_text.get_width() // 2, 60))
|
| 162 |
+
|
| 163 |
+
conf_text = small_font.render(f"Confidence: {confidence:.1%}", True, (100, 200, 255))
|
| 164 |
+
screen.blit(conf_text, (WIDTH // 2 - conf_text.get_width() // 2, 120))
|
| 165 |
+
|
| 166 |
+
# --- 信心度條形圖 ---
|
| 167 |
+
if prediction is not None:
|
| 168 |
+
bar_y_start = 180
|
| 169 |
+
bar_height = 30
|
| 170 |
+
bar_gap = 12
|
| 171 |
+
bar_max_width = 500
|
| 172 |
+
bar_x = 150
|
| 173 |
+
|
| 174 |
+
for i, prob in enumerate(prediction[0]):
|
| 175 |
+
y = bar_y_start + i * (bar_height + bar_gap)
|
| 176 |
+
label_name = labels[i].strip() if i < len(labels) else f"Class {i}"
|
| 177 |
+
color = bar_colors[i % len(bar_colors)]
|
| 178 |
+
|
| 179 |
+
# 標籤名稱
|
| 180 |
+
label_text = bar_font.render(label_name, True, (200, 200, 200))
|
| 181 |
+
screen.blit(label_text, (bar_x - label_text.get_width() - 10, y + 5))
|
| 182 |
+
|
| 183 |
+
# 背景軌道
|
| 184 |
+
pygame.draw.rect(screen, (40, 40, 55), (bar_x, y, bar_max_width, bar_height), border_radius=6)
|
| 185 |
+
|
| 186 |
+
# 填充條
|
| 187 |
+
fill_width = int(prob * bar_max_width)
|
| 188 |
+
if fill_width > 0:
|
| 189 |
+
pygame.draw.rect(screen, color, (bar_x, y, fill_width, bar_height), border_radius=6)
|
| 190 |
+
|
| 191 |
+
# 百分比文字
|
| 192 |
+
pct_text = bar_font.render(f"{prob:.1%}", True, (255, 255, 255))
|
| 193 |
+
text_x = bar_x + fill_width + 8 if fill_width < bar_max_width - 60 else bar_x + fill_width - 55
|
| 194 |
+
screen.blit(pct_text, (text_x, y + 5))
|
| 195 |
+
|
| 196 |
+
# 標記最高的那一條
|
| 197 |
+
if i == np.argmax(prediction):
|
| 198 |
+
pygame.draw.rect(screen, color, (bar_x - 4, y, 4, bar_height), border_radius=2)
|
| 199 |
+
|
| 200 |
+
# 底部狀態列
|
| 201 |
+
status_text = small_font.render(
|
| 202 |
+
f"Frame: {frame_counter} | Predict every 5 frames | FPS: {clock.get_fps():.0f}",
|
| 203 |
+
True, (80, 80, 100)
|
| 204 |
+
)
|
| 205 |
+
screen.blit(status_text, (WIDTH // 2 - status_text.get_width() // 2, HEIGHT - 35))
|
| 206 |
+
|
| 207 |
+
pygame.display.update()
|
| 208 |
+
clock.tick(30)
|
| 209 |
+
|
| 210 |
+
cap.release()
|
| 211 |
+
pygame.quit()
|
| 212 |
+
sys.exit()
|
projects/2026project_class/examples/w11_classifier_template.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 11 - Teachable Machine 影像分類 + AI 音效觸發 (Template 填空版)
|
| 3 |
+
=====================================================================
|
| 4 |
+
本週目標:
|
| 5 |
+
1. 載入 Teachable Machine 訓練好的 Keras 模型 (.h5)
|
| 6 |
+
2. 使用攝影機即時推論,辨識畫面屬於哪一個類別
|
| 7 |
+
3. 當偵測到特定類別 (如「喝水中」) 時觸發 Pygame 音效
|
| 8 |
+
4. 使用 Boolean Flag 防止音效重複播放(邊緣觸發)
|
| 9 |
+
|
| 10 |
+
前置準備:
|
| 11 |
+
- 從 Teachable Machine 匯出 keras_model.h5 和 labels.txt
|
| 12 |
+
- 把兩個檔案放在跟這個 .py 檔案同一個資料夾裡
|
| 13 |
+
- pip install tensorflow opencv-python numpy pygame
|
| 14 |
+
"""
|
| 15 |
+
import pygame
|
| 16 |
+
import cv2
|
| 17 |
+
import numpy as np
|
| 18 |
+
import sys
|
| 19 |
+
|
| 20 |
+
# 【TODO 1】匯入 Keras 模型載入工具
|
| 21 |
+
# from keras.models import load_model
|
| 22 |
+
|
| 23 |
+
# ============================================
|
| 24 |
+
# 1. 初始化 Pygame
|
| 25 |
+
# ============================================
|
| 26 |
+
pygame.init()
|
| 27 |
+
pygame.mixer.init()
|
| 28 |
+
WIDTH, HEIGHT = 800, 600
|
| 29 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 30 |
+
pygame.display.set_caption("Week 11 - AI 影像分類器 🧠🔮")
|
| 31 |
+
clock = pygame.time.Clock()
|
| 32 |
+
|
| 33 |
+
font = pygame.font.SysFont(None, 48)
|
| 34 |
+
small_font = pygame.font.SysFont(None, 28)
|
| 35 |
+
|
| 36 |
+
# ============================================
|
| 37 |
+
# 2. 載入模型與標籤
|
| 38 |
+
# ============================================
|
| 39 |
+
# 【TODO 2】載入 Teachable Machine 訓練出的模型
|
| 40 |
+
# model = load_model("keras_model.h5", compile=False)
|
| 41 |
+
|
| 42 |
+
# 【TODO 3】讀取標籤文字檔(每一行是一個類別名稱)
|
| 43 |
+
# labels = open("labels.txt", "r").readlines()
|
| 44 |
+
|
| 45 |
+
# ============================================
|
| 46 |
+
# 3. 攝影機
|
| 47 |
+
# ============================================
|
| 48 |
+
cap = cv2.VideoCapture(0)
|
| 49 |
+
# 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_video.mp4")
|
| 50 |
+
|
| 51 |
+
# ============================================
|
| 52 |
+
# 4. 遊戲變數
|
| 53 |
+
# ============================================
|
| 54 |
+
current_class = "等待中..."
|
| 55 |
+
confidence = 0.0
|
| 56 |
+
frame_counter = 0
|
| 57 |
+
|
| 58 |
+
# 【TODO 4】邊緣觸發:宣告「上一個狀態」的旗標
|
| 59 |
+
# is_drinking = False # 目前是不是喝水中
|
| 60 |
+
# 你可以根據自己的類別修改變數名稱
|
| 61 |
+
|
| 62 |
+
# 載入音效(請準備你自己的音效檔案)
|
| 63 |
+
# try:
|
| 64 |
+
# trigger_sound = pygame.mixer.Sound("bubble.wav")
|
| 65 |
+
# except FileNotFoundError:
|
| 66 |
+
# trigger_sound = None
|
| 67 |
+
# print("⚠️ 找不到音效檔案 bubble.wav,將不播放音效")
|
| 68 |
+
|
| 69 |
+
# 背景顏色(根據辨識結果切換)
|
| 70 |
+
bg_color = (20, 20, 35)
|
| 71 |
+
|
| 72 |
+
# ============================================
|
| 73 |
+
# 5. 主迴圈
|
| 74 |
+
# ============================================
|
| 75 |
+
running = True
|
| 76 |
+
while running:
|
| 77 |
+
for event in pygame.event.get():
|
| 78 |
+
if event.type == pygame.QUIT:
|
| 79 |
+
running = False
|
| 80 |
+
|
| 81 |
+
# --- 讀取攝影機 ---
|
| 82 |
+
success, img = cap.read()
|
| 83 |
+
if not success:
|
| 84 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
img = cv2.flip(img, 1)
|
| 88 |
+
|
| 89 |
+
# 【TODO 5】影像前處理 + 模型推論
|
| 90 |
+
# 注意:不要每一幀都推論!每 5 幀做一次就好,避免畫面大卡
|
| 91 |
+
# frame_counter += 1
|
| 92 |
+
# if frame_counter % 5 == 0:
|
| 93 |
+
# # Keras 要求的預處理(這三行請死背!)
|
| 94 |
+
# img_resized = cv2.resize(img, (224, 224))
|
| 95 |
+
# img_array = np.asarray(img_resized, dtype=np.float32).reshape(1, 224, 224, 3)
|
| 96 |
+
# img_array = (img_array / 127.5) - 1
|
| 97 |
+
#
|
| 98 |
+
# # 前饋推論
|
| 99 |
+
# prediction = model.predict(img_array)
|
| 100 |
+
# index = np.argmax(prediction)
|
| 101 |
+
# current_class = labels[index].strip()
|
| 102 |
+
# confidence = prediction[0][index]
|
| 103 |
+
|
| 104 |
+
# 【TODO 6】邊緣觸發 + 播放音效
|
| 105 |
+
# if current_class == "喝水中" and confidence > 0.90:
|
| 106 |
+
# if not is_drinking:
|
| 107 |
+
# if trigger_sound:
|
| 108 |
+
# trigger_sound.play()
|
| 109 |
+
# print(f"🥤 偵測到喝水!信心度: {confidence:.1%}")
|
| 110 |
+
# is_drinking = True
|
| 111 |
+
# bg_color = (20, 50, 35) # 切換背景為深綠
|
| 112 |
+
# else:
|
| 113 |
+
# is_drinking = False
|
| 114 |
+
# bg_color = (20, 20, 35) # 回到預設背景
|
| 115 |
+
|
| 116 |
+
# --- 繪製畫面 ---
|
| 117 |
+
screen.fill(bg_color)
|
| 118 |
+
|
| 119 |
+
# 顯示辨識結果
|
| 120 |
+
class_text = font.render(f"Class: {current_class}", True, (255, 255, 255))
|
| 121 |
+
screen.blit(class_text, (WIDTH // 2 - class_text.get_width() // 2, 200))
|
| 122 |
+
|
| 123 |
+
conf_text = font.render(f"Confidence: {confidence:.1%}", True, (100, 200, 255))
|
| 124 |
+
screen.blit(conf_text, (WIDTH // 2 - conf_text.get_width() // 2, 260))
|
| 125 |
+
|
| 126 |
+
# 【TODO 7】畫出每個類別的信心度條形圖
|
| 127 |
+
# 提示:用 for 迴圈遍歷 prediction[0],每個類別畫一條長條
|
| 128 |
+
# 用不同顏色標示最高機率的類別
|
| 129 |
+
|
| 130 |
+
# 底部提示
|
| 131 |
+
hint = small_font.render("AI is watching...", True, (80, 80, 100))
|
| 132 |
+
screen.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT - 40))
|
| 133 |
+
|
| 134 |
+
pygame.display.update()
|
| 135 |
+
clock.tick(30)
|
| 136 |
+
|
| 137 |
+
cap.release()
|
| 138 |
+
pygame.quit()
|
| 139 |
+
sys.exit()
|
projects/2026project_class/examples/w12_eyeguard_complete.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 12 - AI 生活工具箱:護眼大師 (Complete 完整版)
|
| 3 |
+
=====================================================
|
| 4 |
+
本週目標:
|
| 5 |
+
1. 將 AI 視覺技術整合進一個「解決生活痛點」的完整專案
|
| 6 |
+
2. 使用 MediaPipe 臉部偵測兩眼距離,判斷是否離螢幕太近
|
| 7 |
+
3. 在 Pygame 畫面上顯示即時狀態與警報
|
| 8 |
+
4. 加入防呆機制(try-except、None 檢查)
|
| 9 |
+
|
| 10 |
+
護眼大師 EyeGuard:
|
| 11 |
+
- 偵測臉部兩眼之間的距離(兩眼距離越大 = 離鏡頭越近)
|
| 12 |
+
- 超過安全閾值時發出警報文字 + 閃爍效果 + 音效
|
| 13 |
+
- 安全時累計計時器鼓勵良好坐姿
|
| 14 |
+
|
| 15 |
+
注意:
|
| 16 |
+
- 確保 face_landmarker.task 檔案和這個腳本放在同一個資料夾。
|
| 17 |
+
"""
|
| 18 |
+
import pygame
|
| 19 |
+
import cv2
|
| 20 |
+
import mediapipe as mp
|
| 21 |
+
from mediapipe.tasks import python
|
| 22 |
+
from mediapipe.tasks.python import vision
|
| 23 |
+
import math
|
| 24 |
+
import sys
|
| 25 |
+
import os
|
| 26 |
+
import time
|
| 27 |
+
|
| 28 |
+
# ============================================
|
| 29 |
+
# 1. 初始化
|
| 30 |
+
# ============================================
|
| 31 |
+
pygame.init()
|
| 32 |
+
pygame.mixer.init()
|
| 33 |
+
WIDTH, HEIGHT = 800, 600
|
| 34 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 35 |
+
pygame.display.set_caption("Week 12 - 護眼大師 EyeGuard 👁️🛡️")
|
| 36 |
+
clock = pygame.time.Clock()
|
| 37 |
+
|
| 38 |
+
font = pygame.font.SysFont(None, 48)
|
| 39 |
+
small_font = pygame.font.SysFont(None, 28)
|
| 40 |
+
big_font = pygame.font.SysFont(None, 72)
|
| 41 |
+
tiny_font = pygame.font.SysFont(None, 22)
|
| 42 |
+
|
| 43 |
+
# ============================================
|
| 44 |
+
# 2. 攝影機與 AI 模組
|
| 45 |
+
# ============================================
|
| 46 |
+
cap = cv2.VideoCapture(0)
|
| 47 |
+
if not cap.isOpened():
|
| 48 |
+
print("❌ 無法開啟攝影機,請改用影片檔案")
|
| 49 |
+
pygame.quit()
|
| 50 |
+
sys.exit()
|
| 51 |
+
|
| 52 |
+
# MediaPipe 臉部偵測 (Tasks API)
|
| 53 |
+
try:
|
| 54 |
+
base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'face_landmarker.task'))
|
| 55 |
+
options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
|
| 56 |
+
detector = vision.FaceLandmarker.create_from_options(options)
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"❌ 錯誤:無法載入 MediaPipe 模型:{e}")
|
| 59 |
+
pygame.quit()
|
| 60 |
+
sys.exit()
|
| 61 |
+
|
| 62 |
+
# 關鍵 Landmark 索引
|
| 63 |
+
LEFT_EYE = 33 # 左眼外角
|
| 64 |
+
RIGHT_EYE = 263 # 右眼外角
|
| 65 |
+
|
| 66 |
+
# ============================================
|
| 67 |
+
# 3. 遊戲變數
|
| 68 |
+
# ============================================
|
| 69 |
+
eye_distance = 0
|
| 70 |
+
lx, ly = 0, 0 # 左眼座標
|
| 71 |
+
rx, ry = 0, 0 # 右眼座標
|
| 72 |
+
face_detected = False
|
| 73 |
+
|
| 74 |
+
DANGER_THRESHOLD = 120 # 兩眼像素距離超過此值 = 太近了!
|
| 75 |
+
SAFE_THRESHOLD = 100 # 低於此值 = 安全(遲滯效應,避免臨界值抖動)
|
| 76 |
+
|
| 77 |
+
# 警報狀態
|
| 78 |
+
is_too_close = False
|
| 79 |
+
was_too_close = False # 邊緣觸發
|
| 80 |
+
warning_timer = 0 # 警告閃爍計時器
|
| 81 |
+
|
| 82 |
+
# 統計數據
|
| 83 |
+
total_warnings = 0
|
| 84 |
+
safe_start_time = time.time()
|
| 85 |
+
consecutive_safe_seconds = 0
|
| 86 |
+
frame_counter = 0
|
| 87 |
+
|
| 88 |
+
# 載入音效
|
| 89 |
+
try:
|
| 90 |
+
alarm_sound = pygame.mixer.Sound("hit.wav") # 用現有的 hit.wav 代替警報音
|
| 91 |
+
print("✅ 警報音效載入成功")
|
| 92 |
+
except FileNotFoundError:
|
| 93 |
+
alarm_sound = None
|
| 94 |
+
print("⚠️ 找不到警報音效,將不播放音效")
|
| 95 |
+
|
| 96 |
+
# ============================================
|
| 97 |
+
# 4. 主迴圈
|
| 98 |
+
# ============================================
|
| 99 |
+
running = True
|
| 100 |
+
while running:
|
| 101 |
+
for event in pygame.event.get():
|
| 102 |
+
if event.type == pygame.QUIT:
|
| 103 |
+
running = False
|
| 104 |
+
|
| 105 |
+
# --- 讀取攝影機 ---
|
| 106 |
+
success, img = cap.read()
|
| 107 |
+
if not success:
|
| 108 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
img = cv2.flip(img, 1)
|
| 112 |
+
frame_counter += 1
|
| 113 |
+
|
| 114 |
+
# --- 臉部偵測與兩眼距離計算 ---
|
| 115 |
+
try:
|
| 116 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 117 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 118 |
+
results = detector.detect(mp_image)
|
| 119 |
+
|
| 120 |
+
if results.face_landmarks:
|
| 121 |
+
face_detected = True
|
| 122 |
+
face_lms = results.face_landmarks[0]
|
| 123 |
+
|
| 124 |
+
# 取得左眼和右眼座標
|
| 125 |
+
left_eye = face_lms[LEFT_EYE]
|
| 126 |
+
right_eye = face_lms[RIGHT_EYE]
|
| 127 |
+
|
| 128 |
+
lx = int(left_eye.x * WIDTH)
|
| 129 |
+
ly = int(left_eye.y * HEIGHT)
|
| 130 |
+
rx = int(right_eye.x * WIDTH)
|
| 131 |
+
ry = int(right_eye.y * HEIGHT)
|
| 132 |
+
|
| 133 |
+
# 計算兩眼之間的像素距離
|
| 134 |
+
eye_distance = math.hypot(rx - lx, ry - ly)
|
| 135 |
+
|
| 136 |
+
# 使用遲滯效應判斷(避免臨界值抖動)
|
| 137 |
+
if eye_distance > DANGER_THRESHOLD:
|
| 138 |
+
is_too_close = True
|
| 139 |
+
elif eye_distance < SAFE_THRESHOLD:
|
| 140 |
+
is_too_close = False
|
| 141 |
+
else:
|
| 142 |
+
face_detected = False
|
| 143 |
+
is_too_close = False
|
| 144 |
+
except Exception as e:
|
| 145 |
+
# 防禦性程式碼:任何異常都不應讓程式閃退
|
| 146 |
+
print(f"偵測異常:{e}")
|
| 147 |
+
face_detected = False
|
| 148 |
+
|
| 149 |
+
# --- 邊緣觸發判定 ---
|
| 150 |
+
if is_too_close and not was_too_close:
|
| 151 |
+
# 剛進入危險區
|
| 152 |
+
total_warnings += 1
|
| 153 |
+
warning_timer = 90 # 閃爍 90 幀(約 3 秒)
|
| 154 |
+
if alarm_sound:
|
| 155 |
+
alarm_sound.play()
|
| 156 |
+
safe_start_time = time.time() # 重置安全計時
|
| 157 |
+
print(f"⚠️ 第 {total_warnings} 次警告:你離螢幕太近了!(distance={eye_distance:.0f})")
|
| 158 |
+
|
| 159 |
+
if not is_too_close and was_too_close:
|
| 160 |
+
# 剛離開危險區
|
| 161 |
+
safe_start_time = time.time()
|
| 162 |
+
print("✅ 已回到安全距離")
|
| 163 |
+
|
| 164 |
+
was_too_close = is_too_close
|
| 165 |
+
|
| 166 |
+
# 計算連續安全秒數
|
| 167 |
+
if not is_too_close and face_detected:
|
| 168 |
+
consecutive_safe_seconds = time.time() - safe_start_time
|
| 169 |
+
else:
|
| 170 |
+
consecutive_safe_seconds = 0
|
| 171 |
+
|
| 172 |
+
# 閃爍倒數
|
| 173 |
+
if warning_timer > 0:
|
| 174 |
+
warning_timer -= 1
|
| 175 |
+
|
| 176 |
+
# --- 繪製畫面 ---
|
| 177 |
+
# 背景
|
| 178 |
+
if is_too_close:
|
| 179 |
+
if warning_timer > 0 and warning_timer % 6 < 3:
|
| 180 |
+
screen.fill((90, 15, 15)) # 強烈閃爍紅
|
| 181 |
+
else:
|
| 182 |
+
screen.fill((50, 12, 12)) # 暗紅
|
| 183 |
+
else:
|
| 184 |
+
screen.fill((12, 30, 18)) # 安全深綠
|
| 185 |
+
|
| 186 |
+
if face_detected:
|
| 187 |
+
# --- 距離量表 ---
|
| 188 |
+
gauge_x, gauge_y = 50, 50
|
| 189 |
+
gauge_w, gauge_h = 700, 30
|
| 190 |
+
|
| 191 |
+
# 軌道背景
|
| 192 |
+
pygame.draw.rect(screen, (40, 40, 55), (gauge_x, gauge_y, gauge_w, gauge_h), border_radius=8)
|
| 193 |
+
|
| 194 |
+
# 填充條(根據距離改變顏色與長度)
|
| 195 |
+
fill_ratio = min(eye_distance / 200, 1.0)
|
| 196 |
+
fill_w = int(fill_ratio * gauge_w)
|
| 197 |
+
if eye_distance > DANGER_THRESHOLD:
|
| 198 |
+
fill_color = (239, 68, 68) # 紅色
|
| 199 |
+
elif eye_distance > SAFE_THRESHOLD:
|
| 200 |
+
fill_color = (245, 158, 11) # 黃色
|
| 201 |
+
else:
|
| 202 |
+
fill_color = (34, 197, 94) # 綠色
|
| 203 |
+
pygame.draw.rect(screen, fill_color, (gauge_x, gauge_y, fill_w, gauge_h), border_radius=8)
|
| 204 |
+
|
| 205 |
+
# 閾值標記線
|
| 206 |
+
threshold_x = gauge_x + int(DANGER_THRESHOLD / 200 * gauge_w)
|
| 207 |
+
pygame.draw.line(screen, (255, 255, 255), (threshold_x, gauge_y - 5), (threshold_x, gauge_y + gauge_h + 5), 2)
|
| 208 |
+
threshold_label = tiny_font.render(f"DANGER: {DANGER_THRESHOLD}", True, (200, 200, 200))
|
| 209 |
+
screen.blit(threshold_label, (threshold_x - threshold_label.get_width() // 2, gauge_y + gauge_h + 8))
|
| 210 |
+
|
| 211 |
+
# 距離數值
|
| 212 |
+
dist_text = small_font.render(f"Eye Distance: {eye_distance:.0f} px", True, (200, 200, 220))
|
| 213 |
+
screen.blit(dist_text, (gauge_x, gauge_y - 28))
|
| 214 |
+
|
| 215 |
+
# --- 眼睛位置標記 ---
|
| 216 |
+
pygame.draw.circle(screen, (59, 130, 246), (lx, ly), 8)
|
| 217 |
+
pygame.draw.circle(screen, (59, 130, 246), (rx, ry), 8)
|
| 218 |
+
pygame.draw.line(screen, (59, 130, 246, 100), (lx, ly), (rx, ry), 2)
|
| 219 |
+
|
| 220 |
+
# --- 主要狀態顯示 ---
|
| 221 |
+
if is_too_close:
|
| 222 |
+
warn_text = big_font.render("TOO CLOSE!", True, (255, 80, 80))
|
| 223 |
+
screen.blit(warn_text, (WIDTH // 2 - warn_text.get_width() // 2, HEIGHT // 2 - 60))
|
| 224 |
+
|
| 225 |
+
sub_text = font.render("Please lean back!", True, (255, 150, 150))
|
| 226 |
+
screen.blit(sub_text, (WIDTH // 2 - sub_text.get_width() // 2, HEIGHT // 2 + 20))
|
| 227 |
+
else:
|
| 228 |
+
safe_text = font.render("Safe Distance", True, (80, 220, 100))
|
| 229 |
+
screen.blit(safe_text, (WIDTH // 2 - safe_text.get_width() // 2, HEIGHT // 2 - 30))
|
| 230 |
+
|
| 231 |
+
# 連續安全時間
|
| 232 |
+
if consecutive_safe_seconds > 3:
|
| 233 |
+
streak_text = small_font.render(
|
| 234 |
+
f"Good posture for {consecutive_safe_seconds:.0f}s! Keep it up!",
|
| 235 |
+
True, (100, 200, 150)
|
| 236 |
+
)
|
| 237 |
+
screen.blit(streak_text, (WIDTH // 2 - streak_text.get_width() // 2, HEIGHT // 2 + 20))
|
| 238 |
+
|
| 239 |
+
# --- 統計資訊 ---
|
| 240 |
+
stats_y = HEIGHT - 80
|
| 241 |
+
stats1 = small_font.render(f"Total Warnings: {total_warnings}", True, (150, 150, 180))
|
| 242 |
+
screen.blit(stats1, (20, stats_y))
|
| 243 |
+
|
| 244 |
+
stats2 = small_font.render(f"Frame: {frame_counter} | FPS: {clock.get_fps():.0f}", True, (100, 100, 130))
|
| 245 |
+
screen.blit(stats2, (20, stats_y + 28))
|
| 246 |
+
|
| 247 |
+
else:
|
| 248 |
+
# 沒偵測到臉
|
| 249 |
+
no_face = font.render("No face detected", True, (255, 200, 0))
|
| 250 |
+
screen.blit(no_face, (WIDTH // 2 - no_face.get_width() // 2, HEIGHT // 2 - 20))
|
| 251 |
+
|
| 252 |
+
hint = small_font.render("Please face the camera", True, (180, 180, 100))
|
| 253 |
+
screen.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT // 2 + 30))
|
| 254 |
+
|
| 255 |
+
pygame.display.update()
|
| 256 |
+
clock.tick(30)
|
| 257 |
+
|
| 258 |
+
cap.release()
|
| 259 |
+
pygame.quit()
|
| 260 |
+
sys.exit()
|
projects/2026project_class/examples/w12_eyeguard_template.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 12 - AI 生活工具箱:護眼大師 (Template 填空版)
|
| 3 |
+
=====================================================
|
| 4 |
+
本週目標:
|
| 5 |
+
1. 將 AI 視覺技術整合進一個「解決生活痛點」的完整專案
|
| 6 |
+
2. 使用 MediaPipe 臉部偵測兩眼距離,判斷是否離螢幕太近
|
| 7 |
+
3. 在 Pygame 畫面上顯示即時狀態與警報
|
| 8 |
+
4. 加入防呆機制(try-except、None 檢查)
|
| 9 |
+
|
| 10 |
+
本範例:護眼大師 EyeGuard
|
| 11 |
+
- 偵測臉部兩眼之間的距離(兩眼距離越大 = 離鏡頭越近)
|
| 12 |
+
- 超過安全閾值時發出警報文字 + 閃爍效果
|
| 13 |
+
- 適用場景:打電動、寫作業時防止坐姿太差靠太近
|
| 14 |
+
|
| 15 |
+
注意:
|
| 16 |
+
- 確保 face_landmarker.task 檔案和這個腳本放在同一個資料夾。
|
| 17 |
+
"""
|
| 18 |
+
import pygame
|
| 19 |
+
import cv2
|
| 20 |
+
import mediapipe as mp
|
| 21 |
+
from mediapipe.tasks import python
|
| 22 |
+
from mediapipe.tasks.python import vision
|
| 23 |
+
import math
|
| 24 |
+
import sys
|
| 25 |
+
import os
|
| 26 |
+
|
| 27 |
+
# ============================================
|
| 28 |
+
# 1. 初始化
|
| 29 |
+
# ============================================
|
| 30 |
+
pygame.init()
|
| 31 |
+
pygame.mixer.init()
|
| 32 |
+
WIDTH, HEIGHT = 800, 600
|
| 33 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 34 |
+
pygame.display.set_caption("Week 12 - 護眼大師 EyeGuard 👁️🛡️")
|
| 35 |
+
clock = pygame.time.Clock()
|
| 36 |
+
|
| 37 |
+
font = pygame.font.SysFont(None, 48)
|
| 38 |
+
small_font = pygame.font.SysFont(None, 28)
|
| 39 |
+
big_font = pygame.font.SysFont(None, 72)
|
| 40 |
+
|
| 41 |
+
# ============================================
|
| 42 |
+
# 2. 攝影機與 AI 模組
|
| 43 |
+
# ============================================
|
| 44 |
+
cap = cv2.VideoCapture(0)
|
| 45 |
+
# 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_face_video.mp4")
|
| 46 |
+
|
| 47 |
+
# 【TODO 1】初始化 MediaPipe 臉部偵測 (Face Landmarker)
|
| 48 |
+
# base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'face_landmarker.task'))
|
| 49 |
+
# options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
|
| 50 |
+
# detector = vision.FaceLandmarker.create_from_options(options)
|
| 51 |
+
|
| 52 |
+
# 提示:左眼的 Landmark 索引為 33,右眼為 263
|
| 53 |
+
|
| 54 |
+
# ============================================
|
| 55 |
+
# 3. 遊戲變數
|
| 56 |
+
# ============================================
|
| 57 |
+
eye_distance = 0
|
| 58 |
+
DANGER_THRESHOLD = 120 # 兩眼像素距離超過此值 = 太近了!
|
| 59 |
+
|
| 60 |
+
# 【TODO 2】警報狀態變數
|
| 61 |
+
# is_too_close = False
|
| 62 |
+
# was_too_close = False # 邊緣觸發
|
| 63 |
+
# warning_timer = 0 # 警告閃爍計時器
|
| 64 |
+
|
| 65 |
+
# 統計數據
|
| 66 |
+
total_warnings = 0
|
| 67 |
+
safe_seconds = 0
|
| 68 |
+
frame_counter = 0
|
| 69 |
+
|
| 70 |
+
# ============================================
|
| 71 |
+
# 4. 主迴圈
|
| 72 |
+
# ============================================
|
| 73 |
+
running = True
|
| 74 |
+
while running:
|
| 75 |
+
for event in pygame.event.get():
|
| 76 |
+
if event.type == pygame.QUIT:
|
| 77 |
+
running = False
|
| 78 |
+
|
| 79 |
+
# --- 讀取攝影機 ---
|
| 80 |
+
success, img = cap.read()
|
| 81 |
+
if not success:
|
| 82 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 83 |
+
continue
|
| 84 |
+
|
| 85 |
+
img = cv2.flip(img, 1)
|
| 86 |
+
frame_counter += 1
|
| 87 |
+
|
| 88 |
+
# 【TODO 3】臉部偵測與兩眼距離計算
|
| 89 |
+
# img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 90 |
+
# mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 91 |
+
# results = detector.detect(mp_image)
|
| 92 |
+
#
|
| 93 |
+
# if results.face_landmarks:
|
| 94 |
+
# face_lms = results.face_landmarks[0]
|
| 95 |
+
#
|
| 96 |
+
# # 取得左眼 (33) 和右眼 (263) 座標
|
| 97 |
+
# left_eye = face_lms[33]
|
| 98 |
+
# right_eye = face_lms[263]
|
| 99 |
+
#
|
| 100 |
+
# lx, ly = int(left_eye.x * WIDTH), int(left_eye.y * HEIGHT)
|
| 101 |
+
# rx, ry = int(right_eye.x * WIDTH), int(right_eye.y * HEIGHT)
|
| 102 |
+
#
|
| 103 |
+
# # 計算兩眼之間的像素距離
|
| 104 |
+
# eye_distance = math.hypot(rx - lx, ry - ly)
|
| 105 |
+
#
|
| 106 |
+
# # 【TODO 4】判斷是否太近
|
| 107 |
+
# # is_too_close = eye_distance > DANGER_THRESHOLD
|
| 108 |
+
#
|
| 109 |
+
# # 【TODO 5】邊緣觸發:剛進入危險區才播放警報
|
| 110 |
+
# # if is_too_close and not was_too_close:
|
| 111 |
+
# # total_warnings += 1
|
| 112 |
+
# # warning_timer = 60 # 閃爍 60 幀
|
| 113 |
+
# # print(f"⚠️ 第 {total_warnings} 次警告:你離螢幕太近了!")
|
| 114 |
+
# #
|
| 115 |
+
# # was_too_close = is_too_close
|
| 116 |
+
|
| 117 |
+
# --- 繪製畫面 ---
|
| 118 |
+
# 【TODO 6】根據狀態切換背景顏色
|
| 119 |
+
# if is_too_close:
|
| 120 |
+
# if warning_timer > 0 and warning_timer % 4 < 2:
|
| 121 |
+
# screen.fill((80, 20, 20)) # 閃爍紅色
|
| 122 |
+
# else:
|
| 123 |
+
# screen.fill((50, 15, 15)) # 暗紅色
|
| 124 |
+
# warning_timer = max(0, warning_timer - 1)
|
| 125 |
+
# else:
|
| 126 |
+
# screen.fill((15, 30, 20)) # 安全綠色
|
| 127 |
+
screen.fill((20, 20, 35)) # 預設背景
|
| 128 |
+
|
| 129 |
+
# 【TODO 7】繪製距離量表
|
| 130 |
+
# 提示:用 pygame.draw.rect 畫一個長條,長度 = eye_distance 的比例
|
| 131 |
+
# 安全時綠色,危險時紅色
|
| 132 |
+
|
| 133 |
+
# 【TODO 8】如果太近,顯示大字警告
|
| 134 |
+
# if is_too_close:
|
| 135 |
+
# warn_text = big_font.render("⚠️ TOO CLOSE!", True, (255, 80, 80))
|
| 136 |
+
# screen.blit(warn_text, (WIDTH//2 - warn_text.get_width()//2, HEIGHT//2 - 40))
|
| 137 |
+
# else:
|
| 138 |
+
# safe_text = font.render("✅ Safe Distance", True, (80, 200, 100))
|
| 139 |
+
# screen.blit(safe_text, (WIDTH//2 - safe_text.get_width()//2, HEIGHT//2 - 20))
|
| 140 |
+
|
| 141 |
+
# 顯示數值
|
| 142 |
+
info = small_font.render(f"Eye Distance: {eye_distance:.0f} px | Threshold: {DANGER_THRESHOLD}", True, (150, 150, 180))
|
| 143 |
+
screen.blit(info, (10, 10))
|
| 144 |
+
|
| 145 |
+
stats = small_font.render(f"Warnings: {total_warnings}", True, (150, 150, 180))
|
| 146 |
+
screen.blit(stats, (10, 40))
|
| 147 |
+
|
| 148 |
+
pygame.display.update()
|
| 149 |
+
clock.tick(30)
|
| 150 |
+
|
| 151 |
+
# cap.release()
|
| 152 |
+
pygame.quit()
|
| 153 |
+
sys.exit()
|
projects/2026project_class/examples/w1_1_basic_window.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pygame
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# 【支柱一】初始化
|
| 5 |
+
pygame.init()
|
| 6 |
+
WIDTH, HEIGHT = 800, 600
|
| 7 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 8 |
+
pygame.display.set_caption("我的第一個 Pygame 視窗")
|
| 9 |
+
clock = pygame.time.Clock() # 控制心跳的時鐘
|
| 10 |
+
|
| 11 |
+
my_x = 400
|
| 12 |
+
my_y = 300
|
| 13 |
+
|
| 14 |
+
# ----- 遊戲主迴圈開始 -----
|
| 15 |
+
while True:
|
| 16 |
+
|
| 17 |
+
# 【支柱二】事件監聽:收信與檢查
|
| 18 |
+
for event in pygame.event.get():
|
| 19 |
+
if event.type == pygame.QUIT: # 如果玩家按了紅色的叉叉
|
| 20 |
+
pygame.quit()
|
| 21 |
+
sys.exit() # 結束整個程式
|
| 22 |
+
|
| 23 |
+
# 【支柱三】邏輯更新 (大腦思考)
|
| 24 |
+
# 在這裡寫入更新座標的數學邏輯
|
| 25 |
+
|
| 26 |
+
# 【支柱四】畫面渲染
|
| 27 |
+
screen.fill((30, 30, 30)) # 擦乾淨黑板!填滿深灰色 (R, G, B)
|
| 28 |
+
|
| 29 |
+
# 畫一個紅色的圓形 (畫布, 顏色, 中心座標, 半徑)
|
| 30 |
+
pygame.draw.circle(screen, (255, 50, 50), (my_x, my_y), 30)
|
| 31 |
+
|
| 32 |
+
pygame.display.flip() # 翻轉畫布,呈現給玩家看
|
| 33 |
+
|
| 34 |
+
clock.tick(60) # 限制每秒最多跑 60 幀 (FPS)
|
| 35 |
+
# ----- 遊戲主迴圈結束 -----
|
projects/2026project_class/examples/w1_2_challenge_follower.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pygame
|
| 2 |
+
import sys
|
| 3 |
+
import random
|
| 4 |
+
|
| 5 |
+
# 【支柱一】初始化
|
| 6 |
+
pygame.init()
|
| 7 |
+
WIDTH, HEIGHT = 800, 600
|
| 8 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 9 |
+
pygame.display.set_caption("變色追隨者")
|
| 10 |
+
clock = pygame.time.Clock()
|
| 11 |
+
|
| 12 |
+
my_x = 400
|
| 13 |
+
my_y = 300
|
| 14 |
+
color = (255, 50, 50)
|
| 15 |
+
|
| 16 |
+
# ----- 遊戲主迴圈開始 -----
|
| 17 |
+
while True:
|
| 18 |
+
|
| 19 |
+
# 【支柱二】事件監聽
|
| 20 |
+
for event in pygame.event.get():
|
| 21 |
+
if event.type == pygame.QUIT:
|
| 22 |
+
pygame.quit()
|
| 23 |
+
sys.exit()
|
| 24 |
+
|
| 25 |
+
# 挑戰任務:偵測按鍵
|
| 26 |
+
if event.type == pygame.KEYDOWN:
|
| 27 |
+
if event.key == pygame.K_SPACE:
|
| 28 |
+
# 隨機產生新的 RGB 顏色
|
| 29 |
+
color = (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255))
|
| 30 |
+
|
| 31 |
+
# 【支柱三】邏輯更新
|
| 32 |
+
# 挑戰任務:獲取滑鼠座標
|
| 33 |
+
mouse_x, mouse_y = pygame.mouse.get_pos()
|
| 34 |
+
|
| 35 |
+
# 基本要求:直接跟隨
|
| 36 |
+
# my_x = mouse_x
|
| 37 |
+
# my_y = mouse_y
|
| 38 |
+
|
| 39 |
+
# 進階要求:平滑跟隨 (Lerp)
|
| 40 |
+
my_x = my_x + (mouse_x - my_x) / 10
|
| 41 |
+
my_y = my_y + (mouse_y - my_y) / 10
|
| 42 |
+
|
| 43 |
+
# 【支柱四】畫面渲染
|
| 44 |
+
screen.fill((30, 30, 30))
|
| 45 |
+
pygame.draw.circle(screen, color, (int(my_x), int(my_y)), 30)
|
| 46 |
+
pygame.display.flip()
|
| 47 |
+
|
| 48 |
+
clock.tick(60)
|
| 49 |
+
# ----- 遊戲主迴圈結束 -----
|
projects/2026project_class/examples/w2_1_basic_api.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
# 1. 準備好我們的目標網址 (餐廳菜單上的某一頁)
|
| 4 |
+
url = "https://api.ipify.org?format=json"
|
| 5 |
+
|
| 6 |
+
try:
|
| 7 |
+
# 2. 派出服務生 (使用 get 請求)
|
| 8 |
+
print("正在發送網路請求...")
|
| 9 |
+
# timeout=3 意思是:如果服務生等超過 3 秒廚房都沒反應,就直接放棄
|
| 10 |
+
response = requests.get(url, timeout=3)
|
| 11 |
+
|
| 12 |
+
# 檢查狀態碼是不是 200 成功
|
| 13 |
+
if response.status_code == 200:
|
| 14 |
+
# 3. 把伺服器給的一團文字,自動轉換為 Python 字典
|
| 15 |
+
data = response.json()
|
| 16 |
+
print("成功獲取資料:", data)
|
| 17 |
+
print("-------------------")
|
| 18 |
+
|
| 19 |
+
# 4. 精準提取資料
|
| 20 |
+
print("我的 IP 號碼是:", data["ip"])
|
| 21 |
+
else:
|
| 22 |
+
print(f"哎呀!伺服器回應錯誤碼:{response.status_code}")
|
| 23 |
+
|
| 24 |
+
# 如果發生網路斷線、超時等各式連線災難,通通會跌進這個 except 的緩衝墊裡:
|
| 25 |
+
except requests.exceptions.RequestException as e:
|
| 26 |
+
print("【系統警告】連線失敗!請檢查您的網路或等一下再試。")
|
projects/2026project_class/examples/w2_2_challenge_joke_api.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
# 挑戰任務:官方隨機笑話庫 API
|
| 4 |
+
url = "https://official-joke-api.appspot.com/random_joke"
|
| 5 |
+
|
| 6 |
+
print("==============================")
|
| 7 |
+
print(" 🤖 歡迎來到終端機冷笑話產生器")
|
| 8 |
+
print("==============================\n")
|
| 9 |
+
|
| 10 |
+
while True:
|
| 11 |
+
user_input = input("按 Enter 取得下一個笑話 (輸入 q 退出):")
|
| 12 |
+
|
| 13 |
+
if user_input.lower() == 'q':
|
| 14 |
+
print("掰掰!")
|
| 15 |
+
break
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
# 發送 GET 請求,設定超時以防卡死
|
| 19 |
+
response = requests.get(url, timeout=3)
|
| 20 |
+
|
| 21 |
+
if response.status_code == 200:
|
| 22 |
+
# 成功取得資料,解析 JSON
|
| 23 |
+
data = response.json()
|
| 24 |
+
|
| 25 |
+
# 從 JSON 中萃取我們需要的欄位
|
| 26 |
+
setup = data["setup"]
|
| 27 |
+
punchline = data["punchline"]
|
| 28 |
+
|
| 29 |
+
print("\n------------------------------")
|
| 30 |
+
print(f"🤔 鋪陳: {setup}")
|
| 31 |
+
print(f"🤣 笑點: {punchline}")
|
| 32 |
+
print("------------------------------\n")
|
| 33 |
+
|
| 34 |
+
else:
|
| 35 |
+
print(f"\n伺服器不太舒服 (錯誤碼:{response.status_code})\n")
|
| 36 |
+
|
| 37 |
+
except requests.exceptions.Timeout:
|
| 38 |
+
print("\n⏳ 網路連線太慢了,服務生等不及了 (Timeout)\n")
|
| 39 |
+
except requests.exceptions.ConnectionError:
|
| 40 |
+
print("\n🔌 網路斷線!請檢查您的 Wi-Fi 是否正常連線。\n")
|
| 41 |
+
except Exception as e:
|
| 42 |
+
print(f"\n⚠️ 發生未知的錯誤:{e}\n")
|
projects/2026project_class/examples/w4_dashboard_template.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pygame, sys, requests, math, random
|
| 2 |
+
|
| 3 |
+
pygame.init()
|
| 4 |
+
|
| 5 |
+
# ===== 全域常數與設定 =====
|
| 6 |
+
WIDTH, HEIGHT = 900, 650
|
| 7 |
+
NEON_BLUE = (0, 210, 255)
|
| 8 |
+
NEON_GREEN = (0, 255, 128)
|
| 9 |
+
NEON_ORANGE = (255, 150, 50)
|
| 10 |
+
NEON_PINK = (255, 80, 180)
|
| 11 |
+
TEXT_WHITE = (245, 245, 255)
|
| 12 |
+
TEXT_DIM = (160, 160, 180)
|
| 13 |
+
|
| 14 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 15 |
+
pygame.display.set_caption("數位助理儀表板 天氣版 (學生用模板)")
|
| 16 |
+
clock = pygame.time.Clock()
|
| 17 |
+
|
| 18 |
+
font_huge = pygame.font.SysFont("microsoftjhenghei", 76, bold=True)
|
| 19 |
+
font_big = pygame.font.SysFont("microsoftjhenghei", 38, bold=True)
|
| 20 |
+
font_mid = pygame.font.SysFont("microsoftjhenghei", 26, bold=True)
|
| 21 |
+
font_small = pygame.font.SysFont("microsoftjhenghei", 20)
|
| 22 |
+
|
| 23 |
+
# ===== TODO 0:客製化你的城市清單 =====
|
| 24 |
+
# 請隨意保留或更改你想查詢的城市名稱。
|
| 25 |
+
# 經緯度怎麼查?打開 Google Maps,對著你想查的地點按「滑鼠右鍵」,第一行出現的數字就是經緯度,點擊即可複製!
|
| 26 |
+
CITIES = {
|
| 27 |
+
"台北": {"lat": 25.04, "lon": 121.56},
|
| 28 |
+
"你的夢想城市": {"lat": 0.00, "lon": 0.00}, # <- 查好後,把 0.00 換成真實的緯度與經度
|
| 29 |
+
"最想去玩的地方": {"lat": 0.00, "lon": 0.00},
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
# ===== UI 繪圖輔助函式 (已經為你寫好啦!) =====
|
| 33 |
+
def draw_gradient_bg(surface, top_color, bottom_color):
|
| 34 |
+
rect = surface.get_rect()
|
| 35 |
+
color_surface = pygame.Surface((1, 2))
|
| 36 |
+
pygame.draw.line(color_surface, top_color, (0,0), (0,0))
|
| 37 |
+
pygame.draw.line(color_surface, bottom_color, (0,1), (0,1))
|
| 38 |
+
color_surface = pygame.transform.smoothscale(color_surface, (rect.width, rect.height))
|
| 39 |
+
surface.blit(color_surface, rect)
|
| 40 |
+
|
| 41 |
+
def draw_glass_card(surface, x, y, w, h, radius=18):
|
| 42 |
+
card = pygame.Surface((w, h), pygame.SRCALPHA)
|
| 43 |
+
pygame.draw.rect(card, (30, 35, 55, 220), (0, 0, w, h), border_radius=radius)
|
| 44 |
+
pygame.draw.rect(card, (255, 255, 255, 40), (0, 0, w, h), width=2, border_radius=radius)
|
| 45 |
+
surface.blit(card, (x, y))
|
| 46 |
+
|
| 47 |
+
def draw_glowing_circle(surface, color, center, radius):
|
| 48 |
+
for i in range(radius * 4, radius, -2):
|
| 49 |
+
alpha = int(120 * (radius / i))
|
| 50 |
+
glow = pygame.Surface((i*2, i*2), pygame.SRCALPHA)
|
| 51 |
+
pygame.draw.circle(glow, (*color, alpha), (i, i), i)
|
| 52 |
+
surface.blit(glow, (center[0]-i, center[1]-i))
|
| 53 |
+
pygame.draw.circle(surface, color, center, radius)
|
| 54 |
+
|
| 55 |
+
def draw_glowing_rect(surface, color, rect, radius):
|
| 56 |
+
for i in range(15, 0, -3):
|
| 57 |
+
alpha = int(80 * (1 - i/15))
|
| 58 |
+
glow = pygame.Surface((rect.width + i*2, rect.height + i*2), pygame.SRCALPHA)
|
| 59 |
+
pygame.draw.rect(glow, (*color, alpha), (0, 0, rect.width + i*2, rect.height + i*2), border_radius=radius+i)
|
| 60 |
+
surface.blit(glow, (rect.x - i, rect.y - i))
|
| 61 |
+
|
| 62 |
+
class NinjaButton:
|
| 63 |
+
def __init__(self, x, y, text, is_real=True):
|
| 64 |
+
self.w, self.h = 160, 45
|
| 65 |
+
self.rect = pygame.Rect(x, y, self.w, self.h)
|
| 66 |
+
self.text, self.is_real, self.visible, self.hovered = text, is_real, True, False
|
| 67 |
+
|
| 68 |
+
def draw(self, surface):
|
| 69 |
+
if not self.visible: return
|
| 70 |
+
color = NEON_PINK if self.hovered else (40, 50, 80)
|
| 71 |
+
if self.hovered: draw_glowing_rect(surface, NEON_PINK, self.rect, 12)
|
| 72 |
+
pygame.draw.rect(surface, color, self.rect, border_radius=12)
|
| 73 |
+
pygame.draw.rect(surface, TEXT_WHITE, self.rect, width=2, border_radius=12)
|
| 74 |
+
text_surf = font_small.render(self.text, True, TEXT_WHITE)
|
| 75 |
+
surface.blit(text_surf, text_surf.get_rect(center=self.rect.center))
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# ==========================================
|
| 79 |
+
# ===== 🛠️ 以下是你的實作區域 (TODOs) 🛠️ =====
|
| 80 |
+
# ==========================================
|
| 81 |
+
|
| 82 |
+
def fetch_weather(city_name="台北"):
|
| 83 |
+
"""抓取指定即時天氣"""
|
| 84 |
+
coords = CITIES.get(city_name, CITIES["台北"])
|
| 85 |
+
try:
|
| 86 |
+
# 【TODO 1-1】請使用 coords['lat'] 與 coords['lon'] 組合出正確的 Open-Meteo API 網址 (需加上 &timezone=Asia%2FTaipei)
|
| 87 |
+
url = "" # <- 替換為正確的字串
|
| 88 |
+
|
| 89 |
+
# 【TODO 1-2】使用 requests.get 發送請求,並設定 timeout=5
|
| 90 |
+
# r = ...
|
| 91 |
+
|
| 92 |
+
# 假設已經成功抓到了 (以下需要你把註解打開並完成填空)
|
| 93 |
+
# if r.status_code == 200:
|
| 94 |
+
# data = r.json()
|
| 95 |
+
#
|
| 96 |
+
# # 【TODO 1-3】從 data 中安全地取出 'current_weather' 的字典
|
| 97 |
+
# curr = {}
|
| 98 |
+
#
|
| 99 |
+
# return {
|
| 100 |
+
# "city": city_name,
|
| 101 |
+
# # 【TODO 1-4】從 curr 字典中分別抓出 'temperature', 'windspeed', 'time' 的數值 (用 get 比較安全)
|
| 102 |
+
# "temp": f"??°C", # <- 替換 ??
|
| 103 |
+
# "wind": f"?? km/h", # <- 替換 ??
|
| 104 |
+
# "time": "??".replace("T", " "), # <- 替換 ??
|
| 105 |
+
# "ok": True
|
| 106 |
+
# }
|
| 107 |
+
pass
|
| 108 |
+
except:
|
| 109 |
+
pass
|
| 110 |
+
return {"city": city_name, "temp": "N/A", "wind": "N/A", "time": "無法連線", "ok": False}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def fetch_ip():
|
| 114 |
+
"""抓取目前的 IP 位址"""
|
| 115 |
+
try:
|
| 116 |
+
# 【TODO 2】使用 requests 抓取 "https://api.ipify.org?format=json" 的資料
|
| 117 |
+
# 並從回傳的 JSON 字典中取出 "ip" 這個欄位的字串回傳
|
| 118 |
+
pass # <- 替換為你的程式碼
|
| 119 |
+
except:
|
| 120 |
+
pass
|
| 121 |
+
return "127.0.0.1 (Offline)"
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# 初始抓取(程式剛打開時的第一次呼叫)
|
| 125 |
+
weather = fetch_weather()
|
| 126 |
+
my_ip = fetch_ip()
|
| 127 |
+
|
| 128 |
+
# 跑馬燈與狀態初始化
|
| 129 |
+
marquee = font_mid.render("快點擊更新按鈕試試看影分身術吧!", True, NEON_ORANGE)
|
| 130 |
+
text_width = marquee.get_width()
|
| 131 |
+
scroll_x = WIDTH
|
| 132 |
+
|
| 133 |
+
main_btn = NinjaButton(340, 280, "點我更新")
|
| 134 |
+
ninja_buttons = [main_btn]
|
| 135 |
+
ninja_mode = False
|
| 136 |
+
|
| 137 |
+
# ===== 遊戲主迴圈 =====
|
| 138 |
+
while True:
|
| 139 |
+
mouse_pos = pygame.mouse.get_pos()
|
| 140 |
+
for btn in ninja_buttons:
|
| 141 |
+
btn.hovered = btn.rect.collidepoint(mouse_pos)
|
| 142 |
+
|
| 143 |
+
for event in pygame.event.get():
|
| 144 |
+
|
| 145 |
+
# 【TODO 3-1:關閉視窗】如果收到 pygame.QUIT 事件,請結束程式
|
| 146 |
+
pass # <- 替換為你的程式碼
|
| 147 |
+
|
| 148 |
+
# 【TODO 3-2:滑鼠點擊】如果收到滑鼠點擊事件 (pygame.MOUSEBUTTONDOWN) 且按鍵是左鍵 (event.button == 1)
|
| 149 |
+
if False: # <- 將 False 替換為正確的 if 判斷式
|
| 150 |
+
for btn in reversed(ninja_buttons):
|
| 151 |
+
if btn.visible and btn.hovered:
|
| 152 |
+
if not ninja_mode:
|
| 153 |
+
ninja_mode = True
|
| 154 |
+
ninja_buttons.clear()
|
| 155 |
+
positions = [(random.randint(50, WIDTH - 200), random.randint(120, HEIGHT - 150)) for _ in range(6)]
|
| 156 |
+
real_idx = random.randint(0, 5)
|
| 157 |
+
for i, pos in enumerate(positions):
|
| 158 |
+
ninja_buttons.append(NinjaButton(pos[0], pos[1], "點我更新", is_real=(i == real_idx)))
|
| 159 |
+
marquee = font_mid.render("警告:按鈕已發動影分身術!找出真正的按鈕才能更新天氣!", True, NEON_PINK)
|
| 160 |
+
text_width = marquee.get_width()
|
| 161 |
+
scroll_x = WIDTH
|
| 162 |
+
else:
|
| 163 |
+
if btn.is_real:
|
| 164 |
+
# 【TODO 4:打贏分身術的獎勵】玩家點到真正的按鈕了!請實作以下功能:
|
| 165 |
+
# 1. 用 random.choice() 與 list(CITIES.keys()) 抽出一個隨機城市,存入變數 next_city
|
| 166 |
+
# 2. 呼叫 fetch_weather(next_city) 重新取得天氣資料,並寫入 weather 全域變數中
|
| 167 |
+
|
| 168 |
+
next_city = "台北" # <- 修改這行
|
| 169 |
+
|
| 170 |
+
ninja_mode = False
|
| 171 |
+
ninja_buttons = [main_btn]
|
| 172 |
+
main_btn.visible = True
|
| 173 |
+
marquee = font_mid.render(f"恭喜!成功識破影分身術,已為您切換至【{next_city}】的天氣!", True, NEON_GREEN)
|
| 174 |
+
text_width = marquee.get_width()
|
| 175 |
+
scroll_x = WIDTH
|
| 176 |
+
else:
|
| 177 |
+
btn.visible = False
|
| 178 |
+
break
|
| 179 |
+
|
| 180 |
+
scroll_x -= 3
|
| 181 |
+
if scroll_x < -text_width: scroll_x = WIDTH
|
| 182 |
+
|
| 183 |
+
draw_gradient_bg(screen, (9, 13, 30), (28, 15, 45))
|
| 184 |
+
float_y = math.sin(pygame.time.get_ticks() / 350.0) * 6
|
| 185 |
+
|
| 186 |
+
# 標題
|
| 187 |
+
title = font_big.render("我的天氣助理儀表板", True, NEON_BLUE)
|
| 188 |
+
screen.blit(title, (40, 30))
|
| 189 |
+
pygame.draw.line(screen, (255, 255, 255, 80), (40, 80), (WIDTH - 40, 80), 2)
|
| 190 |
+
|
| 191 |
+
# 天氣面板
|
| 192 |
+
# 【TODO 5:繪製卡片背景】請呼叫 draw_glass_card(screen, x, y, w, h),將天氣面板畫在 x=40, y=110,寬長 480x240 的位置
|
| 193 |
+
|
| 194 |
+
icon = font_mid.render(f"{weather.get('city', '台北')}即時天氣", True, NEON_ORANGE)
|
| 195 |
+
screen.blit(icon, (65, 135 + float_y))
|
| 196 |
+
|
| 197 |
+
# 【TODO 6:畫面字體渲染與蓋印】請用 font_huge 字體,將 weather['temp'] 渲染為白色 (TEXT_WHITE) 文字
|
| 198 |
+
# 並將產出的文字圖片繪製 (blit) 到畫面座標 (65, 185) 處
|
| 199 |
+
# temp_text = ...
|
| 200 |
+
# screen.blit(...)
|
| 201 |
+
|
| 202 |
+
wind = font_small.render(f"風速:{weather['wind']}", True, NEON_BLUE)
|
| 203 |
+
time_lbl = font_small.render(f"更新於:{weather['time']}", True, TEXT_DIM)
|
| 204 |
+
screen.blit(wind, (65, 275))
|
| 205 |
+
screen.blit(time_lbl, (65, 305))
|
| 206 |
+
|
| 207 |
+
# IP 面板
|
| 208 |
+
# 【TODO 5-2:繪製卡片背景】同樣地,把 IP 面板畫在 x=40, y=380,大小 480x120 的位置
|
| 209 |
+
|
| 210 |
+
ip_title = font_mid.render("你的網路位置 (IP)", True, NEON_PINK)
|
| 211 |
+
ip_text = font_big.render(my_ip, True, TEXT_WHITE)
|
| 212 |
+
screen.blit(ip_title, (65, 400))
|
| 213 |
+
screen.blit(ip_text, (65, 440))
|
| 214 |
+
|
| 215 |
+
# 狀態面板 (這張卡片先幫你寫好當參考)
|
| 216 |
+
draw_glass_card(screen, 550, 110, 310, 390)
|
| 217 |
+
sys_title = font_mid.render("系統狀態", True, TEXT_WHITE)
|
| 218 |
+
screen.blit(sys_title, (580, 135))
|
| 219 |
+
|
| 220 |
+
status_color = NEON_GREEN if weather["ok"] else (255, 50, 50)
|
| 221 |
+
draw_glowing_circle(screen, status_color, (600, 210), 4)
|
| 222 |
+
status_msg = "天氣 API 運作正常" if weather["ok"] else "天氣 API 離線中"
|
| 223 |
+
screen.blit(font_small.render(status_msg, True, TEXT_WHITE), (625, 198))
|
| 224 |
+
|
| 225 |
+
draw_glowing_circle(screen, NEON_GREEN, (600, 260), 4)
|
| 226 |
+
screen.blit(font_small.render("網際網路連線正常", True, TEXT_WHITE), (625, 248))
|
| 227 |
+
|
| 228 |
+
for btn in ninja_buttons:
|
| 229 |
+
btn.draw(screen)
|
| 230 |
+
|
| 231 |
+
# 跑馬燈
|
| 232 |
+
footer = pygame.Surface((WIDTH, 50), pygame.SRCALPHA)
|
| 233 |
+
pygame.draw.rect(footer, (0, 0, 0, 180), (0, 0, WIDTH, 50))
|
| 234 |
+
screen.blit(footer, (0, HEIGHT - 50))
|
| 235 |
+
screen.blit(marquee, (scroll_x, HEIGHT - 38))
|
| 236 |
+
|
| 237 |
+
pygame.display.flip()
|
| 238 |
+
clock.tick(60)
|
projects/2026project_class/examples/w4_dashboard_weather.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pygame, sys, requests, math, random
|
| 2 |
+
|
| 3 |
+
pygame.init()
|
| 4 |
+
|
| 5 |
+
# ===== 全域常數 =====
|
| 6 |
+
WIDTH, HEIGHT = 900, 650
|
| 7 |
+
NEON_BLUE = (0, 210, 255)
|
| 8 |
+
NEON_GREEN = (0, 255, 128)
|
| 9 |
+
NEON_ORANGE = (255, 150, 50)
|
| 10 |
+
NEON_PINK = (255, 80, 180)
|
| 11 |
+
TEXT_WHITE = (245, 245, 255)
|
| 12 |
+
TEXT_DIM = (160, 160, 180)
|
| 13 |
+
|
| 14 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 15 |
+
pygame.display.set_caption("數位助理儀表板 天氣版")
|
| 16 |
+
clock = pygame.time.Clock()
|
| 17 |
+
|
| 18 |
+
# 字體設定(使用加粗提升質感)
|
| 19 |
+
font_huge = pygame.font.SysFont("microsoftjhenghei", 76, bold=True)
|
| 20 |
+
font_big = pygame.font.SysFont("microsoftjhenghei", 38, bold=True)
|
| 21 |
+
font_mid = pygame.font.SysFont("microsoftjhenghei", 26, bold=True)
|
| 22 |
+
font_small = pygame.font.SysFont("microsoftjhenghei", 20)
|
| 23 |
+
|
| 24 |
+
def draw_gradient_bg(surface, top_color, bottom_color):
|
| 25 |
+
"""繪製垂直漸層背景"""
|
| 26 |
+
rect = surface.get_rect()
|
| 27 |
+
color_surface = pygame.Surface((1, 2))
|
| 28 |
+
pygame.draw.line(color_surface, top_color, (0,0), (0,0))
|
| 29 |
+
pygame.draw.line(color_surface, bottom_color, (0,1), (0,1))
|
| 30 |
+
color_surface = pygame.transform.smoothscale(color_surface, (rect.width, rect.height))
|
| 31 |
+
surface.blit(color_surface, rect)
|
| 32 |
+
|
| 33 |
+
def draw_glass_card(surface, x, y, w, h, radius=18):
|
| 34 |
+
"""繪製具備毛玻璃質感的半透明卡片"""
|
| 35 |
+
card = pygame.Surface((w, h), pygame.SRCALPHA)
|
| 36 |
+
pygame.draw.rect(card, (30, 35, 55, 220), (0, 0, w, h), border_radius=radius)
|
| 37 |
+
pygame.draw.rect(card, (255, 255, 255, 40), (0, 0, w, h), width=2, border_radius=radius)
|
| 38 |
+
surface.blit(card, (x, y))
|
| 39 |
+
|
| 40 |
+
def draw_glowing_circle(surface, color, center, radius):
|
| 41 |
+
"""繪製會發光的霓虹圓點"""
|
| 42 |
+
for i in range(radius * 4, radius, -2):
|
| 43 |
+
alpha = int(120 * (radius / i))
|
| 44 |
+
glow = pygame.Surface((i*2, i*2), pygame.SRCALPHA)
|
| 45 |
+
pygame.draw.circle(glow, (*color, alpha), (i, i), i)
|
| 46 |
+
surface.blit(glow, (center[0]-i, center[1]-i))
|
| 47 |
+
pygame.draw.circle(surface, color, center, radius)
|
| 48 |
+
|
| 49 |
+
def draw_glowing_rect(surface, color, rect, radius):
|
| 50 |
+
"""繪製發光矩形"""
|
| 51 |
+
for i in range(15, 0, -3):
|
| 52 |
+
alpha = int(80 * (1 - i/15))
|
| 53 |
+
glow = pygame.Surface((rect.width + i*2, rect.height + i*2), pygame.SRCALPHA)
|
| 54 |
+
pygame.draw.rect(glow, (*color, alpha), (0, 0, rect.width + i*2, rect.height + i*2), border_radius=radius+i)
|
| 55 |
+
surface.blit(glow, (rect.x - i, rect.y - i))
|
| 56 |
+
|
| 57 |
+
class NinjaButton:
|
| 58 |
+
"""影分身按鈕類別"""
|
| 59 |
+
def __init__(self, x, y, text, is_real=True):
|
| 60 |
+
self.w = 160
|
| 61 |
+
self.h = 45
|
| 62 |
+
self.rect = pygame.Rect(x, y, self.w, self.h)
|
| 63 |
+
self.text = text
|
| 64 |
+
self.is_real = is_real
|
| 65 |
+
self.visible = True
|
| 66 |
+
self.hovered = False
|
| 67 |
+
|
| 68 |
+
def draw(self, surface):
|
| 69 |
+
if not self.visible: return
|
| 70 |
+
|
| 71 |
+
color = NEON_PINK if self.hovered else (40, 50, 80)
|
| 72 |
+
|
| 73 |
+
if self.hovered:
|
| 74 |
+
draw_glowing_rect(surface, NEON_PINK, self.rect, 12)
|
| 75 |
+
|
| 76 |
+
pygame.draw.rect(surface, color, self.rect, border_radius=12)
|
| 77 |
+
pygame.draw.rect(surface, TEXT_WHITE, self.rect, width=2, border_radius=12)
|
| 78 |
+
|
| 79 |
+
text_surf = font_small.render(self.text, True, TEXT_WHITE)
|
| 80 |
+
text_rect = text_surf.get_rect(center=self.rect.center)
|
| 81 |
+
surface.blit(text_surf, text_rect)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# 城市座標字典
|
| 85 |
+
CITIES = {
|
| 86 |
+
"台北": {"lat": 25.04, "lon": 121.56},
|
| 87 |
+
"東京": {"lat": 35.68, "lon": 139.75},
|
| 88 |
+
"倫敦": {"lat": 51.51, "lon": -0.13},
|
| 89 |
+
"雪梨": {"lat": -33.87, "lon": 151.21},
|
| 90 |
+
"紐約": {"lat": 40.71, "lon": -74.01}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
def fetch_weather(city_name="台北"):
|
| 94 |
+
"""抓取指定即時天氣"""
|
| 95 |
+
coords = CITIES.get(city_name, CITIES["台北"])
|
| 96 |
+
try:
|
| 97 |
+
r = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={coords['lat']}&longitude={coords['lon']}¤t_weather=true&timezone=Asia%2FTaipei", timeout=5)
|
| 98 |
+
if r.status_code == 200:
|
| 99 |
+
curr = r.json().get("current_weather", {})
|
| 100 |
+
return {
|
| 101 |
+
"city": city_name,
|
| 102 |
+
"temp": f"{curr.get('temperature', '--')}°C",
|
| 103 |
+
"wind": f"{curr.get('windspeed', '--')} km/h",
|
| 104 |
+
"time": curr.get("time", "").replace("T", " "),
|
| 105 |
+
"ok": True
|
| 106 |
+
}
|
| 107 |
+
except: ...
|
| 108 |
+
return {"city": city_name, "temp": "N/A", "wind": "N/A", "time": "無法連線", "ok": False}
|
| 109 |
+
|
| 110 |
+
def fetch_ip():
|
| 111 |
+
try:
|
| 112 |
+
r = requests.get("https://api.ipify.org?format=json", timeout=3)
|
| 113 |
+
if r.status_code == 200: return r.json()["ip"]
|
| 114 |
+
except: ...
|
| 115 |
+
return "127.0.0.1 (Offline)"
|
| 116 |
+
|
| 117 |
+
weather = fetch_weather()
|
| 118 |
+
my_ip = fetch_ip()
|
| 119 |
+
|
| 120 |
+
marquee = font_mid.render("最新消息:Pygame 好好玩!快點擊更新按鈕試試看影分身術吧!", True, NEON_ORANGE)
|
| 121 |
+
text_width = marquee.get_width()
|
| 122 |
+
scroll_x = WIDTH
|
| 123 |
+
|
| 124 |
+
# --- 忍者按鈕初始化 ---
|
| 125 |
+
main_btn = NinjaButton(340, 280, "點我更新")
|
| 126 |
+
ninja_buttons = [main_btn]
|
| 127 |
+
ninja_mode = False
|
| 128 |
+
|
| 129 |
+
while True:
|
| 130 |
+
mouse_pos = pygame.mouse.get_pos()
|
| 131 |
+
for btn in ninja_buttons:
|
| 132 |
+
btn.hovered = btn.rect.collidepoint(mouse_pos)
|
| 133 |
+
|
| 134 |
+
for event in pygame.event.get():
|
| 135 |
+
if event.type == pygame.QUIT:
|
| 136 |
+
pygame.quit()
|
| 137 |
+
sys.exit()
|
| 138 |
+
|
| 139 |
+
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
| 140 |
+
for btn in reversed(ninja_buttons):
|
| 141 |
+
if btn.visible and btn.hovered:
|
| 142 |
+
if not ninja_mode:
|
| 143 |
+
# 觸發影分身
|
| 144 |
+
ninja_mode = True
|
| 145 |
+
ninja_buttons.clear()
|
| 146 |
+
# 產生 6 個分身按鈕,散佈在畫面各處
|
| 147 |
+
positions = []
|
| 148 |
+
for _ in range(6):
|
| 149 |
+
rx = random.randint(50, WIDTH - 200)
|
| 150 |
+
ry = random.randint(120, HEIGHT - 150)
|
| 151 |
+
positions.append((rx, ry))
|
| 152 |
+
|
| 153 |
+
real_idx = random.randint(0, 5)
|
| 154 |
+
for i, pos in enumerate(positions):
|
| 155 |
+
ninja_buttons.append(NinjaButton(pos[0], pos[1], "點我更新", is_real=(i == real_idx)))
|
| 156 |
+
# 改變跑馬燈文字
|
| 157 |
+
marquee = font_mid.render("警告:按鈕已發動影分身術!找出真正的按鈕才能更新天氣!", True, NEON_PINK)
|
| 158 |
+
text_width = marquee.get_width()
|
| 159 |
+
scroll_x = WIDTH
|
| 160 |
+
else:
|
| 161 |
+
if btn.is_real:
|
| 162 |
+
# 點到本尊,隨機選一個新城市
|
| 163 |
+
next_city = random.choice(list(CITIES.keys()))
|
| 164 |
+
weather = fetch_weather(next_city)
|
| 165 |
+
ninja_mode = False
|
| 166 |
+
ninja_buttons = [main_btn]
|
| 167 |
+
main_btn.visible = True
|
| 168 |
+
marquee = font_mid.render(f"恭喜!成功識破影分身術,已為您切換至【{next_city}】的天氣!", True, NEON_GREEN)
|
| 169 |
+
text_width = marquee.get_width()
|
| 170 |
+
scroll_x = WIDTH
|
| 171 |
+
else:
|
| 172 |
+
# 點到假身,假身消失
|
| 173 |
+
btn.visible = False
|
| 174 |
+
break
|
| 175 |
+
|
| 176 |
+
scroll_x -= 3
|
| 177 |
+
if scroll_x < -text_width: scroll_x = WIDTH
|
| 178 |
+
|
| 179 |
+
# 1. 繪製漸層背景
|
| 180 |
+
draw_gradient_bg(screen, (9, 13, 30), (28, 15, 45))
|
| 181 |
+
|
| 182 |
+
float_y = math.sin(pygame.time.get_ticks() / 350.0) * 6
|
| 183 |
+
|
| 184 |
+
# 2. 標題列
|
| 185 |
+
title = font_big.render("我的天氣助理儀表板", True, NEON_BLUE)
|
| 186 |
+
screen.blit(title, (40, 30))
|
| 187 |
+
pygame.draw.line(screen, (255, 255, 255, 80), (40, 80), (WIDTH - 40, 80), 2)
|
| 188 |
+
|
| 189 |
+
# 3. 天氣面板 (不含按鈕了)
|
| 190 |
+
draw_glass_card(screen, 40, 110, 480, 240)
|
| 191 |
+
icon = font_mid.render(f"{weather.get('city', '台北')}即時天氣", True, NEON_ORANGE)
|
| 192 |
+
screen.blit(icon, (65, 135 + float_y))
|
| 193 |
+
temp_text = font_huge.render(weather['temp'], True, TEXT_WHITE)
|
| 194 |
+
screen.blit(temp_text, (65, 185))
|
| 195 |
+
wind = font_small.render(f"風速:{weather['wind']}", True, NEON_BLUE)
|
| 196 |
+
time_lbl = font_small.render(f"更新於:{weather['time']}", True, TEXT_DIM)
|
| 197 |
+
screen.blit(wind, (65, 275))
|
| 198 |
+
screen.blit(time_lbl, (65, 305))
|
| 199 |
+
|
| 200 |
+
# 4. IP 面板
|
| 201 |
+
draw_glass_card(screen, 40, 380, 480, 120)
|
| 202 |
+
ip_title = font_mid.render("你的網路位置 (IP)", True, NEON_PINK)
|
| 203 |
+
ip_text = font_big.render(my_ip, True, TEXT_WHITE)
|
| 204 |
+
screen.blit(ip_title, (65, 400))
|
| 205 |
+
screen.blit(ip_text, (65, 440))
|
| 206 |
+
|
| 207 |
+
# 5. 右側狀態面板
|
| 208 |
+
draw_glass_card(screen, 550, 110, 310, 390)
|
| 209 |
+
sys_title = font_mid.render("系統狀態", True, TEXT_WHITE)
|
| 210 |
+
screen.blit(sys_title, (580, 135))
|
| 211 |
+
|
| 212 |
+
status_color = NEON_GREEN if weather["ok"] else (255, 50, 50)
|
| 213 |
+
draw_glowing_circle(screen, status_color, (600, 210), 4)
|
| 214 |
+
status_msg = "天氣 API 運作正常" if weather["ok"] else "天氣 API 離線中"
|
| 215 |
+
status_txt = font_small.render(status_msg, True, TEXT_WHITE)
|
| 216 |
+
screen.blit(status_txt, (625, 198))
|
| 217 |
+
|
| 218 |
+
draw_glowing_circle(screen, NEON_GREEN, (600, 260), 4)
|
| 219 |
+
net_txt = font_small.render("網際網路連線正常", True, TEXT_WHITE)
|
| 220 |
+
screen.blit(net_txt, (625, 248))
|
| 221 |
+
|
| 222 |
+
# 畫出所有的忍者按鈕(注意要確保畫在卡片上方,所以放最後)
|
| 223 |
+
for btn in ninja_buttons:
|
| 224 |
+
btn.draw(screen)
|
| 225 |
+
|
| 226 |
+
# 6. 底部跑馬燈
|
| 227 |
+
footer = pygame.Surface((WIDTH, 50), pygame.SRCALPHA)
|
| 228 |
+
pygame.draw.rect(footer, (0, 0, 0, 180), (0, 0, WIDTH, 50))
|
| 229 |
+
screen.blit(footer, (0, HEIGHT - 50))
|
| 230 |
+
screen.blit(marquee, (scroll_x, HEIGHT - 38))
|
| 231 |
+
|
| 232 |
+
pygame.display.flip()
|
| 233 |
+
clock.tick(60)
|
projects/2026project_class/examples/w5_udp_chat.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import socket
|
| 2 |
+
import threading
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
# 1. 建立 UDP Socket 並綁定大門
|
| 6 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 7 |
+
try:
|
| 8 |
+
sock.bind(("0.0.0.0", 5000))
|
| 9 |
+
except OSError:
|
| 10 |
+
print("Port 5000 已經被佔用,請確定之前的接收端是否已關閉!")
|
| 11 |
+
sys.exit(1)
|
| 12 |
+
|
| 13 |
+
# 2. 定義「背景收信」的工作內容
|
| 14 |
+
def receive_messages():
|
| 15 |
+
while True:
|
| 16 |
+
try:
|
| 17 |
+
data, addr = sock.recvfrom(1024)
|
| 18 |
+
print(f"\n[收到來自 {addr[0]}]:{data.decode('utf-8')}", end="\n> ")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
# Socket 關閉或錯誤時退出迴圈
|
| 21 |
+
break
|
| 22 |
+
|
| 23 |
+
# 召喚小精靈去背景執行 (與主程式分工合作)
|
| 24 |
+
threading.Thread(target=receive_messages, daemon=True).start()
|
| 25 |
+
|
| 26 |
+
# 3. 前景發信主程式
|
| 27 |
+
print("聊天室已啟動!自己的接收端運作於本機的 Port 5000")
|
| 28 |
+
target_ip = input("請輸入對方 IP (直接按 Enter 可預設 127.0.0.1 測試): ").strip()
|
| 29 |
+
if not target_ip:
|
| 30 |
+
target_ip = "127.0.0.1"
|
| 31 |
+
|
| 32 |
+
print(f"準備傳送訊息給 {target_ip}:5000 (輸入 'exit' 可離開程式)")
|
| 33 |
+
|
| 34 |
+
while True:
|
| 35 |
+
try:
|
| 36 |
+
msg = input("> ") # 這裡會卡住等你打字,但背景的 recvfrom() 依然在辛勤運作!
|
| 37 |
+
if msg.strip().lower() == "exit":
|
| 38 |
+
break
|
| 39 |
+
if msg.strip() != "":
|
| 40 |
+
sock.sendto(msg.encode('utf-8'), (target_ip, 5000))
|
| 41 |
+
except KeyboardInterrupt:
|
| 42 |
+
break
|
| 43 |
+
except OSError as e:
|
| 44 |
+
print("【系統】網路傳輸失敗,請檢查 Wi-Fi 是否連線,或 IP 格式是否正確!例外細節:", e)
|
| 45 |
+
|
| 46 |
+
# 離開時關閉 socket
|
| 47 |
+
sock.close()
|
| 48 |
+
print("已離開聊天室。")
|
projects/2026project_class/examples/w5_udp_receiver.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import socket
|
| 2 |
+
|
| 3 |
+
# 建立 UDP Socket 水管
|
| 4 |
+
# AF_INET = IPv4 位址格式, SOCK_DGRAM = UDP 模式 (Datagram)
|
| 5 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 6 |
+
|
| 7 |
+
# "0.0.0.0" 代表此台電腦的任何網路介面都傾聽這個 5000 敲門聲
|
| 8 |
+
sock.bind(("0.0.0.0", 5000))
|
| 9 |
+
print("成功佔用 Port 5000,等待訊息中...")
|
| 10 |
+
|
| 11 |
+
# 一次最多接 1024 bytes 的包裹。這裡會「卡住(阻塞)」直到收到東西為止。
|
| 12 |
+
data, addr = sock.recvfrom(1024)
|
| 13 |
+
|
| 14 |
+
# 收到的資料也是 bytes,解碼顯示給人類看
|
| 15 |
+
text = data.decode('utf-8')
|
| 16 |
+
print(f"收到來自 {addr} 的訊息:", text)
|
projects/2026project_class/examples/w5_udp_sender.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import socket
|
| 2 |
+
|
| 3 |
+
# 建立 UDP Socket 水管
|
| 4 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 5 |
+
|
| 6 |
+
# 把文字編成機器碼 bytes
|
| 7 |
+
message = "Hello 前方同學!".encode('utf-8')
|
| 8 |
+
|
| 9 |
+
# 發射!如果對方沒開接收端,用UDP傳送也不會閃退,信件會直接在虛空中消失。
|
| 10 |
+
# 這裡預設為本機 IP 127.0.0.1 供測試
|
| 11 |
+
target_info = ("127.0.0.1", 5000) # [請修改為同學的 IP]
|
| 12 |
+
|
| 13 |
+
print(f"準備發送訊息至 {target_info}")
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
sock.sendto(message, target_info)
|
| 17 |
+
print("訊息已發送出去了!")
|
| 18 |
+
except OSError as e:
|
| 19 |
+
# 網卡罷工或斷線時的防範
|
| 20 |
+
print("【系統】網路傳輸失敗,請檢查 Wi-Fi 是否連線,或 IP 格式是否正確!例外細節:", e)
|