Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions Src/bot_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def __init__(self, device=None):
self.unit_series
) = self.merge_series = self.df_groups = self.info = self.combat_step = None
self.logger = logging.getLogger("__main__")
logging.getLogger("ocr_utils").setLevel(logging.DEBUG)
if device is None:
device = port_scan.get_device()
if not device:
Expand Down Expand Up @@ -702,7 +703,20 @@ def mana_level(self, cards, hero_power=False):
# Start a dungeon floor from PvE page
def play_dungeon(self, floor=5):
self.logger.debug(f"Starting Dungeon floor {floor}")
chapter_num = int(np.ceil(floor / 3))
# Explicit mapping of floors to chapters
floor_map = {
1: range(1, 4),
2: range(4, 7),
3: range(7, 10),
4: range(10, 13),
5: [13],
6: [14],
}
chapter_num = 1
for chap, floors in floor_map.items():
if floor in floors:
chapter_num = chap
break
self.logger.debug(f"Looking for chapter {chapter_num}")
pos = np.array([0, 0])
avail_buttons = self.get_current_icons(available=True)
Expand All @@ -711,17 +725,26 @@ def play_dungeon(self, floor=5):
# Swipe to the top
[self.swipe([0, 0], [2, 0]) for _ in range(14)]
self.click(30, 600, 5) # stop scroll and scan screen for buttons
expanded = 0
self.getScreen()
# Log visible floors for debugging
visible = ocr_utils.find_chapter_headers(self.screenRGB)
for chap, xy in visible.items():
floors_here = ocr_utils.read_floor_from_chapter(self.screenRGB, xy)
self.logger.debug(f"Visible chapter {chap} floors: {floors_here}")

for i in range(12):
self.getScreen()
chapters = ocr_utils.find_chapter_headers(self.screenRGB)
self.logger.debug(f"Iteration {i}: OCR chapters {chapters}")
if chapter_num in chapters:
pos = np.array(chapters[chapter_num])
self.logger.info(f"Found chapter {chapter_num} at {pos}")
if not expanded:
expanded = 1
self.click_button(pos + [500, 90])
# Expand chapter only if a collapse icon is detected
icons = self.get_current_icons(available=True)
chapter_icon = f"chapter_{chapter_num}.png"
if (icons["icon"] == chapter_icon).any():
self.logger.debug(f"Chapter {chapter_num} collapsed, expanding")
self.click_button(pos)
self.getScreen()
if pos[1] < 550 and floor % 3 != 0:
break
Expand Down Expand Up @@ -760,7 +783,10 @@ def play_dungeon(self, floor=5):
chosen_offset = slot_offsets[3]

self.click_button(pos + chosen_offset)
self.click_button((500, 600))
# Play selected floor then choose random partner
self.click_button((500, 600)) # Play
time.sleep(0.5)
self.click_button((500, 800)) # Random
for i in range(10):
time.sleep(2)
avail_buttons = self.get_current_icons(available=True)
Expand Down
37 changes: 35 additions & 2 deletions Src/ocr_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

from typing import Dict, Optional, Tuple

import logging
import cv2
import numpy as np

logger = logging.getLogger(__name__)

try:
import os
import shutil
Expand All @@ -21,6 +24,7 @@
_tess_path = os.getenv("TESSERACT_PATH")
if _tess_path and os.path.exists(_tess_path):
pytesseract.pytesseract.tesseract_cmd = _tess_path
logger.debug("Using tesseract executable at %s", _tess_path)
else:
# Common Windows locations
candidates = [
Expand All @@ -31,35 +35,47 @@
for c in candidates:
if os.path.exists(c):
pytesseract.pytesseract.tesseract_cmd = c
logger.debug("Found tesseract executable at %s", c)
found = True
break
if not found:
which = shutil.which("tesseract.exe") or shutil.which("tesseract")
if which:
pytesseract.pytesseract.tesseract_cmd = which
logger.debug("Using tesseract executable from PATH: %s", which)
except Exception:
_TESS = False
logger.warning("Tesseract not available, OCR functions will be disabled")


def _prep_digits(img_bgr: np.ndarray) -> np.ndarray:
"""Preprocess ROI to improve OCR on digits: grayscale, CLAHE, blur, threshold, morph."""
logger.debug("Preprocessing digits ROI with shape %s", img_bgr.shape)
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
logger.debug("Converted ROI to grayscale")
# Local contrast boost
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
logger.debug("Applied CLAHE for local contrast enhancement")
# De-noise but keep edges
gray = cv2.bilateralFilter(gray, d=5, sigmaColor=40, sigmaSpace=40)
logger.debug("Applied bilateral filter to reduce noise")
# Adaptive threshold
th = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 5)
logger.debug("Applied adaptive thresholding")
# Morph to unify characters
kernel = np.ones((2, 2), np.uint8)
th = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel, iterations=1)
th = cv2.dilate(th, kernel, iterations=1)
logger.debug("Performed morphological operations")
# Scale up for better OCR
h, w = th.shape
scale = 2 if max(h, w) < 80 else 1
if scale != 1:
th = cv2.resize(th, (w * scale, h * scale), interpolation=cv2.INTER_CUBIC)
logger.debug("Resized ROI by factor %s", scale)
else:
logger.debug("No resizing applied (scale=1)")
return th


