rerender on assign/alive/shell, add Windows build tooling - Board.add_target/add_ally's id auto-assignment used a bare next(c for c in string.ascii_uppercase if c not in used), which raises StopIteration once 26 entities of a group exist -- a real crash confirmed via a live traceback, and a direct regression from moving that sequence from per-type to per-group. This was the actual cause of "Accept as"/"Accept all" silently doing nothing. Fixed with _next_free_id(), which rolls over to two-letter ids instead of raising. - New TargetType.STRIKE_REQUEST: the bearing/distance-offset "taking fire" fire-support request (see the earlier two-entity split) now creates this instead of reusing STRIKE, so a radioed-in request is never confused with a strike the player placed themselves. Same crosshair icon, excluded from type pickers/dedupe like STRIKE. - The "Accept as..." popover on a detected map marker now uses the same icon grid the entity-edit "Change type" popover does (was a plain unfiltered text list of every TargetType, which also wrongly offered STRIKE/STRIKE_REQUEST as pickable). - Firing panel: _cycle_assignment/_toggle_alive/_pick_shell no longer route through app.py's full solver+dedupe+canvas+panel refresh -- none of the three can affect the solver or dedupe, and none change which cards exist or their order (except _toggle_alive in hide/sort_later mode). New FiringPanel._rebuild_one() rebuilds just the one changed card; on_visual_change is a new, lighter callback (just a map redraw) for the two of these three that actually affect it. This was a real, confirmed lag source with many units on the board: every click on any of these was previously rebuilding every card of every target. - Map right-click entity menu: added "Mark destroyed"/"Mark alive", reusing the same cheap-refresh path (new FiringPanel.refresh_after_alive_change). - packaging/windows/: a from-scratch (untested against a real boot) MSYS2 + WiX .msi build pipeline for Windows, driven from Linux via dockur/windows (KVM-in-container), no Windows machine or GitHub required. See its own README for status/caveats. - New/updated tests: id-namespace sharing + the 26-entity overflow regression (tests/test_models.py), StrikeRequest split (tests/test_ocr.py), warp_to_map's img_scale param (tests/test_map_vision_warp.py). 44/44 passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
60 lines
2.5 KiB
Python
60 lines
2.5 KiB
Python
"""warp_to_map's img_scale param: a caller can hand it a differently-sized
|
|
image than the one `sol` was actually solved against (see
|
|
map_vision.load_full_res / ScreenshotImport.full_image), scaled to
|
|
compensate. This checks that compensation is correct, without needing a
|
|
real fixture screenshot or the (slow) line-detection/solve pipeline --
|
|
just a synthetic image and a stub solution with a predictable transform.
|
|
"""
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from fenigma import map_vision
|
|
|
|
|
|
class _IdentitySolution:
|
|
"""H and lattice_to_grid() both identity: warp_to_map's transform then
|
|
reduces to just grid_to_map, so the output is a directly px_per_km-
|
|
scaled (and row-flipped, per warp_to_map's own comment) copy of
|
|
whatever region of the input `warp_to_map` reads as "grid space"."""
|
|
H = np.eye(3)
|
|
|
|
def lattice_to_grid(self):
|
|
return np.eye(3)
|
|
|
|
|
|
def test_img_scale_compensates_for_a_bigger_source_image():
|
|
# A small solid-color source, plus a 2x upscaled copy of it -- same
|
|
# content, different pixel dimensions.
|
|
small = np.zeros((20, 20, 3), dtype=np.uint8)
|
|
small[:, :] = (10, 20, 30) # BGR
|
|
big = np.zeros((40, 40, 3), dtype=np.uint8)
|
|
big[:, :] = (10, 20, 30)
|
|
|
|
sol = _IdentitySolution()
|
|
out_small, ppk_small = map_vision.warp_to_map(small, sol, px_per_km=1)
|
|
out_big, ppk_big = map_vision.warp_to_map(big, sol, px_per_km=1, img_scale=2.0)
|
|
|
|
assert ppk_small == ppk_big == 1
|
|
assert out_small.shape == out_big.shape # output is always MAP_KM_W/H * px_per_km, regardless of source size
|
|
# Same solid color warped in (opaque region only -- compare where both
|
|
# actually painted something, alpha channel nonzero).
|
|
painted = (out_small[:, :, 3] > 0) & (out_big[:, :, 3] > 0)
|
|
assert painted.any()
|
|
np.testing.assert_array_equal(out_small[painted][:, :3], out_big[painted][:, :3])
|
|
|
|
|
|
def test_default_img_scale_is_unchanged_behavior():
|
|
"""img_scale's default (1.0) must reproduce pre-existing behavior
|
|
exactly -- every other warp_to_map call site doesn't pass it."""
|
|
img = np.zeros((20, 20, 3), dtype=np.uint8)
|
|
img[:, :] = (1, 2, 3)
|
|
sol = _IdentitySolution()
|
|
out_default, _ = map_vision.warp_to_map(img, sol, px_per_km=1)
|
|
out_explicit, _ = map_vision.warp_to_map(img, sol, px_per_km=1, img_scale=1.0)
|
|
np.testing.assert_array_equal(out_default, out_explicit)
|
|
|
|
|
|
def test_load_full_res_raises_like_load_on_a_bad_path(tmp_path):
|
|
with pytest.raises(ValueError):
|
|
map_vision.load_full_res(tmp_path / "does-not-exist.png")
|