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:
+9
-1
@@ -850,9 +850,17 @@ class MainWindow(Adw.ApplicationWindow):
|
||||
box = page()
|
||||
lbl = Gtk.Label(xalign=0, margin_start=4, margin_bottom=2)
|
||||
side = "friendly" if proposal.side == "friendly" else "hostile"
|
||||
# detected_id (map_vision.read_marker_id's best-effort read of
|
||||
# the marker's own "#<N>" label, see its own docstring) is
|
||||
# shown here so it's visible right when there's still a
|
||||
# screenshot to actually check it against -- accept already
|
||||
# uses it for the entity's id when present (see
|
||||
# _accept_proposal), this is just making that fact visible
|
||||
# before the click, not a separate signal.
|
||||
id_part = f", id #{proposal.detected_id}" if proposal.detected_id else ""
|
||||
lbl.set_markup(
|
||||
f"<b>{GLib.markup_escape_text(proposal.coord)}</b> — {side}, "
|
||||
f"{detected.value if detected else 'type unknown'}")
|
||||
f"{detected.value if detected else 'type unknown'}{GLib.markup_escape_text(id_part)}")
|
||||
box.append(lbl)
|
||||
box.append(Gtk.Separator(margin_top=2, margin_bottom=2))
|
||||
button(box, f"Accept as {detected.value if detected else TargetType.UNKNOWN.value}",
|
||||
|
||||
@@ -638,8 +638,14 @@ class GridCanvas(Gtk.DrawingArea):
|
||||
what a proposal is until the user accepts it."""
|
||||
for p, coord in self._pending_proposals():
|
||||
color = CATEGORY_COLOR["ally" if p.side == "friendly" else "target"]
|
||||
# detected_id (map_vision.read_marker_id's best-effort read of
|
||||
# the marker's own "#<N>" label) shown right on the map while
|
||||
# the screenshot backing it is still up, so it's checkable
|
||||
# against the actual pixels -- same id _accept_proposal will
|
||||
# use for the entity if this gets accepted, see its own comment.
|
||||
id_part = f" #{p.detected_id}" if p.detected_id else ""
|
||||
self._draw_marker(cr, view, coord.as_fraction(), color,
|
||||
f"? {coord.label()}", width, height,
|
||||
f"?{id_part} {coord.label()}", width, height,
|
||||
hollow=True, coord=coord)
|
||||
|
||||
def _hit_test(self, view: _View, x: float, y: float):
|
||||
|
||||
+34
-33
@@ -18,7 +18,6 @@ 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
|
||||
@@ -503,27 +502,28 @@ class ScoutFlight:
|
||||
return f"ScoutFlight#{self.id}"
|
||||
|
||||
|
||||
def _next_free_id(used: set[str]) -> str:
|
||||
"""Next unused id in a short, human-friendly sequence: single
|
||||
uppercase letters (A..Z) first, then two-letter combinations
|
||||
(AA..ZZ, spreadsheet-column style) once those run out, and so on.
|
||||
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.
|
||||
|
||||
A real regression lived here: `next(c for c in string.ascii_uppercase
|
||||
if c not in used)` raises StopIteration the instant all 26 letters
|
||||
are taken, which used to need 26+ auto-added entities of one TYPE
|
||||
(rare) but, once add_target()/add_ally() moved to one shared id
|
||||
sequence per GROUP instead of per type (so a Tank and an Infantry
|
||||
added back to back get 'A'/'B', not both 'A', see their own
|
||||
comments), needs only 26 auto-added entities of ANY type in that
|
||||
group -- reachable in a single big screenshot import. This can't run
|
||||
out: it just grows the id length instead."""
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
SAVE_FORMAT_VERSION = 3
|
||||
@@ -596,14 +596,15 @@ class Board:
|
||||
location: Location | Coord | None = None,
|
||||
id_: str | None = None,
|
||||
) -> Target:
|
||||
# One shared A/B/C... sequence across every target regardless of
|
||||
# type, not one sequence per type -- a Tank and an Infantry auto-
|
||||
# assigned back to back get 'A' and 'B', never both 'A'. Only
|
||||
# targets-vs-allies is a separate id namespace (see add_ally),
|
||||
# type never subdivides it further.
|
||||
# Own 1/2/3... 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.
|
||||
if not id_:
|
||||
used = {t.id for t in self.targets}
|
||||
id_ = _next_free_id(used)
|
||||
used = {t.id for t in self.targets if t.type == type_}
|
||||
id_ = _next_free_numeric_id(used)
|
||||
t = Target(type=type_, id=id_, location=_as_location(location))
|
||||
self.targets.append(t)
|
||||
return t
|
||||
@@ -620,12 +621,12 @@ class Board:
|
||||
) -> Ally:
|
||||
# A separate id namespace from add_target()'s: an ally Tank#1
|
||||
# and a hostile Target Tank#1 are unrelated, so auto-assignment
|
||||
# here only looks at other allies, never self.targets. Same as
|
||||
# add_target though, that's the ONLY split: one shared A/B/C...
|
||||
# sequence across every ally regardless of type, not one per type.
|
||||
# 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.
|
||||
if not id_:
|
||||
used = {a.id for a in self.allies}
|
||||
id_ = _next_free_id(used)
|
||||
used = {a.id for a in self.allies if a.type == type_}
|
||||
id_ = _next_free_numeric_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