galaxy.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. # processing the lines from the server ***
  2. if line.startswith("The shortest path (") or line.startswith(" TO > "):
  3. self.linestate = "warpline"
  4. self.lastwarp = 0
  5. elif line.startswith(" Items Status Trading % of max OnBoard"):
  6. self.linestate = "port"
  7. elif line.startswith("<Thievery>"):
  8. self.linestate = "thievery"
  9. elif self.linestate == "warpline":
  10. if line == "":
  11. self.linestate = ""
  12. else:
  13. self.warpline(line)
  14. elif self.linestate == "portcim" or self.linestate == "warpcim":
  15. if line == ": ENDINTERROG":
  16. self.linestate = ""
  17. elif line == ": ":
  18. self.linestate = "cim"
  19. elif line == "":
  20. self.linestate = ""
  21. else:
  22. if len(line) > 2:
  23. self.cimline(line)
  24. elif self.linestate == "cim":
  25. if line == ": ENDINTERROG" or line == "":
  26. self.linestate = ""
  27. elif len(line) > 2:
  28. if line.rstrip()[-1] == "%":
  29. self.linestate = "portcim"
  30. else:
  31. self.linestate = "warpcim"
  32. self.cimline(line)
  33. elif self.linestate == "thievery":
  34. self.thiefline(line)
  35. elif line == ": ":
  36. self.linestate = "cim"
  37. elif line.startswith("Sector : "):
  38. # Sector : 2565 in uncharted space.
  39. self.linestate = "sector"
  40. work = line.strip()
  41. parts = re.split(r"\s+", work)
  42. self.current_sector = int(parts[2])
  43. elif self.linestate == "sector":
  44. self.sectorline(line)
  45. elif self.linestate == "port":
  46. if line == "":
  47. self.linestate = ""
  48. else:
  49. self.portline(line)
  50. self.observer.emit("game-line", line)
  51. def portline(self, line: str):
  52. # Map these items to which keys
  53. self.log.debug("portline({0}): {1}".format(self.current_sector, line))
  54. mapto = {"Fuel": "fuel", "Organics": "org", "Equipment": "equ"}
  55. if "%" in line:
  56. # Fuel Ore Buying 2890 100% 0
  57. work = line.replace("Fuel Ore", "Fuel").replace("%", "")
  58. parts = re.split(r"\s+", work)
  59. data = {
  60. mapto[parts[0]]: {
  61. "sale": parts[1][0],
  62. "units": parts[2],
  63. "pct": int(parts[3]),
  64. }
  65. }
  66. # log.debug("Setting {0} to {1}".format(self.current_sector, data))
  67. self.gamedata.set_port(self.current_sector, data)
  68. # log.debug("NOW: {0}".format(self.gamedata.ports[self.current_sector]))
  69. def thiefline(self, line: str):
  70. self.log.debug("thiefline({0}): {1}".format(self.current_sector, line))
  71. if "Suddenly you're Busted!" in line:
  72. # Lets add it into the bust list
  73. self.gamedata.set_bust(self.current_sector)
  74. elif "(You realize the guards saw you last time!)" in line:
  75. self.linestate = ""
  76. def cimline(self, line: str):
  77. # log.debug(self.linestate, ":", line)
  78. if line[-1] == "%":
  79. self.linestate = "portcim"
  80. if self.linestate == "warpcim":
  81. # warps
  82. work = line.strip()
  83. if work != "":
  84. parts = re.split(r"(?<=\d)\s", work)
  85. parts = [int(x) for x in parts]
  86. sector = parts.pop(0)
  87. self.gamedata.warp_to(sector, *parts)
  88. elif self.linestate == "portcim":
  89. # ports
  90. work = line.replace("%", "")
  91. parts = re.parts = re.split(r"(?<=\d)\s", work)
  92. if len(parts) == 8:
  93. sector = int(parts[0].strip())
  94. data = dict()
  95. def portBS(info):
  96. if info[0] == "-":
  97. bs = "B"
  98. else:
  99. bs = "S"
  100. return (bs, int(info[1:].strip()))
  101. data["fuel"] = dict()
  102. data["fuel"]["sale"], data["fuel"]["units"] = portBS(parts[1])
  103. data["fuel"]["pct"] = int(parts[2].strip())
  104. data["org"] = dict()
  105. data["org"]["sale"], data["org"]["units"] = portBS(parts[3])
  106. data["org"]["pct"] = int(parts[4].strip())
  107. data["equ"] = dict()
  108. data["equ"]["sale"], data["equ"]["units"] = portBS(parts[5])
  109. data["equ"]["pct"] = int(parts[6].strip())
  110. # Store what this port is buying/selling
  111. data["port"] = (
  112. data["fuel"]["sale"] + data["org"]["sale"] + data["equ"]["sale"]
  113. )
  114. # Convert BBS/SBB to Class number 1-8
  115. data["class"] = CLASSES_PORT[data["port"]]
  116. self.gamedata.set_port(sector, data)
  117. else:
  118. self.linestate = "cim"
  119. def sectorline(self, line: str):
  120. self.log.debug("sector: {0} : {1}".format(self.current_sector, line))
  121. if line.startswith("Beacon : "):
  122. pass # get beacon text
  123. elif line.startswith("Ports : "):
  124. # Ports : Ballista, Class 1 (BBS)
  125. self.sector_state = "port"
  126. if "<=-DANGER-=>" in line:
  127. # Port is destroyed
  128. if self.current_sector in self.gamedata.ports:
  129. del self.gamedata.ports[self.current_sector]
  130. # elif "(StarDock)" not in line:
  131. # Ports : Stargate Alpha I, Class 9 (Special) (StarDock)
  132. else:
  133. _, _, class_port = line.partition(", Class ")
  134. c, port = class_port.split(" ", maxsplit=1)
  135. c = int(c)
  136. if "StarDock" in port:
  137. port = "StarDock"
  138. port = port.replace("(", "").replace(")", "")
  139. data = {"port": port, "class": c}
  140. self.gamedata.set_port(self.current_sector, data)
  141. elif line.startswith("Planets : "):
  142. # Planets : (O) Flipper
  143. self.sector_state = "planet"
  144. elif line.startswith("Traders : "):
  145. self.sector_state = "trader"
  146. elif line.startswith("Ships : "):
  147. self.sector_state = "ship"
  148. elif line.startswith("Fighters: "):
  149. self.sector_state = "fighter"
  150. elif line.startswith("NavHaz : "):
  151. pass
  152. elif line.startswith("Mines : "):
  153. self.sector_state = "mine"
  154. elif line.startswith(" "):
  155. # continues
  156. if self.sector_state == "mines":
  157. pass
  158. if self.sector_state == "planet":
  159. pass
  160. if self.sector_state == "trader":
  161. pass
  162. if self.sector_state == "ship":
  163. elif len(line) > 8 and line[8] == ":":
  164. self.sector_state = "normal"
  165. elif line.startswith("Warps to Sector(s) :"):
  166. # Warps to Sector(s) : 5468
  167. _, _, work = line.partition(":")
  168. # TO FIX: We are interested in (unexplored) sectors.
  169. work = work.strip().replace("(", "").replace(")", "").replace(" - ", " ")
  170. parts = [int(x) for x in work.split(" ")]
  171. self.log.debug("Sectorline warps {0}".format(parts))
  172. self.gamedata.warp_to(self.current_sector, *parts)
  173. self.sector_state = "normal"
  174. self.linestate = ""
  175. # gamedata
  176. PORT_CLASSES = {
  177. 1: "BBS",
  178. 2: "BSB",
  179. 3: "SBB",
  180. 4: "SSB",
  181. 5: "SBS",
  182. 6: "BSS",
  183. 7: "SSS",
  184. 8: "BBB",
  185. }
  186. CLASSES_PORT = {v: k for k, v in PORT_CLASSES.items()}
  187. class GameData(object):
  188. def __init__(self, usergame: tuple):
  189. # Construct the GameData storage object
  190. self.usergame = usergame
  191. self.warps = {}
  192. self.ports = {}
  193. self.busts = (
  194. {}
  195. ) # Added for evilTrade, just contains sector of port and datetime
  196. self.config = {}
  197. # 10 = 300 bytes
  198. self.warp_groups = 10
  199. # 3 = 560 bytes
  200. # 5 = 930 bytes
  201. self.port_groups = 3
  202. # Not sure, I don't think it will be big.
  203. self.bust_groups = 5
  204. self.activated = False # Did we activate the proxy? Not sure if this is the right spot to put it in.
  205. def reset_ports(self):
  206. self.ports = {}
  207. def reset_warps(self):
  208. self.warps = {}
  209. def reset_busts(self):
  210. # 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
  211. self.busts = {}
  212. def special_ports(self):
  213. """ Save the special class ports 0, 9 """
  214. return {
  215. p: self.ports[p]
  216. for p in self.ports
  217. if "class" in self.ports[p] and self.ports[p]["class"] in (0, 9)
  218. }
  219. def save(self, *_):
  220. def load(self):
  221. def get_warps(self, sector: int):
  222. if sector in self.warps:
  223. return self.warps[sector]
  224. return None
  225. def warp_to(self, source: int, *dest):
  226. """ connect sector source to destination.
  227. """
  228. log.debug("Warp {0} to {1}".format(source, dest))
  229. if source not in self.warps:
  230. self.warps[source] = set()
  231. for d in dest:
  232. if d not in self.warps[source]:
  233. self.warps[source].add(d)
  234. def get_config(self, key, default=None):
  235. if key in self.config:
  236. return self.config[key]
  237. else:
  238. if default is not None:
  239. self.config[key] = default
  240. return default
  241. def set_config(self, key, value):
  242. self.config.update({key: value})
  243. def set_port(self, sector: int, data: dict):
  244. log.debug("Port {0} : {1}".format(sector, data))
  245. if sector not in self.ports:
  246. self.ports[sector] = dict()
  247. self.ports[sector].update(data)
  248. if "port" not in self.ports[sector]:
  249. # incomplete port type - can we "complete" it?
  250. if all(x in self.ports for x in ["fuel", "org", "equ"]):
  251. # We have all of the port types, so:
  252. port = "".join(
  253. [self.ports[sector][x]["sale"] for x in ["fuel", "org", "equ"]]
  254. )
  255. self.ports[sector]["port"] = port
  256. self.ports[sector]["class"] = CLASSES_PORT[port]
  257. log.debug("completed {0} : {1}".format(sector, self.ports[sector]))
  258. def port_buying(self, sector: int, cargo: str):
  259. """ Given a sector, is this port buying this?
  260. cargo is a char (F/O/E)
  261. """
  262. cargo_to_index = {"F": 0, "O": 1, "E": 2}
  263. cargo_types = ("fuel", "org", "equ")
  264. cargo = cargo[0]
  265. if sector not in self.ports:
  266. log.warn("port_buying( {0}, {1}): sector unknown!".format(sector, cargo))
  267. return False
  268. port = self.ports[sector]
  269. if "port" not in port:
  270. log.warn("port_buying( {0}, {1}): port unknown!".format(sector, cargo))
  271. return True
  272. if sector in self.busts: # Abort! This given sector is a busted port!
  273. log.warn(
  274. "port_buying({0}, {1}): sector contains a busted port!".format(
  275. sector, cargo
  276. )
  277. )
  278. return False
  279. cargo_index = cargo_to_index[cargo]
  280. if port["port"] in ("Special", "StarDock"):
  281. log.warn(
  282. "port_buying( {0}, {1}): not buying (is {2})".format(
  283. sector, cargo, port["port"]
  284. )
  285. )
  286. return False
  287. if port["port"][cargo_index] == "S":
  288. log.warn("port_buying( {0}, {1}): not buying cargo".format(sector, cargo))
  289. return False
  290. # ok, they buy it, but *WILL THEY* really buy it?
  291. cargo_key = cargo_types[cargo_index]
  292. if cargo_key in port:
  293. if int(port[cargo_key]["units"]) > 40:
  294. log.warn(
  295. "port_buying( {0}, {1}): Yes, buying {2}".format(
  296. sector, cargo, port[cargo_key]["sale"]
  297. )
  298. )
  299. return True
  300. else:
  301. log.warn(
  302. "port_buying( {0}, {1}): No, units < 40 {2}".format(
  303. sector, cargo, port[cargo_key]["sale"]
  304. )
  305. )
  306. return False
  307. else:
  308. log.warn(
  309. "port_buying( {0}, {1}): Yes, buying (but values unknown)".format(
  310. sector, cargo
  311. )
  312. )
  313. return True # unknown port, we're guess yes.
  314. return False
  315. @staticmethod
  316. def port_burnt(port: dict):
  317. """ Is this port burned out? """
  318. if all(x in port for x in ["fuel", "org", "equ"]):
  319. if all("pct" in port[x] for x in ["fuel", "org", "equ"]):
  320. if (
  321. port["equ"]["pct"] <= 20
  322. or port["fuel"]["pct"] <= 20
  323. or port["org"]["pct"] <= 20
  324. ):
  325. return True
  326. return False
  327. # Since we don't have any port information, hope for the best, assume it isn't burnt.
  328. return False
  329. @staticmethod
  330. def flip(buy_sell):
  331. # Invert B's and S's to determine if we can trade or not between ports.
  332. return buy_sell.replace("S", "W").replace("B", "S").replace("W", "B")
  333. @staticmethod
  334. def port_trading(port1, port2):
  335. # Given the port settings, can we trade between these?
  336. if port1 == port2:
  337. return False
  338. if port1 in ("Special", "StarDock") or port2 in ("Special", "StarDock"):
  339. return False
  340. # Oops, hey, we are given port settings not a sector a port is in,
  341. # So don't try to check it against the busted list.
  342. p1 = [c for c in port1]
  343. p2 = [c for c in port2]
  344. rem = False
  345. for i in range(3):
  346. if p1[i] == p2[i]:
  347. p1[i] = "X"
  348. p2[i] = "X"
  349. rem = True
  350. if rem:
  351. j1 = "".join(p1).replace("X", "")
  352. j2 = "".join(p2).replace("X", "")
  353. if j1 == "BS" and j2 == "SB":
  354. return True
  355. if j1 == "SB" and j2 == "BS":
  356. return True
  357. rport1 = GameData.flip(port1)
  358. c = 0
  359. match = []
  360. for i in range(3):
  361. if rport1[i] == port2[i]:
  362. match.append(port2[i])
  363. c += 1
  364. if c > 1:
  365. # Remove first match, flip it
  366. f = GameData.flip(match.pop(0))
  367. # Verify it is in there.
  368. # so we're not matching SSS/BBB
  369. if f in match:
  370. return True
  371. return False
  372. return False
  373. @staticmethod
  374. def port_pct(port: dict):
  375. # Make sure these exist in the port data given.
  376. if all(x in port for x in ["fuel", "org", "equ"]):
  377. return "{0},{1},{2}%".format(
  378. GameData.color_pct(port["fuel"]["pct"]),
  379. GameData.color_pct(port["org"]["pct"]),
  380. GameData.color_pct(port["equ"]["pct"]),
  381. )
  382. else:
  383. return "---,---,---%"
  384. @staticmethod
  385. def port_show_part(sector: int, sector_port: dict):
  386. return "{0:5} ({1}) {2}".format(
  387. sector, sector_port["port"], GameData.port_pct(sector_port)
  388. )
  389. def port_above(self, port: dict, limit: int) -> bool:
  390. if all(x in port for x in ["fuel", "org", "equ"]):
  391. if all(
  392. x in port and port[x]["pct"] >= limit for x in ["fuel", "org", "equ"]
  393. ):
  394. return True
  395. else:
  396. return False
  397. # Port is unknown, we'll assume it is above the limit.
  398. return True
  399. def port_trade_show(self, sector: int, warp: int, limit: int = 90):
  400. sector_port = self.ports[sector]
  401. warp_port = self.ports[warp]
  402. if self.port_above(sector_port, limit) and self.port_above(warp_port, limit):
  403. # sector_pct = GameData.port_pct(sector_port)
  404. # warp_pct = GameData.port_pct(warp_port)
  405. return "{0} \xae\xcd\xaf {1}".format(
  406. GameData.port_show_part(sector, sector_port),
  407. GameData.port_show_part(warp, warp_port),
  408. )
  409. return None
  410. @staticmethod
  411. def port_show(sector: int, sector_port: dict, warp: int, warp_port: dict):
  412. # sector_pct = GameData.port_pct(sector_port)
  413. # warp_pct = GameData.port_pct(warp_port)
  414. return "{0} -=- {1}".format(
  415. GameData.port_show_part(sector, sector_port),
  416. GameData.port_show_part(warp, warp_port),
  417. )
  418. def find_nearest_tradepairs(self, sector: int, obj):
  419. """ find nearest tradepair
  420. When do we use good? When do we use ok?
  421. """
  422. searched = set()
  423. if sector not in self.warps:
  424. log.warn(":Sector {0} not in warps.".format(sector))
  425. obj.target_sector = None
  426. return None
  427. if sector in self.busts:
  428. log.warn(":Sector {0} in busted".format(sector))
  429. obj.target_sector = None
  430. return None
  431. # Start with the current sector
  432. look = set((sector,))
  433. while len(look) > 0:
  434. log.warn("Searched [{0}]".format(searched))
  435. log.warn("Checking [{0}]".format(look))
  436. for s in look:
  437. if s in self.ports:
  438. # Ok, there's a port at least
  439. sp = self.ports[s]
  440. if sp["port"] in ("Special", "StarDock"):
  441. continue
  442. if self.port_burnt(sp):
  443. continue
  444. if "class" not in sp:
  445. continue
  446. if s in self.busts: # Check for busted port
  447. continue
  448. sc = sp["class"]
  449. if s not in self.warps:
  450. continue
  451. log.warn("{0} has warps {1}".format(s, self.warps[s]))
  452. # Ok, check for tradepairs.
  453. for w in self.warps[s]:
  454. if not w in self.warps:
  455. continue
  456. if not s in self.warps[w]:
  457. continue
  458. if not w in self.ports:
  459. continue
  460. # Ok, has possible port
  461. cp = self.ports[w]
  462. if cp["port"] in ("Special", "StarDock"):
  463. continue
  464. if self.port_burnt(cp):
  465. continue
  466. if "class" not in cp:
  467. continue
  468. if w in self.busts: # Check for busted
  469. continue
  470. cc = cp["class"]
  471. log.warn("{0} {1} - {2} {3}".format(s, sc, w, cc))
  472. if sc in (1, 5) and cc in (2, 4):
  473. # Good!
  474. log.warn("GOOD: {0}".format(s))
  475. obj.target_sector = s
  476. return s
  477. if sc in (2, 4) and cc in (1, 5):
  478. # Good!
  479. log.warn("GOOD: {0}".format(s))
  480. obj.target_sector = s
  481. return s
  482. # What about "OK" pairs?
  483. # Ok, not found here.
  484. searched.update(look)
  485. step_from = look
  486. look = set()
  487. for s in step_from:
  488. if s in self.warps:
  489. look.update(self.warps[s])
  490. # Look only contains warps we haven't searched
  491. look = look.difference(searched)
  492. yield
  493. obj.target_sector = None
  494. return None
  495. def find_nearest_selling(
  496. self, sector: int, selling: str, at_least: int = 100
  497. ) -> int:
  498. """ find nearest port that is selling at_least amount of this item
  499. selling is 'f', 'o', or 'e'.
  500. """
  501. names = {"e": "equ", "o": "org", "f": "fuel"}
  502. pos = {"f": 0, "o": 1, "e": 2}
  503. sell = names[selling[0].lower()]
  504. s_pos = pos[selling[0].lower()]
  505. log.warn(
  506. "find_nearest_selling({0}, {1}, {2}): {3}, {4}".format(
  507. sector, selling, at_least, sell, s_pos
  508. )
  509. )
  510. searched = set()
  511. if sector not in self.warps:
  512. log.warn("( {0}, {1}): sector not in warps".format(sector, selling))
  513. return 0
  514. if sector in self.busts:
  515. log.warn(
  516. "({0}, {1}): sector is in busted ports list".format(sector, selling)
  517. )
  518. return 0
  519. # Start with the current sector
  520. look = set((sector,))
  521. while len(look) > 0:
  522. for s in look:
  523. if s in self.ports:
  524. # Ok, possibly?
  525. sp = self.ports[s]
  526. if sp["port"] in ("Special", "StarDock"):
  527. continue
  528. if s in self.busts: # Busted!
  529. continue
  530. if sp["port"][s_pos] == "S":
  531. # Ok, they are selling!
  532. if sell in sp:
  533. if int(sp[sell]["units"]) >= at_least:
  534. log.warn(
  535. "find_nearest_selling( {0}, {1}): {2} {3} units".format(
  536. sector, selling, s, sp[sell]["units"]
  537. )
  538. )
  539. return s
  540. else:
  541. # We know they sell it, but we don't know units
  542. log.warn(
  543. "find_nearest_selling( {0}, {1}): {2} {3}".format(
  544. sector, selling, s, sp["port"]
  545. )
  546. )
  547. return s
  548. # Ok, not found here. Branch out.
  549. searched.update(look)
  550. step_from = look
  551. look = set()
  552. for s in step_from:
  553. if s in self.warps:
  554. look.update(self.warps[s])
  555. # look only contains warps we haven't searched
  556. look = look.difference(searched)
  557. # Ok, we have run out of places to search
  558. log.warn("find_nearest_selling( {0}, {1}) : failed".format(sector, selling))
  559. return 0