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:
+2
-1
@@ -760,7 +760,8 @@ class MainWindow(Adw.ApplicationWindow):
|
||||
# than losing this one accept's traceability to the game's own
|
||||
# number.
|
||||
id_ = proposal.detected_id
|
||||
existing = {a.id for a in self.board.allies} if is_ally else {t.id for t in self.board.targets}
|
||||
group = self.board.allies if is_ally else self.board.targets
|
||||
existing = {o.id for o in group if o.type == type_} # per-type, same as Board.add_target/add_ally
|
||||
if id_ in existing:
|
||||
id_ = None
|
||||
if is_ally:
|
||||
|
||||
+45
-32
@@ -18,6 +18,7 @@ Coord) to work out everything else. This module just defines the shape.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import string
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
@@ -502,28 +503,37 @@ class ScoutFlight:
|
||||
return f"ScoutFlight#{self.id}"
|
||||
|
||||
|
||||
def _next_free_numeric_id(used: set[str]) -> str:
|
||||
"""Next unused id in a plain 1, 2, 3... sequence -- matches the small
|
||||
integer ids the game itself shows per unit (see
|
||||
map_vision.read_marker_id and app.py's _accept_proposal, which
|
||||
prefers that detected id over this auto-assignment whenever it has
|
||||
one), scoped per TYPE, not per group: add_target()/add_ally() only
|
||||
look at existing entities of the SAME type when building `used`, so
|
||||
a Tank and an Infantry added back to back both start at '1', each
|
||||
type keeping its own independent count (Infantry #1/#2/#3,
|
||||
Mechanized #1/#2/#3, ..., same as the game's own numbering) rather
|
||||
than sharing one sequence across every type in the group.
|
||||
def _next_free_id(used: set[str]) -> str:
|
||||
"""Next unused id in a short, human-friendly LETTER sequence: single
|
||||
uppercase letters (A..Z) first, then two-letter combinations
|
||||
(AA..ZZ, spreadsheet-column style) once those run out, and so on.
|
||||
Scoped per TYPE, not per group: add_target()/add_ally() only look at
|
||||
existing entities of the SAME type when building `used`, so a Tank
|
||||
and an Infantry added back to back both start at 'A' (Tank#A,
|
||||
Infantry#A), each type keeping its own independent sequence.
|
||||
|
||||
Older versions of this used a letter sequence (A, B, C..., rolling
|
||||
over to AA/AB/... past 26) shared across a whole group instead of
|
||||
per type -- switched away from per-type once before (see TODO.md)
|
||||
because sharing made it too easy to run past 26 letters. That's not
|
||||
a concern here: a plain counter never runs out, there's no fixed
|
||||
alphabet to exhaust regardless of how it's scoped."""
|
||||
n = 1
|
||||
while str(n) in used:
|
||||
n += 1
|
||||
return str(n)
|
||||
Deliberately letters, not numbers: a manually-added entity (map
|
||||
right-click "Add target", or an accepted screenshot proposal with no
|
||||
confident id read) has no real game id to report, so it gets an
|
||||
obviously-not-a-real-id placeholder instead -- app.py's
|
||||
_accept_proposal reserves plain numbers for an id it's actually
|
||||
confident was read off the marker itself (map_vision.read_marker_id
|
||||
via Proposal.detected_id), passed straight through as this
|
||||
function's caller's `id_` and never touching this auto-assignment at
|
||||
all. Letters can't collide with a real (numeric) detected id either,
|
||||
on top of just reading honestly as 'made up'.
|
||||
|
||||
Can't run out the way `next(c for c in string.ascii_uppercase if c
|
||||
not in used)` used to (a real regression, see TODO.md): rolls over to
|
||||
two-letter ids ('AA', 'AB', ...) past 26 instead of raising
|
||||
StopIteration."""
|
||||
length = 1
|
||||
while True:
|
||||
for combo in itertools.product(string.ascii_uppercase, repeat=length):
|
||||
candidate = "".join(combo)
|
||||
if candidate not in used:
|
||||
return candidate
|
||||
length += 1
|
||||
|
||||
|
||||
SAVE_FORMAT_VERSION = 3
|
||||
@@ -596,15 +606,18 @@ class Board:
|
||||
location: Location | Coord | None = None,
|
||||
id_: str | None = None,
|
||||
) -> Target:
|
||||
# Own 1/2/3... sequence per TYPE, not one shared across every
|
||||
# Own A/B/C... sequence per TYPE, not one shared across every
|
||||
# target regardless of type -- 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. targets-vs-allies
|
||||
# is still its own separate id namespace (see add_ally); type
|
||||
# now subdivides it further too. See _next_free_numeric_id.
|
||||
# assigned back to back both start at 'A' (Tank#A, Infantry#A).
|
||||
# targets-vs-allies is still its own separate id namespace (see
|
||||
# add_ally); type now subdivides it further too. Letters, not
|
||||
# numbers, when auto-assigning here specifically: see
|
||||
# _next_free_id's own docstring for why (a real detected id, when
|
||||
# there is one, is passed in as `id_` and never reaches this
|
||||
# auto-assignment at all).
|
||||
if not id_:
|
||||
used = {t.id for t in self.targets if t.type == type_}
|
||||
id_ = _next_free_numeric_id(used)
|
||||
id_ = _next_free_id(used)
|
||||
t = Target(type=type_, id=id_, location=_as_location(location))
|
||||
self.targets.append(t)
|
||||
return t
|
||||
@@ -619,14 +632,14 @@ class Board:
|
||||
location: Location | Coord | None = None,
|
||||
id_: str | None = None,
|
||||
) -> Ally:
|
||||
# A separate id namespace from add_target()'s: an ally Tank#1
|
||||
# and a hostile Target Tank#1 are unrelated, so auto-assignment
|
||||
# A separate id namespace from add_target()'s: an ally Tank#A
|
||||
# and a hostile Target Tank#A are unrelated, so auto-assignment
|
||||
# here only looks at other allies, never self.targets. Own
|
||||
# 1/2/3... sequence per TYPE too, same as add_target -- see its
|
||||
# own comment and _next_free_numeric_id.
|
||||
# A/B/C... sequence per TYPE too, same as add_target -- see its
|
||||
# own comment and _next_free_id.
|
||||
if not id_:
|
||||
used = {a.id for a in self.allies if a.type == type_}
|
||||
id_ = _next_free_numeric_id(used)
|
||||
id_ = _next_free_id(used)
|
||||
a = Ally(type=type_, id=id_, location=_as_location(location))
|
||||
self.allies.append(a)
|
||||
return a
|
||||
|
||||
Reference in New Issue
Block a user