shipbuilding.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. from __future__ import annotations
  2. import collections
  3. import typing
  4. import cache
  5. import roi
  6. BUY = frozenset([
  7. # definitely buy
  8. 'C', 'FLX', 'H', 'H2O', 'HAL', 'HCP', 'HE', 'LST', 'MG', 'N', 'NA', 'NCS', 'NS', 'O', 'PE', 'PG', 'S', 'TCL', 'THF',
  9. # maybe buy
  10. 'AIR', 'AU', 'BE', 'BRM', 'CU', 'FAN', 'FC', 'FE', 'HCC', 'HD', 'LDI', 'LI', 'MFK', 'MWF', 'REA', 'RG', 'RGO', 'ROM', 'SFK', 'SI', 'STL', 'TI', 'TPU',
  11. # import
  12. 'AAR', 'AWF', 'CAP', 'CF',
  13. # skip
  14. 'LFE', 'LHP', 'MFE', 'SFE', 'SSC',
  15. ])
  16. def main() -> None:
  17. blueprint = {
  18. 'FFC': 1,
  19. 'FSE': 1,
  20. 'LFE': 2,
  21. 'MFE': 2,
  22. 'QCR': 1,
  23. 'SFE': 1,
  24. 'LCB': 1,
  25. 'MFL': 1,
  26. 'MSL': 1,
  27. 'LHP': 94,
  28. 'SSC': 128,
  29. 'BR1': 1,
  30. 'CQM': 1,
  31. }
  32. prices: typing.Mapping[str, RawPrice] = {p['MaterialTicker']: p
  33. for p in cache.get('https://refined-prun.github.io/refined-prices/all.json') if p['ExchangeCode'] == 'IC1'}
  34. recipes = recipe_for_mats()
  35. production: dict[str, dict[str, float]] = collections.defaultdict(lambda: collections.defaultdict(float))
  36. buy: dict[str, float] = collections.defaultdict(float)
  37. cost = 0.0
  38. for mat, amount in blueprint.items():
  39. cost += analyze_mat(0, mat, amount, production, buy, prices, recipes)
  40. print()
  41. print(f'total cost: {cost:,}')
  42. buildings: typing.Sequence[roi.Building] = cache.get('https://api.prunplanner.org/data/buildings/', expiry=cache.ONE_DAY)
  43. expertise = collections.defaultdict(list)
  44. for building in buildings:
  45. if building['building_ticker'] in production:
  46. expertise[building['expertise']].append(building['building_ticker'])
  47. for expertise, buildings in expertise.items():
  48. print(expertise)
  49. for building in buildings:
  50. print(f'\t{building:3}:', end='')
  51. for mat, amount in production[building].items():
  52. traded = prices[mat]['AverageTraded30D'] or 0
  53. if traded > amount * 2:
  54. print(f' {amount:g}×\033[32m{mat}\033[0m', end='')
  55. else:
  56. print(f' {amount:g}×\033[31m{mat}\033[0m', end='')
  57. print()
  58. print('buy')
  59. for mat, amount in buy.items():
  60. price = prices[mat]
  61. print(f'{amount:g}×{mat}', price['Demand'])
  62. def recipe_for_mats() -> dict[str, roi.Recipe]:
  63. all_recipes: list[roi.Recipe] = cache.get('https://api.prunplanner.org/data/recipes/', expiry=cache.ONE_DAY)
  64. mat_recipes = collections.defaultdict(list) # all ways to make a mat
  65. for recipe in all_recipes:
  66. for output in recipe['outputs']:
  67. mat_recipes[output['material_ticker']].append(recipe)
  68. mat_recipe: dict[str, roi.Recipe] = {} # mats for which there's only one recipe to make
  69. for mat, recipes in mat_recipes.items():
  70. if len(recipes) == 1:
  71. mat_recipe[mat] = recipes[0]
  72. return mat_recipe
  73. def analyze_mat(level: int, mat: str, amount: float, production: dict[str, dict[str, float]], buy: dict[str, float],
  74. prices: typing.Mapping[str, RawPrice], recipes: dict[str, roi.Recipe]) -> float:
  75. price = prices[mat]
  76. traded = price['AverageTraded30D'] or 0
  77. if mat in BUY:
  78. print('\t' * level + f'{amount:g}×{mat} buy: {price["Ask"]}, daily traded {traded:5.1f}')
  79. buy[mat] += amount
  80. assert price['Ask'] is not None
  81. return price['Ask'] * amount
  82. else:
  83. if (recipe := recipes.get(mat)) is None:
  84. print('\t' * level + f'{amount:g}×{mat} make (unknown recipe)')
  85. return 0
  86. else:
  87. building = recipe['building_ticker']
  88. production[building][mat] += amount
  89. liquid = '\033[31mnot liquid\033[0m'
  90. if traded > amount * 2:
  91. liquid = '\033[32mliquid\033[0m'
  92. print('\t' * level + f'{amount:g}×{mat} make ({building}, {liquid})')
  93. total_cost = 0.0
  94. for input_mat in recipe['inputs']:
  95. input_amount = input_mat['material_amount'] * amount / recipe['outputs'][0]['material_amount']
  96. total_cost += analyze_mat(level + 1, input_mat['material_ticker'], input_amount, production, buy, prices, recipes)
  97. print('\t' * level + f'\tcost: {total_cost:,.0f}')
  98. return total_cost
  99. class RawPrice(typing.TypedDict):
  100. MaterialTicker: str
  101. ExchangeCode: str
  102. Ask: float | None
  103. AverageTraded30D: float | None # averaged daily traded volume over last 30 days
  104. Supply: int
  105. Demand: int
  106. if __name__ == '__main__':
  107. main()