From 171fc64ca92610a93f99cf9b59c3acc702e34f20 Mon Sep 17 00:00:00 2001 From: Aeth3r Date: Fri, 16 Oct 2026 12:40:00 -0400 Subject: [PATCH] Split turn stats into mirrored per-player tables Replace the "Selected turn" and "Whole game" tables (each with You, Opponent and Total columns) with one table per player: yours on the left (stat, turn, game) and the opponent's on the right, mirrored (game, turn, stat), so whole-game numbers sit next to the turn picker. Co-Authored-By: Claude Opus 5.5 --- README.md | 9 ++-- src/mtga_assistant/ui/turn_view.py | 85 ++++++++++++++++-------------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 4d64da2..e3e1c64 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ Two views, switched from the top bar (Ctrl+1 / Ctrl+2): emblems, hand/library/graveyard counts, turn timer and timeouts, land drops used/allowed, who was on the play, plus the match panel (turn, phase, game number, Bo3 score and draw count, game time). - - *Selected turn · Turn · Whole game*: what happened in a turn for you, your opponent and in - total (cards drawn, spells cast, lands played, creatures died, life gained/lost, damage, - discards, mills, exiles, counters, tokens, land drops). The middle panel picks the turn - (◀ ▶, or *Live* to follow the current one). + - *You · Turn · Opponent*: each player's stats for the selected turn and the whole game + (cards drawn, spells cast, lands played, creatures died, life gained/lost, damage, discards, + mills, exiles, counters, tokens, land drops). The two sides mirror each other, with the + whole-game column next to the middle panel, which picks the turn (◀ ▶, or *Live* to follow + the current one). - *My deck · Opponent · Preview*: remaining library with draw odds, your zones; the opponent's board, graveyard, exile, cards known to be in their hand or library, and every card seen this match; hover a card for its [Scryfall](https://scryfall.com) image. diff --git a/src/mtga_assistant/ui/turn_view.py b/src/mtga_assistant/ui/turn_view.py index d016957..f6fe04e 100644 --- a/src/mtga_assistant/ui/turn_view.py +++ b/src/mtga_assistant/ui/turn_view.py @@ -29,39 +29,50 @@ TURN_TOGGLES = [(LAND_DROP, "Land drop (played/allowed)")] + STATS TURN_DEFAULTS_OFF = {"permanents_died", "energy_spent"} -class StatsTable(QTableWidget): - def __init__(self, parent=None): +class PlayerStatsTable(QTableWidget): + """One player's stats: label | turn | game, or mirrored (game | turn | label) for the right side, + so the whole-game column always sits next to the turn picker in the middle.""" + + def __init__(self, mirrored: bool, parent=None): super().__init__(0, 3, parent) - self.setHorizontalHeaderLabels(["You", "Opponent", "Total"]) + self.mirrored = mirrored + self.label_col, self.turn_col, self.game_col = (2, 1, 0) if mirrored else (0, 1, 2) + headers = ["", "", ""] + headers[self.turn_col], headers[self.game_col] = "Turn", "Game" + self.setHorizontalHeaderLabels(headers) self.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) self.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.setShowGrid(False) self.setAlternatingRowColors(True) + self.verticalHeader().hide() self.verticalHeader().setDefaultSectionSize(28) self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed) - self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) - self.verticalHeader().setStyleSheet(f"QHeaderView::section {{ color: {theme.TEXT}; font-weight: 400;" - f" padding: 0 12px; border: none; }}") + hdr = self.horizontalHeader() + hdr.setSectionResizeMode(self.label_col, QHeaderView.ResizeMode.Stretch) + for col in (self.turn_col, self.game_col): + hdr.setSectionResizeMode(col, QHeaderView.ResizeMode.Fixed) + self.setColumnWidth(col, 80) + hdr.setDefaultAlignment(Qt.AlignmentFlag.AlignCenter) - def fill(self, rows: list[tuple[str, list[int | str | None]]]) -> None: - """rows: (label, [you, opponent, total]). Ints: 0 is dimmed. Strings shown as-is; None = n/a.""" + def fill(self, rows: list[tuple[str, int | str | None, int | str | None]]) -> None: + """rows: (label, this turn, whole game). Ints: 0 is dimmed. Strings shown as-is; None = n/a.""" self.setRowCount(len(rows)) - self.setVerticalHeaderLabels([label for label, _ in rows]) - for r, (_label, values) in enumerate(rows): - for c, v in enumerate(values): + label_align = (Qt.AlignmentFlag.AlignRight if self.mirrored else Qt.AlignmentFlag.AlignLeft) \ + | Qt.AlignmentFlag.AlignVCenter + for r, (label, turn_value, game_value) in enumerate(rows): + label_item = QTableWidgetItem(label) + label_item.setTextAlignment(label_align) + self.setItem(r, self.label_col, label_item) + for col, v in ((self.turn_col, turn_value), (self.game_col, game_value)): empty = v is None or v == 0 item = QTableWidgetItem("·" if v == 0 else "–" if v is None else str(v)) item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) if empty: item.setForeground(QColor(theme.BORDER)) - elif c == 2: + elif col == self.game_col: item.setForeground(QColor(theme.MINT_TEXT)) - self.setItem(r, c, item) - - -def counts_row(counts: Counter[int], me: int, opp: int) -> list[int]: - return [counts.get(me, 0), counts.get(opp, 0), counts.get(me, 0) + counts.get(opp, 0)] + self.setItem(r, col, item) def fraction(used: int, allowed: int) -> str: @@ -110,10 +121,12 @@ class TurnView(QWidget): self.picker_frame = card_frame("Turn", picker) self.picker_frame.setFixedWidth(170) - self.turn_table = StatsTable() - self.game_table = StatsTable() - self.turn_frame = card_frame("Selected turn", self.turn_table) - self.game_frame = card_frame("Whole game", self.game_table) + self.my_table = PlayerStatsTable(mirrored=False) + self.opp_table = PlayerStatsTable(mirrored=True) + self.my_frame = card_frame("You", self.my_table) + self.opp_frame = card_frame("Opponent", self.opp_table) + self.opp_title = self.opp_frame.findChild(QLabel) + self.opp_title.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.empty = QLabel("Turn stats appear once a game starts.") self.empty.setObjectName("muted") @@ -121,9 +134,9 @@ class TurnView(QWidget): lay = QHBoxLayout(self) lay.setContentsMargins(0, 0, 0, 0) - lay.addWidget(self.turn_frame, 1) + lay.addWidget(self.my_frame, 1) lay.addWidget(self.picker_frame) - lay.addWidget(self.game_frame, 1) + lay.addWidget(self.opp_frame, 1) lay.addWidget(self.empty, 1) def _turns(self, g: GameState) -> list[int]: @@ -157,7 +170,7 @@ class TurnView(QWidget): g = t.game has_game = g is not None self.empty.setVisible(not has_game) - for w in (self.turn_frame, self.picker_frame, self.game_frame): + for w in (self.my_frame, self.picker_frame, self.opp_frame): w.setVisible(has_game) if g is None: return @@ -177,23 +190,15 @@ class TurnView(QWidget): turn_stats = g.turns.turn(shown) totals = g.turns.game_totals() me, opp = g.my_seat, g.opp_seat + self.opp_title.setText(t.opponent_name or "Opponent") - def turn_row(key: str) -> list: + def row(key: str, label: str, seat: int) -> tuple: if key == LAND_DROP: - # Only the active player can play lands; show used/allowed for them. + # Only the active player plays lands: show used/allowed on their turns. drop = g.turns.land_drop(shown) if shown else None - active = g.turns.active.get(shown) - if drop is None: - return [None, None, None] - cell = fraction(*drop) - return [cell if active == me else None, cell if active == opp else None, cell] - return counts_row(turn_stats.get(key, Counter()), me, opp) + turn_cell = fraction(*drop) if drop and g.turns.active.get(shown) == seat else None + return label, turn_cell, fraction(*g.turns.land_drops_total(seat)) + return label, turn_stats.get(key, Counter()).get(seat, 0), totals.get(key, Counter()).get(seat, 0) - def game_row(key: str) -> list: - if key == LAND_DROP: - mine, theirs = g.turns.land_drops_total(me), g.turns.land_drops_total(opp) - return [fraction(*mine), fraction(*theirs), fraction(mine[0] + theirs[0], mine[1] + theirs[1])] - return counts_row(totals.get(key, Counter()), me, opp) - - self.turn_table.fill([(label, turn_row(key)) for key, label in enabled]) - self.game_table.fill([(label, game_row(key)) for key, label in enabled]) + self.my_table.fill([row(key, label, me) for key, label in enabled]) + self.opp_table.fill([row(key, label, opp) for key, label in enabled])