This commit is contained in:
2019-03-11 12:36:02 +04:00
commit 220c777786
12 changed files with 711 additions and 0 deletions

13
Pipfile Normal file
View File

@@ -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"

58
Pipfile.lock generated Normal file
View File

@@ -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": {}
}

30
src/client.py Normal file
View File

@@ -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())

28
src/clientmsgs.py Normal file
View File

@@ -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

137
src/clientstates.py Normal file
View File

@@ -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)

74
src/misc.py Normal file
View File

@@ -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()

33
src/msgs.py Normal file
View File

@@ -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

60
src/server.py Normal file
View File

@@ -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())

48
src/servermsgs.py Normal file
View File

@@ -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

138
src/serverstates.py Normal file
View File

@@ -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)

79
src/state.py Normal file
View File

@@ -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

13
src/test.py Normal file
View File

@@ -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""")