"""Firing-solution math, from High Command's gunnery tables. Distances/bearings reuse the same board-units-are-km convention as solver.py. Powder charge controls elevation: more charge, lower arc for the same distance, up to MAX_POWDER_CHARGE; min_powder_charge() is the least charge that can still reach a given distance at all. """ from __future__ import annotations import math from .models import Coord MAX_POWDER_CHARGE = 6 def distance_km_point(a: tuple[float, float], b: tuple[float, float]) -> float: return math.hypot(b[0] - a[0], b[1] - a[1]) def bearing_deg_point(a: tuple[float, float], b: tuple[float, float]) -> float: """Compass bearing from a to b: 0 = north (+row), 90 = east (+col), matching solver.py's convention, the inverse of solver.point_from_bearing_distance().""" return math.degrees(math.atan2(b[0] - a[0], b[1] - a[1])) % 360 def distance_km(a: Coord, b: Coord) -> float: return distance_km_point(a.as_fraction(), b.as_fraction()) def bearing_deg(a: Coord, b: Coord) -> float: return bearing_deg_point(a.as_fraction(), b.as_fraction()) def min_powder_charge(dist_km: float, eps: float = 1e-9) -> int: n = dist_km / 5 rounded = round(n) if abs(n - rounded) < eps: return max(1, rounded) return math.ceil(n) def elevation_deg(dist_km: float, charges: int) -> float: return 12 * dist_km / charges