supply.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. from __future__ import annotations
  2. import dataclasses
  3. import sys
  4. import tomllib
  5. import typing
  6. import cache
  7. def main() -> None:
  8. planet_names = sys.argv[1:]
  9. planets = [Planet(fio_burn) for fio_burn in get_fio_burn(planet_names)]
  10. raw_materials: typing.Sequence[Material] = cache.get('https://rest.fnar.net/material/allmaterials')
  11. materials = {mat['Ticker']: mat for mat in raw_materials}
  12. target_days = float('inf')
  13. for planet in planets:
  14. vol_per_day = weight_per_day = 0
  15. for consumption in planet.net_consumption:
  16. ticker = consumption['MaterialTicker']
  17. vol_per_day += materials[ticker]['Volume'] * consumption['net_consumption']
  18. weight_per_day += materials[ticker]['Weight'] * consumption['net_consumption']
  19. days = planet.inventory.get(ticker, 0) / consumption['net_consumption']
  20. if days < target_days:
  21. target_days = days
  22. print(planet.name, f'consumes {vol_per_day:.1f}㎥, {weight_per_day:.1f}t per day')
  23. load_more = True
  24. optimal = dict.fromkeys(p.name for p in planets)
  25. target_days = round(target_days + 0.05, 1)
  26. while load_more:
  27. total_weight_used = total_volume_used = 0
  28. for planet in planets:
  29. buy, weight_used, volume_used = planet.buy_for_target(materials, target_days)
  30. total_weight_used += weight_used
  31. total_volume_used += volume_used
  32. if total_weight_used > 500 or total_volume_used > 500:
  33. load_more = False
  34. break
  35. optimal[planet.name] = buy
  36. target_days += 0.1
  37. print('supply for', round(target_days, 1), 'days')
  38. for planet in planets:
  39. print('\n' + planet.name)
  40. for consumption in planet.net_consumption:
  41. ticker = consumption['MaterialTicker']
  42. avail = planet.inventory.get(ticker, 0)
  43. daily_consumption = consumption['net_consumption']
  44. days = avail / daily_consumption
  45. print(f'{ticker:>3}: {avail:5d} ({daily_consumption:8.2f}/d) {days:4.1f} d', end='')
  46. if need := optimal[planet.name].get(ticker): # pyright: ignore[reportOptionalMemberAccess]
  47. print(f' | {need:8.1f}')
  48. else:
  49. print()
  50. def get_fio_burn(planet_names: typing.Sequence[str]) -> typing.Iterator[FIOBurn]:
  51. with open('config.toml', 'rb') as f:
  52. config = tomllib.load(f)
  53. planets: list[FIOBurn] = cache.get('https://rest.fnar.net/fioweb/burn/user/' + config['username'],
  54. headers={'Authorization': config['fio_api_key']})
  55. for name in planet_names:
  56. name = name.casefold()
  57. for planet_data in planets:
  58. if name in (planet_data['PlanetName'].casefold(), planet_data['PlanetNaturalId'].casefold()):
  59. assert planet_data['Error'] is None
  60. yield planet_data
  61. break
  62. else:
  63. raise ValueError(name + ' not found')
  64. class FIOBurn(typing.TypedDict):
  65. PlanetName: str
  66. PlanetNaturalId: str
  67. Error: typing.Any
  68. OrderConsumption: list[Amount]
  69. WorkforceConsumption: list[Amount]
  70. Inventory: list[Inventory]
  71. OrderProduction: list[Amount]
  72. @dataclasses.dataclass(init=False, eq=False, slots=True)
  73. class Planet:
  74. name: str
  75. inventory: dict[str, int]
  76. net_consumption: typing.Sequence[Amount]
  77. def __init__(self, fio_burn: FIOBurn) -> None:
  78. self.name = fio_burn['PlanetName'] or fio_burn['PlanetNaturalId']
  79. self.inventory = {item['MaterialTicker']: item['MaterialAmount'] for item in fio_burn['Inventory']}
  80. producing = {item['MaterialTicker']: item for item in fio_burn['OrderProduction']}
  81. self.net_consumption = []
  82. for c in fio_burn['OrderConsumption'] + fio_burn['WorkforceConsumption']:
  83. net = c['DailyAmount']
  84. if production := producing.get(c['MaterialTicker']):
  85. net -= production['DailyAmount']
  86. if net < 0:
  87. continue
  88. c['net_consumption'] = net
  89. self.net_consumption.append(c)
  90. def buy_for_target(self, materials: dict[str, Material], target_days: float) -> tuple[dict[str, float], float, float]:
  91. weight_used = volume_used = 0
  92. buy: dict[str, float] = {}
  93. for consumption in self.net_consumption:
  94. ticker = consumption['MaterialTicker']
  95. avail = self.inventory.get(ticker, 0)
  96. daily_consumption = consumption['net_consumption']
  97. days = avail / daily_consumption
  98. if days < target_days:
  99. buy[ticker] = (target_days - days) * daily_consumption
  100. weight_used += buy[ticker] * materials[ticker]['Weight']
  101. volume_used += buy[ticker] * materials[ticker]['Volume']
  102. return buy, weight_used, volume_used
  103. class Amount(typing.TypedDict):
  104. MaterialTicker: str
  105. DailyAmount: float
  106. net_consumption: float
  107. class Inventory(typing.TypedDict):
  108. MaterialTicker: str
  109. MaterialAmount: int
  110. class Material(typing.TypedDict):
  111. Ticker: str
  112. Weight: float
  113. Volume: float
  114. if __name__ == '__main__':
  115. main()