Working harvesting detection, working manufacturing detection.
|
After Width: | Height: | Size: 768 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||