Search code examples
pythonasynchronoustornadocoroutinegame-development

Tornado client with stdin


I am trying to build a multiplayer game that uses Python. I am using Tornado to build the client and server. Ideally, what I would like to happen are as follows:

(a) For the client to wait for user input from the command line

(b) When the client gets user input, to send the user input to the server

(c) for the server to simulate some processing(which will be the game engine) on it and send the response back to a client.

The server

"""
    Server module for game server
"""

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.websocket
import uuid
import json

class Controller(object):
    def __init__(self):
        self.players = ()
        self.connections = ()

    def check(self):
        return "Hi"

    def run_somthing(self, text):
        new_text = "Server: " + text
        return new_text

class InitMessageHandler(tornado.web.RequestHandler):
    def get(self):
        user_data = {}
        user_data['user_id'] = str(uuid.uuid4())
        self.write(json.dumps(user_data))

class GameHandler(tornado.websocket.WebSocketHandler):

    def open(self):
        # called anytime a new connection with this server is opened
        print("Client connected") 
        print("Client sent: ", self)
        if seif not in self.application.controller.connections:
            self.application.controller.connections.add(self)

    def on_message(self):
        # called anytime a new message is received
        pass

    def check_origin(self, origin):
        return True

    def on_close(self):
        # called a websocket connection is closed
        if self in self.application.controller.connections:
            self.application.controller.connections.remove(self)

class Server(tornado.web.Application):
    def __init__(self):
        self.controller = Controller()
        handlers = [
            (r"/join", InitMessageHandler),
            (r"/game", GameHandler)
        ]
        tornado.web.Application.__init__(self, handlers)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    try:
        application = Server()
        server = tornado.httpserver.HTTPServer(application)
        server.listen(8888)
        tornado.ioloop.IOLoop.instance().start()
    except KeyboardInterrupt:
        tornado.ioloop.IOLoop.instance().stop()
        print("closed") 

The client

"""
    Client module for the game clients(Players)
"""

import tornado.ioloop
import tornado.websocket
import requests
import json
import sys
import tornado.gen

class Client(object):
    def __init__(self, join_url, play_url):
        self.wsconn = None
        self.join_url = join_url
        self.play_url = play_url
        #self.io_loop = tornado.ioloop.IOLoop.instance()
        #self.io_loop.add_handler(sys.stdin, self.handle_user_input, tornado.ioloop.IOLoop.READ)
        self.user_details = {}
        self.has_initialised = False
        #self.io_loop.start()
        self.main()

    def get_user_input(self, question=None):
        str_choice = input(question)
        while any((str_choice is None, not str_choice.strip())):
            print("You entered an empty answer")
            str_choice = input(question)
        return str_choice 

    def _merge_dicts(*dict_args):
        """
        Given any number of dicts, shallow copy and merge into a new dict,
        precedence goes to key value pairs in latter dicts.
        """
        result = {}
        for dictionary in dict_args:
            result.update(dictionary)
        return result

    def generate_wsmessage(self):
        msg_line = input("Enter message to send to server")
        while any((msg_line is None, not msg_line.strip())):
            print("You entered an empty answer")
            msg_line = input("Enter message to send to server")
        msg = {}
        msg['message'] = msg_line
        msg_to_send = self._merge_dicts(self.user_details, msg)
        return json.dumps(msg_to-send)

    def init(self):
        print("Heh")
        username = self.get_user_input("What is your username? ")
        print("Getting initial user details")
        req = requests.get(self.join_url)
        response = json.loads(req.text)
        print(response)
        self.user_details['name'] = username
        self.user_details['user_id'] = response['user_id']
        self.has_initialised = True

    def server_recv(self, msg):
        print("Server has connected on websocket socket with msg=", msg)

    @tornado.gen.coroutine
    def connect_on_websocket(self):
        try:
            self.wsconn = yield tornado.websocket.websocket_connect(self.play_url, on_message_callback=self.server_recv)
        except Exception as e:
            print("Connection error: {}".format(e))
        else:
            print("Connected")

    @tornado.gen.coroutine
    def send_wsmessage(self):
        msg = self.generate_wsmessage()
        yield self.wsconn.write_message(msg)

    @tornado.gen.coroutine
    def communicate_with_websocket(self):
        self.send_wsmessage()
        while True:
            recv_msg = yield self.wsconn.read_message()
            if recv_msg is None: 
                self.wsconn.close()
                break
            yield tornado.gen.sleep(0.1)
            self.send_wsmessage()
        print("IoLoop terminate")

    def main(self):
        choice = input("Do you want to continue(y/n)? ")
        if choice == "y" and self.has_initialised == False:
            print("Yup")
            self.init()
        if self.has_initialised == True:
            self.connect_on_websocket()
        self.communicate_with_websocket()

if __name__ == "__main__":
    try:
        client = Client("http://localhost:8888/join", "ws://localhost:8888/game")
        tornado.ioloop.IOLoop.instance().start()
    except (SystemExit, KeyboardInterrupt):
        print("Client closed")

From reading the some examples online, I came up the code above, but it is not working. So my main question is

how to make Tornado coroutines work with stdin(command line input)

My other questions are:

(a) Is the code I have written the right way to work with Tornado coroutines or not?

(b) If it is not, could you ELI5? Also I would appreciate code examples that really use Tornado in interesting ways(at any intermediate level) so that I can learn from them.

(c) Is there a more intuitive way to do what I want to do,in Python? Like a Flask+Gevents or Twisted version or just pure sockets version that might be easier to work with?

Thanks for your help.

EDIT: Flan pointed out some errors for me and I fixed it and it works now.


Solution

  • As I can see at the moment, problem is not in stdin interaction, but your wrong way of using coroutines. Your connect_on_websocket and communicate_with_websocket functions are coroutines but you are using them as a plain functions and it won't work. I propose these changes.

    1. Make main() coroutine (add decorator), don't call it, remove from the Client.__init__().
    2. In name=main block schedule client.main() invocation with tornado.ioloop.IOLoop.instance().add_callback(client.main).
    3. In main and all your code change coroutine-functions' (with @tornado.gen.coroutine) calls to yield, for example yield self.connect_on_websocket() instead of just self.connect_on_websocket()

    That should be sufficient so you can proceed your development further.