Same pattern already established for AMMO_CACHE -> SUPPLY_CACHE: a 'Coastal Battery' is just a heavy fixed-emplacement HostileArtillery under a different name in the game's own text, not a meaningfully different unit type worth its own enum member. Removed the TargetType.COASTAL_BATTERY member, added 'CoastalBattery' to ocr.py's _TYPE_WORD_ALIASES (so 'Coastal Battery#2:' still parses, now as a HostileArtillery), and a models.py migration entry so any save file written before this change still loads correctly. Verified the migration directly (COASTAL_BATTERY -> HOSTILE_ARTILLERY, existing AMMO_CACHE -> SUPPLY_CACHE unaffected) and updated the one test that asserted the old type.
220 lines
8.2 KiB
Python
220 lines
8.2 KiB
Python
"""Regression coverage for every intel-text format ocr.py understands.
|
|
|
|
Each format below was added incrementally in response to a real
|
|
screenshot/paste the user hit, and at least one of them (the '<ref>:
|
|
<value>' grammar colliding with the 'Type#id:' header shape) has already
|
|
regressed silently once because there was no test suite to catch it.
|
|
One test per format, named after what it covers, so a future change
|
|
that breaks an old format fails loudly and specifically instead of
|
|
being noticed (or not) days later.
|
|
"""
|
|
from fenigma import ocr
|
|
from fenigma.models import Coord, TargetType
|
|
|
|
|
|
def test_standard_target_and_rp_blocks():
|
|
text = """
|
|
Target#5 Spotted. 088, 12.10km from Spotter#1
|
|
.
|
|
Reference Point Alpha:
|
|
Bearing 094 from Spotter#1
|
|
Distance 13.26km from Spotter#2
|
|
.
|
|
AmmoCache#3:
|
|
Bearing 217 & Distance 10.48km from AmmoCache#2
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
|
|
assert (TargetType.UNKNOWN, "5") in info.targets
|
|
raw, clues, coord, shell, requested_time = info.targets[(TargetType.UNKNOWN, "5")]
|
|
assert clues == [ocr.Clue(reference="Spotter#1", bearing_deg=88.0, distance_km=12.1)]
|
|
|
|
# "AmmoCache" is an alias for SupplyCache, both in the header and in
|
|
# a reference to an existing one.
|
|
assert (TargetType.SUPPLY_CACHE, "3") in info.targets
|
|
raw, clues, coord, shell, requested_time = info.targets[(TargetType.SUPPLY_CACHE, "3")]
|
|
assert clues == [ocr.Clue(reference="SupplyCache#2", bearing_deg=217.0, distance_km=10.48)]
|
|
|
|
assert "Alpha" in info.reference_points
|
|
|
|
|
|
def test_calibration_target_line():
|
|
text = "IRON NEST location - H3 5:5\nTARGET COORDINATES:\nTarget is at- Q4 4:2"
|
|
info = ocr.parse_text(text)
|
|
assert info.nest_coord == Coord("H", 3, 5, 5)
|
|
assert info.targets[(TargetType.UNKNOWN, "1")][2] == Coord("Q", 4, 4, 2)
|
|
|
|
|
|
def test_destroyed_reports_digit_and_letter_id():
|
|
text = "SupplyCache#2 Destroyed. Additional Requisition Granted.\nDirect Hit! HostileTank#3 Destroyed."
|
|
info = ocr.parse_text(text)
|
|
assert info.destroyed == {(TargetType.SUPPLY_CACHE, "2"), (TargetType.HOSTILE_TANK, "3")}
|
|
|
|
|
|
def test_train_arrival_intel():
|
|
text = """ARRIVAL STATION:
|
|
Valle de Mula MainStation: J6 0:4
|
|
.
|
|
Estimated arrival: T=10:16:50
|
|
.
|
|
TRACK ALIGNMENT:
|
|
Rail line runs straight. Bearing 090 from MainStation.
|
|
.
|
|
FINAL APPROACH:
|
|
Waypoint A - 6.00km from station: T=10:06:50
|
|
Waypoint B - 4.00km from station: T=10:10:10
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert "MainStation" in info.reference_points
|
|
assert info.reference_points["MainStation"][2] == Coord("J", 6, 0, 4)
|
|
assert "Waypoint A" in info.reference_points
|
|
_, clues, coord = info.reference_points["Waypoint A"]
|
|
assert clues == [ocr.Clue(reference="MainStation", bearing_deg=90.0, distance_km=6.0)]
|
|
|
|
|
|
def test_enemy_multiword_name_becomes_its_own_target_type():
|
|
text = """Enemy Signal Station:
|
|
Distance 4.40km from Spotter#1
|
|
Distance 5.96km from Spotter#2
|
|
.
|
|
Enemy Assembly Area:
|
|
Bearing 034 from Enemy Signal Station
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert (TargetType.ENEMY, "SignalStation") in info.targets
|
|
assert (TargetType.ENEMY, "AssemblyArea") in info.targets
|
|
_, clues, *_ = info.targets[(TargetType.ENEMY, "AssemblyArea")]
|
|
assert clues == [ocr.Clue(reference="Enemy#SignalStation", bearing_deg=34.0)]
|
|
|
|
|
|
def test_enemy_destroyed_report():
|
|
text = "Priority target Enemy Signal Station Destroyed, +25 Requisition."
|
|
info = ocr.parse_text(text)
|
|
assert (TargetType.ENEMY, "SignalStation") in info.destroyed
|
|
|
|
|
|
def test_listening_post_and_coastal_battery():
|
|
"""'Coastal Battery' is an alias for HostileArtillery (see
|
|
_TYPE_WORD_ALIASES), not its own TargetType, it's just the
|
|
fixed-emplacement flavor of the same thing."""
|
|
text = """Listening Post#1 at K6 7:8 audio reports on:
|
|
Coastal Battery#2:
|
|
Distance 6.28km South-East from Listening Post#1
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert info.reference_points["ListeningPost#1"][2] == Coord("K", 6, 7, 8)
|
|
assert (TargetType.HOSTILE_ARTILLERY, "2") in info.targets
|
|
_, clues, *_ = info.targets[(TargetType.HOSTILE_ARTILLERY, "2")]
|
|
assert clues == [ocr.Clue(reference="ListeningPost#1", bearing_deg=135.0, distance_km=6.28)]
|
|
|
|
|
|
def test_marine_garrison_fire_support_request():
|
|
text = """Marine Garrison#1 pinned!
|
|
SMK Shells requested on J6 8:3
|
|
Requested before - T10:31:41 -
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert (TargetType.MARINE_GARRISON, "1") in info.targets
|
|
raw, clues, coord, shell, requested_time = info.targets[(TargetType.MARINE_GARRISON, "1")]
|
|
assert coord == Coord("J", 6, 8, 3)
|
|
from fenigma.shells import Shell
|
|
assert shell is Shell.SMK
|
|
assert requested_time == "T10:31:41"
|
|
|
|
|
|
def test_multiword_rp_names_with_bold_markup():
|
|
text = """Reference Point <b>The Mole</b>:
|
|
Bearing <b>100°</b> from <b>Spotter#1</b>
|
|
Bearing <b>048°</b> from <b>Spotter#2</b>
|
|
.
|
|
Reference Point <b>Dockmaster's House</b>:
|
|
Distance <b>6.14km</b> from <b>The Mole</b>
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert "TheMole" in info.reference_points
|
|
assert "DockmastersHouse" in info.reference_points
|
|
_, clues, _ = info.reference_points["DockmastersHouse"]
|
|
assert clues == [ocr.Clue(reference="TheMole", distance_km=6.14)]
|
|
|
|
|
|
def test_bare_name_header_becomes_a_target_not_an_rp():
|
|
text = """<b>HMS Rockingham</b>:
|
|
Distance <b>4.65km</b> from <b>Spotter#2</b>
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert (TargetType.UNKNOWN, "HMSRockingham") in info.targets
|
|
assert not info.reference_points
|
|
|
|
|
|
def test_bold_coordinate_spans_are_not_corrupted_by_name_squashing():
|
|
"""A real regression: squashing multi-word bold spans into single
|
|
tokens (for names) once also mangled multi-word COORD spans like
|
|
'C9 7:9' into garbage ('C979'), because nothing distinguished the
|
|
two cases. Guarded here permanently."""
|
|
text = """IRON NEST - <b>A2 9:8</b>
|
|
.
|
|
OBSERVATION ASSETS:
|
|
<b>Spotter#1</b> - <b>C9 7:9</b>
|
|
<b>Spotter#2</b> - <b>E5 5:0</b>
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert info.nest_coord == Coord("A", 2, 9, 8)
|
|
assert info.spotters == {1: Coord("C", 9, 7, 9), 2: Coord("E", 5, 5, 0)}
|
|
|
|
|
|
def test_forward_observer_reports_triangulate_without_leaving_ephemeral_entities():
|
|
text = """FO#5 Audio report on HMS Rockingham: 2.24km From I8 6:9 . . .
|
|
- - -
|
|
FO#4 Eyes on HMS Rockingham: 087° From G7 6:7 . . .
|
|
- - -
|
|
FO Eyes on HMS Rockingham: 099° From C9 1:2 . . .
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert (TargetType.UNKNOWN, "HMSRockingham") in info.targets
|
|
_, clues, coord, *_ = info.targets[(TargetType.UNKNOWN, "HMSRockingham")]
|
|
# solved immediately via a scratch board, no Clues referencing an FO
|
|
# persist, and no FO ever leaks in as a real reference point.
|
|
assert clues == []
|
|
assert coord is not None
|
|
assert not info.reference_points
|
|
|
|
|
|
def test_ref_colon_value_clue_grammar():
|
|
"""'Spotter#2: 4.04km' has the exact same 'Word#digits:' shape as a
|
|
real block header ('AmmoCache#3:'), a real regression: it hijacked
|
|
the block before any clues could attach to the entity above it."""
|
|
text = """Enemy Assembly Area:
|
|
Spotter#2: 4.04km
|
|
Spotter#3: 298°
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
assert (TargetType.ENEMY, "AssemblyArea") in info.targets
|
|
_, clues, *_ = info.targets[(TargetType.ENEMY, "AssemblyArea")]
|
|
assert clues == [
|
|
ocr.Clue(reference="Spotter#2", distance_km=4.04),
|
|
ocr.Clue(reference="Spotter#3", bearing_deg=298.0),
|
|
]
|
|
|
|
|
|
def test_compass_word_bearings_carry_a_tolerance():
|
|
text = """Enemy Field Command:
|
|
Spotter#1: West
|
|
Spotter#2: North-West
|
|
Spotter#3: North Northwest
|
|
"""
|
|
info = ocr.parse_text(text)
|
|
_, clues, *_ = info.targets[(TargetType.ENEMY, "FieldCommand")]
|
|
by_ref = {c.reference: c for c in clues}
|
|
assert by_ref["Spotter#1"].bearing_deg == 270.0
|
|
assert by_ref["Spotter#2"].bearing_deg == 315.0
|
|
assert by_ref["Spotter#3"].bearing_deg == 337.5
|
|
for clue in clues:
|
|
assert clue.bearing_tolerance_deg == 11.25
|
|
|
|
|
|
def test_grid_only_coord_no_sub_position():
|
|
text = "Enemy Signal Station:\n Reported active in grid D10"
|
|
info = ocr.parse_text(text)
|
|
_, clues, coord, *_ = info.targets[(TargetType.ENEMY, "SignalStation")]
|
|
assert coord == Coord("D", 10, 5, 5)
|