Working harvesting detection, working manufacturing detection.

This commit is contained in:
2026-08-12 13:54:40 -06:00
parent 2181655801
commit d7755df2e8
112 changed files with 1105 additions and 177 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

+70
View File
@@ -0,0 +1,70 @@
import shutil
from pathlib import Path
import pytest
import analyze
ALL_IMAGES = (
sorted(Path("tests/images/harvesting").glob("*.png"))
+ sorted(Path("tests/images/not_harvesting").glob("*.png"))
)
assert len(ALL_IMAGES) > 0, "no images found under tests/images"
# Inventory capacity was observed to change as the player collects items.
# Frames not listed here are expected to read 106.
INVENTORY_EXPECTED = {
"frame_00000.png": 110,
"frame_00001.png": 110,
"frame_00002.png": 108,
"frame_00003.png": 108,
}
OCR_AVAILABLE = analyze.pytesseract is not None and shutil.which("tesseract") is not None
pytestmark = pytest.mark.skipif(
not OCR_AVAILABLE,
reason="pytesseract or tesseract-ocr not installed",
)
@pytest.fixture(scope="session")
def bar_values():
values = {}
for path in ALL_IMAGES:
image = analyze.load_image(path)
assert image is not None, f"cannot load {path}"
values[path.name] = analyze.read_bar_values(image)
return values
def test_mana_is_32(bar_values):
for name, values in bar_values.items():
assert values["mana"] == 32, name
def test_food_is_39(bar_values):
for name, values in bar_values.items():
assert values["food"] == 39, name
def test_health_is_40(bar_values):
for name, values in bar_values.items():
assert values["health"] == 40, name
def test_action_points_are_140(bar_values):
for name, values in bar_values.items():
assert values["action_points"] == 140, name
def test_inventory_matches_expected(bar_values):
for name, values in bar_values.items():
expected = INVENTORY_EXPECTED.get(name, 106)
assert values["inventory"] == expected, name
def test_every_bar_has_a_value(bar_values):
for name, values in bar_values.items():
assert all(v is not None for v in values.values()), name
+10
View File
@@ -0,0 +1,10 @@
from pathlib import Path
import analyze
CORRUPT = sorted(Path("tests/images/corrupt").glob("*.png"))
def test_corrupt_images_are_skipped_gracefully():
for path in CORRUPT:
assert analyze.load_image(path) is None, path
+23
View File
@@ -0,0 +1,23 @@
from pathlib import Path
import analyze
HARVESTING = sorted(Path("tests/images/harvesting").glob("*.png"))
NOT_HARVESTING = sorted(Path("tests/images/not_harvesting").glob("*.png"))
assert len(HARVESTING) > 0, "no images found in tests/images/harvesting"
assert len(NOT_HARVESTING) > 0, "no images found in tests/images/not_harvesting"
def test_harvesting_frames_report_harvesting():
for path in HARVESTING:
image = analyze.load_image(path)
assert image is not None, f"cannot load {path}"
assert analyze.is_harvesting(image)["is_harvesting"] is True, path
def test_not_harvesting_frames_report_not_harvesting():
for path in NOT_HARVESTING:
image = analyze.load_image(path)
assert image is not None, f"cannot load {path}"
assert analyze.is_harvesting(image)["is_harvesting"] is False, path
+139
View File
@@ -0,0 +1,139 @@
import shutil
from pathlib import Path
import pytest
import analyze
MFG_IMAGES = sorted(Path("tests/images/manufacturing").glob("*/*.png"))
INV_IMAGES = sorted(Path("tests/images/inventory").glob("*.png"))
assert len(MFG_IMAGES) > 0, "no images found under tests/images/manufacturing"
assert len(INV_IMAGES) > 0, "no images found under tests/images/inventory"
OCR_AVAILABLE = analyze.pytesseract is not None and shutil.which("tesseract") is not None
pytestmark = pytest.mark.skipif(
not OCR_AVAILABLE,
reason="pytesseract or tesseract-ocr not installed",
)
# Pinned Manufacturing title-bar positions per fixture frame, measured from the
# real captures (the window was dragged to 5 different places across sessions).
MFG_POSITION = {
"snapshots_f00000.png": (488, 480),
"snapshots_f00002.png": (488, 480),
"snapshots_f00003.png": (488, 480),
"snapshots_f00006.png": (376, 262),
"snapshots_f00007.png": (1330, 194),
"snapshots_f00010.png": (1005, 565),
"snapshots_f00013.png": (281, 676),
"snapshots_f00015.png": (281, 676),
"snapshots2_f00000.png": (1104, 327),
"snapshots2_f00001.png": (1104, 327),
"snapshots2_f00002.png": (1104, 327),
"snapshots2_f00003.png": (1104, 327),
"snapshots2_f00007.png": (1104, 327),
"snapshots2_f00013.png": (1104, 327),
"snapshots2_f00033.png": (1104, 337),
"harvest_frame_00000.png": (173, 771),
}
# The Inventory window never moved across the capture sessions; every fixture
# frame pins the same title-bar position.
INV_POSITION = (1629, 686)
# find_windows recovery is a full-frame OCR scan (~7s/frame) so it is only
# exercised on a representative subset. "inventory": None for the harvesting
# frame is a known miss: the full-frame scan does not read its Inventory title.
RECOVERY_CASES = {
"snapshots_f00000.png": ((488, 480), (1629, 686)),
"snapshots_f00013.png": ((281, 676), (1629, 686)),
"snapshots2_f00003.png": ((1104, 327), (1629, 686)),
"snapshots2_f00013.png": ((1104, 327), (1629, 686)),
"snapshots2_f00033.png": ((1104, 337), (1629, 686)),
"harvest_frame_00000.png": ((173, 771), None),
}
_TOLERANCE = 15
def _load(path):
image = analyze.load_image(path)
assert image is not None, f"cannot load {path}"
return image
def _find_fixture(name):
for root in list(Path("tests/images/manufacturing").glob("*")) + [Path("tests/images/inventory")]:
candidate = root / name
if candidate.exists():
return candidate
raise AssertionError(f"no fixture file named {name}")
def _mfg_positions():
assert set(MFG_POSITION) == {p.name for p in MFG_IMAGES}, "MFG_POSITION does not match fixture files"
for path in MFG_IMAGES:
yield path, MFG_POSITION[path.name]
def test_manufacturing_title_is_verified_at_pinned_position():
for path, pos in _mfg_positions():
image = _load(path)
assert analyze.verify_window(image, pos[0], pos[1], "manufacturing") is True, path
def test_inventory_title_is_verified_at_pinned_position():
for path in INV_IMAGES:
image = _load(path)
assert analyze.verify_window(image, INV_POSITION[0], INV_POSITION[1], "inventory") is True, path
def test_manufacturing_message_category_matches_folder():
for path in MFG_IMAGES:
image = _load(path)
x, y = MFG_POSITION[path.name]
message = analyze.read_manufacturing_message(image, x, y)
assert message is not None, path
assert message["category"] == path.parent.name, path
def test_find_windows_recovers_pinned_positions():
for name, (exp_mfg, exp_inv) in RECOVERY_CASES.items():
image = _load(_find_fixture(name))
found = analyze.find_windows(image)
if exp_mfg is None:
assert found["manufacturing"] is None, name
else:
pos = found["manufacturing"]
assert pos is not None, f"manufacturing not recovered: {name}"
assert abs(pos[0] - exp_mfg[0]) <= _TOLERANCE, name
assert abs(pos[1] - exp_mfg[1]) <= _TOLERANCE, name
if exp_inv is None:
assert found["inventory"] is None, name
else:
pos = found["inventory"]
assert pos is not None, f"inventory not recovered: {name}"
assert abs(pos[0] - exp_inv[0]) <= _TOLERANCE, name
assert abs(pos[1] - exp_inv[1]) <= _TOLERANCE, name
def test_classify_message_maps_real_strings():
# Ground-truth texts from the captured frames, one per category.
assert analyze.classify_message("you stopped working.", "red") == "stopped"
assert analyze.classify_message("you started working...", "green") == "working"
assert analyze.classify_message("you successfully created 1 thread", "green") == "success"
assert analyze.classify_message("you failed to create a[n] thread", "red") == "failed"
assert (
analyze.classify_message(
"you failed to create a[n] potion of minor healing, and lost the ingredients", "red"
)
== "failed_lost"
)
assert analyze.classify_message(
"you are hungry, you can't manufacture anything. eat until your food level is over 0", "red"
) == "hungry"
assert analyze.classify_message("you are overloaded, you can't carry 1 thread!", "red") == "overloaded"
assert analyze.classify_message("", "green") == "none"
assert analyze.classify_message("garbled text", "red") == "unknown"