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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions pyclashbot/bot/fight.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pyclashbot.bot.nav import (
check_for_in_battle_with_delay,
check_for_trophy_reward_menu,
check_if_battle_has_ended,
check_if_in_battle,
check_if_on_clash_main_menu,
get_to_activity_log,
Expand Down Expand Up @@ -233,6 +234,7 @@ def wait_for_elixer(
) -> Literal["restart", "no battle"] | bool:
"""Method to wait for 4 elixer during a battle"""
start_time = time.time()
battle_detection_lost_count = 0

while not count_elixer(emulator, random_elixer_wait):
# debug screenshot saving removed from production
Expand All @@ -256,8 +258,24 @@ def wait_for_elixer(
return "restart"

if not check_for_in_battle_with_delay(emulator):
logger.change_status(status="Not in battle, stopping waiting for elixer.")
return "no battle"
if check_if_battle_has_ended(emulator):
logger.change_status(status="Not in battle anymore (confirmed), stopping waiting for elixer.")
return "no battle"

battle_detection_lost_count += 1
logger.change_status(
status="Lost battle detection while waiting for elixer; assuming still in battle.",
)
if battle_detection_lost_count >= 4:
logger.change_status(
status="Lost battle detection repeatedly while waiting for elixer; assuming battle ended.",
)
return "no battle"

interruptible_sleep(0.5)
continue

battle_detection_lost_count = 0

logger.change_status(
f"Took {str(time.time() - start_time)[:4]}s for {random_elixer_wait} elixer.",
Expand Down Expand Up @@ -672,12 +690,34 @@ def _fight_loop(emulator, logger: Logger, recording_flag: bool) -> bool:
create_default_bridge_iar(emulator)
collections.deque(maxlen=3)
prev_cards_played = logger.get_cards_played()
battle_detection_lost_count = 0

# Initialize battle strategy and start timing
battle_strategy = BattleStrategy()
battle_strategy.start_battle()

while check_for_in_battle_with_delay(emulator):
while True:
if not check_for_in_battle_with_delay(emulator):
if check_if_battle_has_ended(emulator):
break

battle_detection_lost_count += 1
logger.change_status(
f"Lost battle detection mid-fight ({battle_detection_lost_count}); waiting it out.",
)

# If we've lost detection several times in a row, assume the battle
# ended even if we couldn't confirm it (prevents infinite loops if UI changes).
if battle_detection_lost_count >= 4:
logger.change_status(
"Lost battle detection repeatedly; assuming battle ended.",
)
break

interruptible_sleep(1)
continue

battle_detection_lost_count = 0
# debug screenshot saving removed from production

# Get elixir amount and thresholds based on current battle phase
Expand All @@ -702,8 +742,12 @@ def _fight_loop(emulator, logger: Logger, recording_flag: bool) -> bool:
break

if not check_if_in_battle(emulator):
logger.change_status("Not in a battle anymore")
break
if check_if_battle_has_ended(emulator):
logger.change_status("Not in a battle anymore (confirmed)")
break

logger.change_status("Lost battle detection; continuing fight loop.")
continue

play_start_time = time.time()
if play_a_card(emulator, logger, recording_flag, battle_strategy) is False:
Expand All @@ -726,9 +770,29 @@ def _random_fight_loop(emulator, logger) -> bool:
logger.change_status(status="Starting battle with random plays")
fight_timeout = 5 * 60 # 5 minutes
start_time = time.time()
battle_detection_lost_count = 0

# while in battle:
while check_if_in_battle(emulator):
while True:
if not check_for_in_battle_with_delay(emulator):
if check_if_battle_has_ended(emulator):
break

battle_detection_lost_count += 1
logger.change_status(
f"Lost battle detection mid-fight ({battle_detection_lost_count}); waiting it out.",
)

if battle_detection_lost_count >= 4:
logger.change_status(
"Lost battle detection repeatedly; assuming battle ended.",
)
break

interruptible_sleep(1)
continue

battle_detection_lost_count = 0
if time.time() - start_time > fight_timeout:
logger.change_status("_random_fight_loop() timed out. Breaking")
return False
Expand Down
47 changes: 44 additions & 3 deletions pyclashbot/bot/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def check_for_in_battle_with_delay(emulator) -> bool:
battle_result = check_if_in_battle(emulator)
if battle_result: # True for any battle type ("1v1", "2v2")
return True
interruptible_sleep(0.1)
return False


Expand All @@ -90,10 +91,19 @@ def get_pixel(y: int, x: int) -> list[int] | None:
return iar[y][x].tolist()

def is_bright(pixel: list[int] | None, threshold: int = 180) -> bool:
return (pixel is not None and all(channel >= threshold for channel in pixel)) or (
if pixel is None:
return False
return all(channel >= threshold for channel in pixel) or (
150 <= pixel[0] <= 195 and 40 <= pixel[1] <= 60 and 40 <= pixel[2] <= 60
)

def is_filled_crown(pixel: list[int] | None) -> bool:
"""Check if pixel is a gold/yellow filled crown or UI accent."""
if pixel is None:
return False
r, g, b = pixel
return r >= 170 and g >= 130 and b <= 140

def is_scoreboard_purple(pixel: list[int] | None) -> bool:
if pixel is None:
return False
Expand All @@ -102,8 +112,10 @@ def is_scoreboard_purple(pixel: list[int] | None) -> bool:

def check_mode(coords: list[tuple[int, int]]) -> bool:
pixels = [get_pixel(y, x) for y, x in coords]
bright_required = len(coords) - 1
bright_count = sum(1 for pixel in pixels[:-1] if is_bright(pixel))
# Allow one of the "bright" UI pixels to change (crowns filling, overlays,
# small rendering differences) while still requiring the purple scoreboard.
bright_required = max(1, len(coords) - 2)
bright_count = sum(1 for pixel in pixels[:-1] if is_bright(pixel) or is_filled_crown(pixel))
return bright_count >= bright_required and is_scoreboard_purple(pixels[-1])

# When the emote is closed these pixels are not considered bright:
Expand All @@ -119,6 +131,35 @@ def check_mode(coords: list[tuple[int, int]]) -> bool:
return False


def check_for_post_battle_button(emulator) -> bool:
"""Checks for the post-battle OK/Exit buttons (battle-end screen)."""
image = emulator.screenshot()
if image is None:
return False

if find_image(image, "ok_post_battle_button", tolerance=0.85) is not None:
return True

if find_image(image, "exit_battle_button", tolerance=0.9) is not None:
return True

return False


def check_if_battle_has_ended(emulator) -> bool:
"""Best-effort confirmation that a battle ended (avoid false positives mid-fight)."""
if check_if_on_clash_main_menu(emulator):
return True

if check_for_trophy_reward_menu(emulator):
return True

if check_for_post_battle_button(emulator):
return True

return False


def check_for_trophy_reward_menu(emulator) -> bool:
iar = emulator.screenshot()

Expand Down