flexible.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299
  1. from twisted.internet import reactor
  2. from twisted.internet import task
  3. from twisted.internet import defer
  4. from colorama import Fore, Back, Style
  5. from twisted.python import log
  6. from twisted.internet.task import coiterate
  7. from twisted.internet.defer import gatherResults
  8. from itertools import cycle
  9. import pendulum
  10. from pprint import pformat
  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. class PlayerInput(object):
  15. """ Player Input
  16. Example:
  17. from flexible import PlayerInput
  18. ask = PlayerInput(self.game)
  19. # abort_blank means, if the input field is blank, abort. Use error_back.
  20. d = ask.prompt("What is your quest?", 40, name="quest", abort_blank=True)
  21. # Display the user's input / but not needed.
  22. d.addCallback(ask.output)
  23. d.addCallback(
  24. lambda ignore: ask.prompt(
  25. "What is your favorite color?", 10, name="color"
  26. )
  27. )
  28. d.addCallback(ask.output)
  29. d.addCallback(
  30. lambda ignore: ask.prompt(
  31. "What is your least favorite number?",
  32. 12,
  33. name="number",
  34. digits=True,
  35. )
  36. )
  37. d.addCallback(ask.output)
  38. def show_values(show):
  39. log.msg(show)
  40. self.queue_game.put(pformat(show).replace("\n", "\n\r") + self.nl)
  41. d.addCallback(lambda ignore: show_values(ask.keep))
  42. d.addCallback(self.welcome_back)
  43. # On error, just return back
  44. d.addErrback(self.welcome_back)
  45. """
  46. def __init__(self, game):
  47. # I think game gives us access to everything we need
  48. self.game = game
  49. self.observer = self.game.observer
  50. self.save = None
  51. self.deferred = None
  52. self.queue_game = game.queue_game
  53. self.keep = {}
  54. # default colors
  55. self.c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  56. self.cp = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  57. # useful consts
  58. self.r = Style.RESET_ALL
  59. self.nl = "\n\r"
  60. self.bsb = "\b \b"
  61. self.keepalive = None
  62. def color(self, c):
  63. self.c = c
  64. def colorp(self, cp):
  65. self.cp = cp
  66. def alive(self):
  67. log.msg("PlayerInput.alive()")
  68. self.game.queue_player.put(" ")
  69. def prompt(self, user_prompt, limit, **kw):
  70. """ Generate prompt for user input.
  71. Note: This returns deferred.
  72. prompt = text displayed.
  73. limit = # of characters allowed.
  74. default = (text to default to)
  75. keywords:
  76. abort_blank : Abort if they give us blank text.
  77. name : Stores the input in self.keep dict.
  78. digits : Only allow 0-9 to be entered.
  79. """
  80. log.msg("PlayerInput({0}, {1}, {2}".format(user_prompt, limit, kw))
  81. self.limit = limit
  82. self.input = ""
  83. self.kw = kw
  84. assert self.save is None
  85. assert self.keepalive is None
  86. # Note: This clears out the server "keep alive"
  87. self.save = self.observer.save()
  88. self.observer.connect("player", self.get_input)
  89. self.keepalive = task.LoopingCall(self.alive)
  90. self.keepalive.start(30)
  91. # We need to "hide" the game output.
  92. # Otherwise it WITH mess up the user input display.
  93. self.to_player = self.game.to_player
  94. self.game.to_player = False
  95. # Display prompt
  96. # self.queue_game.put(self.r + self.nl + self.c + user_prompt + " " + self.cp)
  97. self.queue_game.put(self.r + self.c + user_prompt + self.r + " " + self.cp)
  98. # Set "Background of prompt"
  99. self.queue_game.put(" " * limit + "\b" * limit)
  100. assert self.deferred is None
  101. d = defer.Deferred()
  102. self.deferred = d
  103. log.msg("Return deferred ...", self.deferred)
  104. return d
  105. def get_input(self, chunk):
  106. """ Data from player (in bytes) """
  107. chunk = chunk.decode("utf-8", "ignore")
  108. for ch in chunk:
  109. if ch == "\b":
  110. if len(self.input) > 0:
  111. self.queue_game.put(self.bsb)
  112. self.input = self.input[0:-1]
  113. else:
  114. self.queue_game.put("\a")
  115. elif ch == "\r":
  116. self.queue_game.put(self.r + self.nl)
  117. log.msg("Restore observer dispatch", self.save)
  118. assert not self.save is None
  119. self.observer.load(self.save)
  120. self.save = None
  121. log.msg("Disable keepalive")
  122. self.keepalive.stop()
  123. self.keepalive = None
  124. line = self.input
  125. self.input = ""
  126. assert not self.deferred is None
  127. self.game.to_player = self.to_player
  128. # If they gave us the keyword name, save the value as that name
  129. if "name" in self.kw:
  130. self.keep[self.kw["name"]] = line
  131. if "abort_blank" in self.kw and self.kw["abort_blank"]:
  132. # Abort on blank input
  133. if line.strip() == "":
  134. # Yes, input is blank, abort.
  135. log.msg("errback, abort_blank")
  136. reactor.callLater(
  137. 0, self.deferred.errback, Exception("abort_blank")
  138. )
  139. self.deferred = None
  140. return
  141. # Ok, use deferred.callback, or reactor.callLater?
  142. # self.deferred.callback(line)
  143. reactor.callLater(0, self.deferred.callback, line)
  144. self.deferred = None
  145. return
  146. elif ch.isprintable():
  147. # Printable, but is it acceptable?
  148. if "digits" in self.kw:
  149. if not ch.isdigit():
  150. self.queue_game.put("\a")
  151. continue
  152. if len(self.input) + 1 <= self.limit:
  153. self.input += ch
  154. self.queue_game.put(ch)
  155. else:
  156. self.queue_game.put("\a")
  157. def output(self, line):
  158. """ A default display of what they just input. """
  159. log.msg("PlayerInput.output({0})".format(line))
  160. self.game.queue_game.put(self.r + "[{0}]".format(line) + self.nl)
  161. return line
  162. PORT_CLASSES = {
  163. 1: "BBS",
  164. 2: "BSB",
  165. 3: "SBB",
  166. 4: "SSB",
  167. 5: "SBS",
  168. 6: "BSS",
  169. 7: "SSS",
  170. 8: "BBB",
  171. }
  172. CLASSES_PORT = {v: k for k, v in PORT_CLASSES.items()}
  173. import re
  174. class CIMWarpReport(object):
  175. def __init__(self, game):
  176. self.game = game
  177. self.queue_game = game.queue_game
  178. self.queue_player = game.queue_player
  179. self.observer = game.observer
  180. # Yes, at this point we would activate
  181. self.prompt = game.buffer
  182. self.save = self.observer.save()
  183. # I actually don't want the player input, but I'll grab it anyway.
  184. self.observer.connect("player", self.player)
  185. self.observer.connect("prompt", self.game_prompt)
  186. self.observer.connect("game-line", self.game_line)
  187. # If we want it, it's here.
  188. self.defer = None
  189. self.to_player = self.game.to_player
  190. # Hide what's happening from the player
  191. self.game.to_player = False
  192. self.queue_player.put("^") # Activate CIM
  193. self.state = 1
  194. self.warpdata = {}
  195. self.warpcycle = cycle(["/", "-", "\\", "|"])
  196. def game_prompt(self, prompt):
  197. if prompt == ": ":
  198. if self.state == 1:
  199. # Ok, then we're ready to request the port report
  200. self.warpcycle = cycle(["/", "-", "\\", "|"])
  201. self.queue_player.put("I")
  202. self.state = 2
  203. elif self.state == 2:
  204. self.queue_player.put("Q")
  205. self.state = 3
  206. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  207. if self.state == 3:
  208. # Ok, time to exit
  209. # exit from this...
  210. self.game.to_player = self.to_player
  211. self.observer.load(self.save)
  212. self.save = None
  213. self.game.warpdata = self.warpdata
  214. self.queue_game.put("\b \b\r\n")
  215. if not self.defer is None:
  216. self.defer.callback(self.warpdata)
  217. self.defer = None
  218. def game_line(self, line):
  219. if line == "" or line == ": ":
  220. return
  221. if line == ": ENDINTERROG":
  222. return
  223. if line.startswith('Command [TL='):
  224. return
  225. # This should be the CIM Report Data -- parse it
  226. if self.warpcycle:
  227. if len(self.warpdata) % 10 == 0:
  228. self.queue_game.put("\b" + next(self.warpcycle))
  229. work = line.strip()
  230. parts = re.split(r"(?<=\d)\s", work)
  231. parts = [int(x) for x in parts]
  232. sector = parts.pop(0)
  233. # tuples are nicer on memory, and the warpdata map isn't going to be changing.
  234. self.warpdata[sector] = tuple(parts)
  235. def __del__(self):
  236. log.msg("CIMWarpReport {0} RIP".format(self))
  237. def whenDone(self):
  238. self.defer = defer.Deferred()
  239. # Call this to chain something after we exit.
  240. return self.defer
  241. def player(self, chunk):
  242. """ Data from player (in bytes). """
  243. chunk = chunk.decode("utf-8", "ignore")
  244. key = chunk.upper()
  245. log.msg("CIMWarpReport.player({0}) : I AM stopping...".format(key))
  246. # Stop the keepalive if we are activating something else
  247. # or leaving...
  248. # self.keepalive.stop()
  249. self.queue_game.put("\b \b\r\n")
  250. if not self.defer is None:
  251. # We have something, so:
  252. self.game.to_player = self.to_player
  253. self.observer.load(self.save)
  254. self.save = None
  255. self.defer.errback(Exception("User Abort"))
  256. self.defer = None
  257. else:
  258. # Still "exit" out.
  259. self.game.to_player = self.to_player
  260. self.observer.load(self.save)
  261. class CIMPortReport(object):
  262. """ Parse data from CIM Port Report
  263. Example:
  264. from flexible import CIMPortReport
  265. report = CIMPortReport(self.game)
  266. d = report.whenDone()
  267. d.addCallback(self.port_report)
  268. d.addErrback(self.welcome_back)
  269. def port_report(self, portdata):
  270. self.portdata = portdata
  271. self.queue_game.put("Loaded {0} records.".format(len(portdata)) + self.nl)
  272. self.welcome_back()
  273. def welcome_back(self,*_):
  274. ... restore keep alive timers, etc.
  275. """
  276. def __init__(self, game):
  277. self.game = game
  278. self.queue_game = game.queue_game
  279. self.queue_player = game.queue_player
  280. self.observer = game.observer
  281. # Yes, at this point we would activate
  282. self.prompt = game.buffer
  283. self.save = self.observer.save()
  284. # I actually don't want the player input, but I'll grab it anyway.
  285. self.observer.connect("player", self.player)
  286. self.observer.connect("prompt", self.game_prompt)
  287. self.observer.connect("game-line", self.game_line)
  288. # If we want it, it's here.
  289. self.defer = None
  290. self.to_player = self.game.to_player
  291. log.msg("to_player (stored)", self.to_player)
  292. # Hide what's happening from the player
  293. self.game.to_player = False
  294. self.queue_player.put("^") # Activate CIM
  295. self.state = 1
  296. self.portdata = {}
  297. self.portcycle = cycle(["/", "-", "\\", "|"])
  298. def game_prompt(self, prompt):
  299. if prompt == ": ":
  300. if self.state == 1:
  301. # Ok, then we're ready to request the port report
  302. self.portcycle = cycle(["/", "-", "\\", "|"])
  303. self.queue_player.put("R")
  304. self.state = 2
  305. elif self.state == 2:
  306. self.queue_player.put("Q")
  307. self.state = 3
  308. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  309. if self.state == 3:
  310. # Ok, time to exit
  311. # exit from this...
  312. self.game.to_player = self.to_player
  313. self.observer.load(self.save)
  314. self.save = None
  315. self.game.portdata = self.portdata
  316. self.queue_game.put("\b \b\r\n")
  317. if not self.defer is None:
  318. self.defer.callback(self.portdata)
  319. self.defer = None
  320. def game_line(self, line):
  321. if line == "" or line == ": ":
  322. return
  323. if line == ": ENDINTERROG":
  324. return
  325. # This should be the CIM Report Data -- parse it
  326. if self.portcycle:
  327. if len(self.portdata) % 10 == 0:
  328. self.queue_game.put("\b" + next(self.portcycle))
  329. work = line.replace("%", "")
  330. parts = re.split(r"(?<=\d)\s", work)
  331. if len(parts) == 8:
  332. port = int(parts[0].strip())
  333. data = dict()
  334. def portBS(info):
  335. if info[0] == "-":
  336. bs = "B"
  337. else:
  338. bs = "S"
  339. return (bs, int(info[1:].strip()))
  340. data["fuel"] = dict()
  341. data["fuel"]["sale"], data["fuel"]["units"] = portBS(parts[1])
  342. data["fuel"]["pct"] = int(parts[2].strip())
  343. data["org"] = dict()
  344. data["org"]["sale"], data["org"]["units"] = portBS(parts[3])
  345. data["org"]["pct"] = int(parts[4].strip())
  346. data["equ"] = dict()
  347. data["equ"]["sale"], data["equ"]["units"] = portBS(parts[5])
  348. data["equ"]["pct"] = int(parts[6].strip())
  349. # Store what this port is buying/selling
  350. data["port"] = (
  351. data["fuel"]["sale"] + data["org"]["sale"] + data["equ"]["sale"]
  352. )
  353. # Convert BBS/SBB to Class number 1-8
  354. data["class"] = CLASSES_PORT[data["port"]]
  355. self.portdata[port] = data
  356. else:
  357. log.msg("CIMPortReport:", line, "???")
  358. def __del__(self):
  359. log.msg("CIMPortReport {0} RIP".format(self))
  360. def whenDone(self):
  361. self.defer = defer.Deferred()
  362. # Call this to chain something after we exit.
  363. return self.defer
  364. def player(self, chunk):
  365. """ Data from player (in bytes). """
  366. chunk = chunk.decode("utf-8", "ignore")
  367. key = chunk.upper()
  368. log.msg("CIMPortReport.player({0}) : I AM stopping...".format(key))
  369. # Stop the keepalive if we are activating something else
  370. # or leaving...
  371. # self.keepalive.stop()
  372. self.queue_game.put("\b \b\r\n")
  373. if not self.defer is None:
  374. # We have something, so:
  375. self.game.to_player = self.to_player
  376. self.observer.load(self.save)
  377. self.save = None
  378. self.defer.errback(Exception("User Abort"))
  379. self.defer = None
  380. else:
  381. # Still "exit" out.
  382. self.game.to_player = self.to_player
  383. self.observer.load(self.save)
  384. def port_burnt(port):
  385. """ Is this port burned out? """
  386. if all( x in port for x in ['fuel', 'org', 'equ']):
  387. if all( 'pct' in port[x] for x in ['fuel', 'org', 'equ']):
  388. if port['equ']['pct'] <= 20 or port['fuel']['pct'] <= 20 or port['org']['pct'] <= 20:
  389. return True
  390. return False
  391. # Since we don't have any port information, hope for the best, assume it isn't burnt.
  392. return False
  393. def flip(port):
  394. return port.replace('S', 'W').replace('B', 'S').replace('W', 'B')
  395. def port_trading(port1, port2):
  396. """ Are there possible trades at these ports? """
  397. if port1 == port2:
  398. return False
  399. p1 = [ c for c in port1]
  400. p2 = [ c for c in port2]
  401. # Any that are the same? Remove them.
  402. rem = False
  403. for i in range(3):
  404. if p1[i] == p2[i]:
  405. p1[i] = 'X'
  406. p2[i] = 'X'
  407. rem = True
  408. if rem:
  409. j1 = "".join(p1).replace('X', '')
  410. j2 = "".join(p2).replace('X', '')
  411. if j1 == 'BS' and j2 == 'SB':
  412. return True
  413. if j1 == 'SB' and j2 == 'BS':
  414. return True
  415. # Matching 2 of them.
  416. rport1 = 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. f = flip(match.pop(0))
  425. if f in match:
  426. return True
  427. return False
  428. return False
  429. class ScriptPort(object):
  430. """ Performs the Port script.
  431. This is close to the original.
  432. We don't ask for the port to trade with --
  433. because that information is available to us after "D" (display).
  434. We look at the adjacent sectors, and see if we know any ports.
  435. If the ports are burnt (< 20%), we remove them from the list.
  436. If there's just one, we use it. Otherwise we ask them to choose.
  437. """
  438. def __init__(self, game):
  439. self.game = game
  440. self.queue_game = game.queue_game
  441. self.queue_player = game.queue_player
  442. self.observer = game.observer
  443. self.r = Style.RESET_ALL
  444. self.nl = "\n\r"
  445. self.this_sector = None # Starting sector
  446. self.sector1 = None # Current Sector
  447. self.sector2 = None # Next Sector Stop
  448. self.percent = 5 # Stick with the good default.
  449. self.credits = 0
  450. self.last_credits = None
  451. self.times_left = 0
  452. # Activate
  453. self.prompt = game.buffer
  454. self.save = self.observer.save()
  455. self.observer.connect('player', self.player)
  456. self.observer.connect("prompt", self.game_prompt)
  457. self.observer.connect("game-line", self.game_line)
  458. self.defer = None
  459. self.queue_game.put(
  460. self.nl + "Script based on: Port Pair Trading v2.00" + self.r + self.nl
  461. )
  462. self.possible_sectors = None
  463. self.state = 1
  464. self.queue_player.put("D")
  465. # Original, send 'D' to display current sector.
  466. # We could get the sector number from the self.prompt string -- HOWEVER:
  467. # IF! We send 'D', we can also get the sectors around -- we might not even need to
  468. # prompt for sector to trade with (we could possibly figure it out ourselves).
  469. # [Command [TL=00:00:00]:[967] (?=Help)? : D]
  470. # [<Re-Display>]
  471. # []
  472. # [Sector : 967 in uncharted space.]
  473. # [Planets : (M) Into the Darkness]
  474. # [Warps to Sector(s) : 397 - (562) - (639)]
  475. # []
  476. def whenDone(self):
  477. self.defer = defer.Deferred()
  478. # Call this to chain something after we exit.
  479. return self.defer
  480. def deactivate(self):
  481. self.state = 0
  482. log.msg("ScriptPort.deactivate ({0})".format(self.times_left))
  483. assert(not self.save is None)
  484. self.observer.load(self.save)
  485. self.save = None
  486. if self.defer:
  487. self.defer.callback('done')
  488. self.defer = None
  489. def player(self, chunk: bytes):
  490. # If we receive anything -- ABORT!
  491. self.deactivate()
  492. def game_prompt(self, prompt: str):
  493. log.msg("{0} : {1}".format(self.state, prompt))
  494. if self.state == 3:
  495. log.msg("game_prompt: ", prompt)
  496. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  497. self.state = 4
  498. log.msg("Ok, state 4")
  499. if self.sector2 is None:
  500. # Ok, we need to prompt for this.
  501. self.queue_game.put(self.r + self.nl +
  502. "Which sector to trade with? {0}".format(port_show_part(self.this_sector, self.game.portdata[self.this_sector])) +
  503. self.nl + Fore.CYAN)
  504. for i, p in enumerate(self.possible):
  505. self.queue_game.put(" " + str(i + 1) + " : " + port_show_part(p, self.game.portdata[p]) + self.nl)
  506. pi = PlayerInput(self.game)
  507. def got_need1(*_):
  508. log.msg("Ok, I have:", pi.keep)
  509. if pi.keep['count'].strip() == '':
  510. self.deactivate()
  511. return
  512. self.times_left = int(pi.keep['count'])
  513. if pi.keep['choice'].strip() == '':
  514. self.deactivate()
  515. return
  516. c = int(pi.keep['choice']) -1
  517. if c < 0 or c >= len(self.possible):
  518. self.deactivate()
  519. return
  520. self.sector2 = self.possible[int(pi.keep['choice']) -1]
  521. # self.queue_game.put(pformat(pi.keep).replace("\n", "\n\r"))
  522. self.state = 5
  523. self.trade()
  524. d = pi.prompt("Choose -=>", 5, name='choice', digits=True)
  525. d.addCallback(lambda ignore: pi.prompt("Times to execute script:", 5, name='count', digits=True))
  526. d.addCallback(got_need1)
  527. else:
  528. # We already have our target port, so...
  529. self.queue_game.put(self.r + self.nl + "Trading from {0} ({1}) to default {2} ({3}).".format(
  530. self.this_sector,
  531. self.game.portdata[self.this_sector]['port'],
  532. self.sector2, self.game.portdata[self.sector2]['port']) + self.nl
  533. )
  534. self.queue_game.put(self.r + self.nl + "Trading {0}".format(
  535. port_show(self.this_sector,
  536. self.game.portdata[self.this_sector],
  537. self.sector2,
  538. self.game.portdata[self.sector2])
  539. ))
  540. # The code is smart enough now, that this can't happen. :( Drat!
  541. if self.game.portdata[self.this_sector]['port'] == self.game.portdata[self.sector2]['port']:
  542. self.queue_game.put("Hey dummy! Look out the window! These ports are the same class!" + nl)
  543. self.deactivate()
  544. return
  545. pi = PlayerInput(self.game)
  546. def got_need2(*_):
  547. if pi.keep['count'].strip() == '':
  548. self.deactivate()
  549. return
  550. self.times_left = int(pi.keep['count'])
  551. log.msg("Ok, I have:", pi.keep)
  552. # self.queue_game.put(pformat(pi.keep).replace("\n", "\n\r"))
  553. self.state = 5
  554. self.trade()
  555. self.queue_game.put(self.r + self.nl)
  556. d = pi.prompt("Times to execute script", 5, name='count')
  557. d.addCallback(got_need2)
  558. elif self.state == 7:
  559. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  560. # Done
  561. if self.this_sector == self.sector1:
  562. self.this_sector = self.sector2
  563. self.queue_player.put("{0}\r".format(self.sector2))
  564. self.state = 10
  565. else:
  566. self.times_left -= 1
  567. if self.times_left <= 0:
  568. # Ok, exit out
  569. self.deactivate()
  570. return
  571. if self.last_credits is None:
  572. self.last_credits = self.credits
  573. else:
  574. if self.credits <= self.last_credits:
  575. log.msg("We don't seem to be making any money here...")
  576. self.queue_game.put(self.r + self.nl + "We don't seem to be making any money here. I'm stopping!" + self.nl)
  577. self.deactivate()
  578. return
  579. self.this_sector = self.sector1
  580. self.queue_player.put("{0}\r".format(self.sector1))
  581. self.state = 10
  582. elif self.state == 8:
  583. # What are we trading
  584. # How many holds of Equipment do you want to buy [75]?
  585. if re.match(r"How many holds of .+ do you want to buy \[\d+\]\?", prompt):
  586. parts = prompt.split()
  587. trade_type = parts[4]
  588. if trade_type == 'Fuel':
  589. if (self.tpc in (5,7)) and (self.opc in (2,3,4,8)):
  590. # Can buy equipment - fuel ore is worthless.
  591. self.queue_player.put("0\r")
  592. return
  593. if (self.tpc in(4,7)) and (self.opc in (1,3,5,8)):
  594. # Can buy organics - fuel ore is worthless.
  595. self.queue_player.put("0\r")
  596. return
  597. if (self.tpc in (4,7,3,5)) and (self.opc in (3,4,5,7)):
  598. # No point in buying fuel ore if it can't be sold.
  599. self.queue_player.put("0\r")
  600. return
  601. elif trade_type == 'Organics':
  602. if (self.tpc in (6,7)) and (self.opc in (2,3,4,8)):
  603. # Can buy equipment - organics is worthless.
  604. self.queue_player.put("0\r")
  605. return
  606. if (self.tpc in (2,4,6,7)) and (self.opc in (2,4,6,7)):
  607. # No point in buying organics if it can't be sold.
  608. self.queue_player.put("0\r")
  609. return
  610. elif trade_type == 'Equipment':
  611. if (self.opc in (1,5,6,7)):
  612. # No point in buying equipment if it can't be sold.
  613. self.queue_player.put("0\r")
  614. return
  615. self.queue_player.put("\r")
  616. elif re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  617. # Done
  618. if self.this_sector == self.sector1:
  619. self.this_sector = self.sector2
  620. self.queue_player.put("{0}\r".format(self.sector2))
  621. self.state = 10
  622. else:
  623. self.times_left -= 1
  624. if self.times_left <= 0:
  625. # Ok, exit out
  626. self.deactivate()
  627. return
  628. if self.last_credits is None:
  629. self.last_credits = self.credits
  630. else:
  631. if self.credits <= self.last_credits:
  632. log.msg("We don't seem to be making any money here...")
  633. self.deactivate()
  634. return
  635. self.this_sector = self.sector1
  636. self.queue_player.put("{0}\r".format(self.sector1))
  637. self.state = 10
  638. def trade(self, *_):
  639. # state 5
  640. log.msg("trade!")
  641. self.queue_player.put("pt") # Port Trade
  642. self.this_port = self.game.portdata[self.this_sector]
  643. if self.this_sector == self.sector1:
  644. self.other_port = self.game.portdata[self.sector2]
  645. else:
  646. self.other_port = self.game.portdata[self.sector1]
  647. # Ok, perform some calculations
  648. self.tpc = self.this_port['class']
  649. self.opc = self.other_port['class']
  650. # [ Items Status Trading % of max OnBoard]
  651. # [ ----- ------ ------- -------- -------]
  652. # [Fuel Ore Selling 2573 93% 0]
  653. # [Organics Buying 2960 100% 0]
  654. # [Equipment Buying 1958 86% 0]
  655. # []
  656. # []
  657. # [You have 1,000 credits and 20 empty cargo holds.]
  658. # []
  659. # [We are selling up to 2573. You have 0 in your holds.]
  660. # [How many holds of Fuel Ore do you want to buy [20]? 0]
  661. def game_line(self, line: str):
  662. if line.startswith("You have ") and 'credits and' in line:
  663. parts = line.replace(',', '').split()
  664. credits = int(parts[2])
  665. log.msg("Credits: {0}".format(credits))
  666. self.credits = credits
  667. if self.state == 1:
  668. # First exploration
  669. if line.startswith("Sector :"):
  670. # We have starting sector information
  671. parts = re.split("\s+", line)
  672. self.this_sector = int(parts[2])
  673. # These will be the ones swapped around as we trade back and forth.
  674. self.sector1 = self.this_sector
  675. elif line.startswith("Warps to Sector(s) : "):
  676. # Warps to Sector(s) : 397 - (562) - (639)
  677. _, _, warps = line.partition(':')
  678. warps = warps.replace('-', '').replace('(', '').replace(')', '').strip()
  679. log.msg("Warps: [{0}]".format(warps))
  680. self.warps = [ int(x) for x in re.split("\s+", warps)]
  681. log.msg("Warps: [{0}]".format(self.warps))
  682. self.state = 2
  683. elif self.state == 2:
  684. if line == "":
  685. # Ok, we're done
  686. self.state = 3
  687. # Check to see if we have information on any possible ports
  688. if hasattr(self.game, 'portdata'):
  689. if not self.this_sector in self.game.portdata:
  690. self.state = 0
  691. log.msg("Current sector {0} not in portdata.".format(self.this_sector))
  692. self.queue_game.put(self.r + self.nl + "I can't find the current sector in the portdata." + self.nl)
  693. self.deactivate()
  694. return
  695. else:
  696. # Ok, we are in the portdata
  697. pd = self.game.portdata[self.this_sector]
  698. if port_burnt(pd):
  699. log.msg("Current sector {0} port is burnt (<= 20%).".format(self.this_sector))
  700. self.queue_game.put(self.r + self.nl + "Current sector port is burnt out. <= 20%." + self.nl)
  701. self.deactivate()
  702. return
  703. possible = [ x for x in self.warps if x in self.game.portdata ]
  704. log.msg("Possible:", possible)
  705. # BUG: Sometimes links to another sector, don't link back!
  706. # This causes the game to plot a course / autopilot.
  707. if hasattr(self.game, 'warpdata'):
  708. # Great! verify that those warps link back to us!
  709. possible = [ x for x in possible if self.this_sector in self.game.warpdata[x]]
  710. if len(possible) == 0:
  711. self.state = 0
  712. self.queue_game.put(self.r + self.nl + "I don't see any ports in [{0}].".format(self.warps) + self.nl)
  713. self.deactivate()
  714. return
  715. possible = [ x for x in possible if not port_burnt(self.game.portdata[x]) ]
  716. log.msg("Possible:", possible)
  717. if len(possible) == 0:
  718. self.state = 0
  719. self.queue_game.put(self.r + self.nl + "I don't see any unburnt ports in [{0}].".format(self.warps) + self.nl)
  720. self.deactivate()
  721. return
  722. possible = [ x for x in possible if port_trading(self.game.portdata[self.this_sector]['port'], self.game.portdata[x]['port'])]
  723. self.possible = possible
  724. if len(possible) == 0:
  725. self.state = 0
  726. self.queue_game.put(self.r + self.nl + "I don't see any possible port trades in [{0}].".format(self.warps) + self.nl)
  727. self.deactivate()
  728. return
  729. elif len(possible) == 1:
  730. # Ok! there's only one!
  731. self.sector2 = possible[0]
  732. # Display possible ports:
  733. # spos = [ str(x) for x in possible]
  734. # self.queue_game.put(self.r + self.nl + self.nl.join(spos) + self.nl)
  735. # At state 3, we only get a prompt.
  736. return
  737. else:
  738. self.state = 0
  739. log.msg("We don't have any portdata!")
  740. self.queue_game.put(self.r + self.nl + "I have no portdata. Please run CIM Port Report." + self.nl)
  741. self.deactivate()
  742. return
  743. elif self.state == 5:
  744. if "-----" in line:
  745. self.state = 6
  746. elif self.state == 6:
  747. if "We are buying up to" in line:
  748. # Sell
  749. self.state = 7
  750. self.queue_player.put("\r")
  751. self.sell_perc = 100 + self.percent
  752. if "We are selling up to" in line:
  753. # Buy
  754. self.state = 8
  755. self.sell_perc = 100 - self.percent
  756. if "You don't have anything they want" in line:
  757. # Neither! DRAT!
  758. self.deactivate()
  759. return
  760. if "We're not interested." in line:
  761. log.msg("Try, try again. :(")
  762. self.state = 5
  763. self.trade()
  764. elif self.state == 7:
  765. # Haggle Sell
  766. if "We'll buy them for" in line or "Our final offer" in line:
  767. if "Our final offer" in line:
  768. self.sell_perc -= 1
  769. parts = line.replace(',', '').split()
  770. start_price = int(parts[4])
  771. price = start_price * self.sell_perc // 100
  772. log.msg("start: {0} % {1} price {2}".format(start_price, self.sell_perc, price))
  773. self.sell_perc -= 1
  774. self.queue_player.put("{0}\r".format(price))
  775. if "We are selling up to" in line:
  776. # Buy
  777. self.state = 8
  778. self.sell_perc = 100 - self.percent
  779. if "We're not interested." in line:
  780. log.msg("Try, try again. :(")
  781. self.state = 5
  782. self.trade()
  783. elif self.state == 8:
  784. # Haggle Buy
  785. if "We'll sell them for" in line or "Our final offer" in line:
  786. if "Our final offer" in line:
  787. self.sell_perc += 1
  788. parts = line.replace(',', '').split()
  789. start_price = int(parts[4])
  790. price = start_price * self.sell_perc // 100
  791. log.msg("start: {0} % {1} price {2}".format(start_price, self.sell_perc, price))
  792. self.sell_perc += 1
  793. self.queue_player.put("{0}\r".format(price))
  794. if "We're not interested." in line:
  795. log.msg("Try, try again. :(")
  796. self.state = 5
  797. self.trade()
  798. elif self.state == 10:
  799. if "Sector : " in line:
  800. # Trade
  801. self.state = 5
  802. reactor.callLater(0, self.trade, 0)
  803. # self.trade()
  804. # elif self.state == 3:
  805. # log.msg("At state 3 [{0}]".format(line))
  806. # self.queue_game.put("At state 3.")
  807. # self.deactivate()
  808. # return
  809. def port_pct(port):
  810. # Make sure these exist in the port data given.
  811. if all( x in port for x in ['fuel', 'org', 'equ']):
  812. return "{0:3},{1:3},{2:3}%".format(
  813. port['fuel']['pct'],
  814. port['org']['pct'],
  815. port['equ']['pct'])
  816. else:
  817. return "---,---,---%"
  818. def port_show_part(sector, sector_port):
  819. return "{0:5} ({1}) {2}".format(sector, sector_port['port'], port_pct(sector_port))
  820. def port_show(sector, sector_port, warp, warp_port):
  821. sector_pct = port_pct(sector_port)
  822. warp_pct = port_pct(warp_port)
  823. return "{0} -=- {1}".format(
  824. port_show_part(sector, sector_port),
  825. port_show_part(warp, warp_port)
  826. )
  827. class ProxyMenu(object):
  828. """ Display ProxyMenu
  829. Example:
  830. from flexible import ProxyMenu
  831. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  832. menu = ProxyMenu(self.game)
  833. """
  834. def __init__(self, game):
  835. self.nl = "\n\r"
  836. self.c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  837. self.r = Style.RESET_ALL
  838. self.c1 = merge(Style.BRIGHT + Fore.BLUE)
  839. self.c2 = merge(Style.NORMAL + Fore.CYAN)
  840. self.portdata = None
  841. self.game = game
  842. self.queue_game = game.queue_game
  843. self.observer = game.observer
  844. # Am I using self or game? (I think I want game, not self.)
  845. if hasattr(self.game, "portdata"):
  846. self.portdata = self.game.portdata
  847. else:
  848. self.portdata = {}
  849. if hasattr(self.game, 'warpdata'):
  850. self.warpdata = self.game.warpdata
  851. else:
  852. self.warpdata = {}
  853. if hasattr(self.game, 'trade_report'):
  854. self.trade_report = self.game.trade_report
  855. else:
  856. self.trade_report = []
  857. # Yes, at this point we would activate
  858. self.prompt = game.buffer
  859. self.save = self.observer.save()
  860. self.observer.connect("player", self.player)
  861. # If we want it, it's here.
  862. self.defer = None
  863. self.keepalive = task.LoopingCall(self.awake)
  864. self.keepalive.start(30)
  865. self.menu()
  866. def __del__(self):
  867. log.msg("ProxyMenu {0} RIP".format(self))
  868. def whenDone(self):
  869. self.defer = defer.Deferred()
  870. # Call this to chain something after we exit.
  871. return self.defer
  872. def menu(self):
  873. self.queue_game.put(
  874. self.nl + self.c + "TradeWars Proxy active." + self.r + self.nl
  875. )
  876. def menu_item(ch: str, desc: str):
  877. self.queue_game.put(
  878. " " + self.c1 + ch + self.c2 + " - " + self.c1 + desc + self.nl
  879. )
  880. menu_item("D", "Display Report again")
  881. # menu_item("Q", "Quest")
  882. if hasattr(self.game, 'portdata'):
  883. ports = len(self.game.portdata)
  884. else:
  885. ports = '?'
  886. menu_item("P", "Port CIM Report ({0})".format(ports))
  887. if hasattr(self.game, 'warpdata'):
  888. warps = len(self.game.warpdata)
  889. else:
  890. warps = '?'
  891. menu_item("W", "Warp CIM Report ({0})".format(warps))
  892. menu_item("T", "Trading Report")
  893. menu_item("S", "Scripts")
  894. menu_item("X", "eXit")
  895. self.queue_game.put(" " + self.c + "-=>" + self.r + " ")
  896. def awake(self):
  897. log.msg("ProxyMenu.awake()")
  898. self.game.queue_player.put(" ")
  899. def port_report(self, portdata: dict):
  900. self.portdata = portdata
  901. self.game.portdata = portdata
  902. self.queue_game.put("Loaded {0} ports.".format(len(portdata)) + self.nl)
  903. self.welcome_back()
  904. def warp_report(self, warpdata: dict):
  905. self.warpdata = warpdata
  906. self.game.warpdata = warpdata
  907. self.queue_game.put("Loaded {0} sectors.".format(len(warpdata)) + self.nl)
  908. self.welcome_back()
  909. def make_trade_report(self):
  910. log.msg("make_trade_report()")
  911. ok_trades = []
  912. best_trades = []
  913. for sector, pd in self.game.portdata.items():
  914. if not port_burnt(pd):
  915. pc = pd['class']
  916. # Ok, let's look into it.
  917. if not sector in self.game.warpdata:
  918. continue
  919. warps = self.game.warpdata[sector]
  920. for w in warps:
  921. # Verify that we have that warp's info, and that the sector is in it.
  922. # (We can get back from it)
  923. if w in self.game.warpdata and sector in self.game.warpdata[w]:
  924. # Ok, we can get there -- and get back!
  925. if w > sector and w in self.game.portdata and not port_burnt(self.game.portdata[w]):
  926. # it is > and has a port.
  927. wd = self.game.portdata[w]
  928. wc = wd['class']
  929. # 1: "BBS",
  930. # 2: "BSB",
  931. # 3: "SBB",
  932. # 4: "SSB",
  933. # 5: "SBS",
  934. # 6: "BSS",
  935. # 7: "SSS",
  936. # 8: "BBB",
  937. if pc in (1,5) and wc in (2,4):
  938. best_trades.append(port_show(sector, pd, w, wd))
  939. # best_trades.append( "{0:5} -=- {1:5}".format(sector, w))
  940. elif pc in (2,4) and wc in (1,5):
  941. best_trades.append(port_show(sector, pd, w, wd))
  942. # best_trades.append( "{0:5} -=- {1:5}".format(sector, w))
  943. elif port_trading(pd['port'], self.game.portdata[w]['port']):
  944. # ok_trades.append( "{0:5} -=- {1:5}".format(sector,w))
  945. ok_trades.append(port_show(sector, pd, w, wd))
  946. yield
  947. self.trade_report.append("Best Trades: (org/equ)")
  948. self.trade_report.extend(best_trades)
  949. self.trade_report.append("Ok Trades:")
  950. self.trade_report.extend(ok_trades)
  951. # self.queue_game.put("BEST TRADES:" + self.nl + self.nl.join(best_trades) + self.nl)
  952. # self.queue_game.put("OK TRADES:" + self.nl + self.nl.join(ok_trades) + self.nl)
  953. def show_trade_report(self, *_):
  954. self.game.trade_report = self.trade_report
  955. for t in self.trade_report:
  956. self.queue_game.put(t + self.nl)
  957. self.welcome_back()
  958. def player(self, chunk: bytes):
  959. """ Data from player (in bytes). """
  960. chunk = chunk.decode("utf-8", "ignore")
  961. key = chunk.upper()
  962. log.msg("ProxyMenu.player({0})".format(key))
  963. # Stop the keepalive if we are activating something else
  964. # or leaving...
  965. self.keepalive.stop()
  966. if key == "T":
  967. self.queue_game.put(self.c + key + self.r + self.nl)
  968. # do we have enough information to do this?
  969. if not hasattr(self.game, 'portdata') and not hasattr(self.game, 'warpdata'):
  970. self.queue_game.put("Missing portdata and warpdata." + self.nl)
  971. elif not hasattr(self.game, 'portdata'):
  972. self.queue_game.put("Missing portdata." + self.nl)
  973. elif not hasattr(self.game, 'warpdata'):
  974. self.queue_game.put("Missing warpdata." + self.nl)
  975. else:
  976. # Yes, so let's start!
  977. self.trade_report = []
  978. d = coiterate(self.make_trade_report())
  979. d.addCallback(self.show_trade_report)
  980. return
  981. elif key == "P":
  982. self.queue_game.put(self.c + key + self.r + self.nl)
  983. # Activate CIM Port Report
  984. report = CIMPortReport(self.game)
  985. d = report.whenDone()
  986. d.addCallback(self.port_report)
  987. d.addErrback(self.welcome_back)
  988. return
  989. elif key == "W":
  990. self.queue_game.put(self.c + key + self.r + self.nl)
  991. # Activate CIM Port Report
  992. report = CIMWarpReport(self.game)
  993. d = report.whenDone()
  994. d.addCallback(self.warp_report)
  995. d.addErrback(self.welcome_back)
  996. return
  997. elif key == "S":
  998. self.queue_game.put(self.c + key + self.r + self.nl)
  999. self.activate_scripts_menu()
  1000. return
  1001. elif key == "D":
  1002. self.queue_game.put(self.c + key + self.r + self.nl)
  1003. for t in self.trade_report:
  1004. self.queue_game.put(t + self.nl)
  1005. # self.queue_game.put(pformat(self.portdata).replace("\n", "\n\r") + self.nl)
  1006. # self.queue_game.put(pformat(self.warpdata).replace("\n", "\n\r") + self.nl)
  1007. elif key == "Q":
  1008. self.queue_game.put(self.c + key + self.r + self.nl)
  1009. # This is an example of chaining PlayerInput prompt calls.
  1010. ask = PlayerInput(self.game)
  1011. d = ask.prompt("What is your quest?", 40, name="quest", abort_blank=True)
  1012. # Display the user's input
  1013. d.addCallback(ask.output)
  1014. d.addCallback(
  1015. lambda ignore: ask.prompt(
  1016. "What is your favorite color?", 10, name="color"
  1017. )
  1018. )
  1019. d.addCallback(ask.output)
  1020. d.addCallback(
  1021. lambda ignore: ask.prompt(
  1022. "What is the meaning of the squirrel?",
  1023. 12,
  1024. name="squirrel",
  1025. digits=True,
  1026. )
  1027. )
  1028. d.addCallback(ask.output)
  1029. def show_values(show):
  1030. log.msg(show)
  1031. self.queue_game.put(pformat(show).replace("\n", "\n\r") + self.nl)
  1032. d.addCallback(lambda ignore: show_values(ask.keep))
  1033. d.addCallback(self.welcome_back)
  1034. # On error, just return back
  1035. # This doesn't seem to be getting called.
  1036. # d.addErrback(lambda ignore: self.welcome_back)
  1037. d.addErrback(self.welcome_back)
  1038. return
  1039. elif key == "X":
  1040. self.queue_game.put(self.c + key + self.r + self.nl)
  1041. self.queue_game.put("Proxy done." + self.nl)
  1042. self.observer.load(self.save)
  1043. self.save = None
  1044. # It isn't running (NOW), so don't try to stop it.
  1045. # self.keepalive.stop()
  1046. self.keepalive = None
  1047. # Ok, this is a HORRIBLE idea, because the prompt might be
  1048. # outdated.
  1049. # self.queue_game.put(self.prompt)
  1050. self.prompt = None
  1051. # Possibly: Send '\r' to re-display the prompt
  1052. # instead of displaying the original one.
  1053. self.game.queue_player.put("d")
  1054. # Were we asked to do something when we were done here?
  1055. if self.defer:
  1056. reactor.CallLater(0, self.defer.callback)
  1057. # self.defer.callback()
  1058. self.defer = None
  1059. return
  1060. self.keepalive.start(30, True)
  1061. self.menu()
  1062. def activate_scripts_menu(self):
  1063. self.observer.disconnect("player", self.player)
  1064. self.observer.connect("player", self.scripts_player)
  1065. self.scripts_menu()
  1066. def deactivate_scripts_menu(self, *_):
  1067. self.observer.disconnect("player", self.scripts_player)
  1068. self.observer.connect("player", self.player)
  1069. self.welcome_back()
  1070. def scripts_menu(self, *_):
  1071. c1 = merge(Style.BRIGHT + Fore.CYAN)
  1072. c2 = merge(Style.NORMAL + Fore.CYAN)
  1073. def menu_item(ch, desc):
  1074. self.queue_game.put(
  1075. " " + c1 + ch + c2 + " - " + c1 + desc + self.nl
  1076. )
  1077. menu_item("1", "Ports (Trades between two sectors)")
  1078. menu_item("2", "TODO")
  1079. menu_item("3", "TODO")
  1080. menu_item("X", "eXit")
  1081. self.queue_game.put(" " + c1 + "-=>" + self.r + " ")
  1082. def scripts_player(self, chunk: bytes):
  1083. """ Data from player (in bytes). """
  1084. chunk = chunk.decode("utf-8", "ignore")
  1085. key = chunk.upper()
  1086. if key == '1':
  1087. self.queue_game.put(self.c + key + self.r + self.nl)
  1088. # Activate this magical event here
  1089. ports = ScriptPort(self.game)
  1090. d = ports.whenDone()
  1091. # d.addCallback(self.scripts_menu)
  1092. # d.addErrback(self.scripts_menu)
  1093. d.addCallback(self.deactivate_scripts_menu)
  1094. d.addErrback(self.deactivate_scripts_menu)
  1095. return
  1096. elif key == 'X':
  1097. self.queue_game.put(self.c + key + self.r + self.nl)
  1098. self.deactivate_scripts_menu()
  1099. return
  1100. else:
  1101. self.queue_game.put(self.c + "?" + self.r + self.nl)
  1102. self.scripts_menu()
  1103. def welcome_back(self, *_):
  1104. log.msg("welcome_back")
  1105. self.keepalive.start(30, True)
  1106. self.menu()