Upload clashcr/game/deck_tracker.py with huggingface_hub
Browse files- clashcr/game/deck_tracker.py +104 -0
clashcr/game/deck_tracker.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deck and cycle inference.
|
| 2 |
+
|
| 3 |
+
- Do not require knowing opponent deck in advance.
|
| 4 |
+
- Build opponent deck as cards are confirmed.
|
| 5 |
+
- Use deck/cycle inference only as a secondary prior, never as the main detector.
|
| 6 |
+
- Optionally use RoyaleAPI meta deck data after first 4-8 confirmed cards.
|
| 7 |
+
- Never emit a card solely because it fits a meta deck.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import logging
|
| 12 |
+
from dataclasses import dataclass, field
|
| 13 |
+
from typing import Dict, List, Optional, Set
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class DeckState:
|
| 20 |
+
confirmed_cards: List[str] = field(default_factory=list)
|
| 21 |
+
deck: Set[str] = field(default_factory=set) # unique cards seen
|
| 22 |
+
cycle: List[str] = field(default_factory=list) # last 4 played
|
| 23 |
+
hand_prediction: List[str] = field(default_factory=list)
|
| 24 |
+
cards_played: int = 0
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class DeckTracker:
|
| 28 |
+
"""Tracks opponent deck and card cycle from confirmed plays."""
|
| 29 |
+
|
| 30 |
+
DECK_SIZE = 8
|
| 31 |
+
CYCLE_SIZE = 4
|
| 32 |
+
|
| 33 |
+
def __init__(self):
|
| 34 |
+
self.state = DeckState()
|
| 35 |
+
self._meta_prior: Dict[str, float] = {} # card -> likelihood from meta
|
| 36 |
+
|
| 37 |
+
def reset(self) -> None:
|
| 38 |
+
self.state = DeckState()
|
| 39 |
+
self._meta_prior = {}
|
| 40 |
+
|
| 41 |
+
def register_play(self, card_key: str, confidence: float = 1.0) -> None:
|
| 42 |
+
if card_key == "UNKNOWN" or confidence < 0.5:
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
self.state.confirmed_cards.append(card_key)
|
| 46 |
+
self.state.deck.add(card_key)
|
| 47 |
+
self.state.cards_played += 1
|
| 48 |
+
|
| 49 |
+
# Update cycle
|
| 50 |
+
if card_key in self.state.cycle:
|
| 51 |
+
self.state.cycle.remove(card_key)
|
| 52 |
+
self.state.cycle.append(card_key)
|
| 53 |
+
if len(self.state.cycle) > self.CYCLE_SIZE:
|
| 54 |
+
self.state.cycle.pop(0)
|
| 55 |
+
|
| 56 |
+
self._update_hand_prediction()
|
| 57 |
+
|
| 58 |
+
def _update_hand_prediction(self) -> None:
|
| 59 |
+
if len(self.state.deck) < 5:
|
| 60 |
+
self.state.hand_prediction = []
|
| 61 |
+
return
|
| 62 |
+
|
| 63 |
+
# Standard cycle logic: hand = deck \ cycle
|
| 64 |
+
hand = [c for c in self.state.deck if c not in self.state.cycle]
|
| 65 |
+
self.state.hand_prediction = hand
|
| 66 |
+
|
| 67 |
+
def get_hand_prediction(self) -> List[str]:
|
| 68 |
+
return self.state.hand_prediction
|
| 69 |
+
|
| 70 |
+
def get_likely_remaining(self) -> List[str]:
|
| 71 |
+
"""Return cards likely still in deck, sorted by meta prior if available."""
|
| 72 |
+
if len(self.state.deck) < self.DECK_SIZE:
|
| 73 |
+
# Unknown cards
|
| 74 |
+
return []
|
| 75 |
+
|
| 76 |
+
remaining = [c for c in self.state.deck if c not in self.state.confirmed_cards[-self.CYCLE_SIZE:]]
|
| 77 |
+
if self._meta_prior:
|
| 78 |
+
remaining.sort(key=lambda c: self._meta_prior.get(c, 0.0), reverse=True)
|
| 79 |
+
return remaining
|
| 80 |
+
|
| 81 |
+
def load_meta_prior(self, meta_decks: List[List[str]]) -> None:
|
| 82 |
+
"""Load popular deck data from RoyaleAPI to build priors.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
meta_decks: List of 8-card deck lists.
|
| 86 |
+
"""
|
| 87 |
+
from collections import Counter
|
| 88 |
+
card_counts = Counter()
|
| 89 |
+
for deck in meta_decks:
|
| 90 |
+
for card in deck:
|
| 91 |
+
card_counts[card] += 1
|
| 92 |
+
|
| 93 |
+
total = len(meta_decks)
|
| 94 |
+
self._meta_prior = {
|
| 95 |
+
card: count / total
|
| 96 |
+
for card, count in card_counts.items()
|
| 97 |
+
}
|
| 98 |
+
logger.info("Loaded meta prior for %d cards from %d decks", len(self._meta_prior), total)
|
| 99 |
+
|
| 100 |
+
def is_new_card_plausible(self, card_key: str) -> bool:
|
| 101 |
+
"""Check if a never-seen card is plausible given current deck size."""
|
| 102 |
+
if len(self.state.deck) < self.DECK_SIZE:
|
| 103 |
+
return True
|
| 104 |
+
return card_key in self.state.deck
|