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 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3325e8ecc
commit
171fc64ca9
2 files changed
+50
-44
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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])
|
||||
Reference in new issue
Block a user