import { test } from 'node:test' import assert from 'node:assert/strict' import { addPoint, createMatch, settleMatch, getServing, getWinner, parseTeams, setFirstServer, swapCourt, undo, } from '../src/match.js' import { renderKeyboard, renderText } from '../src/render.js' test('parseTeams:雙打 / 單打 / 目標分數', () => { assert.deepEqual(parseTeams('小明 小華 vs 阿強 阿美'), { teams: [['小明', '小華'], ['阿強', '阿美']], target: 21, }) assert.deepEqual(parseTeams('小明/小華 VS 阿強、阿美 15'), { teams: [['小明', '小華'], ['阿強', '阿美']], target: 15, }) assert.deepEqual(parseTeams('小明 vs 阿強').teams, [['小明'], ['阿強']]) assert.ok(parseTeams('小明 小華 vs 阿強').error) assert.ok(parseTeams('小明 小華').error) assert.ok(parseTeams('').error) }) test('雙打發球規則:0 分右區、同隊得分換位、失分換發不換位', () => { const m = createMatch(['A1', 'A2'], ['B1', 'B2']) assert.equal(addPoint(m, 0), false, '未選先發不能加分') setFirstServer(m, 0) let s = getServing(m) assert.deepEqual([s.server, s.court, s.receiver], ['A1', 'right', 'B1']) addPoint(m, 0) // A 1:0,A 隊換位,1 分左區 → A1 在左區發 s = getServing(m) assert.deepEqual([m.score, s.server, s.court, s.receiver], [[1, 0], 'A1', 'left', 'B2']) addPoint(m, 0) // A 2:0,再換位,2 分右區 → A1 仍在右區發 s = getServing(m) assert.deepEqual([s.server, s.court, s.receiver], ['A1', 'right', 'B1']) addPoint(m, 1) // B 得分 2:1,換發,B 隊不換位,1 分左區 → B2 發 s = getServing(m) assert.deepEqual([m.score, s.team, s.server, s.court], [[2, 1], 1, 'B2', 'left']) // 接發者:A 隊左區的人。A 隊換位兩次回到原位 → A2 在左區 assert.equal(s.receiver, 'A2') }) test('單打發球:只看分數奇偶決定發球區', () => { const m = createMatch(['A'], ['B']) setFirstServer(m, 1) addPoint(m, 1) const s = getServing(m) assert.deepEqual([s.server, s.court, s.receiver], ['B', 'left', 'A']) }) test('swapCourt 只能在 0:0,且會改變先發者', () => { const m = createMatch(['A1', 'A2'], ['B1', 'B2']) assert.equal(swapCourt(m, 0), true) setFirstServer(m, 0) assert.equal(getServing(m).server, 'A2') addPoint(m, 0) assert.equal(swapCourt(m, 0), false) }) test('undo 可復原分數、發球權與站位', () => { const m = createMatch(['A1', 'A2'], ['B1', 'B2']) setFirstServer(m, 0) addPoint(m, 0) addPoint(m, 1) assert.equal(undo(m), true) assert.deepEqual(m.score, [1, 0]) assert.equal(m.server, 0) assert.equal(getServing(m).server, 'A1') assert.equal(undo(m), true) assert.deepEqual(m.score, [0, 0]) assert.equal(undo(m), false) }) test('勝負判定:21 分、Deuce 領先 2 分、30 分封頂', () => { const m = createMatch(['A'], ['B']) setFirstServer(m, 0) m.score = [20, 20] addPoint(m, 0) assert.equal(getWinner(m), null, '21:20 未分勝負') addPoint(m, 0) assert.equal(getWinner(m), 0, '22:20 A 勝') assert.equal(m.finished, true) assert.equal(addPoint(m, 1), false, '結束後不能加分') const n = createMatch(['A'], ['B']) setFirstServer(n, 0) n.score = [29, 29] addPoint(n, 1) assert.equal(getWinner(n), 1, '30:29 直接獲勝') }) test('renderText 會標記發球者、跳脫 HTML,並依畫面位置排列(上 1 2 / 下 4 3)', () => { const m = createMatch(['', 'A2'], ['B1', 'B2']) setFirstServer(m, 0) let text = renderText(m) assert.match(text, /🅰️ 🏸<A1> \/ A2/) assert.match(text, /🅱️ B2 \/ B1/, '下排 B2(4) 在左、B1(3) 在右') assert.match(text, /發球:🅰️ <A1><\/b>(右區) 接發:B1/) addPoint(m, 0) // A 得分換位:A1 到左區 → 畫面上排右邊 text = renderText(m) assert.match(text, /🅰️ A2 \/ 🏸<A1>/) }) test('結算:時間到以目前比分定勝負,可 undo;鍵盤為三排', () => { const m = createMatch(['A'], ['B']) assert.equal(settleMatch(m), false, '未選先發不能結算') setFirstServer(m, 0) assert.deepEqual( renderKeyboard(m).inline_keyboard.map((r) => r.map((b) => b.text)), [['🅰️ +1', '🅱️ +1'], ['↩️ 上一步', '🔄 重新開始'], ['⏱ 結算', '🏁 結束']], ) addPoint(m, 0) addPoint(m, 0) addPoint(m, 1) assert.equal(settleMatch(m), true) assert.equal(m.finished, true) assert.equal(getWinner(m), 0) assert.match(renderText(m), /⏱ 時間到結算 2 : 1 🅰️ 獲勝/) assert.equal(addPoint(m, 1), false) assert.equal(undo(m), true) assert.equal(m.finished, false) assert.equal(m.settled, false) assert.deepEqual(m.score, [2, 1]) addPoint(m, 1) settleMatch(m) assert.equal(getWinner(m), null) assert.match(renderText(m), /2 : 2 平手/) }) test('scoreList 沿用 web 版格式 [round, starter, winCount, winner],undo/reset 一併處理', () => { const m = createMatch(['A1', 'A2'], ['B1', 'B2']) setFirstServer(m, 0) addPoint(m, 0) // 0:0 A 發,偶數 → 1 號位(0) addPoint(m, 0) // 1:0 A 發,奇數 → 2 號位(1),連勝 1 addPoint(m, 1) // 2:0 A 發,偶數 → 0;B 得分,連勝歸 0 addPoint(m, 1) // 2:1 B 發,奇數 → 4 號位(3),B 連勝 1 addPoint(m, 1) // 2:2 B 發,偶數 → 3 號位(2),B 連勝 2 assert.deepEqual(m.scoreList, [ [0, 0, 0, 0], [1, 1, 1, 0], [2, 0, 0, 1], [3, 3, 1, 1], [4, 2, 2, 1], ]) assert.ok(!/starter|winCount/.test(renderText(m)), '不顯示在 TG 記分板') undo(m) assert.equal(m.scoreList.length, 4) settleMatch(m) undo(m) assert.equal(m.scoreList.length, 4, '結算/復原不影響 scoreList') const singles = createMatch(['A'], ['B']) setFirstServer(singles, 1) addPoint(singles, 0) assert.deepEqual(singles.scoreList, [[0, 2, 0, 0]], '單打同樣以位置編號記') })