Browse Source

supply: simultaneously supply multiple planets

raylu 3 weeks ago
parent
commit
b329bcc774
2 changed files with 93 additions and 75 deletions
  1. 2 2
      pyproject.toml
  2. 91 73
      supply.py

+ 2 - 2
pyproject.toml

@@ -3,6 +3,6 @@ name = 'pruncalc'
 version = '0'
 requires-python = '>=3.13'
 dependencies = [
-    'cbor2',
-    'httpx',
+	'cbor2',
+	'httpx',
 ]

+ 91 - 73
supply.py

@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import dataclasses
 import sys
 import tomllib
 import typing
@@ -7,95 +8,112 @@ import typing
 import cache
 
 def main() -> None:
-	planet = sys.argv[1].casefold()
-	burn, raw_materials = fio_data(planet)
-
-	inventory = {item['MaterialTicker']: item['MaterialAmount'] for item in burn['Inventory']}
+	planet_names = sys.argv[1:]
+	planets = [Planet(fio_burn) for fio_burn in get_fio_burn(planet_names)]
+	raw_materials: typing.Sequence[Material] = cache.get('https://rest.fnar.net/material/allmaterials')
 	materials = {mat['Ticker']: mat for mat in raw_materials}
-	producing = {item['MaterialTicker']: item for item in burn['OrderProduction']}
-	net_consumption = []
-	for c in burn['OrderConsumption'] + burn['WorkforceConsumption']:
-		net = c['DailyAmount']
-		if production := producing.get(c['MaterialTicker']):
-			net -= production['DailyAmount']
-			if net < 0:
-				continue
-		c['net_consumption'] = net
-		net_consumption.append(c)
-
-	vol_per_day = 0.0
-	weight_per_day = 0.0
+
 	target_days = float('inf')
-	for consumption in net_consumption:
-		ticker = consumption['MaterialTicker']
-		vol_per_day += materials[ticker]['Volume'] * consumption['net_consumption']
-		weight_per_day += materials[ticker]['Weight'] * consumption['net_consumption']
-		days = inventory.get(ticker, 0) / consumption['net_consumption']
-		if days < target_days:
-			target_days = days
-
-	print(f'consuming {vol_per_day:.1f}㎥/d')
-	print(f'consuming {weight_per_day:.1f}t/d')
+	for planet in planets:
+		vol_per_day = weight_per_day = 0
+		for consumption in planet.net_consumption:
+			ticker = consumption['MaterialTicker']
+			vol_per_day += materials[ticker]['Volume'] * consumption['net_consumption']
+			weight_per_day += materials[ticker]['Weight'] * consumption['net_consumption']
+			days = planet.inventory.get(ticker, 0) / consumption['net_consumption']
+			if days < target_days:
+				target_days = days
+
+		print(planet.name, f'consumes {vol_per_day:.1f}㎥, {weight_per_day:.1f}t per day')
+
+	load_more = True
+	optimal = dict.fromkeys(p.name for p in planets)
 	target_days = round(target_days + 0.05, 1)
-	while True:
-		buy, weight_used, volume_used = buy_for_target(materials, net_consumption, inventory, target_days)
-		if weight_used > 500 or volume_used > 500:
-			break
-		optimal = buy
-		target_days += 0.1
+	while load_more:
+		total_weight_used = total_volume_used = 0
+		for planet in planets:
+			buy, weight_used, volume_used = planet.buy_for_target(materials, target_days)
+			total_weight_used += weight_used
+			total_volume_used += volume_used
+			if total_weight_used > 500 or total_volume_used > 500:
+				load_more = False
+				break
+			optimal[planet.name] = buy
+			target_days += 0.1
 	print('supply for', round(target_days, 1), 'days')
 
-	for consumption in net_consumption:
-		ticker = consumption['MaterialTicker']
-		avail = inventory.get(ticker, 0)
-		daily_consumption = consumption['net_consumption']
-		days = avail / daily_consumption
-		print(f'{ticker:>3}: {avail:5d} ({daily_consumption:8.2f}/d) {days:4.1f} d', end='')
-		if need := optimal.get(ticker): # pyright: ignore[reportPossiblyUnboundVariable]
-			print(f' | {need:8.1f}')
-		else:
-			print()
-
-def fio_data(planet: str) -> tuple[PlanetData, typing.Sequence[Material]]:
+	for planet in planets:
+		print('\n' + planet.name)
+		for consumption in planet.net_consumption:
+			ticker = consumption['MaterialTicker']
+			avail = planet.inventory.get(ticker, 0)
+			daily_consumption = consumption['net_consumption']
+			days = avail / daily_consumption
+			print(f'{ticker:>3}: {avail:5d} ({daily_consumption:8.2f}/d) {days:4.1f} d', end='')
+			if need := optimal[planet.name].get(ticker): # pyright: ignore[reportOptionalMemberAccess]
+				print(f' | {need:8.1f}')
+			else:
+				print()
+
+def get_fio_burn(planet_names: typing.Sequence[str]) -> typing.Iterator[FIOBurn]:
 	with open('config.toml', 'rb') as f:
 		config = tomllib.load(f)
 
