Files
JianMiauandClaude Fable 5 0386c94546 記分板新增隨機配對、計分人權限與換人計分,調整按鈕排法
根本原因:
每打完一輪都要人工決定誰上場、誰搭誰、誰負責記分,記分板也沒有「兩隊對換」和退回選先發球的操作,現場排場地或換人都得重開記分板。

影響:
/記分 多一顆 🎲 隨機配對:勾選出席者(預設全選)後,依 tg_members.skill 與今天的 history 戰績排 2、3 號場地(休息最久優先、搭檔與對手不重複、實力平衡),預覽後一鍵推出各場地記分板;每場從休息者抽一位計分人(SCORER_EXCLUDE 不抽),只有他和管理員能按,休息人不夠則不鎖權限;開局可 🙋 換人計分、🔃 🅰️🅱️ 對換;計分鍵盤改為左欄 🅰️+1/🅱️+1,拿掉「重新開始」,0:0 時上一步可退回選先發球。

修法:
- 新增 src/pairing.js:移植 badminton-match-hub V2 的配對規則,改成一次只排下一輪、以「休息輪數」取代平均場數;assignScorers 從休息者抽計分人
- history.js 新增 loadTodayGames 讀今日戰績;signup.js attendees() 帶 skill 與 user_id(權限靠 TG id 比對)
- scoreboard.js 以 phase(pick → roster → paired)管理同一則訊息的選人/勾選/預覽流程,推出的記分板 ownerId 指向計分人;新增換人計分(ho / hoN / hoany / hocancel),候選排除場上、現任計分人與 SCORER_EXCLUDE
- match.js 新增 swapTeams,setFirstServer 記入 history 讓 undo 可退回開局;render.js 重排鍵盤並新增勾選、預覽、換人面板與場地標題
- config 新增 SCORER_EXCLUDE;README、.env.example 與 37 個測試同步更新

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 22:24:16 +08:00

