flexible.py 39 KB

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