flexible.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
  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 port['equ']['pct'] <= 20 or port['fuel']['pct'] <= 20 or port['org']['pct'] <= 20:
  387. return True
  388. return False
  389. def flip(port):
  390. return port.replace('S', 'W').replace('B', 'S').replace('W', 'B')
  391. def port_trading(port1, port2):
  392. """ Are there possible trades at these ports? """
  393. if port1 == port2:
  394. return False
  395. p1 = [ c for c in port1]
  396. p2 = [ c for c in port2]
  397. # Any that are the same? Remove them.
  398. rem = False
  399. for i in range(3):
  400. if p1[i] == p2[i]:
  401. p1[i] = 'X'
  402. p2[i] = 'X'
  403. rem = True
  404. if rem:
  405. j1 = "".join(p1).replace('X', '')
  406. j2 = "".join(p2).replace('X', '')
  407. if j1 == 'BS' and j2 == 'SB':
  408. return True
  409. if j1 == 'SB' and j2 == 'BS':
  410. return True
  411. # Matching 2 of them.
  412. rport1 = flip(port1)
  413. c = 0
  414. match = []
  415. for i in range(3):
  416. if rport1[i] == port2[i]:
  417. match.append(port2[i])
  418. c += 1
  419. if c > 1:
  420. f = flip(match.pop(0))
  421. if f in match:
  422. return True
  423. return False
  424. return False
  425. class ScriptPort(object):
  426. """ Performs the Port script.
  427. This is close to the original.
  428. We don't ask for the port to trade with --
  429. because that information is available to us after "D" (display).
  430. We look at the adjacent sectors, and see if we know any ports.
  431. If the ports are burnt (< 20%), we remove them from the list.
  432. If there's just one, we use it. Otherwise we ask them to choose.
  433. """
  434. def __init__(self, game):
  435. self.game = game
  436. self.queue_game = game.queue_game
  437. self.queue_player = game.queue_player
  438. self.observer = game.observer
  439. self.r = Style.RESET_ALL
  440. self.nl = "\n\r"
  441. self.this_sector = None # Starting sector
  442. self.sector1 = None # Current Sector
  443. self.sector2 = None # Next Sector Stop
  444. self.percent = 5 # Stick with the good default.
  445. self.credits = 0
  446. # Activate
  447. self.prompt = game.buffer
  448. self.save = self.observer.save()
  449. self.observer.connect('player', self.player)
  450. self.observer.connect("prompt", self.game_prompt)
  451. self.observer.connect("game-line", self.game_line)
  452. self.defer = None
  453. self.queue_game.put(
  454. self.nl + "Script based on: Port Pair Trading v2.00" + self.r + self.nl
  455. )
  456. self.possible_sectors = None
  457. self.state = 1
  458. self.queue_player.put("D")
  459. # Original, send 'D' to display current sector.
  460. # We could get the sector number from the self.prompt string -- HOWEVER:
  461. # IF! We send 'D', we can also get the sectors around -- we might not even need to
  462. # prompt for sector to trade with (we could possibly figure it out ourselves).
  463. # [Command [TL=00:00:00]:[967] (?=Help)? : D]
  464. # [<Re-Display>]
  465. # []
  466. # [Sector : 967 in uncharted space.]
  467. # [Planets : (M) Into the Darkness]
  468. # [Warps to Sector(s) : 397 - (562) - (639)]
  469. # []
  470. def whenDone(self):
  471. self.defer = defer.Deferred()
  472. # Call this to chain something after we exit.
  473. return self.defer
  474. def deactivate(self):
  475. self.state = 0
  476. log.msg("ScriptPort.deactivate")
  477. assert(not self.save is None)
  478. self.observer.load(self.save)
  479. self.save = None
  480. if self.defer:
  481. self.defer.callback('done')
  482. self.defer = None
  483. def player(self, chunk: bytes):
  484. # If we receive anything -- ABORT!
  485. self.deactivate()
  486. def game_prompt(self, prompt: str):
  487. log.msg("{0} : {1}".format(self.state, prompt))
  488. if self.state == 3:
  489. log.msg("game_prompt: ", prompt)
  490. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  491. self.state = 4
  492. log.msg("Ok, state 4")
  493. if self.sector2 is None:
  494. # Ok, we need to prompt for this.
  495. self.queue_game.put(self.r + self.nl + "Which sector to trade with? {0} ({1})".format(self.this_sector, self.game.portdata[self.this_sector]['port']) + self.nl + Fore.CYAN)
  496. for i, p in enumerate(self.possible):
  497. self.queue_game.put(" " + str(i + 1) + " : " + str(p) + " (" + self.game.portdata[p]['port'] + ")" + self.nl)
  498. pi = PlayerInput(self.game)
  499. def got_need1(*_):
  500. log.msg("Ok, I have:", pi.keep)
  501. if pi.keep['count'].strip() == '':
  502. self.deactivate()
  503. return
  504. self.times_left = int(pi.keep['count'])
  505. if pi.keep['choice'].strip() == '':
  506. self.deactivate()
  507. return
  508. c = int(pi.keep['choice']) -1
  509. if c < 0 or c >= len(self.possible):
  510. self.deactivate()
  511. return
  512. self.sector2 = self.possible[int(pi.keep['choice']) -1]
  513. # self.queue_game.put(pformat(pi.keep).replace("\n", "\n\r"))
  514. self.state = 5
  515. self.trade()
  516. d = pi.prompt("Choose -=>", 5, name='choice', digits=True)
  517. d.addCallback(lambda ignore: pi.prompt("Times to execute script:", 5, name='count', digits=True))
  518. d.addCallback(got_need1)
  519. else:
  520. # We already have our target port, so...
  521. self.queue_game.put(self.r + self.nl + "Trading from {0} ({1}) to default {2} ({3}).".format(
  522. self.this_sector,
  523. self.game.portdata[self.this_sector]['port'],
  524. self.sector2, self.game.portdata[self.sector2]['port']) + self.nl
  525. )
  526. if self.game.portdata[self.this_sector]['port'] == self.game.portdata[self.sector2]['port']:
  527. self.queue_game.put("Hey dummy! Look out the window! These ports are the same class!" + nl)
  528. self.deactivate()
  529. return
  530. pi = PlayerInput(self.game)
  531. def got_need2(*_):
  532. if pi.keep['count'].strip() == '':
  533. self.deactivate()
  534. return
  535. self.times_left = int(pi.keep['count'])
  536. log.msg("Ok, I have:", pi.keep)
  537. # self.queue_game.put(pformat(pi.keep).replace("\n", "\n\r"))
  538. self.state = 5
  539. self.trade()
  540. self.queue_game.put(self.r + self.nl)
  541. d = pi.prompt("Times to execute script", 5, name='count')
  542. d.addCallback(got_need2)
  543. elif self.state == 7:
  544. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  545. # Done
  546. if self.this_sector == self.sector1:
  547. self.this_sector = self.sector2
  548. self.queue_player.put("{0}\r".format(self.sector2))
  549. self.state = 10
  550. else:
  551. self.times_left -= 1
  552. if self.times_left <= 0:
  553. # Ok, exit out
  554. self.deactivate()
  555. return
  556. self.this_sector = self.sector1
  557. self.queue_player.put("{0}\r".format(self.sector1))
  558. self.state = 10
  559. elif self.state == 8:
  560. # What are we trading
  561. # How many holds of Equipment do you want to buy [75]?
  562. if re.match(r"How many holds of .+ do you want to buy \[\d+\]\?", prompt):
  563. parts = prompt.split()
  564. trade_type = parts[4]
  565. if trade_type == 'Fuel':
  566. if (self.tpc in (5,7)) and (self.opc in (2,3,4,8)):
  567. # Can buy equipment - fuel ore is worthless.
  568. self.queue_player.put("0\r")
  569. return
  570. if (self.tpc in(4,7)) and (self.opc in (1,3,5,8)):
  571. # Can buy organics - fuel ore is worthless.
  572. self.queue_player.put("0\r")
  573. return
  574. if (self.tpc in (4,7,3,5)) and (self.opc in (3,4,5,7)):
  575. # No point in buying fuel ore if it can't be sold.
  576. self.queue_player.put("0\r")
  577. return
  578. elif trade_type == 'Organics':
  579. if (self.tpc in (6,7)) and (self.opc in (2,3,4,8)):
  580. # Can buy equipment - organics is worthless.
  581. self.queue_player.put("0\r")
  582. return
  583. if (self.tpc in (2,4,6,7)) and (self.opc in (2,4,6,7)):
  584. # No point in buying organics if it can't be sold.
  585. self.queue_player.put("0\r")
  586. return
  587. elif trade_type == 'Equipment':
  588. if (self.opc in (1,5,6,7)):
  589. # No point in buying equipment if it can't be sold.
  590. self.queue_player.put("0\r")
  591. return
  592. self.queue_player.put("\r")
  593. elif re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  594. # Done
  595. if self.this_sector == self.sector1:
  596. self.this_sector = self.sector2
  597. self.queue_player.put("{0}\r".format(self.sector2))
  598. self.state = 10
  599. else:
  600. self.times_left -= 1
  601. if self.times_left <= 0:
  602. # Ok, exit out
  603. self.deactivate()
  604. return
  605. self.this_sector = self.sector1
  606. self.queue_player.put("{0}\r".format(self.sector1))
  607. self.state = 10
  608. def trade(self, *_):
  609. # state 5
  610. log.msg("trade!")
  611. self.queue_player.put("pt") # Port Trade
  612. self.this_port = self.game.portdata[self.this_sector]
  613. if self.this_sector == self.sector1:
  614. self.other_port = self.game.portdata[self.sector2]
  615. else:
  616. self.other_port = self.game.portdata[self.sector1]
  617. # Ok, perform some calculations
  618. self.tpc = self.this_port['class']
  619. self.opc = self.other_port['class']
  620. # [ Items Status Trading % of max OnBoard]
  621. # [ ----- ------ ------- -------- -------]
  622. # [Fuel Ore Selling 2573 93% 0]
  623. # [Organics Buying 2960 100% 0]
  624. # [Equipment Buying 1958 86% 0]
  625. # []
  626. # []
  627. # [You have 1,000 credits and 20 empty cargo holds.]
  628. # []
  629. # [We are selling up to 2573. You have 0 in your holds.]
  630. # [How many holds of Fuel Ore do you want to buy [20]? 0]
  631. pass
  632. def game_line(self, line: str):
  633. if self.state == 1:
  634. # First exploration
  635. if line.startswith("Sector :"):
  636. # We have starting sector information
  637. parts = re.split("\s+", line)
  638. self.this_sector = int(parts[2])
  639. # These will be the ones swapped around as we trade back and forth.
  640. self.sector1 = self.this_sector
  641. elif line.startswith("Warps to Sector(s) : "):
  642. # Warps to Sector(s) : 397 - (562) - (639)
  643. _, _, warps = line.partition(':')
  644. warps = warps.replace('-', '').replace('(', '').replace(')', '').strip()
  645. log.msg("Warps: [{0}]".format(warps))
  646. self.warps = [ int(x) for x in re.split("\s+", warps)]
  647. log.msg("Warps: [{0}]".format(self.warps))
  648. self.state = 2
  649. elif self.state == 2:
  650. if line == "":
  651. # Ok, we're done
  652. self.state = 3
  653. # Check to see if we have information on any possible ports
  654. if hasattr(self.game, 'portdata'):
  655. if not self.this_sector in self.game.portdata:
  656. self.state = 0
  657. log.msg("Current sector {0} not in portdata.".format(self.this_sector))
  658. self.queue_game.put(self.r + self.nl + "I can't find the current sector in the portdata." + self.nl)
  659. self.deactivate()
  660. return
  661. else:
  662. # Ok, we are in the portdata
  663. pd = self.game.portdata[self.this_sector]
  664. if port_burnt(pd):
  665. log.msg("Current sector {0} port is burnt (<= 20%).".format(self.this_sector))
  666. self.queue_game.put(self.r + self.nl + "Current sector port is burnt out. <= 20%." + self.nl)
  667. self.deactivate()
  668. return
  669. possible = [ x for x in self.warps if x in self.game.portdata ]
  670. log.msg("Possible:", possible)
  671. # BUG: Sometimes links to another sector, don't link back!
  672. # This causes the game to plot a course / autopilot.
  673. if hasattr(self.game, 'warpdata'):
  674. # Great! verify that those warps link back to us!
  675. possible = [ x for x in possible if self.this_sector in self.game.warpdata[x]]
  676. if len(possible) == 0:
  677. self.state = 0
  678. self.queue_game.put(self.r + self.nl + "I don't see any ports in [{0}].".format(self.warps) + self.nl)
  679. self.deactivate()
  680. return
  681. possible = [ x for x in possible if not port_burnt(self.game.portdata[x]) ]
  682. log.msg("Possible:", possible)
  683. if len(possible) == 0:
  684. self.state = 0
  685. self.queue_game.put(self.r + self.nl + "I don't see any unburnt ports in [{0}].".format(self.warps) + self.nl)
  686. self.deactivate()
  687. return
  688. possible = [ x for x in possible if port_trading(self.game.portdata[self.this_sector]['port'], self.game.portdata[x]['port'])]
  689. self.possible = possible
  690. if len(possible) == 0:
  691. self.state = 0
  692. self.queue_game.put(self.r + self.nl + "I don't see any possible port trades in [{0}].".format(self.warps) + self.nl)
  693. self.deactivate()
  694. return
  695. elif len(possible) == 1:
  696. # Ok! there's only one!
  697. self.sector2 = possible[0]
  698. # Display possible ports:
  699. # spos = [ str(x) for x in possible]
  700. # self.queue_game.put(self.r + self.nl + self.nl.join(spos) + self.nl)
  701. # At state 3, we only get a prompt.
  702. return
  703. else:
  704. self.state = 0
  705. log.msg("We don't have any portdata!")
  706. self.queue_game.put(self.r + self.nl + "I have no portdata. Please run CIM Port Report." + self.nl)
  707. self.deactivate()
  708. return
  709. elif self.state == 5:
  710. if "-----" in line:
  711. self.state = 6
  712. elif self.state == 6:
  713. if "We are buying up to" in line:
  714. # Sell
  715. self.state = 7
  716. self.queue_player.put("\r")
  717. self.sell_perc = 100 + self.percent
  718. if "We are selling up to" in line:
  719. # Buy
  720. self.state = 8
  721. self.sell_perc = 100 - self.percent
  722. if "You don't have anything they want" in line:
  723. # Neither! DRAT!
  724. self.deactivate()
  725. return
  726. if "We're not interested." in line:
  727. log.msg("Try, try again. :(")
  728. self.state = 5
  729. self.trade()
  730. elif self.state == 7:
  731. # Haggle Sell
  732. if "We'll buy them for" in line or "Our final offer" in line:
  733. if "Our final offer" in line:
  734. self.sell_perc -= 1
  735. parts = line.replace(',', '').split()
  736. start_price = int(parts[4])
  737. price = start_price * self.sell_perc // 100
  738. log.msg("start: {0} % {1} price {2}".format(start_price, self.sell_perc, price))
  739. self.sell_perc -= 1
  740. self.queue_player.put("{0}\r".format(price))
  741. if "We are selling up to" in line:
  742. # Buy
  743. self.state = 8
  744. self.sell_perc = 100 - self.percent
  745. if line.startswith("You have ") and 'credits and' in line:
  746. parts = line.replace(',', '').split()
  747. credits = int(parts[2])
  748. if self.credits == 0:
  749. self.credits = credits
  750. else:
  751. if credits <= self.credits:
  752. log.msg("We don't appear to be making any money here {0}.".format(credits))
  753. self.deactivate()
  754. return
  755. if "We're not interested." in line:
  756. log.msg("Try, try again. :(")
  757. self.state = 5
  758. self.trade()
  759. elif self.state == 8:
  760. # Haggle Buy
  761. if "We'll sell them for" in line or "Our final offer" in line:
  762. if "Our final offer" in line:
  763. self.sell_perc += 1
  764. parts = line.replace(',', '').split()
  765. start_price = int(parts[4])
  766. price = start_price * self.sell_perc // 100
  767. log.msg("start: {0} % {1} price {2}".format(start_price, self.sell_perc, price))
  768. self.sell_perc += 1
  769. self.queue_player.put("{0}\r".format(price))
  770. if "We're not interested." in line:
  771. log.msg("Try, try again. :(")
  772. self.state = 5
  773. self.trade()
  774. elif self.state == 10:
  775. if "Sector : " in line:
  776. # Trade
  777. self.state = 5
  778. reactor.callLater(0, self.trade, 0)
  779. # self.trade()
  780. # elif self.state == 3:
  781. # log.msg("At state 3 [{0}]".format(line))
  782. # self.queue_game.put("At state 3.")
  783. # self.deactivate()
  784. # return
  785. class ProxyMenu(object):
  786. """ Display ProxyMenu
  787. Example:
  788. from flexible import ProxyMenu
  789. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  790. menu = ProxyMenu(self.game)
  791. """
  792. def __init__(self, game):
  793. self.nl = "\n\r"
  794. self.c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  795. self.r = Style.RESET_ALL
  796. self.c1 = merge(Style.BRIGHT + Fore.BLUE)
  797. self.c2 = merge(Style.NORMAL + Fore.CYAN)
  798. self.portdata = None
  799. self.game = game
  800. self.queue_game = game.queue_game
  801. self.observer = game.observer
  802. # Am I using self or game? (I think I want game, not self.)
  803. if hasattr(self.game, "portdata"):
  804. self.portdata = self.game.portdata
  805. else:
  806. self.portdata = {}
  807. if hasattr(self.game, 'warpdata'):
  808. self.warpdata = self.game.warpdata
  809. else:
  810. self.warpdata = {}
  811. # Yes, at this point we would activate
  812. self.prompt = game.buffer
  813. self.save = self.observer.save()
  814. self.observer.connect("player", self.player)
  815. # If we want it, it's here.
  816. self.defer = None
  817. self.keepalive = task.LoopingCall(self.awake)
  818. self.keepalive.start(30)
  819. self.menu()
  820. def __del__(self):
  821. log.msg("ProxyMenu {0} RIP".format(self))
  822. def whenDone(self):
  823. self.defer = defer.Deferred()
  824. # Call this to chain something after we exit.
  825. return self.defer
  826. def menu(self):
  827. self.queue_game.put(
  828. self.nl + self.c + "TradeWars Proxy active." + self.r + self.nl
  829. )
  830. def menu_item(ch: str, desc: str):
  831. self.queue_game.put(
  832. " " + self.c1 + ch + self.c2 + " - " + self.c1 + desc + self.nl
  833. )
  834. menu_item("D", "Diagnostics")
  835. # menu_item("Q", "Quest")
  836. if hasattr(self.game, 'portdata'):
  837. ports = len(self.game.portdata)
  838. else:
  839. ports = '?'
  840. menu_item("P", "Port CIM Report ({0})".format(ports))
  841. if hasattr(self.game, 'warpdata'):
  842. warps = len(self.game.warpdata)
  843. else:
  844. warps = '?'
  845. menu_item("W", "Warp CIM Report ({0})".format(warps))
  846. menu_item("T", "Trading Report")
  847. menu_item("S", "Scripts")
  848. menu_item("X", "eXit")
  849. self.queue_game.put(" " + self.c + "-=>" + self.r + " ")
  850. def awake(self):
  851. log.msg("ProxyMenu.awake()")
  852. self.game.queue_player.put(" ")
  853. def port_report(self, portdata: dict):
  854. self.portdata = portdata
  855. self.game.portdata = portdata
  856. self.queue_game.put("Loaded {0} ports.".format(len(portdata)) + self.nl)
  857. self.welcome_back()
  858. def warp_report(self, warpdata: dict):
  859. self.warpdata = warpdata
  860. self.game.warpdata = warpdata
  861. self.queue_game.put("Loaded {0} sectors.".format(len(warpdata)) + self.nl)
  862. self.welcome_back()
  863. def make_trade_report(self):
  864. log.msg("make_trade_report()")
  865. ok_trades = []
  866. best_trades = []
  867. def port_pct(port):
  868. return "{0:3},{1:3},{2:3}%".format(
  869. port['fuel']['pct'],
  870. port['org']['pct'],
  871. port['equ']['pct'])
  872. def port_show(sector, sector_port, warp, warp_port):
  873. sector_pct = port_pct(sector_port)
  874. warp_pct = port_pct(warp_port)
  875. return "{0:5} ({1}) {2}-=- {3:5} ({4}) {5}".format(
  876. sector,
  877. sector_port['port'],
  878. port_pct(sector_port),
  879. warp,
  880. warp_port['port'],
  881. port_pct(warp_port)
  882. )
  883. for sector, pd in self.game.portdata.items():
  884. if not port_burnt(pd):
  885. pc = pd['class']
  886. # Ok, let's look into it.
  887. if not sector in self.game.warpdata:
  888. continue
  889. warps = self.game.warpdata[sector]
  890. for w in warps:
  891. # Verify that we have that warp's info, and that the sector is in it.
  892. # (We can get back from it)
  893. if w in self.game.warpdata and sector in self.game.warpdata[w]:
  894. # Ok, we can get there -- and get back!
  895. if w > sector and w in self.game.portdata and not port_burnt(self.game.portdata[w]):
  896. # it is > and has a port.
  897. wd = self.game.portdata[w]
  898. wc = wd['class']
  899. # 1: "BBS",
  900. # 2: "BSB",
  901. # 3: "SBB",
  902. # 4: "SSB",
  903. # 5: "SBS",
  904. # 6: "BSS",
  905. # 7: "SSS",
  906. # 8: "BBB",
  907. if pc in (1,5) and wc in (2,4):
  908. best_trades.append(port_show(sector, pd, w, wd))
  909. # best_trades.append( "{0:5} -=- {1:5}".format(sector, w))
  910. elif pc in (2,4) and wc in (1,5):
  911. best_trades.append(port_show(sector, pd, w, wd))
  912. # best_trades.append( "{0:5} -=- {1:5}".format(sector, w))
  913. elif port_trading(pd['port'], self.game.portdata[w]['port']):
  914. # ok_trades.append( "{0:5} -=- {1:5}".format(sector,w))
  915. ok_trades.append(port_show(sector, pd, w, wd))
  916. yield
  917. self.trade_report.append("Best Trades: (org/equ)")
  918. self.trade_report.extend(best_trades)
  919. self.trade_report.append("Ok Trades:")
  920. self.trade_report.extend(ok_trades)
  921. # self.queue_game.put("BEST TRADES:" + self.nl + self.nl.join(best_trades) + self.nl)
  922. # self.queue_game.put("OK TRADES:" + self.nl + self.nl.join(ok_trades) + self.nl)
  923. def show_trade_report(self, *_):
  924. for t in self.trade_report:
  925. self.queue_game.put(t + self.nl)
  926. self.welcome_back()
  927. def player(self, chunk: bytes):
  928. """ Data from player (in bytes). """
  929. chunk = chunk.decode("utf-8", "ignore")
  930. key = chunk.upper()
  931. log.msg("ProxyMenu.player({0})".format(key))
  932. # Stop the keepalive if we are activating something else
  933. # or leaving...
  934. self.keepalive.stop()
  935. if key == "T":
  936. self.queue_game.put(self.c + key + self.r + self.nl)
  937. # do we have enough information to do this?
  938. if not hasattr(self.game, 'portdata') and not hasattr(self.game, 'warpdata'):
  939. self.queue_game.put("Missing portdata and warpdata." + self.nl)
  940. elif not hasattr(self.game, 'portdata'):
  941. self.queue_game.put("Missing portdata." + self.nl)
  942. elif not hasattr(self.game, 'warpdata'):
  943. self.queue_game.put("Missing warpdata." + self.nl)
  944. else:
  945. # Yes, so let's start!
  946. self.trade_report = []
  947. d = coiterate(self.make_trade_report())
  948. d.addCallback(self.show_trade_report)
  949. return
  950. elif key == "P":
  951. self.queue_game.put(self.c + key + self.r + self.nl)
  952. # Activate CIM Port Report
  953. report = CIMPortReport(self.game)
  954. d = report.whenDone()
  955. d.addCallback(self.port_report)
  956. d.addErrback(self.welcome_back)
  957. return
  958. elif key == "W":
  959. self.queue_game.put(self.c + key + self.r + self.nl)
  960. # Activate CIM Port Report
  961. report = CIMWarpReport(self.game)
  962. d = report.whenDone()
  963. d.addCallback(self.warp_report)
  964. d.addErrback(self.welcome_back)
  965. return
  966. elif key == "S":
  967. self.queue_game.put(self.c + key + self.r + self.nl)
  968. self.activate_scripts_menu()
  969. return
  970. elif key == "D":
  971. self.queue_game.put(self.c + key + self.r + self.nl)
  972. self.queue_game.put(pformat(self.portdata).replace("\n", "\n\r") + self.nl)
  973. self.queue_game.put(pformat(self.warpdata).replace("\n", "\n\r") + self.nl)
  974. elif key == "Q":
  975. self.queue_game.put(self.c + key + self.r + self.nl)
  976. # This is an example of chaining PlayerInput prompt calls.
  977. ask = PlayerInput(self.game)
  978. d = ask.prompt("What is your quest?", 40, name="quest", abort_blank=True)
  979. # Display the user's input
  980. d.addCallback(ask.output)
  981. d.addCallback(
  982. lambda ignore: ask.prompt(
  983. "What is your favorite color?", 10, name="color"
  984. )
  985. )
  986. d.addCallback(ask.output)
  987. d.addCallback(
  988. lambda ignore: ask.prompt(
  989. "What is the meaning of the squirrel?",
  990. 12,
  991. name="squirrel",
  992. digits=True,
  993. )
  994. )
  995. d.addCallback(ask.output)
  996. def show_values(show):
  997. log.msg(show)
  998. self.queue_game.put(pformat(show).replace("\n", "\n\r") + self.nl)
  999. d.addCallback(lambda ignore: show_values(ask.keep))
  1000. d.addCallback(self.welcome_back)
  1001. # On error, just return back
  1002. # This doesn't seem to be getting called.
  1003. # d.addErrback(lambda ignore: self.welcome_back)
  1004. d.addErrback(self.welcome_back)
  1005. return
  1006. elif key == "X":
  1007. self.queue_game.put(self.c + key + self.r + self.nl)
  1008. self.queue_game.put("Proxy done." + self.nl)
  1009. self.observer.load(self.save)
  1010. self.save = None
  1011. # It isn't running (NOW), so don't try to stop it.
  1012. # self.keepalive.stop()
  1013. self.keepalive = None
  1014. # Ok, this is a HORRIBLE idea, because the prompt might be
  1015. # outdated.
  1016. # self.queue_game.put(self.prompt)
  1017. self.prompt = None
  1018. # Possibly: Send '\r' to re-display the prompt
  1019. # instead of displaying the original one.
  1020. self.game.queue_player.put("d")
  1021. # Were we asked to do something when we were done here?
  1022. if self.defer:
  1023. reactor.CallLater(0, self.defer.callback)
  1024. # self.defer.callback()
  1025. self.defer = None
  1026. return
  1027. self.keepalive.start(30, True)
  1028. self.menu()
  1029. def activate_scripts_menu(self):
  1030. self.observer.disconnect("player", self.player)
  1031. self.observer.connect("player", self.scripts_player)
  1032. self.scripts_menu()
  1033. def deactivate_scripts_menu(self, *_):
  1034. self.observer.disconnect("player", self.scripts_player)
  1035. self.observer.connect("player", self.player)
  1036. self.welcome_back()
  1037. def scripts_menu(self, *_):
  1038. c1 = merge(Style.BRIGHT + Fore.CYAN)
  1039. c2 = merge(Style.NORMAL + Fore.CYAN)
  1040. def menu_item(ch, desc):
  1041. self.queue_game.put(
  1042. " " + c1 + ch + c2 + " - " + c1 + desc + self.nl
  1043. )
  1044. menu_item("1", "Ports (Trades between two sectors)")
  1045. menu_item("2", "TODO")
  1046. menu_item("3", "TODO")
  1047. menu_item("X", "eXit")
  1048. self.queue_game.put(" " + c1 + "-=>" + self.r + " ")
  1049. def scripts_player(self, chunk: bytes):
  1050. """ Data from player (in bytes). """
  1051. chunk = chunk.decode("utf-8", "ignore")
  1052. key = chunk.upper()
  1053. if key == '1':
  1054. self.queue_game.put(self.c + key + self.r + self.nl)
  1055. # Activate this magical event here
  1056. ports = ScriptPort(self.game)
  1057. d = ports.whenDone()
  1058. # d.addCallback(self.scripts_menu)
  1059. # d.addErrback(self.scripts_menu)
  1060. d.addCallback(self.deactivate_scripts_menu)
  1061. d.addErrback(self.deactivate_scripts_menu)
  1062. return
  1063. elif key == 'X':
  1064. self.queue_game.put(self.c + key + self.r + self.nl)
  1065. self.deactivate_scripts_menu()
  1066. return
  1067. else:
  1068. self.queue_game.put(self.c + "?" + self.r + self.nl)
  1069. self.scripts_menu()
  1070. def welcome_back(self, *_):
  1071. log.msg("welcome_back")
  1072. self.keepalive.start(30, True)
  1073. self.menu()