From 220c777786129b7540b797773648b9e0edf91c8f Mon Sep 17 00:00:00 2001 From: svxf Date: Mon, 11 Mar 2019 12:36:02 +0400 Subject: [PATCH] init --- Pipfile | 13 +++++ Pipfile.lock | 58 +++++++++++++++++++ src/client.py | 30 ++++++++++ src/clientmsgs.py | 28 +++++++++ src/clientstates.py | 137 +++++++++++++++++++++++++++++++++++++++++++ src/misc.py | 74 ++++++++++++++++++++++++ src/msgs.py | 33 +++++++++++ src/server.py | 60 +++++++++++++++++++ src/servermsgs.py | 48 +++++++++++++++ src/serverstates.py | 138 ++++++++++++++++++++++++++++++++++++++++++++ src/state.py | 79 +++++++++++++++++++++++++ src/test.py | 13 +++++ 12 files changed, 711 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 src/client.py create mode 100644 src/clientmsgs.py create mode 100644 src/clientstates.py create mode 100644 src/misc.py create mode 100644 src/msgs.py create mode 100644 src/server.py create mode 100644 src/servermsgs.py create mode 100644 src/serverstates.py create mode 100644 src/state.py create mode 100644 src/test.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..26a83c1 --- /dev/null +++ b/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +numpy = "*" +jsonpickle = "*" + +[dev-packages] + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..c396621 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,58 @@ +{ + "_meta": { + "hash": { + "sha256": "e6a28f99a9431629e2780f9040349bda28f7b058b716eb2dd761d7b13a1decf7" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "jsonpickle": { + "hashes": [ + "sha256:0231d6f7ebc4723169310141352d9c9b7bbbd6f3be110cf634575d2bf2af91f0", + "sha256:625098cc8e5854b8c23b587aec33bc8e33e0e597636bfaca76152249c78fe5c1" + ], + "index": "pypi", + "version": "==1.1" + }, + "numpy": { + "hashes": [ + "sha256:1980f8d84548d74921685f68096911585fee393975f53797614b34d4f409b6da", + "sha256:22752cd809272671b273bb86df0f505f505a12368a3a5fc0aa811c7ece4dfd5c", + "sha256:23cc40313036cffd5d1873ef3ce2e949bdee0646c5d6f375bf7ee4f368db2511", + "sha256:2b0b118ff547fecabc247a2668f48f48b3b1f7d63676ebc5be7352a5fd9e85a5", + "sha256:3a0bd1edf64f6a911427b608a894111f9fcdb25284f724016f34a84c9a3a6ea9", + "sha256:3f25f6c7b0d000017e5ac55977a3999b0b1a74491eacb3c1aa716f0e01f6dcd1", + "sha256:4061c79ac2230594a7419151028e808239450e676c39e58302ad296232e3c2e8", + "sha256:560ceaa24f971ab37dede7ba030fc5d8fa173305d94365f814d9523ffd5d5916", + "sha256:62be044cd58da2a947b7e7b2252a10b42920df9520fc3d39f5c4c70d5460b8ba", + "sha256:6c692e3879dde0b67a9dc78f9bfb6f61c666b4562fd8619632d7043fb5b691b0", + "sha256:6f65e37b5a331df950ef6ff03bd4136b3c0bbcf44d4b8e99135d68a537711b5a", + "sha256:7a78cc4ddb253a55971115f8320a7ce28fd23a065fc33166d601f51760eecfa9", + "sha256:80a41edf64a3626e729a62df7dd278474fc1726836552b67a8c6396fd7e86760", + "sha256:893f4d75255f25a7b8516feb5766c6b63c54780323b9bd4bc51cdd7efc943c73", + "sha256:972ea92f9c1b54cc1c1a3d8508e326c0114aaf0f34996772a30f3f52b73b942f", + "sha256:9f1d4865436f794accdabadc57a8395bd3faa755449b4f65b88b7df65ae05f89", + "sha256:9f4cd7832b35e736b739be03b55875706c8c3e5fe334a06210f1a61e5c2c8ca5", + "sha256:adab43bf657488300d3aeeb8030d7f024fcc86e3a9b8848741ea2ea903e56610", + "sha256:bd2834d496ba9b1bdda3a6cf3de4dc0d4a0e7be306335940402ec95132ad063d", + "sha256:d20c0360940f30003a23c0adae2fe50a0a04f3e48dc05c298493b51fd6280197", + "sha256:d3b3ed87061d2314ff3659bb73896e622252da52558f2380f12c421fbdee3d89", + "sha256:dc235bf29a406dfda5790d01b998a1c01d7d37f449128c0b1b7d1c89a84fae8b", + "sha256:fb3c83554f39f48f3fa3123b9c24aecf681b1c289f9334f8215c1d3c8e2f6e5b" + ], + "index": "pypi", + "version": "==1.16.2" + } + }, + "develop": {} +} diff --git a/src/client.py b/src/client.py new file mode 100644 index 0000000..a7495ea --- /dev/null +++ b/src/client.py @@ -0,0 +1,30 @@ +import asyncio + +import msgs +from clientstates import ClientInitState +from misc import PORT +from state import StateMachine + + +async def run_client(): + reader, writer = await asyncio.open_connection('abra.me', PORT) + + state_machine = StateMachine() + init_state = ClientInitState(writer, state_machine) + + async def receiver_coro(): + while True: + msg = await msgs.recv(reader) + await state_machine.enqueue_msg(msg) + + receiver_task = asyncio.create_task(receiver_coro()) + + await state_machine.run(init_state) + + receiver_task.cancel() + + print('Close the connection') + writer.close() + + +asyncio.run(run_client()) diff --git a/src/clientmsgs.py b/src/clientmsgs.py new file mode 100644 index 0000000..3288659 --- /dev/null +++ b/src/clientmsgs.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass + +from msgs import Msg + + +@dataclass +class ClientDisconnected(Msg): + pass + + +@dataclass +class ClientCreateLobby(Msg): + pass + + +@dataclass +class ClientJoinLobby(Msg): + lobby_id: str + + +@dataclass +class ClientSherlockAnswered(Msg): + text: str + + +@dataclass +class ClientWatsonAsked(Msg): + text: str diff --git a/src/clientstates.py b/src/clientstates.py new file mode 100644 index 0000000..6972691 --- /dev/null +++ b/src/clientstates.py @@ -0,0 +1,137 @@ +import asyncio + +from clientmsgs import * +from servermsgs import * +from misc import user_input, Option, stdout_print +from state import State, EndState + + +class ClientInitState(State): + def __init__(self, writer, state_machine): + super().__init__(writer, state_machine) + + async def recv_ServerHelloMsg(self, msg: ServerHelloMsg): + await stdout_print(msg.motd) + + option_new_lobby = Option(r'1') + option_join_lobby = Option(r'2') + + result = await user_input( + '1) Create a new lobby\n' + '2) Join an existing lobby\n' + '> ', + option_new_lobby, + option_join_lobby, + ) + + if result == option_new_lobby: + await self.send(ClientCreateLobby()) + return ClientJoiningLobby(self.writer, self.state_machine, True) + else: + option_lobby_id = Option(r'.+') + + result = await user_input( + 'Lobby id: ', + option_lobby_id, + ) + + await self.send(ClientJoinLobby(lobby_id=result.text)) + return ClientJoiningLobby(self.writer, self.state_machine, False) + + +class ClientJoiningLobby(State): + is_sherlock: bool + + def __init__(self, writer, state_machine, is_sherlock): + super().__init__(writer, state_machine) + self.is_sherlock = is_sherlock + + async def recv_ServerJoinedLobby(self, msg: ServerJoinedLobby): + lobby_id = msg.lobby_id + + if self.is_sherlock: + return ClientSherlockState(self.writer, self.state_machine, lobby_id) + else: + return ClientWatsonState(self.writer, self.state_machine, lobby_id) + + async def recv_ServerNoSuchLobby(self, msg: ServerNoSuchLobby): + await stdout_print('No such lobby :(') + + return ClientInitState(self.writer, self.state_machine) + + async def recv_ServerFullLobby(self, msg: ServerFullLobby): + await stdout_print('Lobby is already full :(') + + return ClientInitState(self.writer, self.state_machine) + + +class ClientSherlockState(State): + lobby_id: str + ui_task: asyncio.Task + + def __init__(self, writer, state_machine, lobby_id): + super().__init__(writer, state_machine) + + self.lobby_id = lobby_id + self.ui_task = None + + async def init(self): + await stdout_print(f'You joined lobby {self.lobby_id}') + + self.ui_task = asyncio.create_task(self.ui()) + + async def ui(self): + option = Option(r'Y|N') + + while True: + await user_input('Wait for a question and answer with Y/N\n', option) + + await self.send(ClientSherlockAnswered(option.text)) + + async def recv_ServerNoWatson(self, msg: ServerNoWatson): + await stdout_print('Wait, there\'s no Watson yet!') + + async def recv_ServerWatsonAsked(self, msg: ServerWatsonAsked): + await stdout_print(f'Watson asked: {msg.text}') + + async def recv_ServerWatsonConnected(self, msg: ServerWatsonConnected): + await stdout_print(f'A Watson connected to your lobby!') + + async def recv_ServerPartnerDisconnected(self, msg: ServerPartnerDisconnected): + await stdout_print('Your Watson died untimely :(') + + self.ui_task.cancel() + + return EndState(self.writer, self.state_machine) + + +class ClientWatsonState(State): + lobby_id: str + ui_task: asyncio.Task + + def __init__(self, writer, state_machine, lobby_id): + super().__init__(writer, state_machine) + + self.lobby_id = lobby_id + self.ui_task = None + + async def init(self): + await stdout_print(f'You joined lobby {self.lobby_id}') + + self.ui_task = asyncio.create_task(self.ui()) + + async def ui(self): + option = Option(r'.+\?') + + while True: + await user_input('Ask a yes/no question: ', option) + + await self.send(ClientWatsonAsked(option.text)) + + async def recv_ServerSherlockAnswered(self, msg: ServerSherlockAnswered): + await stdout_print(f'Sherlock answered: {msg.text}') + + async def recv_ServerPartnerDisconnected(self, msg: ServerPartnerDisconnected): + await stdout_print('Your Sherlock died untimely :(') + + return EndState(self.writer, self.state_machine) diff --git a/src/misc.py b/src/misc.py new file mode 100644 index 0000000..fc8ddbe --- /dev/null +++ b/src/misc.py @@ -0,0 +1,74 @@ +import asyncio +import os +import re +import sys +from dataclasses import dataclass + +PORT = 51423 + + +@dataclass +class Option: + regex: str + text: str + + def __init__(self, regex): + self.regex = regex + self.text = None + + +async def user_input(prompt, *options): + while True: + await stdout_print(prompt, end='') + + text = await stdin_readline() + + # await stdout_print(f'user->"{text}"') + + for option in options: + if re.fullmatch(option.regex, text): + option.text = text + # await stdout_print(f'OK {option}') + return option + + await stdout_print('Unrecognized option') + + +async def stdio(limit=asyncio.streams._DEFAULT_LIMIT, loop=None): + if loop is None: + loop = asyncio.get_event_loop() + + reader = asyncio.StreamReader(limit=limit, loop=loop) + await loop.connect_read_pipe( + lambda: asyncio.StreamReaderProtocol(reader, loop=loop), sys.stdin) + + writer_transport, writer_protocol = await loop.connect_write_pipe( + lambda: asyncio.streams.FlowControlMixin(loop=loop), + os.fdopen(sys.stdout.fileno(), 'wb')) + writer = asyncio.streams.StreamWriter( + writer_transport, writer_protocol, None, loop) + + return reader, writer + + +stdio_reader = None +stdio_writer = None + + +async def stdin_readline(): + global stdio_reader, stdio_writer + + if stdio_reader is None: + stdio_reader, stdio_writer = await stdio() + + return (await stdio_reader.readline()).decode().strip() + + +async def stdout_print(*args, end='\n'): + global stdio_reader, stdio_writer + + if stdio_reader is None: + stdio_reader, stdio_writer = await stdio() + + stdio_writer.write((' '.join(args) + end).encode()) + await stdio_writer.drain() diff --git a/src/msgs.py b/src/msgs.py new file mode 100644 index 0000000..3dca661 --- /dev/null +++ b/src/msgs.py @@ -0,0 +1,33 @@ +from asyncio import StreamReader, StreamWriter +from dataclasses import dataclass + +import jsonpickle + + +@dataclass +class Msg: + pass + + +async def send(writer: StreamWriter, msg: Msg): + msg_str = jsonpickle.dumps(msg) + '\n' + msg_bytes = msg_str.encode() + + # print(f'Sending -> {msg_str}') + + writer.write(msg_bytes) + await writer.drain() + + +async def recv(reader: StreamReader) -> Msg: + msg_bytes = await reader.readline() + msg_str = msg_bytes.decode() + + # print(f'Received <- {msg_str}') + + msg = jsonpickle.loads(msg_str) + + if not isinstance(msg, Msg): + raise ValueError() + + return msg diff --git a/src/server.py b/src/server.py new file mode 100644 index 0000000..7051e64 --- /dev/null +++ b/src/server.py @@ -0,0 +1,60 @@ +import asyncio +from asyncio import StreamReader, StreamWriter + +import msgs +from clientmsgs import ClientDisconnected +from misc import PORT +from serverstates import ServerInitState +from state import StateMachine + + +async def run_server(reader: StreamReader, writer: StreamWriter): + addr = writer.get_extra_info('peername') + print(f'New connection from {addr}.') + + state_machine = StateMachine() + init_state = ServerInitState(writer, state_machine) + + # separate concurrent coroutine to read incoming messages + + async def receiver_coro(): + while True: + msg = await msgs.recv(reader) + await state_machine.enqueue_msg(msg) + + receiver_task = asyncio.create_task(receiver_coro()) + + # and another to check for client drops + + async def disconnect_checker_coro(): + while True: + if reader.at_eof(): + await state_machine.enqueue_msg(ClientDisconnected()) + return + await asyncio.sleep(0.1) + + disconnect_checker_task = asyncio.create_task(disconnect_checker_coro()) + + # run the machine! + + await state_machine.run(init_state) + + receiver_task.cancel() + disconnect_checker_task.cancel() + + print(f'Connection over {addr}.') + writer.close() + + +async def main(): + server = await asyncio.start_server(run_server, '0.0.0.0', PORT) + + addr = server.sockets[0].getsockname() + print(f'Serving on {addr}') + + async with server: + await server.serve_forever() + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/servermsgs.py b/src/servermsgs.py new file mode 100644 index 0000000..a6e3558 --- /dev/null +++ b/src/servermsgs.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass + +from msgs import Msg + + +@dataclass +class ServerHelloMsg(Msg): + motd: str + + +@dataclass +class ServerJoinedLobby(Msg): + lobby_id: str + + +@dataclass +class ServerNoSuchLobby(Msg): + pass + + +@dataclass +class ServerFullLobby(Msg): + pass + + +@dataclass +class ServerPartnerDisconnected(Msg): + pass + + +@dataclass +class ServerSherlockAnswered(Msg): + text: str + + +@dataclass +class ServerWatsonAsked(Msg): + text: str + + +@dataclass +class ServerNoWatson(Msg): + pass + + +@dataclass +class ServerWatsonConnected(Msg): + pass diff --git a/src/serverstates.py b/src/serverstates.py new file mode 100644 index 0000000..09fc7ae --- /dev/null +++ b/src/serverstates.py @@ -0,0 +1,138 @@ +import uuid + +from servermsgs import * +from clientmsgs import * +from state import State, StateMachine, EndState + +lobbies = {} + + +class Lobby: + id: str + sherlock_sm: StateMachine + watson_sm: StateMachine + + def __init__(self, id, sherlock_sm): + self.id = id + self.sherlock_sm = sherlock_sm + self.watson_sm = None + + +# noinspection PyUnusedLocal +class ServerInitState(State): + def __init__(self, writer, state_machine): + super().__init__(writer, state_machine) + + async def init(self): + await self.hello() + + async def hello(self): + await self.send(ServerHelloMsg( + motd='Privet!!!\n' + f'There are {len(lobbies)} lobbies' + )) + + async def recv_ClientCreateLobby(self, msg: ClientCreateLobby): + lobby_id = uuid.uuid4().hex + + lobby = Lobby(id=lobby_id, sherlock_sm=self.state_machine) + lobbies[lobby_id] = lobby + + await self.send(ServerJoinedLobby(lobby_id)) + return ServerSherlockState(self.writer, self.state_machine, lobby) + + async def recv_ClientJoinLobby(self, msg: ClientJoinLobby): + lobby_id = msg.lobby_id + + if lobby_id not in lobbies: + await self.send(ServerNoSuchLobby()) + await self.hello() + return + + lobby = lobbies[lobby_id] + + if lobby.watson_sm is not None: + await self.send(ServerFullLobby()) + await self.hello() + return + + lobby.watson_sm = self.state_machine + await lobby.sherlock_sm.enqueue_msg(ServerWatsonConnected()) + + await self.send(ServerJoinedLobby(lobby_id)) + return ServerWatsonState(self.writer, self.state_machine, lobby) + + async def recv_ClientDisconnected(self, msg: ClientDisconnected): + print('Client died :(') + return EndState(self.writer, self.state_machine) + + +class ServerSherlockState(State): + lobby: Lobby + + def __init__(self, writer, state_machine, lobby): + super().__init__(writer, state_machine) + + self.lobby = lobby + + async def recv_ServerWatsonConnected(self, msg: ServerWatsonConnected): + await self.send(msg) + + async def recv_ClientSherlockAnswered(self, msg: ClientSherlockAnswered): + if not self.lobby.watson_sm: + await self.send(ServerNoWatson()) + return + + await self.lobby.watson_sm.enqueue_msg(ServerSherlockAnswered(msg.text)) + + async def recv_ServerWatsonAsked(self, msg: ServerWatsonAsked): + await self.send(msg) + + def delist(self): + del lobbies[self.lobby.id] + print(f'Delisted lobby {self.lobby.id}') + + async def recv_ClientDisconnected(self, msg: ClientDisconnected): + print('Shelock died :(') + + self.delist() + + if self.lobby.watson_sm: + await self.lobby.watson_sm.enqueue_msg(ServerPartnerDisconnected()) + print(f'Killing watson') + + return EndState(self.writer, self.state_machine) + + async def recv_ServerPartnerDisconnected(self, msg: ServerPartnerDisconnected): + await self.send(ServerPartnerDisconnected()) + + self.delist() + + return EndState(self.writer, self.state_machine) + + +class ServerWatsonState(State): + lobby: Lobby + + def __init__(self, writer, state_machine, lobby): + super().__init__(writer, state_machine) + + self.lobby = lobby + + async def recv_ClientWatsonAsked(self, msg: ClientWatsonAsked): + await self.lobby.sherlock_sm.enqueue_msg(ServerWatsonAsked(msg.text)) + + async def recv_ServerSherlockAnswered(self, msg: ServerSherlockAnswered): + await self.send(msg) + + async def recv_ClientDisconnected(self, msg: ClientDisconnected): + print('Watson died :(') + + await self.lobby.sherlock_sm.enqueue_msg(ServerPartnerDisconnected()) + + return EndState(self.writer, self.state_machine) + + async def recv_ServerPartnerDisconnected(self, msg: ServerPartnerDisconnected): + await self.send(ServerPartnerDisconnected()) + + return EndState(self.writer, self.state_machine) diff --git a/src/state.py b/src/state.py new file mode 100644 index 0000000..61066b9 --- /dev/null +++ b/src/state.py @@ -0,0 +1,79 @@ +from asyncio import StreamWriter, Queue + +import msgs +from msgs import Msg + + +class State: + writer: StreamWriter + state_machine: 'StateMachine' + + def __init__(self, writer, state_machine): + self.writer = writer + self.state_machine = state_machine + + async def init(self): + pass + + async def send(self, msg: Msg): + await msgs.send(self.writer, msg) + + async def route_msg(self, msg): + if not isinstance(msg, Msg): + raise ValueError() + + msg_class_name = msg.__class__.__name__ + + method_name = f'recv_{msg_class_name}' + + if not hasattr(self, method_name): + print(f'ignoring msg: {self} <- {msg}') + return + + method = getattr(self, method_name) + + return await method(msg=msg) + + def __str__(self): + return f'{self.__class__.__name__}' + + +class EndState(State): + pass + + +class StateMachine: + state: State + msg_queue: Queue + + def __init__(self): + self.state = None + self.msg_queue = Queue() + + async def enqueue_msg(self, msg: Msg): + await self.msg_queue.put(msg) + + async def run(self, init_state): + self.state = init_state + state_initialized = False + + while not isinstance(self.state, EndState): + if not state_initialized: + await self.state.init() + state_initialized = True + + msg = await self.msg_queue.get() + + # print(f'state {self.state} received {msg}') + + next_state = await self.state.route_msg(msg) + + if next_state is None: + next_state = self.state + + if next_state is not self.state: + state_initialized = False + + # print(f'{self.state} + {msg} = {next_state}') + + self.state = next_state diff --git a/src/test.py b/src/test.py new file mode 100644 index 0000000..5471336 --- /dev/null +++ b/src/test.py @@ -0,0 +1,13 @@ +import json +from dataclasses import dataclass + + +@dataclass +class A: + a: int + + +if __name__ == '__main__': + print( + """1) Create a new lobby + 2) Join an existing lobby""")