The same defect has now appeared twice: detect_current_act was fixed in Bug 28, wait_for_town_spawn kept it, and nothing linked the two. These tests assert the invariant for both, so a future fix to one cannot silently leave the other behind: - both must require _ACT_DETECT_MARGIN over the best rival-act marker - both must score own-act and rival markers on ONE image (scores from two different grabs are not comparable — the character can move between them) - ambiguous detection must return None rather than guess Audited every other TOWN_MARKERS consumer: bot.py:561 and main_menu.py:40/61 use best_match only as a boolean "are we in town?" check and never derive an act, so they need no margin. Co-Authored-By: Claude Opus 5 <[email protected]>
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Act identity must never be decided on a thin margin.
|
|
|
|
Bug 28: detect_current_act committed to whatever town marker won, and in Act 4
|
|
the A5_TOWN_1 template scored 0.636 on corner scenery while the genuine
|
|
A4_TOWN_5 scored 0.619. A 1.7pp gap decided the act, and the bot ran A5 pathing
|
|
inside Act 4 after every Halbu repair trip.
|
|
|
|
That was fixed in detect_current_act and NOT in wait_for_town_spawn, which runs
|
|
first every game and sets the act for everything after it. On 2026-08-28 the
|
|
character spawned in Harrogath and it reported a4_town_start every game.
|
|
|
|
These tests pin the invariant for BOTH, because the bug has now been the same
|
|
twice: a fix applied to one call site while its twin kept the defect.
|
|
"""
|
|
import inspect
|
|
|
|
import pytest
|
|
|
|
|
|
ACT_DECIDING_FUNCS = ["detect_current_act", "wait_for_town_spawn"]
|
|
|
|
|
|
@pytest.mark.parametrize("func_name", ACT_DECIDING_FUNCS)
|
|
def test_act_decision_requires_a_margin(func_name):
|
|
from town.town_manager import TownManager
|
|
|
|
src = inspect.getsource(getattr(TownManager, func_name))
|
|
assert "_ACT_DETECT_MARGIN" in src, (
|
|
f"{func_name} decides an act without requiring a margin over the best "
|
|
"marker from another act — a marginal winner is not evidence"
|
|
)
|
|
assert "TOWN_MARKERS_ACT" in src, f"{func_name} does not separate own-act from rival markers"
|
|
|
|
|
|
@pytest.mark.parametrize("func_name", ACT_DECIDING_FUNCS)
|
|
def test_act_decision_scores_both_sides_on_one_image(func_name):
|
|
"""Comparing scores from two different grabs is not a comparison.
|
|
|
|
The character can move between them, so the margin becomes meaningless.
|
|
"""
|
|
from town.town_manager import TownManager
|
|
|
|
src = inspect.getsource(getattr(TownManager, func_name))
|
|
grabs = src.count("grab(force_new=True)") + src.count("grab()")
|
|
assert grabs <= 1, (
|
|
f"{func_name} grabs {grabs} images; own-act and rival scores must come "
|
|
"from the same frame to be comparable"
|
|
)
|
|
|
|
|
|
def test_margin_is_meaningfully_large():
|
|
from town.town_manager import TownManager
|
|
|
|
assert TownManager._ACT_DETECT_MARGIN >= 0.03, "margin too small to separate a phantom from a real marker"
|
|
|
|
|
|
def test_ambiguous_detection_returns_none_not_a_guess():
|
|
"""Refusing to answer is the safe outcome.
|
|
|
|
Callers treat None as "keep the assumed act", which makes go_to_act actually
|
|
travel rather than wrongly early-returning. A confident wrong answer skips
|
|
the recovery path entirely.
|
|
"""
|
|
from town.town_manager import TownManager
|
|
|
|
src = inspect.getsource(TownManager.detect_current_act)
|
|
assert "refusing to guess" in src
|
|
assert src.rstrip().endswith("return None")
|