├── .gitignore ├── LICENSE ├── README.md ├── datatypes.py ├── game.py ├── index.html ├── player.py ├── requirements.txt ├── server.py ├── settings.py ├── simple ├── game_loop_basic.py ├── game_loop_global.py ├── game_loop_handler.py ├── game_loop_process.py ├── game_loop_thread.py ├── game_loop_wait.py └── index.html └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snakepit 2 | 3 | Online multiplayer snake game written in `python` and `asyncio` 4 | 5 | http://snakepit-game.com 6 | 7 | Main article 8 | 9 | Part 1 10 | https://7webpages.com/blog/writing-online-multiplayer-game-with-python-asyncio-getting-asynchronous/ 11 | 12 | Part 2 13 | https://7webpages.com/blog/writing-online-multiplayer-game-with-python-and-asyncio-writing-game-loop/ 14 | 15 | Part 3 (game explanation) 16 | https://7webpages.com/blog/writing-online-multiplayer-game-with-python-and-asyncio-part-3/ 17 | -------------------------------------------------------------------------------- /datatypes.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Position = namedtuple("Position", "x y") 4 | 5 | Vector = namedtuple("Vector", "xdir ydir") 6 | 7 | Char = namedtuple("Char", "char color") 8 | 9 | Draw = namedtuple("Draw", "x y char color") 10 | -------------------------------------------------------------------------------- /game.py: -------------------------------------------------------------------------------- 1 | from random import randint, choice 2 | import json 3 | 4 | import settings 5 | from player import Player 6 | from datatypes import Char, Draw 7 | 8 | 9 | class Game: 10 | 11 | def __init__(self): 12 | self._last_id = 0 13 | self._colors = [] 14 | self._players = {} 15 | self._top_scores = [] 16 | self._world = [] 17 | self.running = False 18 | self.create_world() 19 | self.read_top_scores() 20 | 21 | def create_world(self): 22 | for y in range(0, settings.FIELD_SIZE_Y): 23 | self._world.append([Char(" ", 0)] * settings.FIELD_SIZE_X) 24 | 25 | def reset_world(self): 26 | for y in range(0, settings.FIELD_SIZE_Y): 27 | for x in range(0, settings.FIELD_SIZE_X): 28 | if self._world[y][x].char != " ": 29 | self._world[y][x] = Char(" ", 0) 30 | self.send_all("reset_world") 31 | 32 | def new_player(self, name, ws): 33 | self._last_id += 1 34 | player_id = self._last_id 35 | self.send_personal(ws, "handshake", name, player_id) 36 | 37 | self.send_personal(ws, "world", self._world) 38 | self.send_personal(ws, *self.top_scores_msg()) 39 | for p in self._players.values(): 40 | if p.alive: 41 | self.send_personal(ws, "p_joined", p._id, p.name, p.color, p.score) 42 | 43 | player = Player(player_id, name, ws) 44 | self._players[player_id] = player 45 | return player 46 | 47 | def join(self, player): 48 | if player.alive: 49 | return 50 | if self.count_alive_players() == settings.MAX_PLAYERS: 51 | self.send_personal(player.ws, "error", "Maximum players reached") 52 | return 53 | # pick a color 54 | if not len(self._colors): 55 | # color 0 is reserved for interface and stones 56 | self._colors = list(range(1, settings.NUM_COLORS + 1)) 57 | color = choice(self._colors) 58 | self._colors.remove(color) 59 | # init snake 60 | player.new_snake(color) 61 | # notify all about new player 62 | self.send_all("p_joined", player._id, player.name, color, 0) 63 | 64 | def game_over(self, player): 65 | player.alive = False 66 | self.send_all("p_gameover", player._id) 67 | self._colors.append(player.color) 68 | self.calc_top_scores(player) 69 | self.send_all(*self.top_scores_msg()) 70 | 71 | render = player.render_game_over() 72 | if not self.count_alive_players(): 73 | render += self.render_text(" >>> GAME OVER <<< ", 74 | randint(1, settings.NUM_COLORS)) 75 | self.store_top_scores() 76 | return render 77 | 78 | 79 | def calc_top_scores(self, player): 80 | if not player.score: 81 | return 82 | ts_dict = dict(self._top_scores) 83 | if player.score <= ts_dict.get(player.name, 0): 84 | return 85 | ts_dict[player.name] = player.score 86 | self._top_scores = sorted(ts_dict.items(), key=lambda x: -x[1]) 87 | self._top_scores = self._top_scores[:settings.MAX_TOP_SCORES] 88 | 89 | def top_scores_msg(self): 90 | top_scores = [(t[0], t[1], randint(1, settings.NUM_COLORS)) 91 | for t in self._top_scores] 92 | return ("top_scores", top_scores) 93 | 94 | def read_top_scores(self): 95 | try: 96 | f = open("top_scores.txt", "r+") 97 | content = f.read() 98 | if content: 99 | self._top_scores = json.loads(content) 100 | else: 101 | self._top_scores = [] 102 | f.close() 103 | except FileNotFoundError: 104 | pass 105 | 106 | def store_top_scores(self): 107 | f = open("top_scores.txt", "w") 108 | f.write(json.dumps(self._top_scores)) 109 | f.close() 110 | 111 | 112 | def player_disconnected(self, player): 113 | player.ws = None 114 | if player.alive: 115 | render = self.game_over(player) 116 | self.apply_render(render) 117 | del self._players[player._id] 118 | del player 119 | 120 | def count_alive_players(self): 121 | return sum([int(p.alive) for p in self._players.values()]) 122 | 123 | def next_frame(self): 124 | messages = [] 125 | render_all = [] 126 | for p_id, p in self._players.items(): 127 | 128 | if not p.alive: 129 | continue 130 | # check if snake already exists 131 | if len(p.snake): 132 | # check next position's content 133 | pos = p.next_position() 134 | # check bounds 135 | if pos.x < 0 or pos.x >= settings.FIELD_SIZE_X or\ 136 | pos.y < 0 or pos.y >= settings.FIELD_SIZE_Y: 137 | 138 | render_all += self.game_over(p) 139 | continue 140 | 141 | char = self._world[pos.y][pos.x].char 142 | grow = 0 143 | if char.isdigit(): 144 | # start growing next turn in case we eaten a digit 145 | grow = int(char) 146 | p.score += grow 147 | messages.append(["p_score", p_id, p.score]) 148 | elif char != " ": 149 | render_all += self.game_over(p) 150 | continue 151 | 152 | render_all += p.render_move() 153 | p.grow += grow 154 | 155 | # spawn digits proportionally to the number of snakes 156 | render_all += self.spawn_digit() 157 | else: 158 | # newborn snake 159 | render_all += p.render_new_snake() 160 | # and it's birthday present 161 | render_all += self.spawn_digit(right_now=True) 162 | 163 | render_all += self.spawn_stone() 164 | # send all render messages 165 | self.apply_render(render_all) 166 | # send additional messages 167 | if messages: 168 | self.send_all_multi(messages) 169 | 170 | def _get_spawn_place(self): 171 | x = None 172 | y = None 173 | for i in range(0,2): 174 | x = randint(0, settings.FIELD_SIZE_X - 1) 175 | y = randint(0, settings.FIELD_SIZE_Y - 1) 176 | if self._world[y][x].char == " ": 177 | break 178 | return x, y 179 | 180 | def spawn_digit(self, right_now=False): 181 | render = [] 182 | if right_now or\ 183 | randint(1, 100) <= settings.DIGIT_SPAWN_RATE: 184 | x, y = self._get_spawn_place() 185 | if x and y: 186 | char = str(randint(1,9)) 187 | color = randint(1, settings.NUM_COLORS) 188 | render += [Draw(x, y, char, color)] 189 | return render 190 | 191 | def spawn_stone(self, right_now=False): 192 | render = [] 193 | if right_now or\ 194 | randint(1, 100) <= settings.STONE_SPAWN_RATE: 195 | x, y = self._get_spawn_place() 196 | if x and y: 197 | render += [Draw(x, y, '#', 0)] 198 | return render 199 | 200 | def apply_render(self, render): 201 | messages = [] 202 | for draw in render: 203 | # apply to local 204 | self._world[draw.y][draw.x] = Char(draw.char, draw.color) 205 | # send messages 206 | messages.append(["render"] + list(draw)) 207 | self.send_all_multi(messages) 208 | 209 | def render_text(self, text, color): 210 | # render in the center of play field 211 | posy = int(settings.FIELD_SIZE_Y / 2) 212 | posx = int(settings.FIELD_SIZE_X / 2 - len(text)/2) 213 | render = [] 214 | for i in range(0, len(text)): 215 | render.append(Draw(posx + i, posy, text[i], color)) 216 | return render 217 | 218 | def send_personal(self, ws, *args): 219 | msg = json.dumps([args]) 220 | ws.send_str(msg) 221 | 222 | def send_all(self, *args): 223 | self.send_all_multi([args]) 224 | 225 | def send_all_multi(self, commands): 226 | msg = json.dumps(commands) 227 | for player in self._players.values(): 228 | if player.ws: 229 | player.ws.send_str(msg) 230 | 231 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Snakepit 5 | 6 | 7 | 172 | 173 | 174 | 175 |
 
