Files
deste-hearthstone-linux-tra…/data/decks.py
T
yusufipk c2a5a2d4d2 Hearthstone deck tracker for Linux, first public version
Reads the game's own Power.log: no screen capture, no image
recognition, no memory reading.

- deck tracking (remaining cards, draw chance, played cards fade in place)
- opponent panel (revealed cards, hand and deck counts)
- decks read from the game's offline cache, matched to the match
- card art in rows, full card on hover
- overlay and window modes, adjustable transparency, tray icon
- English and Turkish interface
- consistency tests over a real log corpus, cross-checked against Zone.log
2026-07-27 22:31:13 +07:00

175 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Deste kütüphanesi: oyunun önbelleğinden ve kullanıcının kaydettiği deste
kodlarından desteleri toplar, maça uygun olanı seçer.
Kart kimliği (CATA_560) ile dbfId (122939) arasındaki çeviriyi burada yapıyoruz,
core katmanı yalnızca kart kimlikleriyle çalışır.
"""
from __future__ import annotations
import json
import os
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from core import deckstring
from core.state import Game
from .cards import CardDB
from .localdecks import LocalDeck, read_all_decks
CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser() / "deste"
DECKS_PATH = CONFIG_DIR / "decks.json"
@dataclass
class TrackedDeck:
name: str
cards: Counter[str] = field(default_factory=Counter) # kart kimliği -> adet
hero_card_id: str = ""
player_class: str = ""
format_name: str = ""
source: str = "local" # local (oyun önbelleği) veya manual (deste kodu)
deck_code: str = ""
@property
def card_count(self) -> int:
return sum(self.cards.values())
def _deck_from_dbf(
cards_db: CardDB,
name: str,
dbf_cards: dict[int, int],
hero_dbf: int,
format_name: str,
source: str,
deck_code: str = "",
) -> TrackedDeck:
counter: Counter[str] = Counter()
for dbf_id, count in dbf_cards.items():
card_id = cards_db.id_from_dbf(dbf_id)
if card_id:
counter[card_id] += count
hero_card_id = cards_db.id_from_dbf(hero_dbf) if hero_dbf else ""
return TrackedDeck(
name=name,
cards=counter,
hero_card_id=hero_card_id,
player_class=cards_db.card_class(hero_card_id) if hero_card_id else "",
format_name=format_name,
source=source,
deck_code=deck_code,
)
def from_local(cards_db: CardDB, deck: LocalDeck) -> TrackedDeck:
return _deck_from_dbf(
cards_db, deck.name, deck.cards, deck.hero_dbf, deck.format_name, "local"
)
def from_deck_code(cards_db: CardDB, code: str, name: str = "") -> TrackedDeck:
"""Deste kodundan takip edilebilir deste üretir."""
parsed = deckstring.parse(code)
hero = parsed.heroes[0] if parsed.heroes else 0
return _deck_from_dbf(
cards_db,
name or "Deste kodu",
parsed.as_counter(),
hero,
parsed.format_name,
"manual",
code.strip(),
)
class DeckLibrary:
"""Oyunun önbelleğindeki ve elle eklenen desteler."""
def __init__(self, cards_db: CardDB, game_dir: Path | None = None):
self.cards_db = cards_db
self.game_dir = game_dir
self.decks: list[TrackedDeck] = []
def load(self) -> "DeckLibrary":
self.decks = []
if self.game_dir is not None:
for local in read_all_decks(self.game_dir):
self.decks.append(from_local(self.cards_db, local))
self.decks.extend(self._load_manual())
return self
def _load_manual(self) -> list[TrackedDeck]:
if not DECKS_PATH.exists():
return []
try:
payload = json.loads(DECKS_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return []
result: list[TrackedDeck] = []
for entry in payload.get("decks", []):
code = entry.get("code", "")
if not code:
continue
try:
result.append(from_deck_code(self.cards_db, code, entry.get("name", "")))
except deckstring.DeckstringError:
continue
return result
def add_manual(self, code: str, name: str = "") -> TrackedDeck:
"""Deste kodunu kaydeder ve kütüphaneye ekler."""
deck = from_deck_code(self.cards_db, code, name)
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
payload = {"decks": []}
if DECKS_PATH.exists():
try:
payload = json.loads(DECKS_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
pass
entries = payload.setdefault("decks", [])
if not any(e.get("code") == deck.deck_code for e in entries):
entries.append({"name": deck.name, "code": deck.deck_code})
DECKS_PATH.write_text(
json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
)
self.decks.append(deck)
return deck
# --- eşleştirme -----------------------------------------------------
def candidates(self, player_class: str) -> list[TrackedDeck]:
if not player_class:
return list(self.decks)
return [d for d in self.decks if d.player_class == player_class]
def match(self, game: Game) -> TrackedDeck | None:
"""Maça en uygun desteyi seçer.
Sınıfa göre süzer, sonra destemizden çıkmış kartlarla çelişmeyenleri
bırakır. Tek aday kalırsa odur; birden fazla aday varsa çıkan kartları
en çok kapsayan seçilir. Hiçbiri uymuyorsa None döner ve arayüz
kullanıcıya sorar.
"""
hero = game.hero_card_id(game.local_player_id)
player_class = self.cards_db.card_class(hero) if hero else ""
candidates = self.candidates(player_class)
if not candidates:
return None
consistent = [d for d in candidates if game.deck_list_mismatch(d.cards) == 0]
pool = consistent or candidates
if len(pool) == 1:
return pool[0]
def coverage(deck: TrackedDeck) -> tuple[int, int]:
covered = sum(
min(count, deck.cards.get(card, 0))
for card, count in game.cards_left_deck.items()
)
return (covered, -game.deck_list_mismatch(deck.cards))
return max(pool, key=coverage)