diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..12ef52a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: 'maven' + + - name: Run Tests + run: mvn test diff --git a/README.md b/README.md index 84f054d..010cae0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Ele foi escrito para rodar na AWS sem necessidade de servidor próprio, usando * ## 📦 Pré-requisitos -- **Java JDK** (versão 11 ou superior) +- **Java JDK** (versão 17 ou superior) [Download do JDK](https://www.oracle.com/java/technologies/javase-downloads.html) ou use o OpenJDK. - **Maven CLI** [Download Maven](https://maven.apache.org/download.cgi) @@ -45,7 +45,9 @@ O projeto segue a estrutura **Maven Standard Directory Layout**: │ │ │ └── com │ │ │ └── mauadev │ │ │ └── code -│ │ │ └── Handler.java # Código principal!!! +│ │ │ ├── Logic.java # ⭐ SEU ARQUIVO PRINCIPAL +│ │ │ ├── Handler.java # Ponte Lambda ↔ Logic (não mexa) +│ │ │ └── entities/ # Modelos de dados (não mexa) │ │ └── resources │ │ └── (se necessário, arquivos de configuração) │ └── test @@ -54,6 +56,16 @@ O projeto segue a estrutura **Maven Standard Directory Layout**: --- +## ⭐ Onde implementar sua snake + +| | Arquivo | O que fazer | +|---|---|---| +| ✅ **EDITE** | `src/main/java/com/mauadev/code/Logic.java` | **Este é o seu arquivo principal.** Implemente toda a inteligência da cobra aqui. | +| 🚫 Não altere | `Handler.java` | Roteador HTTP entre o API Gateway e a sua lógica. | +| 🚫 Não altere | `entities/` | Modelos de dados (GameState, Board, Snake…). | +| 🚫 Não altere | `.github/`, Terraform, infraestrutura | Deploy automático — já configurado. | + + ## ⚙️ Dependências (`pom.xml`) O `pom.xml`, nosso arquivo de dependencias, padrão para rodar este projeto: @@ -232,6 +244,7 @@ Assim que o Terraform concluir o deploy, será criada automaticamente uma URL p 5. Salve as configurações 6. Agora você pode testar a cobra nos jogos e desafios! +Ou acesse a [Arena Mauá](https://arena.devmaua.com) para competir com outros alunos! --- @@ -246,7 +259,7 @@ O `Handler.java` implementa a interface do **AWS Lambda RequestHandler**, recebe - Retorna resposta JSON ou erro 404 - **`handleInfo()`** → informações da cobra (cor, cabeça, cauda, autor, versão da API) - **`handleStart()`** → chamado no início do jogo (log inicial) -- **`handleMove()`** → lógica da jogada (neste exemplo, sempre "up") +- **`handleMove()`** → repassa para `Logic.getMove()`, que decide o movimento - **`handleEnd()`** → chamado no final do jogo (log final) - Usa **Gson** para converter objetos Java para JSON - Usa **APIGatewayProxyRequestEvent** e **APIGatewayProxyResponseEvent** para receber/enviar dados @@ -276,6 +289,21 @@ mvn test ## 📌 Observações -- O método `handleMove()` deve conter a inteligência do jogo. +- O método `Logic.getMove()` deve conter a inteligência do jogo. - O uso do **maven-shade-plugin** é obrigatório para empacotar todas as dependências no JAR final. - **EVITE** adicionar mais dependencias, pois o shade plugin empacota **todas as dependencias** em um JAR, tornando-o **talvez muito pesado** para o deploy!!! + +--- + +## 📈 Progressão pedagógica + +Use esta tabela como roteiro para evoluir a inteligência da sua cobra: + +| Nível | Nome | O que implementar | +|---|---|---| +| 0 | **Random** | movimento aleatório (já vem pronto) | +| 1 | **Don't Die** | não voltar, não bater na parede, não bater em si mesmo | +| 2 | **Food** | procurar comida | +| 3 | **Space** | avaliar espaço disponível, evitar becos | +| 4 | **Opponents** | considerar outras cobras, head-to-head | +| 5 | **Advanced** | BFS, flood fill, A*, avaliação de território | diff --git a/src/main/java/com/mauadev/code/Handler.java b/src/main/java/com/mauadev/code/Handler.java index 162595e..7a72ec4 100644 --- a/src/main/java/com/mauadev/code/Handler.java +++ b/src/main/java/com/mauadev/code/Handler.java @@ -1,159 +1,289 @@ package com.mauadev.code; +// Ponte entre o AWS Lambda e a logica da sua cobra. +// +// Voce NAO precisa mexer aqui. Este arquivo recebe o evento da Lambda, +// descobre qual rota do Battlesnake foi chamada e repassa para Logic.java. +// +// Rotas da API (https://docs.battlesnake.com/api): +// GET / -> aparencia da cobra +// POST /start -> a partida comecou +// POST /move -> escolha a jogada deste turno +// POST /end -> a partida acabou +// +// O evento chega como Map porque a Lambda tem dois formatos de payload e o +// template aceita os dois: +// - 1.0, do API Gateway REST: o caminho vem em "path" +// - 2.0, da Function URL e do HTTP API: vem em "rawPath" e em +// "requestContext.http.path" +// Tipar a entrada como APIGatewayProxyRequestEvent aceitaria so o 1.0, e todo +// evento 2.0 chegaria com os campos nulos. + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.google.gson.Gson; -import com.mauadev.code.entities.Board; -import com.mauadev.code.entities.Coordinate; +import com.google.gson.JsonSyntaxException; import com.mauadev.code.entities.GameState; -import com.mauadev.code.entities.Snake; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -public class Handler implements RequestHandler { +public class Handler implements RequestHandler, APIGatewayProxyResponseEvent> { - // Gson é uma biblioteca para converter objetos Java para JSON e vice-versa. - private static final Gson gson = new Gson(); + private static final Gson GSON = new Gson(); - @Override - public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent request, Context context) { - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent(); - String path = request.getPath(); - Object responseBody = null; + /** Rotas do Battlesnake, na forma canonica. */ + private static final String[] ROTAS = {"/start", "/move", "/end"}; + + /** Nomes de stage que o API Gateway pode colocar na frente do caminho. */ + private static final String[] STAGES = {"dev", "homolog", "prod", "staging"}; + + /** Corpo de requisicao que nao da para usar: vira 400, e nao 500. */ + private static class CorpoInvalidoException extends RuntimeException { + CorpoInvalidoException(String mensagem) { + super(mensagem); + } + } + @Override + public APIGatewayProxyResponseEvent handleRequest(Map request, Context context) { try { - // Roteador para os diferentes endpoints da API BattleSnake - switch (path) { - case "/": - // Informações da sua cobra - responseBody = handleInfo(); - break; - case "/start": - // Lógica para o início do jogo - handleStart(request, context); - break; - case "/move": - // Lógica para decidir o próximo movimento - responseBody = handleMove(request, context); - break; - case "/end": - // Lógica para o fim do jogo - handleEnd(request, context); - break; - default: - // Se a rota não for encontrada, retorna um erro 404 - // Precisamos passar \ antes das aspas para não dar erro quando convertemos pra json - return response.withStatusCode(404).withBody("{\"error\": \"Path not found\"}"); + String rota = extrairRota(request); + + if (rota == null) { + return resposta(404, erro("rota nao encontrada: " + caminhoBruto(request))); } - // Configura a resposta de sucesso - response.setStatusCode(200); - response.setHeaders(Collections.singletonMap("Content-Type", "application/json")); - if (responseBody != null) { - // Converte o objeto de resposta para uma string JSON - response.setBody(gson.toJson(responseBody)); + if (!metodoValido(request, rota)) { + return resposta(404, erro("metodo nao permitido para a rota: " + caminhoBruto(request))); } + switch (rota) { + case "/start": { + GameState state = lerEstado(request); + log(context, "JOGO COMECOU (partida " + state.getGame().getId() + ")"); + Logic.start(state); + return resposta(200, "ok"); + } + case "/move": { + GameState state = lerEstado(request); + String jogada = Logic.getMove(state); + log(context, "MOVE " + state.getTurn() + ": " + jogada); + return resposta(200, Map.of("move", jogada)); + } + case "/end": { + GameState state = lerEstado(request); + log(context, "FIM DE JOGO apos " + state.getTurn() + " turnos"); + Logic.end(state); + return resposta(200, "ok"); + } + default: + return resposta(200, Logic.info()); + } + + } catch (CorpoInvalidoException e) { + log(context, "REQUISICAO INVALIDA: " + e.getMessage()); + return resposta(400, erro(e.getMessage())); + } catch (Exception e) { - // Em caso de erro em qualquer parte da lógica - context.getLogger().log("ERROR: " + e.getMessage()); - response.setStatusCode(500); - response.setBody(String.format("{\"error\": \"%s\"}", e.getMessage())); + log(context, "ERROR: " + e); + return resposta(500, erro(e.getMessage())); } - - return response; } + // ------------------------------------------------------------------ + // Roteamento + // ------------------------------------------------------------------ + /** - * Responde ao endpoint / com as informações da sua cobra. 🐍 + * Descobre qual rota do Battlesnake foi chamada, nos dois formatos de evento. + * + *