176 | 177 |
Snakepit
178 |
ver 0.12
179 | 180 |
181 |
182 |
183 | Name 184 | 185 | 186 |
187 | 188 |
189 |

Snakepit is an open-source multiplayer snake game.

190 |

It is designed as a demo of asyncio library for Python 3.5.

191 |

Check out source code at our github repo.

192 | 193 |

Rules:

194 | 195 |
196 | 197 | 1. Use arrows to control the snake.

198 | 2. Eat digits to grow up.

199 | 3. Do not bite: stones (#), snakes, borders.

200 | 4. Invite friends and have fun together! 201 |
202 |
203 | 204 |

Written by Kyrylo Subbotin. (c) 2016 7webpages.

205 | 206 |
207 |
208 | 209 |
210 |
211 | Welcome to the server, ! Invite some friends and Join. 212 |
213 |
214 | 215 |
216 | 217 |
218 |
Active players
219 |
220 |
221 |
222 |
223 |
Top scores
224 |
225 |
226 |
227 | 228 |
229 | 230 | 231 |
232 | 233 | 234 | -------------------------------------------------------------------------------- /player.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from random import randint 3 | 4 | import settings 5 | from datatypes import Vector, Position, Draw 6 | 7 | class Player: 8 | 9 | HEAD_CHAR = "%" 10 | BODY_CHAR = "@" 11 | TAIL_CHAR = "*" 12 | 13 | DEAD_HEAD_CHAR = "x" 14 | DEAD_BODY_CHAR = "@" 15 | DEAD_TAIL_CHAR = "+" 16 | 17 | UP = Vector(0, -1) 18 | DOWN = Vector(0, 1) 19 | LEFT = Vector(-1, 0) 20 | RIGHT = Vector(1, 0) 21 | 22 | DIRECTIONS = [UP, DOWN, LEFT, RIGHT] 23 | 24 | keymap = {37: LEFT, 25 | 38: UP, 26 | 39: RIGHT, 27 | 40: DOWN 28 | } 29 | 30 | def __init__(self, player_id, name, ws): 31 | self._id = player_id 32 | self.name = name 33 | self.ws = ws 34 | self.alive = False 35 | self.direction = None 36 | 37 | def new_snake(self, color): 38 | self.color = color 39 | self.grow = 0 40 | self.score = 0 41 | 42 | self.alive = True 43 | self.snake = deque() 44 | 45 | def render_new_snake(self): 46 | # try to spawn snake at some distance from world's borders 47 | distance = settings.INIT_LENGHT + 2 48 | x = randint(distance, settings.FIELD_SIZE_X - distance) 49 | y = randint(distance, settings.FIELD_SIZE_Y - distance) 50 | self.direction = self.DIRECTIONS[randint(0, 3)] 51 | # create snake from tail to head 52 | render = [] 53 | pos = Position(x, y) 54 | for i in range(0, settings.INIT_LENGHT): 55 | self.snake.appendleft(pos) 56 | if i == 0: 57 | char = self.TAIL_CHAR 58 | elif i == settings.INIT_LENGHT - 1: 59 | char = self.HEAD_CHAR 60 | else: 61 | char = self.BODY_CHAR 62 | render.append(Draw(pos.x, pos.y, char, self.color)) 63 | pos = self.next_position() 64 | return render 65 | 66 | def next_position(self): 67 | # next position of the snake's head 68 | return Position(self.snake[0].x + self.direction.xdir, 69 | self.snake[0].y + self.direction.ydir) 70 | 71 | def render_move(self): 72 | # moving snake to the next position 73 | render = [] 74 | new_head = self.next_position() 75 | self.snake.appendleft(new_head) 76 | # draw head in the next position 77 | render.append(Draw(new_head.x, new_head.y, 78 | self.HEAD_CHAR, self.color)) 79 | # draw body in the old place of head 80 | render.append(Draw(self.snake[1].x, self.snake[1].y, 81 | self.BODY_CHAR, self.color)) 82 | # if we grow this turn, the tail remains in place 83 | if self.grow > 0: 84 | self.grow -= 1 85 | else: 86 | # otherwise the tail moves 87 | old_tail = self.snake.pop() 88 | render.append(Draw(old_tail.x, old_tail.y, " ", 0)) 89 | new_tail = self.snake[-1] 90 | render.append(Draw(new_tail.x, new_tail.y, 91 | self.TAIL_CHAR, self.color)) 92 | return render 93 | 94 | def render_game_over(self): 95 | render = [] 96 | # dead snake 97 | for i, pos in enumerate(self.snake): 98 | if i == 0: 99 | render.append(Draw(pos.x, pos.y, self.DEAD_HEAD_CHAR, 0)) 100 | elif i == len(self.snake) - 1: 101 | render.append(Draw(pos.x, pos.y, self.DEAD_TAIL_CHAR, 0)) 102 | else: 103 | render.append(Draw(pos.x, pos.y, self.DEAD_BODY_CHAR, 0)) 104 | return render 105 | 106 | def keypress(self, code): 107 | if not self.alive: 108 | return 109 | direction = self.keymap.get(code) 110 | if direction: 111 | # do not move in the opposite direction 112 | if not (self.direction and 113 | direction.xdir == -self.direction.xdir and 114 | direction.ydir == -self.direction.ydir): 115 | self.direction = direction 116 | 117 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import json 4 | from aiohttp import web 5 | 6 | import settings 7 | from game import Game 8 | 9 | async def handle(request): 10 | ALLOWED_FILES = ["index.html", "style.css"] 11 | name = request.match_info.get('name', 'index.html') 12 | if name in ALLOWED_FILES: 13 | try: 14 | with open(name, 'rb') as index: 15 | return web.Response(body=index.read(), content_type='text/html') 16 | except FileNotFoundError: 17 | pass 18 | return web.Response(status=404) 19 | 20 | 21 | async def wshandler(request): 22 | print("Connected") 23 | app = request.app 24 | game = app["game"] 25 | ws = web.WebSocketResponse() 26 | await ws.prepare(request) 27 | 28 | player = None 29 | while True: 30 | msg = await ws.receive() 31 | if msg.tp == web.MsgType.text: 32 | print("Got message %s" % msg.data) 33 | 34 | data = json.loads(msg.data) 35 | if type(data) == int and player: 36 | # Interpret as key code 37 | player.keypress(data) 38 | if type(data) != list: 39 | continue 40 | if not player: 41 | if data[0] == "new_player": 42 | player = game.new_player(data[1], ws) 43 | elif data[0] == "join": 44 | if not game.running: 45 | game.reset_world() 46 | 47 | print("Starting game loop") 48 | asyncio.ensure_future(game_loop(game)) 49 | 50 | game.join(player) 51 | 52 | elif msg.tp == web.MsgType.close: 53 | break 54 | 55 | if player: 56 | game.player_disconnected(player) 57 | 58 | print("Closed connection") 59 | return ws 60 | 61 | async def game_loop(game): 62 | game.running = True 63 | while 1: 64 | game.next_frame() 65 | if not game.count_alive_players(): 66 | print("Stopping game loop") 67 | break 68 | await asyncio.sleep(1./settings.GAME_SPEED) 69 | game.running = False 70 | 71 | 72 | event_loop = asyncio.get_event_loop() 73 | event_loop.set_debug(True) 74 | 75 | app = web.Application() 76 | 77 | app["game"] = Game() 78 | 79 | app.router.add_route('GET', '/connect', wshandler) 80 | app.router.add_route('GET', '/{name}', handle) 81 | app.router.add_route('GET', '/', handle) 82 | 83 | # get port for heroku 84 | port = int(os.environ.get('PORT', 5000)) 85 | web.run_app(app, port=port) 86 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | 2 | GAME_SPEED = 2.3 # fps, the more the faster 3 | 4 | MAX_PLAYERS = 10 5 | MAX_TOP_SCORES = 15 6 | NUM_COLORS = 6 # set according to the number of css classes 7 | 8 | FIELD_SIZE_X = 50 # game field size in characters 9 | FIELD_SIZE_Y = 25 10 | 11 | INIT_LENGHT = 3 12 | 13 | DIGIT_SPAWN_RATE = 6 # probability to spawn per frame in % 14 | STONE_SPAWN_RATE = 6 # note that digit spawn is calculated for every snake 15 | # while stone spawn is calculated once per frame 16 | -------------------------------------------------------------------------------- /simple/game_loop_basic.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | 4 | async def handle(request): 5 | index = open("index.html", 'rb') 6 | content = index.read() 7 | return web.Response(body=content, content_type='text/html') 8 | 9 | 10 | async def wshandler(request): 11 | app = request.app 12 | ws = web.WebSocketResponse() 13 | await ws.prepare(request) 14 | app["sockets"].append(ws) 15 | 16 | while 1: 17 | msg = await ws.receive() 18 | if msg.tp == web.MsgType.text: 19 | print("Got message %s" % msg.data) 20 | ws.send_str("Pressed key code: {}".format(msg.data)) 21 | elif msg.tp == web.MsgType.close or\ 22 | msg.tp == web.MsgType.error: 23 | break 24 | 25 | app["sockets"].remove(ws) 26 | print("Closed connection") 27 | return ws 28 | 29 | async def game_loop(app): 30 | while 1: 31 | for ws in app["sockets"]: 32 | ws.send_str("game loop says: tick") 33 | await asyncio.sleep(2) 34 | 35 | 36 | app = web.Application() 37 | app["sockets"] = [] 38 | 39 | asyncio.ensure_future(game_loop(app)) 40 | 41 | app.router.add_route('GET', '/connect', wshandler) 42 | app.router.add_route('GET', '/', handle) 43 | 44 | web.run_app(app) 45 | -------------------------------------------------------------------------------- /simple/game_loop_global.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | 4 | async def handle(request): 5 | index = open("index.html", 'rb') 6 | content = index.read() 7 | return web.Response(body=content, content_type='text/html') 8 | 9 | 10 | async def wshandler(request): 11 | app = request.app 12 | ws = web.WebSocketResponse() 13 | await ws.prepare(request) 14 | 15 | if app["game_loop"] is None or \ 16 | app["game_loop"].cancelled(): 17 | app["game_loop"] = asyncio.ensure_future(game_loop(app)) 18 | # this is required to propagate exceptions 19 | app["game_loop"].add_done_callback(lambda t: t.result() 20 | if not t.cancelled() else None) 21 | app["sockets"].append(ws) 22 | while 1: 23 | msg = await ws.receive() 24 | if msg.tp == web.MsgType.text: 25 | ws.send_str("Pressed key code: {}".format(msg.data)) 26 | print("Got message %s" % msg.data) 27 | elif msg.tp == web.MsgType.close or\ 28 | msg.tp == web.MsgType.error: 29 | break 30 | 31 | app["sockets"].remove(ws) 32 | 33 | if len(app["sockets"]) == 0: 34 | print("Stopping game loop") 35 | app["game_loop"].cancel() 36 | 37 | print("Closed connection") 38 | return ws 39 | 40 | async def game_loop(app): 41 | print("Game loop started") 42 | while 1: 43 | for ws in app["sockets"]: 44 | ws.send_str("game loop passed") 45 | await asyncio.sleep(2) 46 | 47 | 48 | app = web.Application() 49 | 50 | app["sockets"] = [] 51 | app["game_loop"] = None 52 | 53 | app.router.add_route('GET', '/connect', wshandler) 54 | app.router.add_route('GET', '/', handle) 55 | 56 | web.run_app(app) 57 | -------------------------------------------------------------------------------- /simple/game_loop_handler.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | 4 | async def handle(request): 5 | index = open("index.html", 'rb') 6 | content = index.read() 7 | return web.Response(body=content, content_type='text/html') 8 | 9 | 10 | async def wshandler(request): 11 | app = request.app 12 | ws = web.WebSocketResponse() 13 | await ws.prepare(request) 14 | app["sockets"].append(ws) 15 | 16 | if app["game_is_running"] == False: 17 | asyncio.ensure_future(game_loop(app)) 18 | while 1: 19 | msg = await ws.receive() 20 | if msg.tp == web.MsgType.text: 21 | print("Got message %s" % msg.data) 22 | ws.send_str("Pressed key code: {}".format(msg.data)) 23 | elif msg.tp == web.MsgType.close or\ 24 | msg.tp == web.MsgType.error: 25 | break 26 | 27 | app["sockets"].remove(ws) 28 | print("Closed connection") 29 | 30 | return ws 31 | 32 | async def game_loop(app): 33 | app["game_is_running"] = True 34 | while 1: 35 | for ws in app["sockets"]: 36 | ws.send_str("game loop says: tick") 37 | if len(app["sockets"]) == 0: 38 | break 39 | await asyncio.sleep(2) 40 | app["game_is_running"] = False 41 | 42 | 43 | app = web.Application() 44 | 45 | app["sockets"] = [] 46 | app["game_is_running"] = False 47 | 48 | app.router.add_route('GET', '/connect', wshandler) 49 | app.router.add_route('GET', '/', handle) 50 | 51 | web.run_app(app) 52 | -------------------------------------------------------------------------------- /simple/game_loop_process.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | 4 | from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor 5 | from multiprocessing import Queue, Process 6 | import os 7 | from time import sleep 8 | 9 | 10 | async def handle(request): 11 | index = open("index.html", 'rb') 12 | content = index.read() 13 | return web.Response(body=content, content_type='text/html') 14 | 15 | 16 | tick = asyncio.Condition() 17 | 18 | async def wshandler(request): 19 | ws = web.WebSocketResponse() 20 | await ws.prepare(request) 21 | 22 | recv_task = None 23 | tick_task = None 24 | while 1: 25 | if not recv_task: 26 | recv_task = asyncio.ensure_future(ws.receive()) 27 | if not tick_task: 28 | await tick.acquire() 29 | tick_task = asyncio.ensure_future(tick.wait()) 30 | 31 | done, pending = await asyncio.wait( 32 | [recv_task, 33 | tick_task], 34 | return_when=asyncio.FIRST_COMPLETED) 35 | 36 | if recv_task in done: 37 | msg = recv_task.result() 38 | if msg.tp == web.MsgType.text: 39 | print("Got message %s" % msg.data) 40 | ws.send_str("Pressed key code: {}".format(msg.data)) 41 | elif msg.tp == web.MsgType.close or\ 42 | msg.tp == web.MsgType.error: 43 | break 44 | recv_task = None 45 | 46 | if tick_task in done: 47 | ws.send_str("game loop ticks") 48 | tick.release() 49 | tick_task = None 50 | 51 | return ws 52 | 53 | def game_loop(asyncio_loop): 54 | # coroutine to run in main thread 55 | async def notify(): 56 | await tick.acquire() 57 | tick.notify_all() 58 | tick.release() 59 | 60 | queue = Queue() 61 | 62 | # function to run in a different process 63 | def worker(): 64 | while 1: 65 | print("doing heavy calculation in process {}".format(os.getpid())) 66 | sleep(1) 67 | queue.put("calculation result") 68 | 69 | Process(target=worker).start() 70 | 71 | while 1: 72 | # blocks this thread but not main thread with event loop 73 | result = queue.get() 74 | print("getting {} in process {}".format(result, os.getpid())) 75 | task = asyncio.run_coroutine_threadsafe(notify(), asyncio_loop) 76 | task.result() 77 | 78 | asyncio_loop = asyncio.get_event_loop() 79 | executor = ThreadPoolExecutor(max_workers=1) 80 | asyncio_loop.run_in_executor(executor, game_loop, asyncio_loop) 81 | 82 | app = web.Application() 83 | 84 | app.router.add_route('GET', '/connect', wshandler) 85 | app.router.add_route('GET', '/', handle) 86 | 87 | web.run_app(app) 88 | -------------------------------------------------------------------------------- /simple/game_loop_thread.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | 4 | from concurrent.futures import ThreadPoolExecutor 5 | import threading 6 | from time import sleep 7 | 8 | 9 | async def handle(request): 10 | index = open("index.html", 'rb') 11 | content = index.read() 12 | return web.Response(body=content, content_type='text/html') 13 | 14 | 15 | tick = asyncio.Condition() 16 | 17 | async def wshandler(request): 18 | ws = web.WebSocketResponse() 19 | await ws.prepare(request) 20 | 21 | recv_task = None 22 | tick_task = None 23 | while 1: 24 | if not recv_task: 25 | recv_task = asyncio.ensure_future(ws.receive()) 26 | if not tick_task: 27 | await tick.acquire() 28 | tick_task = asyncio.ensure_future(tick.wait()) 29 | 30 | done, pending = await asyncio.wait( 31 | [recv_task, 32 | tick_task], 33 | return_when=asyncio.FIRST_COMPLETED) 34 | 35 | if recv_task in done: 36 | msg = recv_task.result() 37 | if msg.tp == web.MsgType.text: 38 | print("Got message %s" % msg.data) 39 | ws.send_str("Pressed key code: {}".format(msg.data)) 40 | elif msg.tp == web.MsgType.close or\ 41 | msg.tp == web.MsgType.error: 42 | break 43 | recv_task = None 44 | 45 | if tick_task in done: 46 | ws.send_str("game loop ticks") 47 | tick.release() 48 | tick_task = None 49 | 50 | return ws 51 | 52 | def game_loop(asyncio_loop): 53 | print("Game loop thread id {}".format(threading.get_ident())) 54 | # a coroutine to run in main thread 55 | async def notify(): 56 | print("Notify thread id {}".format(threading.get_ident())) 57 | await tick.acquire() 58 | tick.notify_all() 59 | tick.release() 60 | 61 | while 1: 62 | task = asyncio.run_coroutine_threadsafe(notify(), asyncio_loop) 63 | # blocking the thread 64 | sleep(1) 65 | # make sure the task has finished 66 | task.result() 67 | 68 | print("Main thread id {}".format(threading.get_ident())) 69 | 70 | asyncio_loop = asyncio.get_event_loop() 71 | executor = ThreadPoolExecutor(max_workers=1) 72 | asyncio_loop.run_in_executor(executor, game_loop, asyncio_loop) 73 | 74 | app = web.Application() 75 | 76 | app.router.add_route('GET', '/connect', wshandler) 77 | app.router.add_route('GET', '/', handle) 78 | 79 | web.run_app(app) 80 | -------------------------------------------------------------------------------- /simple/game_loop_wait.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiohttp import web 3 | 4 | async def handle(request): 5 | index = open("index.html", 'rb') 6 | content = index.read() 7 | return web.Response(body=content, content_type='text/html') 8 | 9 | 10 | 11 | tick = asyncio.Condition() 12 | 13 | async def wshandler(request): 14 | ws = web.WebSocketResponse() 15 | await ws.prepare(request) 16 | 17 | recv_task = None 18 | tick_task = None 19 | while 1: 20 | if not recv_task: 21 | recv_task = asyncio.ensure_future(ws.receive()) 22 | if not tick_task: 23 | await tick.acquire() 24 | tick_task = asyncio.ensure_future(tick.wait()) 25 | 26 | done, pending = await asyncio.wait( 27 | [recv_task, 28 | tick_task], 29 | return_when=asyncio.FIRST_COMPLETED) 30 | 31 | if recv_task in done: 32 | msg = recv_task.result() 33 | if msg.tp == web.MsgType.text: 34 | print("Got message %s" % msg.data) 35 | ws.send_str("Pressed key code: {}".format(msg.data)) 36 | elif msg.tp == web.MsgType.close or\ 37 | msg.tp == web.MsgType.error: 38 | break 39 | recv_task = None 40 | 41 | if tick_task in done: 42 | ws.send_str("game loop ticks") 43 | tick.release() 44 | tick_task = None 45 | 46 | return ws 47 | 48 | async def game_loop(): 49 | while 1: 50 | await tick.acquire() 51 | tick.notify_all() 52 | tick.release() 53 | await asyncio.sleep(1) 54 | 55 | asyncio.ensure_future(game_loop()) 56 | 57 | app = web.Application() 58 | 59 | app.router.add_route('GET', '/connect', wshandler) 60 | app.router.add_route('GET', '/', handle) 61 | 62 | web.run_app(app) 63 | -------------------------------------------------------------------------------- /simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Async demo 5 | 6 | 48 | 49 | 50 |
 
51 | 52 |
53 | 54 |
55 | 56 |
57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: DejaVu Sans Mono,Bitstream Vera Sans Mono,monospace !important; 3 | background-color: black; 4 | color: silver; 5 | font-size: 11pt; 6 | text-align:center; 7 | line-height: 1.2em; 8 | } 9 | 10 | a { 11 | color: orange; 12 | text-decoration: none; 13 | } 14 | a:hover { 15 | text-decoration: underline; 16 | } 17 | 18 | /*body { 19 | -ms-transform: scale(1.2,1); /* IE 9 20 | -webkit-transform: scale(1.2,1); /* Safari 21 | transform: scale(1.2,1); /* Standard syntax 22 | }*/ 23 | 24 | input { 25 | border: 1px solid; 26 | padding: 0.5em; 27 | border-color: silver; 28 | background: black; 29 | color: lime; 30 | font-size: 1em; 31 | text-align: left; 32 | } 33 | 34 | button { 35 | border: 0; 36 | text-align: center; 37 | background: silver; 38 | color: green; 39 | font-size: 1em; 40 | padding: 0.5em; 41 | } 42 | 43 | .color0 { color:white } 44 | .color1 { color:red } 45 | .color2 { color:yellow } 46 | .color3 { color:lime } 47 | .color4 { color:aqua } 48 | .color5 { color:#4444FF } 49 | .color6 { color:fuchsia } 50 | 51 | 52 | #userButtons { 53 | margin-top: 1em; 54 | } 55 | 56 | #btnJoin { 57 | color: maroon; 58 | } 59 | #btnDisconnect { 60 | color: black; 61 | } 62 | 63 | #playScreen { 64 | display: none; 65 | } 66 | 67 | #connectForm{ 68 | margin-top: 2em; 69 | margin-bottom: 5em; 70 | } 71 | 72 | #activePlayers, #topScores { 73 | width: 13em; 74 | color: white; 75 | display: inline-block; 76 | vertical-align: top; 77 | } 78 | 79 | #activePlayers .name, #topScores .name { 80 | width: 10em; 81 | text-align: left; 82 | } 83 | #activePlayers .score, #topScores .score { 84 | width: 3em; 85 | text-align: right; 86 | } 87 | 88 | #activePlayers div, #topScores div { 89 | display: inline-block; 90 | } 91 | 92 | #topScores { 93 | text-align: left; 94 | } 95 | 96 | #playField { 97 | margin-top: 1em; 98 | } 99 | 100 | #worldHolder { 101 | display: inline-block; 102 | border: 1px solid; 103 | border-color: silver; 104 | padding: 0.3em; 105 | line-height: 1em; 106 | } 107 | 108 | #world { 109 | padding:0; 110 | border:0; 111 | margin:0; 112 | line-height: 1em; 113 | table-layout: fixed 114 | } 115 | 116 | #world .td { 117 | width: 1em; 118 | height: 1em; 119 | 120 | padding:0; 121 | margin:0; 122 | line-height: 1em; 123 | } 124 | --------------------------------------------------------------------------------