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 <noreply@anthropic.com>
This commit is contained in:
alexpolo1
2026-08-27 06:26:01 +02:00
parent abe0a54849
commit fa72da56e3
3 changed files with 83 additions and 5 deletions

View File

@@ -589,7 +589,46 @@ exact failing case (repair started from `a4_town_start`) and completed successfu
**Residual, different cause:** the failures that remain are NPC-detection flakiness — e.g.
`open_npc_menu: timed out finding qual_kehk` during merc resurrect burns ~40s and strands the
char. Same family as Bugs 3/4/6/7, not act desync.
char. Same family as Bugs 3/4/6/7, not act desync. See Bug 29.
### Bug 29: undetectable resurrect NPC burned ~80s in EVERY game (2026-08-27)
**Files:** `src/bot.py`, `src/game_stats.py`
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
across 12 games). Each failed hunt costs ~40s, and the code retried once, so a dead merc cost
**~80s per game, forever**. Game length blew out to 185-250s versus a normal ~60s.
**Why the existing guard didn't help:** `GameStats._merc_resurrect_failed` is reset in
`log_start_game`, so it only suppresses a second attempt *within one game*. Nothing carried
the knowledge across games.
**The name tag template is degenerate, not merely stale.** Every grid-sweep "hit" reported
the *identical* score at completely different screen positions:
```
grid sweep found name tag at (255, 227) (score 0.424)
grid sweep found name tag at (1110, 100) (score 0.424)
grid sweep found name tag at (930, 310) (score 0.424)
```
An identical score at unrelated positions is a constant/degenerate match — the template is
matching uniform background, so "found" is meaningless. **If you see a repeated identical
match score, suspect the template, not the search.**
**Fix (cost containment, not detection):** a cross-game circuit breaker on `GameStats`,
deliberately NOT reset by `log_start_game`:
- `_merc_resurrect_fail_streak` — consecutive failed resurrects
- `_merc_resurrect_skip_until` — game number to resume trying at
- `Bot._MERC_RESURRECT_FAIL_LIMIT = 2`, `_MERC_RESURRECT_SKIP_GAMES = 15`
- the retry is skipped once the streak is ≥1 (a second guaranteed-futile 40s hunt)
- both counters reset on any successful resurrect, so a transient failure can't permanently
disable resurrecting
Over 30 games with an undetectable NPC: **60 hunts → 4** (~40 min of waste → ~2.7 min).
Measured live: game times went 250s / 185s → **14s, 14s, 43s, 71s, 111s** once the breaker
engaged. The bot runs mercless for 15 games, then tries again.
**Still open:** Qual-Kehk detection itself. Recapturing `qual_name_tag_white.png` is the real
fix; the breaker only stops it being expensive.
---

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

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