Add match, turn and history views with a dark theme

- Top bar switching between Deck, Match, Turn and History views
- Match view: life, player counters, commanders, emblems, zone counts,
  turn timer and timeouts, land drops used/allowed, play/draw and Bo3
  draw counts, with the deck view below
- Turn view: per-player and total stats per turn with history, plus
  whole-game totals and land drops
- History: every finished game (colours, winner, who was on the play);
  click to reopen it in the other views; imports Player-prev.log
- Customize menu to show/hide items and an Arrange dialog to reorder
  deck sections and turn stats, remembered across sessions
- Static dark theme with mint accent, rounded corners, larger text
- Commanders tracked separately from emblems, with location and tax
- Quieter, non-sticky warning for cards lost to MTGA log gaps
- Correct Scryfall images for tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aeth3randClaude Opus 5.5 committed 2026-10-16 11:18:44 -04:00
1 parent 7dc2ce0da8
commit 5b3c8c8e63
22 files changed
+2599 -193

No files matched your search

+31
View File
@@ -194,3 +194,34 @@ def test_object_logged_before_its_id_change_is_one_card():
t.handle(GreMessages([gsm(3, objects=[card(220, 5005, BATTLEFIELD)])]))
t.handle(GreMessages([gsm(4, annotations=[id_changed(210, 220)])]))
assert t.opp_seen_match == {5005: 1}
def test_commanders_are_not_emblems():
COMMAND, STACK = 26, 27
t = MatchTracker()
t.handle(GreMessages([{
"type": "GREMessageType_ConnectResp", "systemSeatIds": [ME],
"connectResp": {"deckMessage": {"deckCards": [1001] * 3, "commanderCards": [8000]}},
}]))
t.handle(GreMessages([gsm(1, full=True, zones=[
zone(COMMAND, "Command", [50, 51, 52]), zone(STACK, "Stack", []), zone(BATTLEFIELD, "Battlefield", []),
zone(MY_LIB, "Library", [100, 101, 102], ME), zone(OPP_HAND, "Hand", [], OPP),
], objects=[card(50, 8000, COMMAND, ME), card(51, 9000, COMMAND, OPP),
card(52, 7777, COMMAND, OPP, otype="Emblem")])]))
g = t.game
assert g.commander_status(ME) == [(8000, "command zone")]
assert g.commander_status(OPP) == [(9000, "command zone")]
assert g.emblems(OPP) == {7777: 1}
assert g.emblems(ME) == {}
assert sum(t.my_library_remaining().values()) == 3 # the commander isn't part of the library
# Opponent casts their commander from the command zone: it's on the stack, tax goes up.
cast = {"type": ["AnnotationType_ZoneTransfer"], "affectedIds": [60], "details": [
{"key": "zone_src", "valueInt32": [COMMAND]}, {"key": "zone_dest", "valueInt32": [STACK]},
{"key": "category", "valueString": ["CastSpell"]}]}
t.handle(GreMessages([gsm(2, annotations=[id_changed(51, 60), cast], zones=[
zone(COMMAND, "Command", [50, 52]), zone(STACK, "Stack", [60]),
], objects=[card(60, 9000, STACK, OPP)], deleted=[51])]))
assert g.commander_status(OPP) == [(9000, "stack")]
assert g.commander_tax(OPP) == 2
assert g.commander_tax(ME) == 0