@@ -0,0 +1,359 @@
import asyncio
from asyncio.queues import Queue
from collections import defaultdict
from collections import deque
from datetime import datetime
import json
import logging

import aiohttp
import websockets

from dywypi.event import Event

log = logging.getLogger(__name__)


# TODO SOME PROBLEMS STILL
# - when you disconnect, you have to rejoin the battle manually..?? i did not get an updatechallenges. but it still let me continue the battle??
# possibly listed in updatesearch? this was localhost though so i can't be sure it's not just... a list of arbitrary battles, or ones i was watching as well, or...
# <<< recv: None: updatesearch ['{"searching":[],"games":{"battle-randombattle-2":"Random Battle"}}']
# - how do you tell what's old stuff from joining a channel and what's new? i guess, uh, maybe the effects of a move can just be the return value from making the move?


# TODO make all this stuff align better with the existing IRC stuff, and come up with some interfaces, and so on

class ShowdownUser:
def __init__(self, name, mode):
self.name = name
self.mode = mode

@classmethod
def parse(cls, s):
return cls(s[1:], s[0])


class ShowdownMessage:
def __init__(self, room, type, args):
self.room = room
# TODO rename to msgtype?
self.type = type
self.args = args

@classmethod
def parse(cls, room, line):
if line[0] == '|':
_, type, *args = line.split('|')
return cls(room, type, args)
else:
return cls(room, None, (line,))


# ------------------------------------------------------------------------------
# Battle stuff

# TODO docs on expanding this:
# https://github.com/Zarel/Pokemon-Showdown/blob/master/PROTOCOL.md#action-requests
# currently not handled:
# - megas (add as a final param to /move)
# - 2v2 or 3v3 (simultaneous choices -- makes `await choose()` more complicated)
# - moves with targets (only 2v2 or 3v3)

class BattleMove:
def __init__(self, client, room, data):
self.client = client
self.room = room

self.name = data['move']
self.ident = data['id']
# These may not exist if we're "trapped"
self.pp = data['pp']
self.max_pp = data['maxpp']
self.target = data['target']
self.disabled = data['disabled']

async def choose(self):
await self.client.send_raw(self.room, '/move ' + self.ident)


class BattlePokemon:
def __init__(self, client, room, position, data):
self.client = client
self.room = room

self.position = position
# TODO should parse this; seems to be "playerid: Name"
self.ident = data['ident']
# TODO parse me; is "Name, Lnn[, M/F]"
self.details = data['details']
self.condition = data['condition']
self.active = data['active']
self.stats = data['stats']
self.moves = data['moves']
self.base_ability = data['baseAbility']
self.item = data['item']
self.pokeball = data['pokeball']

async def choose(self):
await self.client.send_raw(self.room, '/switch ' + str(self.position))


class BattleTeam:
def __init__(self, client, room, data):
self.client = client
self.room = room

self.name = data['name']
self.id = data['id']
self.pokemon = [
BattlePokemon(client, room, n, datum)
for n, datum in enumerate(data['pokemon'], start=1)
]

def __iter__(self):
return iter(self.pokemon)


class BattleState:
def __init__(self, client, room, data):
# TODO maybe these should be wrapped in a Battle object
self.client = client
self.room = room

# TODO unclear how exactly this works in 2v2 or 3v3
self.must_switch = any(data.get('forceSwitch', ()))

if 'active' in data:
# TODO this sometimes has a key 'trapped', but... is that useful or interesting?
# TODO actually i think i'd like to just auto-respond when we're trapped
self.active_moves = [
[BattleMove(client, room, movedef)
for movedef in activedef['moves']]
for activedef in data['active']
]

self.team = BattleTeam(client, room, data['side'])
# TODO use this when choosing
self.request_id = data.get('rqid')


class ChallengeReceived(Event):
_invalid = False

def __init__(self, from_user, to_user, battle_type, **kwargs):
super().__init__(**kwargs)

self.issued_at = datetime.now()
self.from_user = from_user
self.to_user = to_user
self.battle_type = battle_type

async def accept(self):
if self._invalid:
raise RuntimeError
await self.client.send_raw(None, '/accept ' + self.from_user)
# TODO at this point it really becomes a battle, right? unless it's expired, or...
self._invalid = True


class BattleEnded(Event):
def __init__(self, winner, **kwargs):
super().__init__(**kwargs)

self.winner = winner


class ActionRequest(Event):
"""A request from the server for you to make a move or switch out."""
def __init__(self, raw_data, **kwargs):
super().__init__(**kwargs)

self.battle_state = BattleState(self.client, self.raw_message.room, raw_data)


# state
class Channel:
def __init__(self, name, room_type):
self.name = name
self.room_type = room_type



class ShowdownClient:
def __init__(self, loop, network):
self.loop = loop
self.network = network

# TODO the whole "network" system is designed for irc and makes no
# sense for showdown; this is the primary server, hardcoded for now
#self.url = 'ws://sim.smogon.com/showdown/websocket'
# or, for local development:
self.url = 'ws://localhost:8000/showdown/websocket'

self.websocket = None
self.username = None
self.is_guest = None
self.avatar = None

# TODO private until i can figure out how this should look
self._challenges_from = {}

