- Board.clear() now also drops allies; the "clear board?" guard checks
allies too. New Board.clear_units() + Clear button right-click menu
("clear enemies, units & flights", keeps Nest/spotters/RPs).
- An Ally with the ad-hoc TargetType.ENEMY showed "Enemy" on the map
popover/toast instead of "Ally" (icons.target_type_label already had
the fix for the picker, now reused everywhere else via app.py's
_display_name).
- Firing panel drag-reorder no longer triggers a full app refresh
(solver + dedupe + map redraw) on every drop, just a local rebuild.
- "Always show geo" didn't draw for Allies (missing from the overlay
candidate list); blast radius only respected selection, not the
show_geo_desc pin.
- ocr.py: added a second fire-support-request grammar ("Infantry#N
taking fire ... Requesting X Shell on our position at <coord> before
<time>", plus a bearing/distance-from-position variant), distinct
from the existing Marine Garrison one.
- New debug_capture.py: saves screenshots (+ metadata) the app handled
badly, for later tuning of map_vision/ocr against real failures:
map-read errors, user grid corrections (paired with the auto-detected
grid), screenshots that read as text but may have been a map, and
marker-detection ground truth (every proposal's accept/reject verdict
plus units added with no matching proposal) captured whenever a
screenshot stops being the active one.
- README: Known issues section (map screenshot reading, grid + unit
detection, is unreliable and fails often).
- 14 new tests (tests/test_models.py, tests/test_debug_capture.py, +
additions to tests/test_ocr.py), 38/38 passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two related changes:
1. TargetType.HOSTILE_ARTILLERY/HOSTILE_TANK renamed to ARTILLERY/TANK,
dropping the baked-in hostility assumption from the type name
itself (a type describes the unit kind now, not an allegiance).
Migration entries added for both, plus the already-existing
COASTAL_BATTERY one, so old save files still load.
2. A friendly contact ('FriendlyTank#1:', detected by stripping a
leading 'Friendly'/'Hostile' word off the type word before matching
it, see ocr.py's _resolve_target_type()) is NOT a Target with a flag
flipped, it's tracked as a new, entirely separate Ally
(Board.allies), with its own id namespace: an ally Tank#1 and a
hostile Target Tank#1 are two unrelated things that happen to share
an id, not a collision (verified directly, see the rendered
screenshot both coexisting). Ally intentionally has none of Target's
firing-relevant fields (shell/powder_charges/assignment/alive),
allies are never fired on. 'Hostile' and no prefix at all both mean
a regular (non-ally) Target, not-ally is the default.
Wired through: Board.add_ally/remove_ally, placed_entities_all()/
ambiguous_entities_all() (new 'ally' category, cyan on the map,
distinct from every other category's color), solver.resolve_board()
(allies' own clues resolve too), find_by_name() (an ally can be a
clue reference target), save/load round-trip, a new 'Allies' header
popover mirroring Targets' (position/hide/geo-overlay/remove, no
shell/charge/alive controls), and ParsedInfo.allies as a same-shaped
but separate dict from ParsedInfo.targets, merged by a new
_merge_allies() alongside _merge_targets() in _merge_all().
Verified: full test suite (added a dedicated OCR test for the Friendly/
Hostile/bare-prefix routing), a GTK smoke test round-tripping an ally
through save/load and the popover build, and a rendered screenshot
showing an ally Tank#1 and a hostile Target Tank#1 both on the map at
once with the same id, distinct colors, no collision.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same pattern already established for AMMO_CACHE -> SUPPLY_CACHE: a
'Coastal Battery' is just a heavy fixed-emplacement HostileArtillery
under a different name in the game's own text, not a meaningfully
different unit type worth its own enum member. Removed the
TargetType.COASTAL_BATTERY member, added 'CoastalBattery' to
ocr.py's _TYPE_WORD_ALIASES (so 'Coastal Battery#2:' still parses,
now as a HostileArtillery), and a models.py migration entry so any
save file written before this change still loads correctly.
Verified the migration directly (COASTAL_BATTERY -> HOSTILE_ARTILLERY,
existing AMMO_CACHE -> SUPPLY_CACHE unaffected) and updated the one
test that asserted the old type.
No test suite existed before this, which is exactly how a real
regression (the bold-span coordinate-squashing bug, and the
'Type#id:'/'<ref>: <value>' header collision, both from this session)
went unnoticed until manually re-triggered. One test per format,
cross-referenced against the full commit history so nothing already
shipped gets silently dropped by a future change:
tests/test_ocr.py: standard blocks, the calibration target line,
destruction reports (digit and letter id), train-arrival intel,
ad-hoc Enemy installations (+ their destroyed reports), Listening
Post/Coastal Battery, Marine Garrison fire-support requests, multi-
word RP names, bare-name-header targets, the bold-span coordinate-
squashing regression specifically, forward-observer report
triangulation, the '<ref>: <value>' clue grammar (+ its header-
collision regression specifically), 16-point compass tolerance, and
grid-only coordinates.
tests/test_solver.py: direct bearing+distance resolution, two-bearing
triangulation, genuine two-distance ambiguity, the nested-circles
compromise-point fallback, toleranced bearings never being used to
triangulate, and manual coord overrides clearing a stale note.
Runs via ============================= test session starts ==============================
platform linux -- Python 3.14.6, pytest-8.4.2, pluggy-1.6.0
rootdir: /home/dodox/Projects/FeNigma
configfile: pytest.ini
plugins: anyio-4.13.0
collected 21 items
tests/test_ocr.py ............... [ 71%]
tests/test_solver.py ...... [100%]
============================== 21 passed in 0.72s ============================== (pythonpath configured in pytest.ini), dev-only
dependency in requirements-dev.txt so the app itself stays
dependency-light. Documented in the README.