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>
This commit is contained in:
+47
-3
@@ -274,6 +274,22 @@ def _extract_bearing_distance_from_position_coord(text: str) -> Coord | None:
|
||||
return solver.point_to_coord(point)
|
||||
|
||||
|
||||
def _extract_our_position_coord(text: str) -> Coord | None:
|
||||
"""The bearing/distance variant's OWN inline position ('...from our
|
||||
position, J6 2:5, by ...'), as opposed to
|
||||
_extract_bearing_distance_from_position_coord's computed offset from
|
||||
it. Reported unit and requested fire point are two different places
|
||||
for this variant (unlike the direct "on our position at <coord>" one,
|
||||
a real danger-close call), so parse_intel_blocks's flush() uses this
|
||||
for the reporting unit's own entry and the offset for a second,
|
||||
separate Strike entry -- see its comment."""
|
||||
m = _BEARING_DISTANCE_FROM_POSITION_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
_bearing, _distance, letter, y, x, yy = m.groups()
|
||||
return _coord_from_groups(letter, y, x, yy)
|
||||
|
||||
|
||||
# "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
|
||||
@@ -734,18 +750,46 @@ def parse_intel_blocks(text: str) -> list[dict]:
|
||||
if current is not None:
|
||||
joined = "\n".join(current["raw"])
|
||||
current["clues"] = _parse_all_clues(joined)
|
||||
# The bearing/distance taking-fire variant names TWO different
|
||||
# places (see _extract_our_position_coord's docstring): the
|
||||
# reporting unit's own position, and a separate fire point
|
||||
# offset from it. Everything else in this module is "one block
|
||||
# -> one entry", so that offset gets split into a second,
|
||||
# synthetic StrikeRequest entry below rather than folded into
|
||||
# this one -- otherwise the fire point either overwrites the
|
||||
# unit's real position (wrong place) or gets silently dropped.
|
||||
offset_coord = _extract_bearing_distance_from_position_coord(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_our_position_coord(joined)
|
||||
or _extract_large_grid_only_coord(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)
|
||||
shell = _extract_shell_request(joined) or _extract_requesting_shell(joined)
|
||||
requested_time = _extract_requested_time(joined) or _extract_taking_fire_time(joined)
|
||||
# The offset variant's shell/deadline describe the FIRE POINT,
|
||||
# not the reporting unit itself -- they move to the synthetic
|
||||
# StrikeRequest entry below, not kept here too.
|
||||
current["shell"] = None if offset_coord is not None else shell
|
||||
current["requested_time"] = None if offset_coord is not None else requested_time
|
||||
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
|
||||
entries.append(current)
|
||||
if offset_coord is not None:
|
||||
# TargetType.STRIKE_REQUEST, not STRIKE: this is a
|
||||
# friendly unit calling in a strike over the radio, not
|
||||
# one the player placed themselves (see that type's own
|
||||
# comment in models.py). type_word must match its
|
||||
# TargetType.short exactly ("StrikeRequest", no space),
|
||||
# same as every other type_word this module produces.
|
||||
strike_id = f"{current['type_word']}{current['id']}"
|
||||
entries.append({
|
||||
"kind": "named", "name": f"StrikeRequest#{strike_id}",
|
||||
"type_word": "StrikeRequest",
|
||||
"id": strike_id, "raw": joined, "clues": [], "coord": offset_coord,
|
||||
"shell": shell, "requested_time": requested_time,
|
||||
})
|
||||
current = None
|
||||
|
||||
for raw_line in text.splitlines():
|
||||
|
||||
Reference in New Issue
Block a user