stevenkhan commited on
Commit
94dc72c
·
verified ·
1 Parent(s): 3773161

Upload clashcr/game/deck_tracker.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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