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>
8.2 KiB
Bug Backlog (from user report, 2026-08-11)
Status legend: [x] fixed+tested, [~] partially addressed, [ ] open/needs input
-
Allies and enemies seem to share indices. First pass on this was wrong: I only checked that targets and allies are separate id namespaces (they are, always were) and stopped there. The actual bug was one level down:
Board.add_target/add_ally's auto-id assignment (used = {t.id for t in self.targets if t.type == type_}) was scoped per type, not per group — a Tank and an Infantry auto-added back to back both got id "A", each type getting its own independent A/B/C... sequence instead of sharing one across the whole group. Fixed: the id namespace split is targets-vs-allies ONLY, type never subdivides it further. New regression test (test_auto_id_is_shared_across_types_within_targets_and_within_allies). -
Regression FROM the fix above, caught via a real traceback: sharing one A/B/C... sequence across a whole group (instead of per-type) made it much easier to actually run out of the 26 letters --
next(c for c in string.ascii_uppercase if c not in used)raisesStopIterationthe instant all 26 are taken, silently killing whatever button click triggeredadd_target/add_ally(this is what "Accept as"/"Accept all" doing nothing turned out to be, see below). Fixed with_next_free_id(): rolls over to two-letter ids ("AA", "AB", ...) instead of raising, can't run out. New test (test_auto_id_survives_past_26_entities_in_one_group). -
Ally type 'ally' is called Enemy on map title.
icons._target_type_label(now publicicons.target_type_label) already special-cased this for the type picker, but the map's right-click popover heading, "Change type (...)" button, and toast all printedobj.type.valuedirectly instead, so an Ally with the ad-hoc TargetType.ENEMY still showed "Enemy" everywhere except the picker itself. Fixed inapp.py(_display_name, and the three spots using it). -
Reordering firing commands lags UI hard.
FiringPanel._reorder()was callingself.on_change()— app.py's full-app refresh (re-solve every target's clue graph, dedupe, redraw the map, THEN rebuild the panel) — on every single drag-drop, even though reordering touches no location/clue/coord state at all. Now calls a localself.refresh()instead. -
"Always show geo" doesn't reliably work / blast radius should stay shown too.
GridCanvas._draw_geo_overlays()'s candidate list wasreference_points + targetsonly — Allies have ashow_geo_descpin in the UI and can carry OCR'd clues too, but were never drawn. Added._draw_blast_radius()only ever looked atself.selected, ignoringshow_geo_descentirely, so pinning it and then selecting/ deselecting something else made it vanish; now iterates every selected-or-pinned target. -
Clearing the board doesn't clear allies.
Board.clear()cleared everything exceptself.allies. Fixed, plus the "clear board?" confirm-dialog's early-return guard (which skipped the whole action if only allies were on the board) now checks allies too. -
Allow right-click on Clear button: clear all enemies/units/flights, keep spotters/RPs/nest. New
Board.clear_units()+ a right-click popover on the header's Clear button wired to it. -
On map-reading error: save a screenshot locally to adapt the algo. New
debug_capture.py—save_map_read_failure()writes the PNG + the solver's rejection reason under$XDG_DATA_HOME/fenigma/debug_captures/failures/, wired intoapp.py's_start_map_import. -
When the user corrects the grid, store screenshot + ground truth too.
debug_capture.save_grid_correction(), wired into_accept_grid: fires only when the acceptedGridSolutionisn't the one auto-solve produced (the user actually dragged a handle in GridFixDialog), saves both solutions under.../debug_captures/corrections/. -
Many map screenshots seem to get read as text; if nothing relevant is found, also store the image to check whether it was actually a map.
debug_capture.save_maybe_map(), wired into_ocr_png: fires when a screenshot (not a plain-text paste) fell through to the OCR/text path and_merge_allfound nothing at all. Saved under.../debug_captures/maybe_map/. -
Unable to parse 3 given chat messages (Infantry "taking fire" fire- support requests). A different grammar from the existing Marine Garrison fire-support request: reversed shell word order ("Requesting X Shell" vs "X Shells requested"), a bare "before/by
-
Follow-up bug in the above: the bearing/distance-offset variant names TWO different places (the reporting unit's own position, and a separate fire point offset from it), but only produced one Target entity, sitting at the offset point but still labeled with the unit's own type/id (e.g. "Infantry#11" at a spot no infantry is actually at). Math itself was right; the single-entity shape wasn't. Now produces two entries: the original (Infantry#N etc.) keeps its own reported position with no shell/deadline, and a new synthetic
Strike#<TypeWord><id>entry (e.g.Strike#Infantry11) carries the shell/deadline at the computed offset coord. 2 more regression tests. -
When the user deletes/replaces the map screenshot, capture whatever units they confirmed as ground truth for it.
ScreenshotImport.baseline_targets/baseline_allies(a snapshot ofboard.targets/board.alliestaken when the grid is confirmed,Target/Allyare identity-hashable so these are plain sets of the live objects) letapp.pytell "added while this screenshot was up" apart from "was already on the board".Proposalalso now recordsconfirmed_type(what the user actually accepted it as, which can differ from the detector's own guess via "Accept as..."). All of it -- every proposal's accept/reject/undecided verdict, plus every target/ally added with no matching proposal at all (a manual add or an OCR-text merge run alongside the screenshot) -- is saved viadebug_capture.save_marker_ground_truth()under.../debug_captures/marker_ground_truth/. Wired into all three places a screenshot stops being "the active one": explicit drop, a new screenshot pasted straight over it, and window close.
Resolved via a real traceback (not guessed)
- "Accept as" / "Accept all" on proposed targets doing nothing.
A real traceback from the running app nailed it:
StopIterationfromBoard.add_ally's id auto-assignment once 26 allies existed already (see the id-namespace regression entry above) — every accept attempt after that silently died before the ally/target ever got added, popover already closed by the time it happened. Fixed there; not a separate bug.
Needs more scope / your input before I keep going
- Enemy type detection needs to be more robust; read the entity id
label so dedup is reliable; detect death from the log.
All three are real computer-vision/OCR feature work (better marker
classification in
map_vision.py'sclassify_marker, a new OCR pass reading each marker's id label off the map screenshot, and a "# Destroyed" log-scan tied into a dedup key that includes that read id) rather than bugs with a small fix. Worth its own pass once there's a batch of thedebug_capturefailure/maybe_map screenshots above to develop against.