Revert auto-assigned ids back to letters; only detected ids are numeric
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>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
"""_accept_proposal: an accepted proposal's entity id should prefer the
|
||||
marker's own detected "#<N>" id (map_vision.read_marker_id, via
|
||||
Proposal.detected_id) over an auto-assigned number, so ids on the board
|
||||
match what's actually on screen -- falling back to auto-assign only when
|
||||
there's no detection, or it collides with an id already used for that
|
||||
type in that group (see _accept_proposal's own docstring, and
|
||||
models.py's _next_free_numeric_id for the per-type auto-assignment
|
||||
these fall back to).
|
||||
Proposal.detected_id) over an auto-assigned letter, so ids on the board
|
||||
match what's actually on screen -- falling back to auto-assign (a
|
||||
letter, deliberately not a number, so it can't collide with or be
|
||||
mistaken for a real detected id -- see models.py's _next_free_id) only
|
||||
when there's no detection, or it collides with an id already used for
|
||||
that type in that group (see _accept_proposal's own docstring).
|
||||
|
||||
Needs a real Adw/Gtk init (MainWindow.__new__ skips __init__, so no
|
||||
window/widgets are actually built, but Adw.init() is still required for
|
||||
@@ -46,7 +46,7 @@ def test_accept_uses_the_detected_id_when_present():
|
||||
def test_accept_falls_back_to_auto_id_with_no_detection():
|
||||
win = _window()
|
||||
win._accept_proposal(_proposal(detected_id=None))
|
||||
assert win.board.targets[0].id == "1"
|
||||
assert win.board.targets[0].id == "A"
|
||||
|
||||
|
||||
def test_accept_falls_back_to_auto_id_on_a_detected_id_collision():
|
||||
|
||||
+23
-21
@@ -54,58 +54,60 @@ 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 '1' independently."""
|
||||
on each side starts from 'A' independently. Explicit id_="1" here
|
||||
(as an accepted screenshot proposal's detected_id would pass, see
|
||||
app.py's _accept_proposal) to also check that auto-assignment
|
||||
correctly skips a real numeric id already in use, not just other
|
||||
letters."""
|
||||
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 == "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
|
||||
assert t_auto.id == "A" # first free letter among *target* Tanks only
|
||||
assert a_auto.id == "A" # first free letter among *ally* Tanks only, unaffected by the target above
|
||||
|
||||
|
||||
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
|
||||
"""Each TYPE gets its own independent A/B/C... 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."""
|
||||
back to back both start at 'A' (Tank#A, Infantry#A), 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 == "1"
|
||||
assert infantry.id == "1" # own sequence, not '2' just because a Tank came first
|
||||
assert tank.id == "A"
|
||||
assert infantry.id == "A" # own sequence, not 'B' 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
|
||||
assert second_tank.id == "B" # 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 == "1"
|
||||
assert ally_infantry.id == "1"
|
||||
assert ally_tank.id == "A"
|
||||
assert ally_infantry.id == "A"
|
||||
|
||||
|
||||
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."""
|
||||
def test_auto_id_survives_past_26_entities_of_one_type():
|
||||
"""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/adding 26+ of the same type into one group
|
||||
in a single session. Must roll over to two-letter ids ('AA', 'AB',
|
||||
...) instead of raising."""
|
||||
board = Board()
|
||||
for _ in range(26):
|
||||
board.add_target(TargetType.TANK, _coord())
|
||||
twenty_seventh = board.add_target(TargetType.TANK, _coord())
|
||||
assert twenty_seventh.id == "27"
|
||||
assert twenty_seventh.id == "AA"
|
||||
|
||||
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 == "27"
|
||||
assert twenty_seventh_ally.id == "AA"
|
||||
|
||||
|
||||
def test_find_by_name_prefers_target_over_same_named_ally():
|
||||
|
||||
Reference in New Issue
Block a user