## Shared self-consistent constant-velocity forecast used by the Linear gun and ## the GuessFactor family (guess_factor, decay_gf, knn_gun). ## ## Why this exists: the virtual-bullet metric resolves a bullet when its travel ## distance reaches the distance to its aim point, then scores that single point ## against the enemy's position on that tick. A gun that places its aim point at ## the FIRE-time distance therefore stops at the wrong radius whenever the target ## has moved radially over the flight, and misses even when its angle is ## perfect. Measured symptoms: ## * the whole GF family scored ~0% on the circular/wall-bounce/random-walk ## fixtures while the model-fitting guns scored 40-100%; ## * the Linear gun scored 87% (p3.0 = 63/100) on the constant-velocity ## fixture where a self-consistent forecast scores 100%. ## The GF angle range was never clamped (0/837 shots), so the earlier ## "MEA too narrow" hypothesis was wrong. ## ## Fix: iterate the flight time until the predicted point sits at the distance ## the bullet actually travels (the same fixed point circular.nim uses). The GF ## family additionally measures its histogram as the residual of the actual ## bearing against this forecast's bearing, so it learns the deviation from a ## base model instead of having to encode the whole lead angle. ## ## Coordinate system: 0° = East, CCW positive (Tank Royale standard). import std/math import gun_harness/gun_interface type BaseForecast* = object x*, y*: float ## absolute predicted enemy position dist*: float ## distance from shooter to the predicted position bearing*: float ## bearing from shooter to the predicted position (rad) proc forecastLinear*(state: WorldState, bulletSpeed: float): BaseForecast = ## Constant-velocity forecast with self-consistent flight time. The enemy is ## assumed to keep its current heading/speed; the flight time is the fixed ## point t = |predictedPos(t) - self| / bulletSpeed (5 iterations, matching ## circular.nim). Enemy speed (< 8 px/tick) is always below bulletSpeed ## (>= 11), so the iteration contracts. let d0 = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY) let hr = degToRad(state.enemyHeading) let v = state.enemySpeed var t = if bulletSpeed > 0.0: d0 / bulletSpeed else: 0.0 var ex = state.enemyX var ey = state.enemyY for _ in 0..4: ex = state.enemyX + cos(hr) * v * t ey = state.enemyY + sin(hr) * v * t if bulletSpeed > 0.0: t = hypot(ex - state.selfX, ey - state.selfY) / bulletSpeed result.x = ex result.y = ey result.dist = hypot(ex - state.selfX, ey - state.selfY) result.bearing = arctan2(ey - state.selfY, ex - state.selfX)