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:
parent
cf7fafd2ba
commit
bbbfe61094
11 changed files with 1432 additions and 488 deletions
178
swiss_calc/bbp_wrapper.py
Normal file
178
swiss_calc/bbp_wrapper.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue