"""Regression coverage for Board's bulk-mutation helpers (clear/clear_units) and the id namespaces Target/Ally are supposed to keep separate.""" from fenigma.models import Board, Coord, TargetType def _coord(x=0, y=0): return Coord(X="A", Y=1, x=x, y=y) def test_clear_drops_allies_too(): """Board.clear() used to leave self.allies untouched -- the "clear board" action then reported success but a previously-placed ally stayed on the map.""" board = Board() board.nest.coord = _coord() board.add_spotter(_coord()) board.add_reference_point(_coord()) board.add_target(TargetType.TANK, _coord()) board.add_ally(TargetType.TANK, _coord()) board.add_scout_flight((1.0, 1.0), 45.0) board.clear() assert board.nest.coord is None assert board.spotters == [] assert board.reference_points == [] assert board.targets == [] assert board.allies == [] assert board.scout_flights == [] def test_clear_units_keeps_recon_infrastructure(): """The Clear button's right-click "Clear enemies, units & flights" option: drops targets/allies/scout flights but keeps the Nest, spotters, and reference points.""" board = Board() board.nest.coord = _coord() sp = board.add_spotter(_coord()) rp = board.add_reference_point(_coord()) board.add_target(TargetType.TANK, _coord()) board.add_ally(TargetType.TANK, _coord()) board.add_scout_flight((1.0, 1.0), 45.0) board.clear_units() assert board.nest.coord is not None assert board.spotters == [sp] assert board.reference_points == [rp] assert board.targets == [] assert board.allies == [] assert board.scout_flights == [] 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. 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 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 *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 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 '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 == "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 == "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 == "A" assert ally_infantry.id == "A" 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 == "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 == "AA" def test_find_by_name_prefers_target_over_same_named_ally(): """find_by_name() (used to resolve Clue references) checks targets before allies -- documented, deliberate priority, not a namespace collision: an ally and a same-typed/same-id target are still two distinct objects, this only matters when something's Clue names one ambiguously by the shared display name.""" board = Board() target = board.add_target(TargetType.TANK, _coord(x=1), id_="1") board.add_ally(TargetType.TANK, _coord(x=2), id_="1") assert board.find_by_name("Tank#1") is target