time.time() on Windows has ~15 ms resolution; a 0.0-second budget produced a deadline equal to the current tick, so the anchor-loop check never fired and traverse_calls reached 5 instead of 1. Using -1.0 puts the deadline one second in the past — guaranteed expired on any hardware. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
"""Regression tests for A5.open_wp fast-fail behavior.
|
|
|
|
Background: a stale curr_loc can land the char off the WP stone, so
|
|
select_by_template("A5_WP") never matches. The old escalation (NPC anchors +
|
|
a 6-step directed sweep) could loop for 5+ minutes, stranding the bot in town
|
|
("stuck in town"). open_wp now bails within a hard wall-clock budget and drops
|
|
the directed sweep, so the caller's fallback (buy at Malah / skip to stash)
|
|
takes over. See log/log.txt 2026-06-24.
|
|
"""
|
|
import town.a5 as a5_module
|
|
from town.a5 import A5
|
|
from pather import Location
|
|
|
|
|
|
class _Match:
|
|
def __init__(self, valid):
|
|
self.valid = valid
|
|
|
|
|
|
class _FakePather:
|
|
"""Pather whose node paths always 'succeed' (return True) — mirrors the real
|
|
failure mode where traverse returns True but the char is off the WP stone."""
|
|
def __init__(self):
|
|
self.traverse_calls = 0
|
|
|
|
def traverse_nodes(self, path, char, **kwargs):
|
|
self.traverse_calls += 1
|
|
return True
|
|
|
|
|
|
class _FakeChar:
|
|
def __init__(self):
|
|
self.select_calls = 0
|
|
|
|
def move(self, *args, **kwargs):
|
|
pass
|
|
|
|
def select_by_template(self, *args, **kwargs):
|
|
# The WP stone is never clickable in this scenario.
|
|
self.select_calls += 1
|
|
return False
|
|
|
|
|
|
def _patch_screen(monkeypatch):
|
|
# template_finder.search must report "WP not on screen" and grab()/is_visible
|
|
# must be cheap no-ops so the test never touches the real screen.
|
|
monkeypatch.setattr(a5_module.template_finder, "search", lambda *a, **k: _Match(False))
|
|
monkeypatch.setattr(a5_module, "grab", lambda *a, **k: None)
|
|
monkeypatch.setattr(a5_module, "is_visible", lambda *a, **k: False)
|
|
monkeypatch.setattr(a5_module, "wait", lambda *a, **k: None)
|
|
|
|
|
|
def test_open_wp_fails_fast_when_wp_never_clickable(monkeypatch):
|
|
_patch_screen(monkeypatch)
|
|
pather = _FakePather()
|
|
char = _FakeChar()
|
|
subject = A5(pather, char)
|
|
|
|
result = subject.open_wp(Location.A5_TOWN_START, quick=False)
|
|
|
|
assert result is False
|
|
# Direct attempt + at most the three NPC anchors — never an unbounded sweep.
|
|
# (Old code added a 6-step directed sweep that re-traversed each step.)
|
|
assert pather.traverse_calls <= 7
|
|
|
|
|
|
def test_open_wp_quick_mode_skips_anchors(monkeypatch):
|
|
_patch_screen(monkeypatch)
|
|
pather = _FakePather()
|
|
char = _FakeChar()
|
|
subject = A5(pather, char)
|
|
|
|
result = subject.open_wp(Location.A5_TOWN_START, quick=True)
|
|
|
|
assert result is False
|
|
# quick mode: only the single direct node path is attempted, no anchors.
|
|
assert pather.traverse_calls == 1
|
|
|
|
|
|
def test_open_wp_honors_time_budget(monkeypatch):
|
|
_patch_screen(monkeypatch)
|
|
# Force the budget negative so the deadline is always in the past (time.time()
|
|
# resolution on Windows is ~15 ms, so 0.0 stays non-expired within one tick).
|
|
monkeypatch.setattr(A5, "_OPEN_WP_BUDGET_S", -1.0)
|
|
pather = _FakePather()
|
|
char = _FakeChar()
|
|
subject = A5(pather, char)
|
|
|
|
result = subject.open_wp(Location.A5_TOWN_START, quick=False)
|
|
|
|
assert result is False
|
|
# Only the direct traverse runs; the deadline check stops the anchor loop.
|
|
assert pather.traverse_calls == 1
|