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>
66 lines
2.3 KiB
Python
66 lines
2.3 KiB
Python
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) == []
|