galaxy.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. import jsonlines
  2. import os
  3. import logging
  4. import pendulum
  5. # from twisted.python import log
  6. from copy import deepcopy
  7. from pprint import pprint
  8. from colorama import Fore, Back, Style
  9. import config
  10. log = logging.getLogger(__name__)
  11. def merge(color_string):
  12. """ Given a string of colorama ANSI, merge them if you can. """
  13. return color_string.replace("m\x1b[", ";")
  14. PORT_CLASSES = {
  15. 1: "BBS",
  16. 2: "BSB",
  17. 3: "SBB",
  18. 4: "SSB",
  19. 5: "SBS",
  20. 6: "BSS",
  21. 7: "SSS",
  22. 8: "BBB",
  23. }
  24. CLASSES_PORT = {v: k for k, v in PORT_CLASSES.items()}
  25. class GameData(object):
  26. def __init__(self, usergame: tuple):
  27. # Construct the GameData storage object
  28. self.usergame = usergame
  29. self.warps = {}
  30. self.ports = {}
  31. self.busts = (
  32. {}
  33. ) # Added for evilTrade, just contains sector of port and datetime
  34. self.config = {}
  35. # 10 = 300 bytes
  36. self.warp_groups = 10
  37. # 3 = 560 bytes
  38. # 5 = 930 bytes
  39. self.port_groups = 3
  40. # Not sure, I don't think it will be big.
  41. self.bust_groups = 5
  42. self.activated = False # Did we activate the proxy? Not sure if this is the right spot to put it in.
  43. def storage_filename(self):
  44. """ return filename
  45. username.lower _ game.upper.json
  46. """
  47. user, game = self.usergame
  48. if "base" in config.config:
  49. base = config.config["base"] + "_"
  50. else:
  51. base = ""
  52. return "{0}{1}_{2}.json".format(base, user.lower(), game.upper())
  53. def reset_ports(self):
  54. self.ports = {}
  55. def reset_warps(self):
  56. self.warps = {}
  57. def reset_busts(self):
  58. # Ok well we won't ever use this since maint_busts() goes thru them all and filters out those that are 7 days or older
  59. self.busts = {}
  60. def special_ports(self):
  61. """ Save the special class ports 0, 9 """
  62. return {
  63. p: self.ports[p]
  64. for p in self.ports
  65. if "class" in self.ports[p] and self.ports[p]["class"] in (0, 9)
  66. }
  67. def display(self):
  68. pprint(self.warps)
  69. pprint(self.ports)
  70. pprint(self.busts)
  71. def save(self, *_):
  72. """ save gamedata as jsonlines.
  73. Enable sort_keys=True to provide stable json data output.
  74. We also sorted(.keys()) to keep records in order.
  75. Note: There's a "bug" when serializing to json, keys must be strings!
  76. """
  77. filename = self.storage_filename()
  78. with jsonlines.open(filename, mode="w", sort_keys=True) as writer:
  79. # for warp, sectors in self.warps.items():
  80. c = {"config": self.config}
  81. writer.write(c)
  82. w = {"warp": {}}
  83. for warp in sorted(self.warps.keys()):
  84. sectors = self.warps[warp]
  85. # log.debug("save:", warp, sectors)
  86. sects = sorted(list(sectors)) # make a list
  87. w["warp"][warp] = sects
  88. if len(w["warp"]) >= self.warp_groups:
  89. writer.write(w)
  90. w = {"warp": {}}
  91. yield
  92. # log.debug(w)
  93. # writer.write(w)
  94. # yield
  95. if len(w["warp"]) > 0:
  96. writer.write(w)
  97. # for sector, port in self.ports.items():
  98. p = {"port": {}}
  99. for sector in sorted(self.ports.keys()):
  100. port = self.ports[sector]
  101. p["port"][sector] = port
  102. if len(p["port"]) >= self.port_groups:
  103. writer.write(p)
  104. p = {"port": {}}
  105. yield
  106. if len(p["port"]) > 0:
  107. writer.write(p)
  108. # Added for evil
  109. b = {"busts": {}}
  110. for sector in sorted(self.busts.keys()):
  111. bust = self.busts[sector]
  112. b["busts"][sector] = bust
  113. if len(b["busts"]) >= self.bust_groups:
  114. writer.write(b)
  115. b = {"busts": {}}
  116. yield
  117. if len(b["busts"]) > 0:
  118. writer.write(b)
  119. log.info(
  120. "Saved {0} {1}/{2}/{3}/{4}".format(
  121. filename,
  122. len(self.ports),
  123. len(self.warps),
  124. len(self.config),
  125. len(self.busts),
  126. )
  127. )
  128. def untwisted_save(self, *_):
  129. """ save gamedata as jsonlines.
  130. Enable sort_keys=True to provide stable json data output.
  131. We also sorted(.keys()) to keep records in order.
  132. Note: There's a "bug" when serializing to json, keys must be strings!
  133. """
  134. filename = self.storage_filename()
  135. with jsonlines.open(filename, mode="w", sort_keys=True) as writer:
  136. # for warp, sectors in self.warps.items():
  137. c = {"config": self.config}
  138. writer.write(c)
  139. w = {"warp": {}}
  140. for warp in sorted(self.warps.keys()):
  141. sectors = self.warps[warp]
  142. # log.debug("save:", warp, sectors)
  143. sects = sorted(list(sectors)) # make a list
  144. w["warp"][warp] = sects
  145. if len(w["warp"]) >= self.warp_groups:
  146. writer.write(w)
  147. w = {"warp": {}}
  148. # log.debug(w)
  149. # writer.write(w)
  150. # yield
  151. if len(w["warp"]) > 0:
  152. writer.write(w)
  153. # for sector, port in self.ports.items():
  154. p = {"port": {}}
  155. for sector in sorted(self.ports.keys()):
  156. port = self.ports[sector]
  157. p["port"][sector] = port
  158. if len(p["port"]) >= self.port_groups:
  159. writer.write(p)
  160. p = {"port": {}}
  161. if len(p["port"]) > 0:
  162. writer.write(p)
  163. # Added for evil
  164. b = {"busts": {}}
  165. for sector in sorted(self.busts.keys()):
  166. bust = self.busts[sector]
  167. b["busts"][sector] = bust
  168. if len(b["busts"]) >= self.bust_groups:
  169. writer.write(b)
  170. b = {"busts": {}}
  171. if len(b["busts"]) > 0:
  172. writer.write(b)
  173. log.info(
  174. "Saved {0} {1}/{2}/{3}/{4}".format(
  175. filename,
  176. len(self.ports),
  177. len(self.warps),
  178. len(self.config),
  179. len(self.busts),
  180. )
  181. )
  182. def load(self):
  183. filename = self.storage_filename()
  184. self.warps = {}
  185. self.ports = {}
  186. self.config = {}
  187. self.busts = {}
  188. if os.path.exists(filename):
  189. # Load it
  190. with jsonlines.open(filename) as reader:
  191. for obj in reader:
  192. if "config" in obj:
  193. self.config.update(obj["config"])
  194. if "warp" in obj:
  195. for s, w in obj["warp"].items():
  196. # log.debug(s, w)
  197. self.warps[int(s)] = set(w)
  198. # self.warps.update(obj["warp"])
  199. if "port" in obj:
  200. for s, p in obj["port"].items():
  201. self.ports[int(s)] = p
  202. # self.ports.update(obj["port"])
  203. if "busts" in obj: # evil ports list
  204. for s, d in obj["busts"].items():
  205. self.busts[int(s)] = d
  206. yield
  207. # Clean Busts after loading
  208. self.maint_busts()
  209. log.info(
  210. "Loaded {0} {1}/{2}/{3}/{4}".format(
  211. filename,
  212. len(self.ports),
  213. len(self.warps),
  214. len(self.config),
  215. len(self.busts),
  216. )
  217. )
  218. def untwisted_load(self):
  219. """ Load file without twisted deferred
  220. This is for testing things out.
  221. """
  222. filename = self.storage_filename()
  223. self.warps = {}
  224. self.ports = {}
  225. self.config = {}
  226. self.busts = {}
  227. if os.path.exists(filename):
  228. # Load it
  229. with jsonlines.open(filename) as reader:
  230. for obj in reader:
  231. if "config" in obj:
  232. self.config.update(obj["config"])
  233. if "warp" in obj:
  234. for s, w in obj["warp"].items():
  235. # log.debug(s, w)
  236. self.warps[int(s)] = set(w)
  237. # self.warps.update(obj["warp"])
  238. if "port" in obj:
  239. for s, p in obj["port"].items():
  240. self.ports[int(s)] = p
  241. # self.ports.update(obj["port"])
  242. if "busts" in obj:
  243. for s, d in obj["busts"].items():
  244. self.busts[int(s)] = d
  245. # Clean Busts after loading
  246. self.maint_busts()
  247. log.info(
  248. "Loaded {0} {1}/{2}/{3}/{4}".format(
  249. filename,
  250. len(self.ports),
  251. len(self.warps),
  252. len(self.config),
  253. len(self.busts),
  254. )
  255. )
  256. def get_warps(self, sector: int):
  257. if sector in self.warps:
  258. return self.warps[sector]
  259. return None
  260. def warp_to(self, source: int, *dest):
  261. """ connect sector source to destination.
  262. """
  263. log.debug("Warp {0} to {1}".format(source, dest))
  264. if source not in self.warps:
  265. self.warps[source] = set()
  266. for d in dest:
  267. if d not in self.warps[source]:
  268. self.warps[source].add(d)
  269. def get_config(self, key, default=None):
  270. if key in self.config:
  271. return self.config[key]
  272. else:
  273. if default is not None:
  274. self.config[key] = default
  275. return default
  276. def set_config(self, key, value):
  277. self.config.update({key: value})
  278. def set_port(self, sector: int, data: dict):
  279. log.debug("Port {0} : {1}".format(sector, data))
  280. if sector not in self.ports:
  281. self.ports[sector] = dict()
  282. self.ports[sector].update(data)
  283. if "port" not in self.ports[sector]:
  284. # incomplete port type - can we "complete" it?
  285. if all(x in self.ports for x in ["fuel", "org", "equ"]):
  286. # We have all of the port types, so:
  287. port = "".join(
  288. [self.ports[sector][x]["sale"] for x in ["fuel", "org", "equ"]]
  289. )
  290. self.ports[sector]["port"] = port
  291. self.ports[sector]["class"] = CLASSES_PORT[port]
  292. log.debug("completed {0} : {1}".format(sector, self.ports[sector]))
  293. def set_bust(self, sect: int):
  294. # Given sector we add it to busts (avoid using these ports)
  295. log.debug("Bust {0}".format(sect))
  296. if sect not in self.busts:
  297. self.busts[sect] = str(pendulum.now())
  298. log.debug("Done {0} | {1}".format(sect, self.busts))
  299. else:
  300. log.debug("{0} already is in there".format(sect))
  301. def maint_busts(self):
  302. # Checks the current date to see if any busts need to be cleared
  303. rightNow = pendulum.now()
  304. dropped = 0 # Add in debug message so we can see if we are running correctly
  305. new = deepcopy(
  306. self.busts
  307. ) # Get essentially a backup, one we will be changing without iterating over
  308. for s in self.busts:
  309. d = self.busts[s] # Pull string of DateTime object
  310. d = pendulum.parse(d) # Convert string to DateTime obj
  311. d = d.add(7) # Add 7 days
  312. if d <= rightNow: # Compare
  313. del new[s] # remove it from a 2nd dict
  314. dropped += 1
  315. self.busts = new
  316. log.debug("Dropped {0} sectors from busted list".format(dropped))
  317. def port_buying(self, sector: int, cargo: str):
  318. """ Given a sector, is this port buying this?
  319. cargo is a char (F/O/E)
  320. """
  321. cargo_to_index = {"F": 0, "O": 1, "E": 2}
  322. cargo_types = ("fuel", "org", "equ")
  323. cargo = cargo[0]
  324. if sector not in self.ports:
  325. log.warn("port_buying( {0}, {1}): sector unknown!".format(sector, cargo))
  326. return False
  327. port = self.ports[sector]
  328. if "port" not in port:
  329. log.warn("port_buying( {0}, {1}): port unknown!".format(sector, cargo))
  330. return True
  331. if sector in self.busts: # Abort! This given sector is a busted port!
  332. log.warn(
  333. "port_buying({0}, {1}): sector contains a busted port!".format(
  334. sector, cargo
  335. )
  336. )
  337. return False
  338. cargo_index = cargo_to_index[cargo]
  339. if port["port"] in ("Special", "StarDock"):
  340. log.warn(
  341. "port_buying( {0}, {1}): not buying (is {2})".format(
  342. sector, cargo, port["port"]
  343. )
  344. )
  345. return False
  346. if port["port"][cargo_index] == "S":
  347. log.warn("port_buying( {0}, {1}): not buying cargo".format(sector, cargo))
  348. return False
  349. # ok, they buy it, but *WILL THEY* really buy it?
  350. cargo_key = cargo_types[cargo_index]
  351. if cargo_key in port:
  352. if int(port[cargo_key]["units"]) > 40:
  353. log.warn(
  354. "port_buying( {0}, {1}): Yes, buying {2}".format(
  355. sector, cargo, port[cargo_key]["sale"]
  356. )
  357. )
  358. return True
  359. else:
  360. log.warn(
  361. "port_buying( {0}, {1}): No, units < 40 {2}".format(
  362. sector, cargo, port[cargo_key]["sale"]
  363. )
  364. )
  365. return False
  366. else:
  367. log.warn(
  368. "port_buying( {0}, {1}): Yes, buying (but values unknown)".format(
  369. sector, cargo
  370. )
  371. )
  372. return True # unknown port, we're guess yes.
  373. return False
  374. @staticmethod
  375. def port_burnt(port: dict):
  376. """ Is this port burned out? """
  377. if all(x in port for x in ["fuel", "org", "equ"]):
  378. if all("pct" in port[x] for x in ["fuel", "org", "equ"]):
  379. if (
  380. port["equ"]["pct"] <= 20
  381. or port["fuel"]["pct"] <= 20
  382. or port["org"]["pct"] <= 20
  383. ):
  384. return True
  385. return False
  386. # Since we don't have any port information, hope for the best, assume it isn't burnt.
  387. return False
  388. @staticmethod
  389. def flip(buy_sell):
  390. # Invert B's and S's to determine if we can trade or not between ports.
  391. return buy_sell.replace("S", "W").replace("B", "S").replace("W", "B")
  392. @staticmethod
  393. def port_trading(port1, port2):
  394. # Given the port settings, can we trade between these?
  395. if port1 == port2:
  396. return False
  397. if port1 in ("Special", "StarDock") or port2 in ("Special", "StarDock"):
  398. return False
  399. # Oops, hey, we are given port settings not a sector a port is in,
  400. # So don't try to check it against the busted list.
  401. p1 = [c for c in port1]
  402. p2 = [c for c in port2]
  403. rem = False
  404. for i in range(3):
  405. if p1[i] == p2[i]:
  406. p1[i] = "X"
  407. p2[i] = "X"
  408. rem = True
  409. if rem:
  410. j1 = "".join(p1).replace("X", "")
  411. j2 = "".join(p2).replace("X", "")
  412. if j1 == "BS" and j2 == "SB":
  413. return True
  414. if j1 == "SB" and j2 == "BS":
  415. return True
  416. rport1 = GameData.flip(port1)
  417. c = 0
  418. match = []
  419. for i in range(3):
  420. if rport1[i] == port2[i]:
  421. match.append(port2[i])
  422. c += 1
  423. if c > 1:
  424. # Remove first match, flip it
  425. f = GameData.flip(match.pop(0))
  426. # Verify it is in there.
  427. # so we're not matching SSS/BBB
  428. if f in match:
  429. return True
  430. return False
  431. return False
  432. @staticmethod
  433. def color_pct(pct: int):
  434. if pct > 50:
  435. # green
  436. return "{0}{1:3}{2}".format(Fore.GREEN, pct, Style.RESET_ALL)
  437. elif pct > 25:
  438. return "{0}{1:3}{2}".format(
  439. merge(Fore.YELLOW + Style.BRIGHT), pct, Style.RESET_ALL
  440. )
  441. else:
  442. return "{0:3}".format(pct)
  443. @staticmethod
  444. def port_pct(port: dict):
  445. # Make sure these exist in the port data given.
  446. if all(x in port for x in ["fuel", "org", "equ"]):
  447. return "{0},{1},{2}%".format(
  448. GameData.color_pct(port["fuel"]["pct"]),
  449. GameData.color_pct(port["org"]["pct"]),
  450. GameData.color_pct(port["equ"]["pct"]),
  451. )
  452. else:
  453. return "---,---,---%"
  454. @staticmethod
  455. def port_show_part(sector: int, sector_port: dict):
  456. return "{0:5} ({1}) {2}".format(
  457. sector, sector_port["port"], GameData.port_pct(sector_port)
  458. )
  459. def port_above(self, port: dict, limit: int) -> bool:
  460. if all(x in port for x in ["fuel", "org", "equ"]):
  461. if all(
  462. x in port and port[x]["pct"] >= limit for x in ["fuel", "org", "equ"]
  463. ):
  464. return True
  465. else:
  466. return False
  467. # Port is unknown, we'll assume it is above the limit.
  468. return True
  469. def port_trade_show(self, sector: int, warp: int, limit: int = 90):
  470. sector_port = self.ports[sector]
  471. warp_port = self.ports[warp]
  472. if self.port_above(sector_port, limit) and self.port_above(warp_port, limit):
  473. # sector_pct = GameData.port_pct(sector_port)
  474. # warp_pct = GameData.port_pct(warp_port)
  475. return "{0} \xae\xcd\xaf {1}".format(
  476. GameData.port_show_part(sector, sector_port),
  477. GameData.port_show_part(warp, warp_port),
  478. )
  479. return None
  480. @staticmethod
  481. def port_show(sector: int, sector_port: dict, warp: int, warp_port: dict):
  482. # sector_pct = GameData.port_pct(sector_port)
  483. # warp_pct = GameData.port_pct(warp_port)
  484. return "{0} -=- {1}".format(
  485. GameData.port_show_part(sector, sector_port),
  486. GameData.port_show_part(warp, warp_port),
  487. )
  488. def find_nearest_tradepairs(self, sector: int, obj):
  489. """ find nearest tradepair
  490. When do we use good? When do we use ok?
  491. """
  492. searched = set()
  493. if sector not in self.warps:
  494. log.warn(":Sector {0} not in warps.".format(sector))
  495. obj.target_sector = None
  496. return None
  497. if sector in self.busts:
  498. log.warn(":Sector {0} in busted".format(sector))
  499. obj.target_sector = None
  500. return None
  501. # Start with the current sector
  502. look = set((sector,))
  503. while len(look) > 0:
  504. log.warn("Searched [{0}]".format(searched))
  505. log.warn("Checking [{0}]".format(look))
  506. for s in look:
  507. if s in self.ports:
  508. # Ok, there's a port at least
  509. sp = self.ports[s]
  510. if sp["port"] in ("Special", "StarDock"):
  511. continue
  512. if self.port_burnt(sp):
  513. continue
  514. if "class" not in sp:
  515. continue
  516. if s in self.busts: # Check for busted port
  517. continue
  518. sc = sp["class"]
  519. if s not in self.warps:
  520. continue
  521. log.warn("{0} has warps {1}".format(s, self.warps[s]))
  522. # Ok, check for tradepairs.
  523. for w in self.warps[s]:
  524. if not w in self.warps:
  525. continue
  526. if not s in self.warps[w]:
  527. continue
  528. if not w in self.ports:
  529. continue
  530. # Ok, has possible port
  531. cp = self.ports[w]
  532. if cp["port"] in ("Special", "StarDock"):
  533. continue
  534. if self.port_burnt(cp):
  535. continue
  536. if "class" not in cp:
  537. continue
  538. if w in self.busts: # Check for busted
  539. continue
  540. cc = cp["class"]
  541. log.warn("{0} {1} - {2} {3}".format(s, sc, w, cc))
  542. if sc in (1, 5) and cc in (2, 4):
  543. # Good!
  544. log.warn("GOOD: {0}".format(s))
  545. obj.target_sector = s
  546. return s
  547. if sc in (2, 4) and cc in (1, 5):
  548. # Good!
  549. log.warn("GOOD: {0}".format(s))
  550. obj.target_sector = s
  551. return s
  552. # What about "OK" pairs?
  553. # Ok, not found here.
  554. searched.update(look)
  555. step_from = look
  556. look = set()
  557. for s in step_from:
  558. if s in self.warps:
  559. look.update(self.warps[s])
  560. # Look only contains warps we haven't searched
  561. look = look.difference(searched)
  562. yield
  563. obj.target_sector = None
  564. return None
  565. def find_nearest_evilpairs(self, sector: int, obj):
  566. """ find nearest evilpair
  567. XXB -=- XXB
  568. When do we use good? When do we use ok?
  569. """
  570. searched = set()
  571. if sector not in self.warps:
  572. log.warn(":Sector {0} not in warps.".format(sector))
  573. obj.target_sector = None
  574. return None
  575. if sector in self.busts:
  576. log.warn(":Sector {0} in busted".format(sector))
  577. obj.target_sector = None
  578. return None
  579. # Start with the current sector
  580. look = set((sector,))
  581. while len(look) > 0:
  582. log.warn("Searched [{0}]".format(searched))
  583. log.warn("Checking [{0}]".format(look))
  584. for s in look:
  585. if s in self.ports:
  586. # Ok, there's a port at least
  587. sp = self.ports[s]
  588. if sp["port"] in ("Special", "StarDock"):
  589. continue
  590. if self.port_burnt(sp):
  591. continue
  592. if "class" not in sp:
  593. continue
  594. if s in self.busts: # Check for busted port
  595. continue
  596. sc = sp["class"]
  597. if s not in self.warps:
  598. continue
  599. log.warn("{0} has warps {1}".format(s, self.warps[s]))
  600. # Ok, check for tradepairs.
  601. for w in self.warps[s]:
  602. if not w in self.warps:
  603. continue
  604. if not s in self.warps[w]:
  605. continue
  606. if not w in self.ports:
  607. continue
  608. # Ok, has possible port
  609. cp = self.ports[w]
  610. if cp["port"] in ("Special", "StarDock"):
  611. continue
  612. if self.port_burnt(cp):
  613. continue
  614. if "class" not in cp:
  615. continue
  616. if w in self.busts: # Check for busted
  617. continue
  618. cc = cp["class"]
  619. log.warn("{0} {1} - {2} {3}".format(s, sc, w, cc))
  620. if sc in (2, 3, 4, 8) and cc in (2, 3, 4, 8):
  621. # Good!
  622. log.warn("GOOD: {0}".format(s))
  623. obj.target_sector = s
  624. return s
  625. # What about "OK" pairs?
  626. # Ok, not found here.
  627. searched.update(look)
  628. step_from = look
  629. look = set()
  630. for s in step_from:
  631. if s in self.warps:
  632. look.update(self.warps[s])
  633. # Look only contains warps we haven't searched
  634. look = look.difference(searched)
  635. yield
  636. obj.target_sector = None
  637. return None
  638. def find_nearest_selling(
  639. self, sector: int, selling: str, at_least: int = 100
  640. ) -> int:
  641. """ find nearest port that is selling at_least amount of this item
  642. selling is 'f', 'o', or 'e'.
  643. """
  644. names = {"e": "equ", "o": "org", "f": "fuel"}
  645. pos = {"f": 0, "o": 1, "e": 2}
  646. sell = names[selling[0].lower()]
  647. s_pos = pos[selling[0].lower()]
  648. log.warn(
  649. "find_nearest_selling({0}, {1}, {2}): {3}, {4}".format(
  650. sector, selling, at_least, sell, s_pos
  651. )
  652. )
  653. searched = set()
  654. if sector not in self.warps:
  655. log.warn("( {0}, {1}): sector not in warps".format(sector, selling))
  656. return 0
  657. if sector in self.busts:
  658. log.warn(
  659. "({0}, {1}): sector is in busted ports list".format(sector, selling)
  660. )
  661. return 0
  662. # Start with the current sector
  663. look = set((sector,))
  664. while len(look) > 0:
  665. for s in look:
  666. if s in self.ports:
  667. # Ok, possibly?
  668. sp = self.ports[s]
  669. if sp["port"] in ("Special", "StarDock"):
  670. continue
  671. if s in self.busts: # Busted!
  672. continue
  673. if sp["port"][s_pos] == "S":
  674. # Ok, they are selling!
  675. if sell in sp:
  676. if int(sp[sell]["units"]) >= at_least:
  677. log.warn(
  678. "find_nearest_selling( {0}, {1}): {2} {3} units".format(
  679. sector, selling, s, sp[sell]["units"]
  680. )
  681. )
  682. return s
  683. else:
  684. # We know they sell it, but we don't know units
  685. log.warn(
  686. "find_nearest_selling( {0}, {1}): {2} {3}".format(
  687. sector, selling, s, sp["port"]
  688. )
  689. )
  690. return s
  691. # Ok, not found here. Branch out.
  692. searched.update(look)
  693. step_from = look
  694. look = set()
  695. for s in step_from:
  696. if s in self.warps:
  697. look.update(self.warps[s])
  698. # look only contains warps we haven't searched
  699. look = look.difference(searched)
  700. # Ok, we have run out of places to search
  701. log.warn("find_nearest_selling( {0}, {1}) : failed".format(sector, selling))
  702. return 0