Add runway tool with auto-detected inbound ships; fix multi-base state tagging

- tools/runway.py: days of runway per material before a base stalls, from simulate()'s real
  net consumption, base storage, plus (by default) any ship currently flying to that planet
- tools/state.py: tag each STORE entry with its planet (FIO AddressableId -> site SiteId) and
  each ship with its live flight origin/destination (keyed by ShipId, not StlFuelStoreId).
  Needed now that a 2nd base exists - storage/ship data was previously unscoped, which would
  have silently mixed bases' inventories together
- tests, docs updated (docs/library.md notes the multi-base tagging as a reusable pattern)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 21:56:18 +02:00
co-authored by Claude Sonnet 5
parent 58db08b921
commit b24f132210
6 changed files with 155 additions and 2 deletions
+45
View File
@@ -0,0 +1,45 @@
import importlib.util
from pathlib import Path
spec = importlib.util.spec_from_file_location("runway", Path(__file__).resolve().parent.parent / "tools" / "runway.py")
runway = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runway)
STATE = {
"storage": [
{"type": "STORE", "planet": "ZV-759c", "items": {"C": 6, "STL": 0}},
{"type": "STORE", "planet": "ZV-194a", "items": {"RAT": 30}}, # Nike's, must not leak into Deimos
{"type": "SHIP_STORE", "name": "AVI-07JFB", "items": {"C": 39, "STL": 15}},
{"type": "SHIP_STORE", "name": "AVI-07JFC", "items": {"MCG": 205}}, # not inbound to Deimos in this fixture
],
"ships": [
{"reg": "AVI-07JFB", "flight": {"destination": "Antares II (ZV-759) - Deimos (ZV-759c)"}},
{"reg": "AVI-07JFC", "flight": {"destination": "Antares III (ZV-194) - Nike (ZV-194a)"}},
{"reg": "AVI-IDLE", "flight": None},
],
}
def test_inbound_ships_matches_destination_planet():
assert runway.inbound_ships(STATE, "ZV-759c") == ["AVI-07JFB"]
assert runway.inbound_ships(STATE, "ZV-194a") == ["AVI-07JFC"]
def test_inbound_ships_ignores_ships_with_no_flight():
assert "AVI-IDLE" not in runway.inbound_ships(STATE, "ZV-759c")
def test_on_hand_scopes_store_to_the_right_planet():
have = runway.on_hand(STATE, "ZV-759c", ships=[])
assert have == {"C": 6, "STL": 0} # Nike's RAT must not appear here
def test_on_hand_adds_named_ship_cargo():
have = runway.on_hand(STATE, "ZV-759c", ships=["AVI-07JFB"])
assert have == {"C": 45, "STL": 15} # 6+39, 0+15
def test_on_hand_with_inbound_ships_end_to_end():
ships = runway.inbound_ships(STATE, "ZV-759c")
have = runway.on_hand(STATE, "ZV-759c", ships)
assert have == {"C": 45, "STL": 15} # only JFB counted, not JFC (heading to Nike)