Auto-assignment (no real id known: a manual add, or an accepted
proposal with no confident marker-id read) must stay visually
distinct from a genuinely detected id, or a made-up number could
collide with or be mistaken for a real one. Reverts the previous
commit's switch to numeric auto-assignment (_next_free_numeric_id) --
that was wrong, caught by the user immediately. _next_free_id
(letters, rolling over to "AA"/"AB"/... past 26) is back as the
fallback, still scoped per type (that part of the previous change was
correct and stays). Plain numbers are reserved for an id
_accept_proposal is actually confident was read off the marker itself
(Proposal.detected_id), passed straight through and never touching
auto-assignment.
Also fixes detected_id's own collision pre-check in _accept_proposal,
which wasn't scoped per type either -- same bug as the Change ID
popover fix, just in a second place: a detected id could get
needlessly discarded because an unrelated type already used that
number, not because of a real collision.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-assignment (Board.add_target/add_ally with no explicit id_) now
scopes its 1/2/3... sequence per TYPE within each group, not one
sequence shared across every type in the group -- Tank#1/Infantry#1
rather than Tank#A/Infantry#B, matching the game's own numbering.
Reverses the type-scoping half of an earlier fix in this file (see
TODO.md's "Allies and enemies seem to share indices" entry) per
explicit user direction; the targets-vs-allies namespace split that
fix also made is untouched, still correct. _next_free_id (letters,
rolling over to "AA" past 26) is replaced by _next_free_numeric_id --
a plain counter can't run out the way a fixed alphabet could, so
there's no equivalent rollover concern. test_models.py updated to
match (one test asserts the opposite of before, renamed accordingly).
detected_id (map_vision.read_marker_id) was being logged but never
shown anywhere a human could actually check it against the
screenshot before now: added to the proposal popover's heading and
the pending-proposal's own on-map label.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
read_marker_id (map_vision.py) reads each marker's own small "#<N>" id
label via template correlation, same approach as read_cell_label and
for the same documented reason (this text sits over the same aerial-
photo backdrop that defeated detection-based approaches for grid
labels). Wired end-to-end: find_markers -> Proposal.detected_id ->
save_marker_ground_truth's JSON. Reads against ScreenshotImport's
full_image when available, since the id text is tiny. Crop region and
threshold are a single-screenshot calibration, not yet validated
against real ground truth (documented as such).
Also switches save_marker_ground_truth/save_grid_correction to use
full_image over the WORK_W-downscaled image, so a human reviewing a
capture can actually read the small id text well enough to judge it.
Logs unit_score/unit_margin on every Proposal too (previously only
pass/fail `unit` was recorded), and measured current type-detection
reliability against the 6 existing ground-truth captures: 0/72 (0%)
accepted proposals had any confident detected_unit at all, not just
wrong guesses -- classify_marker never clears its own confidence floor
against real screenshots. Findings and next steps in TODO.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Accept as..." (proposal type-picker) and "Change type" (entity-edit)
opened to a visibly empty/unchanged popover with no traceback: swapping
an already-open Popover's child and re-popup()ing it reported the right
size internally but the compositor never repainted the reused surface.
Confirmed live via temporary debug instrumentation, not guessed. Fixed
by popping the old popover down and opening a genuinely new one at the
same anchor point instead of resizing in place.
Also adds a Target.underground_tier (1-3) marker: a "Mark underground"
entry in the entity-edit popover, rendered as the game's own Armor-tier
additive badge stacked directly on the unit icon. The badge is
scaled/positioned off its real opaque content (PIL bbox), not its PNG
canvas, since the additive art carries a lot of off-center transparent
padding; and overlaps down into the icon by a fixed pixel amount, since
both shapes taper to a point at the seam and exact bbox-touching still
read as a visible gap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
- 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>