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

Upload clashcr/game/elixir_tracker.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. clashcr/game/elixir_tracker.py +123 -0
clashcr/game/elixir_tracker.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Elixir tracker: estimate opponent elixir from wall-clock regen and confirmed plays.
2
+
3
+ Regen schedule:
4
+ - Single elixir: 1 elixir per 2.8 seconds
5
+ - Double elixir: starts at 2:00 (1 elixir per 1.4 seconds)
6
+ - Triple elixir: starts at 3:00 (1 elixir per 0.933 seconds)
7
+ - Overtime: varies by mode; we use double/triple based on game mode hint.
8
+
9
+ Uncertainty bounds tracked explicitly.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from typing import Dict, List, Optional
17
+
18
+ from clashcr.utils.card_registry import CardRegistry
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class ElixirState:
25
+ current: float
26
+ max_elixir: float = 10.0
27
+ uncertainty_low: float = 0.0
28
+ uncertainty_high: float = 0.0
29
+ mode: str = "single" # single, double, triple
30
+ last_update: float = 0.0
31
+
32
+
33
+ class ElixirTracker:
34
+ """Tracks opponent elixir using wall-clock regeneration."""
35
+
36
+ BASE_REGEN_PER_SECOND = 1.0 / 2.8
37
+ MODE_MULTIPLIERS = {
38
+ "single": 1.0,
39
+ "double": 2.0,
40
+ "triple": 3.0,
41
+ "overtime_double": 2.0,
42
+ "overtime_triple": 3.0,
43
+ }
44
+
45
+ def __init__(self, registry: Optional[CardRegistry] = None):
46
+ self.registry = registry or CardRegistry()
47
+ if not self.registry.cards:
48
+ self.registry.load()
49
+ self.state = ElixirState(current=5.0, last_update=time.monotonic())
50
+ self._history: List[Dict] = []
51
+ self._match_start_time: Optional[float] = None
52
+ self._mode_schedule = [
53
+ (0.0, "single"),
54
+ (120.0, "double"),
55
+ (180.0, "triple"),
56
+ ]
57
+
58
+ def start_match(self) -> None:
59
+ self.state = ElixirState(current=5.0, last_update=time.monotonic())
60
+ self._history = []
61
+ self._match_start_time = time.monotonic()
62
+ logger.info("Elixir tracker reset for new match")
63
+
64
+ def _get_mode(self, elapsed: float) -> str:
65
+ mode = "single"
66
+ for threshold, m in self._mode_schedule:
67
+ if elapsed >= threshold:
68
+ mode = m
69
+ return mode
70
+
71
+ def update(self, force_mode: Optional[str] = None) -> ElixirState:
72
+ now = time.monotonic()
73
+ elapsed = now - self.state.last_update
74
+ if self._match_start_time:
75
+ match_elapsed = now - self._match_start_time
76
+ mode = force_mode or self._get_mode(match_elapsed)
77
+ else:
78
+ mode = force_mode or "single"
79
+
80
+ multiplier = self.MODE_MULTIPLIERS.get(mode, 1.0)
81
+ regen = elapsed * self.BASE_REGEN_PER_SECOND * multiplier
82
+
83
+ self.state.current = min(self.state.max_elixir, self.state.current + regen)
84
+ self.state.mode = mode
85
+ self.state.last_update = now
86
+
87
+ # Uncertainty grows with time since last confirmed event
88
+ self.state.uncertainty_high = min(2.0, elapsed * 0.5)
89
+ self.state.uncertainty_low = 0.0
90
+ return self.state
91
+
92
+ def deduct(self, card_key: str, confidence: float = 1.0) -> None:
93
+ """Deduct elixir for a confirmed opponent card play."""
94
+ if card_key == "UNKNOWN" or confidence < 0.5:
95
+ logger.debug("Skipping elixir deduction for %s (confidence=%.2f)", card_key, confidence)
96
+ return
97
+
98
+ cost = self.registry.get_cost(card_key)
99
+ if cost < 0:
100
+ logger.warning("Unknown elixir cost for %s", card_key)
101
+ cost = 0
102
+
103
+ self.update()
104
+ old = self.state.current
105
+ self.state.current = max(0.0, self.state.current - cost)
106
+ self.state.uncertainty_low = 0.0
107
+ self.state.uncertainty_high = 0.0
108
+
109
+ self._history.append({
110
+ "timestamp": time.monotonic(),
111
+ "card": card_key,
112
+ "cost": cost,
113
+ "before": old,
114
+ "after": self.state.current,
115
+ })
116
+ logger.debug("Deducted %d elixir for %s (%.1f -> %.1f)", cost, card_key, old, self.state.current)
117
+
118
+ def get_state(self) -> ElixirState:
119
+ return self.update()
120
+
121
+ def set_mode_schedule(self, schedule: List[tuple]) -> None:
122
+ """Override default mode schedule. List of (seconds, mode)."""
123
+ self._mode_schedule = schedule