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:
parent
89f5861ee3
commit
ba293dcb70
2 changed files with 73 additions and 14 deletions
|
|
@ -104,12 +104,15 @@ def _pad_col(s: str, width: int) -> str:
|
||||||
return s + ' ' * (width - cur)
|
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)
|
locale = _get_locale(lang)
|
||||||
pairings = pairings_data['pairings']
|
pairings = pairings_data['pairings']
|
||||||
rnd = pairings_data['round']
|
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])
|
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)}|'
|
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 \
|
footer = locale['footer_bbp'] if source == 'bbp_pairings_fide_2025' else \
|
||||||
locale['footer_fallback'] if source == 'swiss_algorithm_simplified' 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 {
|
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,
|
'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,
|
'table_header': table_header,
|
||||||
'footer': footer,
|
'footer': footer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_table_rows(pairings: list, w_board: int, w_name: int, w_rating: int,
|
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):
|
def _rating_str(p):
|
||||||
return str(p.rating) if p.rating else ''
|
return str(p.rating) if p.rating else ''
|
||||||
|
|
||||||
def _tb1_str(p):
|
def _tb1_str(p):
|
||||||
return f"{p.tb[0]:.1f}" if p.tb else ''
|
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 = []
|
rows = []
|
||||||
board = 1
|
board = 1
|
||||||
for pi, pairing in enumerate(pairings):
|
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
|
continue
|
||||||
p1, p2, color = pairing
|
p1, p2, color = pairing
|
||||||
|
|
||||||
|
if board < lo:
|
||||||
|
board += 1
|
||||||
|
continue
|
||||||
|
if board > hi:
|
||||||
|
break
|
||||||
|
|
||||||
if color == 'bye':
|
if color == 'bye':
|
||||||
rows.append(
|
rows.append(
|
||||||
f'{_pad_col(str(board), w_board)}|'
|
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(_rating_str(p1), w_rating)}|'
|
||||||
f'{_pad_col(f"{p1.points:.1f}", w_pts)}|'
|
f'{_pad_col(f"{p1.points:.1f}", w_pts)}|'
|
||||||
f'{_pad_col(_tb1_str(p1), w_tb1)}'
|
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)}'
|
f'{_pad_col("", w_tb1)}'
|
||||||
)
|
)
|
||||||
board += 1
|
board += 1
|
||||||
if pi + 1 < len(pairings):
|
if pi + 1 < len(pairings) and board <= hi:
|
||||||
rows.append(border)
|
rows.append(border)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -172,22 +218,21 @@ def _build_table_rows(pairings: list, w_board: int, w_name: int, w_rating: int,
|
||||||
|
|
||||||
rows.append(
|
rows.append(
|
||||||
f'{_pad_col(str(board), w_board)}|'
|
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(_rating_str(white), w_rating)}|'
|
||||||
f'{_pad_col(f"{white.points:.1f}", w_pts)}|'
|
f'{_pad_col(f"{white.points:.1f}", w_pts)}|'
|
||||||
f'{_pad_col(_tb1_str(white), w_tb1)}'
|
f'{_pad_col(_tb1_str(white), w_tb1)}'
|
||||||
)
|
)
|
||||||
rows.append(
|
rows.append(
|
||||||
f'{_pad_col("", w_board)}|'
|
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(_rating_str(black), w_rating)}|'
|
||||||
f'{_pad_col(f"{black.points:.1f}", w_pts)}|'
|
f'{_pad_col(f"{black.points:.1f}", w_pts)}|'
|
||||||
f'{_pad_col(_tb1_str(black), w_tb1)}'
|
f'{_pad_col(_tb1_str(black), w_tb1)}'
|
||||||
)
|
)
|
||||||
board += 1
|
board += 1
|
||||||
|
|
||||||
# Separator between pairs
|
if pi + 1 < len(pairings) and board <= hi:
|
||||||
if pi + 1 < len(pairings):
|
|
||||||
rows.append(border)
|
rows.append(border)
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
@ -342,7 +387,7 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
|
||||||
safe_tour = _md_escape(t['name'])
|
safe_tour = _md_escape(t['name'])
|
||||||
lines.append(f' • {safe_tour}{dates}')
|
lines.append(f' • {safe_tour}{dates}')
|
||||||
lines.append('\nЯ буду присылать результаты и жеребьёвку\.\n'
|
lines.append('\nЯ буду присылать результаты и жеребьёвку\.\n'
|
||||||
'/myplayers — список подписок')
|
'/myplayers \\- список подписок')
|
||||||
await update.message.reply_text('\n'.join(lines),
|
await update.message.reply_text('\n'.join(lines),
|
||||||
parse_mode=ParseMode.MARKDOWN_V2)
|
parse_mode=ParseMode.MARKDOWN_V2)
|
||||||
else:
|
else:
|
||||||
|
|
@ -607,7 +652,20 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
return
|
return
|
||||||
|
|
||||||
lang_code = user.language_code or ''
|
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)
|
chunks = _render_chunks(fmt)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -728,7 +728,8 @@ async def check_all_subscriptions(context):
|
||||||
if current_rd > sub['last_round_done'] and current_rd < tournament.get('num_rounds', 0):
|
if current_rd > sub['last_round_done'] and current_rd < tournament.get('num_rounds', 0):
|
||||||
try:
|
try:
|
||||||
result = calculate_next_round(tournament)
|
result = calculate_next_round(tournament)
|
||||||
fmt = format_pairings(result, tournament.get('name', ''), sub['lang'])
|
fmt = format_pairings(result, tournament.get('name', ''), sub['lang'],
|
||||||
|
sub['player_name'])
|
||||||
chunks = _render_chunks(fmt)
|
chunks = _render_chunks(fmt)
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue