Name emblems and boons instead of showing "Card #N"
Command-zone objects such as boons have small grpIds that aren't cards.
Name them from the log's name id (e.g. "The Ring") or the card that
created them ("Swiftspear's Teachings (boon)"), and preview the source
card on hover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f16a67e042
commit
d9ef771db8
4 files changed
+62
-10
No files matched your search
@@ -6,7 +6,7 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field, replace
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -117,6 +117,24 @@ def open_card_db(db: Path | None, copy_dir: Path, platform: str = sys.platform)
|
|||||||
return CardDB(db)
|
return CardDB(db)
|
||||||
|
|
||||||
|
|
||||||
|
def command_object_info(cards: "CardDB", obj: dict) -> CardInfo:
|
||||||
|
"""Display info for a non-card command-zone object (an emblem, boon, ...).
|
||||||
|
|
||||||
|
These have their own small grpIds that aren't cards (e.g. 6 for a boon), so naming them by
|
||||||
|
grpId gives "Card #6". Use the object's own name if the log gives one, otherwise the card that
|
||||||
|
created it. The returned grp_id is the source card's, so hovering previews that card.
|
||||||
|
"""
|
||||||
|
kind = (obj.get("type") or "").removeprefix("GameObjectType_") or "Emblem"
|
||||||
|
source = obj.get("objectSourceGrpId")
|
||||||
|
source_info = cards.get(source) if source else None
|
||||||
|
name = cards.loc(obj["name"]) if obj.get("name") else None
|
||||||
|
if not name:
|
||||||
|
name = f"{source_info.name} ({kind.lower()})" if source_info else kind
|
||||||
|
# Without a source card there's nothing sensible to preview (grp_id 0 = no hover preview).
|
||||||
|
base = source_info or CardInfo(grp_id=0, name=name)
|
||||||
|
return replace(base, name=name, mana=(), types=(kind,), is_token=False)
|
||||||
|
|
||||||
|
|
||||||
def unknown_card(grp_id: int) -> CardInfo:
|
def unknown_card(grp_id: int) -> CardInfo:
|
||||||
return CardInfo(grp_id=grp_id, name=f"Unknown card #{grp_id}")
|
return CardInfo(grp_id=grp_id, name=f"Unknown card #{grp_id}")
|
||||||
|
|
||||||
@@ -169,6 +187,17 @@ class CardDB:
|
|||||||
colors=tuple(c for c in WUBRG if c in color_set),
|
colors=tuple(c for c in WUBRG if c in color_set),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def loc(self, loc_id: int) -> str | None:
|
||||||
|
"""A localized (English) string by LocId, e.g. an emblem's name."""
|
||||||
|
if self._conn is None:
|
||||||
|
return None
|
||||||
|
row = self._conn.execute(
|
||||||
|
"SELECT Loc FROM Localizations_enUS WHERE LocId = ? ORDER BY Formatted = 1 DESC LIMIT 1",
|
||||||
|
(loc_id,),
|
||||||
|
).fetchone()
|
||||||
|
return _strip_tags(row[0]) if row and row[0] else None
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
@lru_cache(maxsize=None)
|
||||||
def counter_name(self, counter_type: int) -> str:
|
def counter_name(self, counter_type: int) -> str:
|
||||||
if self._conn is not None:
|
if self._conn is not None:
|
||||||
|
|||||||
@@ -295,17 +295,17 @@ class GameState:
|
|||||||
if seat := obj.get("ownerSeatId"):
|
if seat := obj.get("ownerSeatId"):
|
||||||
self.commander_casts[seat] += 1
|
self.commander_casts[seat] += 1
|
||||||
|
|
||||||
def emblems(self, seat: int) -> Counter[int]:
|
def emblems(self, seat: int) -> list[dict[str, Any]]:
|
||||||
"""Emblems and other non-card objects in the seat's command zone (commanders excluded)."""
|
"""Emblems, boons and other non-card objects in the seat's command zone (commanders excluded)."""
|
||||||
out: Counter[int] = Counter()
|
out = []
|
||||||
for z in self.zones.values():
|
for z in self.zones.values():
|
||||||
if z.type != "ZoneType_Command":
|
if z.type != "ZoneType_Command":
|
||||||
continue
|
continue
|
||||||
for iid in z.ids:
|
for iid in z.ids:
|
||||||
obj = self.objects.get(iid)
|
obj = self.objects.get(iid)
|
||||||
if obj and obj.get("grpId") and obj.get("type") != "GameObjectType_Card" \
|
if obj and obj.get("type") != "GameObjectType_Card" \
|
||||||
and obj.get("ownerSeatId", obj.get("controllerSeatId")) == seat:
|
and obj.get("ownerSeatId", obj.get("controllerSeatId")) == seat:
|
||||||
out[obj["grpId"]] += 1
|
out.append(obj)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def _land_rules(self) -> dict:
|
def _land_rules(self) -> dict:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from PySide6.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..carddb import CardDB
|
from ..carddb import CardDB, command_object_info
|
||||||
from ..gamestate import GameState, MatchTracker
|
from ..gamestate import GameState, MatchTracker
|
||||||
from . import theme
|
from . import theme
|
||||||
from .card_list import CardList, Row, Section
|
from .card_list import CardList, Row, Section
|
||||||
@@ -164,8 +164,12 @@ class PlayerCard(QFrame):
|
|||||||
self.commander.setVisible(t.on("commander") and bool(status))
|
self.commander.setVisible(t.on("commander") and bool(status))
|
||||||
|
|
||||||
emblems = g.emblems(seat)
|
emblems = g.emblems(seat)
|
||||||
|
counts: dict[str, list] = {} # name -> [info, count]
|
||||||
|
for obj in emblems:
|
||||||
|
info = command_object_info(self.cards, obj)
|
||||||
|
counts.setdefault(info.name, [info, 0])[1] += 1
|
||||||
self.emblems.set_sections([Section("emblems", "Emblems", [
|
self.emblems.set_sections([Section("emblems", "Emblems", [
|
||||||
Row(self.cards.get(grp), n) for grp, n in emblems.items()
|
Row(info, n) for info, n in counts.values()
|
||||||
])] if emblems else [])
|
])] if emblems else [])
|
||||||
self.emblems.setVisible(t.on("emblems") and bool(emblems))
|
self.emblems.setVisible(t.on("emblems") and bool(emblems))
|
||||||
|
|
||||||
|
|||||||
+21
-2
@@ -211,8 +211,8 @@ def test_commanders_are_not_emblems():
|
|||||||
g = t.game
|
g = t.game
|
||||||
assert g.commander_status(ME) == [(8000, "command zone")]
|
assert g.commander_status(ME) == [(8000, "command zone")]
|
||||||
assert g.commander_status(OPP) == [(9000, "command zone")]
|
assert g.commander_status(OPP) == [(9000, "command zone")]
|
||||||
assert g.emblems(OPP) == {7777: 1}
|
assert [o["grpId"] for o in g.emblems(OPP)] == [7777]
|
||||||
assert g.emblems(ME) == {}
|
assert g.emblems(ME) == []
|
||||||
assert sum(t.my_library_remaining().values()) == 3 # the commander isn't part of the library
|
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.
|
# Opponent casts their commander from the command zone: it's on the stack, tax goes up.
|
||||||
@@ -247,3 +247,22 @@ def test_known_cards_never_exceed_zone_size():
|
|||||||
t.handle(GreMessages([gsm(2, zones=[zone(OPP_HAND, "Hand", [201], OPP)], objects=[
|
t.handle(GreMessages([gsm(2, zones=[zone(OPP_HAND, "Hand", [201], OPP)], objects=[
|
||||||
card(400, 7007, OPP_HAND, otype="RevealedCard"), card(401, 7008, OPP_HAND, otype="RevealedCard")])]))
|
card(400, 7007, OPP_HAND, otype="RevealedCard"), card(401, 7008, OPP_HAND, otype="RevealedCard")])]))
|
||||||
assert sum(t.game.opp_known_in("ZoneType_Hand").values()) == 1
|
assert sum(t.game.opp_known_in("ZoneType_Hand").values()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_zone_objects_are_named_from_log_or_source_card():
|
||||||
|
from mtga_assistant.carddb import command_object_info
|
||||||
|
|
||||||
|
class Cards:
|
||||||
|
def get(self, grp):
|
||||||
|
return CardInfo(grp_id=grp, name={97038: "Swiftspear's Teachings", 87496: "The Ring"}.get(grp, f"Card #{grp}"))
|
||||||
|
|
||||||
|
def loc(self, loc_id):
|
||||||
|
return {732510: "The Ring"}.get(loc_id)
|
||||||
|
|
||||||
|
boon = {"grpId": 6, "type": "GameObjectType_Boon", "objectSourceGrpId": 97038}
|
||||||
|
emblem = {"grpId": 2, "type": "GameObjectType_Emblem", "objectSourceGrpId": 87496, "name": 732510}
|
||||||
|
bare = {"grpId": 3, "type": "GameObjectType_Emblem"}
|
||||||
|
b, e, x = (command_object_info(Cards(), o) for o in (boon, emblem, bare))
|
||||||
|
assert (b.name, b.types, b.grp_id) == ("Swiftspear's Teachings (boon)", ("Boon",), 97038) # hover shows the source card
|
||||||
|
assert (e.name, e.types, e.grp_id) == ("The Ring", ("Emblem",), 87496)
|
||||||
|
assert x.name == "Emblem" and x.grp_id == 0 # no source card: nothing to preview
|
||||||
Reference in new issue
Block a user