mirror of
https://github.com/yusufipk/deste-hearthstone-linux-tracker.git
synced 2026-09-11 10:46:12 +00:00
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
This commit is contained in:
+174
@@ -0,0 +1,174 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user