fix(merc): stop re-hunting an undetectable resurrect NPC every game (~80s each)

In nightmare the merc dies most games, so resurrect_merc runs constantly — and Qual-Kehk
detection was failing 100% of the time (5 timeouts in 5 attempts, 107 hover attempts over
12 games). Each failed hunt costs ~40s and the code retried once, so a dead merc cost
~80s in EVERY game. Game length blew out to 185-250s against a normal ~60s.

GameStats._merc_resurrect_failed did not help: log_start_game resets it, so it only ever
suppressed a second attempt within one game. Nothing carried across games.

The name tag template is degenerate rather than merely stale — every grid-sweep "hit"
reported the identical score at unrelated positions:
    found name tag at (255, 227)  (score 0.424)
    found name tag at (1110, 100) (score 0.424)
    found name tag at (930, 310)  (score 0.424)
so "found" is meaningless; it is matching uniform background.

Fix is cost containment, not detection: a cross-game circuit breaker on GameStats that
log_start_game deliberately does NOT reset — _merc_resurrect_fail_streak and
_merc_resurrect_skip_until, with Bot._MERC_RESURRECT_FAIL_LIMIT=2 and
_MERC_RESURRECT_SKIP_GAMES=15. The retry is also skipped once the streak is >=1, since
that is a second guaranteed-futile 40s hunt. Both counters clear on any successful
resurrect so a transient failure cannot permanently disable resurrecting.

Simulated over 30 games with an undetectable NPC: 60 hunts -> 4 (~40 min -> ~2.7 min).
Measured live: the breaker engaged on game 2 and game times went 250s / 185s -> 14s, 14s,
43s, 71s, 111s.

Still open: recapturing qual_name_tag_white.png is the actual fix for detection.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
alexpolo1
2026-08-27 06:26:01 +02:00
co-authored by Claude Opus 5
parent abe0a54849
commit fa72da56e3
3 changed files with 83 additions and 5 deletions
+34 -4
View File
@@ -54,6 +54,12 @@ from utils.stealth import maybe_afk_break, should_skip_run
class Bot:
# Merc-resurrect circuit breaker. A resurrect NPC that cannot be detected costs a full
# NPC hunt (~40s) per attempt, twice per game, in every game — with nothing to show for
# it. After this many consecutive failures, stop trying for a while and run mercless.
_MERC_RESURRECT_FAIL_LIMIT = 2
_MERC_RESURRECT_SKIP_GAMES = 15
def __init__(self, game_stats: GameStats):
self._game_stats = game_stats
self._messenger = Messenger()
@@ -744,18 +750,38 @@ class Bot:
keyboard.send("o")
wait(0.2, 0.3)
merc_visible = True
gs = self._game_stats
skip_until = getattr(gs, "_merc_resurrect_skip_until", 0)
if not merc_visible and gs._game_counter < skip_until:
Logger.debug(
f"Skipping merc resurrect until game {skip_until} "
f"(resurrect NPC unreachable {getattr(gs, '_merc_resurrect_fail_streak', 0)}x in a row)"
)
merc_visible = True # suppress this game's attempt without touching state
if not merc_visible and not self._game_stats._merc_resurrect_failed:
Logger.info("Resurrect merc")
new_loc = self._town_manager.resurrect(self._curr_loc)
if new_loc is False:
# Retry once - sometimes resurrect fails due to timing
Logger.warning("Resurrect failed, retrying")
wait(0.5, 0.6)
new_loc = self._town_manager.resurrect(self._curr_loc)
# Only retry while resurrect still works sometimes. When the NPC simply
# cannot be found, the retry is a second guaranteed ~40s hunt.
if getattr(gs, "_merc_resurrect_fail_streak", 0) < 1:
Logger.warning("Resurrect failed, retrying")
wait(0.5, 0.6)
new_loc = self._town_manager.resurrect(self._curr_loc)
else:
Logger.warning("Resurrect failed — skipping the retry, this NPC has been unreachable")
if new_loc is False:
# Failed to resurrect (can't afford or other error) - don't log death, just continue
Logger.warning("Failed to resurrect merc after retry, continuing without merc")
self._game_stats._merc_resurrect_failed = True
gs._merc_resurrect_fail_streak = getattr(gs, "_merc_resurrect_fail_streak", 0) + 1
if gs._merc_resurrect_fail_streak >= self._MERC_RESURRECT_FAIL_LIMIT:
gs._merc_resurrect_skip_until = gs._game_counter + self._MERC_RESURRECT_SKIP_GAMES
Logger.warning(
f"Merc resurrect has failed {gs._merc_resurrect_fail_streak}x in a row — "
f"skipping it until game {gs._merc_resurrect_skip_until} to stop burning "
f"~40s per hunt. Running without a merc until then."
)
# resurrect() may have traveled to A4 before failing — re-anchor
breadcrumb = getattr(self._town_manager, "last_known_loc", None)
if breadcrumb:
@@ -768,6 +794,10 @@ class Bot:
else:
self._game_stats.log_merc_death()
self._curr_loc = new_loc
# It worked — clear the breaker so a transient failure never
# permanently disables resurrecting.
gs._merc_resurrect_fail_streak = 0
gs._merc_resurrect_skip_until = 0
# Gamble if needed
if _maint_timed_out("gamble"): return
+9
View File
@@ -97,6 +97,11 @@ class GameStats:
self._last_status_report_run = 0
self._last_failure_reason = None
self._merc_resurrect_failed = False
# Cross-game merc-resurrect circuit breaker. Qual-Kehk detection can fail 100% of
# the time (stale name-tag template -> a degenerate constant 0.424 match), and the
# per-game flag above does not stop that being retried every game at ~40s a hunt.
self._merc_resurrect_fail_streak = 0
self._merc_resurrect_skip_until = 0
# Auto-downgrade tracking: timestamps of chickens+deaths
self._downgrade_events: deque[float] = deque()
os.makedirs("log/stats", exist_ok=True)
@@ -271,6 +276,10 @@ class GameStats:
self._game_counter += 1
self._timer = time.time()
self._merc_resurrect_failed = False
# NOTE: _merc_resurrect_fail_streak / _merc_resurrect_skip_until are deliberately
# NOT reset here. They are the cross-game circuit breaker — resetting the per-game
# flag alone meant a permanently undetectable resurrect NPC was re-hunted (twice,
# ~40s each) in every single game.
# Clear the previous game's failure reason so this game's events can't be
# attributed to a stale reason from an earlier failure.
self._last_failure_reason = None