def received_message(self, msg_block): """ Showdown sometimes sends multiple newline-separated messages in a single websocket message, so split them and process each one. 0-1 character messages can be ignored. """ msg_block = unicode(msg_block).encode("ascii", "ignore") log.i(received("\n".join((msg_block, "-" * 60)))) msg_block = msg_block.splitlines() battleroom = self.battleroom if msg_block[0].startswith(">battle"): if battleroom != msg_block[0][1:]: # Instantiate new battle room if msg_block[1] == "|init|battle": self.battleroom = msg_block[0][1:] self.battleclient = BattleClient( self.username, self.battleroom, self.send, self.show_calcs, self.ai_strategy ) self.latest_request = None self.challenging = None else: log.i("Battle message received for an inactive room:\n%s", msg_block) return elif battleroom == msg_block[0][1:] and ( msg_block[1].startswith("|expire") or msg_block[1].startswith("|deinit") ): self.battleroom = None self.battleclient = None self.send("|/leave %s" % battleroom) return # Save the most recent "request object"; use it to build team if client hasn't done so. # Set .request on the battleclient, because it occasionally needs to peek at the request # that it will process following this block if len(msg_block) > 1 and msg_block[1].startswith("|request|"): self.latest_request = self.battleclient.request = json.loads(msg_block[1].split("|")[2]) if self.battleclient.my_side is None: self.battleclient.build_my_side(self.latest_request) return for msg in msg_block: try: self.process_message(msg) except Exception: log.exception("Exception processing msg: %s", msg) # Process the request after the next message is sent (the server always sends it one message # before I want to use it). if self.latest_request is not None and msg_block[0].startswith(">battle"): self.battleclient.handle_request(self.latest_request) self.latest_request = None
class Bot(WebSocketClient): """ Encapsulates a user's connection to the Pokemon Showdown server. The Bot's responsibilities are: - Triage messages from the server and delegate to the appropriate room handler or ignore them - Create and delete rooms/handlers as needed - Handle 'challstr' messages to complete the login process TODO: allow simultaneous rooms? for chatbotting this is necessary, however the AI for battling is very CPU intensive; even one battle will pin resources so simultaneous battles is low priority. """ def __init__( self, username=None, password=None, accept_challenges=False, show_calcs=False, ai_strategy=None, *args, **kwargs ): super(Bot, self).__init__(*args, **kwargs) self.username = (username or raw_input("Showdown username: "******"utf-8").encode("ascii", "ignore") self.password = password or getpass.getpass() self.challenging = None self.accept_challenges = accept_challenges self.ai_strategy = ai_strategy self.show_calcs = show_calcs self.latest_request = None self.battleclient = None self.battleroom = None self.logged_in = False @property def battle_in_progress(self): return self.battleclient is not None and self.battleclient.win is None def start(self, interactive=True): self.logged_in = False self.connect() if not interactive: self.run_forever() # block until connection closes self.logged_in = False while not self.logged_in: time.sleep(0.1) def opened(self): log.i("Connected to %s" % self.url) def send(self, msg, _=False): log.i(sent(msg)) super(Bot, self).send(msg) def received_message(self, msg_block): """ Showdown sometimes sends multiple newline-separated messages in a single websocket message, so split them and process each one. 0-1 character messages can be ignored. """ msg_block = unicode(msg_block).encode("ascii", "ignore") log.i(received("\n".join((msg_block, "-" * 60)))) msg_block = msg_block.splitlines() battleroom = self.battleroom if msg_block[0].startswith(">battle"): if battleroom != msg_block[0][1:]: # Instantiate new battle room if msg_block[1] == "|init|battle": self.battleroom = msg_block[0][1:] self.battleclient = BattleClient( self.username, self.battleroom, self.send, self.show_calcs, self.ai_strategy ) self.latest_request = None self.challenging = None else: log.i("Battle message received for an inactive room:\n%s", msg_block) return elif battleroom == msg_block[0][1:] and ( msg_block[1].startswith("|expire") or msg_block[1].startswith("|deinit") ): self.battleroom = None self.battleclient = None self.send("|/leave %s" % battleroom) return # Save the most recent "request object"; use it to build team if client hasn't done so. # Set .request on the battleclient, because it occasionally needs to peek at the request # that it will process following this block if len(msg_block) > 1 and msg_block[1].startswith("|request|"): self.latest_request = self.battleclient.request = json.loads(msg_block[1].split("|")[2]) if self.battleclient.my_side is None: self.battleclient.build_my_side(self.latest_request) return for msg in msg_block: try: self.process_message(msg) except Exception: log.exception("Exception processing msg: %s", msg) # Process the request after the next message is sent (the server always sends it one message # before I want to use it). if self.latest_request is not None and msg_block[0].startswith(">battle"): self.battleclient.handle_request(self.latest_request) self.latest_request = None def process_message(self, msg): if msg.startswith(">") or len(msg) < 2: return msg = msg.split("|") msg_type = msg[0] if msg_type == "": msg.remove("") msg_type = msg[0] if msg_type in self.IGNORE_MSGS: return if msg_type in self.BATTLE_MSGS or msg_type.startswith("-"): return self.battleclient.handle(msg_type, msg) if msg_type in self.BOT_MSGS: return getattr(self, "handle_%s" % msg_type)(msg) log.e("Unhandled msg:\n%s", msg) BOT_MSGS = {"challstr", "updatechallenges", "popup"} BATTLE_MSGS = { "switch", "turn", "move", "request", "detailschange", "faint", "player", "inactive", "drag", "cant", "-item", "-enditem", "-ability", "-transform", "-start", "-end", "-activate", "callback", "-singleturn", "-singlemove", "-sidestart", "-sideend", "-fieldstart", "-fieldend", "-formechange", "detailschange", "-mega", "-supereffective", "-resisted", "-miss", "-immune", "-fail", "-crit", "win", "tie", "prematureend", "replace", "choice", } IGNORE_MSGS = { "updateuser", "queryresponse", "formats", "updatesearch", "title", "join", "gen", "tier", "rated", "rule", "start", "init", "gametype", "variation", "-hint", "-center", "-message", "-notarget", "-hitcount", "-nothing", "-waiting", "-combine", "chat", "c", "chatmsg", "chatmsg-raw", "raw", "html", "pm", "askreg", "inactiveoff", "join", "j", "leave", "l", "L", "spectator", "spectatorleave", "clearpoke", "poke", "teampreview", "swap", "done", "", "error", "warning", "gen", "debug", "unlink", "users", ":", "c:", "expire", "seed", "-endability", "-fieldactivate", "-primal", "n", } def handle_challstr(self, msg): url = "%s/action.php" % LOGIN_SERVER_URL values = { "act": "login", "name": self.username, "pass": self.password, "challengekeyid": msg[1], "challenge": msg[2], } r = requests.post(url, data=values) response = json.loads(r.text[1:]) # for reasons, the JSON response starts with a ']' self.send("|/trn %s,0,%s" % (self.username, response["assertion"])) self.logged_in = True def handle_updatechallenges(self, msg): """ |updatechallenges|{"challengesFrom":{"1raichu":"randombattle"}, "challengeTo":{"to":"0raichu","format":"randombattle"}} """ challenges = json.loads(msg[1]) challengeTo, challengesFrom = challenges["challengeTo"], challenges["challengesFrom"] if challengeTo: log.i("Challenging %s to %s", challengeTo["to"], challengeTo["format"]) self.challenging = challengeTo["to"] else: self.challenging = None if challengesFrom and self.accept_challenges and not self.battle_in_progress: challenger = challengesFrom.keys()[0] self.send("|/utm null") self.send("|/accept %s" % challenger) log.i("Accepted challenge from %s", challenger) def send_challenge(self, opponent, cancel=False): if cancel: self.cancel_challenge(self.challenging or opponent) if not self.challenging: self.send("|/challenge %s, randombattle" % opponent) self.challenging = opponent def cancel_challenge(self, opponent): self.send("|/cancelchallenge %s" % opponent) def handle_popup(self, msg): log.i("Popup: %s", msg[1])