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`)
+
+
### 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`
+
+
+"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**
+
+
+4. Navigate to the **main menu** screen
+
+
+6. Run the script:
```bash
-python cats_farm.py
+python cats_farmV3.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.
+"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!
+
+
+To stop the script press **q** in the terminal.
+
+
---
-## πΊοΈ 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.
+
---
## π§ 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