flexible.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828
  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. from galaxy import GameData, PORT_CLASSES, CLASSES_PORT
  12. class SpinningCursor(object):
  13. """ Spinner class, that handles every so many clicks
  14. s = SpinningCursor(5) # every 5
  15. for x in range(10):
  16. if s.click():
  17. print(s.cycle())
  18. """
  19. def __init__(self, every=10):
  20. self.itercycle = cycle(["/", "-", "\\", "|"])
  21. self.count = 0
  22. self.every = every
  23. def reset(self):
  24. self.itercycle = cycle(["/", "-", "\\", "|"])
  25. self.count = 0
  26. def click(self):
  27. self.count += 1
  28. return self.count % self.every == 0
  29. def cycle(self):
  30. return next(self.itercycle)
  31. def merge(color_string):
  32. """ Given a string of colorama ANSI, merge them if you can. """
  33. return color_string.replace("m\x1b[", ";")
  34. class PlayerInput(object):
  35. """ Player Input
  36. Example:
  37. from flexible import PlayerInput
  38. ask = PlayerInput(self.game)
  39. # abort_blank means, if the input field is blank, abort. Use error_back.
  40. d = ask.prompt("What is your quest?", 40, name="quest", abort_blank=True)
  41. # Display the user's input / but not needed.
  42. d.addCallback(ask.output)
  43. d.addCallback(
  44. lambda ignore: ask.prompt(
  45. "What is your favorite color?", 10, name="color"
  46. )
  47. )
  48. d.addCallback(ask.output)
  49. d.addCallback(
  50. lambda ignore: ask.prompt(
  51. "What is your least favorite number?",
  52. 12,
  53. name="number",
  54. digits=True,
  55. )
  56. )
  57. d.addCallback(ask.output)
  58. def show_values(show):
  59. log.msg(show)
  60. self.queue_game.put(pformat(show).replace("\n", "\n\r") + self.nl)
  61. d.addCallback(lambda ignore: show_values(ask.keep))
  62. d.addCallback(self.welcome_back)
  63. # On error, just return back
  64. d.addErrback(self.welcome_back)
  65. """
  66. def __init__(self, game):
  67. # I think game gives us access to everything we need
  68. self.game = game
  69. self.observer = self.game.observer
  70. self.save = None
  71. self.deferred = None
  72. self.queue_game = game.queue_game
  73. self.keep = {}
  74. # default colors
  75. self.c = merge(Style.BRIGHT + Fore.WHITE + Back.BLUE)
  76. self.cp = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  77. # useful consts
  78. self.r = Style.RESET_ALL
  79. self.nl = "\n\r"
  80. self.bsb = "\b \b"
  81. self.keepalive = None
  82. def color(self, c):
  83. self.c = c
  84. def colorp(self, cp):
  85. self.cp = cp
  86. def alive(self):
  87. log.msg("PlayerInput.alive()")
  88. self.game.queue_player.put(" ")
  89. def prompt(self, user_prompt, limit, **kw):
  90. """ Generate prompt for user input.
  91. Note: This returns deferred.
  92. prompt = text displayed.
  93. limit = # of characters allowed.
  94. default = (text to default to)
  95. keywords:
  96. abort_blank : Abort if they give us blank text.
  97. name : Stores the input in self.keep dict.
  98. digits : Only allow 0-9 to be entered.
  99. """
  100. log.msg("PlayerInput({0}, {1}, {2}".format(user_prompt, limit, kw))
  101. self.limit = limit
  102. self.input = ""
  103. self.kw = kw
  104. assert self.save is None
  105. assert self.keepalive is None
  106. # Note: This clears out the server "keep alive"
  107. self.save = self.observer.save()
  108. self.observer.connect("player", self.get_input)
  109. self.keepalive = task.LoopingCall(self.alive)
  110. self.keepalive.start(30)
  111. # We need to "hide" the game output.
  112. # Otherwise it WITH mess up the user input display.
  113. self.to_player = self.game.to_player
  114. self.game.to_player = False
  115. # Display prompt
  116. # self.queue_game.put(self.r + self.nl + self.c + user_prompt + " " + self.cp)
  117. self.queue_game.put(self.r + self.c + user_prompt + self.r + " " + self.cp)
  118. # Set "Background of prompt"
  119. self.queue_game.put(" " * limit + "\b" * limit)
  120. assert self.deferred is None
  121. d = defer.Deferred()
  122. self.deferred = d
  123. log.msg("Return deferred ...", self.deferred)
  124. return d
  125. def get_input(self, chunk):
  126. """ Data from player (in bytes) """
  127. chunk = chunk.decode("latin-1", "ignore")
  128. for ch in chunk:
  129. if ch == "\b":
  130. if len(self.input) > 0:
  131. self.queue_game.put(self.bsb)
  132. self.input = self.input[0:-1]
  133. else:
  134. self.queue_game.put("\a")
  135. elif ch == "\r":
  136. self.queue_game.put(self.r + self.nl)
  137. log.msg("Restore observer dispatch", self.save)
  138. assert not self.save is None
  139. self.observer.load(self.save)
  140. self.save = None
  141. log.msg("Disable keepalive")
  142. self.keepalive.stop()
  143. self.keepalive = None
  144. line = self.input
  145. self.input = ""
  146. assert not self.deferred is None
  147. self.game.to_player = self.to_player
  148. # If they gave us the keyword name, save the value as that name
  149. if "name" in self.kw:
  150. self.keep[self.kw["name"]] = line
  151. if "abort_blank" in self.kw and self.kw["abort_blank"]:
  152. # Abort on blank input
  153. if line.strip() == "":
  154. # Yes, input is blank, abort.
  155. log.msg("errback, abort_blank")
  156. reactor.callLater(
  157. 0, self.deferred.errback, Exception("abort_blank")
  158. )
  159. self.deferred = None
  160. return
  161. # Ok, use deferred.callback, or reactor.callLater?
  162. # self.deferred.callback(line)
  163. reactor.callLater(0, self.deferred.callback, line)
  164. self.deferred = None
  165. return
  166. elif ch.isprintable():
  167. # Printable, but is it acceptable?
  168. if "digits" in self.kw:
  169. if not ch.isdigit():
  170. self.queue_game.put("\a")
  171. continue
  172. if len(self.input) + 1 <= self.limit:
  173. self.input += ch
  174. self.queue_game.put(ch)
  175. else:
  176. self.queue_game.put("\a")
  177. def output(self, line):
  178. """ A default display of what they just input. """
  179. log.msg("PlayerInput.output({0})".format(line))
  180. self.game.queue_game.put(self.r + "[{0}]".format(line) + self.nl)
  181. return line
  182. import re
  183. # The CIMWarpReport -- is only needed if the json file gets damaged in some way.
  184. # or needs to be reset. The warps should automatically update themselves now.
  185. class CIMWarpReport(object):
  186. def __init__(self, game):
  187. self.game = game
  188. self.queue_game = game.queue_game
  189. self.queue_player = game.queue_player
  190. self.observer = game.observer
  191. # Yes, at this point we would activate
  192. self.prompt = game.buffer
  193. self.save = self.observer.save()
  194. # I actually don't want the player input, but I'll grab it anyway.
  195. self.observer.connect("player", self.player)
  196. self.observer.connect("prompt", self.game_prompt)
  197. self.observer.connect("game-line", self.game_line)
  198. # If we want it, it's here.
  199. self.defer = None
  200. self.to_player = self.game.to_player
  201. # Hide what's happening from the player
  202. self.game.to_player = False
  203. self.queue_player.put("^") # Activate CIM
  204. self.state = 1
  205. # self.warpdata = {}
  206. self.warpcycle = SpinningCursor()
  207. def game_prompt(self, prompt):
  208. if prompt == ": ":
  209. if self.state == 1:
  210. # Ok, then we're ready to request the port report
  211. self.warpcycle.reset()
  212. self.queue_player.put("I")
  213. self.state = 2
  214. elif self.state == 2:
  215. self.queue_player.put("Q")
  216. self.state = 3
  217. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  218. if self.state == 3:
  219. # Ok, time to exit
  220. # exit from this...
  221. self.game.to_player = self.to_player
  222. self.observer.load(self.save)
  223. self.save = None
  224. # self.game.warpdata = self.warpdata
  225. self.queue_game.put("\b \b\r\n")
  226. if not self.defer is None:
  227. self.defer.callback(self.game.gamedata.warps)
  228. self.defer = None
  229. def game_line(self, line):
  230. if line == "" or line == ": ":
  231. return
  232. if line == ": ENDINTERROG":
  233. return
  234. if line.startswith('Command [TL='):
  235. return
  236. # This should be the CIM Report Data -- parse it
  237. if self.warpcycle:
  238. if self.warpcycle.click():
  239. self.queue_game.put("\b" + self.warpcycle.cycle())
  240. work = line.strip()
  241. parts = re.split(r"(?<=\d)\s", work)
  242. parts = [int(x) for x in parts]
  243. sector = parts.pop(0)
  244. # tuples are nicer on memory, and the warpdata map isn't going to be changing.
  245. # self.warpdata[sector] = tuple(parts)
  246. self.game.gamedata.warp_to(sector, *parts)
  247. def __del__(self):
  248. log.msg("CIMWarpReport {0} RIP".format(self))
  249. def whenDone(self):
  250. self.defer = defer.Deferred()
  251. # Call this to chain something after we exit.
  252. return self.defer
  253. def player(self, chunk):
  254. """ Data from player (in bytes). """
  255. chunk = chunk.decode("latin-1", "ignore")
  256. key = chunk.upper()
  257. log.msg("CIMWarpReport.player({0}) : I AM stopping...".format(key))
  258. # Stop the keepalive if we are activating something else
  259. # or leaving...
  260. # self.keepalive.stop()
  261. self.queue_game.put("\b \b\r\n")
  262. if not self.defer is None:
  263. # We have something, so:
  264. self.game.to_player = self.to_player
  265. self.observer.load(self.save)
  266. self.save = None
  267. self.defer.errback(Exception("User Abort"))
  268. self.defer = None
  269. else:
  270. # Still "exit" out.
  271. self.game.to_player = self.to_player
  272. self.observer.load(self.save)
  273. # the CIMPortReport will still be needed.
  274. # We can't get fresh report data (that changes) any other way.
  275. class CIMPortReport(object):
  276. """ Parse data from CIM Port Report
  277. Example:
  278. from flexible import CIMPortReport
  279. report = CIMPortReport(self.game)
  280. d = report.whenDone()
  281. d.addCallback(self.port_report)
  282. d.addErrback(self.welcome_back)
  283. def port_report(self, portdata):
  284. self.portdata = portdata
  285. self.queue_game.put("Loaded {0} records.".format(len(portdata)) + self.nl)
  286. self.welcome_back()
  287. def welcome_back(self,*_):
  288. ... restore keep alive timers, etc.
  289. """
  290. def __init__(self, game):
  291. self.game = game
  292. self.queue_game = game.queue_game
  293. self.queue_player = game.queue_player
  294. self.observer = game.observer
  295. # Yes, at this point we would activate
  296. self.prompt = game.buffer
  297. self.save = self.observer.save()
  298. # I actually don't want the player input, but I'll grab it anyway.
  299. self.observer.connect("player", self.player)
  300. self.observer.connect("prompt", self.game_prompt)
  301. self.observer.connect("game-line", self.game_line)
  302. # If we want it, it's here.
  303. self.defer = None
  304. self.to_player = self.game.to_player
  305. log.msg("to_player (stored)", self.to_player)
  306. # Hide what's happening from the player
  307. self.game.to_player = False
  308. self.queue_player.put("^") # Activate CIM
  309. self.state = 1
  310. # self.portdata = {}
  311. self.portcycle = SpinningCursor()
  312. def game_prompt(self, prompt):
  313. if prompt == ": ":
  314. if self.state == 1:
  315. # Ok, then we're ready to request the port report
  316. self.portcycle.reset()
  317. self.queue_player.put("R")
  318. self.state = 2
  319. elif self.state == 2:
  320. self.queue_player.put("Q")
  321. self.state = 3
  322. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  323. if self.state == 3:
  324. # Ok, time to exit
  325. # exit from this...
  326. self.game.to_player = self.to_player
  327. self.observer.load(self.save)
  328. self.save = None
  329. # self.game.portdata = self.portdata
  330. self.queue_game.put("\b \b\r\n")
  331. if not self.defer is None:
  332. self.defer.callback(self.game.gamedata.ports)
  333. self.defer = None
  334. def game_line(self, line: str):
  335. if line == "" or line == ": ":
  336. return
  337. if line == ": ENDINTERROG":
  338. return
  339. # This should be the CIM Report Data -- parse it
  340. if self.portcycle:
  341. if self.portcycle.click():
  342. self.queue_game.put("\b" + self.portcycle.cycle())
  343. work = line.replace("%", "")
  344. parts = re.split(r"(?<=\d)\s", work)
  345. if len(parts) == 8:
  346. port = int(parts[0].strip())
  347. data = dict()
  348. def portBS(info):
  349. if info[0] == "-":
  350. bs = "B"
  351. else:
  352. bs = "S"
  353. return (bs, int(info[1:].strip()))
  354. data["fuel"] = dict()
  355. data["fuel"]["sale"], data["fuel"]["units"] = portBS(parts[1])
  356. data["fuel"]["pct"] = int(parts[2].strip())
  357. data["org"] = dict()
  358. data["org"]["sale"], data["org"]["units"] = portBS(parts[3])
  359. data["org"]["pct"] = int(parts[4].strip())
  360. data["equ"] = dict()
  361. data["equ"]["sale"], data["equ"]["units"] = portBS(parts[5])
  362. data["equ"]["pct"] = int(parts[6].strip())
  363. # Store what this port is buying/selling
  364. data["port"] = (
  365. data["fuel"]["sale"] + data["org"]["sale"] + data["equ"]["sale"]
  366. )
  367. # Convert BBS/SBB to Class number 1-8
  368. data["class"] = CLASSES_PORT[data["port"]]
  369. self.game.gamedata.set_port(port, data)
  370. # self.portdata[port] = data
  371. else:
  372. log.msg("CIMPortReport:", line, "???")
  373. def __del__(self):
  374. log.msg("CIMPortReport {0} RIP".format(self))
  375. def whenDone(self):
  376. self.defer = defer.Deferred()
  377. # Call this to chain something after we exit.
  378. return self.defer
  379. def player(self, chunk):
  380. """ Data from player (in bytes). """
  381. chunk = chunk.decode("latin-1", "ignore")
  382. key = chunk.upper()
  383. log.msg("CIMPortReport.player({0}) : I AM stopping...".format(key))
  384. # Stop the keepalive if we are activating something else
  385. # or leaving...
  386. # self.keepalive.stop()
  387. self.queue_game.put("\b \b\r\n")
  388. if not self.defer is None:
  389. # We have something, so:
  390. self.game.to_player = self.to_player
  391. self.observer.load(self.save)
  392. self.save = None
  393. self.defer.errback(Exception("User Abort"))
  394. self.defer = None
  395. else:
  396. # Still "exit" out.
  397. self.game.to_player = self.to_player
  398. self.observer.load(self.save)
  399. class ScriptPort(object):
  400. """ Performs the Port script.
  401. This is close to the original.
  402. We don't ask for the port to trade with --
  403. because that information is available to us after "D" (display).
  404. We look at the adjacent sectors, and see if we know any ports.
  405. If the ports are burnt (< 20%), we remove them from the list.
  406. If there's just one, we use it. Otherwise we ask them to choose.
  407. """
  408. def __init__(self, game):
  409. self.game = game
  410. self.queue_game = game.queue_game
  411. self.queue_player = game.queue_player
  412. self.observer = game.observer
  413. self.r = Style.RESET_ALL
  414. self.nl = "\n\r"
  415. self.this_sector = None # Starting sector
  416. self.sector1 = None # Current Sector
  417. self.sector2 = None # Next Sector Stop
  418. self.percent = 5 # Stick with the good default.
  419. self.credits = 0
  420. self.last_credits = None
  421. self.times_left = 0
  422. # Activate
  423. self.prompt = game.buffer
  424. self.save = self.observer.save()
  425. self.observer.connect('player', self.player)
  426. self.observer.connect("prompt", self.game_prompt)
  427. self.observer.connect("game-line", self.game_line)
  428. self.defer = None
  429. self.queue_game.put(
  430. self.nl + "Script based on: Port Pair Trading v2.00" + self.r + self.nl
  431. )
  432. self.possible_sectors = None
  433. self.state = 1
  434. self.queue_player.put("D")
  435. # Original, send 'D' to display current sector.
  436. # We could get the sector number from the self.prompt string -- HOWEVER:
  437. # IF! We send 'D', we can also get the sectors around -- we might not even need to
  438. # prompt for sector to trade with (we could possibly figure it out ourselves).
  439. # [Command [TL=00:00:00]:[967] (?=Help)? : D]
  440. # [<Re-Display>]
  441. # []
  442. # [Sector : 967 in uncharted space.]
  443. # [Planets : (M) Into the Darkness]
  444. # [Warps to Sector(s) : 397 - (562) - (639)]
  445. # []
  446. def whenDone(self):
  447. self.defer = defer.Deferred()
  448. # Call this to chain something after we exit.
  449. return self.defer
  450. def deactivate(self):
  451. self.state = 0
  452. log.msg("ScriptPort.deactivate ({0})".format(self.times_left))
  453. assert(not self.save is None)
  454. self.observer.load(self.save)
  455. self.save = None
  456. if self.defer:
  457. self.defer.callback('done')
  458. self.defer = None
  459. def player(self, chunk: bytes):
  460. # If we receive anything -- ABORT!
  461. self.deactivate()
  462. def game_prompt(self, prompt: str):
  463. log.msg("{0} : {1}".format(self.state, prompt))
  464. if self.state == 3:
  465. log.msg("game_prompt: ", prompt)
  466. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  467. self.state = 4
  468. log.msg("Ok, state 4")
  469. if self.sector2 is None:
  470. # Ok, we need to prompt for this.
  471. self.queue_game.put(self.r + self.nl +
  472. "Which sector to trade with? {0}".format(GameData.port_show_part(self.this_sector, self.game.gamedata.ports[self.this_sector])) +
  473. self.nl + Fore.CYAN)
  474. for i, p in enumerate(self.possible):
  475. self.queue_game.put(" " + str(i + 1) + " : " + GameData.port_show_part(p, self.game.gamedata.ports[p]) + self.nl)
  476. pi = PlayerInput(self.game)
  477. def got_need1(*_):
  478. log.msg("Ok, I have:", pi.keep)
  479. if pi.keep['count'].strip() == '':
  480. self.deactivate()
  481. return
  482. self.times_left = int(pi.keep['count'])
  483. if pi.keep['choice'].strip() == '':
  484. self.deactivate()
  485. return
  486. c = int(pi.keep['choice']) -1
  487. if c < 0 or c >= len(self.possible):
  488. self.deactivate()
  489. return
  490. self.sector2 = self.possible[int(pi.keep['choice']) -1]
  491. # self.queue_game.put(pformat(pi.keep).replace("\n", "\n\r"))
  492. self.state = 5
  493. self.trade()
  494. d = pi.prompt("Choose -=>", 5, name='choice', digits=True)
  495. d.addCallback(lambda ignore: pi.prompt("Times to execute script:", 5, name='count', digits=True))
  496. d.addCallback(got_need1)
  497. else:
  498. # We already have our target port, so...
  499. self.queue_game.put(self.r + self.nl + "Trading from {0} ({1}) to default {2} ({3}).".format(
  500. self.this_sector,
  501. self.game.gamedata.ports[self.this_sector]['port'],
  502. self.sector2, self.game.gamedata.ports[self.sector2]['port']) + self.nl
  503. )
  504. self.queue_game.put(self.r + self.nl + "Trading {0}".format(
  505. self.game.gamedata.port_trade_show(self.this_sector,
  506. self.sector2)
  507. ))
  508. # The code is smart enough now, that this can't happen. :( Drat!
  509. if self.game.gamedata.ports[self.this_sector]['port'] == self.game.gamedata.ports[self.sector2]['port']:
  510. self.queue_game.put("Hey dummy! Look out the window! These ports are the same class!" + nl)
  511. self.deactivate()
  512. return
  513. pi = PlayerInput(self.game)
  514. def got_need2(*_):
  515. if pi.keep['count'].strip() == '':
  516. self.deactivate()
  517. return
  518. self.times_left = int(pi.keep['count'])
  519. log.msg("Ok, I have:", pi.keep)
  520. # self.queue_game.put(pformat(pi.keep).replace("\n", "\n\r"))
  521. self.state = 5
  522. self.trade()
  523. self.queue_game.put(self.r + self.nl)
  524. d = pi.prompt("Times to execute script", 5, name='count')
  525. d.addCallback(got_need2)
  526. elif self.state == 6:
  527. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  528. if self.fixable:
  529. log.msg("Fixing...")
  530. if self.this_sector == self.sector1:
  531. self.this_sector = self.sector2
  532. self.queue_player.put("{0}\r".format(self.sector2))
  533. self.state = 5
  534. self.trade()
  535. else:
  536. self.this_sector = self.sector1
  537. self.queue_player.put("{0}\r".format(self.sector1))
  538. self.state = 5
  539. self.trade()
  540. elif self.state == 7:
  541. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  542. # Done
  543. if self.this_sector == self.sector1:
  544. self.this_sector = self.sector2
  545. self.queue_player.put("{0}\r".format(self.sector2))
  546. self.state = 10
  547. else:
  548. self.times_left -= 1
  549. if self.times_left <= 0:
  550. # Ok, exit out
  551. self.deactivate()
  552. return
  553. if self.last_credits is None:
  554. self.last_credits = self.credits
  555. else:
  556. if self.credits <= self.last_credits:
  557. log.msg("We don't seem to be making any money here...")
  558. self.queue_game.put(self.r + self.nl + "We don't seem to be making any money here. I'm stopping!" + self.nl)
  559. self.deactivate()
  560. return
  561. self.this_sector = self.sector1
  562. self.queue_player.put("{0}\r".format(self.sector1))
  563. self.state = 10
  564. elif re.match(r'Your offer \[\d+\] \?', prompt):
  565. if self.fix_offer:
  566. # Make real offer / WHAT?@?!
  567. work = prompt.replace(',', '')
  568. parts = re.split(r"\s+", work)
  569. amount = parts[2]
  570. # Ok, we have the amount, now to figure pct...
  571. if self.sell_pct > 100:
  572. self.sell_pct -= 1
  573. else:
  574. self.sell_pct += 1
  575. price = amount * self.sell_pct // 100
  576. log.msg("start: {0} % {1} price {2}".format(amount, self.sell_perc, price))
  577. if self.sell_pct > 100:
  578. self.sell_pct -= 1
  579. else:
  580. self.sell_pct += 1
  581. self.queue_player.put("{0}\r".format(price))
  582. elif self.state == 8:
  583. # What are we trading
  584. # How many holds of Equipment do you want to buy [75]?
  585. if re.match(r"How many holds of .+ do you want to buy \[\d+\]\?", prompt):
  586. parts = prompt.split()
  587. trade_type = parts[4]
  588. if trade_type == 'Fuel':
  589. if (self.tpc in (5,7)) and (self.opc in (2,3,4,8)):
  590. # Can buy equipment - fuel ore is worthless.
  591. self.queue_player.put("0\r")
  592. return
  593. if (self.tpc in(4,7)) and (self.opc in (1,3,5,8)):
  594. # Can buy organics - fuel ore is worthless.
  595. self.queue_player.put("0\r")
  596. return
  597. if (self.tpc in (4,7,3,5)) and (self.opc in (3,4,5,7)):
  598. # No point in buying fuel ore if it can't be sold.
  599. self.queue_player.put("0\r")
  600. return
  601. elif trade_type == 'Organics':
  602. if (self.tpc in (6,7)) and (self.opc in (2,3,4,8)):
  603. # Can buy equipment - organics is worthless.
  604. self.queue_player.put("0\r")
  605. return
  606. if (self.tpc in (2,4,6,7)) and (self.opc in (2,4,6,7)):
  607. # No point in buying organics if it can't be sold.
  608. self.queue_player.put("0\r")
  609. return
  610. elif trade_type == 'Equipment':
  611. if (self.opc in (1,5,6,7)):
  612. # No point in buying equipment if it can't be sold.
  613. self.queue_player.put("0\r")
  614. return
  615. self.queue_player.put("\r")
  616. elif re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  617. # Done
  618. if self.this_sector == self.sector1:
  619. self.this_sector = self.sector2
  620. self.queue_player.put("{0}\r".format(self.sector2))
  621. self.state = 10
  622. else:
  623. self.times_left -= 1
  624. if self.times_left <= 0:
  625. # Ok, exit out
  626. self.deactivate()
  627. return
  628. if self.last_credits is None:
  629. self.last_credits = self.credits
  630. else:
  631. if self.credits <= self.last_credits:
  632. log.msg("We don't seem to be making any money here...")
  633. self.deactivate()
  634. return
  635. self.this_sector = self.sector1
  636. self.queue_player.put("{0}\r".format(self.sector1))
  637. self.state = 10
  638. def trade(self, *_):
  639. # state 5
  640. log.msg("trade!")
  641. self.queue_player.put("pt") # Port Trade
  642. self.this_port = self.game.gamedata.ports[self.this_sector]
  643. if self.this_sector == self.sector1:
  644. self.other_port = self.game.gamedata.ports[self.sector2]
  645. else:
  646. self.other_port = self.game.gamedata.ports[self.sector1]
  647. # Ok, perform some calculations
  648. self.tpc = self.this_port['class']
  649. self.opc = self.other_port['class']
  650. self.fixable = 0
  651. self.fix_offer = 0
  652. # [ Items Status Trading % of max OnBoard]
  653. # [ ----- ------ ------- -------- -------]
  654. # [Fuel Ore Selling 2573 93% 0]
  655. # [Organics Buying 2960 100% 0]
  656. # [Equipment Buying 1958 86% 0]
  657. # []
  658. # []
  659. # [You have 1,000 credits and 20 empty cargo holds.]
  660. # []
  661. # [We are selling up to 2573. You have 0 in your holds.]
  662. # [How many holds of Fuel Ore do you want to buy [20]? 0]
  663. def game_line(self, line: str):
  664. if line.startswith("You have ") and 'credits and' in line:
  665. parts = line.replace(',', '').split()
  666. credits = int(parts[2])
  667. log.msg("Credits: {0}".format(credits))
  668. self.credits = credits
  669. if self.state == 1:
  670. # First exploration
  671. if line.startswith("Sector :"):
  672. # We have starting sector information
  673. parts = re.split("\s+", line)
  674. self.this_sector = int(parts[2])
  675. # These will be the ones swapped around as we trade back and forth.
  676. self.sector1 = self.this_sector
  677. elif line.startswith("Warps to Sector(s) : "):
  678. # Warps to Sector(s) : 397 - (562) - (639)
  679. _, _, warps = line.partition(':')
  680. warps = warps.replace('-', '').replace('(', '').replace(')', '').strip()
  681. log.msg("Warps: [{0}]".format(warps))
  682. self.warps = [ int(x) for x in re.split("\s+", warps)]
  683. log.msg("Warps: [{0}]".format(self.warps))
  684. self.state = 2
  685. elif self.state == 2:
  686. if line == "":
  687. # Ok, we're done
  688. self.state = 3
  689. # Check to see if we have information on any possible ports
  690. # if hasattr(self.game, 'portdata'):
  691. if True:
  692. if not self.this_sector in self.game.gamedata.ports:
  693. self.state = 0
  694. log.msg("Current sector {0} not in portdata.".format(self.this_sector))
  695. self.queue_game.put(self.r + self.nl + "I can't find the current sector in the portdata." + self.nl)
  696. self.deactivate()
  697. return
  698. else:
  699. # Ok, we are in the portdata
  700. pd = self.game.gamedata.ports[self.this_sector]
  701. if GameData.port_burnt(pd):
  702. log.msg("Current sector {0} port is burnt (<= 20%).".format(self.this_sector))
  703. self.queue_game.put(self.r + self.nl + "Current sector port is burnt out. <= 20%." + self.nl)
  704. self.deactivate()
  705. return
  706. possible = [ x for x in self.warps if x in self.game.gamedata.ports ]
  707. log.msg("Possible:", possible)
  708. # BUG: Sometimes links to another sector, don't link back!
  709. # This causes the game to plot a course / autopilot.
  710. # if hasattr(self.game, 'warpdata'):
  711. if True:
  712. # Great! verify that those warps link back to us!
  713. possible = [ x for x in possible if x in self.game.gamedata.warps and self.this_sector in self.game.gamedata.warps[x]]
  714. if len(possible) == 0:
  715. self.state = 0
  716. self.queue_game.put(self.r + self.nl + "I don't see any ports in [{0}].".format(self.warps) + self.nl)
  717. self.deactivate()
  718. return
  719. possible = [ x for x in possible if not GameData.port_burnt(self.game.gamedata.ports[x]) ]
  720. log.msg("Possible:", possible)
  721. if len(possible) == 0:
  722. self.state = 0
  723. self.queue_game.put(self.r + self.nl + "I don't see any unburnt ports in [{0}].".format(self.warps) + self.nl)
  724. self.deactivate()
  725. return
  726. possible = [ x for x in possible if GameData.port_trading(self.game.gamedata.ports[self.this_sector]['port'], self.game.gamedata.ports[x]['port'])]
  727. self.possible = possible
  728. if len(possible) == 0:
  729. self.state = 0
  730. self.queue_game.put(self.r + self.nl + "I don't see any possible port trades in [{0}].".format(self.warps) + self.nl)
  731. self.deactivate()
  732. return
  733. elif len(possible) == 1:
  734. # Ok! there's only one!
  735. self.sector2 = possible[0]
  736. # Display possible ports:
  737. # spos = [ str(x) for x in possible]
  738. # self.queue_game.put(self.r + self.nl + self.nl.join(spos) + self.nl)
  739. # At state 3, we only get a prompt.
  740. return
  741. else:
  742. self.state = 0
  743. log.msg("We don't have any portdata!")
  744. self.queue_game.put(self.r + self.nl + "I have no portdata. Please run CIM Port Report." + self.nl)
  745. self.deactivate()
  746. return
  747. elif self.state == 5:
  748. if "-----" in line:
  749. self.state = 6
  750. elif self.state == 6:
  751. if "We are buying up to" in line:
  752. # Sell
  753. self.state = 7
  754. self.queue_player.put("\r")
  755. self.sell_perc = 100 + self.percent
  756. if "We are selling up to" in line:
  757. # Buy
  758. self.state = 8
  759. self.sell_perc = 100 - self.percent
  760. if line.startswith('Fuel Ore') or line.startswith('Organics') or line.startswith('Equipment'):
  761. work = line.replace('Fuel Ore', 'Fuel')
  762. parts = re.split(r"\s+", work)
  763. # log.msg(parts)
  764. # Equipment, Selling xxx x% xxx
  765. if parts[-1] != '0' and parts[2] != '0' and parts[1] != 'Buying':
  766. log.msg("We have a problem -- they aren't buying what we have in stock!")
  767. stuff = line[0] # F O or E.
  768. if stuff == 'F':
  769. spos = 0
  770. elif stuff == 'O':
  771. spos = 1
  772. else:
  773. spos = 2
  774. other = self.other_port['port']
  775. if other[spos] == 'B':
  776. self.fixable = 1
  777. if "You don't have anything they want" in line:
  778. # Neither! DRAT!
  779. if not self.fixable:
  780. self.deactivate()
  781. return
  782. if "We're not interested." in line:
  783. log.msg("Try, try again. :(")
  784. self.state = 5
  785. self.trade()
  786. elif self.state == 7:
  787. # Haggle Sell
  788. if "We'll buy them for" in line or "Our final offer" in line:
  789. if "Our final offer" in line:
  790. self.sell_perc -= 1
  791. parts = line.replace(',', '').split()
  792. start_price = int(parts[4])
  793. price = start_price * self.sell_perc // 100
  794. log.msg("start: {0} % {1} price {2}".format(start_price, self.sell_perc, price))
  795. self.sell_perc -= 1
  796. self.queue_player.put("{0}\r".format(price))
  797. if "We are selling up to" in line:
  798. # Buy
  799. self.state = 8
  800. self.sell_perc = 100 - self.percent
  801. if "We're not interested." in line:
  802. log.msg("Try, try again. :(")
  803. self.state = 5
  804. self.trade()
  805. if "WHAT?!@!? you must be crazy!" in line:
  806. log.msg("fix offer")
  807. self.fix_offer = 1
  808. if "So, you think I'm as stupid as you look?" in line:
  809. log.msg("fix offer")
  810. self.fix_offer = 1
  811. if "Quit playing around, you're wasting my time!" in line:
  812. log.msg("fix offer")
  813. self.fix_offer = 1
  814. elif self.state == 8:
  815. # Haggle Buy
  816. if "We'll sell them for" in line or "Our final offer" in line:
  817. if "Our final offer" in line:
  818. self.sell_perc += 1
  819. parts = line.replace(',', '').split()
  820. start_price = int(parts[4])
  821. price = start_price * self.sell_perc // 100
  822. log.msg("start: {0} % {1} price {2}".format(start_price, self.sell_perc, price))
  823. self.sell_perc += 1
  824. self.queue_player.put("{0}\r".format(price))
  825. if "We're not interested." in line:
  826. log.msg("Try, try again. :(")
  827. self.state = 5
  828. self.trade()
  829. elif self.state == 10:
  830. if "Sector : " in line:
  831. # Trade
  832. self.state = 5
  833. reactor.callLater(0, self.trade, 0)
  834. # self.trade()
  835. # elif self.state == 3:
  836. # log.msg("At state 3 [{0}]".format(line))
  837. # self.queue_game.put("At state 3.")
  838. # self.deactivate()
  839. # return
  840. class ScriptExplore(object):
  841. """ Script Explore v1.00
  842. By: David Thielemann
  843. WARNINGS:
  844. We assume the player has a Holo-Scanner!
  845. We assume the player has lots o turns, or unlimited turns!
  846. We assume the player is aware we run infinitly until we can't find new sectors to move to!
  847. """
  848. def __init__(self, game):
  849. self.game = game
  850. self.queue_game = game.queue_game
  851. self.queue_player = game.queue_player
  852. self.observer = game.observer
  853. self.r = Style.RESET_ALL
  854. self.nl = "\n\r"
  855. # Our Stuff, Not our pants!
  856. self.dense = [] # We did a density, store that info.
  857. self.didScan = False # Track if we did a scan already... prevents us calling a scan multiple times.
  858. self.clear = [] # Warps that we know are clear.
  859. self.highsector = 0 # Selected Sector to move to next!
  860. self.highwarp = 0 # Selected Sector's Warp Count!
  861. self.stacksector = set() # Set of sectors that we have not picked but are unexplored... even though we did a holo!
  862. self.oneMoveSector = False
  863. self.times = 0
  864. self.maxtimes = 0
  865. # Activate
  866. self.prompt = game.buffer
  867. self.save = self.observer.save()
  868. self.observer.connect('player', self.player)
  869. self.observer.connect("prompt", self.game_prompt)
  870. self.observer.connect("game-line", self.game_line)
  871. self.defer = None
  872. self.send2player("Explorer v1.01")
  873. # How many times we going to go today?
  874. ask = PlayerInput(self.game)
  875. def settimes(*_):
  876. times = int(ask.keep['times'].strip())
  877. log.msg("settimes got '{0}'".format(times))
  878. if times == None:
  879. self.deactivate()
  880. else:
  881. self.times = times
  882. self.maxtimes = times
  883. self.send2game("D")
  884. self.state = 1
  885. d = ask.prompt("How many sectors would you lick to explode?", 5, name="times", digits=True)
  886. #d.addCallback(ask.output)
  887. #d.addCallback(lambda ignore: self.settimes(ask.keep))
  888. d.addCallback(settimes)
  889. def whenDone(self):
  890. self.defer = defer.Deferred()
  891. # Call this to chain something after we exit.
  892. return self.defer
  893. def deactivate(self):
  894. self.state = 0
  895. log.msg("ScriptExplore.deactivate()")
  896. assert(not self.save is None)
  897. self.observer.load(self.save)
  898. self.save = None
  899. if self.defer:
  900. self.defer.callback('done')
  901. self.defer = None
  902. def player(self, chunk: bytes):
  903. # If we receive anything -- ABORT!
  904. self.deactivate()
  905. def send2game(self, txt):
  906. self.queue_player.put(txt)
  907. def send2player(self, txt):
  908. self.queue_game.put(
  909. self.nl + txt + self.r + self.nl
  910. )
  911. def game_prompt(self, prompt: str):
  912. log.msg("{0} : {1}".format(self.state, prompt))
  913. if self.state == 3:
  914. log.msg("dense is {0} sectors big".format(len(self.dense)))
  915. self.state = 4
  916. self.send2game("SH")
  917. def game_line(self, line: str):
  918. log.msg("{0} | {1}".format(self.state, line))
  919. if "Mine Control" in line: # If we don't have a Holo-Scanner and we attempted to do a Holo-scan, abort
  920. self.deactivate()
  921. if self.state == 1:
  922. # Density Scan, (Assume we have the Holo-Scanner)
  923. if not self.didScan:
  924. self.send2game("SD")
  925. self.didScan = True
  926. if "Relative Density Scan" in line:
  927. self.state = 2
  928. elif self.state == 2:
  929. if line.startswith("Sector"):
  930. work = line.replace(':', '').replace(')', '').replace('%', '').replace('==>', '')
  931. work = re.split(r"\s+", work)
  932. # 'Sector', '8192', '0', 'Warps', '4', 'NavHaz', '0', 'Anom', 'No'
  933. # 'Sector', '(', '8192)', '0', 'Warps', '4', 'NavHaz', '0', 'Anom', 'No'
  934. # New Sector?
  935. if work[1] == '(':
  936. temp1 = True
  937. else:
  938. temp1 = False
  939. # Yes new sector! So we will add it!
  940. if temp1:
  941. # Switch Anom into bool state
  942. if(work[9] == 'No'):
  943. temp = False
  944. else:
  945. temp = True
  946. self.dense.append( {'sector': int(work[2]), 'density': int(work[3]), 'warps': int(work[5]), 'navhaz': int(work[7]), 'anom': temp} )
  947. # {'sector': 8192, 'density': 0, 'warps': 4, 'navhaz': 0, 'anom': False}
  948. elif line == "":
  949. self.state = 3
  950. elif self.state == 4:
  951. # Ok so we do our Holo-Scan now how can we parse this... hmm.
  952. # Or do we just do that and then do a move? Hmmm... (I think we get enough info to make a choice)
  953. # Perhaps what we will do is do a Holo-Scan just to "count" as us going into that sector
  954. # TEST
  955. #self.dense = [
  956. # {'sector': 2, 'density': 0, 'warps': 1, 'navhaz': 0, 'anom': False},
  957. # {'sector': 1, 'density': 0, 'warps': 1, 'navhaz': 0, 'anom': False},
  958. # {'sector': 3, 'density': 0, 'warps': 2, 'navhaz': 0, 'anom': False},
  959. #]
  960. # Do we have a new place to go? (That is also worth going to)
  961. if not self.dense: # Dense contains no new sectors, abort
  962. log.msg("No New Sectors Found!")
  963. self.send2player("Find a new area for me to search in!")
  964. # Attempt to resolve no new sectors!
  965. if self.stacksector: # Do we have anything on the stack? (If so we set highsector with one of the randomly selected sectors)
  966. self.highsector = self.stacksector.pop()
  967. # Ok so here is were we should jump into some other way to safely get to that said sector, and safely also meaning if we see and
  968. # new sectors along the way stop and grab them too... or just throw them onto the stack.
  969. self.deactivate()
  970. elif self.dense: # Dense does contain at least 1 new sector, continue on
  971. t = [] # Pre-Test to check if there are just a bunch of 1 warp sectors
  972. for d in self.dense:
  973. if d['warps'] != 1:
  974. t.append(d['sector'])
  975. if not t: # If there are no sectors with more that 1 warp, abort
  976. log.msg("No Sectors Found except one move sector!")
  977. self.send2player("Find a new area for me to look at!")
  978. # Attempt to resolve no new sectors with more than 1 warp!
  979. if self.stacksector: # Do we have anything on the stack? (If so we set highsector with one of the randomly selected sectors)
  980. self.highsector = self.stacksector.pop()
  981. # Ok so here is were we should jump into some other way to safely get to that said sector, and safely also meaning if we see and
  982. # new sectors along the way stop and grab them too... or just throw them onto the stack.
  983. self.deactivate()
  984. # Is the sector safe to go into?
  985. for d in self.dense:
  986. if not d['anom']:
  987. # Sector does not contain a Anomoly
  988. if d['navhaz'] == 0:
  989. # Sector does not contain Hazards
  990. if d['density'] == 0 or d['density'] == 1 or d['density'] == 100 or d['density'] == 101:
  991. # Sector does contain empty space / a beacon / a port / or a beacon and port
  992. if d['warps'] > 1:
  993. # If Sector is worth checking out?
  994. self.clear.append(d['sector'])
  995. if self.clear: # We have sector(s) we can move to!
  996. log.msg("Clear Sectors: {0}".format(len(self.clear)))
  997. self.state = 5
  998. elif self.state == 5:
  999. # Ok so we now have a idea of howmany and what sectors are clear and have greater than 1 warps
  1000. # Now to sort thru that to see which ones have the highest warps and if they have a port or not
  1001. # Then randomly if needed pick from those to actually move too! \o/ And poof we will be ready to make it loop
  1002. # TEST, Just to show that my code picks the highest sector number if multiple sectors have the same warp count!
  1003. #self.clear = [1, 2, 3]
  1004. #log.msg("Clear: {0} Dense: {1}".format(len(self.clear), len(self.dense)))
  1005. # Sort to find greatest warp count
  1006. for c in self.clear:
  1007. for d in self.dense:
  1008. if d['sector'] == c:
  1009. if d['warps'] > self.highwarp:
  1010. self.highwarp = d['warps']
  1011. self.highsector = c
  1012. elif d['warps'] == self.highwarp:
  1013. if c > self.highsector:
  1014. self.highsector = c
  1015. # If we found the best sector to move to and with previous test of safety we will now move to that sector!
  1016. if self.highwarp != 0 or self.highsector != 0:
  1017. log.msg("Sector: {0:5d} Warps: {1}".format(self.highsector, self.highwarp))
  1018. self.state = 6
  1019. else:
  1020. log.msg("Oh Nose! We didn't find any Sector with higher Warps than any of the others!")
  1021. self.deactivate()
  1022. elif self.state == 6:
  1023. # Add the dense scan of unknown sectors onto the stack of sectors
  1024. for d in self.dense:
  1025. self.stacksector.add(d['sector'])
  1026. # Remove the sector we are just about to go to, we use discard so if the sector does not exist we don't throw a error!
  1027. self.stacksector.discard(self.highsector)
  1028. # Ok we know the sector we want to go to now let's move it!
  1029. self.send2game("m{0}\n\r".format(self.highsector))
  1030. # Reset Variables for fresh data
  1031. self.didScan = False
  1032. self.dense = []
  1033. self.clear = []
  1034. self.highwarp = 0
  1035. self.highsector = 0
  1036. self.state = 1
  1037. # Warning! Yes we can and will eat all the turns! :P
  1038. self.times -= 1
  1039. if self.times <= 0:
  1040. self.send2player("Completed {0}".format(self.maxtimes))
  1041. self.deactivate()
  1042. class ScriptSpace(object):
  1043. """ Space Exploration script.
  1044. Send "SD", verify paths are clear.
  1045. Find nearest unknown. Sector + CR.
  1046. Save "shortest path from to" information.
  1047. At 'Engage the Autopilot', Send "S" (Single Step)
  1048. At '[Stop in this sector', Send "SD", verify path is clear.
  1049. Send "SH" (glean sector/port information along the way.)
  1050. Send "N" (Next)!
  1051. Send "SD" / Verify clear.
  1052. Send "SH"
  1053. Repeat for Next closest.
  1054. """
  1055. def __init__(self, game):
  1056. self.game = game
  1057. self.queue_game = game.queue_game
  1058. self.queue_player = game.queue_player
  1059. self.observer = game.observer
  1060. self.r = Style.RESET_ALL
  1061. self.nl = "\n\r"
  1062. self.this_sector = None # Starting sector
  1063. self.target_sector = None # Sector going to
  1064. self.path = []
  1065. self.times_left = 0 # How many times to look for target
  1066. self.density = dict() # Results of density scan. (Just the numbers)
  1067. # Activate
  1068. self.prompt = game.buffer
  1069. self.save = self.observer.save()
  1070. self.observer.connect('player', self.player)
  1071. self.observer.connect("prompt", self.game_prompt)
  1072. self.observer.connect("game-line", self.game_line)
  1073. self.defer = None
  1074. self.queue_game.put(
  1075. self.nl + "Bugz (like space), is big." + self.r + self.nl
  1076. )
  1077. self.state = 1
  1078. self.queue_player.put("SD")
  1079. # Get current density scan + also get the current sector.
  1080. # [Command [TL=00:00:00]:[XXXX] (?=Help)? : D]
  1081. def whenDone(self):
  1082. self.defer = defer.Deferred()
  1083. # Call this to chain something after we exit.
  1084. return self.defer
  1085. def deactivate(self):
  1086. self.state = 0
  1087. log.msg("ScriptPort.deactivate ({0})".format(self.times_left))
  1088. assert(not self.save is None)
  1089. self.observer.load(self.save)
  1090. self.save = None
  1091. if self.defer:
  1092. self.defer.callback('done')
  1093. self.defer = None
  1094. def player(self, chunk: bytes):
  1095. # If we receive anything -- ABORT!
  1096. self.deactivate()
  1097. def unknown_search(self, starting_sector):
  1098. seen = set()
  1099. possible = set()
  1100. possible.add(int(starting_sector))
  1101. done = False
  1102. while not done:
  1103. next_possible = set()
  1104. for s in possible:
  1105. p = self.game.gamedata.get_warps(s)
  1106. if p is not None:
  1107. for pos in p:
  1108. if pos not in seen:
  1109. next_possible.add(pos)
  1110. else:
  1111. log.msg("unknown found:", s)
  1112. self.unknown = s
  1113. done = True
  1114. break
  1115. seen.add(s)
  1116. if self.unknown is None:
  1117. log.msg("possible:", next_possible)
  1118. possible = next_possible
  1119. yield
  1120. def find_unknown(self, starting_sector):
  1121. log.msg("find_unknown( {0})".format(starting_sector))
  1122. d = defer.Deferred()
  1123. # Process things
  1124. self.unknown = None
  1125. c = coiterate(self.unknown_search(starting_sector))
  1126. c.addCallback(lambda unknown: d.callback(self.unknown))
  1127. return d
  1128. def show_unknown(self, sector):
  1129. if sector is None:
  1130. self.deactivate()
  1131. return
  1132. log.msg("Travel to {0}...".format(sector))
  1133. self.queue_player.put("{0}\r".format(sector))
  1134. def game_prompt(self, prompt: str):
  1135. log.msg("{0} : {1}".format(self.state, prompt))
  1136. if self.state == 3:
  1137. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  1138. # this_sector code isn't working -- so! Get sector from prompt
  1139. self.state = 4
  1140. _, _, sector = prompt.partition(']:[')
  1141. sector, _, _ = sector.partition(']')
  1142. self.this_sector = int(sector)
  1143. # Ok, we're done with Density Scan, and we're back at the command prompt
  1144. log.msg("Go find the nearest unknown...")
  1145. d = self.find_unknown(sector)
  1146. d.addCallback(self.show_unknown)
  1147. elif self.state == 6:
  1148. # Engage the autopilot?
  1149. if prompt.startswith('Engage the Autopilot? (Y/N/Single step/Express) [Y]'):
  1150. self.state = 7
  1151. sector = self.path.pop(0)
  1152. if sector in self.density:
  1153. if self.density[sector] in (0, 100):
  1154. # Ok, looks safe!
  1155. self.queue_player.put("S")
  1156. self.this_sector = sector
  1157. else:
  1158. log.msg("FATAL: Density for {0} is {1}".format(sector, self.density[sector]))
  1159. self.deactivate()
  1160. return
  1161. else:
  1162. log.msg("{0} not in density scan? (how's that possible?)".format(sector))
  1163. self.deactivate()
  1164. return
  1165. elif self.state == 7:
  1166. # Ok, we're in a new sector (single stepping through space)
  1167. # update density scan
  1168. if prompt.startswith('Stop in this sector (Y,N,E,I,R,S,D,P,?) (?=Help) [N] ?'):
  1169. self.queue_player.put("SD")
  1170. self.state = 8
  1171. elif self.state == 10:
  1172. # Because we're here
  1173. if prompt.startswith('Stop in this sector (Y,N,E,I,R,S,D,P,?) (?=Help) [N] ?'):
  1174. self.queue_player.put("SH")
  1175. self.state = 11
  1176. elif self.state == 11:
  1177. if prompt.startswith('Stop in this sector (Y,N,E,I,R,S,D,P,?) (?=Help) [N] ?'):
  1178. # Ok, is the density scan clear?
  1179. sector = self.path.pop(0)
  1180. if sector in self.density:
  1181. if self.density[sector] in (0, 100):
  1182. # Ok, looks safe
  1183. self.queue_player.put("N")
  1184. self.state = 7
  1185. self.this_sector = sector
  1186. else:
  1187. log.msg("FATAL: Density for {0} is {1}".format(sector, self.density[sector]))
  1188. self.deactivate()
  1189. return
  1190. else:
  1191. log.msg("{0} not in density scane? (how's that possible...)".format(sector))
  1192. self.deactivate()
  1193. return
  1194. def next_unknown(self, sector):
  1195. log.msg("Unknown is :", sector)
  1196. self.deactivate()
  1197. def game_line(self, line: str):
  1198. log.msg("line {0} : {1}".format(self.state, line))
  1199. if line.startswith('Sector : '):
  1200. work = line.strip()
  1201. parts = re.split(r"\s+", work)
  1202. self.this_sector = int(parts[2])
  1203. log.msg("game_line sector", self.this_sector)
  1204. elif line.startswith("Command [TL=]"):
  1205. # Ok, get the current sector from this
  1206. _, _, sector = line.partition("]:[")
  1207. sector, _, _ = sector.partition("]")
  1208. self.this_sector = int(sector)
  1209. log.msg("current sector: {0}".format(self.this_sector))
  1210. elif line.startswith('Warps to Sector(s) :'):
  1211. # Warps to Sector(s) : 5468
  1212. _, _, work = line.partition(':')
  1213. work = work.strip().replace('(', '').replace(')', '').replace(' - ', ' ')
  1214. parts = [ int(x) for x in work.split(' ')]
  1215. self.path = list(parts)
  1216. if self.state in (1, 8):
  1217. if 'Relative Density Scan' in line:
  1218. # Start Density Scan
  1219. self.state += 1
  1220. self.density = {}
  1221. elif self.state in (2, 9):
  1222. if line == '':
  1223. # End of Density Scan
  1224. self.state += 1
  1225. log.msg("Density: {0}".format(self.density))
  1226. # self.deactivate()
  1227. elif line.startswith('Sector'):
  1228. # Parse Density Scan values
  1229. work = line.replace('(', '').replace(')', '').replace(':', '').replace('%', '').replace(',', '')
  1230. parts = re.split(r'\s+', work)
  1231. log.msg("Sector", parts)
  1232. sector = int(parts[1])
  1233. self.density[sector] = int(parts[3])
  1234. if parts[7] != '0':
  1235. log.msg("NavHaz {0} : {1}".format(parts[7], work))
  1236. self.density[sector] += 99
  1237. if parts[9] != 'No':
  1238. log.msg("Anom {0} : {1}".format(parts[9], work))
  1239. self.density[sector] += 990
  1240. elif self.state == 4:
  1241. # Looking for shortest path message / warp info
  1242. # Or possibly, "We're here!"
  1243. if line.startswith('Sector :') and str(self.unknown) in line:
  1244. # Ok, I'd guess that we're already there!
  1245. # Try it again!
  1246. self.queue_player.put("SD")
  1247. self.state = 1
  1248. if line.startswith('The shortest path'):
  1249. self.state = 5
  1250. elif self.state == 5:
  1251. # This is the warps line
  1252. # Can this be multiple lines?
  1253. if line == "":
  1254. self.state = 6
  1255. else:
  1256. work = line.replace("(", "").replace(")", "").replace(">", "").strip()
  1257. self.path = [int(x) for x in work.split()]
  1258. log.msg("Path:", self.path)
  1259. # Verify
  1260. current = self.path.pop(0)
  1261. if current != self.this_sector:
  1262. log.msg("Failed: {0} != {1}".format(current, self.this_sector))
  1263. self.deactivate()
  1264. return
  1265. elif self.state == 7:
  1266. if self.unknown == self.this_sector:
  1267. # We have arrived!
  1268. log.msg("We're here!")
  1269. self.deactivate()
  1270. class ProxyMenu(object):
  1271. """ Display ProxyMenu
  1272. Example:
  1273. from flexible import ProxyMenu
  1274. if re.match(r"Command \[TL=.* \(\?=Help\)\? :", prompt):
  1275. menu = ProxyMenu(self.game)
  1276. """
  1277. def __init__(self, game):
  1278. self.nl = "\n\r"
  1279. self.c = merge(Style.BRIGHT + Fore.YELLOW + Back.BLUE)
  1280. self.r = Style.RESET_ALL
  1281. self.c1 = merge(Style.BRIGHT + Fore.BLUE)
  1282. self.c2 = merge(Style.NORMAL + Fore.CYAN)
  1283. # self.portdata = None
  1284. self.game = game
  1285. self.queue_game = game.queue_game
  1286. self.observer = game.observer
  1287. # Am I using self or game? (I think I want game, not self.)
  1288. # if hasattr(self.game, "portdata"):
  1289. # self.portdata = self.game.portdata
  1290. # else:
  1291. # self.portdata = {}
  1292. # if hasattr(self.game, 'warpdata'):
  1293. # self.warpdata = self.game.warpdata
  1294. # else:
  1295. # self.warpdata = {}
  1296. if hasattr(self.game, 'trade_report'):
  1297. self.trade_report = self.game.trade_report
  1298. else:
  1299. self.trade_report = []
  1300. # Yes, at this point we would activate
  1301. self.prompt = game.buffer
  1302. self.save = self.observer.save()
  1303. self.observer.connect("player", self.player)
  1304. # If we want it, it's here.
  1305. self.defer = None
  1306. self.keepalive = task.LoopingCall(self.awake)
  1307. self.keepalive.start(30)
  1308. self.menu()
  1309. def __del__(self):
  1310. log.msg("ProxyMenu {0} RIP".format(self))
  1311. def whenDone(self):
  1312. self.defer = defer.Deferred()
  1313. # Call this to chain something after we exit.
  1314. return self.defer
  1315. def menu(self):
  1316. self.queue_game.put(
  1317. self.nl + self.c + "TradeWars Proxy active." + self.r + self.nl
  1318. )
  1319. def menu_item(ch: str, desc: str):
  1320. self.queue_game.put(
  1321. " " + self.c1 + ch + self.c2 + " - " + self.c1 + desc + self.nl
  1322. )
  1323. menu_item("D", "Display Report again")
  1324. # menu_item("Q", "Quest")
  1325. # if hasattr(self.game, 'portdata'):
  1326. # ports = len(self.game.portdata)
  1327. # else:
  1328. # ports = '?'
  1329. menu_item("P", "Port CIM Report ({0})".format(len(self.game.gamedata.ports)))
  1330. # if hasattr(self.game, 'warpdata'):
  1331. # warps = len(self.game.warpdata)
  1332. # else:
  1333. # warps = '?'
  1334. menu_item("W", "Warp CIM Report ({0})".format(len(self.game.gamedata.warps)))
  1335. menu_item("T", "Trading Report")
  1336. menu_item("S", "Scripts")
  1337. menu_item("X", "eXit")
  1338. self.queue_game.put(" " + self.c + "-=>" + self.r + " ")
  1339. def awake(self):
  1340. log.msg("ProxyMenu.awake()")
  1341. self.game.queue_player.put(" ")
  1342. def port_report(self, portdata: dict):
  1343. # self.portdata = portdata
  1344. # self.game.portdata = portdata
  1345. self.queue_game.put("Loaded {0} ports.".format(len(portdata)) + self.nl)
  1346. self.welcome_back()
  1347. def warp_report(self, warpdata: dict):
  1348. # self.warpdata = warpdata
  1349. # self.game.warpdata = warpdata
  1350. self.queue_game.put("Loaded {0} sectors.".format(len(warpdata)) + self.nl)
  1351. self.welcome_back()
  1352. def make_trade_report(self):
  1353. log.msg("make_trade_report()")
  1354. ok_trades = []
  1355. best_trades = []
  1356. # for sector, pd in self.game.gamedata.ports.items():
  1357. for sector in sorted(self.game.gamedata.ports.keys()):
  1358. pd = self.game.gamedata.ports[sector]
  1359. if not GameData.port_burnt(pd):
  1360. pc = pd['class']
  1361. # Ok, let's look into it.
  1362. if not sector in self.game.gamedata.warps:
  1363. continue
  1364. warps = self.game.gamedata.warps[sector]
  1365. for w in warps:
  1366. # Verify that we have that warp's info, and that the sector is in it.
  1367. # (We can get back from it)
  1368. if w in self.game.gamedata.warps and sector in self.game.gamedata.warps[w]:
  1369. # Ok, we can get there -- and get back!
  1370. if w > sector and w in self.game.gamedata.ports and not GameData.port_burnt(self.game.gamedata.ports[w]):
  1371. # it is > and has a port.
  1372. wd = self.game.gamedata.ports[w]
  1373. wc = wd['class']
  1374. # 1: "BBS",
  1375. # 2: "BSB",
  1376. # 3: "SBB",
  1377. # 4: "SSB",
  1378. # 5: "SBS",
  1379. # 6: "BSS",
  1380. # 7: "SSS",
  1381. # 8: "BBB",
  1382. if pc in (1,5) and wc in (2,4):
  1383. best_trades.append(self.game.gamedata.port_trade_show(sector, w))
  1384. # best_trades.append( "{0:5} -=- {1:5}".format(sector, w))
  1385. elif pc in (2,4) and wc in (1,5):
  1386. best_trades.append(self.game.gamedata.port_trade_show(sector, w))
  1387. # best_trades.append( "{0:5} -=- {1:5}".format(sector, w))
  1388. elif GameData.port_trading(pd['port'], self.game.gamedata.ports[w]['port']):
  1389. # ok_trades.append( "{0:5} -=- {1:5}".format(sector,w))
  1390. ok_trades.append(self.game.gamedata.port_trade_show(sector, w))
  1391. yield
  1392. self.trade_report.append("Best Trades: (org/equ)")
  1393. self.trade_report.extend(best_trades)
  1394. self.trade_report.append("Ok Trades:")
  1395. self.trade_report.extend(ok_trades)
  1396. # self.queue_game.put("BEST TRADES:" + self.nl + self.nl.join(best_trades) + self.nl)
  1397. # self.queue_game.put("OK TRADES:" + self.nl + self.nl.join(ok_trades) + self.nl)
  1398. def show_trade_report(self, *_):
  1399. self.game.trade_report = self.trade_report
  1400. for t in self.trade_report:
  1401. self.queue_game.put(t + self.nl)
  1402. self.welcome_back()
  1403. def player(self, chunk: bytes):
  1404. """ Data from player (in bytes). """
  1405. chunk = chunk.decode("latin-1", "ignore")
  1406. key = chunk.upper()
  1407. log.msg("ProxyMenu.player({0})".format(key))
  1408. # Stop the keepalive if we are activating something else
  1409. # or leaving...
  1410. self.keepalive.stop()
  1411. if key == "T":
  1412. self.queue_game.put(self.c + key + self.r + self.nl)
  1413. # Trade Report
  1414. # do we have enough information to do this?
  1415. # if not hasattr(self.game, 'portdata') and not hasattr(self.game, 'warpdata'):
  1416. # self.queue_game.put("Missing portdata and warpdata." + self.nl)
  1417. # elif not hasattr(self.game, 'portdata'):
  1418. # self.queue_game.put("Missing portdata." + self.nl)
  1419. # elif not hasattr(self.game, 'warpdata'):
  1420. # self.queue_game.put("Missing warpdata." + self.nl)
  1421. # else:
  1422. if True:
  1423. # Yes, so let's start!
  1424. self.trade_report = []
  1425. d = coiterate(self.make_trade_report())
  1426. d.addCallback(self.show_trade_report)
  1427. return
  1428. elif key == "P":
  1429. self.queue_game.put(self.c + key + self.r + self.nl)
  1430. # Activate CIM Port Report
  1431. report = CIMPortReport(self.game)
  1432. d = report.whenDone()
  1433. d.addCallback(self.port_report)
  1434. d.addErrback(self.welcome_back)
  1435. return
  1436. elif key == "W":
  1437. self.queue_game.put(self.c + key + self.r + self.nl)
  1438. # Activate CIM Warp Report
  1439. report = CIMWarpReport(self.game)
  1440. d = report.whenDone()
  1441. d.addCallback(self.warp_report)
  1442. d.addErrback(self.welcome_back)
  1443. return
  1444. elif key == "S":
  1445. self.queue_game.put(self.c + key + self.r + self.nl)
  1446. # Scripts
  1447. self.activate_scripts_menu()
  1448. return
  1449. elif key == "D":
  1450. self.queue_game.put(self.c + key + self.r + self.nl)
  1451. # (Re) Display Trade Report
  1452. if self.trade_report:
  1453. for t in self.trade_report:
  1454. self.queue_game.put(t + self.nl)
  1455. else:
  1456. self.queue_game.put("Missing trade_report." + self.nl)
  1457. # self.queue_game.put(pformat(self.portdata).replace("\n", "\n\r") + self.nl)
  1458. # self.queue_game.put(pformat(self.warpdata).replace("\n", "\n\r") + self.nl)
  1459. elif key == "Q":
  1460. self.queue_game.put(self.c + key + self.r + self.nl)
  1461. # This is an example of chaining PlayerInput prompt calls.
  1462. ask = PlayerInput(self.game)
  1463. d = ask.prompt("What is your quest?", 40, name="quest", abort_blank=True)
  1464. # Display the user's input
  1465. d.addCallback(ask.output)
  1466. d.addCallback(
  1467. lambda ignore: ask.prompt(
  1468. "What is your favorite color?", 10, name="color"
  1469. )
  1470. )
  1471. d.addCallback(ask.output)
  1472. d.addCallback(
  1473. lambda ignore: ask.prompt(
  1474. "What is the meaning of the squirrel?",
  1475. 12,
  1476. name="squirrel",
  1477. digits=True,
  1478. )
  1479. )
  1480. d.addCallback(ask.output)
  1481. def show_values(show):
  1482. log.msg(show)
  1483. self.queue_game.put(pformat(show).replace("\n", "\n\r") + self.nl)
  1484. d.addCallback(lambda ignore: show_values(ask.keep))
  1485. d.addCallback(self.welcome_back)
  1486. # On error, just return back
  1487. # This doesn't seem to be getting called.
  1488. # d.addErrback(lambda ignore: self.welcome_back)
  1489. d.addErrback(self.welcome_back)
  1490. return
  1491. elif key == "X":
  1492. self.queue_game.put(self.c + key + self.r + self.nl)
  1493. self.queue_game.put("Proxy done." + self.nl)
  1494. self.observer.load(self.save)
  1495. self.save = None
  1496. # It isn't running (NOW), so don't try to stop it.
  1497. # self.keepalive.stop()
  1498. self.keepalive = None
  1499. # Ok, this is a HORRIBLE idea, because the prompt might be
  1500. # outdated.
  1501. # self.queue_game.put(self.prompt)
  1502. self.prompt = None
  1503. # Possibly: Send '\r' to re-display the prompt
  1504. # instead of displaying the original one.
  1505. self.game.queue_player.put("d")
  1506. # Were we asked to do something when we were done here?
  1507. if self.defer:
  1508. reactor.CallLater(0, self.defer.callback)
  1509. # self.defer.callback()
  1510. self.defer = None
  1511. return
  1512. self.keepalive.start(30, True)
  1513. self.menu()
  1514. def activate_scripts_menu(self):
  1515. self.observer.disconnect("player", self.player)
  1516. self.observer.connect("player", self.scripts_player)
  1517. self.scripts_menu()
  1518. def deactivate_scripts_menu(self, *_):
  1519. self.observer.disconnect("player", self.scripts_player)
  1520. self.observer.connect("player", self.player)
  1521. self.welcome_back()
  1522. def scripts_menu(self, *_):
  1523. c1 = merge(Style.BRIGHT + Fore.CYAN)
  1524. c2 = merge(Style.NORMAL + Fore.CYAN)
  1525. def menu_item(ch, desc):
  1526. self.queue_game.put(
  1527. " " + c1 + ch + c2 + " - " + c1 + desc + self.nl
  1528. )
  1529. menu_item("1", "Ports (Trades between two sectors)")
  1530. menu_item("2", "Explore (Strange new sectors)")
  1531. menu_item("3", "Space... the final frontier...")
  1532. menu_item("X", "eXit")
  1533. self.queue_game.put(" " + c1 + "-=>" + self.r + " ")
  1534. def scripts_player(self, chunk: bytes):
  1535. """ Data from player (in bytes). """
  1536. chunk = chunk.decode("latin-1", "ignore")
  1537. key = chunk.upper()
  1538. if key == '1':
  1539. self.queue_game.put(self.c + key + self.r + self.nl)
  1540. # Activate this magical event here
  1541. ports = ScriptPort(self.game)
  1542. d = ports.whenDone()
  1543. # d.addCallback(self.scripts_menu)
  1544. # d.addErrback(self.scripts_menu)
  1545. d.addCallback(self.deactivate_scripts_menu)
  1546. d.addErrback(self.deactivate_scripts_menu)
  1547. return
  1548. elif key == '2':
  1549. self.queue_game.put(self.c + key + self.r + self.nl)
  1550. explore = ScriptExplore(self.game)
  1551. d = explore.whenDone()
  1552. d.addCallback(self.deactivate_scripts_menu)
  1553. d.addErrback(self.deactivate_scripts_menu)
  1554. return
  1555. elif key == '3':
  1556. self.queue_game.put(self.c + key + self.r + self.nl)
  1557. space = ScriptSpace(self.game)
  1558. d = space.whenDone()
  1559. d.addCallback(self.deactivate_scripts_menu)
  1560. d.addErrback(self.deactivate_scripts_menu)
  1561. return
  1562. elif key == 'X':
  1563. self.queue_game.put(self.c + key + self.r + self.nl)
  1564. self.deactivate_scripts_menu()
  1565. return
  1566. else:
  1567. self.queue_game.put(self.c + "?" + self.r + self.nl)
  1568. self.scripts_menu()
  1569. def welcome_back(self, *_):
  1570. log.msg("welcome_back")
  1571. self.keepalive.start(30, True)
  1572. self.menu()