chessCalc: исправлена точность жеребьёвки до 100%

Основные изменения:
- swiss.py: переписан fallback-алгоритм (bye, downfloat, цвета, форс-сведение)
- trf_generator.py: генератор TRF-16 для bbpPairings
- bbp_wrapper.py: обёртка subprocess для bbpPairings.exe
- parser.py: полное переписывание парсера art=2/art=4/art=5
  - поддержка 10- и 12-колоночных форматов
  - пересборка результатов из art=2 для корректных SNo
  - финальный проход для forfeit/bye результатов
  - нормализация имён и fuzzy-мэтчинг
  - фикс пустых SNo-колонок
- __main__.py: починен JSON-вывод, поддержка bye
- display.py: отображение bye и источника расчёта

Ключевой фикс точности: убран XXC rank из TRF — bbpPairings
теперь использует порядок из турнирной таблицы вместо поля Rank.
Проверено на 5 турнирах (4 из 5 — 100%, 1 — 85% из-за ручных bye).
This commit is contained in:
Roman Vrubel 2026-06-14 20:29:27 +00:00
parent cf7fafd2ba
commit bbbfe61094
11 changed files with 1432 additions and 488 deletions

178
swiss_calc/bbp_wrapper.py Normal file
View file

@ -0,0 +1,178 @@
"""
Wrapper for bbpPairings calls the external binary, parses output.
"""
import subprocess
import tempfile
import os
import re
from pathlib import Path
from typing import List, Tuple, Optional
# Path to bbpPairings binary — configurable
# Checks multiple locations: env var, project dir, system path
import sys as _sys
def _find_bbp() -> str:
"""Find bbpPairings binary."""
# Check env var
env_path = os.environ.get('BBP_PATH', '')
if env_path and os.path.exists(env_path):
return env_path
# Check project root (where this file is)
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
candidates = [
os.path.join(project_root, 'bbpPairings.exe'),
'/usr/local/bin/bbpPairings.exe',
'/tmp/bbpPairings/bbpPairings.exe',
]
for path in candidates:
if os.path.exists(path):
return path
return env_path or '/usr/local/bin/bbpPairings.exe'
BBP_PATH = _find_bbp()
class BbpError(Exception):
pass
def call_bbp(
trf_content: str,
bbp_path: str = BBP_PATH,
timeout: int = 60,
) -> Tuple[List[Tuple[int, int]], str]:
"""Call bbpPairings to compute next round pairings.
Args:
trf_content: TRF file content.
bbp_path: Path to bbpPairings binary.
timeout: Timeout in seconds.
Returns:
(pairings, checklist) where:
- pairings: list of (white_sno, black_sno) tuples.
If black_sno==0, white_sno has a bye.
- checklist: human-readable checklist text (only if -l flag used).
Raises:
BbpError: If bbpPairings fails.
"""
if not os.path.exists(bbp_path):
raise BbpError(f"bbpPairings binary not found at {bbp_path}")
with tempfile.NamedTemporaryFile(
mode='w', suffix='.trf', delete=False, encoding='utf-8'
) as f:
f.write(trf_content)
trf_path = f.name
try:
result = subprocess.run(
[bbp_path, '--dutch', trf_path, '-p'],
capture_output=True,
text=True,
timeout=timeout,
)
except subprocess.TimeoutExpired:
raise BbpError("bbpPairings timed out")
finally:
os.unlink(trf_path)
if result.returncode != 0:
stderr = result.stderr.strip()
raise BbpError(f"bbpPairings exited with {result.returncode}: {stderr}")
pairings = parse_pairing_output(result.stdout)
return pairings, ''
def parse_pairing_output(output: str) -> List[Tuple[int, int]]:
"""Parse bbpPairings -p output.
Format:
<count>
<white_sno> <black_sno>
...
<bye_sno> 0
"""
lines = output.strip().split('\n')
if not lines:
return []
try:
count = int(lines[0].strip())
except ValueError:
raise BbpError(f"Invalid pairing output: {output[:200]}")
pairings = []
for line in lines[1:]:
line = line.strip()
if not line:
continue
parts = line.split()
if len(parts) != 2:
continue
try:
white = int(parts[0])
black = int(parts[1])
except ValueError:
continue
pairings.append((white, black))
return pairings
def call_bbp_with_checklist(
trf_content: str,
bbp_path: str = BBP_PATH,
timeout: int = 60,
) -> Tuple[List[Tuple[int, int]], str]:
"""Call bbpPairings with checklist output.
Returns:
(pairings, checklist_text)
"""
if not os.path.exists(bbp_path):
raise BbpError(f"bbpPairings binary not found at {bbp_path}")
with tempfile.NamedTemporaryFile(
mode='w', suffix='.trf', delete=False, encoding='utf-8'
) as f:
f.write(trf_content)
trf_path = f.name
# Also create temp file for checklist
checklist_fd, checklist_path = tempfile.mkstemp(suffix='.txt')
try:
result = subprocess.run(
[bbp_path, '--dutch', trf_path, '-p', '-l', checklist_path],
capture_output=True,
text=True,
timeout=timeout,
)
except subprocess.TimeoutExpired:
raise BbpError("bbpPairings timed out")
finally:
os.unlink(trf_path)
if result.returncode != 0:
os.unlink(checklist_path)
stderr = result.stderr.strip()
raise BbpError(f"bbpPairings exited with {result.returncode}: {stderr}")
pairings = parse_pairing_output(result.stdout)
# Read checklist
with open(checklist_path, 'r') as f:
checklist = f.read()
os.unlink(checklist_path)
return pairings, checklist