diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 83972fa..0000000 --- a/.gitignore +++ /dev/null @@ -1,218 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -# Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -# poetry.lock -# poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -# pdm.lock -# pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -# pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# Redis -*.rdb -*.aof -*.pid - -# RabbitMQ -mnesia/ -rabbitmq/ -rabbitmq-data/ - -# ActiveMQ -activemq-data/ - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -# .idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ -# Temporary file for partial code execution -tempCodeRunnerFile.py - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ - -# Streamlit -.streamlit/secrets.toml diff --git a/C.A.T.S.: Crash Arena Turbo Stars b/C.A.T.S.: Crash Arena Turbo Stars deleted file mode 100644 index 8b13789..0000000 --- a/C.A.T.S.: Crash Arena Turbo Stars +++ /dev/null @@ -1 +0,0 @@ - diff --git a/README C.A.T.S.: Crash Arena Turbo Stars b/README C.A.T.S.: Crash Arena Turbo Stars deleted file mode 100644 index 50b4cf1..0000000 --- a/README C.A.T.S.: Crash Arena Turbo Stars +++ /dev/null @@ -1,116 +0,0 @@ -# πŸ€– C.A.T.S. BlueStacks Auto Fight Script - -An automation script that automatically farms Quick Fights in **C.A.T.S.: Crash Arena Turbo Stars** using BlueStacks 5. It detects win/loss outcomes and taps the correct button every time β€” no ads, no mistakes. - ---- - -## ✨ Features - -- Automatically taps Quick Fight, Start Fight, and the result button -- Detects **Victory**, **Defeat**, and **Defeat with Win Streak** separately -- Avoids accidental ad clicks on the "Keep Win Streak" button -- Tracks your win streak across fights -- Runs in a loop until you stop it - ---- - -## πŸ“‹ Requirements - -- [BlueStacks 5](https://www.bluestacks.com/) -- [Python 3.13+](https://www.python.org/downloads/) -- [ADB (Android Debug Bridge)](https://developer.android.com/tools/releases/platform-tools) -- Python libraries: - - `Pillow` - -Install the required library with: -```bash -pip install Pillow -``` - ---- - -## βš™οΈ Setup - -### 1. Enable ADB in BlueStacks -1. Open BlueStacks and go to **Settings β†’ Advanced** -2. Enable **Android Debug Bridge (ADB)** -3. Note the IP and port shown (usually `127.0.0.1:5555`) - -### 2. Download ADB -1. Download [Platform Tools](https://developer.android.com/tools/releases/platform-tools) from Google -2. Extract it to a folder on your PC (e.g. `C:\adb\`) - -### 3. Configure the script -Open `cats_farm.py` and update the following lines to match your setup: - -```python -ADB = r"C:\path\to\platform-tools\adb.exe" # Path to your adb.exe -DEVICE = "127.0.0.1:5555" # Your BlueStacks IP and port -``` - -### 4. Connect ADB to BlueStacks -Open a terminal and run: -```bash -adb connect 127.0.0.1:5555 -``` -You should see: `connected to 127.0.0.1:5555` - ---- - -## ▢️ How to Use - -1. Open BlueStacks and launch **C.A.T.S.** -2. Navigate to the **Quick Fight** screen -3. Run the script: -```bash -python cats_farm.py -``` -4. When prompted, enter your current win streak (or `0` if you have none) -5. Switch to BlueStacks β€” the script starts in 3 seconds! - -To stop the script press **Ctrl + C** in the terminal. - ---- - -## πŸ—ΊοΈ Button Coordinates - -The script uses percentage-based coordinates (works across resolutions). These are calibrated for the default BlueStacks 5 layout: - -| Action | X | Y | -|---|---|---| -| Quick Fight | 85 | 120 | -| Start Fight | 50 | 40 | -| OK (Victory) | 58 | 85 | -| Retreat (No streak) | 80 | 120 | -| Retreat (Streak loss) | 70 | 120 | - -> If buttons are in different positions on your setup, hover over them in BlueStacks to get the coordinates and update the `tap()` calls in the script. - ---- - -## πŸ”§ Troubleshooting - -**Script clicks the wrong button** -β†’ Hover over the correct button in BlueStacks and update the coordinates in the script. - -**Fight not finished before result is detected** -β†’ Increase `time.sleep(12)` to a higher value like `time.sleep(20)`. - -**ADB not recognized** -β†’ Make sure the path to `adb.exe` in the script is correct. - -**Unknown result detected** -β†’ The script will print the RGB value of the banner. Share it and the detection thresholds can be updated. - ---- - -## πŸ“„ License - -MIT License β€” free to use and modify. - ---- - -## πŸ™ Credits - -Built with Python + ADB + BlueStacks 5. -Game: [C.A.T.S.: Crash Arena Turbo Stars](https://zeptolab.com/games/cats) diff --git a/README.md b/README.md deleted file mode 100644 index adcf12b..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# BlueStacks-Scripts -I will put the script i use for BlueStacks games in here with a detailed readme - - diff --git a/__pycache__/cats_FarmV3cpython-313.pyc b/__pycache__/cats_FarmV3cpython-313.pyc new file mode 100644 index 0000000..c6e87c8 Binary files /dev/null and b/__pycache__/cats_FarmV3cpython-313.pyc differ diff --git a/cats_FarmV3 b/cats_FarmV3 new file mode 100644 index 0000000..9d7081d --- /dev/null +++ b/cats_FarmV3 @@ -0,0 +1,613 @@ +import subprocess +import os +import time +import threading +import msvcrt +import io +import shutil +from PIL import Image, ImageDraw, ImageFont +try: + import cv2 + import numpy as np +except Exception: + cv2 = None + np = None +# USE YOUR OWN PATH TO +ADB = r"platform-tools\adb.exe" +# USE YOUR OWN DEVICE IP/PORT +DEVICE = "127.0.0.1:5555" + +def get_adb_executable(): + # Prefer bundled adb if present, otherwise fall back to system adb in PATH + if os.path.exists(ADB): + return ADB + exe = shutil.which("adb") + if exe is None: + raise FileNotFoundError(f"adb not found. Checked: {ADB} and system PATH") + return exe + + +def list_connected_devices(): + exe = get_adb_executable() + res = subprocess.run([exe, "devices"], capture_output=True, text=True) + if res.returncode != 0: + return [] + devices = [] + for line in (res.stdout or "").splitlines(): + line = line.strip() + if not line or line.startswith("List of devices"): + continue + parts = line.split() + if len(parts) >= 2 and parts[1] == "device": + devices.append(parts[0]) + return devices + + +def connect_adb_device(): + if not DEVICE: + return False + exe = get_adb_executable() + res = subprocess.run([exe, "connect", DEVICE], capture_output=True, text=True) + out = (res.stdout or "").strip().lower() + err = (res.stderr or "").strip().lower() + success = "connected to" in out or "already connected" in out + if success: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"ADB connected to {DEVICE}: {out}") + return True + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"ADB connect failed for {DEVICE}: returncode={res.returncode} stdout={out} stderr={err}") + return False + + +def get_target_device(): + exe = get_adb_executable() + if DEVICE: + res = subprocess.run([exe, "-s", DEVICE, "get-state"], capture_output=True, text=True) + if res.returncode == 0 and "device" in (res.stdout or "").lower(): + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"Using configured ADB device: {DEVICE}") + return DEVICE + if connect_adb_device(): + return DEVICE + devices = list_connected_devices() + if devices: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"Auto-detected adb device: {devices[0]}") + return devices[0] + if os.getenv("CFV3_DEBUG", "0") == "1": + print("No adb devices found") + return None + + +def adb(cmd): + exe = get_adb_executable() + device = get_target_device() + if device: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"adb command using device {device}: {' '.join(cmd)}") + return subprocess.run([exe, "-s", device] + cmd, capture_output=True) + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"adb command without explicit device: {' '.join(cmd)}") + return subprocess.run([exe] + cmd, capture_output=True) + + +def capture_screen_bytes(): + result = adb(["exec-out", "screencap", "-p"]) + if result.returncode != 0 or not result.stdout: + if os.getenv("CFV3_DEBUG", "0") == "1": + stderr = result.stderr.decode(errors="ignore") if result.stderr else "" + print(f"adb screencap failed: returncode={result.returncode} stderr={stderr}") + return None + return result.stdout + + +# Event used to request the main loop to stop from a key press +stop_event = threading.Event() + + +def _watch_stop_key(): + """Watch for 'q' keypress in the console and set stop_event to stop the bot.""" + print("Press 'q' to stop the bot gracefully, or create stop.txt in the script folder.") + while not stop_event.is_set(): + if msvcrt.kbhit(): + ch = msvcrt.getwch() + if ch.lower() == 'q': + print("Stop key pressed. Exiting loop after current iteration...") + stop_event.set() + break + time.sleep(0.1) + + +def stop_requested(): + if stop_event.is_set(): + return True + if os.path.exists("stop.txt"): + print("Found stop.txt. Exiting loop after current iteration...") + try: + os.remove("stop.txt") + except Exception: + pass + stop_event.set() + return True + return False + + +def sleep_with_stop(duration, interval=0.1): + end = time.time() + duration + while time.time() < end: + if stop_requested(): + break + time.sleep(interval) + + +def get_device_size(): + """Return (w,h) of the device as reported by `adb shell wm size` or None.""" + try: + exe = get_adb_executable() + device = get_target_device() + cmd = [exe] + if device: + cmd += ["-s", device] + cmd += ["shell", "wm", "size"] + res = subprocess.run(cmd, capture_output=True, text=True) + out = (res.stdout or "").strip() + (res.stderr or "").strip() + # look for 'Physical size: 1920x1080' or 'Physical size: 1280x720' + for part in out.splitlines(): + if "Physical size:" in part: + try: + _, size = part.split(":", 1) + w, h = size.strip().split("x") + return int(w), int(h) + except Exception: + continue + except Exception: + pass + return None + +def tap(x, y): + width = 1920 + height = 1080 + # Interpret inputs flexibly: + # - fractions (0 <= v < 1): treated as normalized (0.0-1.0) + # - values 1..100: treated as percentages + # - values >100: treated as raw pixels + if 0 <= x < 1: + px = int(x * width) + elif x <= 100: + px = int(x / 100 * width) + else: + px = int(x) + + if 0 <= y < 1: + py = int(y * height) + elif y <= 100: + py = int(y / 100 * height) + else: + py = int(y) + + adb(["shell", "input", "tap", str(px), str(py)]) + + +def tap_center(): + """Tap the center of the device screen.""" + dev = get_device_size() or (1920, 1080) + tx = dev[0] // 2 + ty = dev[1] // 2 + adb(["shell", "input", "tap", str(tx), str(ty)]) + + +def normalize_cv2_screen(screen): + """Rotate portrait screenshots to landscape when needed.""" + if screen is None: + return None + if screen.shape[1] < screen.shape[0]: + return cv2.rotate(screen, cv2.ROTATE_90_CLOCKWISE) + return screen + + +def normalize_pil_image(img): + """Rotate portrait PIL images to landscape when needed.""" + if img is None: + return None + if img.width < img.height: + return img.rotate(-90, expand=True) + return img + + +def _build_digit_templates(size=(80, 120)): + fonts = [] + font_paths = [ + r"C:\Windows\Fonts\arial.ttf", + r"C:\Windows\Fonts\verdana.ttf", + r"C:\Windows\Fonts\tahoma.ttf", + ] + for path in font_paths: + try: + fonts.append(ImageFont.truetype(path, 100)) + except Exception: + pass + if not fonts: + fonts.append(ImageFont.load_default()) + + templates = {} + for digit in range(10): + best_img = None + best_err = None + for font in fonts: + img = Image.new("L", size, color=255) + draw = ImageDraw.Draw(img) + text = str(digit) + try: + w, h = font.getsize(text) + except Exception: + try: + bbox = font.getbbox(text) + w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] + except Exception: + bbox = draw.textbbox((0, 0), text, font=font) + w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] + draw.text(((size[0] - w) / 2, (size[1] - h) / 2), text, font=font, fill=0) + arr = np.array(img) + _, arr = cv2.threshold(arr, 128, 255, cv2.THRESH_BINARY_INV) + error = np.mean(arr.astype(np.float32)) + if best_err is None or error < best_err: + best_err = error + best_img = arr + templates[str(digit)] = best_img + return templates + + +def _classify_digit(img_gray): + if img_gray is None: + return None + _, thresh = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + templates = _build_digit_templates((80, 120)) + best_digit = None + best_score = -1.0 + for digit, tpl in templates.items(): + resized = cv2.resize(thresh, tpl.shape[::-1], interpolation=cv2.INTER_AREA) + score = cv2.matchTemplate(resized, tpl, cv2.TM_CCOEFF_NORMED) + _, maxVal, _, _ = cv2.minMaxLoc(score) + if maxVal > best_score: + best_score = maxVal + best_digit = digit + return int(best_digit) if best_digit is not None else None + + +def _segment_digit_regions(gray, min_area=250): + blurred = cv2.GaussianBlur(gray, (3, 3), 0) + _, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) + thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=1) + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + rects = [] + for cnt in contours: + x, y, w, h = cv2.boundingRect(cnt) + area = w * h + if area < min_area or h < 20 or w < 10: + continue + rects.append((x, y, w, h)) + rects = sorted(rects, key=lambda r: r[0]) + digits = [] + for x, y, w, h in rects: + pad = 4 + x0 = max(0, x - pad) + y0 = max(0, y - pad) + x1 = min(gray.shape[1], x + w + pad) + y1 = min(gray.shape[0], y + h + pad) + digits.append(gray[y0:y1, x0:x1]) + return digits, thresh + + +def auto_detect_streak(): + if cv2 is None or np is None: + return None + data = capture_screen_bytes() + if data is None: + return None + try: + img = Image.open(io.BytesIO(data)) + except Exception: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("Failed to parse screenshot image for streak detection") + return None + img = normalize_pil_image(img) + width, height = img.size + streak_box = (1360, 760, 1520, 930) + if streak_box[2] > width or streak_box[3] > height: + streak_box = (int(width * 0.7), int(height * 0.7), int(width * 0.95), int(height * 0.9)) + streak_img = img.crop(streak_box) + streak_gray = cv2.cvtColor(np.array(streak_img), cv2.COLOR_RGB2GRAY) + if os.getenv("CFV3_DEBUG", "0") == "1": + os.makedirs("debug", exist_ok=True) + cv2.imwrite(os.path.join("debug", "streak_region.png"), streak_gray) + digits, thresh = _segment_digit_regions(streak_gray) + if os.getenv("CFV3_DEBUG", "0") == "1": + cv2.imwrite(os.path.join("debug", "streak_thresh.png"), thresh) + if not digits: + return None + recognized = [] + for i, digit_img in enumerate(digits): + digit = _classify_digit(digit_img) + if digit is None: + return None + recognized.append(str(digit)) + if os.getenv("CFV3_DEBUG", "0") == "1": + os.makedirs("debug", exist_ok=True) + cv2.imwrite(os.path.join("debug", f"streak_digit_{i}.png"), digit_img) + try: + streak_value = int("".join(recognized)) + except ValueError: + return None + if 0 <= streak_value < 1000: + print(f"Auto-detected streak: {streak_value}") + return streak_value + return None + + +def find_template_on_screen(template_path, threshold=0.8, search_region=None): + """Return (x,y) center of matched template on current screen, or None. + + This does multi-scale (template resized) matching and prints debug info when requested. + """ + if cv2 is None or np is None: + raise RuntimeError("OpenCV (cv2) and numpy are required for template matching. Install via 'pip install opencv-python numpy'") + if not os.path.exists(template_path): + return None + result = adb(["exec-out", "screencap", "-p"]) + if result.returncode != 0 or not result.stdout: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"adb screencap failed: returncode={result.returncode} stderr={result.stderr.decode(errors='ignore')}") + return None + screen = cv2.imdecode(np.frombuffer(result.stdout, np.uint8), cv2.IMREAD_COLOR) + if screen is None: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("cv2.imdecode failed on screenshot data") + return None + screen = normalize_cv2_screen(screen) + + screen_h, screen_w = screen.shape[:2] + # convert to gray for robust matching + screen_gray = cv2.cvtColor(screen, cv2.COLOR_BGR2GRAY) + if search_region is not None: + x1, y1, x2, y2 = search_region + if 0 <= x1 <= 1 and 0 <= x2 <= 1: + x1 = int(x1 * screen_w) + x2 = int(x2 * screen_w) + if 0 <= y1 <= 1 and 0 <= y2 <= 1: + y1 = int(y1 * screen_h) + y2 = int(y2 * screen_h) + x1 = max(0, min(screen_w - 1, x1)) + x2 = max(0, min(screen_w, x2)) + y1 = max(0, min(screen_h - 1, y1)) + y2 = max(0, min(screen_h, y2)) + if x2 <= x1 or y2 <= y1: + return None + region_gray = screen_gray[y1:y2, x1:x2] + else: + region_gray = screen_gray + x1 = y1 = 0 + + tpl = cv2.imread(template_path, cv2.IMREAD_COLOR) + if tpl is None: + return None + tpl_gray = cv2.cvtColor(tpl, cv2.COLOR_BGR2GRAY) + + best_val = -1.0 + best_loc = None + best_size = None + + # try multiple template scales to handle DPI/scale differences + for scale in np.linspace(0.7, 1.3, 13): + tw = int(tpl_gray.shape[1] * scale) + th = int(tpl_gray.shape[0] * scale) + if tw < 8 or th < 8 or tw > region_gray.shape[1] or th > region_gray.shape[0]: + continue + tpl_resized = cv2.resize(tpl_gray, (tw, th), interpolation=cv2.INTER_AREA if scale < 1 else cv2.INTER_LINEAR) + try: + res = cv2.matchTemplate(region_gray, tpl_resized, cv2.TM_CCOEFF_NORMED) + except Exception: + continue + _, maxVal, _, maxLoc = cv2.minMaxLoc(res) + if maxVal > best_val: + best_val = maxVal + best_loc = maxLoc + best_size = (tw, th) + + # write debug artifacts if requested via env var + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + os.makedirs("debug", exist_ok=True) + cv2.imwrite(os.path.join("debug", "screen.png"), screen) + with open(os.path.join("debug", "match.txt"), "w") as f: + f.write(f"best_val={best_val}\n") + f.write(f"best_size={best_size}\n") + if best_val >= threshold and best_loc is not None and best_size is not None: + cx = best_loc[0] + best_size[0] // 2 + x1 + cy = best_loc[1] + best_size[1] // 2 + y1 + if dbg: + print(f"Template matched: val={best_val:.3f} loc={best_loc} size={best_size} screen_shape={screen.shape} search_region={(x1,y1,x2,y2) if search_region is not None else None}") + # return center coords and best_val and screen size + return (cx, cy, float(best_val), screen.shape[1], screen.shape[0]) + if dbg: + print(f"No match (best_val={best_val:.3f}) for template {template_path}") + return None + + +def _distance(a, b): + return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5 + + +def _is_safe_match(match, avoid_matches, max_distance=120): + if match is None: + return False + if not avoid_matches: + return True + mx, my, _, _, _ = match + for avoid in avoid_matches: + if avoid is None: + continue + ax, ay, _, _, _ = avoid + if _distance((mx, my), (ax, ay)) < max_distance: + return False + return True + + +def tap_button(template_path=None, fallback=None, threshold=0.8, search_region=None, avoid_templates=None, skip_if_avoid=False): + """Try to find `template_path` on screen and tap its center; otherwise tap `fallback`. + `fallback` may be a (x,y) tuple in pixels or percentages (1-100).""" + if stop_requested(): + return False + if template_path: + try: + found = find_template_on_screen(template_path, threshold=threshold, search_region=search_region) + except RuntimeError: + found = None + if found: + avoid_matches = [] + if avoid_templates: + for avoid_template in avoid_templates: + try: + avoid_match = find_template_on_screen( + avoid_template, + threshold=0.85, + search_region=search_region, + ) + except RuntimeError: + avoid_match = None + avoid_matches.append(avoid_match) + if not _is_safe_match(found, avoid_matches): + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + print(f"Skipping template tap because avoid template matched too close to {template_path}") + if skip_if_avoid: + print(f"Avoided unsafe tap for {template_path}; not using fallback.") + return False + found = None + if found: + # found = (cx, cy, best_val, screen_w, screen_h) + cx, cy, best_val, screen_w, screen_h = found + # determine device size and scale if needed + dev = get_device_size() + if dev: + dev_w, dev_h = dev + sx = dev_w / float(screen_w) + sy = dev_h / float(screen_h) + tx = int(cx * sx) + ty = int(cy * sy) + else: + tx = int(cx) + ty = int(cy) + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + print(f"Tapping matched template {template_path} -> screen({cx},{cy}) score={best_val:.3f} -> device({tx},{ty})") + adb(["shell", "input", "tap", str(tx), str(ty)]) + return True + if fallback: + if stop_requested(): + return False + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + if isinstance(fallback[0], float) or isinstance(fallback[1], float) or (0 <= fallback[0] <= 100 and 0 <= fallback[1] <= 100): + print(f"Using fallback tap percentages: {fallback}") + else: + print(f"Using fallback tap pixels: {fallback}") + tap(fallback[0], fallback[1]) + return True + return False + +def detect_result(timeout=25, interval=1): + deadline = time.time() + timeout + while True: + if stop_requested(): + return "stopped" + data = capture_screen_bytes() + if data is None: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("Failed to capture screen for result detection") + return "unknown" + try: + img = Image.open(io.BytesIO(data)) + except Exception: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("Failed to parse screenshot image for result detection") + return "unknown" + img = normalize_pil_image(img) + r, g, b, *_ = img.getpixel((728, 200)) + print(f" Banner pixel RGB: ({r}, {g}, {b})") + if r > 180 and g < 60 and b < 60: + return "victory" + if r > 80 and g < 60 and b < 40: + return "defeat" + if time.time() >= deadline: + if os.getenv("CFV3_DEBUG", "0") == "1": + os.makedirs("debug", exist_ok=True) + img.save(os.path.join("debug", "unknown_result.png")) + print(" Saved debug unknown result screen to debug/unknown_result.png") + return "unknown" + print(" Result not detected yet, waiting...") + sleep_with_stop(interval) + +win_streak = auto_detect_streak() +if win_streak is None: + win_streak = int(input("Do you have a current win streak? Enter the number (0 if none): ")) +print("Starting in 3 seconds... switch to BlueStacks!") +time.sleep(3) + +fight_count = 0 + +# Start background thread to listen for stop key +threading.Thread(target=_watch_stop_key, daemon=True).start() + +while True: + if stop_requested(): + print("Stop requested. Exiting.") + break + print(f"\n--- Fight {fight_count + 1} | Current streak: {win_streak} ---") + fight_count += 1 + + # 1. Quick Fight + print("Tapping Quick Fight...") + if fight_count > 1: + time.sleep(4) # Extra delay for 2nd fight onwards + clicked = tap_button( + template_path="templates/quick_fight.png", + fallback=(1300, 860), + threshold=0.9, + search_region=(0.35, 0.55, 0.95, 0.95), + avoid_templates=["templates/avoid_keep_win_streak.png"], + skip_if_avoid=True, + ) + if not clicked: + print("Quick Fight tap skipped because the blocked button was detected. Waiting and retrying next loop.") + sleep_with_stop(3) + continue + sleep_with_stop(3) + + # 2. Start Fight + print("Tapping Start Fight...") + tap_button(template_path="templates/start_fight.png", fallback=(960, 860)) + sleep_with_stop(12) + + # 3. Detect result + result = detect_result() + print(f" Result: {result}") + + if result == "stopped": + print("Bot detected stop signal during result wait. Exiting.") + break + if result == "victory": + win_streak += 1 + print(f" WIN - streak is now {win_streak} - Tapping OK...") + tap_button(template_path="templates/ok.png", fallback=(960, 870)) + + elif result == "defeat": + if win_streak > 0: + print(f" LOSS - had streak of {win_streak} - Tapping streak loss Retreat...") + tap_button(template_path="templates/streak_loss_retreat.png", fallback=(760, 870)) + else: + print(" LOSS - no streak - Tapping OK...") + tap_button(template_path="templates/ok.png", fallback=(960, 870)) + win_streak = 0 + sleep_with_stop(8) # Wait for main screen to fully load before next loop diff --git a/cats_FarmV4.py b/cats_FarmV4.py new file mode 100644 index 0000000..8dbc295 --- /dev/null +++ b/cats_FarmV4.py @@ -0,0 +1,637 @@ +import subprocess +import os +import time +import threading +import msvcrt +import io +import shutil +from PIL import Image, ImageDraw, ImageFont +try: + import cv2 + import numpy as np +except Exception: + cv2 = None + np = None + +ADB = r"YOUR OWN PATH" +DEVICE = "127.0.0.1:5555" + +def get_adb_executable(): + # Prefer bundled adb if present, otherwise fall back to system adb in PATH + if os.path.exists(ADB): + return ADB + exe = shutil.which("adb") + if exe is None: + raise FileNotFoundError(f"adb not found. Checked: {ADB} and system PATH") + return exe + + +def list_connected_devices(): + exe = get_adb_executable() + res = subprocess.run([exe, "devices"], capture_output=True, text=True) + if res.returncode != 0: + return [] + devices = [] + for line in (res.stdout or "").splitlines(): + line = line.strip() + if not line or line.startswith("List of devices"): + continue + parts = line.split() + if len(parts) >= 2 and parts[1] == "device": + devices.append(parts[0]) + return devices + + +def connect_adb_device(): + if not DEVICE: + return False + exe = get_adb_executable() + res = subprocess.run([exe, "connect", DEVICE], capture_output=True, text=True) + out = (res.stdout or "").strip().lower() + err = (res.stderr or "").strip().lower() + success = "connected to" in out or "already connected" in out + if success: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"ADB connected to {DEVICE}: {out}") + return True + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"ADB connect failed for {DEVICE}: returncode={res.returncode} stdout={out} stderr={err}") + return False + + +def get_target_device(): + exe = get_adb_executable() + if DEVICE: + res = subprocess.run([exe, "-s", DEVICE, "get-state"], capture_output=True, text=True) + if res.returncode == 0 and "device" in (res.stdout or "").lower(): + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"Using configured ADB device: {DEVICE}") + return DEVICE + if connect_adb_device(): + return DEVICE + devices = list_connected_devices() + if devices: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"Auto-detected adb device: {devices[0]}") + return devices[0] + if os.getenv("CFV3_DEBUG", "0") == "1": + print("No adb devices found") + return None + + +def adb(cmd): + exe = get_adb_executable() + device = get_target_device() + if device: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"adb command using device {device}: {' '.join(cmd)}") + return subprocess.run([exe, "-s", device] + cmd, capture_output=True) + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"adb command without explicit device: {' '.join(cmd)}") + return subprocess.run([exe] + cmd, capture_output=True) + + +def capture_screen_bytes(): + result = adb(["exec-out", "screencap", "-p"]) + if result.returncode != 0 or not result.stdout: + if os.getenv("CFV3_DEBUG", "0") == "1": + stderr = result.stderr.decode(errors="ignore") if result.stderr else "" + print(f"adb screencap failed: returncode={result.returncode} stderr={stderr}") + return None + return result.stdout + + +# Event used to request the main loop to stop from a key press +stop_event = threading.Event() + + +def _watch_stop_key(): + """Watch for 'q' keypress in the console and set stop_event to stop the bot.""" + print("Press 'q' to stop the bot gracefully, or create stop.txt in the script folder.") + while not stop_event.is_set(): + if msvcrt.kbhit(): + ch = msvcrt.getwch() + if ch.lower() == 'q': + print("Stop key pressed. Exiting loop after current iteration...") + stop_event.set() + break + time.sleep(0.1) + + +def stop_requested(): + if stop_event.is_set(): + return True + if os.path.exists("stop.txt"): + print("Found stop.txt. Exiting loop after current iteration...") + try: + os.remove("stop.txt") + except Exception: + pass + stop_event.set() + return True + return False + + +def sleep_with_stop(duration, interval=0.1): + end = time.time() + duration + while time.time() < end: + if stop_requested(): + break + time.sleep(interval) + + +def get_device_size(): + """Return (w,h) of the device as reported by `adb shell wm size` or None.""" + try: + exe = get_adb_executable() + device = get_target_device() + cmd = [exe] + if device: + cmd += ["-s", device] + cmd += ["shell", "wm", "size"] + res = subprocess.run(cmd, capture_output=True, text=True) + out = (res.stdout or "").strip() + (res.stderr or "").strip() + # look for 'Physical size: 1920x1080' or 'Physical size: 1280x720' + for part in out.splitlines(): + if "Physical size:" in part: + try: + _, size = part.split(":", 1) + w, h = size.strip().split("x") + return int(w), int(h) + except Exception: + continue + except Exception: + pass + return None + +def tap(x, y): + width = 1920 + height = 1080 + # Interpret inputs flexibly: + # - fractions (0 <= v < 1): treated as normalized (0.0-1.0) + # - values 1..100: treated as percentages + # - values >100: treated as raw pixels + if 0 <= x < 1: + px = int(x * width) + elif x <= 100: + px = int(x / 100 * width) + else: + px = int(x) + + if 0 <= y < 1: + py = int(y * height) + elif y <= 100: + py = int(y / 100 * height) + else: + py = int(y) + + adb(["shell", "input", "tap", str(px), str(py)]) + + +def tap_center(): + """Tap the center of the device screen.""" + dev = get_device_size() or (1920, 1080) + tx = dev[0] // 2 + ty = dev[1] // 2 + adb(["shell", "input", "tap", str(tx), str(ty)]) + + +def normalize_cv2_screen(screen): + """Rotate portrait screenshots to landscape when needed.""" + if screen is None: + return None + if screen.shape[1] < screen.shape[0]: + return cv2.rotate(screen, cv2.ROTATE_90_CLOCKWISE) + return screen + + +def normalize_pil_image(img): + """Rotate portrait PIL images to landscape when needed.""" + if img is None: + return None + if img.width < img.height: + return img.rotate(-90, expand=True) + return img + + +def _build_digit_templates(size=(80, 120)): + fonts = [] + font_paths = [ + r"C:\Windows\Fonts\arial.ttf", + r"C:\Windows\Fonts\verdana.ttf", + r"C:\Windows\Fonts\tahoma.ttf", + ] + for path in font_paths: + try: + fonts.append(ImageFont.truetype(path, 100)) + except Exception: + pass + if not fonts: + fonts.append(ImageFont.load_default()) + + templates = {} + for digit in range(10): + best_img = None + best_err = None + for font in fonts: + img = Image.new("L", size, color=255) + draw = ImageDraw.Draw(img) + text = str(digit) + try: + w, h = font.getsize(text) + except Exception: + try: + bbox = font.getbbox(text) + w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] + except Exception: + bbox = draw.textbbox((0, 0), text, font=font) + w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] + draw.text(((size[0] - w) / 2, (size[1] - h) / 2), text, font=font, fill=0) + arr = np.array(img) + _, arr = cv2.threshold(arr, 128, 255, cv2.THRESH_BINARY_INV) + error = np.mean(arr.astype(np.float32)) + if best_err is None or error < best_err: + best_err = error + best_img = arr + templates[str(digit)] = best_img + return templates + + +def _classify_digit(img_gray): + if img_gray is None: + return None + _, thresh = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + templates = _build_digit_templates((80, 120)) + best_digit = None + best_score = -1.0 + for digit, tpl in templates.items(): + resized = cv2.resize(thresh, tpl.shape[::-1], interpolation=cv2.INTER_AREA) + score = cv2.matchTemplate(resized, tpl, cv2.TM_CCOEFF_NORMED) + _, maxVal, _, _ = cv2.minMaxLoc(score) + if maxVal > best_score: + best_score = maxVal + best_digit = digit + return int(best_digit) if best_digit is not None else None + + +def _segment_digit_regions(gray, min_area=250): + blurred = cv2.GaussianBlur(gray, (3, 3), 0) + _, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) + thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=1) + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + rects = [] + for cnt in contours: + x, y, w, h = cv2.boundingRect(cnt) + area = w * h + if area < min_area or h < 20 or w < 10: + continue + rects.append((x, y, w, h)) + rects = sorted(rects, key=lambda r: r[0]) + digits = [] + for x, y, w, h in rects: + pad = 4 + x0 = max(0, x - pad) + y0 = max(0, y - pad) + x1 = min(gray.shape[1], x + w + pad) + y1 = min(gray.shape[0], y + h + pad) + digits.append(gray[y0:y1, x0:x1]) + return digits, thresh + + +def auto_detect_streak(): + if cv2 is None or np is None: + return None + data = capture_screen_bytes() + if data is None: + return None + try: + img = Image.open(io.BytesIO(data)) + except Exception: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("Failed to parse screenshot image for streak detection") + return None + img = normalize_pil_image(img) + width, height = img.size + streak_box = (1360, 760, 1520, 930) + if streak_box[2] > width or streak_box[3] > height: + streak_box = (int(width * 0.7), int(height * 0.7), int(width * 0.95), int(height * 0.9)) + streak_img = img.crop(streak_box) + streak_gray = cv2.cvtColor(np.array(streak_img), cv2.COLOR_RGB2GRAY) + if os.getenv("CFV3_DEBUG", "0") == "1": + os.makedirs("debug", exist_ok=True) + cv2.imwrite(os.path.join("debug", "streak_region.png"), streak_gray) + digits, thresh = _segment_digit_regions(streak_gray) + if os.getenv("CFV3_DEBUG", "0") == "1": + cv2.imwrite(os.path.join("debug", "streak_thresh.png"), thresh) + if not digits: + return None + recognized = [] + for i, digit_img in enumerate(digits): + digit = _classify_digit(digit_img) + if digit is None: + return None + recognized.append(str(digit)) + if os.getenv("CFV3_DEBUG", "0") == "1": + os.makedirs("debug", exist_ok=True) + cv2.imwrite(os.path.join("debug", f"streak_digit_{i}.png"), digit_img) + try: + streak_value = int("".join(recognized)) + except ValueError: + return None + if 0 <= streak_value < 1000: + print(f"Auto-detected streak: {streak_value}") + return streak_value + return None + + +def find_template_on_screen(template_path, threshold=0.8, search_region=None): + """Return (x,y) center of matched template on current screen, or None. + + This does multi-scale (template resized) matching and prints debug info when requested. + """ + if cv2 is None or np is None: + raise RuntimeError("OpenCV (cv2) and numpy are required for template matching. Install via 'pip install opencv-python numpy'") + if not os.path.exists(template_path): + return None + result = adb(["exec-out", "screencap", "-p"]) + if result.returncode != 0 or not result.stdout: + if os.getenv("CFV3_DEBUG", "0") == "1": + print(f"adb screencap failed: returncode={result.returncode} stderr={result.stderr.decode(errors='ignore')}") + return None + screen = cv2.imdecode(np.frombuffer(result.stdout, np.uint8), cv2.IMREAD_COLOR) + if screen is None: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("cv2.imdecode failed on screenshot data") + return None + screen = normalize_cv2_screen(screen) + + screen_h, screen_w = screen.shape[:2] + # convert to gray for robust matching + screen_gray = cv2.cvtColor(screen, cv2.COLOR_BGR2GRAY) + if search_region is not None: + x1, y1, x2, y2 = search_region + if 0 <= x1 <= 1 and 0 <= x2 <= 1: + x1 = int(x1 * screen_w) + x2 = int(x2 * screen_w) + if 0 <= y1 <= 1 and 0 <= y2 <= 1: + y1 = int(y1 * screen_h) + y2 = int(y2 * screen_h) + x1 = max(0, min(screen_w - 1, x1)) + x2 = max(0, min(screen_w, x2)) + y1 = max(0, min(screen_h - 1, y1)) + y2 = max(0, min(screen_h, y2)) + if x2 <= x1 or y2 <= y1: + return None + region_gray = screen_gray[y1:y2, x1:x2] + else: + region_gray = screen_gray + x1 = y1 = 0 + + tpl = cv2.imread(template_path, cv2.IMREAD_COLOR) + if tpl is None: + return None + tpl_gray = cv2.cvtColor(tpl, cv2.COLOR_BGR2GRAY) + + best_val = -1.0 + best_loc = None + best_size = None + + # try multiple template scales to handle DPI/scale differences + for scale in np.linspace(0.7, 1.3, 13): + tw = int(tpl_gray.shape[1] * scale) + th = int(tpl_gray.shape[0] * scale) + if tw < 8 or th < 8 or tw > region_gray.shape[1] or th > region_gray.shape[0]: + continue + tpl_resized = cv2.resize(tpl_gray, (tw, th), interpolation=cv2.INTER_AREA if scale < 1 else cv2.INTER_LINEAR) + try: + res = cv2.matchTemplate(region_gray, tpl_resized, cv2.TM_CCOEFF_NORMED) + except Exception: + continue + _, maxVal, _, maxLoc = cv2.minMaxLoc(res) + if maxVal > best_val: + best_val = maxVal + best_loc = maxLoc + best_size = (tw, th) + + # write debug artifacts if requested via env var + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + os.makedirs("debug", exist_ok=True) + cv2.imwrite(os.path.join("debug", "screen.png"), screen) + with open(os.path.join("debug", "match.txt"), "w") as f: + f.write(f"best_val={best_val}\n") + f.write(f"best_size={best_size}\n") + if best_val >= threshold and best_loc is not None and best_size is not None: + cx = best_loc[0] + best_size[0] // 2 + x1 + cy = best_loc[1] + best_size[1] // 2 + y1 + if dbg: + print(f"Template matched: val={best_val:.3f} loc={best_loc} size={best_size} screen_shape={screen.shape} search_region={(x1,y1,x2,y2) if search_region is not None else None}") + # return center coords and best_val and screen size + return (cx, cy, float(best_val), screen.shape[1], screen.shape[0]) + if dbg: + print(f"No match (best_val={best_val:.3f}) for template {template_path}") + return None + + +def dismiss_ad_if_present(search_region=(0.0, 0.0, 1.0, 1.0), threshold=0.85): + """Find and tap the ad close button if it is visible.""" + if stop_requested(): + return False + if tap_button(template_path="templates/ad_button.png", threshold=threshold, search_region=search_region): + print("Ad detected and closed.") + sleep_with_stop(1) + return True + return False + + +def _distance(a, b): + return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5 + + +def _is_safe_match(match, avoid_matches, max_distance=120): + if match is None: + return False + if not avoid_matches: + return True + mx, my, _, _, _ = match + for avoid in avoid_matches: + if avoid is None: + continue + ax, ay, _, _, _ = avoid + if _distance((mx, my), (ax, ay)) < max_distance: + return False + return True + + +def tap_button(template_path=None, fallback=None, threshold=0.8, search_region=None, avoid_templates=None, skip_if_avoid=False): + """Try to find `template_path` on screen and tap its center; otherwise tap `fallback`. + `fallback` may be a (x,y) tuple in pixels or percentages (1-100).""" + if stop_requested(): + return False + if template_path: + try: + found = find_template_on_screen(template_path, threshold=threshold, search_region=search_region) + except RuntimeError: + found = None + if found: + avoid_matches = [] + if avoid_templates: + for avoid_template in avoid_templates: + try: + avoid_match = find_template_on_screen( + avoid_template, + threshold=0.85, + search_region=search_region, + ) + except RuntimeError: + avoid_match = None + avoid_matches.append(avoid_match) + if not _is_safe_match(found, avoid_matches): + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + print(f"Skipping template tap because avoid template matched too close to {template_path}") + if skip_if_avoid: + print(f"Avoided unsafe tap for {template_path}; not using fallback.") + return False + found = None + if found: + # found = (cx, cy, best_val, screen_w, screen_h) + cx, cy, best_val, screen_w, screen_h = found + # determine device size and scale if needed + dev = get_device_size() + if dev: + dev_w, dev_h = dev + sx = dev_w / float(screen_w) + sy = dev_h / float(screen_h) + tx = int(cx * sx) + ty = int(cy * sy) + else: + tx = int(cx) + ty = int(cy) + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + print(f"Tapping matched template {template_path} -> screen({cx},{cy}) score={best_val:.3f} -> device({tx},{ty})") + adb(["shell", "input", "tap", str(tx), str(ty)]) + return True + if fallback: + if stop_requested(): + return False + dbg = os.getenv("CFV3_DEBUG", "0") == "1" + if dbg: + if isinstance(fallback[0], float) or isinstance(fallback[1], float) or (0 <= fallback[0] <= 100 and 0 <= fallback[1] <= 100): + print(f"Using fallback tap percentages: {fallback}") + else: + print(f"Using fallback tap pixels: {fallback}") + tap(fallback[0], fallback[1]) + return True + return False + +def detect_result(timeout=25, interval=1): + deadline = time.time() + timeout + while True: + if stop_requested(): + return "stopped" + if dismiss_ad_if_present(): + print(" Closed ad while waiting for result.") + sleep_with_stop(1) + continue + data = capture_screen_bytes() + if data is None: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("Failed to capture screen for result detection") + return "unknown" + try: + img = Image.open(io.BytesIO(data)) + except Exception: + if os.getenv("CFV3_DEBUG", "0") == "1": + print("Failed to parse screenshot image for result detection") + return "unknown" + img = normalize_pil_image(img) + r, g, b, *_ = img.getpixel((728, 200)) + print(f" Banner pixel RGB: ({r}, {g}, {b})") + if r > 180 and g < 60 and b < 60: + return "victory" + if r > 80 and g < 60 and b < 40: + return "defeat" + if time.time() >= deadline: + if os.getenv("CFV3_DEBUG", "0") == "1": + os.makedirs("debug", exist_ok=True) + img.save(os.path.join("debug", "unknown_result.png")) + print(" Saved debug unknown result screen to debug/unknown_result.png") + return "unknown" + print(" Result not detected yet, waiting...") + sleep_with_stop(interval) + +win_streak = auto_detect_streak() +if win_streak is None: + win_streak = int(input("Do you have a current win streak? Enter the number (0 if none): ")) +print("Starting in 3 seconds... switch to BlueStacks!") +time.sleep(3) + +fight_count = 0 + +# Start background thread to listen for stop key +threading.Thread(target=_watch_stop_key, daemon=True).start() + +while True: + if stop_requested(): + print("Stop requested. Exiting.") + break + print(f"\n--- Fight {fight_count + 1} | Current streak: {win_streak} ---") + fight_count += 1 + + # 1. Quick Fight + print("Tapping Quick Fight...") + if dismiss_ad_if_present(): + print(" Closed ad before selecting Quick Fight.") + sleep_with_stop(2) + if fight_count > 1: + time.sleep(4) # Extra delay for 2nd fight onwards + clicked = tap_button( + template_path="templates/quick_fight.png", + fallback=(1300, 860), + threshold=0.9, + search_region=(0.35, 0.55, 0.95, 0.95), + avoid_templates=["templates/avoid_keep_win_streak.png"], + skip_if_avoid=True, + ) + if not clicked: + print("Quick Fight tap skipped because the blocked button was detected. Waiting and retrying next loop.") + sleep_with_stop(3) + continue + sleep_with_stop(3) + + # 2. Start Fight + print("Tapping Start Fight...") + tap_button(template_path="templates/start_fight.png", fallback=(960, 860)) + sleep_with_stop(3) + if dismiss_ad_if_present(): + print(" Closed ad after Start Fight tap.") + sleep_with_stop(2) + sleep_with_stop(9) + + # 3. Detect result + result = detect_result() + print(f" Result: {result}") + + if result == "stopped": + print("Bot detected stop signal during result wait. Exiting.") + break + if result == "victory": + win_streak += 1 + print(f" WIN - streak is now {win_streak} - Tapping OK...") + dismiss_ad_if_present() + tap_button(template_path="templates/ok.png", fallback=(960, 870)) + + elif result == "defeat": + if win_streak > 0: + print(f" LOSS - had streak of {win_streak} - Tapping streak loss Retreat...") + dismiss_ad_if_present() + tap_button(template_path="templates/streak_loss_retreat.png", fallback=(760, 870)) + else: + print(" LOSS - no streak - Tapping OK...") + dismiss_ad_if_present() + tap_button(template_path="templates/ok.png", fallback=(960, 870)) + win_streak = 0 + sleep_with_stop(8) # Wait for main screen to fully load before next loop diff --git a/cats_farm.py b/cats_farm.py deleted file mode 100644 index dfaa0e6..0000000 --- a/cats_farm.py +++ /dev/null @@ -1,67 +0,0 @@ -import subprocess -import time -import io -from PIL import Image - -ADB = r"C:\Users\lucam\OneDrive\Bureaublad\mappen\Scripts\blue stacks\platform-tools\adb.exe" -DEVICE = "127.0.0.1:5555" - -def adb(cmd): - return subprocess.run([ADB, "-s", DEVICE] + cmd, capture_output=True) - -def tap(x, y): - px = int(x / 100 * 1280) - py = int(y / 100 * 720) - adb(["shell", "input", "tap", str(px), str(py)]) - -def detect_result(): - result = adb(["exec-out", "screencap", "-p"]) - img = Image.open(io.BytesIO(result.stdout)) - r, g, b, *_ = img.getpixel((728, 200)) - print(f" Banner pixel RGB: ({r}, {g}, {b})") - if r > 180 and g < 60 and b < 60: - return "victory" - elif r > 80 and g < 60 and b < 40: - return "defeat" - return "unknown" - -win_streak = int(input("Do you have a current win streak? Enter the number (0 if none): ")) -print("Starting in 3 seconds... switch to BlueStacks!") -time.sleep(3) - -fight_count = 0 - -while True: - fight_count += 1 - print(f"\n--- Fight {fight_count} | Current streak: {win_streak} ---") - - # 1. Quick Fight - print("Tapping Quick Fight...") - if fight_count > 1: - time.sleep(4) # Extra delay for 2nd fight onwards - tap(85, 120) - time.sleep(3) - - # 2. Start Fight - print("Tapping Start Fight...") - tap(50, 40) - time.sleep(12) - - # 3. Detect result - result = detect_result() - print(f" Result: {result}") - - if result == "victory": - win_streak += 1 - print(f" WIN - streak is now {win_streak} - Tapping OK...") - tap(85, 120) - - elif result == "defeat": - if win_streak > 0: - print(f" LOSS - had streak of {win_streak} - Tapping streak loss Retreat...") - tap(70, 120) # <-- update this coordinate once you find it! - else: - print(" LOSS - no streak - Tapping Retreat...") - tap(80, 120) - win_streak = 0 - time.sleep(8) # Wait for main screen to fully load before next loop diff --git a/debug/match.txt b/debug/match.txt new file mode 100644 index 0000000..cb2d766 --- /dev/null +++ b/debug/match.txt @@ -0,0 +1,2 @@ +best_val=0.9521270990371704 +best_size=(620, 240) diff --git a/debug/screen.png b/debug/screen.png new file mode 100644 index 0000000..9b3888f Binary files /dev/null and b/debug/screen.png differ diff --git a/debug/streak1.png b/debug/streak1.png new file mode 100644 index 0000000..98e1c31 Binary files /dev/null and b/debug/streak1.png differ diff --git a/debug/streak2.png b/debug/streak2.png new file mode 100644 index 0000000..5f8fd75 Binary files /dev/null and b/debug/streak2.png differ diff --git a/debug/streak3.png b/debug/streak3.png new file mode 100644 index 0000000..4ef5725 Binary files /dev/null and b/debug/streak3.png differ diff --git a/debug/streak_digit_0.png b/debug/streak_digit_0.png new file mode 100644 index 0000000..e1e8244 Binary files /dev/null and b/debug/streak_digit_0.png differ diff --git a/debug/streak_digit_1.png b/debug/streak_digit_1.png new file mode 100644 index 0000000..8fdcd8e Binary files /dev/null and b/debug/streak_digit_1.png differ diff --git a/debug/streak_digit_2.png b/debug/streak_digit_2.png new file mode 100644 index 0000000..aadb794 Binary files /dev/null and b/debug/streak_digit_2.png differ diff --git a/debug/streak_region.png b/debug/streak_region.png new file mode 100644 index 0000000..951e7bc Binary files /dev/null and b/debug/streak_region.png differ diff --git a/debug/streak_thresh.png b/debug/streak_thresh.png new file mode 100644 index 0000000..90c9198 Binary files /dev/null and b/debug/streak_thresh.png differ diff --git a/debug/unknown_result.png b/debug/unknown_result.png new file mode 100644 index 0000000..6d86755 Binary files /dev/null and b/debug/unknown_result.png differ diff --git a/discord-bot/DISCORD_BOT_SETUP.md b/discord-bot/DISCORD_BOT_SETUP.md new file mode 100644 index 0000000..1b47348 --- /dev/null +++ b/discord-bot/DISCORD_BOT_SETUP.md @@ -0,0 +1,125 @@ +# Discord Bot Setup Guide + +## Step 1: Create Discord Bot + +1. Go to: https://discord.com/developers/applications +2. Click "New Application" β†’ Name it "C.A.T.S. Championship" +3. Go to "Bot" section β†’ Click "Add Bot" +4. Under TOKEN, click "Copy" β†’ **Save this somewhere safe** +5. Go to "Intents" β†’ Turn ON: **Message Content Intent** +6. Go to "OAuth2" β†’ URL Generator + - Scopes: `bot` + - Permissions: + - `Send Messages` + - `Embed Links` + - `Read Message History` +7. Copy the generated URL and open it β†’ Select your Discord server β†’ Authorize + +--- + +## Step 2: Update Bot Token + +Open `discord_countdown_bot.py` and replace line 16: + +```python +TOKEN = "YOUR_DISCORD_BOT_TOKEN_HERE" +``` + +With your actual token: + +```python +TOKEN = "MzA1NTQ3OTIwMzQ3NDA..." # Your actual token +``` + +--- + +## Step 3: Deploy to Replit (Free, Always Running) + +### Option A: Deploy to Replit (Easiest - Recommended) + +1. Go to: https://replit.com (create free account) +2. Click "Create" β†’ Select "Python" +3. Copy the bot code from `discord_countdown_bot.py` +4. Paste it into Replit +5. Create `requirements.txt`: +``` +discord.py +``` +6. Click "Run" β†’ Bot runs 24/7! + +### Option B: Run Locally + +1. Install discord.py: +```powershell +pip install discord.py +``` + +2. Run: +```powershell +cd "C:\Users\lucam\OneDrive\Bureaublad\mappen\Scripts\blue stacks" +python discord_countdown_bot.py +``` + +--- + +## Step 4: Use the Bot in Discord + +In your Discord server, use these commands: + +**Start countdown:** +``` +!set_end_time "03/07/2026 22:00" +``` + +**Check status:** +``` +!countdown_status +``` + +--- + +## Workflow: + +1. **Run your OCR script** to get the timer: +```powershell +python "C.A.T.S. Championship Timer NotifierV3.py" +``` + +2. **Copy the end time** from the output (e.g., `03/07/2026 22:00`) + +3. **Go to Discord** and type: +``` +!set_end_time "03/07/2026 22:00" +``` + +4. **Bot takes over:** + - Updates countdown every minute + - Sends alerts at 60 and 15 minutes + - Runs 24/7 on Replit (or your PC) + +--- + +## Troubleshooting + +**Bot not responding?** +- Check token is correct +- Make sure bot has permission to send messages +- Restart the bot + +**No countdown message?** +- Use `!set_end_time` command first +- Make sure bot can see the channel + +**Alerts not sending?** +- Wait for 60 or 15 minutes before end time +- Use `!countdown_status` to check remaining time + +--- + +## Notes + +- **Replit version:** Bot runs 24/7, never stops (best for always-on countdown) +- **Local version:** Bot runs on your PC only while script is active +- **State saved:** Bot remembers message ID and settings in `bot_settings.json` + +**Recommended:** Deploy to Replit for true always-on operation! πŸš€ diff --git a/discord-bot/discord_countdown_bot.py b/discord-bot/discord_countdown_bot.py new file mode 100644 index 0000000..b39199f --- /dev/null +++ b/discord-bot/discord_countdown_bot.py @@ -0,0 +1,215 @@ +""" +C.A.T.S. Championship Countdown Discord Bot +Runs on Replit or any server, updates countdown live +""" + +import discord +from discord.ext import commands, tasks +from datetime import datetime, timedelta +import json +import os + +# Bot configuration +TOKEN = "Your own bot token" # Get this from Discord Developer Portal +CHANNEL_ID = None # Will be set via command +MESSAGE_ID = None # Will be updated when countdown posts + +# Store settings in file +SETTINGS_FILE = "bot_settings.json" + +def load_settings(): + global CHANNEL_ID, MESSAGE_ID + if os.path.exists(SETTINGS_FILE): + with open(SETTINGS_FILE, "r") as f: + data = json.load(f) + CHANNEL_ID = data.get("channel_id") + MESSAGE_ID = data.get("message_id") + +def save_settings(): + with open(SETTINGS_FILE, "w") as f: + json.dump({ + "channel_id": CHANNEL_ID, + "message_id": MESSAGE_ID + }, f) + +# Initialize bot +intents = discord.Intents.default() +intents.message_content = True +bot = commands.Bot(command_prefix="!", intents=intents) + +# Store end time +end_time = None +alerted_thresholds = [] + +@bot.event +async def on_ready(): + print(f"βœ“ Bot logged in as {bot.user}") + print(f"βœ“ Status: Ready for countdown") + load_settings() + if CHANNEL_ID: + print(f"βœ“ Channel set to: {CHANNEL_ID}") + countdown_loop.start() + +@bot.command(name="set_end_time") +async def set_end_time(ctx, end_time_str: str): + """ + Set championship end time + Usage: !set_end_time "03/07/2026 22:00" + """ + global end_time, alerted_thresholds, CHANNEL_ID, MESSAGE_ID + + try: + end_time = datetime.strptime(end_time_str, "%d/%m/%Y %H:%M") + alerted_thresholds = [] + CHANNEL_ID = ctx.channel.id + save_settings() + + await ctx.send(f"βœ“ Championship end time set to: **{end_time.strftime('%d/%m/%Y %H:%M')}**\n" + f"Countdown will start now!") + print(f"βœ“ End time set: {end_time}") + + except ValueError: + await ctx.send("❌ Invalid format! Use: `!set_end_time \"DD/MM/YYYY HH:MM\"`\n" + "Example: `!set_end_time \"03/07/2026 22:00\"`") + +@bot.command(name="countdown_status") +async def countdown_status(ctx): + """Check countdown status""" + if end_time is None: + await ctx.send("❌ No countdown active. Use `!set_end_time` to start.") + return + + remaining = end_time - datetime.now() + if remaining.total_seconds() <= 0: + await ctx.send("⏰ Championship has ended!") + return + + total_seconds = int(remaining.total_seconds()) + hours, rem = divmod(total_seconds, 3600) + minutes, seconds = divmod(rem, 60) + + await ctx.send(f"⏳ Time remaining: **{hours}h {minutes}m {seconds}s**\n" + f"End time: **{end_time.strftime('%d/%m/%Y %H:%M')}**") + +def format_remaining(td): + """Format timedelta as readable string""" + if td.total_seconds() <= 0: + return "0m 0s" + total_seconds = max(0, int(td.total_seconds())) + h, rem = divmod(total_seconds, 3600) + m, s = divmod(rem, 60) + if h: + return f"{h}h {m}m {s}s" + return f"{m}m {s}s" + +async def send_alert(channel, remaining, threshold): + """Send an alert message""" + total_minutes = int(remaining.total_seconds() // 60) + embed = discord.Embed( + title=f"⏰ C.A.T.S. Championship Alert!", + description=f"The championship ends in **{total_minutes} minutes**!", + color=discord.Color.red() + ) + embed.add_field( + name="Expected End Time", + value=f"**{end_time.strftime('%d/%m/%Y %H:%M')}**", + inline=False + ) + embed.add_field( + name="Remaining Time", + value=f"**{format_remaining(remaining)}**", + inline=False + ) + await channel.send(f"@here", embed=embed) + +@tasks.loop(minutes=1) +async def countdown_loop(): + """Update countdown every minute""" + global end_time, MESSAGE_ID, alerted_thresholds, CHANNEL_ID + + if end_time is None or CHANNEL_ID is None: + return + + try: + channel = bot.get_channel(CHANNEL_ID) + if not channel: + print("❌ Channel not found") + return + + remaining = end_time - datetime.now() + total_seconds = remaining.total_seconds() + + # Championship ended + if total_seconds <= 0: + embed = discord.Embed( + title="πŸ† Championship Ended!", + description="The C.A.T.S. championship is now over.", + color=discord.Color.gold() + ) + await channel.send(embed=embed) + end_time = None + return + + # Prepare countdown message + total_minutes = int(total_seconds // 60) + embed = discord.Embed( + title="⏳ C.A.T.S. Championship Countdown", + description=f"**{format_remaining(remaining)}**", + color=discord.Color.blue() + ) + embed.add_field( + name="End Time", + value=f"**{end_time.strftime('%d/%m/%Y %H:%M')}**", + inline=False + ) + embed.set_footer(text="Updates every minute") + + # Update or post countdown message + if MESSAGE_ID: + try: + msg = await channel.fetch_message(MESSAGE_ID) + await msg.edit(embed=embed) + except discord.NotFound: + msg = await channel.send(embed=embed) + MESSAGE_ID = msg.id + save_settings() + else: + msg = await channel.send(embed=embed) + MESSAGE_ID = msg.id + save_settings() + + # Send alerts at thresholds + thresholds = [60, 15] + for threshold in thresholds: + if total_minutes <= threshold and threshold not in alerted_thresholds: + await send_alert(channel, remaining, threshold) + alerted_thresholds.append(threshold) + print(f"βœ“ Alert sent for {threshold} minute threshold") + + except Exception as e: + print(f"❌ Error in countdown loop: {e}") + +# Run the bot +if __name__ == "__main__": + print("πŸ€– C.A.T.S. Championship Discord Bot") + print("="*50) + print("\n⚠️ SETUP REQUIRED:") + print("\n1. Go to: https://discord.com/developers/applications") + print("2. Create a new application") + print("3. Go to 'Bot' section and create a bot") + print("4. Copy the TOKEN and paste it above (line 16)") + print("5. Set INTENTS to: Message Content Intent (ON)") + print("6. Under OAuth2 > URL Generator:") + print(" - Scopes: bot") + print(" - Permissions: Send Messages, Embed Links, Read Messages") + print("7. Copy the generated URL and invite bot to your server") + print("\n" + "="*50) + print("\nπŸ“ Usage in Discord:") + print(" !set_end_time \"DD/MM/YYYY HH:MM\"") + print(" !countdown_status") + print("\n" + "="*50 + "\n") + + try: + bot.run(TOKEN) + except discord.errors.LoginFailure: + print("❌ Invalid token! Update TOKEN variable and try again.") diff --git a/discord-bot/read_timer_only.py b/discord-bot/read_timer_only.py new file mode 100644 index 0000000..27f7c5c --- /dev/null +++ b/discord-bot/read_timer_only.py @@ -0,0 +1,558 @@ +""" +C.A.T.S. Timer Reader - Send to Discord +Simple script to read the championship timer and post it to Discord +""" + +import os +import io +import sys +import re +import subprocess +import shutil +import cv2 +import numpy as np +import pytesseract +from PIL import Image, ImageDraw, ImageFont +from datetime import datetime, timedelta +import requests + +# ============ CONFIGURATIE ============ + +ADB = r"your own path" +DEVICE = "127.0.0.1:5555" +TESSERACT_CMD = r"your own path" +DISCORD_WEBHOOK_URL = "your own token" + +# Replit bot API β€” updates the live countdown embed automatically +REPLIT_BOT_URL = "Your own token" + +# Timer region on screen (calibrated) +CHAMPIONSHIP_BOX = (280, 930, 510, 1010) + +if TESSERACT_CMD: + pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD + +# ============ ADB HELPERS ============ + +def get_adb_executable(): + if os.path.exists(ADB): + return ADB + exe = shutil.which("adb") + if exe is None: + raise FileNotFoundError(f"adb not found. Checked: {ADB} and system PATH") + return exe + +def get_target_device(): + exe = get_adb_executable() + if DEVICE: + res = subprocess.run([exe, "-s", DEVICE, "get-state"], capture_output=True, text=True) + if res.returncode == 0 and "device" in (res.stdout or "").lower(): + return DEVICE + return None + +def adb(cmd): + exe = get_adb_executable() + device = get_target_device() + if device: + return subprocess.run([exe, "-s", device] + cmd, capture_output=True) + return subprocess.run([exe] + cmd, capture_output=True) + +def capture_screen_bytes(): + result = adb(["exec-out", "screencap", "-p"]) + if result.returncode != 0 or not result.stdout: + return None + return result.stdout + +def normalize_pil_image(img): + if img is None: + return None + if img.width < img.height: + return img.rotate(-90, expand=True) + return img + +# ============ OCR ============ + +def read_championship_timer(): + """Read the championship timer from BlueStacks. + + Improvements: + - Try a few nearby/expanded crop boxes in case CHAMPIONSHIP_BOX is slightly off + - Try both OTSU threshold and adaptive thresholding + - Resilient parsing that accepts HHMMSS digit runs + - Saves debug crops when `DEBUG` environment var is set + """ + def _parse_ocr_timer(ocr_text): + """Try to interpret OCR text as HH:MM:SS even when colons are missing. + Returns a timedelta or None. + """ + if not ocr_text: + return None + # First try direct HH:MM:SS + m = re.search(r"(\d{1,2}):(\d{2}):(\d{2})", ocr_text) + if m: + h, mm, ss = (int(g) for g in m.groups()) + if 0 <= mm < 60 and 0 <= ss < 60: + return timedelta(hours=h, minutes=mm, seconds=ss) + + # Remove non-digits and try to recover from HHMMSS or HMMSS styles + digits = re.sub(r"\D", "", ocr_text) + if len(digits) >= 6: + # Prefer the last 6 digits as HHMMSS (handles stray leading artifacts) + cand = digits[-6:] + hh = int(cand[0:2]) + mm = int(cand[2:4]) + ss = int(cand[4:6]) + if 0 <= mm < 60 and 0 <= ss < 60: + return timedelta(hours=hh, minutes=mm, seconds=ss) + if len(digits) == 5: + # HMMSS -> H:MM:SS + hh = int(digits[0]) + mm = int(digits[1:3]) + ss = int(digits[3:5]) + if 0 <= mm < 60 and 0 <= ss < 60: + return timedelta(hours=hh, minutes=mm, seconds=ss) + return None + + data = capture_screen_bytes() + if data is None: + return None, "" + + try: + img = Image.open(io.BytesIO(data)) + except Exception: + return None, "" + + img = normalize_pil_image(img) + + # candidate boxes: original, slightly expanded, and shifted variants + x1, y1, x2, y2 = CHAMPIONSHIP_BOX + candidates = [ + (x1, y1, x2, y2), + (max(0, x1 - 10), max(0, y1 - 8), x2 + 10, y2 + 8), + (max(0, x1 - 20), max(0, y1 - 12), x2 + 20, y2 + 12), + (max(0, x1 - 6), y1, x2 + 6, y2), + (x1, max(0, y1 - 6), x2, y2 + 6), + ] + + debug = os.getenv("DEBUG", os.getenv("CFV3_DEBUG", "0")) == "1" + best_raw = "" + + for i, box in enumerate(candidates): + bx1, by1, bx2, by2 = box + try: + crop = img.crop((bx1, by1, bx2, by2)) + except Exception: + continue + + gray = cv2.cvtColor(np.array(crop), cv2.COLOR_RGB2GRAY) + scale = 4 + resized = cv2.resize(gray, (gray.shape[1] * scale, gray.shape[0] * scale), + interpolation=cv2.INTER_CUBIC) + + # Try template matcher directly on the resized grayscale first + try: + parsed_img, repr_txt = _digit_template_from_image(resized) + if parsed_img is not None: + return parsed_img, repr_txt + except Exception: + pass + + # Try OTSU binary + _, thresh_otsu = cv2.threshold(resized, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + # Try adaptive (helps with gradients/lighting) + thresh_adapt = cv2.adaptiveThreshold(resized, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, + cv2.THRESH_BINARY, 11, 2) + + for prep, img_for_ocr in (("otsu", thresh_otsu), ("adapt", thresh_adapt)): + # Try multiple variants: normal, inverted, and a slightly sharpened version + variants = [] + variants.append((prep, img_for_ocr)) + try: + inv = cv2.bitwise_not(img_for_ocr) + variants.append((prep + "_inv", inv)) + except Exception: + pass + + # simple unsharp mask (sharpen) + try: + blur = cv2.GaussianBlur(img_for_ocr, (0, 0), 3) + sharpen = cv2.addWeighted(img_for_ocr, 1.5, blur, -0.5, 0) + variants.append((prep + "_sh", sharpen)) + except Exception: + pass + + for vname, vimg in variants: + # Also try morphological variants: eroded (removes thin outlines) and closed (fills gaps) + morphs = [(vname, vimg)] + try: + kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) + eroded = cv2.erode(vimg, kern, iterations=1) + morphs.append((vname + "_er", eroded)) + closed = cv2.morphologyEx(vimg, cv2.MORPH_CLOSE, kern, iterations=1) + morphs.append((vname + "_cl", closed)) + except Exception: + pass + + for mvname, mvimg in morphs: + # Run Tesseract with explicit OEM and try multiple PSM modes + ocr_text = "" + for psm in (7, 6, 8): + try: + txt = pytesseract.image_to_string( + mvimg, + config=f"--oem 3 --psm {psm} -c tessedit_char_whitelist=0123456789:" + ).strip() + except Exception: + txt = "" + if txt: + ocr_text = txt + break + + if debug: + os.makedirs("debug", exist_ok=True) + safename = f"champ_crop_{i}_{vname}.png" + cv2.imwrite(os.path.join("debug", safename), vimg) + with open(os.path.join("debug", f"champ_ocr_{i}_{vname}.txt"), "w", encoding="utf-8") as f: + f.write(ocr_text) + + # quick accept if exact match with colons + m = re.search(r"(\d{1,2}):(\d{2}):(\d{2})", ocr_text) + if m: + h, mm, ss = (int(g) for g in m.groups()) + if 0 <= mm < 60 and 0 <= ss < 60: + return timedelta(hours=h, minutes=mm, seconds=ss), ocr_text + + # try robust digit parsing + parsed = _parse_ocr_timer(ocr_text) + if parsed is not None: + return parsed, ocr_text + + # remember best raw for reporting + if len(ocr_text) > len(best_raw): + best_raw = ocr_text + + # If Tesseract approaches failed, try a template-based digit OCR fallback + try: + parsed_fallback, repr_text = _digit_template_fallback(img.crop((x1, y1, x2, y2))) + if parsed_fallback is not None: + return parsed_fallback, repr_text + except Exception: + pass + + return None, best_raw + + +def calibrate_championship_box(step=10, max_offset=40): + """Scan nearby boxes around CHAMPIONSHIP_BOX to find the crop with the most digit-like contours. + Saves candidate crops to `debug/calibrate/` and prints a recommended box. + """ + data = capture_screen_bytes() + if data is None: + print("Could not capture screen for calibration.") + return None + try: + img = Image.open(io.BytesIO(data)) + except Exception: + print("Failed to parse screenshot for calibration.") + return None + img = normalize_pil_image(img) + x1, y1, x2, y2 = CHAMPIONSHIP_BOX + best = None + scores = [] + os.makedirs(os.path.join("debug", "calibrate"), exist_ok=True) + for dx in range(-max_offset, max_offset + 1, step): + for dy in range(-max_offset, max_offset + 1, step): + bx1 = max(0, x1 + dx) + by1 = max(0, y1 + dy) + bx2 = min(img.width, x2 + dx) + by2 = min(img.height, y2 + dy) + if bx2 <= bx1 or by2 <= by1: + continue + crop = img.crop((bx1, by1, bx2, by2)) + gray = cv2.cvtColor(np.array(crop), cv2.COLOR_RGB2GRAY) + resized = cv2.resize(gray, (gray.shape[1] * 4, gray.shape[0] * 4), interpolation=cv2.INTER_CUBIC) + _, th = cv2.threshold(resized, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + mean = np.mean(th) + mask = th if mean > 127 else 255 - th + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + # score by number of reasonably large contours + cnt_score = 0 + h_img, w_img = resized.shape[:2] + for cnt in contours: + x, y, w, h = cv2.boundingRect(cnt) + if w * h < 200: + continue + if h < 0.25 * h_img: + continue + cnt_score += 1 + scores.append(((bx1, by1, bx2, by2), cnt_score)) + # save crop for visual inspection + fn = os.path.join("debug", "calibrate", f"crop_{bx1}_{by1}_{bx2}_{by2}.png") + cv2.imwrite(fn, resized) + if not scores: + print("No candidate crops generated during calibration.") + return None + scores.sort(key=lambda t: t[1], reverse=True) + best_box, best_score = scores[0] + print(f"Recommended CHAMPIONSHIP_BOX: {best_box} (score={best_score})") + print("Saved candidate crops to debug/calibrate/. Inspect the top crops and update CHAMPIONSHIP_BOX accordingly.") + return best_box + + +def _build_digit_templates(size=(60, 90)): + fonts = [] + font_paths = [ + r"C:\Windows\Fonts\arial.ttf", + r"C:\Windows\Fonts\verdana.ttf", + r"C:\Windows\Fonts\tahoma.ttf", + ] + for path in font_paths: + try: + fonts.append(ImageFont.truetype(path, 100)) + except Exception: + pass + if not fonts: + fonts.append(ImageFont.load_default()) + + templates = {} + for digit in range(10): + best_img = None + best_err = None + for font in fonts: + img = Image.new("L", size, color=255) + draw = ImageDraw.Draw(img) + text = str(digit) + try: + w, h = font.getsize(text) + except Exception: + bbox = draw.textbbox((0, 0), text, font=font) + w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] + draw.text(((size[0] - w) / 2, (size[1] - h) / 2), text, font=font, fill=0) + arr = np.array(img) + _, arr = cv2.threshold(arr, 128, 255, cv2.THRESH_BINARY_INV) + error = np.mean(arr.astype(np.float32)) + if best_err is None or error < best_err: + best_err = error + best_img = arr + templates[str(digit)] = best_img + return templates + + +def _classify_digit_img(img_gray, templates): + if img_gray is None: + return None + try: + _, thresh = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + except Exception: + thresh = img_gray + best_digit = None + best_score = -1.0 + for digit, tpl in templates.items(): + try: + resized = cv2.resize(thresh, tpl.shape[::-1], interpolation=cv2.INTER_AREA) + res = cv2.matchTemplate(resized, tpl, cv2.TM_CCOEFF_NORMED) + _, maxVal, _, _ = cv2.minMaxLoc(res) + except Exception: + maxVal = -1.0 + if maxVal > best_score: + best_score = maxVal + best_digit = digit + if best_digit is None: + return None + return int(best_digit) + + +def _digit_template_fallback(pil_crop): + """Segment digit regions from the crop and match against templates. + Returns (timedelta, repr_text) or (None, '') + """ + img = pil_crop + gray = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2GRAY) + # gentle blur and adaptive threshold to get digits + blur = cv2.GaussianBlur(gray, (3, 3), 0) + _, th = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) + th = cv2.morphologyEx(th, cv2.MORPH_CLOSE, kernel, iterations=1) + # Choose mask where digits are white for contour finding + mean_val = np.mean(th) + if mean_val > 127: + mask = th + else: + mask = 255 - th + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + rects = [] + h_img, w_img = gray.shape[:2] + for cnt in contours: + x, y, w, h = cv2.boundingRect(cnt) + if w * h < 100: # too small + continue + rects.append((x, y, w, h)) + if not rects: + return None, "" + rects = sorted(rects, key=lambda r: r[0]) + templates = _build_digit_templates((60, 90)) + digits = [] + for x, y, w, h in rects: + pad = 4 + x0 = max(0, x - pad) + y0 = max(0, y - pad) + x1 = min(w_img, x + w + pad) + y1 = min(h_img, y + h + pad) + roi = gray[y0:y1, x0:x1] + d = _classify_digit_img(roi, templates) + if d is None: + return None, "" + digits.append(str(d)) + digits_only = "".join(digits) + if len(digits_only) >= 6: + cand = digits_only[-6:] + hh = int(cand[0:2]) + mm = int(cand[2:4]) + ss = int(cand[4:6]) + if 0 <= mm < 60 and 0 <= ss < 60: + return timedelta(hours=hh, minutes=mm, seconds=ss), f"{hh:02d}:{mm:02d}:{ss:02d}" + return None, ":".join(digits) + + +def _digit_template_from_image(resized_gray): + """Attempt digit recognition from a resized grayscale image (numpy array). + Returns (timedelta, repr_text) or (None, ''). + """ + if resized_gray is None: + return None, "" + # Ensure binary where digits are white + try: + _, th = cv2.threshold(resized_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + except Exception: + th = resized_gray + # find contours of the digits using correct polarity + mean_val = np.mean(th) + if mean_val > 127: + mask = th + else: + mask = 255 - th + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return None, "" + rects = [] + h_img, w_img = resized_gray.shape[:2] + for cnt in contours: + x, y, w, h = cv2.boundingRect(cnt) + if w * h < 200: # skip tiny noise + continue + if h < 0.25 * h_img: + continue + rects.append((x, y, w, h)) + if not rects: + return None, "" + rects = sorted(rects, key=lambda r: r[0]) + templates = _build_digit_templates((60, 90)) + digits = [] + for x, y, w, h in rects: + pad = 6 + x0 = max(0, x - pad) + y0 = max(0, y - pad) + x1 = min(w_img, x + w + pad) + y1 = min(h_img, y + h + pad) + roi = resized_gray[y0:y1, x0:x1] + # resize roi to template size for matching + try: + roi_resized = cv2.resize(roi, (templates['0'].shape[1], templates['0'].shape[0]), interpolation=cv2.INTER_AREA) + except Exception: + roi_resized = roi + d = _classify_digit_img(roi_resized, templates) + if d is None: + return None, "" + digits.append(str(d)) + digits_only = "".join(digits) + if len(digits_only) >= 6: + cand = digits_only[-6:] + hh = int(cand[0:2]) + mm = int(cand[2:4]) + ss = int(cand[4:6]) + if 0 <= mm < 60 and 0 <= ss < 60: + return timedelta(hours=hh, minutes=mm, seconds=ss), f"{hh:02d}:{mm:02d}:{ss:02d}" + return None, ":".join(digits) + +# ============ DISCORD ============ + +def send_to_discord(remaining, end_time): + """Send timer info to Discord""" + if remaining is None: + message = { + "content": "❌ Could not read timer. Make sure BlueStacks is running and championship screen is visible." + } + else: + message = { + "content": ( + f"πŸ• **C.A.T.S. Championship Timer**\n\n" + f"**Current Time Remaining:** `{remaining}`\n" + f"**Expected End Time:** `{end_time.strftime('%d/%m/%Y %H:%M')}`\n\n" + f"_Bot countdown updated automatically β€” use `/countdown_status` to check._" + ) + } + + try: + resp = requests.post(DISCORD_WEBHOOK_URL, json=message) + resp.raise_for_status() + print("βœ“ Timer sent to Discord!") + return True + except Exception as e: + print(f"❌ Failed to send to Discord: {e}") + return False + +# ============ REPLIT BOT UPDATE ============ + +def update_replit_bot(end_time): + """Send end time to the Replit bot so it updates the live countdown embed""" + payload = {"end_time": end_time.strftime("%d/%m/%Y %H:%M")} + try: + resp = requests.post(REPLIT_BOT_URL, json=payload, timeout=10) + resp.raise_for_status() + print(f"βœ“ Bot updated! End time: {end_time.strftime('%d/%m/%Y %H:%M')}") + return True + except Exception as e: + print(f"❌ Failed to update bot: {e}") + return False + +# ============ MAIN ============ + +def main(): + print("πŸ• C.A.T.S. Timer Reader") + print("=" * 50) + print("Reading championship timer...\n") + + # Calibration mode + if len(sys.argv) > 1 and sys.argv[1] == "calibrate": + calibrate_championship_box() + return True + + remaining, raw_text = read_championship_timer() + + if remaining is None: + print(f"❌ Could not read timer") + print(f" OCR text: '{raw_text}'") + print(f"\nMake sure:") + print(" 1. BlueStacks is running") + print(" 2. Championship screen is visible") + print(" 3. Timer is clearly visible") + return False + + now = datetime.now() + end_time = now + remaining + + print(f"βœ“ Timer found: {remaining}") + print(f"βœ“ OCR text: '{raw_text}'") + print(f"βœ“ End time: {end_time.strftime('%d/%m/%Y %H:%M')}") + print(f"\nSending to Discord and updating bot...") + + send_to_discord(remaining, end_time) + update_replit_bot(end_time) + + print("\n" + "=" * 50) + print("βœ“ Done! Bot countdown is now live in Discord.") + print("=" * 50) + return True + +if __name__ == "__main__": + main() diff --git a/discord-bot/screenshot-output/discordbot.PNG b/discord-bot/screenshot-output/discordbot.PNG new file mode 100644 index 0000000..8c4531d Binary files /dev/null and b/discord-bot/screenshot-output/discordbot.PNG differ diff --git a/discord-bot/screenshot-output/discordbot2.png b/discord-bot/screenshot-output/discordbot2.png new file mode 100644 index 0000000..44fab1b Binary files /dev/null and b/discord-bot/screenshot-output/discordbot2.png differ diff --git a/discord-bot/turn on-off/turn-off.bat b/discord-bot/turn on-off/turn-off.bat new file mode 100644 index 0000000..fb30df0 --- /dev/null +++ b/discord-bot/turn on-off/turn-off.bat @@ -0,0 +1,10 @@ +ZERO +PROTOCOL +Level 1 badge + +Necropolis +Level 1 badge + +Master of +Orion +Level 1 badge \ No newline at end of file diff --git a/discord-bot/turn on-off/turn-on.bat b/discord-bot/turn on-off/turn-on.bat new file mode 100644 index 0000000..4b76be2 --- /dev/null +++ b/discord-bot/turn on-off/turn-on.bat @@ -0,0 +1,21 @@ +@echo off +REM ============================================================ +REM Sets up a Windows scheduled task that runs the C.A.T.S. +REM timer reader every 15 minutes, automatically. +REM Run this file ONCE (double-click it) to turn automation ON. +REM ============================================================ + +set TASK_NAME=CATS_Timer_Reader +set SCRIPT_PATH=%~dp0C.A.T.S. Timer Reader - Send to Discord read timer only.py + +schtasks /Create /TN "%TASK_NAME%" /TR "python \"%SCRIPT_PATH%\"" /SC MINUTE /MO 15 /F + +echo. +echo ============================================================ +echo Done! The timer reader will now run automatically every +echo 15 minutes in the background. +echo. +echo To STOP it at any time, double-click: stop_auto_timer.bat +echo Or open Task Scheduler and disable/delete "%TASK_NAME%" +echo ============================================================ +pause diff --git a/readme.md b/readme.md index 50b4cf1..f0b1704 100644 --- a/readme.md +++ b/readme.md @@ -2,15 +2,6 @@ An automation script that automatically farms Quick Fights in **C.A.T.S.: Crash Arena Turbo Stars** using BlueStacks 5. It detects win/loss outcomes and taps the correct button every time β€” no ads, no mistakes. ---- - -## ✨ Features - -- Automatically taps Quick Fight, Start Fight, and the result button -- Detects **Victory**, **Defeat**, and **Defeat with Win Streak** separately -- Avoids accidental ad clicks on the "Keep Win Streak" button -- Tracks your win streak across fights -- Runs in a loop until you stop it --- @@ -35,6 +26,8 @@ pip install Pillow 1. Open BlueStacks and go to **Settings β†’ Advanced** 2. Enable **Android Debug Bridge (ADB)** 3. Note the IP and port shown (usually `127.0.0.1:5555`) +image + ### 2. Download ADB 1. Download [Platform Tools](https://developer.android.com/tools/releases/platform-tools) from Google @@ -54,63 +47,69 @@ Open a terminal and run: adb connect 127.0.0.1:5555 ``` You should see: `connected to 127.0.0.1:5555` +image + +"it should perform this action automaticly once you have setup your path correctly in the script" + + --- ## ▢️ How to Use 1. Open BlueStacks and launch **C.A.T.S.** -2. Navigate to the **Quick Fight** screen -3. Run the script: +2. Make sure your resolution is **1920 x 1080** +image + +4. Navigate to the **main menu** screen + +image +6. Run the script: ```bash -python cats_farm.py +python cats_farmV3.py ``` +image + 4. When prompted, enter your current win streak (or `0` if you have none) -5. Switch to BlueStacks β€” the script starts in 3 seconds! -To stop the script press **Ctrl + C** in the terminal. +"is should do this automaticly with the new version with the streak detector function" + +6. you will see a message saying Switch to BlueStacks β€” the script starts in 3 seconds! +image + +To stop the script press **q** in the terminal. +image + --- -## πŸ—ΊοΈ Button Coordinates -The script uses percentage-based coordinates (works across resolutions). These are calibrated for the default BlueStacks 5 layout: +## ✨ Features -| Action | X | Y | -|---|---|---| -| Quick Fight | 85 | 120 | -| Start Fight | 50 | 40 | -| OK (Victory) | 58 | 85 | -| Retreat (No streak) | 80 | 120 | -| Retreat (Streak loss) | 70 | 120 | +- Automatically taps Quick Fight, Start Fight, and the result button +- Detects **Victory**, **Defeat**, and **Defeat with Win Streak** separately +- Avoids accidental ad clicks on the "Keep Win Streak" button +- Tracks your win streak across fights +- Runs in a loop until you stop it -> If buttons are in different positions on your setup, hover over them in BlueStacks to get the coordinates and update the `tap()` calls in the script. +image --- ## πŸ”§ Troubleshooting -**Script clicks the wrong button** -β†’ Hover over the correct button in BlueStacks and update the coordinates in the script. - -**Fight not finished before result is detected** -β†’ Increase `time.sleep(12)` to a higher value like `time.sleep(20)`. **ADB not recognized** β†’ Make sure the path to `adb.exe` in the script is correct. **Unknown result detected** -β†’ The script will print the RGB value of the banner. Share it and the detection thresholds can be updated. +β†’ The script will send a screenshot to the debug folder ---- +**contact information** +β†’ you can send an email if you are really struggeling linked on my [github profile](https://github.com/lkyuca) -## πŸ“„ License - -MIT License β€” free to use and modify. - ---- ## πŸ™ Credits Built with Python + ADB + BlueStacks 5. -Game: [C.A.T.S.: Crash Arena Turbo Stars](https://zeptolab.com/games/cats) +Game: [C.A.T.S.: Crash Arena Turbo Stars](https://www.catsthegame.com/) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f7f016 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +opencv-python +numpy +pillow diff --git a/templates/ad_button.png b/templates/ad_button.png new file mode 100644 index 0000000..e69de29 diff --git a/templates/crop5.png b/templates/crop5.png new file mode 100644 index 0000000..7d7ce6b Binary files /dev/null and b/templates/crop5.png differ diff --git a/templates/quick_fight.png b/templates/quick_fight.png new file mode 100644 index 0000000..ae5bd2e Binary files /dev/null and b/templates/quick_fight.png differ