async def on_rejoin_game(*, log: Logger, ctx: ServerCtx, conn_id: str, msg_id: int, payload: dict): game_id = payload['game_id'] player = payload['player'] player_nonce = payload['player_nonce'] game_id_bytes = uuid.UUID(game_id).bytes session_state, game_id = await ctx.redis_store.read_session(conn_id) if session_state != SessionState.NEED_JOIN: log.msg('unexpected session state', session_state=session_state.value) await ctx.send_fatal( conn_id, wire.ServerMsgType.ILLEGAL_MSG, error='session is not awaiting join', err_msg_id=msg_id, ) return meta = await ctx.redis_store.read_game_meta(game_id_bytes) if not await validate_rejoin_request( ctx, log, player=player, declared_nonce=player_nonce, meta=meta): return if meta.state == GameState.RUNNING: _, game = await ctx.redis_store.read_game(game_id_bytes) try: await ctx.send( conn_id, wire.ServerMsgType.MOVE_PENDING, player=game.player, last_move=meta.get_last_move_json(), ) except WebsocketConnectionLost: log.msg('connection lost') meta = meta.with_conn_id(player, None) await asyncio.gather( ctx.redis_store.delete_session(conn_id), ctx.redis_store.update_game(game_id_bytes, meta=meta), ) return await asyncio.gather( ctx.redis_store.put_session(conn_id, state=SessionState.RUNNING, game_id=game_id), ctx.redis_store.update_game(game_id_bytes, meta=meta.with_conn_id( player, conn_id)), )
async def on_new_game(*, log: Logger, ctx: ServerCtx, conn_id: str, msg_id: int, payload: dict): player: Player = payload['player'] squares: int = payload['squares_per_row'] target_len: int = payload['run_to_win'] # TODO: handle validation of relative values of params game = GameModel(squares=squares, target_len=target_len) meta = GameMeta( state=GameState.JOIN_PENDING, player_nonces=(uuid.uuid4(), uuid.uuid4()), conn_ids=(conn_id, None) if player == 1 else (None, conn_id), last_move=None, ) session_state, _ = await ctx.redis_store.read_session(conn_id) if session_state != SessionState.NEED_JOIN: log.msg('unexpected session state', session_state=session_state.value) await ctx.send_fatal( conn_id, wire.ServerMsgType.ILLEGAL_MSG, error='session is not awaiting join', err_msg_id=msg_id, ) return game_key = await ctx.redis_store.put_game(game=game, meta=meta) await ctx.redis_store.put_session(conn_id, SessionState.NEED_JOIN_ACK, game_key.bytes) try: await ctx.send( conn_id, wire.ServerMsgType.GAME_JOINED, player=player, squares_per_row=squares, run_to_win=target_len, game_id=str(game_key), # TODO player_nonce=str(meta.get_player_nonce(player)), ) except WebsocketConnectionLost: log.msg('connection lost') await asyncio.gather( ctx.redis_store.delete_game(game_key.bytes), ctx.redis_store.delete_session(conn_id), )
async def on_new_move(*, log: Logger, ctx: ServerCtx, conn_id: str, msg_id: int, payload: dict) -> None: session_state, game_id = await ctx.redis_store.read_session(conn_id) if session_state != SessionState.RUNNING: # TODO: disconnect await ctx.send_fatal(conn_id, wire.ServerMsgType.ILLEGAL_MSG, error='', err_msg_id=msg_id) return if game_id is None: raise RuntimeError( 'No game associated with session in running state') meta, (_, game) = await asyncio.gather( ctx.redis_store.read_game_meta(game_id), ctx.redis_store.read_game(game_id)) player = meta.get_player_for_conn_id(conn_id) if player is None: log.msg('Connection cleared from game state') await ctx.ws_manager.close(conn_id) return try: coords = payload['x'], payload['y'] move = Move(player=player, coords=coords) game.apply_move(move) except IllegalMoveException as exc: await ctx.send_fatal(conn_id, wire.ServerMsgType.ILLEGAL_MOVE, error=str(exc)) # TODO: update game state return meta = meta.with_last_move(move) if game.status() != GameStatus.Ongoing: meta = meta.with_state(GameState.COMPLETED) await ctx.redis_store.update_game(game_id, meta=meta, game=game) await broadcast_game_state(ctx, meta=meta, game=game)
async def validate_rejoin_request(ctx: ServerCtx, log: Logger, player: Player, declared_nonce: str, meta: GameMeta) -> bool: if meta.get_player_nonce(player) != declared_nonce: log.msg('incorrect nonce') return False prior_conn_id = meta.get_conn_id(player) if prior_conn_id is not None: # Clean up any connection which failed to ack its join before this request came in # FIXME: I don't really like this racing approach log.msg('tearing down prior connection', prior_conn_id=prior_conn_id) await asyncio.gather( ctx.redis_store.delete_session(prior_conn_id), ctx.ws_manager.close(prior_conn_id), ) return True
async def validate_join_request(ctx: ServerCtx, log: Logger, player: Player, meta: GameMeta) -> bool: if meta.state != GameState.JOIN_PENDING: log.msg('game state is not pending join', game_state=meta.state.value) return False prior_conn_id = meta.get_conn_id(player) if prior_conn_id is None: return True prior_session_state, _ = await ctx.redis_store.read_session(prior_conn_id) if prior_session_state == SessionState.NEED_JOIN_ACK: # Clean up any connection which failed to ack its join before this request came in # FIXME: I don't really like this racing approach log.msg('tearing down prior connection', prior_conn_id=prior_conn_id) await asyncio.gather( ctx.redis_store.delete_session(prior_conn_id), ctx.ws_manager.close(prior_conn_id), ) return True log.msg( 'prior connection already joined', prior_conn_id=prior_conn_id, prior_session_state=prior_session_state.value, ) return False
async def on_ack_game_joined(*, log: Logger, ctx: ServerCtx, conn_id: str, msg_id: int, payload: dict): session_state, game_id = await ctx.redis_store.read_session(conn_id) if session_state != SessionState.NEED_JOIN_ACK: log.msg('unexpected session state', session_state=session_state.value) await ctx.send_fatal( conn_id, wire.ServerMsgType.ILLEGAL_MSG, error='unexpected ack_game_joined', err_msg_id=msg_id, ) await ctx.redis_store.delete_session(conn_id) return assert game_id is not None meta = await ctx.redis_store.read_game_meta(game_id) player = meta.get_player_for_conn_id(conn_id) if player is None: log.msg('connection cleared from game') # FIXME: shouldn't happen? await ctx.send_fatal( conn_id, wire.ServerMsgType.ILLEGAL_MSG, error='game cleared', err_msg_id=msg_id, ) await ctx.redis_store.delete_session(conn_id) return await ctx.redis_store.put_session(conn_id, state=SessionState.RUNNING, game_id=game_id) other_conn = meta.get_conn_id(get_opponent(player)) if other_conn is None: all_joined = False else: other_state, _ = await ctx.redis_store.read_session(other_conn) all_joined = other_state == SessionState.RUNNING if all_joined: log.msg('game fully joined') meta = meta.with_state(GameState.RUNNING) await ctx.redis_store.update_game(game_id, meta=meta) _, game = await ctx.redis_store.read_game(game_id) await broadcast_game_state(ctx, meta, game)
async def on_join_game(*, log: Logger, ctx: ServerCtx, conn_id: str, msg_id: int, payload: dict): game_id: str = payload['game_id'] player: Player = payload['player'] log = log.bind(game_id=game_id, player=player) game_id_bytes = uuid.UUID(game_id).bytes session_state, _ = await ctx.redis_store.read_session(conn_id) if session_state != SessionState.NEED_JOIN: log.msg('unexpected session state', session_state=session_state.value) await ctx.send_fatal( conn_id, wire.ServerMsgType.ILLEGAL_MSG, error='session is not awaiting join', err_msg_id=msg_id, ) return meta = await ctx.redis_store.read_game_meta(game_id_bytes) if not await validate_join_request( ctx, log=log, player=player, meta=meta): log.msg('player already claimed') await ctx.send_fatal( conn_id, wire.ServerMsgType.ILLEGAL_MSG, error='player has already been claimed', err_msg_id=msg_id, ) return meta = meta.with_conn_id(player, conn_id) # TODO: validate state transitions? await asyncio.gather( ctx.redis_store.update_game(game_id_bytes, meta=meta), ctx.redis_store.put_session(conn_id, SessionState.NEED_JOIN_ACK, game_id_bytes), ) # XXX: org? _, game = await ctx.redis_store.read_game(game_id_bytes) try: await ctx.send( conn_id, wire.ServerMsgType.GAME_JOINED, game_id=game_id, player=player, player_nonce=str(meta.get_player_nonce(player)), squares_per_row=game.squares, run_to_win=game.target_len, ) except WebsocketConnectionLost: log.msg('connection lost') # Roll back game updates... meta = meta.with_state(GameState.JOIN_PENDING).with_conn_id( player, None) await asyncio.gather( ctx.redis_store.delete_session(conn_id), ctx.redis_store.update_game(game_id_bytes, meta=meta), )