async def round_socket_handler(request): users = request.app["users"] sockets = request.app["lobbysockets"] seeks = request.app["seeks"] db = request.app["db"] ws = MyWebSocketResponse(heartbeat=3.0, receive_timeout=15.0) ws_ready = ws.can_prepare(request) if not ws_ready.ok: return web.HTTPFound("/") await ws.prepare(request) session = await aiohttp_session.get_session(request) session_user = session.get("user_name") user = users[ session_user] if session_user is not None and session_user in users else None game = None opp_ws = None log.debug("-------------------------- NEW round WEBSOCKET by %s", user) try: async for msg in ws: if msg.type == aiohttp.WSMsgType.TEXT: if msg.data == "close": log.debug("Got 'close' msg.") break else: data = json.loads(msg.data) # log.debug("Websocket (%s) message: %s" % (id(ws), msg)) if game is None: game = await load_game(request.app, data["gameId"]) if game is None: continue if data["type"] == "move": # log.info("Got USER move %s %s %s" % (user.username, data["gameId"], data["move"])) async with game.move_lock: try: await play_move(request.app, user, game, data["move"], data["clocks"], data["ply"]) except Exception: log.exception( "ERROR: Exception in play_move() in %s by %s ", data["gameId"], session_user) elif data["type"] == "berserk": game.berserk(data["color"]) response = {"type": "berserk", "color": data["color"]} await round_broadcast(game, users, response, full=True) elif data["type"] == "analysis_move": await analysis_move(request.app, user, game, data["move"], data["fen"], data["ply"]) elif data["type"] == "ready": opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username opp_player = users[opp_name] if opp_player.bot: # Janggi game start have to wait for human player setup! if game.variant != "janggi" or not (game.bsetup or game.wsetup): await opp_player.event_queue.put( game.game_start) response = { "type": "gameStart", "gameId": data["gameId"] } await ws.send_json(response) else: response = { "type": "gameStart", "gameId": data["gameId"] } await ws.send_json(response) response = { "type": "user_present", "username": user.username } await round_broadcast(game, users, game.spectator_list, full=True) elif data["type"] == "board": if game.variant == "janggi": if (game.bsetup or game.wsetup) and game.status <= STARTED: if game.bsetup: await ws.send_json({ "type": "setup", "color": "black", "fen": game.board.initial_fen }) elif game.wsetup: await ws.send_json({ "type": "setup", "color": "white", "fen": game.board.initial_fen }) else: board_response = game.get_board(full=True) await ws.send_json(board_response) else: board_response = game.get_board(full=True) await ws.send_json(board_response) elif data["type"] == "setup": # Janggi game starts with a prelude phase to set up horses and elephants # First the second player (Red) choses his setup! Then the first player (Blue) game.board.initial_fen = data["fen"] game.initial_fen = game.board.initial_fen game.board.fen = game.board.initial_fen # print("--- Got FEN from %s %s" % (data["color"], data["fen"])) opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username opp_player = users[opp_name] game.steps[0]["fen"] = data["fen"] game.set_dests() if data["color"] == "black": game.bsetup = False response = { "type": "setup", "color": "white", "fen": data["fen"] } await ws.send_json(response) if opp_player.bot: game.board.janggi_setup("w") game.steps[0]["fen"] = game.board.initial_fen game.set_dests() else: opp_ws = users[opp_name].game_sockets[ data["gameId"]] await opp_ws.send_json(response) else: game.wsetup = False response = game.get_board(full=True) # log.info("User %s asked board. Server sent: %s" % (user.username, board_response["fen"])) await ws.send_json(response) if not opp_player.bot: opp_ws = users[opp_name].game_sockets[ data["gameId"]] await opp_ws.send_json(response) if opp_player.bot: await opp_player.event_queue.put(game.game_start) # restart expiration time after setup phase game.stopwatch.restart( game.stopwatch.time_for_first_move) elif data["type"] == "analysis": # If there is any fishnet client, use it. if len(request.app["workers"]) > 0: work_id = "".join( random.choice(string.ascii_letters + string.digits) for x in range(6)) work = { "work": { "type": "analysis", "id": work_id, }, # or: # "work": { # "type": "move", # "id": "work_id", # "level": 5 // 1 to 8 # }, "username": data["username"], "game_id": data["gameId"], # optional "position": game.board. initial_fen, # start position (X-FEN) "variant": game.variant, "chess960": game.chess960, "moves": " ".join(game.board.move_stack ), # moves of the game (UCI) "nodes": 500000, # optional limit # "skipPositions": [1, 4, 5] # 0 is the first position } request.app["works"][work_id] = work request.app["fishnet"].put_nowait( (ANALYSIS, work_id)) else: engine = users.get("Fairy-Stockfish") if (engine is not None) and engine.online: engine.game_queues[ data["gameId"]] = asyncio.Queue() await engine.event_queue.put( game.analysis_start(data["username"])) response = chat_response("roundchat", "", "Analysis request sent...", room="spectator") await ws.send_json(response) elif data["type"] == "rematch": rematch_id = None opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username opp_player = users[opp_name] handicap = data["handicap"] fen = "" if game.variant == "janggi" else game.initial_fen if opp_player.bot: if opp_player.username == "Random-Mover": engine = users.get("Random-Mover") else: engine = users.get("Fairy-Stockfish") if engine is None or not engine.online: # TODO: message that engine is offline, but capture BOT will play instead engine = users.get("Random-Mover") color = "w" if game.wplayer.username == opp_name else "b" if handicap: color = "w" if color == "b" else "b" seek = Seek(user, game.variant, fen=fen, color=color, base=game.base, inc=game.inc, byoyomi_period=game.byoyomi_period, level=game.level, rated=game.rated, player1=user, chess960=game.chess960) seeks[seek.id] = seek response = await join_seek(request.app, engine, seek.id) await ws.send_json(response) await engine.event_queue.put( challenge(seek, response)) gameId = response["gameId"] rematch_id = gameId engine.game_queues[gameId] = asyncio.Queue() else: try: opp_ws = users[opp_name].game_sockets[ data["gameId"]] except KeyError: # opp disconnected continue if opp_name in game.rematch_offers: color = "w" if game.wplayer.username == opp_name else "b" if handicap: color = "w" if color == "b" else "b" seek = Seek(user, game.variant, fen=fen, color=color, base=game.base, inc=game.inc, byoyomi_period=game.byoyomi_period, level=game.level, rated=game.rated, player1=user, chess960=game.chess960) seeks[seek.id] = seek response = await join_seek( request.app, opp_player, seek.id) rematch_id = response["gameId"] await ws.send_json(response) await opp_ws.send_json(response) else: game.rematch_offers.add(user.username) response = { "type": "rematch_offer", "username": user.username, "message": "Rematch offer sent", "room": "player", "user": "" } game.messages.append(response) await ws.send_json(response) await opp_ws.send_json(response) if rematch_id: await round_broadcast(game, users, { "type": "view_rematch", "gameId": rematch_id }) elif data["type"] == "reject_rematch": opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username if opp_name in game.rematch_offers: await round_broadcast( game, users, { "type": "rematch_rejected", "message": "Rematch offer rejected" }, full=True) elif data["type"] == "draw": color = WHITE if user.username == game.wplayer.username else BLACK opp_name = game.wplayer.username if color == BLACK else game.bplayer.username opp_player = users[opp_name] response = await draw(game, user.username, agreement=opp_name in game.draw_offers) await ws.send_json(response) if opp_player.bot: if game.status > STARTED and data[ "gameId"] in opp_player.game_queues: await opp_player.game_queues[ data["gameId"]].put(game.game_end) else: try: opp_ws = users[opp_name].game_sockets[ data["gameId"]] await opp_ws.send_json(response) except KeyError: # opp disconnected pass if opp_name not in game.draw_offers: game.draw_offers.add(user.username) await round_broadcast(game, users, response) elif data["type"] == "reject_draw": color = WHITE if user.username == game.wplayer.username else BLACK opp_name = game.wplayer.username if color == BLACK else game.bplayer.username response = reject_draw(game, opp_name) if response is not None: await round_broadcast(game, users, response, full=True) elif data["type"] == "logout": await ws.close() elif data["type"] == "byoyomi": game.byo_correction += game.inc * 1000 game.byoyomi_periods[data["color"]] = data["period"] # print("BYOYOMI:", data) elif data["type"] in ("abort", "resign", "abandone", "flag"): if data["type"] == "abort" and ( game is not None) and game.board.ply > 2: continue if game.status > STARTED: # game was already finished! # see https://github.com/gbtami/pychess-variants/issues/675 continue async with game.move_lock: response = await game.game_ended( user, data["type"]) await ws.send_json(response) opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username opp_player = users[opp_name] if opp_player.bot: if data["gameId"] in opp_player.game_queues: await opp_player.game_queues[ data["gameId"]].put(game.game_end) else: if data["gameId"] in users[opp_name].game_sockets: opp_ws = users[opp_name].game_sockets[ data["gameId"]] await opp_ws.send_json(response) await round_broadcast(game, users, response) elif data["type"] == "embed_user_connected": response = {"type": "embed_user_connected"} await ws.send_json(response) elif data["type"] == "game_user_connected": if session_user is not None: if data["username"] and data[ "username"] != session_user: log.info( "+++ Existing game_user %s socket connected as %s.", session_user, data["username"]) session_user = data["username"] if session_user in users: user = users[session_user] else: user = User( request.app, username=data["username"], anon=data["username"].startswith( "Anon-")) users[user.username] = user # Update logged in users as spactators if user.username != game.wplayer.username and user.username != game.bplayer.username and game is not None: game.spectators.add(user) else: if session_user in users: user = users[session_user] else: user = User( request.app, username=data["username"], anon=data["username"].startswith( "Anon-")) users[user.username] = user else: log.info( "+++ Existing game_user %s socket reconnected.", data["username"]) session_user = data["username"] if session_user in users: user = users[session_user] else: user = User( request.app, username=data["username"], anon=data["username"].startswith("Anon-")) users[user.username] = user # update websocket if data["gameId"] in user.game_sockets: await user.game_sockets[data["gameId"]].close() user.game_sockets[data["gameId"]] = ws user.update_online() # remove user seeks if len(user.lobby_sockets) == 0 or ( game.status <= STARTED and (user.username == game.wplayer.username or user.username == game.bplayer.username)): await user.clear_seeks(sockets, seeks) if user.username != game.wplayer.username and user.username != game.bplayer.username: game.spectators.add(user) await round_broadcast(game, users, game.spectator_list, full=True) response = { "type": "game_user_connected", "username": user.username, "gameId": data["gameId"], "ply": game.board.ply, "firstmovetime": game.stopwatch.secs } await ws.send_json(response) response = { "type": "crosstable", "ct": game.crosstable } await ws.send_json(response) response = { "type": "fullchat", "lines": list(game.messages) } await ws.send_json(response) response = { "type": "user_present", "username": user.username } await round_broadcast(game, users, response, full=True) # not connected to lobby socket but connected to game socket if len(user.game_sockets ) == 1 and user.username not in sockets: response = { "type": "u_cnt", "cnt": online_count(users) } await lobby_broadcast(sockets, response) elif data["type"] == "is_user_present": player_name = data["username"] player = users.get(player_name) await asyncio.sleep(1) if player is not None and data["gameId"] in ( player.game_queues if player.bot else player.game_sockets): response = { "type": "user_present", "username": player_name } else: response = { "type": "user_disconnected", "username": player_name } await ws.send_json(response) elif data["type"] == "moretime": # TODO: stop and update game stopwatch time with updated secs opp_color = WHITE if user.username == game.bplayer.username else BLACK if opp_color == game.stopwatch.color: opp_time = game.stopwatch.stop() game.stopwatch.restart(opp_time + MORE_TIME) opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username opp_player = users[opp_name] if not opp_player.bot: opp_ws = users[opp_name].game_sockets[ data["gameId"]] response = { "type": "moretime", "username": opp_name } await opp_ws.send_json(response) await round_broadcast(game, users, response) elif data["type"] == "roundchat": gameId = data["gameId"] # Users running a fishnet worker can ask server side analysis with chat message: !analysis if data["message"] == "!analysis" and user.username in request.app[ "fishnet_versions"]: for step in game.steps: if "analysis" in step: del step["analysis"] await ws.send_json({"type": "request_analysis"}) continue response = chat_response("roundchat", user.username, data["message"], room=data["room"]) game.messages.append(response) for name in (game.wplayer.username, game.bplayer.username): player = users[name] if player.bot: if gameId in player.game_queues: await player.game_queues[gameId].put( '{"type": "chatLine", "username": "******", "room": "spectator", "text": "%s"}\n' % (user.username, data["message"])) else: if gameId in player.game_sockets: player_ws = player.game_sockets[gameId] await player_ws.send_json(response) await round_broadcast(game, users, response) elif data["type"] == "leave": gameId = data["gameId"] response = chat_response("roundchat", "", "%s left the game" % user.username, room="player") game.messages.append(response) opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username opp_player = users[opp_name] if not opp_player.bot and gameId in opp_player.game_sockets: opp_player_ws = opp_player.game_sockets[gameId] await opp_player_ws.send_json(response) response = { "type": "user_disconnected", "username": user.username } await opp_player_ws.send_json(response) await round_broadcast(game, users, response) elif data["type"] == "updateTV": if "profileId" in data and data["profileId"] != "": gameId = await tv_game_user( db, users, data["profileId"]) else: gameId = await tv_game(db, request.app) if gameId != data["gameId"] and gameId is not None: response = {"type": "updateTV", "gameId": gameId} await ws.send_json(response) elif data["type"] == "count": cur_player = game.bplayer if game.board.color == BLACK else game.wplayer opp_name = game.wplayer.username if user.username == game.bplayer.username else game.bplayer.username opp_player = users[opp_name] opp_ws = users[opp_name].game_sockets[data["gameId"]] if user.username == cur_player.username: if data["mode"] == "start": game.start_manual_count() response = { "type": "count", "message": "Board's honor counting started", "room": "player", "user": "" } await ws.send_json(response) await opp_ws.send_json(response) await round_broadcast(game, users, response) elif data["mode"] == "stop": game.stop_manual_count() response = { "type": "count", "message": "Board's honor counting stopped", "room": "player", "user": "" } await ws.send_json(response) await opp_ws.send_json(response) await round_broadcast(game, users, response) else: response = { "type": "count", "message": "You can only start/stop board's honor counting on your own turn!", "room": "player", "user": "" } await ws.send_json(response) elif data["type"] == "delete": await db.game.delete_one({"_id": data["gameId"]}) response = {"type": "deleted"} await ws.send_json(response) elif msg.type == aiohttp.WSMsgType.CLOSED: log.debug( "--- Round websocket %s msg.type == aiohttp.WSMsgType.CLOSED", id(ws)) break elif msg.type == aiohttp.WSMsgType.ERROR: log.error( "--- Round ws %s msg.type == aiohttp.WSMsgType.ERROR", id(ws)) break else: log.debug("--- Round ws other msg.type %s %s", msg.type, msg) except OSError: # disconnected? log.exception("ERROR: OSError in round_socket_handler() owned by %s ", session_user) except Exception: log.exception( "ERROR: Exception in round_socket_handler() owned by %s ", session_user) finally: log.debug("--- wsr.py fianlly: await ws.close() %s", session_user) await ws.close() if game is not None and user is not None and not user.bot: if game.id in user.game_sockets: del user.game_sockets[game.id] user.update_online() if user.username != game.wplayer.username and user.username != game.bplayer.username: game.spectators.discard(user) await round_broadcast(game, users, game.spectator_list, full=True) # not connected to lobby socket and not connected to game socket if len(user.game_sockets) == 0 and user.username not in sockets: response = {"type": "u_cnt", "cnt": online_count(users)} await lobby_broadcast(sockets, response) if game is not None and user is not None: response = {"type": "user_disconnected", "username": user.username} await round_broadcast(game, users, response, full=True) return ws
async def play_move(self, move, clocks=None, ply=None): self.stopwatch.stop() self.byo_correction = 0 if self.status > STARTED: return if self.status == CREATED: self.status = STARTED self.app["g_cnt"][0] += 1 response = {"type": "g_cnt", "cnt": self.app["g_cnt"][0]} await lobby_broadcast(self.app["lobbysockets"], response) cur_player = self.bplayer if self.board.color == BLACK else self.wplayer opp_player = self.wplayer if self.board.color == BLACK else self.bplayer # Move cancels draw offer response = reject_draw(self, opp_player.username) if response is not None: await round_broadcast(self, self.app["users"], response, full=True) cur_time = monotonic() # BOT players doesn't send times used for moves if self.bot_game: movetime = int(round((cur_time - self.last_server_clock) * 1000)) # print(self.board.ply, move, movetime) if clocks is None: clocks = { "white": self.ply_clocks[-1]["white"], "black": self.ply_clocks[-1]["black"], "movetime": movetime } if cur_player.bot and self.board.ply >= 2: cur_color = "black" if self.board.color == BLACK else "white" if self.byoyomi: if self.overtime: clocks[cur_color] = self.inc * 1000 else: clocks[cur_color] = max( 0, self.clocks[cur_color] - movetime) else: clocks[cur_color] = max( 0, self.clocks[cur_color] - movetime + (self.inc * 1000)) if clocks[cur_color] == 0: if self.byoyomi and self.byoyomi_periods[cur_color] > 0: self.overtime = True clocks[cur_color] = self.inc * 1000 self.byoyomi_periods[cur_color] -= 1 else: w, b = self.board.insufficient_material() if (w and b) or (cur_color == "black" and w) or (cur_color == "white" and b): result = "1/2-1/2" else: result = "1-0" if self.board.color == BLACK else "0-1" self.update_status(FLAG, result) print(self.result, "flag") await self.save_game() else: if (ply is not None ) and ply <= 2 and self.tournamentId is not None: # Just in case for move and berserk messages race if self.wberserk: clocks["white"] = self.berserk_time if self.bberserk: clocks["black"] = self.berserk_time self.last_server_clock = cur_time if self.status <= STARTED: try: san = self.board.get_san(move) self.lastmove = move self.board.push(move) self.ply_clocks.append(clocks) self.set_dests() self.update_status() # Stop manual counting when the king is bared if self.board.count_started != 0: board_state = self.board.fen.split()[0] white_pieces = sum(1 for c in board_state if c.isupper()) black_pieces = sum(1 for c in board_state if c.islower()) if white_pieces <= 1 or black_pieces <= 1: if self.board.count_started > 0: self.stop_manual_count() self.board.count_started = 0 if self.status > STARTED: await self.save_game() self.steps.append({ "fen": self.board.fen, "move": move, "san": san, "turnColor": "black" if self.board.color == BLACK else "white", "check": self.check }) self.stopwatch.restart() except Exception: log.exception("ERROR: Exception in game %s play_move() %s", self.id, move) result = "1-0" if self.board.color == BLACK else "0-1" self.update_status(INVALIDMOVE, result) await self.save_game() # TODO: this causes random game abort if False: # not self.bot_game: # print("--------------ply-", ply) # print(self.board.color, clocks, self.ply_clocks) opp_color = self.steps[-1]["turnColor"] if clocks[opp_color] < self.ply_clocks[ ply - 1][opp_color] and self.status <= STARTED: self.update_status(ABORTED) await self.save_game(with_clocks=True)