Add MTGA Assistant: native Linux deck and game tracker
PySide6 app that follows MTG Arena's Player.log under Steam/Proton and shows my remaining library with draw odds, my zones, and everything revealed about the opponent (board, graveyard, exile, known hand/library, seen this match). - Auto-detects the Steam library, Proton prefix log and MTGA install - Card names/costs/types from MTGA's local SQLite card database - Tracks card identity across zone changes, shuffles and Bo3 games - Scryfall card images with a lazy, size-capped LRU disk cache - Live log following with restart detection, plus --replay mode - Flags log gaps where MTGA omits oversized game-state messages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
734336168c
commit
7dc2ce0da8
21 files changed
+2736
No files matched your search
@@ -0,0 +1,65 @@
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from mtga_assistant.logparse import ClientDeck, DeckSubmitted, GreMessages, LogParser, MatchRoom
|
||||
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "bo3_match.log"
|
||||
|
||||
|
||||
def parse_file(path: Path) -> list:
|
||||
p = LogParser()
|
||||
out = []
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
out.extend(p.feed(line))
|
||||
return out
|
||||
|
||||
|
||||
def test_fixture_event_kinds():
|
||||
events = parse_file(FIXTURE)
|
||||
kinds = Counter(type(e).__name__ for e in events)
|
||||
assert kinds["MatchRoom"] == 2
|
||||
assert kinds["DeckSubmitted"] == 1
|
||||
assert kinds["ClientDeck"] == 1
|
||||
assert kinds["GreMessages"] >= 15
|
||||
|
||||
rooms = [e for e in events if isinstance(e, MatchRoom)]
|
||||
assert rooms[0].state == "MatchGameRoomStateType_Playing"
|
||||
assert rooms[1].state == "MatchGameRoomStateType_MatchCompleted"
|
||||
assert {p["playerName"] for p in rooms[0].players} == {"Local Player", "Opponent Player"}
|
||||
|
||||
|
||||
def test_deck_submitted():
|
||||
deck = next(e for e in parse_file(FIXTURE) if isinstance(e, DeckSubmitted))
|
||||
assert deck.name == "Cruel Tutelage"
|
||||
assert sum(deck.main.values()) == 60
|
||||
assert sum(deck.sideboard.values()) == 15
|
||||
|
||||
|
||||
def test_client_deck_from_pretty_printed_body():
|
||||
deck = next(e for e in parse_file(FIXTURE) if isinstance(e, ClientDeck))
|
||||
assert sum(deck.main.values()) == 60
|
||||
assert sum(deck.sideboard.values()) == 15
|
||||
|
||||
|
||||
def test_single_line_gre_event():
|
||||
p = LogParser()
|
||||
assert p.feed("[UnityCrossThreadLogger]10/16/2026 12:34:51 AM: Match to X: GreToClientEvent\n") == []
|
||||
evs = p.feed('{ "greToClientEvent": { "greToClientMessages": [ { "type": "GREMessageType_UIMessage" } ] } }\n')
|
||||
assert len(evs) == 1 and isinstance(evs[0], GreMessages)
|
||||
|
||||
|
||||
def test_multiline_body_and_abandoned_fragment():
|
||||
p = LogParser()
|
||||
assert p.feed("{\n") == []
|
||||
assert p.feed(' "payload": { "type": "Other" },\n') == []
|
||||
# A new log entry starts before the body closed: the fragment is dropped.
|
||||
assert p.feed("[UnityCrossThreadLogger]something\n") == []
|
||||
evs = p.feed('{ "greToClientEvent": { "greToClientMessages": [ { "type": "T" } ] } }\n')
|
||||
assert len(evs) == 1
|
||||
|
||||
|
||||
def test_non_json_lines_ignored():
|
||||
p = LogParser()
|
||||
for line in ["OnSceneLoaded for DuelScene", "[UnityCrossThreadLogger]STATE CHANGED {\"old\":1}", ""]:
|
||||
assert p.feed(line) == []
|
||||
Reference in new issue
Block a user