Pairing

編排原理

說明每一輪對陣表如何產生。依賽事設定分為 瑞士制與單循環;個人賽以棋手為單位、團體賽以隊伍為單位,編排邏輯相同。 名次計算見「成績説明」頁,本頁不涉及。

瑞士制 · 個人/團體

瑞士制對陣表如何形成

①

輸入:已完賽各輪成績 → 先重算每位(隊)目前積分與「已交手名單」;退賽者不進入本輪編排池。

getComputedStats → generatePairingsByMode
let players = getComputedStats(group);
const allActive = players.filter((p) => !p.withdrawn);
p1.opponents.push(p2.id);
p2.opponents.push(p1.id);
②

硬性規則:同一賽事中不可重逢(已交手者不得再配)。

getCandidates
const noPlayed = remaining.filter(
  (q) => !p.opponents.includes(q.id)
);
③

同分優先:先在「目前積分相同」的對手中尋找;找不到才允許「跨分組」對戰,並優先積分差最小者。

getCandidates
const same = noPlayed.filter((q) => q.matchPoints === p.matchPoints);
const cross = noPlayed
  .filter((q) => q.matchPoints !== p.matchPoints)
  .sort((a, b) =>
    Math.abs(p.matchPoints - a.matchPoints) -
    Math.abs(p.matchPoints - b.matchPoints)
  );
return same.concat(cross);
④

抽籤號方向(Z/U):同分組內依賽前抽籤所得比賽編號排序;預設 Z 為奇數輪由小到大、偶數輪由大到小;U 則相反。此設定只影響候選順序。

getCandidates · pairingMode
if (pairingMode === "Z") {
  same.sort((a, b) =>
    currentRound % 2 === 1 ? a.number - b.number : b.number - a.number
  );
} else {
  same.sort((a, b) =>
    currentRound % 2 === 1 ? b.number - a.number : a.number - b.number
  );
}
⑤

搜尋目標:在可配對方案中,優先把「單場最大積分差」壓到最小,再看分差平方和、跨分組台數,並盡量讓 #n 在各人(隊)之間平均。

evaluatePairing
return (
  maxDiff * 1000000 +
  sumSq * 10000 +
  totalCrossMatches * 100 +
  repeatCrossPenalty
);
⑥

放寬:先限制每分組最多少量跨組台;若無解,逐步放寬跨組上限後重試。

backtrack 呼叫
const defaultMaxCross = 1;
const pairings =
  backtrack(active, {}, defaultMaxCross, 0) ||
  backtrack(active, {}, defaultMaxCross + 1, 0) ||
  backtrack(active, {}, defaultMaxCross + 2, 0) ||
  backtrack(active, {}, defaultMaxCross + 3, 0) ||
  [];
⑦

輪空:人數(隊數)為奇數時才有輪空。優先從未輪空、且目前積分最低的一組中選人;輪空台置於對陣表最後。

bye 候選人
const allowBye = allActive.length % 2 === 1;
const byeCandidates = active.filter((p) => !p.hadBye);
const minPts = Math.min(...byeCandidates.map((p) => p.matchPoints));
const lowestGroup = byeCandidates.filter(
  (p) => p.matchPoints === minPts
);
⑧

台次:非輪空台大致依雙方「編排當下積分」由高到低排台;再依抽籤號微調。若賽事開啟左右平衡,第二輪起可只交換左右座位、不改對手。

sortMatchesForBoards
if (a.bye && !b.bye) return 1;
const maxA = Math.max(a1, a2);
const maxB = Math.max(b1, b2);
if (maxB !== maxA) return maxB - maxA;

瑞士制每一輪都依「目前積分」重新編排,目標是讓同分者盡量互碰,並避免不必要的大分差對戰。

瑞士制 · #n 如何影響編排

#n 如何影響下一輪對陣

①

定義(官方對陣表頁腳):#n 是至此輪為止,與不同積分/場分對手交手及輪空的累計次數;系統以將 #n 平均分配為原則。

②

何時介入:只有在「必須出現跨分組對局」時才有作用。同分組對打不會增加跨組負擔;#n 不會改變「禁重逢」「同分優先」。

priorCrossMap(供本輪評分)
if (m.bye && m.player1) {
  priorCrossMap[m.player1.id]++;
} else if (Number(m.previousScore1) !== Number(m.previousScore2)) {
  priorCrossMap[m.player1.id]++;
  priorCrossMap[m.player2.id]++;
}
③