138 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tg-shuttle-score — 建喵打羽球 Bot
Telegram bot「建喵打羽球 @JianMiauBadmintonBot」的 Node.js 版本,包含:
1. **今日羽球報名**(從 Node-RED「羽球報名」分頁移植,行為、訊息格式、DB 表完全相同)
2. **群組記分板**(新功能):`/記分` 開局,訊息本身就是記分板,用 inline 按鈕記分
只在白名單群組運作:`建喵測試 (-1002488036686)``羽球團~ (-4679836160)`
## 指令
| 指令 | 英文別名 | 說明 |
| --- | --- | --- |
| `/羽球 [YYYYMMDD]` | `/badminton` | 發今日(或指定日期)報名訊息,附「✅ 參加 / ❌ 不參加」按鈕 |
| `/羽球名單` | `/bdlist` | 回今天的參加名單 |
| `/羽球暱稱 名字` | `/bdname` | 設定報名顯示的名字(存 `tg_members`,綁 user_id |
| `/記分` | `/score``/new` | 列出今天出席的人(`attendance`),依 1→4 點選上場人員:1、2 一隊,3、4 一隊;點滿 4 人自動開局,選 2 人可按「單打開始」 |
| `/記分``🎲 隨機配對` | | 勾選今天要排的人(預設全選)→ 依 `tg_members.skill` 與今天的 `history` 戰績自動排 2、3 號場地 → 預覽後一鍵推出各場地的記分板(見下方「隨機配對」) |
| `/記分 小明 小華 vs 阿強 阿美` | `/score``/new` | 直接開一場雙打記分板(單打各 1 人;最後加數字可改目標分) |
| `/help` | | 指令說明 |
英文指令已用 `setMyCommands` 註冊(啟動時自動),群組打 `/` 會有選單。
### 報名按鈕
- 參加 → `INSERT attendance`;不參加 → `DELETE`;重按只跳提示、不動 DB
- 名單直接改在原訊息上;`callback_data``bd|yes||YYYYMMDD` / `bd|no||YYYYMMDD`**Node-RED 時期發的舊訊息按鈕仍可用**
- 每週一 10:00`SIGNUP_CRON`)自動發到正式群
### 記分板按鈕
- **權限**:只有開局者(下 `/記分` 的人)或群組管理員(creator / administrator,查詢結果快取 5 分鐘)能按;其他人按了只跳提示「只有開局者或群組管理員可以操作記分板」。報名按鈕不受限。
- 選人(無參數開局):點名字依序標 1️⃣~4️⃣,`↩️ 取消上一位``▶️ 單打開始`(剛好 2 人時)、`🎲 隨機配對`(還沒點人時)、`✖️ 取消`
- 開局:`🅰️ / 🅱️ 先發球`;雙打可 `🔀 換位` 決定誰站右區(誰先發/先接);`🔃 🅰️🅱️ 對換` 把兩隊上下對調(只能在 0:0);`🙋 換人計分` 轉交計分人(見下方「計分人」)
- 進行中(左欄上下是加分):`🅰️ +1` `↩️ 上一步` `🅱️ +1` `⏱ 結算` `🏁 結束``↩️ 上一步` 在 0:0 時會退回「選先發球」(所以沒有「重新開始」)
- `⏱ 結算`:時間到以目前比分定勝負(領先方 🏆、同分平手),可 `上一步` 復原
- 分出勝負顯示 🏆;`🏁 結束` 需兩段確認(第一次按 → 出現 `✅ 確定結束 / ↩️ 取消`,按其他按鈕也會取消),確定後移除按鈕、鎖定
- 規則:偶數分右區發、發球方得分同隊換位續發、失分換發不換位、21 分制、Deuce 領先 2 分、30 分封頂(與 badminton-scoreboard 一致)
### 隨機配對(`/記分` → `🎲 隨機配對`)
同一則訊息依序經過三個階段(`phase``pick``roster``paired`),權限同記分板(開局者/管理員):
1. **勾選名單**:今天出席的人全部預設 ✅,點名字切換,`✅ 全選 / ⬜ 全不選`;至少 4 人。`↩️ 返回選人` 回到 1→4 點選
2. **配對預覽**:顯示 `2️⃣ 號場地 🅰️ … vs 🅱️ …``3️⃣ 號場地 …``😴 休息:…``🔁 重新配對` 再抽一次、`↩️ 返回勾選` 改名單
3. **`✅ 開記分板`**:每個場地各推一則記分板(標題帶場地號、`📝 計分:某某`);預覽訊息鎖定
**計分人**(權限靠 `attendance.user_id` 對 Telegram 帳號):
- 每個場地從 **休息的人** 隨機抽一位當計分人,只有他和群組管理員能按那則記分板;`SCORER_EXCLUDE``.env`,逗號分隔暱稱,例如 `景涵`)裡的人不會被抽到
- 休息人數不夠(一場一人)→ 該場顯示 `📝 計分:任何人`,不鎖權限
- **`🙋 換人計分`**(開局鍵盤,只能在還沒選先發球前):由目前計分人或管理員操作,列出今天出席、不在場上、不是現任計分人、不在排除名單的人,點一下就轉交;也可改成「任何人」;已選先發球但還是 0:0 時,按 `↩️ 上一步` 退回就能換
- `/記分 A B vs C D` 或 1→4 點選開的記分板仍是開局者持有,但一樣可用 `🙋 換人計分` 轉交
配對規則(`src/pairing.js`,移植自 badminton-match-hub V2 並改成一次只排下一輪):
- **誰上場**:不是平均場數,而是「每個人都要休息到」——依今天 `history` 戰績算「休息了幾輪」(一輪 = 場地數),休息最久的優先、剛打完的最後;同輪再比今天打的場數少者優先,最後隨機。沒打過的人一定上場
- **搭檔**:今天沒搭過的優先(`16^搭檔次數` 加總最小),不重複的前提下各組實力總和最接近(強弱互補)
- **對戰**:兩隊實力總和最接近優先,再挑今天對戰次數最少的組合;兩場隨機分到 2、3 號場地
- **實力**`tg_members.skill`1~10,優先用 `user_id` 對,對不到用暱稱對,都沒有當 1),與 match-hub 相同;bot 只讀不寫這欄
- 8 人以上排 2、3 號場地各一場,4~7 人只排 2 號場地;讀不到 `history` 時照配、預覽會標示未考慮戰績
## 安裝與啟動
```bash
npm install
cp .env.example .env # 填 BOT_TOKEN、DB_*、ALLOWED_CHAT_IDS
npm test # 35 個單元測試(規則、報名流程、選人、隨機配對、歷史戰績、指令解析)
npm start # 或 npm run dev
```
### ⚠️ 同一個 bot 只能有一個 polling 端
Node-RED`http://192.168.5.45:1880`)的「羽球報名」分頁已於 2026-08-18 停用,由本專案接手。
若要暫時切回 Node-RED,或反過來,用下列指令(兩邊同時 poll 會互相 409):
```bash
npm run nodered:status # 看目前狀態
npm run nodered:pause # 停用 bd_tab + bot 設定改 send-only → 之後 npm start
npm run nodered:resume # 還原 Node-RED(本專案要先停)
```
(透過 Node-RED admin APIdeploy type = nodes,只重啟有變動的節點。)
## Docker / NAS
```bash
sudo docker compose up -d --build
```
`docker-compose.yml``.env`,狀態檔掛在 `./data`。上線步驟:NAS 起容器 → 確認 log 出現「已啟動」→ `npm run nodered:pause`
## 資料庫
與 badminton-scoreboard / Node-RED 共用 `jianmiau.tk:3307/badminton`
| 表 | 用途 |
| --- | --- |
| `attendance` | 今日出席(只存要參加的人;PK poll_date+user_id |
| `history` | 完成的比賽戰績(與 web 版共用,記分板寫入) |
| `tg_poll` | 每天報名訊息的 chat_id / message_id(改暱稱後回頭更新用) |
| `tg_members` | `/羽球暱稱` 設的名字,綁 user_id |
記分板狀態存 `DATA_FILE`(JSON),重啟後舊訊息按鈕仍可用。
**歷史戰績**:分出勝負(打到目標分或 `⏱ 結算`)且有得分的比賽,在按 `✅ 確定結束``🔄 再一局` 時寫入 `history` 表(`DB_HISTORY_TABLE`,預設 `history`,欄位與 web 版相同),web 版「歷史戰績」頁直接看得到。未結算就結束的比賽不會存(確認提示會寫明)。寫入失敗時不會結束,可再按一次重試。
- `players = [1號,2號,3號,4號]``team = [[1,2],[3,4]]``score = [🅰️,🅱️]``type` 0 雙打/1 單打、`winScore` 目標分、`dayOfWeek` 台北時區
- 單打 `players``[p1,p1,p3,p3]`web 歷史頁用 `players[starter]` 顯示發球者)
每一分都會累積 `scoreList`(不顯示在 TG,供之後寫 `history` 用),格式沿用 web 版:`[round, starter, winCount, winner]`
- `round` 第幾分(0 起算)
- `starter` 發球位置編號 0~3 = 畫面 1~4 號位減 1(🅰️ 上排:偶數分 0、奇數分 1;🅱️ 下排:偶數分 2、奇數分 3;只看位置不看人)
- `winCount` 同隊連續得分數,第一分為 0
- `winner` 得分隊 0 = 🅰️、1 = 🅱️
- 上一步會一併回退(退回選先發球時清空);再一局清空;結算不影響
## 專案結構
```
src/
index.js 進入點:白名單、指令註冊、callback 分流、排程
config.js .env 讀取
db.js mysql2 連線池
util.js 共用小工具(HTML 跳脫、日期、指令 regex)
signup.js 報名功能(移植自 Node-RED bd_sm 狀態機)
scoreboard.js 記分板功能(含選人 / 隨機配對流程)
match.js 羽球規則(純邏輯)
pairing.js 隨機配對規則(純邏輯:休息輪數、搭檔 / 對手不重複、實力平衡)
history.js history 表寫入 / 讀今日戰績
render.js 記分板、選人、勾選、配對預覽的文字 + 鍵盤
store.js 記分板狀態持久化
scripts/
nodered-switch.mjs 暫停 / 恢復 Node-RED 舊流程
test/
match.test.js pairing.test.js scoreboard.test.js history.test.js signup.test.js
```