-	planets: list[PlanetData] = cache.get('https://rest.fnar.net/fioweb/burn/user/' + config['username'],
+	planets: list[FIOBurn] = cache.get('https://rest.fnar.net/fioweb/burn/user/' + config['username'],
 			headers={'Authorization': config['fio_api_key']})
-	for planet_data in planets:
-		name = planet_data['PlanetName']
-		if name.casefold() == planet:
-			assert planet_data['Error'] is None
-			break
-	else:
-		raise ValueError(planet + ' not found')
-
-	materials: list[Material] = cache.get('https://rest.fnar.net/material/allmaterials')
-	return planet_data, materials
-
-def buy_for_target(materials: dict[str, Material],  net_consumption: typing.Sequence[Amount], inventory: dict[str, int],
-		target_days: float) -> tuple[dict[str, float], float, float]:
-	weight_used = volume_used = 0
-	buy: dict[str, float] = {}
-	for consumption in net_consumption:
-		ticker = consumption['MaterialTicker']
-		avail = inventory.get(ticker, 0)
-		daily_consumption = consumption['net_consumption']
-		days = avail / daily_consumption
-		if days < target_days:
-			buy[ticker] = (target_days - days) * daily_consumption
-			weight_used += buy[ticker] * materials[ticker]['Weight']
-			volume_used += buy[ticker] * materials[ticker]['Volume']
-	return buy, weight_used, volume_used
-
-class PlanetData(typing.TypedDict):
+	for name in planet_names:
+		name = name.casefold()
+		for planet_data in planets:
+			if name in (planet_data['PlanetName'].casefold(), planet_data['PlanetNaturalId'].casefold()):
+				assert planet_data['Error'] is None
+				yield planet_data
+				break
+		else:
+			raise ValueError(name + ' not found')
+
+class FIOBurn(typing.TypedDict):
 	PlanetName: str
+	PlanetNaturalId: str
 	Error: typing.Any
 	OrderConsumption: list[Amount]
 	WorkforceConsumption: list[Amount]
 	Inventory: list[Inventory]
 	OrderProduction: list[Amount]
 
+@dataclasses.dataclass(init=False, eq=False, slots=True)
+class Planet:
+	name: str
+	inventory: dict[str, int]
+	net_consumption: typing.Sequence[Amount]
+
+	def __init__(self, fio_burn: FIOBurn) -> None:
+		self.name = fio_burn['PlanetName'] or fio_burn['PlanetNaturalId']
+		self.inventory = {item['MaterialTicker']: item['MaterialAmount'] for item in fio_burn['Inventory']}
+		producing = {item['MaterialTicker']: item for item in fio_burn['OrderProduction']}
+		self.net_consumption = []
+		for c in fio_burn['OrderConsumption'] + fio_burn['WorkforceConsumption']:
+			net = c['DailyAmount']
+			if production := producing.get(c['MaterialTicker']):
+				net -= production['DailyAmount']
+				if net < 0:
+					continue
+			c['net_consumption'] = net
+			self.net_consumption.append(c)
+
+	def buy_for_target(self, materials: dict[str, Material], target_days: float) -> tuple[dict[str, float], float, float]:
+		weight_used = volume_used = 0
+		buy: dict[str, float] = {}
+		for consumption in self.net_consumption:
+			ticker = consumption['MaterialTicker']
+			avail = self.inventory.get(ticker, 0)
+			daily_consumption = consumption['net_consumption']
+			days = avail / daily_consumption
+			if days < target_days:
+				buy[ticker] = (target_days - days) * daily_consumption
+				weight_used += buy[ticker] * materials[ticker]['Weight']
+				volume_used += buy[ticker] * materials[ticker]['Volume']
+		return buy, weight_used, volume_used
+
 class Amount(typing.TypedDict):
 	MaterialTicker: str
 	DailyAmount: float