From aa4d866d7d894f3c2abe7da57b4a8513584e5d99 Mon Sep 17 00:00:00 2001 From: VolandSZ <{E-MAIL}> Date: Sun, 13 Sep 2026 15:43:42 +0300 Subject: [PATCH] feat(battle): calibrate exact 5 player and 5 enemy card vertical slots --- .gitignore | 1 + bot_core.py | 55 +++++++++++++++++--- inspect_battle_cards.py | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 inspect_battle_cards.py diff --git a/.gitignore b/.gitignore index 757b5f7..451f873 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ logs/ *.log temp_*.png debug_*.png +debug_cards/ dataset/raw/*.png dataset/annotated/*.jpg dataset/images/ diff --git a/bot_core.py b/bot_core.py index 6aeae82..b1dedfd 100644 --- a/bot_core.py +++ b/bot_core.py @@ -49,6 +49,23 @@ class DirtyLeagueBot: REL_ROI_OFFER_CLOSE = (0.8854, 0.0, 1.0, 0.1463) REL_CROWN_CHEST_POS = (0.2279, 0.6146) + # 5 Player card slots (left vertical column) & 5 Opponent card slots (right vertical column) + PLAYER_CARD_SLOTS = [ + (0.226, 0.150, 0.294, 0.278), + (0.226, 0.295, 0.294, 0.423), + (0.226, 0.440, 0.294, 0.568), + (0.226, 0.585, 0.294, 0.713), + (0.226, 0.730, 0.294, 0.858), + ] + + OPPONENT_CARD_SLOTS = [ + (0.693, 0.150, 0.761, 0.278), + (0.693, 0.295, 0.761, 0.423), + (0.693, 0.440, 0.761, 0.568), + (0.693, 0.585, 0.761, 0.713), + (0.693, 0.730, 0.761, 0.858), + ] + def __init__(self, config: dict): self.config = config self.bot_conf = config.get("bot", {}) @@ -629,6 +646,23 @@ class DirtyLeagueBot: else: time.sleep(1.5) + def extract_battle_cards(self, frame: np.ndarray) -> Tuple[List[np.ndarray], List[np.ndarray]]: + """Extracts exact 5 player and 5 opponent card portrait crops from battle screen.""" + fh, fw = frame.shape[:2] + player_crops = [] + for rx1, ry1, rx2, ry2 in self.PLAYER_CARD_SLOTS: + x1, y1 = int(rx1 * fw), int(ry1 * fh) + x2, y2 = int(rx2 * fw), int(ry2 * fh) + player_crops.append(frame[y1:y2, x1:x2]) + + opponent_crops = [] + for rx1, ry1, rx2, ry2 in self.OPPONENT_CARD_SLOTS: + x1, y1 = int(rx1 * fw), int(ry1 * fh) + x2, y2 = int(rx2 * fw), int(ry2 * fh) + opponent_crops.append(frame[y1:y2, x1:x2]) + + return player_crops, opponent_crops + def handle_in_game(self, frame: np.ndarray, dets: Optional[Dict[str, List[Detection]]] = None): should_surrender = self.derank_mode or (self.current_wins >= self.max_wins_limit) reason = "Forced Derank" if self.derank_mode else f"Win Streak Limit ({self.current_wins}/{self.max_wins_limit})" @@ -638,16 +672,21 @@ class DirtyLeagueBot: self.fast_surrender_pipeline(max_wait_sec=5.0) return else: - if dets and "card" in dets: - cards = sorted(dets["card"], key=lambda d: d.box[0]) - recognized = [] - for d in cards: - crop = frame[d.box[1]:d.box[3], d.box[0]:d.box[2]] - name, sim = self.embedder.identify_card(crop) - recognized.append(f"{name or 'Card'} ({sim:.2f})") - logging.info(f"[IN_GAME] Detected {len(cards)} hand cards: {', '.join(recognized)}") + player_crops, opponent_crops = self.extract_battle_cards(frame) + player_names = [] + for i, crop in enumerate(player_crops, 1): + name, sim = self.embedder.identify_card(crop) + player_names.append(f"#{i}:{name or 'Unknown'}({sim:.2f})") + + enemy_names = [] + for i, crop in enumerate(opponent_crops, 1): + name, sim = self.embedder.identify_card(crop) + enemy_names.append(f"#{i}:{name or 'Unknown'}({sim:.2f})") + + logging.info(f"[IN_GAME] Player Team: [{', '.join(player_names)}] vs Opponent: [{', '.join(enemy_names)}]") logging.info("Match in progress (Normal Play, Auto-battle active). Waiting for match results...") + def handle_victory(self, frame: np.ndarray, dets: Optional[Dict[str, List[Detection]]] = None): if not self._result_recorded_for_match: self._result_recorded_for_match = True diff --git a/inspect_battle_cards.py b/inspect_battle_cards.py new file mode 100644 index 0000000..b7ed8c9 --- /dev/null +++ b/inspect_battle_cards.py @@ -0,0 +1,110 @@ +""" +Inspection script to calibrate exact bounding boxes for: +- 5 Player cards (left vertical column) +- 5 Opponent cards (right vertical column) +on the DirtyLeague Battle Screen (3840x2050 design resolution). +""" + +import os +import cv2 +import numpy as np +from card_embedder import CardEmbedder + + +# Normalized relative coordinates (rx1, ry1, rx2, ry2) for 3840x2050 +# Calibrated from actual 4K battle screenshot +PLAYER_CARD_SLOTS = [ + # Slot 1 (top left) + (0.226, 0.150, 0.294, 0.278), + # Slot 2 + (0.226, 0.295, 0.294, 0.423), + # Slot 3 + (0.226, 0.440, 0.294, 0.568), + # Slot 4 + (0.226, 0.585, 0.294, 0.713), + # Slot 5 (bottom left) + (0.226, 0.730, 0.294, 0.858), +] + +OPPONENT_CARD_SLOTS = [ + # Slot 1 (top right) + (0.693, 0.150, 0.761, 0.278), + # Slot 2 + (0.693, 0.295, 0.761, 0.423), + # Slot 3 + (0.693, 0.440, 0.761, 0.568), + # Slot 4 + (0.693, 0.585, 0.761, 0.713), + # Slot 5 (bottom right) + (0.693, 0.730, 0.761, 0.858), +] + + +def extract_cards(frame: np.ndarray): + fh, fw = frame.shape[:2] + player_crops = [] + opponent_crops = [] + + for rx1, ry1, rx2, ry2 in PLAYER_CARD_SLOTS: + x1, y1 = int(rx1 * fw), int(ry1 * fh) + x2, y2 = int(rx2 * fw), int(ry2 * fh) + player_crops.append(frame[y1:y2, x1:x2]) + + for rx1, ry1, rx2, ry2 in OPPONENT_CARD_SLOTS: + x1, y1 = int(rx1 * fw), int(ry1 * fh) + x2, y2 = int(rx2 * fw), int(ry2 * fh) + opponent_crops.append(frame[y1:y2, x1:x2]) + + return player_crops, opponent_crops + + +def main(): + img_path = os.path.join("dataset", "raw", "dl_screen_20260910_233743_310.png") + if not os.path.exists(img_path): + print(f"[ERROR] Sample battle screenshot not found at: {img_path}") + return + + frame = cv2.imread(img_path) + fh, fw = frame.shape[:2] + print(f"\nAnalyzing battle frame resolution: {fw}x{fh}") + + player_crops, opponent_crops = extract_cards(frame) + + os.makedirs("debug_cards", exist_ok=True) + embedder = CardEmbedder() + + print("\n--- Player Cards (Left Column 1..5) ---") + for i, crop in enumerate(player_crops, 1): + crop_path = os.path.join("debug_cards", f"player_card_{i}.png") + cv2.imwrite(crop_path, crop) + vec = embedder.embed(crop) + print(f" Player Slot #{i}: {crop.shape[1]}x{crop.shape[0]}px | Vector norm: {np.linalg.norm(vec):.4f} | Saved to: {crop_path}") + + print("\n--- Opponent Cards (Right Column 1..5) ---") + for i, crop in enumerate(opponent_crops, 1): + crop_path = os.path.join("debug_cards", f"opponent_card_{i}.png") + cv2.imwrite(crop_path, crop) + vec = embedder.embed(crop) + print(f" Opponent Slot #{i}: {crop.shape[1]}x{crop.shape[0]}px | Vector norm: {np.linalg.norm(vec):.4f} | Saved to: {crop_path}") + + # Draw visual overlay on whole frame + annotated = frame.copy() + for i, (rx1, ry1, rx2, ry2) in enumerate(PLAYER_CARD_SLOTS, 1): + x1, y1 = int(rx1 * fw), int(ry1 * fh) + x2, y2 = int(rx2 * fw), int(ry2 * fh) + cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 0), 3) + cv2.putText(annotated, f"Player #{i}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) + + for i, (rx1, ry1, rx2, ry2) in enumerate(OPPONENT_CARD_SLOTS, 1): + x1, y1 = int(rx1 * fw), int(ry1 * fh) + x2, y2 = int(rx2 * fw), int(ry2 * fh) + cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 0, 255), 3) + cv2.putText(annotated, f"Enemy #{i}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) + + preview_path = os.path.join("debug_cards", "annotated_battle_slots.png") + cv2.imwrite(preview_path, cv2.resize(annotated, (1920, 1025))) + print(f"\n[SUCCESS] Full annotated battle preview saved to: {preview_path}") + + +if __name__ == "__main__": + main()