flexible.py 48 KB

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