Основные изменения: - 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).
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
||
"""Test round-by-round: parse once, truncate results, calculate next round."""
|
||
|
||
import sys
|
||
sys.path.insert(0, '.')
|
||
|
||
from swiss_calc.parser import fetch_tournament, fetch_url, parse_round_pairings
|
||
from swiss_calc.trf_generator import generate_trf
|
||
from swiss_calc.bbp_wrapper import call_bbp
|
||
import re
|
||
|
||
def norm(n):
|
||
return re.sub(r'\s+', ' ', n.replace(',', '')).strip()
|
||
|
||
BASE = 'https://s1.chess-results.com/tnr1289471.aspx?lan=11'
|
||
|
||
print("=" * 60)
|
||
print("ТЕСТ: пошаговый расчёт всех туров")
|
||
print("=" * 60)
|
||
|
||
# Parse once — get full data with correct SNo, byes, forfeits
|
||
data = fetch_tournament(BASE)
|
||
total_rounds = data['current_round']
|
||
standings = data['standings']
|
||
|
||
print(f"Турнир: {data['name'][:60]}")
|
||
print(f"Сыграно туров: {total_rounds}")
|
||
print(f"Участников: {len(standings)}")
|
||
|
||
# Collect actual pairs for comparison
|
||
name_to_sno = {norm(s['name']): s['starting_sno'] for s in standings}
|
||
actual_pairs = {}
|
||
for rd in range(1, total_rounds + 1):
|
||
html = fetch_url(f'{BASE}&art=2&rd={rd}&turdet=YES')
|
||
games = parse_round_pairings(html, rd)
|
||
for g in games:
|
||
if g['white_sno'] == 0:
|
||
g['white_sno'] = name_to_sno.get(norm(g['white_name']), 0)
|
||
if g['black_sno'] == 0:
|
||
g['black_sno'] = name_to_sno.get(norm(g['black_name']), 0)
|
||
actual_pairs[rd] = games
|
||
print(f" Тур {rd}: {len(games)} пар")
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"ПОШАГОВЫЙ РАСЧЁТ")
|
||
print(f"{'='*60}")
|
||
|
||
overall_matched = 0
|
||
overall_total = 0
|
||
|
||
for next_rd in range(2, total_rounds + 1):
|
||
completed = next_rd - 1
|
||
|
||
# Truncate results to only first `completed` rounds
|
||
truncated_standings = []
|
||
for s in standings:
|
||
full_results = s.get('results', [])
|
||
full_orig = s.get('_orig_results', [])
|
||
|
||
# Take results only up to round `completed`
|
||
trunc_results = [r for r in full_results if r.get('round', 99) <= completed]
|
||
trunc_orig = [r for r in full_orig if isinstance(r, dict) and r.get('round', 99) <= completed]
|
||
|
||
points = sum(r['score'] for r in trunc_results)
|
||
|
||
truncated_standings.append({
|
||
'rank': s['rank'],
|
||
'name': s['name'],
|
||
'fed': s['fed'],
|
||
'points': points,
|
||
'starting_sno': s['starting_sno'],
|
||
'rating': s.get('rating', 0),
|
||
'results': trunc_results,
|
||
'_orig_results': trunc_orig,
|
||
'tb': s.get('tb', []),
|
||
})
|
||
|
||
truncated = {
|
||
'name': data['name'],
|
||
'num_rounds': total_rounds,
|
||
'current_round': completed,
|
||
'players': data['players'],
|
||
'standings': truncated_standings,
|
||
}
|
||
|
||
trf = generate_trf(truncated, next_rd, name=data.get('name', 'Test'),
|
||
use_rank=True, initial_color_white=True)
|
||
try:
|
||
bbp_pairs, _ = call_bbp(trf)
|
||
except Exception as e:
|
||
print(f" Тур {next_rd}: bbpPairings ОШИБКА — {e}")
|
||
continue
|
||
|
||
# Compare with actual
|
||
calc_set = set()
|
||
for w_sno, b_sno in bbp_pairs:
|
||
if b_sno == 0 or w_sno == 0:
|
||
continue
|
||
calc_set.add((min(w_sno, b_sno), max(w_sno, b_sno)))
|
||
|
||
real_set = set()
|
||
for g in actual_pairs.get(next_rd, []):
|
||
w, b = g['white_sno'], g['black_sno']
|
||
if w == 0 or b == 0:
|
||
continue
|
||
# Only include completed games (not future pairings)
|
||
if g.get('white_score') is not None:
|
||
real_set.add((min(w, b), max(w, b)))
|
||
|
||
matched = len(calc_set & real_set)
|
||
total = len(real_set)
|
||
if total == 0:
|
||
print(f" Тур {next_rd}: нет сыгранных пар для сравнения")
|
||
continue
|
||
|
||
pct = 100 * matched / total
|
||
overall_matched += matched
|
||
overall_total += total
|
||
|
||
bar = '█' * int(pct / 5) + '░' * (20 - int(pct / 5))
|
||
print(f" Тур {next_rd}: {matched}/{total} совпадений [{bar}] {pct:.0f}%")
|
||
|
||
print(f"\n{'='*60}")
|
||
if overall_total > 0:
|
||
print(f"ИТОГО: {overall_matched}/{overall_total} ({100*overall_matched/overall_total:.0f}%)")
|
||
else:
|
||
print(f"ИТОГО: 0 пар")
|
||
print(f"{'='*60}")
|