Auto-ids per type not per group; show detected_id in proposal UI

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>
This commit is contained in:
2026-08-13 19:28:01 +02:00
co-authored by Claude Sonnet 5
parent 8109db2f39
commit d2f70675b8
6 changed files with 111 additions and 62 deletions
+25 -23
View File
@@ -54,56 +54,58 @@ def test_clear_units_keeps_recon_infrastructure():
def test_ally_and_target_ids_are_independent_namespaces():
"""An ally Tank#1 and a hostile Target Tank#1 are unrelated -- adding
one must never be influenced by the other's ids, and auto-assignment
on each side starts from 'A' independently."""
on each side starts from '1' independently."""
board = Board()
t1 = board.add_target(TargetType.TANK, _coord(), id_="1")
a1 = board.add_ally(TargetType.TANK, _coord(), id_="1")
assert t1.id == a1.id == "1"
assert t1 is not a1
# '1' is already taken (explicitly) for TANK on each side, so the next
# auto-assigned Tank on each side must skip it and land on '2'.
t_auto = board.add_target(TargetType.TANK, _coord())
a_auto = board.add_ally(TargetType.TANK, _coord())
assert t_auto.id == "A" # first free letter among *targets* only
assert a_auto.id == "A" # first free letter among *allies* only, unaffected by the target above
assert t_auto.id == "2" # first free number among *target* Tanks only
assert a_auto.id == "2" # first free number among *ally* Tanks only, unaffected by the target above
def test_auto_id_is_shared_across_types_within_targets_and_within_allies():
"""The id namespace split is targets-vs-allies ONLY -- different types
within the same group (all targets, or all allies) share one A/B/C...
sequence, they do NOT each get their own independent sequence. A Tank
and an Infantry auto-assigned back to back must get 'A' and 'B', never
both 'A'."""
def test_auto_id_is_per_type_within_targets_and_within_allies():
"""Each TYPE gets its own independent 1/2/3... sequence within a group
(all targets, or all allies) -- a Tank and an Infantry auto-assigned
back to back both start at '1' (Tank#1, Infantry#1), matching how the
game itself numbers units, rather than sharing one sequence across
every type in the group."""
board = Board()
tank = board.add_target(TargetType.TANK, _coord())
infantry = board.add_target(TargetType.INFANTRY, _coord())
assert tank.id == "A"
assert infantry.id == "B" # not 'A' again just because it's a different type
assert tank.id == "1"
assert infantry.id == "1" # own sequence, not '2' just because a Tank came first
second_tank = board.add_target(TargetType.TANK, _coord())
assert second_tank.id == "2" # but a SECOND Tank does advance the Tank sequence
ally_tank = board.add_ally(TargetType.TANK, _coord())
ally_infantry = board.add_ally(TargetType.INFANTRY, _coord())
assert ally_tank.id == "A"
assert ally_infantry.id == "B"
assert ally_tank.id == "1"
assert ally_infantry.id == "1"
def test_auto_id_survives_past_26_entities_in_one_group():
"""A real crash: `next(c for c in string.ascii_uppercase if c not in
used)` raises StopIteration the instant all 26 letters are taken --
reachable after accepting 26+ map-screenshot proposals into the same
group (targets, or allies) in one session, since the fix making the
id sequence shared across types (not per-type) made 26 much easier
to hit. Must roll over to two-letter ids ('AA', 'AB', ...) instead of
raising."""
def test_auto_id_keeps_counting_past_26_entities_of_one_type():
"""The id sequence is a plain integer counter now (see
_next_free_numeric_id), not the old letter sequence that could raise
StopIteration past 26 (see TODO.md/git history) -- nothing special
should happen crossing 26, it just keeps counting."""
board = Board()
for _ in range(26):
board.add_target(TargetType.TANK, _coord())
twenty_seventh = board.add_target(TargetType.TANK, _coord())
assert twenty_seventh.id == "AA"
assert twenty_seventh.id == "27"
board2 = Board()
for _ in range(26):
board2.add_ally(TargetType.TANK, _coord())
twenty_seventh_ally = board2.add_ally(TargetType.TANK, _coord())
assert twenty_seventh_ally.id == "AA"
assert twenty_seventh_ally.id == "27"
def test_find_by_name_prefers_target_over_same_named_ally():