Compara pelo sufixo do caminho, entao tolera o nome do stage na frente + * ("/dev/move") e barra sobrando no fim ("/move/"). + * + * @return "/", "/start", "/move", "/end", ou {@code null} se a rota for desconhecida */ - private Map handleInfo() { - Map info = new HashMap<>(); - info.put("apiversion", "1"); - info.put("author", "Leo"); - info.put("color", "#888888"); // Ex: Cinza - info.put("head", "default"); - info.put("tail", "default"); - return info; + static String extrairRota(Map request) { + String caminho = caminhoBruto(request); + + if (caminho == null) { + return null; + } + + String limpo = caminho.trim(); + while (limpo.length() > 1 && limpo.endsWith("/")) { + limpo = limpo.substring(0, limpo.length() - 1); + } + + String normalizado = limpo; + for (String stage : STAGES) { + if (normalizado.equals("/" + stage)) { + normalizado = "/"; + break; + } else if (normalizado.startsWith("/" + stage + "/")) { + normalizado = normalizado.substring(stage.length() + 1); + break; + } + } + + if (normalizado.isEmpty() || normalizado.equals("/")) { + return "/"; + } + + for (String rota : ROTAS) { + if (normalizado.equals(rota)) { + return rota; + } + } + + return null; } - /** - * Chamado no início de cada jogo. Não precisa retornar nada. - */ - private void handleStart(APIGatewayProxyRequestEvent request, Context context) { - // Você pode usar o corpo da requisição (request.getBody()) para obter o estado inicial do jogo. - context.getLogger().log("Game Started!"); + static String extrairMetodo(Map request) { + if (request == null) { + return null; + } + + // formato 1.0 (API Gateway REST) + String metodo = texto(request.get("httpMethod")); + if (metodo != null) { + return metodo; + } + + // formato 2.0 (Function URL e HTTP API) + Object contexto = request.get("requestContext"); + if (contexto instanceof Map) { + @SuppressWarnings("unchecked") + Map rc = (Map) contexto; + Object http = rc.get("http"); + if (http instanceof Map) { + @SuppressWarnings("unchecked") + Map httpMap = (Map) http; + metodo = texto(httpMap.get("method")); + if (metodo != null) { + return metodo; + } + } + return texto(rc.get("httpMethod")); + } + + return null; } - /** - * Chamado a cada turno para decidir o movimento. 🕹️ - */ - private Map handleMove(APIGatewayProxyRequestEvent request, Context context) { - // AQUI VAI A LÓGICA DA SUA COBRA! - // O corpo da requisição (request.getBody()) contém o estado atual do tabuleiro. - // Você deve analisá-lo para tomar uma decisão inteligente. - - // Exemplo de um objeto tabuleiro: - // { - // "height": 11, - // "width": 11, - // "food": [ - // {"x": 5, "y": 5}, - // {"x": 9, "y": 0}, - // {"x": 2, "y": 6} - // ], - // "hazards": [ - // {"x": 0, "y": 0}, - // {"x": 0, "y": 1}, - // {"x": 0, "y": 2} - // ], - // "snakes": [ - // {"id": "snake-one", ... }, - // {"id": "snake-two", ... }, - // {"id": "snake-three", ... } - // ] - // } - - // O tabuleiro deve ser acessado usando: - // Type mapType = new TypeToken>() {}.getType(); - // Map body = gson.fromJson(response.getBody(), mapType); - - // Exemplo de lógica muito simples: sempre se mover para cima. - // CUIDADO: Isso fará sua cobra bater na parede rapidamente! - - String requestBody = request.getBody(); - GameState gameState = gson.fromJson(requestBody, GameState.class); - Board board = gameState.getBoard(); - Snake you = gameState.getYou(); - - System.out.println("--- Turno: " + gameState.getTurn() + " ---"); - System.out.println("Minha vida: " + you.getHealth()); - System.out.println("Posição da minha cabeça: (" + you.getHead().getX() + ", " + you.getHead().getY() + ")"); - - if (!board.getFood().isEmpty()) { - Coordinate firstFood = board.getFood().get(0); - System.out.println("Comida mais próxima em: (" + firstFood.getX() + ", " + firstFood.getY() + ")"); - } - - // Lógica do movimento (exemplo simples) - // Agora que você tem o estado do jogo, pode implementar uma lógica mais inteligente aqui. - - Map move = new HashMap<>(); - move.put("move", "up"); - move.put("shout", "Estou indo para cima!"); // Opcional - - return move; + static boolean metodoValido(Map request, String rota) { + String metodo = extrairMetodo(request); + if (metodo == null) { + return true; + } + + if ("/".equals(rota)) { + String httpMethod = texto(request.get("httpMethod")); + if (httpMethod != null) { + return httpMethod.equalsIgnoreCase("GET"); + } + return metodo.equalsIgnoreCase("GET") || metodo.equalsIgnoreCase("POST"); + } + + return metodo.equalsIgnoreCase("POST"); + } + + /** Le o caminho do evento, seja ele no formato 1.0 ou 2.0. */ + @SuppressWarnings("unchecked") + private static String caminhoBruto(Map request) { + if (request == null) { + return null; + } + + // formato 1.0 (API Gateway REST) + String caminho = texto(request.get("path")); + if (caminho != null) { + return caminho; + } + + // formato 2.0 (Function URL e HTTP API) + caminho = texto(request.get("rawPath")); + if (caminho != null) { + return caminho; + } + + Object contexto = request.get("requestContext"); + if (contexto instanceof Map) { + Map rc = (Map) contexto; + Object http = rc.get("http"); + if (http instanceof Map) { + caminho = texto(((Map) http).get("path")); + if (caminho != null) { + return caminho; + } + } + return texto(rc.get("path")); + } + + return null; + } + + // ------------------------------------------------------------------ + // Corpo da requisicao + // ------------------------------------------------------------------ + + /** Desserializa o corpo como GameState. Corpo ruim vira 400, e nao 500. */ + private static GameState lerEstado(Map request) { + String corpo = texto(request.get("body")); + + if (corpo == null || corpo.isBlank()) { + throw new CorpoInvalidoException("corpo da requisicao vazio"); + } + + GameState state; + try { + state = GSON.fromJson(corpo, GameState.class); + } catch (JsonSyntaxException e) { + throw new CorpoInvalidoException("JSON invalido: " + e.getMessage()); + } + + if (state == null || state.getYou() == null || state.getYou().getHead() == null) { + throw new CorpoInvalidoException("estado de jogo incompleto: falta 'you'"); + } + if (state.getGame() == null) { + throw new CorpoInvalidoException("estado de jogo incompleto: falta 'game'"); + } + + return state; + } + + // ------------------------------------------------------------------ + // Resposta + // ------------------------------------------------------------------ + + /** Monta a resposta com Content-Type, inclusive nos erros. */ + private static APIGatewayProxyResponseEvent resposta(int status, Object corpo) { + Map headers = new HashMap<>(); + headers.put("Content-Type", corpo instanceof String ? "text/plain" : "application/json"); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(status) + .withHeaders(headers) + .withBody(corpo instanceof String ? (String) corpo : GSON.toJson(corpo)); } /** - * Chamado no final de cada jogo. Não precisa retornar nada. + * HashMap, e nao Map.of: se a excecao vier sem mensagem, Map.of lancaria + * NullPointerException dentro do proprio tratador de erro. */ - private void handleEnd(APIGatewayProxyRequestEvent request, Context context) { - // Você pode analisar a requisição para saber se venceu ou perdeu. - context.getLogger().log("Game Ended!"); + private static Map erro(String mensagem) { + Map corpo = new HashMap<>(); + corpo.put("error", mensagem == null ? "erro interno" : mensagem); + return corpo; + } + + private static String texto(Object valor) { + return valor instanceof String ? (String) valor : null; + } + + private static void log(Context context, String mensagem) { + if (context != null && context.getLogger() != null) { + context.getLogger().log(mensagem); + } } -} \ No newline at end of file +} diff --git a/src/main/java/com/mauadev/code/Logic.java b/src/main/java/com/mauadev/code/Logic.java new file mode 100644 index 0000000..c1aa616 --- /dev/null +++ b/src/main/java/com/mauadev/code/Logic.java @@ -0,0 +1,160 @@ +package com.mauadev.code; + +// Bem-vindo ao +// __________ __ __ .__ __ +// \______ \_____ _/ |__/ |_| | ____ ______ ____ _____ | | __ ____ +// | | _/\__ \ __\ __\ | _/ __ \ / ___// \__ \ | |/ // __ \ +// | | \ / __ \| | | | | |_\ ___/ \___ \| | \/ __ \| <\ ___/ +// |________/(______/__| |__| |____/\_____>______>___|__(______/__|__\_____> +// +// ESTE E O ARQUIVO QUE VOCE VAI EDITAR. Todo o resto do projeto existe +// so para levar o estado do jogo ate as quatro funcoes abaixo. +// +// Para comecar, ja deixamos pronta a logica que impede a sua cobra de andar +// para tras (ela morreria na hora). Os TODOs marcam os proximos passos. +// Documentacao: https://docs.battlesnake.com + +import com.mauadev.code.entities.Coordinate; +import com.mauadev.code.entities.GameState; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Logica da cobra. E AQUI que voce programa a inteligencia da snake. + *

+ * GET / -> {@link #info()}
+ * POST /start -> {@link #start(GameState)}
+ * POST /move -> {@link #getMove(GameState)} foco principal
+ * POST /end -> {@link #end(GameState)} + */ +public class Logic { + + // ThreadLocalRandom, e nao um Random estatico: com o SnapStart ligado o + // seed de um campo estatico seria capturado no snapshot, e toda execucao + // restaurada sortearia exatamente a mesma sequencia de jogadas. + + /** + * GET / - chamado quando voce cadastra a cobra e a cada partida. + * Controla a aparencia dela. Opcoes de cabeca, cauda e cor: + * https://docs.battlesnake.com/guides/customizations + */ + public static java.util.Map info() { + java.util.Map info = new java.util.HashMap<>(); + info.put("apiversion", "1"); + info.put("author", ""); // TODO: coloque aqui o SEU usuario do Battlesnake + info.put("color", "#8B0000"); // TODO: escolha a cor da sua cobra + info.put("head", "tiger-king"); // TODO: escolha a cabeca + info.put("tail", "hook"); // TODO: escolha a cauda + return info; + } + + /** + * POST /start - chamado uma vez, quando a partida comeca. + * Bom lugar para preparar qualquer estado inicial. + */ + public static void start(GameState state) { + // state.getTurn(), state.getBoard(), state.getYou() estao disponiveis + } + + /** + * POST /end - chamado uma vez, quando a partida termina. + */ + public static void end(GameState state) { + // Voce pode analisar o estado final para saber se venceu ou perdeu. + } + + /** + * POST /move - chamado a cada turno. Aqui mora a inteligencia da sua cobra. + * Deve retornar "up", "down", "left" ou "right". + * Exemplo do JSON recebido: https://docs.battlesnake.com/api/example-move + * + * @param state estado atual do jogo + * @return direcao escolhida + */ + public static String getMove(GameState state) { + List isMoveSafe = new ArrayList<>(Arrays.asList("up", "down", "left", "right")); + + // --- Impedir que a cobra ande para tras (ja implementado) --- + // O pescoco e a parte do corpo logo atras da cabeca. Voltar por cima + // dele e morte certa, entao removemos aquela direcao da lista. + Coordinate myHead = state.getYou().getHead(); + List body = state.getYou().getBody(); + + if (body != null && body.size() >= 2) { + Coordinate myNeck = body.get(1); + + if (myNeck.getX() < myHead.getX()) { + // pescoco a esquerda da cabeca -> nao va para a esquerda + isMoveSafe.remove("left"); + } else if (myNeck.getX() > myHead.getX()) { + // pescoco a direita da cabeca -> nao va para a direita + isMoveSafe.remove("right"); + } else if (myNeck.getY() < myHead.getY()) { + // pescoco abaixo da cabeca -> nao desca + isMoveSafe.remove("down"); + } else if (myNeck.getY() > myHead.getY()) { + // pescoco acima da cabeca -> nao suba + isMoveSafe.remove("up"); + } + } + + // 2. Impedir que a cobra saia do tabuleiro (paredes) + if (state.getBoard() != null) { + int boardWidth = state.getBoard().getWidth(); + int boardHeight = state.getBoard().getHeight(); + + if (myHead.getX() + 1 >= boardWidth) { + isMoveSafe.remove("right"); + } + if (myHead.getX() - 1 < 0) { + isMoveSafe.remove("left"); + } + if (myHead.getY() + 1 >= boardHeight) { + isMoveSafe.remove("up"); + } + if (myHead.getY() - 1 < 0) { + isMoveSafe.remove("down"); + } + } + + // 3. Impedir que a cobra bata no próprio corpo + if (body != null) { + for (Coordinate segment : body) { + if (segment.getX() == myHead.getX() + 1 && segment.getY() == myHead.getY()) { + isMoveSafe.remove("right"); + } + if (segment.getX() == myHead.getX() - 1 && segment.getY() == myHead.getY()) { + isMoveSafe.remove("left"); + } + if (segment.getX() == myHead.getX() && segment.getY() == myHead.getY() + 1) { + isMoveSafe.remove("up"); + } + if (segment.getX() == myHead.getX() && segment.getY() == myHead.getY() - 1) { + isMoveSafe.remove("down"); + } + } + } + + // TODO: Passo 3 — impedir que a cobra bata nas adversárias + // List opponents = state.getBoard().getSnakes(); + + // Sobrou alguma direcao segura? + if (isMoveSafe.isEmpty()) { + // Emergencia: todas as direcoes sao perigosas. + // Escolhemos uma ao acaso entre as 4 - melhor do que travar. + List allMoves = Arrays.asList("up", "down", "left", "right"); + return allMoves.get(ThreadLocalRandom.current().nextInt(allMoves.size())); + } + + // Escolhe uma direcao segura ao acaso. + String chosen = isMoveSafe.get(ThreadLocalRandom.current().nextInt(isMoveSafe.size())); + + // TODO: Passo 4 - ir atras da comida em vez de sortear, para nao morrer de fome + // List food = state.getBoard().getFood(); + + return chosen; + } +} \ No newline at end of file diff --git a/src/main/java/com/mauadev/code/entities/Game.java b/src/main/java/com/mauadev/code/entities/Game.java new file mode 100644 index 0000000..25fcb38 --- /dev/null +++ b/src/main/java/com/mauadev/code/entities/Game.java @@ -0,0 +1,14 @@ +package com.mauadev.code.entities; + +/** Metadados da partida. */ +public class Game { + private String id; + private int timeout; + + public Game() {} + + public String getId() { return id; } + public int getTimeout() { return timeout; } + public void setId(String id) { this.id = id; } + public void setTimeout(int timeout) { this.timeout = timeout; } +} \ No newline at end of file diff --git a/src/main/java/com/mauadev/code/entities/GameState.java b/src/main/java/com/mauadev/code/entities/GameState.java index 0efef52..27331f1 100644 --- a/src/main/java/com/mauadev/code/entities/GameState.java +++ b/src/main/java/com/mauadev/code/entities/GameState.java @@ -1,41 +1,22 @@ package com.mauadev.code.entities; +/** O pacote completo que chega em /start, /move e /end. */ public class GameState { - // Atributos do estado do jogo - + private Game game; private int turn; private Board board; private Snake you; - public GameState(){ - } - - public GameState( - String turn, - Board board, - Snake you - ){ - this.turn = Integer.parseInt(turn); - this.board = board; - this.you = you; - } + public GameState() {} - public int getTurn() { - return turn; - } - public Board getBoard() { - return board; - } - public Snake getYou() { - return you; - } + public Game getGame() { return game; } + public int getTurn() { return turn; } + public Board getBoard() { return board; } + public Snake getYou() { return you; } - public void setTurn(int turn) { - this.turn = turn; - } - public void setBoard(Board board) { - this.board = board; - } - -} + public void setGame(Game game) { this.game = game; } + public void setTurn(int turn) { this.turn = turn; } + public void setBoard(Board board) { this.board = board; } + public void setYou(Snake you) { this.you = you; } +} \ No newline at end of file diff --git a/src/main/java/com/mauadev/code/entities/Snake.java b/src/main/java/com/mauadev/code/entities/Snake.java index 46b24e6..4a6172c 100644 --- a/src/main/java/com/mauadev/code/entities/Snake.java +++ b/src/main/java/com/mauadev/code/entities/Snake.java @@ -71,7 +71,6 @@ public void setHead(Coordinate head) { } public void setLength(int length) { this.length = length; - this.body = this.body.subList(0, length); } public void setShout(String shout) { this.shout = shout; diff --git a/src/test/java/com/mauadev/HandlerTest.java b/src/test/java/com/mauadev/HandlerTest.java index 19c8ed8..4813034 100644 --- a/src/test/java/com/mauadev/HandlerTest.java +++ b/src/test/java/com/mauadev/HandlerTest.java @@ -1,58 +1,102 @@ package com.mauadev; import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.LambdaLogger; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.mauadev.code.Handler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; import java.util.Map; -import com.mauadev.code.Handler; - import static org.junit.jupiter.api.Assertions.*; /** - * Testes unitários para a classe Handler. + * Testes do Handler - percorrem o caminho completo + * API Gateway -> Handler -> Logic. */ public class HandlerTest { + private static final List DIRECOES = List.of("up", "down", "left", "right"); + private static final Gson GSON = new Gson(); + private Handler handler; - private Context testContext; - private Gson gson; + private Context context; - // @BeforeEach garante que este método seja executado antes de cada @Test @BeforeEach public void setUp() { handler = new Handler(); - gson = new Gson(); - // Criamos um Context de teste "mockado" para passar para o handler. - testContext = new TestContext(); + context = new TestContext(); } - @Test - @DisplayName("Teste da rota / (Info) - Deve retornar informações da cobra com status 200") - public void testHandleInfo_shouldReturnSnakeInfo() { - // Arrange: Prepara a requisição para a rota "/" - APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent().withPath("/"); + // --- Helpers --- + + /** Monta um estado de jogo minimo com cabeca e pescoco. */ + private String gameStateJson(int hx, int hy, int nx, int ny) { + return String.format(""" + { + "game": {"id": "partida-de-teste", "timeout": 500}, + "turn": 4, + "board": { + "height": 11, "width": 11, + "food": [{"x": 5, "y": 5}], + "hazards": [], + "snakes": [] + }, + "you": { + "id": "minha-cobra", "name": "MinhaCobra", "health": 100, + "body": [{"x": %d, "y": %d}, {"x": %d, "y": %d}, {"x": %d, "y": %d}], + "head": {"x": %d, "y": %d}, + "length": 3 + } + } + """, hx, hy, nx, ny, nx, ny - 1, hx, hy); + } - // Act: Executa o método a ser testado - APIGatewayProxyResponseEvent response = handler.handleRequest(request, testContext); + /** Evento no formato 1.0 (API Gateway REST): o caminho vem em "path". */ + private Map request(String path, String body) { + Map req = new HashMap<>(); + req.put("path", path); + req.put("body", body); + return req; + } - // Assert: Verifica os resultados - assertEquals(200, response.getStatusCode()); - assertEquals("application/json", response.getHeaders().get("Content-Type")); - assertNotNull(response.getBody()); + /** Evento no formato 2.0 (Function URL): o caminho vem em "rawPath". */ + private Map requestV2(String path, String body) { + Map http = new HashMap<>(); + http.put("path", path); + http.put("method", "POST"); + + Map requestContext = new HashMap<>(); + requestContext.put("http", http); + + Map req = new HashMap<>(); + req.put("version", "2.0"); + req.put("rawPath", path); + req.put("requestContext", requestContext); + req.put("body", body); + return req; + } + + private Map parseBody(String json) { + Type type = new TypeToken>() {}.getType(); + return GSON.fromJson(json, type); + } - // Analisa o corpo JSON para verificar seu conteúdo - Type mapType = new TypeToken>() {}.getType(); - Map body = gson.fromJson(response.getBody(), mapType); + // --- T1: info retorna campos obrigatorios --- + @Test + @DisplayName("T1 - GET / retorna os campos obrigatorios da cobra") + public void testInfo_retornaCamposObrigatorios() { + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/", null), context); + + assertEquals(200, response.getStatusCode()); + Map body = parseBody(response.getBody()); assertEquals("1", body.get("apiversion")); assertNotNull(body.get("author")); assertNotNull(body.get("color")); @@ -60,173 +104,274 @@ public void testHandleInfo_shouldReturnSnakeInfo() { assertNotNull(body.get("tail")); } + // --- T2: move retorna direcao valida --- + @Test - @DisplayName("Teste da rota /start - Deve retornar status 200 OK sem corpo") - public void testHandleStart_shouldReturn200OK() { - // Arrange - APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent().withPath("/start"); - // O corpo da requisição pode ser um JSON com o estado do jogo, mas para este teste não é necessário - request.setBody("{}"); + @DisplayName("T2 - POST /move retorna uma direcao valida") + public void testMove_retornaDirecaoValida() { + String stateJson = gameStateJson(5, 4, 4, 4); + + for (int i = 0; i < 50; i++) { + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/move", stateJson), context); + assertEquals(200, response.getStatusCode()); + Map body = parseBody(response.getBody()); + assertTrue(DIRECOES.contains(body.get("move")), + "Direcao invalida: " + body.get("move")); + } + } - // Act - APIGatewayProxyResponseEvent response = handler.handleRequest(request, testContext); + // --- T3: nunca volta contra o pescoco --- - // Assert - assertEquals(200, response.getStatusCode()); - // O handler para /start não define um corpo de resposta, então ele pode ser nulo ou vazio - assertTrue(response.getBody() == null || response.getBody().isEmpty()); - } - - @Test - @DisplayName("Teste da rota /move - Deve se mover para cima em direção à comida") - public void testHandleMove_withBoardData_shouldMoveUpTowardsFood() { - // Arrange 🐍 - APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent().withPath("/move"); - - String jsonRequestBody = """ - { - "game": { - "id": "game-id-123", - "ruleset": {"name": "standard", "version": "v1.2.3"}, - "timeout": 500 - }, - "turn": 4, - "board": { - "height": 11, - "width": 11, - "food": [ - {"x": 5, "y": 5}, - {"x": 9, "y": 0}, - {"x": 2, "y": 6} - ], - "hazards": [ - {"x": 3, "y": 3} - ], - "snakes": [ - { - "id": "snake-id-yours", - "name": "MySnake", - "health": 90, - "body": [{"x": 5, "y": 4}, {"x": 4, "y": 4}, {"x": 3, "y": 4}], - "head": {"x": 5, "y": 4}, - "length": 3, - "shout": "Going for food!" - }, - { - "id": "snake-id-opponent", - "name": "OpponentSnake", - "health": 95, - "body": [{"x": 1, "y": 1}, {"x": 1, "y": 2}, {"x": 1, "y": 3}], - "head": {"x": 1, "y": 1}, - "length": 3, - "shout": "Hisss" - } - ] - }, - "you": { - "id": "snake-id-yours", - "name": "MySnake", - "health": 50, - "body": [{"x": 6, "y": 4}, {"x": 4, "y": 4}, {"x": 3, "y": 4}], - "head": {"x": 6, "y": 4}, - "length": 3, - "shout": "Going for food!" - } + @Test + @DisplayName("T3a - nao volta para a esquerda quando pescoco esta a esquerda") + public void testMove_naoVoltaEsquerda() { + String state = gameStateJson(5, 4, 4, 4); // pescoco a esquerda + for (int i = 0; i < 50; i++) { + APIGatewayProxyResponseEvent resp = handler.handleRequest(request("/move", state), context); + assertNotEquals("left", parseBody(resp.getBody()).get("move")); + } + } + + @Test + @DisplayName("T3b - nao volta para a direita quando pescoco esta a direita") + public void testMove_naoVoltaDireita() { + String state = gameStateJson(5, 4, 6, 4); // pescoco a direita + for (int i = 0; i < 50; i++) { + APIGatewayProxyResponseEvent resp = handler.handleRequest(request("/move", state), context); + assertNotEquals("right", parseBody(resp.getBody()).get("move")); + } + } + + @Test + @DisplayName("T3c - nao desce quando pescoco esta abaixo") + public void testMove_naoDesce() { + String state = gameStateJson(5, 4, 5, 3); // pescoco abaixo + for (int i = 0; i < 50; i++) { + APIGatewayProxyResponseEvent resp = handler.handleRequest(request("/move", state), context); + assertNotEquals("down", parseBody(resp.getBody()).get("move")); } - """; + } - request.setBody(jsonRequestBody); + @Test + @DisplayName("T3d - nao sobe quando pescoco esta acima") + public void testMove_naoSobe() { + String state = gameStateJson(5, 4, 5, 5); // pescoco acima + for (int i = 0; i < 50; i++) { + APIGatewayProxyResponseEvent resp = handler.handleRequest(request("/move", state), context); + assertNotEquals("up", parseBody(resp.getBody()).get("move")); + } + } - // Act 🎯 - APIGatewayProxyResponseEvent response = handler.handleRequest(request, testContext); + // --- T4: evita parede quando tem opcao --- - // Assert ✅ - assertEquals(200, response.getStatusCode(), "O status code da resposta deve ser 200"); - assertNotNull(response.getBody(), "O corpo da resposta não pode ser nulo"); + @Test + @DisplayName("T4 - evita parede quando tem outra opcao") + public void testMove_evitaParede() { + // Cobra no canto (0,0), pescoco a direita (1,0): + // left (x=-1) e down (y=-1) saem do tabuleiro. Pescoço bloqueia right. + // A única opção segura é up. + String state = gameStateJson(0, 0, 1, 0); + for (int i = 0; i < 50; i++) { + APIGatewayProxyResponseEvent resp = handler.handleRequest(request("/move", state), context); + String move = parseBody(resp.getBody()).get("move"); + assertNotEquals("left", move, "saiu do tabuleiro para a esquerda"); + assertNotEquals("down", move, "saiu do tabuleiro para baixo"); + assertNotEquals("right", move, "voltou pelo pescoco"); + assertEquals("up", move, "deveria ter escolhido up"); + } + } - Type mapType = new TypeToken>() {}.getType(); - Map body = gson.fromJson(response.getBody(), mapType); + // --- T5: evita proprio corpo quando tem opcao --- - // Aqui você verifica se o seu algorítmo está realizando o movimento certo, alinhado com a lógica implementada. - // Nesse caso, a lógica será sempre up pois a configuração em Handler.java está fixa. - assertEquals("up", body.get("move"), "O movimento esperado era 'up' em direção à comida"); + @Test + @DisplayName("T5 - evita proprio corpo quando tem opcao") + public void testMove_evitaProprioCorpo() { + // Cabeca em (5,4), pescoco em (4,4) [esquerda], corpo em (5,5) [acima] + // Restam right e down + String stateJson = """ + { + "game": {"id": "teste", "timeout": 500}, + "turn": 1, + "board": {"height": 11, "width": 11, "food": [], "hazards": [], "snakes": []}, + "you": { + "id": "s1", "name": "cobra", "health": 100, + "body": [{"x": 5, "y": 4}, {"x": 4, "y": 4}, {"x": 5, "y": 5}, {"x": 4, "y": 3}], + "head": {"x": 5, "y": 4}, "length": 4 + } + } + """; + for (int i = 0; i < 50; i++) { + APIGatewayProxyResponseEvent resp = handler.handleRequest(request("/move", stateJson), context); + String move = parseBody(resp.getBody()).get("move"); + assertNotEquals("left", move, "voltou pelo pescoco"); + assertNotEquals("up", move, "bateu no proprio corpo"); + assertTrue(List.of("right", "down").contains(move)); + } } + // --- T6: comportamento sem safe moves (nao deve lancar excecao) --- @Test - @DisplayName("Teste da rota /end - Deve retornar status 200 OK sem corpo") - public void testHandleEnd_shouldReturn200OK() { - // Arrange - APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent().withPath("/end"); - request.setBody("{}"); + @DisplayName("T6 - comportamento definido quando nao ha safe moves") + public void testMove_comportamentoSemSafeMoves() { + // Cabeca no canto, pescoco acima, corpo a direita + String stateJson = """ + { + "game": {"id": "teste", "timeout": 500}, + "turn": 1, + "board": {"height": 11, "width": 11, "food": [], "hazards": [], "snakes": []}, + "you": { + "id": "s1", "name": "cobra", "health": 100, + "body": [{"x": 0, "y": 0}, {"x": 0, "y": 1}, {"x": 1, "y": 0}], + "head": {"x": 0, "y": 0}, "length": 3 + } + } + """; + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/move", stateJson), context); + assertEquals(200, response.getStatusCode()); + Map body = parseBody(response.getBody()); + assertTrue(DIRECOES.contains(body.get("move")), "Direcao invalida: " + body.get("move")); + } - // Act - APIGatewayProxyResponseEvent response = handler.handleRequest(request, testContext); + // --- start e end retornam 200 --- - // Assert + @Test + @DisplayName("POST /start retorna 200") + public void testStart_retorna200() { + String state = gameStateJson(5, 4, 4, 4); + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/start", state), context); assertEquals(200, response.getStatusCode()); - assertTrue(response.getBody() == null || response.getBody().isEmpty()); } @Test - @DisplayName("Teste de rota inválida - Deve retornar erro 404 Not Found") - public void testInvalidPath_shouldReturn404NotFound() { - // Arrange - APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent().withPath("/caminho-que-nao-existe"); + @DisplayName("POST /end retorna 200") + public void testEnd_retorna200() { + String state = gameStateJson(5, 4, 4, 4); + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/end", state), context); + assertEquals(200, response.getStatusCode()); + } + + // --- T7: formato 2.0 (Function URL e HTTP API) --- - // Act - APIGatewayProxyResponseEvent response = handler.handleRequest(request, testContext); + @Test + @DisplayName("T7a - GET / no formato 2.0 retorna as informacoes da cobra") + public void testFormatoV2_info() { + APIGatewayProxyResponseEvent response = handler.handleRequest(requestV2("/", null), context); + + assertEquals(200, response.getStatusCode()); + assertEquals("1", parseBody(response.getBody()).get("apiversion")); + } + + @Test + @DisplayName("T7b - POST /move no formato 2.0 retorna uma direcao valida") + public void testFormatoV2_move() { + String state = gameStateJson(5, 4, 4, 4); + APIGatewayProxyResponseEvent response = handler.handleRequest(requestV2("/move", state), context); + + assertEquals(200, response.getStatusCode()); + String jogada = parseBody(response.getBody()).get("move"); + assertTrue(DIRECOES.contains(jogada), "Direcao invalida: " + jogada); + } + + @Test + @DisplayName("T7c - formato 2.0 sem rawPath ainda resolve por requestContext.http.path") + public void testFormatoV2_semRawPath() { + String state = gameStateJson(5, 4, 4, 4); + Map req = requestV2("/move", state); + req.remove("rawPath"); + + APIGatewayProxyResponseEvent response = handler.handleRequest(req, context); + + assertEquals(200, response.getStatusCode()); + assertTrue(DIRECOES.contains(parseBody(response.getBody()).get("move"))); + } + + // --- T8: prefixo de stage e barra sobrando --- + + @Test + @DisplayName("T8 - caminho com o stage na frente e barra no fim ainda roteia") + public void testPrefixoDeStage() { + String state = gameStateJson(5, 4, 4, 4); + + assertEquals(200, handler.handleRequest(request("/dev/move", state), context).getStatusCode()); + assertEquals(200, handler.handleRequest(request("/move/", state), context).getStatusCode()); + assertEquals(200, handler.handleRequest(request("/dev", null), context).getStatusCode()); + } + + @Test + @DisplayName("T8b - rota realmente desconhecida continua sendo 404") + public void testRotaDesconhecida_retorna404() { + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/nao-existe", null), context); - // Assert assertEquals(404, response.getStatusCode()); - assertNotNull(response.getBody()); - - Type mapType = new TypeToken>() {}.getType(); - Map body = gson.fromJson(response.getBody(), mapType); - - assertEquals("Path not found", body.get("error")); - } - - /** - * Uma implementação simples da interface Context para fins de teste. - * Ela fornece um logger que imprime no console, evitando NullPointerException. - */ - private static class TestContext implements Context { - @Override - public String getAwsRequestId() { return "test-request-id"; } - @Override - public String getLogGroupName() { return "test-log-group"; } - @Override - public String getLogStreamName() { return "test-log-stream"; } - @Override - public String getFunctionName() { return "test-function"; } - @Override - public String getFunctionVersion() { return "1.0"; } - @Override - public String getInvokedFunctionArn() { return "arn:aws:lambda:us-east-1:123456789012:function:test-function"; } - @Override - public com.amazonaws.services.lambda.runtime.CognitoIdentity getIdentity() { return null; } - @Override - public com.amazonaws.services.lambda.runtime.ClientContext getClientContext() { return null; } - @Override - public int getRemainingTimeInMillis() { return 30000; } - @Override - public int getMemoryLimitInMB() { return 512; } - @Override - public LambdaLogger getLogger() { - // Retorna um logger simples que imprime no console durante o teste. - return new TestLogger(); - } + assertEquals("application/json", response.getHeaders().get("Content-Type")); } - private static class TestLogger implements LambdaLogger { - @Override - public void log(String message) { - System.out.println(message); - } - @Override - public void log(byte[] message) { - System.out.println(new String(message)); - } + @Test + @DisplayName("T8c - prefixo nao reconhecido como stage deve retornar 404") + public void testRoteamentoExato_urlPermissivaRetorna404() { + String state = gameStateJson(5, 4, 4, 4); + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/alguma-coisa/move", state), context); + + assertEquals(404, response.getStatusCode()); + } + + @Test + @DisplayName("T8d - metodo incorreto retorna 404") + public void testMetodoIncorreto_retorna404() { + Map reqGetMove = request("/move", null); + reqGetMove.put("httpMethod", "GET"); + APIGatewayProxyResponseEvent resp1 = handler.handleRequest(reqGetMove, context); + assertEquals(404, resp1.getStatusCode()); + + Map reqPostInfo = request("/", null); + reqPostInfo.put("httpMethod", "POST"); + APIGatewayProxyResponseEvent resp2 = handler.handleRequest(reqPostInfo, context); + assertEquals(404, resp2.getStatusCode()); + } + + // --- T9: corpo invalido vira 400, e nao 200 nem 500 --- + + @Test + @DisplayName("T9a - POST /move sem corpo retorna 400") + public void testMove_semCorpo_retorna400() { + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/move", null), context); + + assertEquals(400, response.getStatusCode()); + assertNotNull(parseBody(response.getBody()).get("error")); + } + + @Test + @DisplayName("T9b - POST /move com JSON quebrado retorna 400") + public void testMove_jsonQuebrado_retorna400() { + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/move", "{isso nao e json}"), context); + + assertEquals(400, response.getStatusCode()); + } + + @Test + @DisplayName("T9c - POST /move sem o campo 'you' retorna 400, e nao uma jogada inventada") + public void testMove_semYou_retorna400() { + String semYou = """ + {"game": {"id": "x", "timeout": 500}, "turn": 1, + "board": {"height": 11, "width": 11, "food": [], "hazards": [], "snakes": []}} + """; + APIGatewayProxyResponseEvent response = handler.handleRequest(request("/move", semYou), context); + + assertEquals(400, response.getStatusCode()); + assertNull(parseBody(response.getBody()).get("move"), "nao pode devolver jogada para estado invalido"); + } + + // --- T10: toda resposta traz Content-Type --- + + @Test + @DisplayName("T10 - todas as respostas trazem Content-Type") + public void testContentTypeSemprePresente() { + String state = gameStateJson(5, 4, 4, 4); + + assertNotNull(handler.handleRequest(request("/", null), context).getHeaders().get("Content-Type")); + assertNotNull(handler.handleRequest(request("/move", state), context).getHeaders().get("Content-Type")); + assertNotNull(handler.handleRequest(request("/move", null), context).getHeaders().get("Content-Type")); + assertNotNull(handler.handleRequest(request("/nao-existe", null), context).getHeaders().get("Content-Type")); } } \ No newline at end of file diff --git a/src/test/java/com/mauadev/TestContext.java b/src/test/java/com/mauadev/TestContext.java new file mode 100644 index 0000000..070e080 --- /dev/null +++ b/src/test/java/com/mauadev/TestContext.java @@ -0,0 +1,74 @@ +package com.mauadev; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; + +public class TestContext implements Context { + + @Override + public String getAwsRequestId() { + return "test-request-id"; + } + + @Override + public String getLogGroupName() { + return "test-log-group"; + } + + @Override + public String getLogStreamName() { + return "test-log-stream"; + } + + @Override + public String getFunctionName() { + return "battlesnake-test"; + } + + @Override + public String getFunctionVersion() { + return "1"; + } + + @Override + public String getInvokedFunctionArn() { + return "arn:aws:lambda:test"; + } + + @Override + public CognitoIdentity getIdentity() { + return null; + } + + @Override + public ClientContext getClientContext() { + return null; + } + + @Override + public int getRemainingTimeInMillis() { + return 30000; + } + + @Override + public int getMemoryLimitInMB() { + return 512; + } + + @Override + public LambdaLogger getLogger() { + return new LambdaLogger() { + @Override + public void log(String message) { + System.out.println(message); + } + + @Override + public void log(byte[] message) { + System.out.println(new String(message)); + } + }; + } +} \ No newline at end of file diff --git a/terraform/app/main.tf b/terraform/app/main.tf index fcfd37c..1ab3c2e 100644 --- a/terraform/app/main.tf +++ b/terraform/app/main.tf @@ -55,9 +55,33 @@ resource "aws_lambda_function" "lambda_battle_snake_java" { handler = "com.mauadev.code.Handler::handleRequest" runtime = "java17" + # A JVM sobe devagar e a CPU da Lambda escala com a memoria: em 128 MB (o + # padrao quando nada e definido) o cold start passa de varios segundos e + # estoura o timeout padrao de 3s. + memory_size = 1024 + timeout = 10 + + # SnapStart tira um snapshot da JVM ja inicializada e restaura a partir dele, + # cortando o cold start de segundos para centenas de milissegundos. So vale + # para versoes publicadas, por isso o publish = true. + publish = true + + snap_start { + apply_on = "PublishedVersions" + } + source_code_hash = filebase64sha256("../../target/battlesnake-lambda-1.0.jar") } +# O snapshot pertence a uma versao publicada. Sem apontar o API Gateway para um +# alias, o trafego iria para $LATEST, que roda sem SnapStart e anula o ganho. +resource "aws_lambda_alias" "live" { + name = "live" + description = "Versao publicada que o API Gateway invoca, com SnapStart ativo" + function_name = aws_lambda_function.lambda_battle_snake_java.function_name + function_version = aws_lambda_function.lambda_battle_snake_java.version +} + # ---- configurando o api gateway ---- @@ -96,7 +120,7 @@ resource "aws_api_gateway_integration" "integration" { http_method = aws_api_gateway_method.method.http_method integration_http_method = "POST" type = "AWS_PROXY" - uri = aws_lambda_function.lambda_battle_snake_java.invoke_arn + uri = aws_lambda_alias.live.invoke_arn } resource "aws_api_gateway_integration" "root_integration" { @@ -105,7 +129,7 @@ resource "aws_api_gateway_integration" "root_integration" { http_method = aws_api_gateway_method.root_method.http_method integration_http_method = "POST" type = "AWS_PROXY" - uri = aws_lambda_function.lambda_battle_snake_java.invoke_arn + uri = aws_lambda_alias.live.invoke_arn } # adicionando permiossoes na lambda para o apigateway poder executala @@ -113,6 +137,7 @@ resource "aws_lambda_permission" "api_gateway_permission" { statement_id = "AllowAPIGatewayToInvoke" action = "lambda:InvokeFunction" function_name = aws_lambda_function.lambda_battle_snake_java.function_name + qualifier = aws_lambda_alias.live.name principal = "apigateway.amazonaws.com" source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*" }