self._read_loop_task = None
self._current_room = None
self._event_queue = Queue(loop=self.loop)
self._awaiting_messages = defaultdict(deque)
self._challenge = None

async def connect(self):
self.websocket = await websockets.connect(self.url, loop=self.loop)
self._read_loop_task = asyncio.ensure_future(self._read_loop(), loop=self.loop)

# TODO this is the worst
await self.set_name('eevee-sandbox')

async def disconnect(self):
self._read_loop_task.cancel()
await self.websocket.close()
self.websocket = None

async def __aenter__(self):
self.connect()
return self

async def __aexit__(self, exc_type, exc, tb):
await self.disconnect()

async def _read_loop(self):
pending = deque()

while True:
while not pending:
text = await self.websocket.recv()
pending.extend(msg for msg in text.split('\n') if msg)
while pending and pending[0].startswith('>'):
self._current_room = pending.popleft()[1:]

event = ShowdownMessage.parse(self._current_room, pending.popleft())
if not (event.type and event.type.isupper()):
log.debug("<<< recv: {}: {} {!r}".format(event.room, event.type, event.args))

method = "_handle__{}".format(event.type)
if hasattr(self, method):
await getattr(self, method)(event)

awaiting = self._awaiting_messages[event.type]
while awaiting:
awaiting.popleft().set_result(None)

await self._event_queue.put(event)

async def read_event(self):
return await self._event_queue.get()

async def _handle__challstr(self, event):
self._challenge = '|'.join(event.args)

def _wait_message(self, message_type):
fut = asyncio.Future()
self._awaiting_messages[message_type].append(fut)
return fut

async def get_challenge(self):
if not self._challenge:
await self._wait_message('challstr')
return self._challenge

async def send_raw(self, room, text):
log.debug('>>> send: {} {}'.format(repr(room), repr(text)))
# TODO do commands have more structured arguments?
if room is None:
room = ''
await self.websocket.send("{}|{}".format(room, text))

async def say(self, room, text):
# TODO should reject commands here eventually
await self.send_raw(room, text)

async def set_name(self, username):
# NOTE: this is the same as login but it uses "getassertion" and no password. also i dunno
# TODO this should wait on the challstr
# TODO but then this can't be called from the same stack that's reading events. unless there's an independent big ol' buffer
challenge = await self.get_challenge()
# TODO in my browser this has a /~~localhost/ bit
async with aiohttp.get('https://play.pokemonshowdown.com/action.php', params={'act': 'getassertion', 'userid': username, 'challstr': challenge}, loop=self.loop) as resp:
# seems to give back either the assertion, or a single semicolon on failure (i.e. the nick is taken)???
assertion = await resp.text()
if assertion == ";":
# TODO yadda yadda
raise RuntimeError
await self.websocket.send('|' + '/trn {},0,{}'.format(username, assertion))

async def login(self, username, password):
# TODO this should wait on the challstr
# TODO but then this can't be called from the same stack that's reading events. unless there's an independent big ol' buffer
challenge = await self.get_challenge()
# TODO in my browser this has a /~~localhost/ bit
async with aiohttp.post('https://play.pokemonshowdown.com/action.php', data={'act': 'login', 'name': username, 'pass': password, 'challstr': challenge}, loop=self.loop) as resp:
text = await resp.text()
login_payload = json.loads(text[1:])
await self.websocket.send('|' + '/trn {},0,{}'.format(username, login_payload['assertion']))

async def _handle__updateuser(self, event):
new_name, new_logged_in, new_avatar = event.args
self.username = new_name
if new_logged_in == '1':
self.is_guest = False
elif new_logged_in == '0':
self.is_guest = True
else:
self.is_guest = None
self.avatar = new_avatar

# --------------------------------------------------------------------------
# CHAT STUFF

async def _handle__init(self, event):
# Joined a channel
# TODO surprise, this doesn't actually do anything
Channel(event.room, event.args[0])

# --------------------------------------------------------------------------
# BATTLE STUFF

async def _handle__updatechallenges(self, event):
try:
new_challenges = json.loads(event.args[0])
except json.decoder.JSONDecodeError:
# TODO what do i do here, i wonder.
return

for from_user, battle_type in new_challenges.get('challengesFrom', {}).items():
if from_user not in self._challenges_from:
# TODO i guess this should use more objects or whatever; do i have user objects from irc land?
await self._event_queue.put(
ChallengeReceived(from_user, self.username, battle_type, client=self, raw=event))

# TODO should update this more, ah, carefully, and put the challenges in it, etc...
self._challenges_from = new_challenges.get('challengesFrom', {})

# TODO handle challenging other users too...!

async def _handle__request(self, message):
# TODO megas are a thing! but i have no idea how you do them
# TODO errors here are being silently lost
# Sim is requesting that you choose what to do
data = json.loads(message.args[0])
log.debug(repr(data))
if not data:
# For some reason I just get 'null' sometimes?
return
if data.get('wait'):
# Nothing to do; don't bother firing an event
return
log.debug("ALRIGHT, let's go, adding to the queue")
await self._event_queue.put(
ActionRequest(data, client=self, raw=message))

async def _handle__win(self, message):
# Sim is announcing the end of a battle
await self._event_queue.put(BattleEnded(message.args[0], client=self, raw=message))