Какое-то время назад решил написать небольшое приложение, чтобы потренироваться работе с вебсокетами. Из питоновских фреймворков мне показалось удобней изкоробочная поддержка их в tornado. Поскольку игрушка предельно простая, может кому-то показаться полезной как пример. Это многопользовательская «змейка».

Весь «фронт» умещается в один html файл. Вот скрипт из него
Мы получаем координаты всех змеек и яблок из вебсокета и рисуем на canvas.
При нажатии стрелок мы отправляем новое направление движения на сервер(если оно изменилось).
Теперь, серверная часть. Она тоже небольшая.
Всех игроков мы храним в поле класса GameSocketHandler, а я его экземляры хранят о них данные и связаны с вебсокетом. Каждые 100 милисекунд вызывается функция game_tick, которая двигает змеек и обнаруживает коллизии.
Целиком исходник можно забрать тут

Весь «фронт» умещается в один html файл. Вот скрипт из него
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>'; } } }
Мы получаем координаты всех змеек и яблок из вебсокета и рисуем на 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(); }
При нажатии стрелок мы отправляем новое направление движения на сервер(если оно изменилось).
Теперь, серверная часть. Она тоже небольшая.
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()
Всех игроков мы храним в поле класса GameSocketHandler, а я его экземляры хранят о них данные и связаны с вебсокетом. Каждые 100 милисекунд вызывается функция game_tick, которая двигает змеек и обнаруживает коллизии.
Целиком исходник можно забрать тут
