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

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_databd|yes||YYYYMMDD / bd|no||YYYYMMDDNode-RED 時期發的舊訊息按鈕仍可用
  • 每週一 10:00SIGNUP_CRON)自動發到正式群

記分板按鈕

  • 權限:只有開局者(下 /記分 的人)或群組管理員(creator / administrator,查詢結果快取 5 分鐘)能按;其他人按了只跳提示「只有開局者或群組管理員可以操作記分板」。報名按鈕不受限。

  • 選人(無參數開局):點名字依序標 1️⃣4️⃣↩️ 取消上一位▶️ 單打開始(剛好 2 人時)、🎲 隨機配對(還沒點人時)、✖️ 取消

  • 開局:🅰️ / 🅱️ 先發球;雙打可 🔀 換位 決定誰站右區(誰先發/先接);🔃 🅰️🅱️ 對換 把兩隊上下對調(只能在 0:0);🙋 換人計分 轉交計分人(見下方「計分人」)

  • 進行中(左欄上下是加分):🅰️ +1 ↩️ 上一步 🅱️ +1 ⏱ 結算 🏁 結束↩️ 上一步 在 0:0 時會退回「選先發球」(所以沒有「重新開始」)

  • ⏱ 結算:時間到以目前比分定勝負(領先方 🏆、同分平手),可 上一步 復原

  • 分出勝負顯示 🏆🏁 結束 需兩段確認(第一次按 → 出現 ✅ 確定結束 / ↩️ 取消,按其他按鈕也會取消),確定後移除按鈕、鎖定

  • 規則:偶數分右區發、發球方得分同隊換位續發、失分換發不換位、21 分制、Deuce 領先 2 分、30 分封頂(與 badminton-scoreboard 一致)

隨機配對(/記分🎲 隨機配對

同一則訊息依序經過三個階段(phasepickrosterpaired),權限同記分板(開局者/管理員):

  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.skill1~10,優先用 user_id 對,對不到用暱稱對,都沒有當 1),與 match-hub 相同;bot 只讀不寫這欄
  • 8 人以上排 2、3 號場地各一場,4~7 人只排 2 號場地;讀不到 history 時照配、預覽會標示未考慮戰績

安裝與啟動

npm install
cp .env.example .env   # 填 BOT_TOKEN、DB_*、ALLOWED_CHAT_IDS
npm test               # 35 個單元測試(規則、報名流程、選人、隨機配對、歷史戰績、指令解析)
npm start              # 或 npm run dev

⚠️ 同一個 bot 只能有一個 polling 端

Node-REDhttp://192.168.5.45:1880)的「羽球報名」分頁已於 2026-08-18 停用,由本專案接手。 若要暫時切回 Node-RED,或反過來,用下列指令(兩邊同時 poll 會互相 409):

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

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_FILEJSON),重啟後舊訊息按鈕仍可用。

歷史戰績:分出勝負(打到目標分或 ⏱ 結算)且有得分的比賽,在按 ✅ 確定結束🔄 再一局 時寫入 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 發球位置編號 03 = 畫面 14 號位減 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
S
Description
No description provided
Readme
134 KiB
Languages
JavaScript 99.9%
Dockerfile 0.1%