diff --git a/README.md b/README.md
index ff821f0..881eabd 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,8 @@
- 每次固定產生 3 組完整配對結果
- 每一隊都由 `1 位 A 區 + 1 位 B 區` 組成
- 每一組都會把 A 區與 B 區名單完整分配完成
-- 若某一區人數不足,系統會自動補入「那個」
+- 兩區人數差 2 以上時,每輪會輪流讓人多的一區成員跨區支援
+- 跨區平衡後仍有缺口(總人數為奇數)時,才補入「那個」
- 產生配對後可用按鈕手動上傳資料到資料庫
- 若指定日期已經有資料,上傳前會先詢問是否覆蓋
- 可讀取指定日期的資料庫內容並回填到畫面
diff --git a/src/App.css b/src/App.css
index 6b20956..b6663d8 100644
--- a/src/App.css
+++ b/src/App.css
@@ -287,18 +287,16 @@
color: #8d2d22;
}
-.round-list,
.team-list {
display: grid;
gap: 14px;
}
-.share-row {
- margin-bottom: 18px;
-}
-
-.share-button {
- min-width: 160px;
+.round-list {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 18px;
+ align-items: start;
}
.round-card,
@@ -368,6 +366,10 @@
grid-template-columns: 1fr;
}
+ .round-list {
+ grid-template-columns: 1fr;
+ }
+
.hero-copy h1 {
max-width: 10ch;
}
diff --git a/src/App.tsx b/src/App.tsx
index 6a0dfec..7424037 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -72,9 +72,10 @@ function App() {
const parsedAreaA = parseRoster(areaAInput)
const parsedAreaB = parseRoster(areaBInput)
- const teamCount = Math.max(parsedAreaA.length, parsedAreaB.length)
- const placeholderCountA = Math.max(0, teamCount - parsedAreaA.length)
- const placeholderCountB = Math.max(0, teamCount - parsedAreaB.length)
+ const zoneGap = Math.abs(parsedAreaA.length - parsedAreaB.length)
+ const crossCount = Math.floor(zoneGap / 2)
+ const placeholderCount = zoneGap % 2
+ const teamCount = Math.ceil((parsedAreaA.length + parsedAreaB.length) / 2)
function generateGroups() {
if (parsedAreaA.length === 0 || parsedAreaB.length === 0) {
@@ -85,10 +86,11 @@ function App() {
return
}
- const prepared = prepareParticipants(parsedAreaA, parsedAreaB)
+ const shuffledA = shuffleList(parsedAreaA)
+ const shuffledB = shuffleList(parsedAreaB)
const nextResults = Array.from({ length: TOTAL_ROUNDS }, (_, index) => ({
id: index + 1,
- teams: createRoundTeams(prepared.areaA, prepared.areaB, index),
+ teams: createRoundTeams(shuffledA, shuffledB, index),
}))
setResults(nextResults)
@@ -213,8 +215,8 @@ function App() {
羽毛球分組配對器
輸入 A 區與 B 區名單後,系統會產生 3 組完整配對。
- 每一隊都是 1 位 A 區成員搭配 1 位 B 區成員;
- 若某一區人數不足,會自動補入「那個」。
+ 兩區人數有落差時,每輪會輪流讓人多的一區成員跨區支援;
+ 補完後仍有缺口才會補入「那個」。
@@ -278,6 +280,16 @@ function App() {
>
讀取指定日期
+ {results.length > 0 ? (
+
+ ) : null}
@@ -312,19 +324,6 @@ function App() {
三組名單
- {results.length > 0 ? (
-
-
-
- ) : null}
-
{results.length > 0 ? (
{results.map((round) => (
@@ -359,14 +358,30 @@ function App() {
)
}
-function prepareParticipants(areaA: string[], areaB: string[]) {
- const targetCount = Math.max(areaA.length, areaB.length)
- const shuffledA = shuffleList(areaA)
- const shuffledB = shuffleList(areaB)
+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 {
- areaA: createParticipants(shuffledA, targetCount, 'A'),
- areaB: createParticipants(shuffledB, targetCount, 'B'),
+ moved: list.filter((_, index) => movedIndexes.has(index)),
+ remaining: list.filter((_, index) => !movedIndexes.has(index)),
}
}
@@ -388,11 +403,16 @@ function createParticipants(players: string[], targetCount: number, zone: 'A' |
return participants
}
-function createRoundTeams(areaA: Participant[], areaB: Participant[], roundIndex: number) {
- return areaA.map((playerA, index) => ({
+function createRoundTeams(areaA: string[], areaB: string[], roundIndex: number) {
+ const balanced = balanceZones(areaA, areaB, roundIndex)
+ const targetCount = Math.max(balanced.areaA.length, balanced.areaB.length)
+ const playersA = createParticipants(balanced.areaA, targetCount, 'A')
+ const playersB = createParticipants(balanced.areaB, targetCount, 'B')
+
+ return playersA.map((playerA, index) => ({
id: index + 1,
playerA,
- playerB: areaB[(index + roundIndex) % areaB.length],
+ playerB: playersB[(index + roundIndex) % playersB.length],
}))
}