simplify per-game table: tracked player only, no rating diff column
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 31s

Reworked the per-game row layout to be relative to the tracked player
instead of white/black: the tracked player's name+accuracy+rating are
always shown first, the opponent's rating+accuracy (no name) always
second, regardless of which color each side played in a given game -
this keeps columns aligned across rows even as the tracked player
switches sides. Dropped the rating-change-per-game column added
earlier; it pushed the line past mobile width limits.
This commit is contained in:
vrubelroman 2026-07-03 10:08:35 +00:00
parent d479e14bf9
commit 26cb066515
3 changed files with 29 additions and 39 deletions

View file

@ -47,22 +47,14 @@ class StatsFormatter:
tracked_won = (result == "1-0" and tracked_is_white) or (result == "0-1" and not tracked_is_white)
return "🟢" if tracked_won else "🔴"
@staticmethod
def _format_rating_diff_paren(diff: Optional[int]) -> str:
"""Format the tracked user's rating change for this game, e.g. '(+5)' / '(-7)' / '(0)'"""
diff = diff or 0
if diff > 0:
return f"(+{diff})"
elif diff < 0:
return f"({diff})"
else:
return "(0)"
@staticmethod
def _format_game_rows_block(rows: list) -> str:
"""
Format a list of individual game rows as a column-aligned, monospace table:
outcome | rating_diff | accuracy1 | White | rating1 | result | Black | rating2 | accuracy2
outcome | accuracy(tracked) | name(tracked) | rating(tracked) | result | rating(opponent) | accuracy(opponent)
Only the tracked player is named the opponent is shown by rating/accuracy
only, keeping the line short enough for mobile screens.
Column widths are computed from the actual data so every column lines up
character-for-character. Caller is expected to wrap the result in a
@ -76,29 +68,36 @@ class StatsFormatter:
prepared = []
for row in rows:
circle = StatsFormatter._format_row_outcome_circle(row)
diff_str = StatsFormatter._format_rating_diff_paren(row.get('tracked_rating_diff'))
white = (row.get('white_name') or '?')[:NAME_WIDTH]
black = (row.get('black_name') or '?')[:NAME_WIDTH]
wr = row.get('white_rating')
br = row.get('black_rating')
wr_str = str(wr) if wr is not None else "-"
br_str = str(br) if br is not None else "-"
wa = StatsFormatter._format_accuracy(row.get('white_accuracy'))
ba = StatsFormatter._format_accuracy(row.get('black_accuracy'))
if row.get('tracked_is_white') is False:
tracked_name = row.get('black_name') or '?'
tracked_rating = row.get('black_rating')
tracked_accuracy = row.get('black_accuracy')
opp_rating = row.get('white_rating')
opp_accuracy = row.get('white_accuracy')
else:
tracked_name = row.get('white_name') or '?'
tracked_rating = row.get('white_rating')
tracked_accuracy = row.get('white_accuracy')
opp_rating = row.get('black_rating')
opp_accuracy = row.get('black_accuracy')
tracked_name = tracked_name[:NAME_WIDTH]
tr_str = str(tracked_rating) if tracked_rating is not None else "-"
or_str = str(opp_rating) if opp_rating is not None else "-"
ta = StatsFormatter._format_accuracy(tracked_accuracy)
oa = StatsFormatter._format_accuracy(opp_accuracy)
result = StatsFormatter._format_result(row.get('result', ''))
prepared.append((circle, diff_str, wa, white, wr_str, result, black, br_str, ba))
prepared.append((circle, ta, tracked_name, tr_str, result, or_str, oa))
diff_width = max(len(p[1]) for p in prepared)
acc_width = max(max(len(p[2]), len(p[8])) for p in prepared)
acc_width = max(max(len(p[1]), len(p[6])) for p in prepared)
name_width = NAME_WIDTH
rating_width = max(max(len(p[4]), len(p[7])) for p in prepared)
result_width = max(len(p[5]) for p in prepared)
rating_width = max(max(len(p[3]), len(p[5])) for p in prepared)
result_width = max(len(p[4]) for p in prepared)
lines = []
for circle, diff_str, wa, white, wr_str, result, black, br_str, ba in prepared:
for circle, ta, tracked_name, tr_str, result, or_str, oa in prepared:
lines.append(
f"{circle} {diff_str:>{diff_width}} {wa:>{acc_width}} {white:<{name_width}} {wr_str:>{rating_width}} "
f"{result:^{result_width}} {black:<{name_width}} {br_str:>{rating_width}} {ba:>{acc_width}}"
f"{circle} {ta:>{acc_width}} {tracked_name:<{name_width}} {tr_str:>{rating_width}} "
f"{result:^{result_width}} {or_str:>{rating_width}} {oa:>{acc_width}}"
)
return "\n".join(lines)

View file

@ -163,7 +163,6 @@ class GameRow(BaseModel):
result: str = Field(..., description="Результат партии: '1-0', '0-1' или '1/2-1/2'")
created_at: int = Field(..., description="Время создания партии (Unix timestamp в миллисекундах)")
tracked_is_white: Optional[bool] = Field(None, description="True, если отслеживаемый пользователь играл белыми; False — чёрными; None — не удалось определить сторону")
tracked_rating_diff: Optional[int] = Field(None, description="Изменение рейтинга отслеживаемого пользователя по итогам этой партии")
class GameStats(BaseModel):
"""

View file

@ -583,13 +583,6 @@ class StatsService:
else:
tracked_is_white = None
if tracked_is_white is True:
tracked_rating_diff = white.get('ratingDiff')
elif tracked_is_white is False:
tracked_rating_diff = black.get('ratingDiff')
else:
tracked_rating_diff = None
return GameRow(
white_name=white_name,
black_name=black_name,
@ -599,8 +592,7 @@ class StatsService:
black_accuracy=(black.get('analysis') or {}).get('accuracy'),
result=result,
created_at=game.get('createdAt', 0),
tracked_is_white=tracked_is_white,
tracked_rating_diff=tracked_rating_diff
tracked_is_white=tracked_is_white
)
def _process_games_of_period(self, games: List[Dict[str, Any]], username: str, include_games: bool = False) -> GamesOfPeriodStats: