orbit_model.py owns the shared state dataclasses (representation only).
orbit_physics.py builds and transforms that model with engine-matched helpers
for fleet speed, sun collision, position prediction, lead-shot planning,
in-flight fleet arrivals, and garrison projection.
Sibling docs: precomputed-table contents in precompute.md; allocation/repair boundary in repair.md; raw observation schema in agent.md; engine quirks in engine.md.
Model-only imports (orbit_model.py:117):
from orbit_model import GameState, PlanetStatePhysics helpers and parsing (orbit_physics.py:1128):
from orbit_physics import build_game_state, lead_shot
state = build_game_state(obs)
shot = lead_shot(state, source_id, target_id, ships)All dataclasses are frozen=True.
Arrival(turn, owner, ships)— fleet arrival event on a planet timeline (orbit_model.py:17).GarrisonPoint(turn, owner, ships)— projected owner/garrison after events atturnresolve (orbit_model.py:26).PlanetState(orbit_model.py:35):id, owner, x, y, radius, ships, production, orbiting, orbital_radius, initial_angle, is_comet, comet_path, comet_path_index, remaining_life, arrivals.posreturns(x, y).owner = -1is neutral;orbital_radiusis0.0andinitial_angleis0.0for static planets;remaining_lifeismath.inffor non-comets.FleetState(id, owner, x, y, angle, from_planet_id, ships)withposproperty (orbit_model.py:58).ShotPlan(angle, turns, target_x, target_y, distance, sun_blocked, path_blocked=False, comet_spawn_risk=False)(orbit_model.py:73).GameState(orbit_model.py:85):player, turn, turns_remaining, angular_velocity, planets: dict[int, PlanetState], fleets, upcoming_comet_spawns, sun_x=50.0, sun_y=50.0, sun_radius=10.0, max_speed=6.0, precomputed: Precomputed | None, comets_active: bool, arrivals_materialized: bool, _position_cache.precomputedand_position_cachearecompare=False, repr=False.position_at/garrison_atmethods atorbit_model.py:106/111delegate toorbit_physics.
BOARD_SIZE=100.0, CENTER=50.0, SUN_RADIUS=10.0,
ROTATION_RADIUS_LIMIT=50.0, LAUNCH_CLEARANCE=0.1, MAX_SHIP_SPEED=6.0,
TOTAL_STEPS=500, COMET_SPAWN_STEPS=(50,150,250,350,450),
LEAD_SHOT_ANGULAR_OFFSETS (9 values, orbit_physics.py:40),
SHOT_CACHE_TURN_RETENTION=8, COMET_RADIUS_BAND (34-row band table,
orbit_physics.py:42).
build_game_state(obs, *, total_steps=TOTAL_STEPS, max_speed=MAX_SHIP_SPEED, arrival_horizon=120, arrival_cache=None) (orbit_physics.py:1053) parses the
Kaggle observation into model dataclasses, builds/reuses the module-level
_PRECOMPUTED_CACHE keyed by game_fingerprint, prunes the stale shot cache,
then materializes observed in-flight fleet arrivals onto destination
PlanetState.arrivals. When arrival_cache is provided, non-negative fleet ids
read/write that table so live-agent turns can reuse predictions. Returned
states have arrivals_materialized=True; hand-built states default to False.
comets_active is set when any planet row is a comet.
reset_precomputed_cache() at orbit_physics.py:81 clears the precompute cache
(tests/tools).
Row parsing helpers accept either sequence rows or attribute-style objects via
_row_value (orbit_physics.py:94):
parse_planet(row, initial_by_id=None, comet_paths=None, arrivals=())(orbit_physics.py:993). Computesorbital_radiusandinitial_anglefrominitial_planets(when present), classifies orbiting viais_orbiting_position, and attaches comet path data when the planet id is incomet_paths.parse_fleet(row)(orbit_physics.py:1041).
| Function | Location | Notes |
|---|---|---|
distance(a, b) |
:100 |
math.hypot |
point_to_segment_distance(point, start, end) |
:104 |
engine-matched |
segment_hits_circle(start, end, center=(50,50), radius=10) |
:126 |
strict < radius |
sun_collision(start, end, sun_radius=10) |
:141 |
thin wrapper |
sun_line_clear(source, target, sun_radius=10) |
:149 |
static source-to-target only |
fleet_speed(ships, max_speed=6.0) |
:157 |
raises on ships < 1 |
launch_position(planet, angle) |
:213 |
radius + LAUNCH_CLEARANCE |
is_orbiting_position(x, y, radius) |
:218 |
dist + radius < 50 |
position_at(planet, future_turn, state=None) |
:222 |
comet → path index; static → fixed; orbiting → see below |
travel_distance_to_target(source, target_pos, target_radius=0) |
:291 |
returns (angle, travel_distance, end, straight_distance) |
fleet_eta(source, target, ships, state=None) |
:319 |
uses precomputed.travel_dist_same when available |
comet_spawn_risk_for_flight(state, launch_pos, angle, speed, eta) |
:378 |
sweeps state.upcoming_comet_spawns against COMET_RADIUS_BAND |
lead_shot(state, source_id, target_id, ships, max_iterations=5, tolerance=0.25, angular_offsets=LEAD_SHOT_ANGULAR_OFFSETS) |
:608 |
see below |
resolve_arrivals(owner, ships, arrivals) |
:796 |
engine combat rule for simultaneous arrivals |
project_garrison(planet, through_turn, current_turn=0) |
:830 |
production-before-combat, returns tuple[GarrisonPoint, ...] |
garrison_at(planet, future_turn, current_turn=0) |
:875 |
last entry of project_garrison |
predict_fleet_arrival(state, fleet, horizon=120) |
:910 |
exact engine swept-pair CCD, returns (planet_id, Arrival) or None |
target_arrival_phase_hits(state, fleet, target_id, arrival_turn) |
:953 |
target-specific swept-pair check for lead-shot repair |
- Cache:
state._position_cache[(planet_id, future_turn)]when the planet is the canonical row instate.planets. - Comets:
comet_path[comet_path_index + (future_turn - state.turn)], orNoneoutside the path. - Precomputed: if
state.precomputedcovers(planet_id, future_turn), the table row is used unlessnan. - Static: returns
(planet.x, planet.y). - Orbiting: engine applies rotation
max(0, step - 1)(orbit_physics.py:275), sofuture_rotation = max(0, future_turn - 1). With astate, the current position seeds the angle fromcurrent_rotation = max(0, state.turn - 1); without astate,initial_angle + angular_velocity * future_rotation.
speed = 1 + (max_speed - 1) * (log(ships) / log(1000)) ** 1.5, clipped to
max_speed. _speed(state, ships) at orbit_physics.py:171 prefers
state.precomputed.speed_table (clamped to 1000) when available.
_closed_form_outer_shot(:548) — static outer-outer corridor, returns immediately when the corridor is clear and no comet band intersects.- Shot cache lookup keyed by
_shot_cache_key(:413). Inner-inner pairs and safe outer-outer pairs use"free"keys (turn-invariant viaRotatingShotTemplate); other pairs use("turn", state.turn). Skip the"free"shortcut atstate.turn == 0(engine off-by-one). - Lead iteration (up to
max_iterations) withtolerance, then a wider horizon search over orbiting/comet targets, then validation acrossangular_offsets. Validation runspredict_fleet_arrival+target_arrival_phase_hits. The first hit returns a successfulShotPlan; otherwise apath_blocked=Trueplan at the natural angle. _cache_shot_plan(:503) storesShotPlanfor("turn", ...)keys and aRotatingShotTemplate(precomputed.RotatingShotTemplate) for inner-inner"free"keys.
Per-tick: advance fleet by speed, bound and sun-collision tests, then a
two-pass planet sweep (collision against the pre-move body position, then
against the planet's sweep into the next position). Returns the first match;
sun hits and out-of-bounds yield None.
- Collision predicates use strict
< radius(orbit_physics.py:138); tangents do not collide. - Production applies before arrivals/combat each projected turn
(
orbit_physics.py:853). - Capture requires the garrison to drop strictly below zero; equality leaves
owner unchanged with
0ships (orbit_physics.py:818). - Orbiting planet positions recomputed from absolute step with the
max(0, step - 1)off-by-one (orbit_physics.py:275). lead_shotdoes not invent curved routes; blocked shots returnpath_blocked=True.
pytest -q tests/test_orbit_physics.py tests/test_transition.py