Exemple #1
0
    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)),
        )
Exemple #2
0
    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),
            )
Exemple #3
0
    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)
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
    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)
Exemple #7
0
    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),
            )