評分順位(後段):先比單場最大分差、分差平方和、跨組台數;這些都相同之後,才用歷史 #n 加權——把「#n 已經較高」的人再放進跨組台,方案分數較差,較不易被選中。

evaluatePairing
if (d > 0) {
  repeatCrossPenalty +=
    (priorCrossMap[m.player1.id] || 0) +
    (priorCrossMap[m.player2.id] || 0);
}
④

微調:初案產生後,會嘗試把「本輪被排去跨組、且歷史 #n 偏高」的人,與「本輪同分組、且歷史 #n 偏低」的人互換對手(仍須滿足禁重逢、不惡化最大分差等),把跨組機會讓給 #n 較低者。

safeMicroAdjust
crossPlayers.sort((a, b) => (prior[b] || 0) - (prior[a] || 0));
nonCrossPlayers.sort((a, b) => (prior[a] || 0) - (prior[b] || 0));
⑤

結果:長期下來,跨分組與輪空的次數會盡量攤在不同人身上,而不是總集中在同一批棋手/隊伍。

註解原文
if (m.bye && m.player1) {
  priorCrossMap[m.player1.id]++;
} else if (
  Number(m.previousScore1 || 0) !== Number(m.previousScore2 || 0)
) {
  priorCrossMap[m.player1.id]++;
  priorCrossMap[m.player2.id]++;
}

對陣表上印出的 #n 是給現場閱讀的標註;真正驅動下一輪編排的是同一套「跨分組+輪空」累計。單循環不用 #n。#n 不是抽籤號。

單循環 · 個人/團體

單循環對陣表如何形成

①

固定輪轉:賽員(隊)先依賽前抽籤編號排序;第一人固定,其餘每人一輪旋轉一格(circle method)。

round_robin
allValid.sort((a, b) => (parseInt(a.number) || 0) - (parseInt(b.number) || 0));
let fixed = allValid[0];
let rotating = allValid.slice(1);
for (let i = 0; i < currentRound - 1; i++) {
  rotating.unshift(rotating.pop());
}
②

配對方式:旋轉後,第 i 台由隊列前端與末端相向配對。

round_robin
let roundPlayers = [fixed, ...rotating];
for (let i = 0; i < N_even / 2; i++) {
  let p1 = roundPlayers[i];
  let p2 = roundPlayers[N_even - 1 - i];
}
③

奇數人:補一個空位;對到空位者該輪輪空,輪空台置末。

round_robin
if (isOdd) allValid.push(null);
if (!p1 || !p2) {
  roundMatches.push({
    player1: activePlayer,
    player2: null,
    bye: true,
    result: group.compMode === "team" ? null : "2:0",
  });
}
④

輪數上限:偶數人最多 N−1 輪;奇數人最多 N 輪,期滿即全部互碰完畢。

round_robin
let maxRound = isOdd ? N_all : N_all - 1;
if (currentRound > maxRound) return null;

單循環對陣由抽籤號與輪次唯一決定,不依中途勝負改表;亦不使用 #n。

共用說明

共用說明:抽籤號與台次

①

比賽編號(抽籤號):賽前抽籤獲得;瑞士制同分組候選順序、單循環輪轉起點,以及台次微調都會用到。缺抽籤號時無法生成對陣表。

generateNextRound
if (missingIdPlayers.length > 0) {
  return;
}
②

台次排序:非輪空台優先排「雙方積分較高」的對局;輪空固定在表末。

sortMatchesForBoards
if (a.bye && !b.bye) return 1;
if (maxB !== maxA) return maxB - maxA;
③

左右平衡(可選):統計歷史左/右次數,若交換左右能使兩邊更平均,則只交換座位、不改對手。

generateNextRound · balanceMode
if (scoreSwap < scoreNoSwap) {
  let tempP = m.player1;
  m.player1 = m.player2;
  m.player2 = tempP;
}

抽籤號 ≠ #n:前者是賽前抽籤編號;後者是瑞士制「跨分組/輪空」累計次數。

個人賽與團體賽只差在編排單位是棋手還是隊伍;上述形成步驟相同。團體場內主將/副將/三將的台次安排屬隊伍內部設定,不在本頁對陣編排範圍。