📜 ⬆️ ⬇️

Writing a multiplayer snake on tornado

Some time ago I decided to write a small application to practice working with websockets. From the Python frameworks it seemed to me more convenient that they were boxed in tornado. Since the toy is extremely simple, it may seem useful to someone as an example. This is a multiplayer "snake".



The whole "front" fits into one html file. Here is a script from it

var canvas = document.getElementById('canvas'), c = canvas.getContext('2d'), direction='up', nick='Anonymous'; c.lineWidth = 1; var snakes=[ ]; var apples=[ [10,10], [2,2] ]; var colors = ['red', 'blue', 'green', 'black', 'purple', 'teal', 'navy', 'lime', 'olive', 'maroon', 'aqua'] function redraw(){ c.clearRect(0, 0, canvas.width, canvas.height); c.stroke(); for(var j=0; j<snakes.length; j++){ c.fillStyle=colors[j]; for(var i=0; i<snakes[j].length; i++) { c.fillRect(snakes[j][i][0]*10,snakes[j][i][1]*10, 10,10); } c.stroke(); } for(var i=0; i<apples.length; i++){ c.strokeStyle="#FF00DD"; c.beginPath(); c.arc(apples[i][0]*10+5,apples[i][1]*10+5,5,0,2*Math.PI); c.stroke(); } } var updater = { socket: null, start: function() { if(updater.socket && updater.socket.readyState !== updater.socket.CLOSED) return; var url = "ws://" + location.host + "/gamesocket"; updater.socket = new ReconnectingWebSocket(url); updater.socket.onmessage = function(event) { updater.showMessage(JSON.parse(event.data)); redraw(); } updater.socket.onopen = function(event){ updater.socket.send(JSON.stringify({'nick': nick})); } }, showMessage: function(message) { snakes = message.snakes; apples = message.apples; document.getElementById('scores').innerHTML=""; for(var i=0; i<message.scores.length; i++){ document.getElementById('scores').innerHTML+=message.scores[i][0]+': '+message.scores[i][1]+'<br>'; } } } 

We get the coordinates of all the snakes and apples from the website and draw on the canvas.
')
  document.onkeydown = function(e){ var keys = {37:'left', 39:'right', 38:'up', 40:'down'}; var k = keys[e.keyCode]; if(k && k != direction){ if(direction == 'up' && k == 'down') return; if(direction == 'down' && k == 'up') return; if(direction == 'left' && k == 'right') return; if(direction == 'right' && k == 'left') return; direction = keys[e.keyCode]; updater.socket.send(JSON.stringify({direction:direction})); } } window.onload = function(){ nick = window.prompt("Enter your name","Anonymous"); if(!nick) nick = 'Anonymous'; updater.start(); } 

When you click the arrows, we send a new direction to the server (if it has changed).

Now, server side. She is also small.

 import logging import tornado.escape import tornado.ioloop import tornado.options import tornado.web import tornado.websocket from tornado import gen import os.path import uuid import json import random from datetime import datetime from tornado.options import define, options SIZE = 100, 60 define("port", default=8000, help="run on the given port", type=int) class Application(tornado.web.Application): def __init__(self): handlers = [ (r"/", IndexHandler), (r"/gamesocket", GameSocketHandler), (r"/files/(.*)", tornado.web.StaticFileHandler, {'path': 'files'}), ] settings = dict( cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, autoreload=True, debug=True, ) tornado.web.Application.__init__(self, handlers, **settings) class IndexHandler(tornado.web.RequestHandler): def get(self): self.render("index.html") class GameSocketHandler(tornado.websocket.WebSocketHandler): players = set() apples = [[random.randint(0, SIZE[0]), random.randint(0, SIZE[1])] for i in range(20) ] def get_compression_options(self): # Non-None enables compression with default options. return {} def open(self): x_real_ip = self.request.headers.get("X-Real-IP") self.ip = x_real_ip or self.request.remote_ip self.direction = 'up' self.score = 0 self.nick = 'Anonymous' self.die() GameSocketHandler.players.add(self) GameSocketHandler.send_updates() def die(self): while 1: x, y = random.randint(0, SIZE[0]), random.randint(0, SIZE[1]) if any(x == sx and y == sy for player in GameSocketHandler.players for sx, sy in player.snake): continue if any(x == sx and y == sy for sx, sy in GameSocketHandler.apples): continue break self.snake = [[x, y]] @classmethod def add_apple(cls): while 1: x, y = random.randint(0, SIZE[0]), random.randint(0, SIZE[1]) if any(x == sx and y == sy for player in GameSocketHandler.players for sx, sy in player.snake): continue if any(x == sx and y == sy for sx, sy in GameSocketHandler.apples): continue break cls.apples.append([x, y]) def on_close(self): GameSocketHandler.players.remove(self) @classmethod def send_updates(cls): #logging.info("sending message to %d waiters", len(cls.players)) data = { 'snakes': [player.snake for player in cls.players], 'apples': cls.apples, 'scores': [[player.nick, player.score] for player in cls.players] } for waiter in cls.players: try: data['head'] = waiter.snake[-1] waiter.write_message(data) except: logging.error("Error sending message", exc_info=True) def on_message(self, message): message = json.loads(message) logging.info("Got message %r" % message) if 'direction' in message: if message['direction'] not in ['up', 'left', 'right', 'down']: return self.direction = message['direction'] elif 'nick' in message: self.nick = message['nick'].replace('<', '').replace('>', '') def game_tick(): for player in GameSocketHandler.players: d = {'up': (0,-1), 'down': (0,1), 'left': (-1, 0), 'right': (1,0)} player.snake.append([player.snake[-1][0]+d[player.direction][0], player.snake[-1][1]+d[player.direction][1]]) if player.snake[-1] in GameSocketHandler.apples: GameSocketHandler.apples.remove(player.snake[-1]) GameSocketHandler.add_apple() player.score += 1 else: player.snake.pop(0) if player.snake[-1][0] < 0 or player.snake[-1][1] < 0 or player.snake[-1][0] >= SIZE[0] or player.snake[-1][1] >= SIZE[1]: player.die() for enemy in GameSocketHandler.players: if enemy != player and player.snake[-1] in enemy.snake: player.die() if player.snake[-1] in player.snake[:-1]: player.die() GameSocketHandler.send_updates() def main(): tornado.options.parse_command_line() app = Application() app.listen(options.port) tornado.ioloop.PeriodicCallback(game_tick, 100, io_loop = tornado.ioloop.IOLoop.current()).start() tornado.ioloop.IOLoop.current().start() if __name__ == "__main__": main() 

We store all players in the field of the GameSocketHandler class, and I keep his copies of the data about them and are connected to the web socket. Every 100 milliseconds, the game_tick function is called, which moves snakes and detects collisions.

The whole source can be taken here.

Source: https://habr.com/ru/post/346696/


All Articles