flexible.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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. def __init__(self, game):
  14. # I think game gives us access to everything we need
  15. self.game = game
  16. self.observer = self.game.observer
  17. self.save = None
  18. self.deferred = None
  19. self.queue_game = game.queue_game
  20. self.keep = {}
  21. # default colors
  22. self.c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  23. self.cp = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  24. # useful consts
  25. self.r = Style.RESET_ALL
  26. self.nl = "\n\r"
  27. self.bsb = "\b \b"
  28. self.keepalive = None
  29. def color(self, c):
  30. self.c = c
  31. def colorp(self, cp):
  32. self.cp = cp
  33. def alive(self):
  34. log.msg("PlayerInput.alive()")
  35. self.game.queue_player.put(" ")
  36. def prompt(self, user_prompt, limit, **kw):
  37. """ Generate prompt for user input.
  38. prompt = text displayed.
  39. limit = # of characters allowed.
  40. default = (text to default to)
  41. keywords:
  42. abort_blank : Abort if they give us blank text.
  43. name : Stores the input in self.keep dict.
  44. """
  45. log.msg("PlayerInput({0}, {1}, {2}".format(user_prompt, limit, kw))
  46. self.limit = limit
  47. self.input = ""
  48. self.kw = kw
  49. assert self.save is None
  50. assert self.keepalive is None
  51. # Note: This clears out the server "keep alive"
  52. self.save = self.observer.save()
  53. self.observer.connect("player", self.get_input)
  54. self.keepalive = task.LoopingCall(self.alive)
  55. self.keepalive.start(30)
  56. # We need to "hide" the game output.
  57. # Otherwise it WITH mess up the user input display.
  58. self.to_player = self.game.to_player
  59. self.game.to_player = False
  60. # Display prompt
  61. # self.queue_game.put(self.r + self.nl + self.c + user_prompt + " " + self.cp)
  62. self.queue_game.put(self.r + self.c + user_prompt + self.r + " " + self.cp)
  63. # Set "Background of prompt"
  64. self.queue_game.put(" " * limit + "\b" * limit)
  65. assert self.deferred is None
  66. d = defer.Deferred()
  67. self.deferred = d
  68. log.msg("Return deferred ...", self.deferred)
  69. return d
  70. def get_input(self, chunk):
  71. """ Data from player (in bytes) """
  72. chunk = chunk.decode("utf-8", "ignore")
  73. for ch in chunk:
  74. if ch == "\b":
  75. if len(self.input) > 0:
  76. self.queue_game.put(self.bsb)
  77. self.input = self.input[0:-1]
  78. else:
  79. self.queue_game.put("\a")
  80. elif ch == "\r":
  81. self.queue_game.put(self.r + self.nl)
  82. log.msg("Restore observer dispatch", self.save)
  83. assert not self.save is None
  84. self.observer.load(self.save)
  85. self.save = None
  86. log.msg("Disable keepalive")
  87. self.keepalive.stop()
  88. self.keepalive = None
  89. line = self.input
  90. self.input = ""
  91. assert not self.deferred is None
  92. self.game.to_player = self.to_player
  93. # If they gave us the keyword name, save the value as that name
  94. if "name" in self.kw:
  95. self.keep[self.kw["name"]] = line
  96. if "abort_blank" in self.kw and self.kw["abort_blank"]:
  97. # Abort on blank input
  98. if line.strip() == "":
  99. # Yes, input is blank, abort.
  100. log.msg("errback, abort_blank")
  101. reactor.callLater(
  102. 0, self.deferred.errback, Exception("abort_blank")
  103. )
  104. self.deferred = None
  105. return
  106. # Ok, use deferred.callback, or reactor.callLater?
  107. # self.deferred.callback(line)
  108. reactor.callLater(0, self.deferred.callback, line)
  109. self.deferred = None
  110. return
  111. elif ch.isprintable():
  112. # Printable, but is it acceptable?
  113. if "digits" in self.kw:
  114. if not ch.isdigit():
  115. self.queue_game.put("\a")
  116. continue
  117. if len(self.input) + 1 <= self.limit:
  118. self.input += ch
  119. self.queue_game.put(ch)
  120. else:
  121. self.queue_game.put("\a")
  122. def output(self, line):
  123. """ A default display of what they just input. """
  124. log.msg("PlayerInput.output({0})".format(line))
  125. self.game.queue_game.put(self.r + "[{0}]".format(line) + self.nl)
  126. return line
  127. PORT_CLASSES = {
  128. 1: "BBS",
  129. 2: "BSB",
  130. 3: "SBB",
  131. 4: "SSB",
  132. 5: "SBS",
  133. 6: "BSS",
  134. 7: "SSS",
  135. 8: "BBB",
  136. }
  137. CLASSES_PORT = {v: k for k, v in PORT_CLASSES.items()}
  138. import re
  139. class CIMPortReport(object):
  140. def __init__(self, game):
  141. self.game = game
  142. self.queue_game = game.queue_game
  143. self.queue_player = game.queue_player
  144. self.observer = game.observer
  145. # Yes, at this point we would activate
  146. self.prompt = game.buffer
  147. self.save = self.observer.save()
  148. # I actually don't want the player input, but I'll grab it anyway.
  149. self.observer.connect("player", self.player)
  150. self.observer.connect("prompt", self.game_prompt)
  151. self.observer.connect("game-line", self.game_line)
  152. # If we want it, it's here.
  153. self.defer = None
  154. self.to_player = self.game.to_player
  155. log.msg("to_player (stored)", self.to_player)
  156. # Hide what's happening from the player
  157. self.game.to_player = False
  158. self.queue_player.put("^") # Activate CIM
  159. self.state = 1
  160. self.portdata = {}
  161. self.portcycle = cycle(["/", "-", "\\", "|"])
  162. def game_prompt(self, prompt):
  163. if prompt == ": ":
  164. if self.state == 1:
  165. # Ok, then we're ready to request the port report
  166. self.portcycle = cycle(["/", "-", "\\", "|"])
  167. self.queue_player.put("R")
  168. self.state = 2
  169. if self.state == 2:
  170. self.queue_player.put("Q")
  171. self.state = 3
  172. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  173. if self.state == 3:
  174. # Ok, time to exit
  175. # exit from this...
  176. self.game.to_player = self.to_player
  177. self.observer.load(self.save)
  178. self.save = None
  179. self.game.portdata = self.portdata
  180. self.queue_game.put("\b \b\r\n")
  181. if not self.defer is None:
  182. self.defer.callback(self.portdata)
  183. self.defer = None
  184. def game_line(self, line):
  185. if line == "" or line == ": ":
  186. return
  187. if line == ": ENDINTERROG":
  188. return
  189. # This should be the CIM Report Data -- parse it
  190. if self.portcycle:
  191. if len(self.portdata) % 10 == 0:
  192. self.queue_game.put("\b" + next(self.portcycle))
  193. work = line.replace("%", "")
  194. parts = re.split(r"(?<=\d)\s", work)
  195. if len(parts) == 8:
  196. port = int(parts[0].strip())
  197. data = dict()
  198. def portBS(info):
  199. if info[0] == "-":
  200. bs = "B"
  201. else:
  202. bs = "S"
  203. return (bs, int(info[1:].strip()))
  204. data["fuel"] = dict()
  205. data["fuel"]["sale"], data["fuel"]["units"] = portBS(parts[1])
  206. data["fuel"]["pct"] = int(parts[2].strip())
  207. data["org"] = dict()
  208. data["org"]["sale"], data["org"]["units"] = portBS(parts[3])
  209. data["org"]["pct"] = int(parts[4].strip())
  210. data["equ"] = dict()
  211. data["equ"]["sale"], data["equ"]["units"] = portBS(parts[5])
  212. data["equ"]["pct"] = int(parts[6].strip())
  213. # Store what this port is buying/selling
  214. data["port"] = (
  215. data["fuel"]["sale"] + data["org"]["sale"] + data["equ"]["sale"]
  216. )
  217. # Convert BBS/SBB to Class number 1-8
  218. data["class"] = CLASSES_PORT[data["port"]]
  219. self.portdata[port] = data
  220. else:
  221. log.msg("CIMPortReport:", line, "???")
  222. def __del__(self):
  223. log.msg("CIMPortReport {0} RIP".format(self))
  224. def whenDone(self):
  225. self.defer = defer.Deferred()
  226. # Call this to chain something after we exit.
  227. return self.defer
  228. def player(self, chunk):
  229. """ Data from player (in bytes). """
  230. chunk = chunk.decode("utf-8", "ignore")
  231. key = chunk.upper()
  232. log.msg("CIMPortReport.player({0}) : I AM stopping...".format(key))
  233. # Stop the keepalive if we are activating something else
  234. # or leaving...
  235. # self.keepalive.stop()
  236. self.queue_game.put("\b \b\r\n")
  237. if not self.defer is None:
  238. # We have something, so:
  239. self.game.to_player = self.to_player
  240. self.observer.load(self.save)
  241. self.save = None
  242. self.defer.errback(Exception("User Abort"))
  243. self.defer = None
  244. else:
  245. # Still "exit" out.
  246. self.game.to_player = self.to_player
  247. self.observer.load(self.save)
  248. class ProxyMenu(object):
  249. def __init__(self, game):
  250. self.nl = "\n\r"
  251. self.c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  252. self.r = Style.RESET_ALL
  253. self.c1 = merge(Style.BRIGHT + Fore.BLUE)
  254. self.c2 = merge(Style.NORMAL + Fore.CYAN)
  255. self.portdata = None
  256. self.game = game
  257. self.queue_game = game.queue_game
  258. self.observer = game.observer
  259. if hasattr(self.game, "portdata"):
  260. self.portdata = self.game.portdata
  261. else:
  262. self.portdata = {}
  263. # Yes, at this point we would activate
  264. self.prompt = game.buffer
  265. self.save = self.observer.save()
  266. self.observer.connect("player", self.player)
  267. # If we want it, it's here.
  268. self.defer = None
  269. self.keepalive = task.LoopingCall(self.awake)
  270. self.keepalive.start(30)
  271. self.menu()
  272. def __del__(self):
  273. log.msg("ProxyMenu {0} RIP".format(self))
  274. def whenDone(self):
  275. self.defer = defer.Deferred()
  276. # Call this to chain something after we exit.
  277. return self.defer
  278. def menu(self):
  279. self.queue_game.put(
  280. self.nl + self.c + "TradeWars Proxy active." + self.r + self.nl
  281. )
  282. def menu_item(ch, desc):
  283. self.queue_game.put(
  284. " " + self.c1 + ch + self.c2 + " - " + self.c1 + desc + self.nl
  285. )
  286. menu_item("D", "Diagnostics")
  287. menu_item("Q", "Quest")
  288. menu_item("T", "Display current Time")
  289. menu_item("P", "Port CIM Report")
  290. menu_item("S", "Scripts")
  291. menu_item("X", "eXit")
  292. self.queue_game.put(" " + self.c + "-=>" + self.r + " ")
  293. def awake(self):
  294. log.msg("ProxyMenu.awake()")
  295. self.game.queue_player.put(" ")
  296. def port_report(self, portdata):
  297. self.portdata = portdata
  298. self.queue_game.put("Loaded {0} records.".format(len(portdata)) + self.nl)
  299. self.welcome_back()
  300. def player(self, chunk):
  301. """ Data from player (in bytes). """
  302. chunk = chunk.decode("utf-8", "ignore")
  303. key = chunk.upper()
  304. log.msg("ProxyMenu.player({0})".format(key))
  305. # Stop the keepalive if we are activating something else
  306. # or leaving...
  307. self.keepalive.stop()
  308. if key == "T":
  309. self.queue_game.put(self.c + key + self.r + self.nl)
  310. # perform T option
  311. now = pendulum.now()
  312. self.queue_game.put(
  313. self.nl + self.c1 + "Current time " + now.to_datetime_string() + self.nl
  314. )
  315. elif key == "P":
  316. self.queue_game.put(self.c + key + self.r + self.nl)
  317. # Activate CIM Port Report
  318. report = CIMPortReport(self.game)
  319. d = report.whenDone()
  320. d.addCallback(self.port_report)
  321. d.addErrback(self.welcome_back)
  322. return
  323. elif key == "S":
  324. self.queue_game.put(self.c + key + self.r + self.nl)
  325. elif key == "D":
  326. self.queue_game.put(self.c + key + self.r + self.nl)
  327. self.queue_game.put(pformat(self.portdata).replace("\n", "\n\r") + self.nl)
  328. elif key == "Q":
  329. self.queue_game.put(self.c + key + self.r + self.nl)
  330. # This is an example of chaining PlayerInput prompt calls.
  331. ask = PlayerInput(self.game)
  332. d = ask.prompt("What is your quest?", 40, name="quest", abort_blank=True)
  333. # Display the user's input
  334. d.addCallback(ask.output)
  335. d.addCallback(
  336. lambda ignore: ask.prompt(
  337. "What is your favorite color?", 10, name="color"
  338. )
  339. )
  340. d.addCallback(ask.output)
  341. d.addCallback(
  342. lambda ignore: ask.prompt(
  343. "What is the meaning of the squirrel?",
  344. 12,
  345. name="squirrel",
  346. digits=True,
  347. )
  348. )
  349. d.addCallback(ask.output)
  350. def show_values(show):
  351. log.msg(show)
  352. self.queue_game.put(pformat(show).replace("\n", "\n\r") + self.nl)
  353. d.addCallback(lambda ignore: show_values(ask.keep))
  354. d.addCallback(self.welcome_back)
  355. # On error, just return back
  356. # This doesn't seem to be getting called.
  357. # d.addErrback(lambda ignore: self.welcome_back)
  358. d.addErrback(self.welcome_back)
  359. return
  360. elif key == "X":
  361. self.queue_game.put(self.c + key + self.r + self.nl)
  362. self.observer.load(self.save)
  363. self.save = None
  364. # It isn't running (NOW), so don't try to stop it.
  365. # self.keepalive.stop()
  366. self.keepalive = None
  367. self.queue_game.put(self.prompt)
  368. self.prompt = None
  369. # Possibly: Send '\r' to re-display the prompt
  370. # instead of displaying the original one.
  371. # Were we asked to do something when we were done here?
  372. if self.defer:
  373. reactor.CallLater(0, self.defer.callback)
  374. # self.defer.callback()
  375. self.defer = None
  376. return
  377. self.keepalive.start(30, True)
  378. self.menu()
  379. def welcome_back(self, *_):
  380. log.msg("welcome_back")
  381. self.keepalive.start(30, True)
  382. self.menu()