feat: pairings output - show ±5 around subscribed player

- _build_table_rows: filter to ±5 boards around player
- Normalize names (strip commas) for matching FIDE vs pairings format
- Highlight subscribed player with  emoji
- Reduce name column width to compensate for emoji
- Add tournament name to pairing header
- Manual URL flow: look up subscription to highlight player
- Fix MarkdownV2 escaping for em-dash
This commit is contained in:
vrubel 2026-06-20 07:53:43 +00:00
parent 89f5861ee3
commit ba293dcb70
2 changed files with 73 additions and 14 deletions

View file

@ -104,12 +104,15 @@ def _pad_col(s: str, width: int) -> str:
return s + ' ' * (width - cur)
def format_pairings(pairings_data: dict, tournament_name: str, lang: str = '') -> str:
def format_pairings(pairings_data: dict, tournament_name: str, lang: str = '',
player_name: str = '') -> str:
locale = _get_locale(lang)
pairings = pairings_data['pairings']
rnd = pairings_data['round']
w_board, w_name, w_rating, w_pts, w_tb1 = 2, 17, 4, 4, 5
# If highlighting a player, reduce name width to compensate for ✅ emoji (visually 2 chars wide)
w_name = 15 if player_name else 17
w_board, w_rating, w_pts, w_tb1 = 2, 4, 4, 5
border = '+'.join('-' * w for w in [w_board, w_name, w_rating, w_pts, w_tb1])
header = (f'{_pad_col("#", w_board)}|{_pad_col(locale["player"], w_name)}|'
@ -121,23 +124,60 @@ def format_pairings(pairings_data: dict, tournament_name: str, lang: str = '') -
footer = locale['footer_bbp'] if source == 'bbp_pairings_fide_2025' else \
locale['footer_fallback'] if source == 'swiss_algorithm_simplified' else ''
safe_tour = _md_escape(tournament_name)
title = f'📋 *{safe_tour}*\n*{locale["title"].format(rnd=rnd)}*'
return {
'title': locale['title'].format(rnd=rnd),
'title': title,
'rows': _build_table_rows(pairings, w_board, w_name, w_rating, w_pts, w_tb1, border,
locale['bye']),
locale['bye'], player_name),
'table_header': table_header,
'footer': footer,
}
def _build_table_rows(pairings: list, w_board: int, w_name: int, w_rating: int,
w_pts: int, w_tb1: int, border: str, bye_label: str = '— BYE —') -> list:
w_pts: int, w_tb1: int, border: str, bye_label: str = '— BYE —',
player_name: str = '') -> list:
def _rating_str(p):
return str(p.rating) if p.rating else ''
def _tb1_str(p):
return f"{p.tb[0]:.1f}" if p.tb else ''
# Find the board where the player plays (normalize names: strip commas, collapse spaces)
def _n(s: str) -> str:
return s.replace(',', '').replace(' ', ' ').lower()
player_board = 0
board_idx = 1
player_norm = _n(player_name) if player_name else ''
for pairing in pairings:
if len(pairing) != 3:
continue
p1, p2, _color = pairing
if player_norm and (player_norm == _n(p1.name) or player_norm == _n(p2.name)):
player_board = board_idx
# Store the actual name from pairings for the ✅ marker
if player_norm == _n(p1.name):
player_name = p1.name
else:
player_name = p2.name
break
board_idx += 1
# If we have a player, show ±5 boards around them
if player_board > 0:
lo = max(1, player_board - 5)
hi = player_board + 5
else:
lo, hi = 1, 999999
def _name_cell(name: str) -> str:
if player_name and name == player_name:
return f'{name}'
return name
rows = []
board = 1
for pi, pairing in enumerate(pairings):
@ -145,10 +185,16 @@ def _build_table_rows(pairings: list, w_board: int, w_name: int, w_rating: int,
continue
p1, p2, color = pairing
if board < lo:
board += 1
continue
if board > hi:
break
if color == 'bye':
rows.append(
f'{_pad_col(str(board), w_board)}|'
f'{_pad_col(p1.name, w_name)}|'
f'{_pad_col(_name_cell(p1.name), w_name)}|'
f'{_pad_col(_rating_str(p1), w_rating)}|'
f'{_pad_col(f"{p1.points:.1f}", w_pts)}|'
f'{_pad_col(_tb1_str(p1), w_tb1)}'
@ -161,7 +207,7 @@ def _build_table_rows(pairings: list, w_board: int, w_name: int, w_rating: int,
f'{_pad_col("", w_tb1)}'
)
board += 1
if pi + 1 < len(pairings):
if pi + 1 < len(pairings) and board <= hi:
rows.append(border)
continue
@ -172,22 +218,21 @@ def _build_table_rows(pairings: list, w_board: int, w_name: int, w_rating: int,
rows.append(
f'{_pad_col(str(board), w_board)}|'
f'{_pad_col(white.name, w_name)}|'
f'{_pad_col(_name_cell(white.name), w_name)}|'
f'{_pad_col(_rating_str(white), w_rating)}|'
f'{_pad_col(f"{white.points:.1f}", w_pts)}|'
f'{_pad_col(_tb1_str(white), w_tb1)}'
)
rows.append(
f'{_pad_col("", w_board)}|'
f'{_pad_col(black.name, w_name)}|'
f'{_pad_col(_name_cell(black.name), w_name)}|'
f'{_pad_col(_rating_str(black), w_rating)}|'
f'{_pad_col(f"{black.points:.1f}", w_pts)}|'
f'{_pad_col(_tb1_str(black), w_tb1)}'
)
board += 1
# Separator between pairs
if pi + 1 < len(pairings):
if pi + 1 < len(pairings) and board <= hi:
rows.append(border)
return rows
@ -342,7 +387,7 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
safe_tour = _md_escape(t['name'])
lines.append(f'{safe_tour}{dates}')
lines.append('\nЯ буду присылать результаты и жеребьёвку\.\n'
'/myplayers список подписок')
'/myplayers \\- список подписок')
await update.message.reply_text('\n'.join(lines),
parse_mode=ParseMode.MARKDOWN_V2)
else:
@ -607,7 +652,20 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
return
lang_code = user.language_code or ''
fmt = format_pairings(result, tournament['name'], lang_code)
# If user has a subscription for this tournament, highlight their player
player_name = ''
try:
subs = tracker.get_user_subs(user.id)
for s in subs:
sub_tour = s.get('tournament_name', '')
if sub_tour and sub_tour == tournament.get('name', ''):
player_name = s.get('player_name', '')
break
except Exception:
pass
fmt = format_pairings(result, tournament['name'], lang_code, player_name)
chunks = _render_chunks(fmt)
try: