light.exe's ICE80 validation rejected essentially every harvested file
("This 32BitComponent ... uses 64BitDirectory") on the first build that
got far enough to reach it -- heat.exe's harvest command had no
-platform x64, so every component defaulted to 32-bit, while
product.wxs's own INSTALLFOLDER is correctly under
ProgramFiles64Folder (a 64-bit mingw64 toolchain is what's actually
being packaged). Real, on-disk mismatch, not a transient VM issue.
Also confirmed the earlier light.exe "timeout" wasn't a real bug either
-- a longer-budget retry finished linking fine in a few more minutes,
it was just slow, not hung.
Not yet re-verified against a live build (found right as this
session's VM time was already heavily spent) -- logged in TODO.md as
the next thing to confirm.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The raw request text (both shells, in order) is already preserved and
shown to the player via the coord dialog's description view
(Location.desc_raw) -- not machine-parsed into a second structured
shell field, but not actually lost either. Lowers the urgency/changes
the framing before deciding whether to build real sequence support.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Turned out to be one bug, not two: "Answer by <time>" phrasing was
already covered by _TAKING_FIRE_TIME_RE ('before|by <time>'). The real
bug was the last-resort bare-"<Name>:" header fallback matching a
same-message "Important:" follow-up line as a brand new named entity
(nothing excluded common prose lead-ins), creating a bogus
Target#Important that stole the deadline into its own requested_time
instead of the real report's. Fixed with a blocklist on that fallback
rule (important/note/warning/attention/caution/alert/reminder/priority).
New regression test, 55 total passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
OCR (src/fenigma/ocr.py):
- A "taking fire" report's reporting unit was added as a hostile Target,
not a friendly Ally -- no "Friendly"/"Hostile" prefix word exists in
that grammar for the usual inference to key off of, so it silently
defaulted to not-ally. Fixed with an explicit force_ally override
(_TAKING_FIRE_RE), and the shell/deadline (which an Ally tuple has no
fields for) now always splits into a synthetic StrikeRequest target at
the reporting position, even for the no-offset "on our position" case
that previously kept them on the entity itself.
- "Enemy <Type>#<id> Destroyed" kill-feed lines were silently dropping
for shorter/less distinctive type words (e.g. "Enemy Field Gun#1") --
_ALLY_PREFIX_RE only ever stripped "Friendly"/"Hostile", never
"Enemy", so the whole "EnemyFieldGun" token got alias/fuzzy-matched
against "Artillery" and missed by a mile. Longer type words
("Enemy Mechanized Infantry#2") only ever worked by fuzzy-match
accident. Now strips "Enemy" too (lookahead guards a BARE "Enemy#N"
report, which IS TargetType.ENEMY itself, from being stripped to an
empty, unresolvable string).
- "Field Gun" added to _TYPE_WORD_ALIASES as plain Artillery under
another name (confirmed by the user), not a missing unit type.
7 new/updated regression tests, 54 total passing.
Windows build (packaging/windows/): three real bugs found and fixed by
actually booting and driving the build VM live (VNC), not just guessing
from the README's "UNTESTED end to end" note:
- install.bat's MSYS2/WiX provisioning previously left NOTHING behind
once C:\OEM stopped existing (a 2-day-old BUILD_REQUEST sat unclaimed
the whole time) -- the build.bat/watch_build.bat persistence fix
(C:\FenigmaBuild instead of C:\OEM) is real and now confirmed live:
after a full container restart, the watcher auto-starts on login and
picks up a pending request with zero manual intervention.
- pip install pytesseract needs --break-system-packages (MSYS2's
mingw64 Python enforces PEP 668).
- mingw-w64-x86_64-opencv is the C++ library only; the actual Python
bindings are the separate mingw-w64-x86_64-python-opencv package,
never in install.bat's dependency list.
With all three, import fenigma.app succeeds and a real build attempt
gets through source copy, sanity check, dist-tree assembly, and WiX
harvest+compile -- further than this pipeline has ever gotten. Full
findings, including the still-open light.exe timeout and the OCR
multi-shell/deadline-phrasing/phantom-header gaps found along the way,
logged in TODO.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>