From 641e51c887667b865a54ff9ccfb5f8c0c0ee30d9 Mon Sep 17 00:00:00 2001 From: JianMiau Date: Mon, 10 Aug 2026 15:47:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=89=8B=E5=8B=95?= =?UTF-8?q?=E5=88=86=E7=B5=84=E6=94=B9=E7=82=BA=E8=B7=A8=E5=8D=80=E5=B9=B3?= =?UTF-8?q?=E8=A1=A1=E9=85=8D=E5=B0=8D=EF=BC=8C=E5=81=B6=E6=95=B8=E7=B8=BD?= =?UTF-8?q?=E4=BA=BA=E6=95=B8=E4=B8=8D=E5=86=8D=E8=A3=9C=E8=BC=AA=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根本原因: 原本配對只以人數多的一區為基準,缺額全部補「輪空」。例如 A 區 7 人、B 區 5 人會補 2 個輪空,但總人數 12 是偶數,其實可以讓 A 區 1 人跨區支援就湊滿 6 隊真人對打。 影響: 兩區差距 2 以上時,每輪自動抽「差距÷2」人跨區支援,且每輪輪替不同人,跨區負擔平均分攤;只有總人數為奇數時才會補 1 個輪空。等人數時行為不變,三輪之間照舊旋轉配對讓搭檔不同。 修法: 自 badminton-match-hub 8cc0518 移植 balanceZones / pickCrossMembers,buildManualGroups 改為每輪先平衡兩區再建隊伍,補位改在平衡後只補至多 1 個。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/match.ts | 58 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/lib/match.ts b/src/lib/match.ts index c06e890..d0f3318 100644 --- a/src/lib/match.ts +++ b/src/lib/match.ts @@ -22,22 +22,56 @@ export function parseRoster(input: string) { } export function buildManualGroups(areaA: string[], areaB: string[]) { - const targetCount = Math.max(areaA.length, areaB.length) const shuffledA = shuffleList(areaA) const shuffledB = shuffleList(areaB) - const paddedA = padTeams(shuffledA, targetCount) - const paddedB = padTeams(shuffledB, targetCount) - return Array.from({ length: TOTAL_GROUPS }, (_, roundIndex) => ({ - id: roundIndex + 1, - teams: paddedA.map((playerA, index) => - createTeam( - index + 1, - playerA, - paddedB[(index + roundIndex) % paddedB.length], + // 每輪先做跨區平衡再配對:兩區差距 2 以上時抽「差距÷2」人跨區支援, + // 且每輪輪替不同人;只有總人數為奇數時才會補 1 個輪空。 + return Array.from({ length: TOTAL_GROUPS }, (_, roundIndex) => { + const balanced = balanceZones(shuffledA, shuffledB, roundIndex) + const targetCount = Math.max(balanced.areaA.length, balanced.areaB.length) + const paddedA = padTeams(balanced.areaA, targetCount) + const paddedB = padTeams(balanced.areaB, targetCount) + + return { + id: roundIndex + 1, + teams: paddedA.map((playerA, index) => + createTeam( + index + 1, + playerA, + paddedB[(index + roundIndex) % paddedB.length], + ), ), - ), - })) + } + }) +} + +function balanceZones(areaA: string[], areaB: string[], roundIndex: number) { + const moveCount = Math.floor(Math.abs(areaA.length - areaB.length) / 2) + + if (moveCount === 0) { + return { areaA, areaB } + } + + if (areaA.length > areaB.length) { + const { moved, remaining } = pickCrossMembers(areaA, moveCount, roundIndex) + return { areaA: remaining, areaB: [...areaB, ...moved] } + } + + const { moved, remaining } = pickCrossMembers(areaB, moveCount, roundIndex) + return { areaA: [...areaA, ...moved], areaB: remaining } +} + +// 依輪次輪替抽出要跨區支援的人,讓跨區的負擔平均分攤在不同人身上。 +function pickCrossMembers(list: string[], moveCount: number, roundIndex: number) { + const movedIndexes = new Set( + Array.from({ length: moveCount }, (_, offset) => (roundIndex * moveCount + offset) % list.length), + ) + + return { + moved: list.filter((_, index) => movedIndexes.has(index)), + remaining: list.filter((_, index) => !movedIndexes.has(index)), + } } export function convertDbRecordToGroups(record: MatchResultsRecord) {