MinimalAPI - artemovsergey/ASP GitHub Wiki

Extension method for minimal api

public static class GameEndpoints
{
    public static WebApplication UseGameEndpoints(
        this WebApplication app
    )
    {
        int TICTACTOE_BOARD_SIZE = int.Parse(Environment.GetEnvironmentVariable("TICTACTOE_BOARD_SIZE")!);

        int TICTACTOE_LINE_TO_WIN = int.Parse(Environment.GetEnvironmentVariable("TICTACTOE_LINE_TO_WIN")!);

        int TICTACTOE_CHANCE =
            int.Parse(Environment.GetEnvironmentVariable("TICTACTOE_LINE_TO_WIN")!); // процент вероятности замены хода
        int TICTACTOE_NUMBER_STEP =
            int.Parse(Environment.GetEnvironmentVariable("TICTACTOE_LINE_TO_WIN")!); // на каком ходу

        app.MapGet("/api/games",
                async (IGameAsyncRepository repo, CancellationToken ct) =>
                {
                    return Results.Ok(await repo.GetGamesAsync(ct));
                })
            .WithTags("TicTacToe.API")
            .WithName("GetAllGames")
            .WithSummary("Список доступных игр")
            .WithDescription("Возвращает список объектов Game")
            .Produces<List<Game>>(StatusCodes.Status200OK)
            .Produces<ErrorResponse>(StatusCodes.Status400BadRequest);

        app.MapGet("/api/games/{Id:guid}",
                async (IGameAsyncRepository repo, Guid Id, CancellationToken ct) =>
                {
                    return Results.Ok(await repo.FindGameByGuidAsync(Id, ct));
                })
            .WithTags("TicTacToe.API")
            .WithName("GetGameById")
            .WithOpenApi(operation =>
            {
                operation.Summary = "Получение игры по Id";
                operation.Description = "Возвращает объект Game";
                return operation;
            })
            .Produces<Game>(StatusCodes.Status200OK)
            .Produces<ErrorResponse>(StatusCodes.Status404NotFound);

        app.MapPost("/api/games/new", async (IGameAsyncRepository repo, GameOption? gameOption, CancellationToken ct) =>
            {
                
                if (gameOption!.size < 3 || gameOption.line_to_win < 1 || gameOption.chance < 1 || gameOption.step < 1 )
                    return Results.Json<ErrorResponse>(new ErrorResponse(
                            statusCode: "400",
                            message: "Размерность должна быть больше от 3, условие победы, вероятность замены и шаг вероятности положительны"
                        ),
                        statusCode: StatusCodes.Status400BadRequest,
                        contentType: "application/json"
                    );

                if (gameOption.line_to_win > gameOption.size)
                    return Results.Json<ErrorResponse>(new ErrorResponse(
                            statusCode: "400",
                            message: "Количество одинаковых элементов должно быть меньше или равно размерности доски!"
                        ),
                        statusCode: StatusCodes.Status400BadRequest,
                        contentType: "application/json"
                    );
                
                
                TICTACTOE_BOARD_SIZE = gameOption!.size;
                TICTACTOE_LINE_TO_WIN = gameOption.line_to_win;
                TICTACTOE_CHANCE = gameOption.chance;
                TICTACTOE_NUMBER_STEP = gameOption.step;

                var game = await repo.CreateGameAsync(TICTACTOE_BOARD_SIZE, ct);

                return Results.CreatedAtRoute("GetGameById", game, value: game);
            })
            .WithTags("TicTacToe.API")
            .WithName("CreateGame")
            .WithSummary("Создание новой игры")
            .WithDescription("Возвращает объект игры Game")
            .Produces<Game>(StatusCodes.Status201Created)
            .Produces<ErrorResponse>(StatusCodes.Status400BadRequest);

        app.MapPost("api/games/{gameId:guid}/move",
                async (HttpResponse r,
                    [FromHeader(Name = "If-Match")] string? ifMatchHeader,
                    IGameAsyncRepository repo,
                    Guid gameId,
                    Move move,
                    ILogger<Program> log,
                    CancellationToken ct) =>
                {
                    var game = await repo.FindGameByGuidAsync(gameId, ct);

                    var currentETag = EtagService.GenerateETag(game);

                    if (ifMatchHeader != null && ifMatchHeader != currentETag)
                    {
                        log.LogError("Etag не совпадает");
                        return Results.Json(
                            new ErrorResponse(
                                statusCode: "412",
                                message: "Обновите состояние игры"
                            ),
                            statusCode: StatusCodes.Status412PreconditionFailed
                        );
                    }

                    if (game.Status != StatusGame.Active)
                    {
                        log.LogError("Игра завершена");
                        return Results.BadRequest(new ErrorResponse(
                                statusCode: "400",
                                message: $"Данная игра уже завершена! Итог: {game.Result}"
                            )
                        );
                    }

                    if (move.x < 0 || move.x > TICTACTOE_BOARD_SIZE - 1 || move.y < 0 ||
                        move.y > TICTACTOE_BOARD_SIZE - 1)
                    {
                        log.LogError("Координаты выходят за пределы поля");
                        return Results.BadRequest(new ErrorResponse(
                                statusCode: "400",
                                message: $"Неверные координаты доски. Разрешено: 0-{TICTACTOE_BOARD_SIZE - 1}"
                            )
                        );
                    }

                    if (game.CurrentMove != move.p)
                    {
                        log.LogError("Ход вне очереди");
                        return Results.BadRequest(new ErrorResponse(
                                statusCode: "400",
                                message: $"Не ваш ход! Сейчас ход: {game.CurrentMove}"
                            )
                        );
                    }

                    if (game.Board[move.x][move.y] != null)
                    {
                        log.LogError($"Ячейка ({move.x},{move.y}) занята");
                        return Results.Conflict(new ErrorResponse(
                                statusCode: "409",
                                message: $"Нельзя осуществить данный ход! Ячейка занята!"
                            )
                        );
                    }

                    game.Board[move.x][move.y] = (move.p.ToString());
                    game.CurrentStep += 1;

                    // Проверяем особое условие на каждый n ход с шансом m % замена выбора

                    bool maybeReplace = false;
                    if (game.CurrentStep > 0 && game.CurrentStep % TICTACTOE_NUMBER_STEP == 0)
                    {
                        var random = new Random();
                        double probability = TICTACTOE_CHANCE / 100.0; // %
                        maybeReplace = random.NextDouble() < probability; // true с вероятностью %

                        if (maybeReplace)
                        {
                            log.LogWarning(
                                $"Сработала вероятность {TICTACTOE_CHANCE}%. Текущий ход: {game.CurrentStep}. Выбор игрока заменен на противоположный!");
                            game.Board[move.x][move.y] = move.p == Player.X ? Player.O.ToString() : Player.X.ToString();
                        }
                    }

                    game.CurrentMove = move.p == Player.X ? Player.O : Player.X;

                    game.Result = GameService.CheckBoardN(game.Board, (move.p).ToString(), TICTACTOE_LINE_TO_WIN);

                    if (game.Result != ResultGame.None)
                    {
                        game.Status = StatusGame.Complete;
                    }

                    await repo.UpdateGameAsync(game, ct);

                    var response = new
                    {
                        Id = gameId,
                        Board = game.Board,
                        Status = game.Status,
                        Result = game.Result,
                        DateTime = DateTime.UtcNow,
                        CurrentStep = game.CurrentStep,
                        CurrentMove = game.CurrentMove,
                        ReplaceMove = maybeReplace
                    };

                    r.Headers.ETag = EtagService.GenerateETag(game);
                    return Results.Ok(response);
                })
            .WithOpenApi(operation =>
            {
                operation.Tags = new List<OpenApiTag>(){ new OpenApiTag(){Name = "TicTacToe.API"}};
                operation.Summary = "Ход игрока X или O";
                operation.Description = "Возвращает состояние игры в виде объекта Game";
                return operation;
            })
            .Produces<Game>(StatusCodes.Status200OK)
            .Produces<ErrorResponse>(StatusCodes.Status404NotFound)
            .Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
            .Produces<ErrorResponse>(StatusCodes.Status409Conflict)
            .Produces<ErrorResponse>(StatusCodes.Status412PreconditionFailed);
        
        return app;
    }
}
⚠️ **GitHub.com Fallback** ⚠️