Expand All @@ -68,21 +84,26 @@ def ocr_digits(img_bgr: np.ndarray, psm: int = 7) -> Tuple[Optional[int], float]
psm 7=single line, 6=block; tries strict whitelist for 0-9.
"""
if not _TESS:
logger.debug("Tesseract not available, skipping OCR")
return None, 0.0
logger.debug("Running digit OCR with psm=%s", psm)
proc = _prep_digits(img_bgr)
logger.debug("Prepared ROI for OCR with shape %s", proc.shape)
cfg = f"--psm {psm} -c tessedit_char_whitelist=0123456789"
try:
txt = pytesseract.image_to_string(proc, config=cfg).strip()
logger.debug("Tesseract raw output: '%s'", txt)
# Optional: confidences via image_to_data
data = pytesseract.image_to_data(proc, config=cfg, output_type=pytesseract.Output.DICT)
confs = [float(c) for c in data.get("conf", []) if c not in ("-1", None)]
conf = (sum(confs) / len(confs)) / 100.0 if confs else 0.0
# Keep only digits
digits = "".join(ch for ch in txt if ch.isdigit())
logger.debug("Filtered digits='%s', confidence=%.2f", digits, conf)
if digits == "":
return None, conf
return int(digits), conf
except Exception:
except Exception as e:
logger.debug("pytesseract.image_to_string failed: %s", e)
return None, 0.0


Expand All @@ -107,17 +128,23 @@ def read_floor_from_chapter(
for slot, (rx, ry, rw, rh) in rois.items():
rx0, ry0 = max(0, rx), max(0, ry)
rx1, ry1 = min(w, rx + rw), min(h, ry + rh)
logger.debug("Slot %s ROI: x=%s y=%s w=%s h=%s", slot, rx, ry, rw, rh)
if rx1 <= rx0 or ry1 <= ry0:
logger.debug("Slot %s ROI out of bounds", slot)
results[slot] = (None, 0.0)
continue
roi = screen_bgr[ry0:ry1, rx0:rx1]
# Try psm 7 first, fallback psm 6
val, conf = ocr_digits(roi, psm=7)
logger.debug("Slot %s psm7 result: %s (conf=%.2f)", slot, val, conf)
if val is None or conf < 0.55:
logger.debug("Slot %s falling back to psm6", slot)
val2, conf2 = ocr_digits(roi, psm=6)
logger.debug("Slot %s psm6 result: %s (conf=%.2f)", slot, val2, conf2)
if (val2 is not None and conf2 >= conf) or val is None:
val, conf = val2, conf2
results[slot] = (val, conf)
logger.debug("Slot %s final result: %s (conf=%.2f)", slot, val, conf)
return results


Expand All @@ -126,22 +153,28 @@ def find_chapter_headers(screen_bgr: np.ndarray) -> Dict[int, Tuple[int, int]]:
Returns mapping {chapter_number: (x, y)} for the top-left of the word 'Chapter'.
"""
if not _TESS:
logger.debug("Tesseract not available, cannot find chapter headers")
return {}
logger.debug("Running OCR to find chapter headers")
try:
data = pytesseract.image_to_data(screen_bgr, output_type=pytesseract.Output.DICT)
except Exception as e:
if isinstance(e, (KeyboardInterrupt, SystemExit)):
raise
logger.debug("image_to_data failed: %s", e)
return {}
results: Dict[int, Tuple[int, int]] = {}
words = [w.strip().lower() for w in data.get("text", [])]
logger.debug("OCR detected %d words", len(words))
for i, w in enumerate(words):
if w == "chapter" and i + 1 < len(words):
try:
num = int(words[i + 1])
x = int(data["left"][i])
y = int(data["top"][i])
results[num] = (x, y)
logger.debug("Found chapter %s at (%s, %s)", num, x, y)
except (ValueError, TypeError):
logger.debug("Skipping invalid chapter entry at index %s", i)
continue
return results