FEnigma/TODO.md
Dominik Roth 23615a8c92 Fix id-namespace regression, add StrikeRequest type, kill full-panel
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>
2026-08-11 20:48:57 +02:00

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) raises StopIteration the instant all 26 are taken, silently killing whatever button click triggered add_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 public icons.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 printed obj.type.value directly instead, so an Ally with the ad-hoc TargetType.ENEMY still showed "Enemy" everywhere except the picker itself. Fixed in app.py (_display_name, and the three spots using it).

  • Reordering firing commands lags UI hard. FiringPanel._reorder() was calling self.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 local self.refresh() instead.

  • "Always show geo" doesn't reliably work / blast radius should stay shown too. GridCanvas._draw_geo_overlays()'s candidate list was reference_points + targets only — Allies have a show_geo_desc pin in the UI and can carry OCR'd clues too, but were never drawn. Added. _draw_blast_radius() only ever looked at self.selected, ignoring show_geo_desc entirely, 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 except self.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.pysave_map_read_failure() writes the PNG + the solver's rejection reason under $XDG_DATA_HOME/fenigma/debug_captures/failures/, wired into app.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 accepted GridSolution isn'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_all found 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 of board.targets/board.allies taken when the grid is confirmed, Target/Ally are identity-hashable so these are plain sets of the live objects) let app.py tell "added while this screenshot was up" apart from "was already on the board". Proposal also now records confirmed_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 via debug_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: StopIteration from Board.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's classify_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 the debug_capture failure/maybe_map screenshots above to develop against.