from mtga_assistant.gamestate import MatchTracker from mtga_assistant.logparse import GreMessages from mtga_assistant.turnstats import COUNTER_ENERGY, COUNTER_POISON from test_gamestate import ( BATTLEFIELD, FIXTURE, ME, MY_HAND, MY_LIB, OPP, OPP_GY, OPP_HAND, OPP_LIB, card, gsm, id_changed, replay, start_game, zone, ) STACK = 27 def transfer(iid: int, src: int, dest: int, category: str) -> dict: return {"type": ["AnnotationType_ZoneTransfer"], "affectedIds": [iid], "details": [ {"key": "zone_src", "valueInt32": [src]}, {"key": "zone_dest", "valueInt32": [dest]}, {"key": "category", "valueString": [category]}]} def life(seat: int, delta: int) -> dict: return {"type": ["AnnotationType_ModifiedLife"], "affectedIds": [seat], "details": [{"key": "life", "valueInt32": [delta]}]} def damage(seat: int, n: int) -> dict: return {"type": ["AnnotationType_DamageDealt"], "affectedIds": [seat], "details": [{"key": "damage", "valueInt32": [n]}]} def counter_added(seat: int, ctype: int, n: int, removed: bool = False) -> dict: kind = "AnnotationType_CounterRemoved" if removed else "AnnotationType_CounterAdded" return {"type": [kind], "affectedIds": [seat], "details": [ {"key": "counter_type", "valueInt32": [ctype]}, {"key": "transaction_amount", "valueInt32": [n]}]} def with_turn(msg: dict, turn: int, active: int) -> dict: msg["gameStateMessage"]["turnInfo"] = {"turnNumber": turn, "activePlayer": active, "phase": "Phase_Main1"} return msg def creature(iid: int, grp: int, zid: int, owner: int) -> dict: obj = card(iid, grp, zid, owner) obj["cardTypes"] = ["CardType_Creature"] return obj def test_turn_events_are_attributed_per_player_and_turn(): t = MatchTracker() start_game(t) # Turn 1 (mine): draw, cast a creature that resolves. t.handle(GreMessages([with_turn(gsm(2, annotations=[ id_changed(100, 150), transfer(150, MY_LIB, MY_HAND, "Draw"), ], zones=[zone(MY_LIB, "Library", [101, 102, 103], ME), zone(MY_HAND, "Hand", [150], ME)], objects=[card(150, 1001, MY_HAND, ME)]), 1, ME)])) t.handle(GreMessages([gsm(3, annotations=[ id_changed(150, 151), transfer(151, MY_HAND, STACK, "CastSpell"), ], zones=[zone(MY_HAND, "Hand", [], ME), zone(STACK, "Stack", [151])], objects=[creature(151, 1001, STACK, ME)])])) # Turn 2 (theirs): they draw (hidden), play a land, lose 3 life and take 2 damage. t.handle(GreMessages([with_turn(gsm(4, annotations=[ id_changed(300, 310), transfer(310, OPP_LIB, OPP_HAND, "Draw"), id_changed(200, 210), transfer(210, OPP_HAND, BATTLEFIELD, "PlayLand"), life(OPP, -3), damage(OPP, 2), ], zones=[zone(OPP_LIB, "Library", [301, 302], OPP), zone(OPP_HAND, "Hand", [201, 310], OPP), zone(BATTLEFIELD, "Battlefield", [210])], objects=[card(210, 5005, BATTLEFIELD)]), 2, OPP)])) # Their creature dies on turn 2 as well. t.handle(GreMessages([gsm(5, annotations=[ id_changed(210, 220), transfer(220, BATTLEFIELD, OPP_GY, "Destroy"), life(ME, 4), ], zones=[zone(BATTLEFIELD, "Battlefield", []), zone(OPP_GY, "Graveyard", [220], OPP)], objects=[creature(220, 5005, OPP_GY, OPP)])])) log = t.game.turns assert log.turn(1)["drawn"] == {ME: 1} assert log.turn(1)["cast"] == {ME: 1} t2 = log.turn(2) assert t2["drawn"] == {OPP: 1} assert t2["lands"] == {OPP: 1} assert t2["life_lost"] == {OPP: 3} assert t2["damage_taken"] == {OPP: 2} assert t2["creatures_died"] == {OPP: 1} assert t2["life_gained"] == {ME: 4} assert log.active == {1: ME, 2: OPP} totals = log.game_totals() assert totals["drawn"] == {ME: 1, OPP: 1} assert log.turn_numbers() == [1, 2] def test_player_counters_and_counter_stats(): t = MatchTracker() start_game(t) t.handle(GreMessages([with_turn(gsm(2, annotations=[ counter_added(OPP, COUNTER_POISON, 3), counter_added(ME, COUNTER_ENERGY, 2), ]), 3, ME)])) t.handle(GreMessages([gsm(3, annotations=[counter_added(ME, COUNTER_ENERGY, 1, removed=True)])])) g = t.game assert g.player_counters[OPP][COUNTER_POISON] == 3 assert g.player_counters[ME][COUNTER_ENERGY] == 1 stats = g.turns.turn(3) assert stats["poison_gained"] == {OPP: 3} assert stats["energy_gained"] == {ME: 2} assert stats["energy_spent"] == {ME: 1} def test_turn_timer_counts_down_from_message_time(): t = MatchTracker() start_game(t) msg = gsm(2) msg["gameStateMessage"]["players"] = [{"systemSeatNumber": ME, "timerIds": [3]}, {"systemSeatNumber": OPP, "timerIds": [9]}] msg["gameStateMessage"]["timers"] = [{"timerId": 3, "type": "TimerType_ActivePlayer", "durationSec": 60, "elapsedMs": 10_000, "running": True, "warningThresholdSec": 30}] t.handle(GreMessages([msg], timestamp_ms=1_000_000)) g = t.game assert g.turn_timer(ME, 1_000_000) == (50.0, 30.0, True) assert g.turn_timer(ME, 1_005_000) == (45.0, 30.0, True) assert g.turn_timer(OPP, 1_005_000) is None # A stopped timer reports its remaining time without counting down. msg = gsm(3) msg["gameStateMessage"]["timers"] = [{"timerId": 3, "type": "TimerType_ActivePlayer", "durationSec": 61, "warningThresholdSec": 30}] t.handle(GreMessages([with_turn(msg, 1, ME)], timestamp_ms=1_010_000)) assert g.turn_timer(ME, 1_090_000) == (61.0, 30.0, False) assert g.started_ms == 1_000_000 def test_fixture_draws_match_draw_transfers(): t = MatchTracker() replay(FIXTURE, t) g = t.game drawn = g.turns.game_totals().get("drawn", {}) # Game 2 of the fixture: the player on the draw draws every turn they take. my_turns = sum(1 for seat in g.turns.active.values() if seat == ME) assert drawn.get(ME, 0) >= my_turns - 1 # -- land drops ------------------------------------------------------------------ from mtga_assistant.carddb import CardInfo, LandRule # noqa: E402 STATIC_EXTRA, EACH_PLAYER, THIS_TURN = 13760, 1169, 6426 EXPLORE_GRP = 4242 # a sorcery whose ability grants one extra land this turn class LandCards: def get(self, grp): return CardInfo(grp_id=grp, name=str(grp)) def base_grp(self, grp): return grp def land_rules(self): return {STATIC_EXTRA: LandRule(1, False, False), EACH_PLAYER: LandRule(1, False, True), THIS_TURN: LandRule(1, True, False)} def ability_ids(self, grp): return (THIS_TURN,) if grp == EXPLORE_GRP else () def with_abilities(obj: dict, *ability_ids: int) -> dict: obj["uniqueAbilities"] = [{"id": i, "grpId": a} for i, a in enumerate(ability_ids)] return obj def resolved(affector: int, grp: int) -> dict: return {"type": ["AnnotationType_ResolutionComplete"], "affectorId": affector, "affectedIds": [affector], "details": [{"key": "grpid", "valueInt32": [grp]}]} def play_land(state_id: int, old: int, new: int, owner: int, hand: int) -> dict: return gsm(state_id, annotations=[id_changed(old, new), transfer(new, hand, BATTLEFIELD, "PlayLand")], objects=[card(new, 1001, BATTLEFIELD, owner)]) def test_land_drop_basic_turn(): t = MatchTracker(card_lookup=LandCards()) start_game(t) t.handle(GreMessages([with_turn(gsm(2), 1, ME)])) g = t.game assert g.land_drop(ME) == (0, 1) assert g.land_drop(OPP) is None # not their turn t.handle(GreMessages([play_land(3, 100, 150, ME, MY_HAND)])) assert g.land_drop(ME) == (1, 1) assert g.turns.land_drop(1) == (1, 1) def test_static_and_one_shot_extra_land_plays(): t = MatchTracker(card_lookup=LandCards()) start_game(t) # Turn 1: I control a permanent with "play an additional land on each of your turns". t.handle(GreMessages([with_turn(gsm(2, zones=[zone(BATTLEFIELD, "Battlefield", [500])], objects=[with_abilities(card(500, 9001, BATTLEFIELD, ME), STATIC_EXTRA)]), 1, ME)])) g = t.game assert g.land_drop(ME) == (0, 2) # Then I resolve a spell granting an additional land this turn. t.handle(GreMessages([gsm(3, annotations=[resolved(501, EXPLORE_GRP)], objects=[card(501, EXPLORE_GRP, 27, ME)])])) assert g.land_drop(ME) == (0, 3) t.handle(GreMessages([play_land(4, 100, 150, ME, MY_HAND)])) assert g.land_drop(ME) == (1, 3) # Turn 2 (theirs): my static doesn't help them; the one-shot is gone. t.handle(GreMessages([with_turn(gsm(5), 2, OPP)])) assert g.land_drop(OPP) == (0, 1) # An "each player" effect on the battlefield helps them too. t.handle(GreMessages([gsm(6, zones=[zone(BATTLEFIELD, "Battlefield", [500, 502])], objects=[with_abilities(card(502, 9002, BATTLEFIELD, ME), EACH_PLAYER)])])) assert g.land_drop(OPP) == (0, 2) # History keeps what each turn allowed. assert g.turns.land_drop(1) == (1, 3) assert g.turns.land_drop(2) == (0, 2) assert g.turns.land_drops_total(ME) == (1, 3)