""" FIDE Swiss — упрощённый алгоритм на основе Folding с перебором offset. Вместо полного max-weight matching (как в JaVaFo), использует перебор вариантов fold + greedy цветовая оптимизация. Для post-round-1 (46 игроков с 1pt) даёт >40% совпадений. """ 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 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: """'w' если p1 белые, 'b' если p1 чёрные.""" pref1 = p1.preferred_color() pref2 = p2.preferred_color() if pref1 != pref2: return pref1 force1, force2 = p1.color_force(), p2.color_force() if force1 > force2: return pref1 if force2 > force1: return 'b' if pref1 == 'w' else 'w' if p1.rating >= p2.rating: return pref1 return 'b' if pref1 == 'w' else 'w' # ═══════════════════════════════════ # ПАРИРОВАНИЕ BRACKET — ПЕРЕБОР OFFSET # ═══════════════════════════════════ def _pair_bracket_fold_search( players: List[Player], all_paired: set, ) -> Tuple[List[Tuple[Player, Player]], List[Player]]: """Параметризованный fold с перебором offset и floaters. Для bracket размером N: 1. Если N нечётное — пробуем каждого как флоатера 2. Для оставшихся M (чётное) — пробуем fold с offset 0..M/2-1 3. Выбираем комбинацию с лучшим цветовым качеством """ available = [p for p in players if p.sno not in all_paired] n = len(available) if n < 2: return [], list(available) available.sort(key=lambda p: -p.rating) best_pairs = [] best_floaters = list(available[-1:]) if n % 2 == 1 else [] best_score = -9999 # Кандидаты на флоат floater_candidates = [None] if n % 2 == 1: floater_candidates = range(n) for fi in floater_candidates: if fi is not None: floater = available[fi] rest = available[:fi] + available[fi+1:] else: floater = None rest = available m = len(rest) # Перебор offset for offset in range(m // 2): pairs = [] ok = True used = set() for i in range(m // 2): a = rest[i] b = rest[m // 2 + ((i + offset) % (m // 2))] if a.has_played(b.sno): ok = False break if a.sno in used or b.sno in used: ok = False break 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) if not ok or len(pairs) < m // 2: continue # Оценка: цветовые несовпадения color_score = 0 for wp, bp in pairs: if wp.preferred_color() == 'w': color_score += 2 elif wp.color_force() >= 2: color_score -= 5 if bp.preferred_color() == 'b': color_score += 2 elif bp.color_force() >= 2: color_score -= 5 if color_score > best_score: best_score = color_score best_pairs = pairs best_floaters = [floater] if floater else [] if not best_pairs and n % 2 == 1: # Фолбэк: float последнего floater = available[-1] rest = available[:-1] m = len(rest) best_pairs = [] 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)) best_floaters = [floater] for wp, bp in best_pairs: all_paired.add(wp.sno) all_paired.add(bp.sno) return best_pairs, best_floaters # ═══════════════════════════════════ # ОСНОВНОЙ АЛГОРИТМ # ═══════════════════════════════════ def fide_swiss_pairing(players: list, current_round: int) -> list: """FIDE Swiss pairings через fold с перебором offset.""" sorted_players = sorted(players, key=sort_key) # Bye if len(sorted_players) % 2 == 1: groups = defaultdict(list) for p in sorted_players: groups[p.points].append(p) lowest = sorted(groups[min(groups.keys())], key=lambda p: p.rating) bye_player = lowest[0] sorted_players = [p for p in sorted_players if p.sno != bye_player.sno] # Score brackets 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) all_pairs = [] all_paired = set() downfloaters = [] for group in brackets: group_avail = [p for p in group if p.sno not in all_paired] for df in downfloaters: if df.sno not in all_paired: group_avail.append(df) if len(group_avail) < 2: downfloaters = group_avail continue pairs, downfloaters = _pair_bracket_fold_search(group_avail, all_paired) all_pairs.extend(pairs) 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 player_map = {} for s in standings: rank = s['rank'] p = Player( sno=s.get('starting_sno', rank), 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 # Предпочитаем предрассчитанные пары с сайта has_calculated = any(s.get('next_opponent') for s in standings) if has_calculated: pairings = [] paired = set() for s in standings: rank = s['rank'] if rank in paired: continue opp_rank = s.get('next_opponent') if opp_rank and opp_rank in player_map and opp_rank not in paired: color = s.get('next_color', 'w') p1, p2 = player_map[rank], player_map[opp_rank] if color == 'w': pairings.append((p1, p2, 'w')) else: pairings.append((p2, p1, 'w')) paired.add(rank); paired.add(opp_rank) return {'round': next_round, 'pairings': pairings, 'players': player_map, 'source': 'chess_results_precalculated'} raw = fide_swiss_pairing(list(player_map.values()), current_round) return {'round': next_round, 'pairings': [(wp, bp, 'w') for wp, bp in raw], 'players': player_map, 'source': 'swiss_algorithm'}