OCR: parse train-arrival intel into MainStation + waypoint RPs
A different intel shape entirely: a station's absolute grid ref, the rail's bearing from it, and waypoints given only as a distance along that same bearing. ocr.parse_train_intel() turns the station and each waypoint into RP-shaped entries (bearing+distance-from-station is solve_location()'s simplest case), merged through the exact same _merge_reference_points() path as any other RP, no new UI or entity type needed. The T=HH:MM:SS timestamp on every line is deliberately never parsed, there's no game clock to compare it against. Verified end to end against the real screenshot: MainStation resolves from its own grid ref, and all three waypoints resolve correctly along the bearing at their reported distances.
This commit is contained in:
parent
980be86908
commit
82b3fca0a0
@ -388,6 +388,98 @@ def parse_intel_blocks(text: str) -> list[dict]:
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
# Train-arrival intel is a different shape entirely, a station's absolute
|
||||||
|
# grid ref, the rail's bearing from it, and a handful of waypoints given
|
||||||
|
# only as a distance along that same bearing, plus a T=HH:MM:SS timestamp
|
||||||
|
# on every line that this app makes no attempt to use, there's no game
|
||||||
|
# clock to compare it against, so it's simply ignored:
|
||||||
|
# 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
|
||||||
|
# The station and each waypoint all come out RP-shaped (kind='rp'), same
|
||||||
|
# as parse_intel_blocks()'s entries, so app.py can merge them the exact
|
||||||
|
# same way, no separate code path needed on that side.
|
||||||
|
_ARRIVAL_STATION_KEYWORD = "ARRIVAL STATION"
|
||||||
|
_TRACK_ALIGNMENT_KEYWORD = "TRACK ALIGNMENT"
|
||||||
|
_FINAL_APPROACH_KEYWORD = "FINAL APPROACH"
|
||||||
|
|
||||||
|
_STATION_LINE_RE = re.compile(
|
||||||
|
rf"(\S+):\s*([A-T])\s*({_DIGIT_CLASS}{{1,2}})\s+({_DIGIT_CLASS})\s*[:;.,]\s*({_DIGIT_CLASS})"
|
||||||
|
)
|
||||||
|
_WAYPOINT_RE = re.compile(r"Waypoint\s+(\S+)\s*-\s*([\d.]+)\s*k?m\s+from\s+(\S+)", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _section(text: str, start_keyword: str, end_keywords: tuple[str, ...]) -> str | None:
|
||||||
|
"""Text between a fuzzy match of `start_keyword` and whichever comes
|
||||||
|
first: a block-end '.' line, one of `end_keywords`, or the end of the
|
||||||
|
text. None if `start_keyword` isn't found at all. Scopes each
|
||||||
|
train-intel sub-parser to just its own section, so e.g. a
|
||||||
|
coincidental ':' + coord shape elsewhere in the text can't be
|
||||||
|
mistaken for the arrival station line."""
|
||||||
|
lines = text.splitlines()
|
||||||
|
start = next((i + 1 for i, line in enumerate(lines) if _fuzzy_contains(line.strip(), start_keyword)), None)
|
||||||
|
if start is None:
|
||||||
|
return None
|
||||||
|
collected = []
|
||||||
|
for line in lines[start:]:
|
||||||
|
stripped = line.strip()
|
||||||
|
if _BLOCK_END_RE.match(stripped) or any(_fuzzy_contains(stripped, k) for k in end_keywords):
|
||||||
|
break
|
||||||
|
collected.append(line)
|
||||||
|
return "\n".join(collected)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_train_intel(text: str) -> list[dict]:
|
||||||
|
"""Parse a train-arrival intel block into RP-shaped entries: the
|
||||||
|
named station itself (absolute grid ref) plus one entry per waypoint,
|
||||||
|
each resolved via bearing (the rail's alignment) + distance (that
|
||||||
|
waypoint's distance along it) from the station, solve_location()'s
|
||||||
|
simplest case, one clue with both. Returns [] if this doesn't look
|
||||||
|
like a train intel block at all (no ARRIVAL STATION section)."""
|
||||||
|
station_block = _section(text, _ARRIVAL_STATION_KEYWORD, (_TRACK_ALIGNMENT_KEYWORD, _FINAL_APPROACH_KEYWORD))
|
||||||
|
if station_block is None:
|
||||||
|
return []
|
||||||
|
station_m = _STATION_LINE_RE.search(station_block)
|
||||||
|
if station_m is None:
|
||||||
|
return []
|
||||||
|
station_name, letter, y, x, yy = station_m.groups()
|
||||||
|
try:
|
||||||
|
station_coord = Coord(X=letter.upper(), Y=int(_fix_digits(y)), x=int(_fix_digits(x)), y=int(_fix_digits(yy)))
|
||||||
|
except ValueError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
entries = [{
|
||||||
|
"kind": "rp", "name": station_name, "type_word": None, "id": None,
|
||||||
|
"raw": station_block.strip(), "clues": [], "coord": station_coord,
|
||||||
|
}]
|
||||||
|
|
||||||
|
track_block = _section(text, _TRACK_ALIGNMENT_KEYWORD, (_FINAL_APPROACH_KEYWORD,))
|
||||||
|
track_clues = _parse_all_clues(track_block) if track_block else []
|
||||||
|
bearing = next((c.bearing_deg for c in track_clues if c.bearing_deg is not None), None)
|
||||||
|
if bearing is None:
|
||||||
|
return entries # the station alone is still useful even without the rail's bearing
|
||||||
|
|
||||||
|
approach_block = _section(text, _FINAL_APPROACH_KEYWORD, ())
|
||||||
|
if approach_block is None:
|
||||||
|
return entries
|
||||||
|
|
||||||
|
for m in _WAYPOINT_RE.finditer(approach_block):
|
||||||
|
letter_id, distance, ref_word = m.groups()
|
||||||
|
ref = station_name if ref_word.strip(".:").lower() == "station" else _clean_reference(ref_word)
|
||||||
|
entries.append({
|
||||||
|
"kind": "rp", "name": f"Waypoint {letter_id}", "type_word": None, "id": None,
|
||||||
|
"raw": m.group(0).strip(),
|
||||||
|
"clues": [Clue(reference=ref, bearing_deg=bearing, distance_km=float(_fix_digits(distance)))],
|
||||||
|
"coord": None,
|
||||||
|
})
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
# Destruction reports are standalone one-liners, not tied to a block:
|
# Destruction reports are standalone one-liners, not tied to a block:
|
||||||
# "SupplyCache#2 Destroyed. Additional Requisition Granted."
|
# "SupplyCache#2 Destroyed. Additional Requisition Granted."
|
||||||
# "Direct Hit! HostileTank#3 Destroyed."
|
# "Direct Hit! HostileTank#3 Destroyed."
|
||||||
@ -466,6 +558,9 @@ def parse_text(text: str) -> ParsedInfo:
|
|||||||
continue
|
continue
|
||||||
info.targets[(target_type, entry["id"])] = (entry["raw"], entry["clues"], entry["coord"])
|
info.targets[(target_type, entry["id"])] = (entry["raw"], entry["clues"], entry["coord"])
|
||||||
|
|
||||||
|
for entry in parse_train_intel(text):
|
||||||
|
info.reference_points[entry["name"]] = (entry["raw"], entry["clues"], entry["coord"])
|
||||||
|
|
||||||
info.destroyed = parse_destroyed(text)
|
info.destroyed = parse_destroyed(text)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user