Fix taking-fire ally misclassification; Windows build fixes; Field Gun
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>
This commit is contained in:
+77
-18
@@ -52,6 +52,43 @@ def test_destroyed_reports_digit_and_letter_id():
|
||||
assert info.destroyed == {(TargetType.SUPPLY_CACHE, "2"), (TargetType.TANK, "3")}
|
||||
|
||||
|
||||
def test_destroyed_report_strips_a_leading_enemy_prefix():
|
||||
"""A real kill-feed paste with an "Enemy <Type>#<id> Destroyed" shape
|
||||
(squashed by squash_multiword_ids to "EnemyMechanizedInfantry#1"
|
||||
before this ever runs) was silently dropping every single-word type
|
||||
("Enemy Infantry#11 Destroyed") -- the un-stripped "Enemy" prefix
|
||||
only accidentally fuzzy-matched for longer/more distinctive type
|
||||
words (Mechanized Infantry), not shorter/more different ones (Field
|
||||
Gun -- see test_field_gun_is_an_artillery_alias). _ALLY_PREFIX_RE now
|
||||
strips "Enemy" the same as "Hostile"."""
|
||||
text = ("Enemy Mechanized Infantry#1 Destroyed, +5 Requisition.\n"
|
||||
"Enemy Infantry#11 Destroyed, +5 Requisition.")
|
||||
info = ocr.parse_text(text)
|
||||
assert info.destroyed == {(TargetType.INFANTRY_MECHANIZED, "1"), (TargetType.INFANTRY, "11")}
|
||||
|
||||
|
||||
def test_bare_enemy_destroyed_report_is_still_target_type_enemy():
|
||||
"""The lookahead in _ALLY_PREFIX_RE (only strip "Enemy" when there's
|
||||
something AFTER it) matters here specifically: a BARE "Enemy#N" is
|
||||
TargetType.ENEMY itself (its own value IS "Enemy") -- stripping the
|
||||
prefix unconditionally would leave an empty type_word and silently
|
||||
drop every ad-hoc "Enemy#N Destroyed" report instead."""
|
||||
text = "Enemy#7 Destroyed, +5 Requisition."
|
||||
info = ocr.parse_text(text)
|
||||
assert (TargetType.ENEMY, "7") in info.destroyed
|
||||
|
||||
|
||||
def test_field_gun_is_an_artillery_alias():
|
||||
"""The game calls plain Artillery "Field Gun" in at least this kill-
|
||||
feed message -- confirmed by the user against a real "Enemy Field
|
||||
Gun#1 Destroyed" line that was otherwise silently dropping (no
|
||||
TargetType.FIELD_GUN exists, nor should one -- see _TYPE_WORD_ALIASES'
|
||||
own comment, same treatment as AmmoCache/CoastalBattery)."""
|
||||
text = "Priority target Enemy Field Gun#1 Destroyed, +50 Requisition."
|
||||
info = ocr.parse_text(text)
|
||||
assert (TargetType.ARTILLERY, "1") in info.destroyed
|
||||
|
||||
|
||||
def test_train_arrival_intel():
|
||||
text = """ARRIVAL STATION:
|
||||
Valle de Mula MainStation: J6 0:4
|
||||
@@ -245,13 +282,29 @@ def test_infantry_taking_fire_direct_position_request():
|
||||
requested'), deadline is a bare 'before <time>' with no 'Requested'/
|
||||
dashes. The '<b>id1</b>' attacker mention is just prose here, not
|
||||
parsed into anything -- only the request itself (shell, position,
|
||||
deadline) matters."""
|
||||
deadline) matters.
|
||||
|
||||
The reporting unit ('Infantry#1 taking fire') is always a FRIENDLY
|
||||
calling in its own distress -- no hostile ever radios in about
|
||||
itself -- so it lands in info.allies, not info.targets (a real bug:
|
||||
it used to default to not-ally, no "Friendly"/"Hostile" prefix word
|
||||
exists in this grammar for the usual inference to key off of). The
|
||||
shell/deadline still need a home a plain Ally tuple doesn't have
|
||||
room for, so they move to a synthetic StrikeRequest target at the
|
||||
SAME coord as the reporting unit ('on our position' means the fire
|
||||
point IS that position, no offset given)."""
|
||||
text = ("Infantry#1 taking fire from <b>id1</b>!\n"
|
||||
"Requesting <u><b>SMK Shell</b></u> on our position at <b>J6 2:7</b> "
|
||||
"before <u>10:38:57</u>!")
|
||||
info = ocr.parse_text(text)
|
||||
assert (TargetType.INFANTRY, "1") in info.targets
|
||||
raw, clues, coord, shell, requested_time = info.targets[(TargetType.INFANTRY, "1")]
|
||||
|
||||
assert (TargetType.INFANTRY, "1") in info.allies
|
||||
assert (TargetType.INFANTRY, "1") not in info.targets
|
||||
raw, clues, coord = info.allies[(TargetType.INFANTRY, "1")]
|
||||
assert coord == Coord("J", 6, 2, 7)
|
||||
|
||||
assert (TargetType.STRIKE_REQUEST, "Infantry1") in info.targets
|
||||
raw, clues, coord, shell, requested_time = info.targets[(TargetType.STRIKE_REQUEST, "Infantry1")]
|
||||
assert coord == Coord("J", 6, 2, 7)
|
||||
assert shell is Shell.SMK
|
||||
assert requested_time == "10:38:57"
|
||||
@@ -262,8 +315,14 @@ def test_infantry_taking_fire_no_attacker_mention():
|
||||
"Requesting <u><b>SMK Shell</b></u> on our position at <b>J6 2:5</b> "
|
||||
"before <u>10:37:52</u>!")
|
||||
info = ocr.parse_text(text)
|
||||
assert (TargetType.INFANTRY, "3") in info.targets
|
||||
raw, clues, coord, shell, requested_time = info.targets[(TargetType.INFANTRY, "3")]
|
||||
|
||||
assert (TargetType.INFANTRY, "3") in info.allies
|
||||
assert (TargetType.INFANTRY, "3") not in info.targets
|
||||
raw, clues, coord = info.allies[(TargetType.INFANTRY, "3")]
|
||||
assert coord == Coord("J", 6, 2, 5)
|
||||
|
||||
assert (TargetType.STRIKE_REQUEST, "Infantry3") in info.targets
|
||||
raw, clues, coord, shell, requested_time = info.targets[(TargetType.STRIKE_REQUEST, "Infantry3")]
|
||||
assert coord == Coord("J", 6, 2, 5)
|
||||
assert shell is Shell.SMK
|
||||
assert requested_time == "10:37:52"
|
||||
@@ -273,22 +332,23 @@ def test_infantry_taking_fire_bearing_distance_from_position():
|
||||
"""The other request shape: the shell isn't wanted right on top of the
|
||||
reporting unit, but at a bearing/distance offset from its own
|
||||
(inline-given) position -- two different places, so this becomes two
|
||||
entries: Infantry#3 stays at its own reported position (no shell/
|
||||
deadline, it's not the fire point), and a separate synthetic Strike
|
||||
entry carries the shell/deadline at the computed offset coord ('our
|
||||
position' isn't a named board entity to hang a Clue off of, so this
|
||||
resolves straight to an absolute coord rather than via one)."""
|
||||
entries: Infantry#3 stays at its own reported position, as an ALLY
|
||||
(see test_infantry_taking_fire_no_attacker_mention's own docstring --
|
||||
same reasoning, this is still a taking-fire report), and a separate
|
||||
synthetic Strike entry carries the shell/deadline at the computed
|
||||
offset coord ('our position' isn't a named board entity to hang a
|
||||
Clue off of, so this resolves straight to an absolute coord rather
|
||||
than via one)."""
|
||||
text = ("Infantry#3 taking fire!\n"
|
||||
"Requesting <u><b>HE Shell</b></u> at bearing <b>239°</b>, distance "
|
||||
"<b>10.76km</b> from our position, <b>J6 2:5</b>, by <u>10:38:18</u> "
|
||||
"or we will be overrun!")
|
||||
info = ocr.parse_text(text)
|
||||
|
||||
assert (TargetType.INFANTRY, "3") in info.targets
|
||||
raw, clues, coord, shell, requested_time = info.targets[(TargetType.INFANTRY, "3")]
|
||||
assert (TargetType.INFANTRY, "3") in info.allies
|
||||
assert (TargetType.INFANTRY, "3") not in info.targets
|
||||
raw, clues, coord = info.allies[(TargetType.INFANTRY, "3")]
|
||||
assert coord == Coord("J", 6, 2, 5)
|
||||
assert shell is None
|
||||
assert requested_time is None
|
||||
|
||||
assert (TargetType.STRIKE_REQUEST, "Infantry3") in info.targets
|
||||
raw, clues, coord, shell, requested_time = info.targets[(TargetType.STRIKE_REQUEST, "Infantry3")]
|
||||
@@ -313,11 +373,10 @@ def test_infantry_taking_fire_bearing_distance_short_range():
|
||||
"or we will be overrun!")
|
||||
info = ocr.parse_text(text)
|
||||
|
||||
assert (TargetType.INFANTRY, "11") in info.targets
|
||||
_, _, coord, shell, requested_time = info.targets[(TargetType.INFANTRY, "11")]
|
||||
assert (TargetType.INFANTRY, "11") in info.allies
|
||||
assert (TargetType.INFANTRY, "11") not in info.targets
|
||||
_, _, coord = info.allies[(TargetType.INFANTRY, "11")]
|
||||
assert coord == Coord("I", 7, 0, 8)
|
||||
assert shell is None
|
||||
assert requested_time is None
|
||||
|
||||
assert (TargetType.STRIKE_REQUEST, "Infantry11") in info.targets
|
||||
_, _, coord, shell, requested_time = info.targets[(TargetType.STRIKE_REQUEST, "Infantry11")]
|
||||
|
||||
Reference in New Issue
Block a user