2026-06-14 13:57:32 +00:00
|
|
|
|
"""
|
|
|
|
|
|
FIDE Swiss — упрощённый алгоритм на основе Folding с перебором offset.
|
|
|
|
|
|
Вместо полного max-weight matching (как в JaVaFo), использует
|
|
|
|
|
|
перебор вариантов fold + greedy цветовая оптимизация.
|
|
|
|
|
|
|
2026-06-14 20:29:27 +00:00
|
|
|
|
Улучшения:
|
|
|
|
|
|
- Учёт предыдущих bye (не даём bye повторно)
|
|
|
|
|
|
- Downfloaters паруются с верхом следующей группы очков
|
|
|
|
|
|
- Абсолютное цветовое предпочтение (|balance| >= 2) — жёсткое правило
|
|
|
|
|
|
- Сортировка по очкам + рейтингу для корректного fold
|
2026-06-14 13:57:32 +00:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from collections import defaultdict
|
|
|
|
|
|
from typing import Optional, List, Tuple
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Player:
|
|
|
|
|
|
def __init__(self, sno: int, name: str, rating: int, fed: str = '',
|
|
|
|
|
|
points: float = 0.0, results: list = None):
|
|
|
|
|
|
self.sno = sno
|
|
|
|
|
|
self.name = name
|
|
|
|
|
|
self.rating = rating
|
|
|
|
|
|
self.fed = fed
|
|
|
|
|
|
self.points = points
|
|
|
|
|
|
self.results = results or []
|
|
|
|
|
|
self.opponents = [r['opponent'] for r in self.results]
|
|
|
|
|
|
self.colors = [r['color'] for r in self.results]
|
|
|
|
|
|
self.tb = []
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def white_count(self): return sum(1 for c in self.colors if c == 'w')
|
|
|
|
|
|
@property
|
|
|
|
|
|
def black_count(self): return sum(1 for c in self.colors if c == 'b')
|
|
|
|
|
|
@property
|
|
|
|
|
|
def color_balance(self): return self.white_count - self.black_count
|
|
|
|
|
|
@property
|
|
|
|
|
|
def last_color(self): return self.colors[-1] if self.colors else None
|
2026-06-14 20:29:27 +00:00
|
|
|
|
@property
|
|
|
|
|
|
def had_bye(self):
|
|
|
|
|
|
return any(r.get('opponent', 0) == 0 for r in self.results)
|
2026-06-14 13:57:32 +00:00
|
|
|
|
|
|
|
|
|
|
def preferred_color(self) -> str:
|
|
|
|
|
|
if not self.colors:
|
|
|
|
|
|
return 'w'
|
|
|
|
|
|
bal = self.color_balance
|
|
|
|
|
|
if bal >= 2: return 'b'
|
|
|
|
|
|
if bal <= -2: return 'w'
|
|
|
|
|
|
if bal == 1: return 'b'
|
|
|
|
|
|
if bal == -1: return 'w'
|
|
|
|
|
|
return 'b' if self.last_color == 'w' else 'w'
|
|
|
|
|
|
|
|
|
|
|
|
def color_force(self) -> int:
|
|
|
|
|
|
bal = abs(self.color_balance)
|
|
|
|
|
|
if bal >= 2: return 2
|
|
|
|
|
|
if bal == 1: return 1
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def has_played(self, opponent_sno: int) -> bool:
|
|
|
|
|
|
return opponent_sno in self.opponents
|
|
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
|
return f"#{self.sno} {self.name} ({self.points}pts, R{self.rating})"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sort_key(player: Player) -> tuple:
|
|
|
|
|
|
return (-player.points, -player.rating)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compute_tiebreakers(players: list, rounds: int) -> None:
|
|
|
|
|
|
pts_map = {p.sno: p.points for p in players}
|
|
|
|
|
|
for p in players:
|
|
|
|
|
|
p.tb = [round(sum(pts_map.get(o, 0) for o in p.opponents), 1)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════
|
|
|
|
|
|
# ЦВЕТА
|
|
|
|
|
|
# ═══════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def _assign_colors(p1: Player, p2: Player) -> str:
|
2026-06-14 20:29:27 +00:00
|
|
|
|
"""Return 'w' if p1 gets white, 'b' if p1 gets black.
|
|
|
|
|
|
|
|
|
|
|
|
FIDE colour allocation rules (C04 Annex E.5):
|
|
|
|
|
|
1. Absolute preference (|bal| >= 2) MUST be satisfied
|
|
|
|
|
|
2. Strong preference (|bal| == 1) SHOULD be satisfied
|
|
|
|
|
|
3. When both have same absolute preference, higher rated gets it
|
|
|
|
|
|
4. When no absolute conflict, alternate from last round
|
|
|
|
|
|
"""
|
|
|
|
|
|
force1 = p1.color_force()
|
|
|
|
|
|
force2 = p2.color_force()
|
2026-06-14 13:57:32 +00:00
|
|
|
|
pref1 = p1.preferred_color()
|
|
|
|
|
|
pref2 = p2.preferred_color()
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Rule 1: absolute colour preference is mandatory
|
|
|
|
|
|
if force1 == 2 and force2 < 2:
|
|
|
|
|
|
return pref1
|
|
|
|
|
|
if force2 == 2 and force1 < 2:
|
|
|
|
|
|
return 'b' if pref2 == 'w' else 'w'
|
|
|
|
|
|
|
|
|
|
|
|
# Both have absolute preference
|
|
|
|
|
|
if force1 == 2 and force2 == 2:
|
|
|
|
|
|
if pref1 != pref2:
|
|
|
|
|
|
return pref1 # both satisfied
|
|
|
|
|
|
# Both want same colour — higher rated gets preference
|
|
|
|
|
|
if p1.rating >= p2.rating:
|
|
|
|
|
|
return pref1
|
|
|
|
|
|
else:
|
|
|
|
|
|
return 'b' if pref1 == 'w' else 'w'
|
|
|
|
|
|
|
|
|
|
|
|
# No absolute preferences — use strong preference, then rating
|
|
|
|
|
|
if force1 > force2:
|
|
|
|
|
|
return pref1
|
|
|
|
|
|
if force2 > force1:
|
|
|
|
|
|
return 'b' if pref2 == 'w' else 'w'
|
|
|
|
|
|
|
|
|
|
|
|
# Equal force (0 or 1), maybe same or different preferences
|
2026-06-14 13:57:32 +00:00
|
|
|
|
if pref1 != pref2:
|
|
|
|
|
|
return pref1
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Same preferences — higher rated decides
|
|
|
|
|
|
if p1.rating >= p2.rating:
|
|
|
|
|
|
return pref1
|
2026-06-14 13:57:32 +00:00
|
|
|
|
return 'b' if pref1 == 'w' else 'w'
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-14 20:29:27 +00:00
|
|
|
|
def _color_score(p1: Player, p2: Player, p1_color: str) -> int:
|
|
|
|
|
|
"""Score colour quality for the pair. Higher is better."""
|
|
|
|
|
|
score = 0
|
|
|
|
|
|
p1_has_pref = (p1_color == p1.preferred_color())
|
|
|
|
|
|
p2_has_pref = (('b' if p1_color == 'w' else 'w') == p2.preferred_color())
|
|
|
|
|
|
|
|
|
|
|
|
if p1_has_pref:
|
|
|
|
|
|
score += 3 if p1.color_force() >= 2 else 2
|
|
|
|
|
|
else:
|
|
|
|
|
|
score -= 5 if p1.color_force() >= 2 else 0
|
|
|
|
|
|
|
|
|
|
|
|
if p2_has_pref:
|
|
|
|
|
|
score += 3 if p2.color_force() >= 2 else 2
|
|
|
|
|
|
else:
|
|
|
|
|
|
score -= 5 if p2.color_force() >= 2 else 0
|
|
|
|
|
|
|
|
|
|
|
|
return score
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
# ═══════════════════════════════════
|
|
|
|
|
|
# ПАРИРОВАНИЕ BRACKET — ПЕРЕБОР OFFSET
|
|
|
|
|
|
# ═══════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def _pair_bracket_fold_search(
|
|
|
|
|
|
players: List[Player],
|
|
|
|
|
|
all_paired: set,
|
|
|
|
|
|
) -> Tuple[List[Tuple[Player, Player]], List[Player]]:
|
2026-06-14 20:29:27 +00:00
|
|
|
|
"""Fold pairing with offset search and floater candidates.
|
|
|
|
|
|
|
|
|
|
|
|
For a bracket of size N:
|
|
|
|
|
|
1. If N odd — try each player as a downfloater
|
|
|
|
|
|
2. For the remaining M (even) — try fold offsets 0..M/2-1
|
|
|
|
|
|
3. Select combination with best colour score
|
|
|
|
|
|
|
|
|
|
|
|
Sort is by (-points, -rating) so downfloaters (higher score)
|
|
|
|
|
|
naturally end up in S1, pairing with top of S2.
|
2026-06-14 13:57:32 +00:00
|
|
|
|
"""
|
|
|
|
|
|
available = [p for p in players if p.sno not in all_paired]
|
|
|
|
|
|
n = len(available)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
if n < 2:
|
|
|
|
|
|
return [], list(available)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
available.sort(key=lambda p: (-p.points, -p.rating))
|
|
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
best_pairs = []
|
|
|
|
|
|
best_floaters = list(available[-1:]) if n % 2 == 1 else []
|
|
|
|
|
|
best_score = -9999
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Floater candidates
|
2026-06-14 13:57:32 +00:00
|
|
|
|
floater_candidates = [None]
|
|
|
|
|
|
if n % 2 == 1:
|
|
|
|
|
|
floater_candidates = range(n)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
for fi in floater_candidates:
|
|
|
|
|
|
if fi is not None:
|
|
|
|
|
|
floater = available[fi]
|
2026-06-14 20:29:27 +00:00
|
|
|
|
rest = available[:fi] + available[fi + 1:]
|
2026-06-14 13:57:32 +00:00
|
|
|
|
else:
|
|
|
|
|
|
floater = None
|
|
|
|
|
|
rest = available
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
m = len(rest)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Try fold with different offsets
|
2026-06-14 13:57:32 +00:00
|
|
|
|
for offset in range(m // 2):
|
|
|
|
|
|
pairs = []
|
|
|
|
|
|
ok = True
|
|
|
|
|
|
used = set()
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
for i in range(m // 2):
|
|
|
|
|
|
a = rest[i]
|
|
|
|
|
|
b = rest[m // 2 + ((i + offset) % (m // 2))]
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
if a.has_played(b.sno):
|
|
|
|
|
|
ok = False
|
|
|
|
|
|
break
|
|
|
|
|
|
if a.sno in used or b.sno in used:
|
|
|
|
|
|
ok = False
|
|
|
|
|
|
break
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
color = _assign_colors(a, b)
|
|
|
|
|
|
if color == 'w':
|
|
|
|
|
|
pairs.append((a, b))
|
|
|
|
|
|
else:
|
|
|
|
|
|
pairs.append((b, a))
|
|
|
|
|
|
used.add(a.sno)
|
|
|
|
|
|
used.add(b.sno)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
if not ok or len(pairs) < m // 2:
|
|
|
|
|
|
continue
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Score colour quality
|
|
|
|
|
|
color_score = sum(_color_score(wp, bp, 'w') for wp, bp in pairs)
|
|
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
if color_score > best_score:
|
|
|
|
|
|
best_score = color_score
|
|
|
|
|
|
best_pairs = pairs
|
|
|
|
|
|
best_floaters = [floater] if floater else []
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
if not best_pairs and n % 2 == 1:
|
2026-06-14 20:29:27 +00:00
|
|
|
|
# Fallback: float the last player
|
2026-06-14 13:57:32 +00:00
|
|
|
|
floater = available[-1]
|
|
|
|
|
|
rest = available[:-1]
|
|
|
|
|
|
m = len(rest)
|
|
|
|
|
|
best_pairs = []
|
2026-06-14 20:29:27 +00:00
|
|
|
|
best_floaters = [floater]
|
2026-06-14 13:57:32 +00:00
|
|
|
|
for i in range(m // 2):
|
|
|
|
|
|
a, b = rest[i], rest[m // 2 + i]
|
|
|
|
|
|
color = _assign_colors(a, b)
|
|
|
|
|
|
if color == 'w':
|
|
|
|
|
|
best_pairs.append((a, b))
|
|
|
|
|
|
else:
|
|
|
|
|
|
best_pairs.append((b, a))
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
if not best_pairs:
|
|
|
|
|
|
# Desperate fallback: force-pair even if already played (no other option)
|
|
|
|
|
|
# This can happen when only 2 players in a score group have met before
|
|
|
|
|
|
rest = list(available)
|
|
|
|
|
|
m = len(rest)
|
|
|
|
|
|
if m >= 2:
|
|
|
|
|
|
best_pairs = []
|
|
|
|
|
|
best_floaters = []
|
|
|
|
|
|
if m % 2 == 1:
|
|
|
|
|
|
best_floaters = [rest[-1]]
|
|
|
|
|
|
rest = rest[:-1]
|
|
|
|
|
|
m = len(rest)
|
|
|
|
|
|
for i in range(m // 2):
|
|
|
|
|
|
a, b = rest[i], rest[m // 2 + i]
|
|
|
|
|
|
color = _assign_colors(a, b)
|
|
|
|
|
|
if color == 'w':
|
|
|
|
|
|
best_pairs.append((a, b))
|
|
|
|
|
|
else:
|
|
|
|
|
|
best_pairs.append((b, a))
|
|
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
for wp, bp in best_pairs:
|
|
|
|
|
|
all_paired.add(wp.sno)
|
|
|
|
|
|
all_paired.add(bp.sno)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
return best_pairs, best_floaters
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════
|
|
|
|
|
|
# ОСНОВНОЙ АЛГОРИТМ
|
|
|
|
|
|
# ═══════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
def fide_swiss_pairing(players: list, current_round: int) -> list:
|
2026-06-14 20:29:27 +00:00
|
|
|
|
"""FIDE Swiss pairings with fold + offset search.
|
|
|
|
|
|
|
|
|
|
|
|
Players sorted by (-points, -rating).
|
|
|
|
|
|
Score brackets processed from highest to lowest.
|
|
|
|
|
|
Downfloaters paired with the top of the lower bracket.
|
|
|
|
|
|
Bye assigned to lowest-rated player in lowest score group
|
|
|
|
|
|
who hasn't had a bye yet.
|
|
|
|
|
|
"""
|
2026-06-14 13:57:32 +00:00
|
|
|
|
sorted_players = sorted(players, key=sort_key)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Bye — assign to eligible player in lowest score group
|
2026-06-14 13:57:32 +00:00
|
|
|
|
if len(sorted_players) % 2 == 1:
|
|
|
|
|
|
groups = defaultdict(list)
|
|
|
|
|
|
for p in sorted_players:
|
|
|
|
|
|
groups[p.points].append(p)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
# Lowest score group, by rating ascending, excluding previous bye receivers
|
|
|
|
|
|
lowest_group = groups[min(groups.keys())]
|
|
|
|
|
|
lowest_group.sort(key=lambda p: (p.had_bye, p.rating))
|
|
|
|
|
|
bye_player = lowest_group[0]
|
2026-06-14 13:57:32 +00:00
|
|
|
|
sorted_players = [p for p in sorted_players if p.sno != bye_player.sno]
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Create score brackets
|
2026-06-14 13:57:32 +00:00
|
|
|
|
brackets = []
|
|
|
|
|
|
i = 0
|
|
|
|
|
|
while i < len(sorted_players):
|
|
|
|
|
|
score = sorted_players[i].points
|
|
|
|
|
|
group = []
|
|
|
|
|
|
while i < len(sorted_players) and sorted_players[i].points == score:
|
|
|
|
|
|
group.append(sorted_players[i])
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
brackets.append(group)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
all_pairs = []
|
|
|
|
|
|
all_paired = set()
|
|
|
|
|
|
downfloaters = []
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
for group in brackets:
|
2026-06-14 20:29:27 +00:00
|
|
|
|
# Available players in this bracket
|
|
|
|
|
|
bracket_avail = [p for p in group if p.sno not in all_paired]
|
|
|
|
|
|
|
|
|
|
|
|
# Downfloaters from above (have more points than this bracket)
|
|
|
|
|
|
floaters = [df for df in downfloaters if df.sno not in all_paired]
|
|
|
|
|
|
|
|
|
|
|
|
if len(bracket_avail) + len(floaters) < 2:
|
|
|
|
|
|
downfloaters = bracket_avail + floaters
|
2026-06-14 13:57:32 +00:00
|
|
|
|
continue
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
|
|
|
|
|
# Priority: pair each downfloater with a bracket member
|
|
|
|
|
|
# (downfloaters get paired with top-rated bracket members)
|
|
|
|
|
|
bracket_avail.sort(key=lambda p: -p.rating)
|
|
|
|
|
|
floaters.sort(key=lambda p: -p.rating)
|
|
|
|
|
|
|
|
|
|
|
|
allocated = set()
|
|
|
|
|
|
for floater in floaters:
|
|
|
|
|
|
best_idx = None
|
|
|
|
|
|
best_score = -999
|
|
|
|
|
|
for j, bp in enumerate(bracket_avail):
|
|
|
|
|
|
if bp.sno in allocated:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if floater.has_played(bp.sno):
|
|
|
|
|
|
continue
|
|
|
|
|
|
color = _assign_colors(floater, bp)
|
|
|
|
|
|
score = _color_score(floater, bp, color)
|
|
|
|
|
|
if score > best_score:
|
|
|
|
|
|
best_score = score
|
|
|
|
|
|
best_idx = j
|
|
|
|
|
|
if best_idx is not None:
|
|
|
|
|
|
bp = bracket_avail[best_idx]
|
|
|
|
|
|
color = _assign_colors(floater, bp)
|
|
|
|
|
|
if color == 'w':
|
|
|
|
|
|
all_pairs.append((floater, bp))
|
|
|
|
|
|
else:
|
|
|
|
|
|
all_pairs.append((bp, floater))
|
|
|
|
|
|
all_paired.add(floater.sno)
|
|
|
|
|
|
all_paired.add(bp.sno)
|
|
|
|
|
|
allocated.add(bp.sno)
|
|
|
|
|
|
downfloaters = [df for df in downfloaters if df.sno != floater.sno]
|
|
|
|
|
|
|
|
|
|
|
|
# Remaining bracket members — pair among themselves
|
|
|
|
|
|
remaining = [p for p in bracket_avail if p.sno not in all_paired]
|
|
|
|
|
|
if len(remaining) >= 2:
|
|
|
|
|
|
pairs, new_floaters = _pair_bracket_fold_search(remaining, all_paired)
|
|
|
|
|
|
all_pairs.extend(pairs)
|
|
|
|
|
|
downfloaters = new_floaters + [df for df in downfloaters if df.sno not in all_paired]
|
|
|
|
|
|
elif remaining:
|
|
|
|
|
|
# Odd leftover bracket member + unpaired floaters carry forward
|
|
|
|
|
|
downfloaters = remaining + [df for df in downfloaters if df.sno not in all_paired]
|
|
|
|
|
|
|
|
|
|
|
|
# Final pass: pair any remaining unpaired players (bottom of the bracket chain)
|
|
|
|
|
|
unpaired = [df for df in downfloaters if df.sno not in all_paired]
|
|
|
|
|
|
if unpaired:
|
|
|
|
|
|
unpaired.sort(key=lambda p: (-p.points, -p.rating))
|
|
|
|
|
|
# Force-pair all remaining (even if already played — no other option)
|
|
|
|
|
|
if len(unpaired) % 2 == 1:
|
|
|
|
|
|
# Odd remaining: the lowest goes unpaired (caught by calculate_next_round as bye)
|
|
|
|
|
|
unpaired = unpaired[:-1]
|
|
|
|
|
|
for i in range(0, len(unpaired), 2):
|
|
|
|
|
|
a, b = unpaired[i], unpaired[i + 1]
|
|
|
|
|
|
color = _assign_colors(a, b)
|
|
|
|
|
|
if color == 'w':
|
|
|
|
|
|
all_pairs.append((a, b))
|
|
|
|
|
|
else:
|
|
|
|
|
|
all_pairs.append((b, a))
|
|
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
return all_pairs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def swiss_pairing(players: list, current_round: int) -> list:
|
|
|
|
|
|
return fide_swiss_pairing(players, current_round)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_next_round(tournament_data: dict) -> dict:
|
|
|
|
|
|
standings = tournament_data['standings']
|
|
|
|
|
|
current_round = tournament_data['current_round']
|
|
|
|
|
|
next_round = current_round + 1
|
2026-06-14 20:29:27 +00:00
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
player_map = {}
|
2026-06-14 20:29:27 +00:00
|
|
|
|
sno_to_player = {s.get('starting_sno', s['rank']): None for s in standings}
|
2026-06-14 13:57:32 +00:00
|
|
|
|
for s in standings:
|
|
|
|
|
|
rank = s['rank']
|
2026-06-14 20:29:27 +00:00
|
|
|
|
sno = s.get('starting_sno', rank)
|
2026-06-14 13:57:32 +00:00
|
|
|
|
p = Player(
|
2026-06-14 20:29:27 +00:00
|
|
|
|
sno=sno,
|
2026-06-14 13:57:32 +00:00
|
|
|
|
name=s['name'], rating=s.get('rating', 0),
|
|
|
|
|
|
fed=s['fed'], points=s['points'], results=s['results'])
|
|
|
|
|
|
p.rank = rank
|
|
|
|
|
|
p.tb = s.get('tb', [])
|
|
|
|
|
|
player_map[rank] = p
|
2026-06-14 20:29:27 +00:00
|
|
|
|
sno_to_player[sno] = p
|
|
|
|
|
|
|
|
|
|
|
|
# Try bbpPairings first (FIDE 2025 Dutch System engine)
|
|
|
|
|
|
bbp_error = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
from .trf_generator import generate_trf
|
|
|
|
|
|
from .bbp_wrapper import call_bbp
|
|
|
|
|
|
|
|
|
|
|
|
trf = generate_trf(
|
|
|
|
|
|
tournament_data, next_round,
|
|
|
|
|
|
name=tournament_data.get('name', 'Chess Tournament'),
|
|
|
|
|
|
use_rank=False,
|
|
|
|
|
|
initial_color_white=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
bbp_pairs, _ = call_bbp(trf)
|
|
|
|
|
|
|
|
|
|
|
|
if bbp_pairs:
|
|
|
|
|
|
pairings = []
|
|
|
|
|
|
for w_sno, b_sno in bbp_pairs:
|
|
|
|
|
|
wp = sno_to_player.get(w_sno)
|
|
|
|
|
|
if wp is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if b_sno == 0:
|
|
|
|
|
|
pairings.append((wp, wp, 'bye'))
|
2026-06-14 13:57:32 +00:00
|
|
|
|
else:
|
2026-06-14 20:29:27 +00:00
|
|
|
|
bp = sno_to_player.get(b_sno)
|
|
|
|
|
|
if bp is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
pairings.append((wp, bp, 'w'))
|
|
|
|
|
|
if pairings:
|
|
|
|
|
|
return {
|
|
|
|
|
|
'round': next_round,
|
|
|
|
|
|
'pairings': pairings,
|
|
|
|
|
|
'players': player_map,
|
|
|
|
|
|
'source': 'bbp_pairings_fide_2025',
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
bbp_error = str(e)
|
|
|
|
|
|
|
|
|
|
|
|
if bbp_error:
|
|
|
|
|
|
import sys
|
|
|
|
|
|
print(f'⚠️ bbpPairings не сработал ({bbp_error}), использую упрощённый Swiss', file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
# Fallback: simplified Swiss algorithm
|
2026-06-14 13:57:32 +00:00
|
|
|
|
raw = fide_swiss_pairing(list(player_map.values()), current_round)
|
2026-06-14 20:29:27 +00:00
|
|
|
|
pairings = [(wp, bp, 'w') for wp, bp in raw]
|
|
|
|
|
|
|
|
|
|
|
|
# Check for missing players (bye from odd count not returned in raw pairs)
|
|
|
|
|
|
all_snos = set()
|
|
|
|
|
|
for wp, bp in raw:
|
|
|
|
|
|
all_snos.add(wp.sno)
|
|
|
|
|
|
all_snos.add(bp.sno)
|
|
|
|
|
|
for p in player_map.values():
|
|
|
|
|
|
if p.sno not in all_snos:
|
|
|
|
|
|
pairings.append((p, p, 'bye'))
|
|
|
|
|
|
|
2026-06-14 13:57:32 +00:00
|
|
|
|
return {'round': next_round,
|
2026-06-14 20:29:27 +00:00
|
|
|
|
'pairings': pairings,
|
|
|
|
|
|
'players': player_map, 'source': 'swiss_algorithm_simplified'}
|