Fix ally/target bugs, OCR fire-support parsing, add debug capture
- 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>
This commit is contained in:
+81
-2
@@ -197,6 +197,83 @@ def _extract_requested_time(text: str) -> str | None:
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
# A second, unrelated fire-support-request grammar, seen from Infantry
|
||||
# under attack ("taking fire") rather than a pinned Marine Garrison:
|
||||
# Infantry#1 taking fire from id1! Requesting SMK Shell on our
|
||||
# position at J6 2:7 before 10:38:57!
|
||||
# Infantry#3 taking fire! Requesting HE Shell at bearing 239°,
|
||||
# distance 10.76km from our position, J6 2:5, by 10:38:18 or we
|
||||
# will be overrun!
|
||||
# Differs from the Marine Garrison shape in every particular: the shell
|
||||
# word order is reversed ("Requesting X Shell", not "X Shells requested"),
|
||||
# the deadline has no "Requested"/dashes, just a bare "before"/"by <time>",
|
||||
# and the target position is either given directly ("on our position at
|
||||
# <coord>") or as a bearing/distance offset from that same inline
|
||||
# position (never a *named* reference -- "our position" isn't a board
|
||||
# entity, so this resolves the offset directly rather than going through
|
||||
# a Clue).
|
||||
# \s* (not \s+) between the shell code and 'Shell(s)': a rich-text paste's
|
||||
# '<b>SMK Shell</b>' span gets squashed into one no-space token 'SMKShell'
|
||||
# by squash_bold_spans() before this ever runs (same as any other
|
||||
# multi-word bold span, see _squash_span_content), same reasoning as
|
||||
# _extract_requesting_shell()'s docstring.
|
||||
_REQUESTING_SHELL_RE = re.compile(r"Requesting\s+([A-Za-z]+?)\s*Shells?\b", re.IGNORECASE)
|
||||
_TAKING_FIRE_TIME_RE = re.compile(r"\b(?:before|by)\s+(T?\d{1,2}:\d{2}:\d{2})\b", re.IGNORECASE)
|
||||
_ON_OUR_POSITION_COORD_RE = re.compile(
|
||||
rf"on\s+our\s+position\s+at\s+{_COORD_FRAGMENT}", re.IGNORECASE
|
||||
)
|
||||
_BEARING_DISTANCE_FROM_POSITION_RE = re.compile(
|
||||
rf"bearing\s*({_DIGIT_CLASS}{{1,3}})\s*°?\s*,?\s*distance\s*([\d.]+)\s*k?m\s+from\s+our\s+"
|
||||
rf"position,?\s*{_COORD_FRAGMENT}",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _extract_requesting_shell(text: str) -> Shell | None:
|
||||
"""'Requesting SMK Shell' -- the taking-fire grammar's shell mention,
|
||||
word order reversed from _extract_shell_request()'s Marine Garrison
|
||||
one ('SMK Shells requested')."""
|
||||
m = _REQUESTING_SHELL_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
return Shell[m.group(1).upper()]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_taking_fire_time(text: str) -> str | None:
|
||||
m = _TAKING_FIRE_TIME_RE.search(text)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _extract_on_our_position_coord(text: str) -> Coord | None:
|
||||
m = _ON_OUR_POSITION_COORD_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
return _coord_from_groups(*m.groups())
|
||||
|
||||
|
||||
def _extract_bearing_distance_from_position_coord(text: str) -> Coord | None:
|
||||
"""The bearing/distance variant of a taking-fire request: the shell is
|
||||
wanted somewhere OFF the reporting unit's own position, given as a
|
||||
bearing/distance from it, with that position itself given inline
|
||||
right there ('...from our position, J6 2:5, by ...'). "our position"
|
||||
isn't a named board entity to hang a Clue off of, so this resolves
|
||||
the offset directly via the same polar-projection math solve_location()
|
||||
uses for an ordinary single bearing+distance clue."""
|
||||
m = _BEARING_DISTANCE_FROM_POSITION_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
bearing, distance, letter, y, x, yy = m.groups()
|
||||
origin = _coord_from_groups(letter, y, x, yy)
|
||||
if origin is None:
|
||||
return None
|
||||
point = solver.point_from_bearing_distance(
|
||||
origin.as_fraction(), float(_fix_digits(bearing)), float(distance))
|
||||
return solver.point_to_coord(point)
|
||||
|
||||
|
||||
# "Reported active in grid D10": only the large-grid cell, no sub-grid
|
||||
# x:y at all, unlike every other coord shape in this file. Tried last
|
||||
# (after _extract_grid_coord, which requires the full x:y and so is
|
||||
@@ -659,10 +736,12 @@ def parse_intel_blocks(text: str) -> list[dict]:
|
||||
current["clues"] = _parse_all_clues(joined)
|
||||
current["coord"] = (
|
||||
_extract_grid_coord(joined) or _extract_requested_on_coord(joined)
|
||||
or _extract_on_our_position_coord(joined)
|
||||
or _extract_bearing_distance_from_position_coord(joined)
|
||||
or _extract_large_grid_only_coord(joined)
|
||||
)
|
||||
current["shell"] = _extract_shell_request(joined)
|
||||
current["requested_time"] = _extract_requested_time(joined)
|
||||
current["shell"] = _extract_shell_request(joined) or _extract_requesting_shell(joined)
|
||||
current["requested_time"] = _extract_requested_time(joined) or _extract_taking_fire_time(joined)
|
||||
if (current["clues"] or current["coord"] is not None
|
||||
or current["shell"] is not None or current["requested_time"] is not None):
|
||||
current["raw"] = joined
|
||||
|
||||
Reference in New Issue
Block a user