From 3f5ba7add8399fe693ed3917b37a5e92b6525436 Mon Sep 17 00:00:00 2001 From: Santaro13 Date: Wed, 13 May 2026 22:00:15 +0200 Subject: [PATCH] Logica del juego preparada para UI --- .idea/copilotDiffState.xml | 83 +++++++ src/main/java/es/iesquevedo/model/Board.java | 223 ++++++++++++++++++ src/main/java/es/iesquevedo/model/Game.java | 18 ++ src/main/java/es/iesquevedo/model/Move.java | 77 ++++++ .../es/iesquevedo/service/MoveValidator.java | 22 ++ .../service/impl/GameServiceImpl.java | 107 +++++++-- .../service/impl/InazumaGoMoveValidator.java | 79 +++++++ .../java/es/iesquevedo/model/BoardTest.java | 163 +++++++++++++ .../iesquevedo/service/GameServiceTest.java | 7 +- .../impl/GameServiceImplExecuteMoveTest.java | 147 ++++++++++++ .../impl/InazumaGoMoveValidatorTest.java | 112 +++++++++ 11 files changed, 1022 insertions(+), 16 deletions(-) create mode 100644 src/main/java/es/iesquevedo/model/Board.java create mode 100644 src/main/java/es/iesquevedo/model/Move.java create mode 100644 src/main/java/es/iesquevedo/service/MoveValidator.java create mode 100644 src/main/java/es/iesquevedo/service/impl/InazumaGoMoveValidator.java create mode 100644 src/test/java/es/iesquevedo/model/BoardTest.java create mode 100644 src/test/java/es/iesquevedo/service/impl/GameServiceImplExecuteMoveTest.java create mode 100644 src/test/java/es/iesquevedo/service/impl/InazumaGoMoveValidatorTest.java diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml index b12a13e..eab55e2 100644 --- a/.idea/copilotDiffState.xml +++ b/.idea/copilotDiffState.xml @@ -20,6 +20,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -37,6 +87,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/es/iesquevedo/model/Board.java b/src/main/java/es/iesquevedo/model/Board.java new file mode 100644 index 0000000..218efeb --- /dev/null +++ b/src/main/java/es/iesquevedo/model/Board.java @@ -0,0 +1,223 @@ +package es.iesquevedo.model; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +/** + * Representa el tablero de Inazuma Go (9x9 intersecciones). + * Estado de cada celda: vacía (0), negra (1), blanca (2). + */ +public class Board { + private static final int SIZE = 9; + private int[][] board; // 0 = empty, 1 = black, 2 = white + + public Board() { + this.board = new int[SIZE][SIZE]; + } + + public Board(Board other) { + this.board = new int[SIZE][SIZE]; + for (int r = 0; r < SIZE; r++) { + for (int c = 0; c < SIZE; c++) { + this.board[r][c] = other.board[r][c]; + } + } + } + + /** + * Obtiene el tamaño del tablero. + */ + public int getSize() { + return SIZE; + } + + /** + * Obtiene el estado de una celda (0=vacía, 1=negra, 2=blanca). + */ + public int getCell(int row, int col) { + if (!isValid(row, col)) return -1; + return board[row][col]; + } + + /** + * Coloca una piedra en el tablero. + */ + public void placeStone(int row, int col, int color) { + if (isValid(row, col)) { + board[row][col] = color; + } + } + + /** + * Remueve una piedra del tablero. + */ + public void removeStone(int row, int col) { + if (isValid(row, col)) { + board[row][col] = 0; + } + } + + /** + * Verifica si una coordenada es válida. + */ + public boolean isValid(int row, int col) { + return row >= 0 && row < SIZE && col >= 0 && col < SIZE; + } + + /** + * Verifica si una celda está vacía. + */ + public boolean isEmpty(int row, int col) { + return isValid(row, col) && board[row][col] == 0; + } + + /** + * Obtiene los vecinos ortogonales de una celda. + */ + public List getNeighbors(int row, int col) { + List neighbors = new ArrayList<>(); + int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; + for (int[] dir : directions) { + int nr = row + dir[0]; + int nc = col + dir[1]; + if (isValid(nr, nc)) { + neighbors.add(new int[]{nr, nc}); + } + } + return neighbors; + } + + /** + * Calcula las libertades (grados de libertad) de un grupo. + * Retorna el número de intersecciones vacías adyacentes al grupo. + */ + public int countLibertiesForGroup(int row, int col) { + int color = board[row][col]; + if (color == 0) return 0; + + Set visited = new HashSet<>(); + Set liberties = new HashSet<>(); + Queue queue = new LinkedList<>(); + queue.add(new int[]{row, col}); + visited.add(row + "," + col); + + while (!queue.isEmpty()) { + int[] current = queue.poll(); + int r = current[0]; + int c = current[1]; + + for (int[] neighbor : getNeighbors(r, c)) { + int nr = neighbor[0]; + int nc = neighbor[1]; + String key = nr + "," + nc; + + if (isEmpty(nr, nc)) { + liberties.add(key); + } else if (board[nr][nc] == color && !visited.contains(key)) { + visited.add(key); + queue.add(new int[]{nr, nc}); + } + } + } + + return liberties.size(); + } + + /** + * Obtiene todas las piedras de un grupo. + */ + public Set getGroup(int row, int col) { + int color = board[row][col]; + if (color == 0) return new HashSet<>(); + + Set group = new HashSet<>(); + Queue queue = new LinkedList<>(); + queue.add(new int[]{row, col}); + group.add(row + "," + col); + + while (!queue.isEmpty()) { + int[] current = queue.poll(); + int r = current[0]; + int c = current[1]; + + for (int[] neighbor : getNeighbors(r, c)) { + int nr = neighbor[0]; + int nc = neighbor[1]; + String key = nr + "," + nc; + + if (board[nr][nc] == color && !group.contains(key)) { + group.add(key); + queue.add(new int[]{nr, nc}); + } + } + } + + return group; + } + + /** + * Detecta y captura grupos sin libertades. + * Retorna el número de piedras capturadas. + */ + public int captureGroupsWithoutLiberties() { + int captured = 0; + for (int r = 0; r < SIZE; r++) { + for (int c = 0; c < SIZE; c++) { + if (board[r][c] != 0 && countLibertiesForGroup(r, c) == 0) { + Set group = getGroup(r, c); + for (String stone : group) { + String[] parts = stone.split(","); + int sr = Integer.parseInt(parts[0]); + int sc = Integer.parseInt(parts[1]); + removeStone(sr, sc); + captured++; + } + } + } + } + return captured; + } + + /** + * Crea una copia del tablero. + */ + public Board clone() { + return new Board(this); + } + + /** + * Compara dos tableros. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Board)) return false; + Board other = (Board) obj; + for (int r = 0; r < SIZE; r++) { + for (int c = 0; c < SIZE; c++) { + if (this.board[r][c] != other.board[r][c]) return false; + } + } + return true; + } + + /** + * Representación en texto del tablero. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int r = 0; r < SIZE; r++) { + for (int c = 0; c < SIZE; c++) { + char ch = board[r][c] == 0 ? '.' : (board[r][c] == 1 ? 'X' : 'O'); + sb.append(ch).append(" "); + } + sb.append("\n"); + } + return sb.toString(); + } +} + diff --git a/src/main/java/es/iesquevedo/model/Game.java b/src/main/java/es/iesquevedo/model/Game.java index 6913e8f..010d10d 100644 --- a/src/main/java/es/iesquevedo/model/Game.java +++ b/src/main/java/es/iesquevedo/model/Game.java @@ -15,6 +15,10 @@ public class Game { private String winnerPlayerId; private LocalDateTime createdAt; private LocalDateTime finishedAt; + private Board board; + private List moves; + private int consecutivePasses; + private Board lastBoardState; public Game(String name, Player player1) { this.id = UUID.randomUUID().toString(); @@ -27,6 +31,10 @@ public Game(String name, Player player1) { this.winnerPlayerId = null; this.createdAt = LocalDateTime.now(); this.finishedAt = null; + this.board = new Board(); + this.moves = new ArrayList<>(); + this.consecutivePasses = 0; + this.lastBoardState = null; } public String getId() { return id; } @@ -44,6 +52,13 @@ public Player getCurrentPlayer() { public void setWinnerPlayerId(String playerId) { this.winnerPlayerId = playerId; } public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getFinishedAt() { return finishedAt; } + public Board getBoard() { return board; } + public List getMoves() { return moves; } + public int getConsecutivePasses() { return consecutivePasses; } + public void incrementConsecutivePasses() { this.consecutivePasses++; } + public void resetConsecutivePasses() { this.consecutivePasses = 0; } + public Board getLastBoardState() { return lastBoardState; } + public void setLastBoardState(Board boardState) { this.lastBoardState = boardState; } public void addPlayer(Player player) { if (state != GameState.WAITING) { @@ -61,6 +76,9 @@ public void start() { } this.state = GameState.IN_PROGRESS; this.currentPlayerIndex = 0; + this.board = new Board(); // Reinicializar tablero limpio + this.moves = new ArrayList<>(); + this.consecutivePasses = 0; } public void nextTurn() { diff --git a/src/main/java/es/iesquevedo/model/Move.java b/src/main/java/es/iesquevedo/model/Move.java new file mode 100644 index 0000000..3f71917 --- /dev/null +++ b/src/main/java/es/iesquevedo/model/Move.java @@ -0,0 +1,77 @@ +package es.iesquevedo.model; + +import java.util.UUID; + +/** + * Representa un movimiento en una partida de Inazuma Go. + */ +public class Move { + private String id; + private String playerId; + private int row; + private int col; + private boolean isPass; // true si es un pase, false si es colocar piedra + private long timestamp; + private int capturedCount; // número de piedras capturadas en este movimiento + + public Move(String playerId, int row, int col) { + this.id = UUID.randomUUID().toString(); + this.playerId = playerId; + this.row = row; + this.col = col; + this.isPass = false; + this.timestamp = System.currentTimeMillis(); + this.capturedCount = 0; + } + + public Move(String playerId, boolean isPass) { + this.id = UUID.randomUUID().toString(); + this.playerId = playerId; + this.isPass = isPass; + this.timestamp = System.currentTimeMillis(); + this.capturedCount = 0; + this.row = -1; + this.col = -1; + } + + public String getId() { + return id; + } + + public String getPlayerId() { + return playerId; + } + + public int getRow() { + return row; + } + + public int getCol() { + return col; + } + + public boolean isPass() { + return isPass; + } + + public long getTimestamp() { + return timestamp; + } + + public int getCapturedCount() { + return capturedCount; + } + + public void setCapturedCount(int count) { + this.capturedCount = count; + } + + @Override + public String toString() { + if (isPass) { + return "Move{" + "playerId='" + playerId + '\'' + ", PASS" + ", timestamp=" + timestamp + '}'; + } + return "Move{" + "playerId='" + playerId + '\'' + ", row=" + row + ", col=" + col + ", timestamp=" + timestamp + '}'; + } +} + diff --git a/src/main/java/es/iesquevedo/service/MoveValidator.java b/src/main/java/es/iesquevedo/service/MoveValidator.java new file mode 100644 index 0000000..390874f --- /dev/null +++ b/src/main/java/es/iesquevedo/service/MoveValidator.java @@ -0,0 +1,22 @@ +package es.iesquevedo.service; + +import es.iesquevedo.exception.InvalidMoveException; +import es.iesquevedo.exception.PlayerNotInTurnException; +import es.iesquevedo.model.Game; +import es.iesquevedo.model.Move; + +/** + * Interfaz para validar movimientos en una partida de Inazuma Go. + */ +public interface MoveValidator { + /** + * Valida un movimiento en el contexto de una partida. + * + * @param game La partida actual + * @param move El movimiento a validar + * @throws InvalidMoveException si el movimiento es inválido + * @throws PlayerNotInTurnException si no es el turno del jugador + */ + void validateMove(Game game, Move move) throws InvalidMoveException, PlayerNotInTurnException; +} + diff --git a/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java b/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java index 3a0f58a..1d54f16 100644 --- a/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java +++ b/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java @@ -2,10 +2,13 @@ import es.iesquevedo.exception.InvalidMoveException; import es.iesquevedo.exception.PlayerNotInTurnException; +import es.iesquevedo.model.Board; import es.iesquevedo.model.Game; import es.iesquevedo.model.GameState; +import es.iesquevedo.model.Move; import es.iesquevedo.model.Player; import es.iesquevedo.service.GameService; +import es.iesquevedo.service.MoveValidator; import java.util.HashMap; import java.util.Map; @@ -18,6 +21,15 @@ public class GameServiceImpl implements GameService { // Almacenamiento en memoria (será reemplazado por repositorio en producción) private final Map games = new HashMap<>(); + private final MoveValidator moveValidator; + + public GameServiceImpl() { + this.moveValidator = new InazumaGoMoveValidator(); + } + + public GameServiceImpl(MoveValidator moveValidator) { + this.moveValidator = moveValidator; + } @Override public Game createGame(String gameName, Player player1) { @@ -65,29 +77,96 @@ public Game executeMove(String gameId, String playerId, Object moveData) { throw new IllegalArgumentException("Partida no encontrada: " + gameId); } - // Validar que es turno del jugador - Player currentPlayer = game.getCurrentPlayer(); - if (currentPlayer == null || !currentPlayer.getId().equals(playerId)) { - throw new PlayerNotInTurnException("No es el turno del jugador: " + playerId); + if (!(moveData instanceof Move)) { + throw new IllegalArgumentException("moveData debe ser instancia de Move"); } - // Validar que el jugador está vivo - if (!currentPlayer.isAlive()) { - throw new InvalidMoveException("El jugador no está vivo"); + Move move = (Move) moveData; + + // Validar movimiento con reglas de Inazuma Go + moveValidator.validateMove(game, move); + + // Guardar estado previo del tablero para detectar repetición + Board previousBoardState = game.getBoard().clone(); + + if (move.isPass()) { + // Registrar pase + game.getMoves().add(move); + game.incrementConsecutivePasses(); + + // Doble pase = fin de partida + if (game.getConsecutivePasses() >= 2) { + game.setState(GameState.FINISHED); + // Determinar ganador por puntuación (simplificado) + determineWinner(game); + return game; + } + } else { + // Ejecutar movimiento: colocar piedra + int color = game.getCurrentPlayerIndex() == 0 ? 1 : 2; // 1=negro, 2=blanco + Board board = game.getBoard(); + board.placeStone(move.getRow(), move.getCol(), color); + + // Capturar grupos enemigos sin libertades + int capturedCount = board.captureGroupsWithoutLiberties(); + move.setCapturedCount(capturedCount); + + // Registrar movimiento + game.getMoves().add(move); + + // Reiniciar contador de pases si hubo captura + if (capturedCount > 0) { + game.resetConsecutivePasses(); + } else { + game.resetConsecutivePasses(); // También reinicia con colocación normal + } + + // Detectar repetición: si el tablero es igual al estado previo, fin de partida + if (game.getLastBoardState() != null && game.getLastBoardState().equals(board)) { + game.setState(GameState.FINISHED); + determineWinner(game); + return game; + } } - // Validar que la partida está en curso - if (game.getState() != GameState.IN_PROGRESS) { - throw new InvalidMoveException("La partida no está en curso"); - } + // Guardar estado actual como "último estado" para próxima comparación + game.setLastBoardState(previousBoardState); - // Aquí iría validación específica de reglas del juego - // Por ahora, aceptamos el movimiento passively - // (MOTOR completa con lógica real según reglamento) + // Cambiar turno + game.nextTurn(); return game; } + /** + * Determina el ganador de acuerdo a la puntuación simplificada. + * (En versión completa, implementar algoritmo de conteo según reglamento) + */ + private void determineWinner(Game game) { + // Versión simplificada: cuenta piedras en el tablero + // TODO: Implementar conteo completo según reglamento (libertades, territorio, komi, etc.) + int blackStones = 0; + int whiteStones = 0; + + Board board = game.getBoard(); + for (int r = 0; r < 9; r++) { + for (int c = 0; c < 9; c++) { + if (board.getCell(r, c) == 1) blackStones++; + else if (board.getCell(r, c) == 2) whiteStones++; + } + } + + // Komi de 5.5 para Blanco + double blackScore = blackStones; + double whiteScore = whiteStones + 5.5; + + if (blackScore > whiteScore) { + game.setWinnerPlayerId(game.getPlayers().get(0).getId()); + } else { + game.setWinnerPlayerId(game.getPlayers().get(1).getId()); + } + } + @Override public Game nextTurn(String gameId) { Game game = getGame(gameId); diff --git a/src/main/java/es/iesquevedo/service/impl/InazumaGoMoveValidator.java b/src/main/java/es/iesquevedo/service/impl/InazumaGoMoveValidator.java new file mode 100644 index 0000000..0a13988 --- /dev/null +++ b/src/main/java/es/iesquevedo/service/impl/InazumaGoMoveValidator.java @@ -0,0 +1,79 @@ +package es.iesquevedo.service.impl; + +import es.iesquevedo.exception.InvalidMoveException; +import es.iesquevedo.exception.PlayerNotInTurnException; +import es.iesquevedo.model.Board; +import es.iesquevedo.model.Game; +import es.iesquevedo.model.GameState; +import es.iesquevedo.model.Move; +import es.iesquevedo.model.Player; +import es.iesquevedo.service.MoveValidator; + +/** + * Validador de movimientos para Inazuma Go. + * Implementa las reglas del juego: turno, posición, libertades, suicidio, etc. + */ +public class InazumaGoMoveValidator implements MoveValidator { + + @Override + public void validateMove(Game game, Move move) throws InvalidMoveException, PlayerNotInTurnException { + // Verificar que la partida está en progreso + if (game.getState() != GameState.IN_PROGRESS) { + throw new InvalidMoveException("La partida no está en progreso"); + } + + // Verificar que es el turno del jugador + Player currentPlayer = game.getCurrentPlayer(); + if (currentPlayer == null || !currentPlayer.getId().equals(move.getPlayerId())) { + throw new PlayerNotInTurnException("No es el turno del jugador: " + move.getPlayerId()); + } + + // Verificar que el jugador está vivo + if (!currentPlayer.isAlive()) { + throw new InvalidMoveException("El jugador no está vivo"); + } + + // Si es un pase, es válido + if (move.isPass()) { + return; + } + + // Validar posición + if (move.getRow() < 0 || move.getRow() >= 9 || move.getCol() < 0 || move.getCol() >= 9) { + throw new InvalidMoveException("Posición fuera del tablero: (" + move.getRow() + "," + move.getCol() + ")"); + } + + // La posición debe estar vacía + Board board = game.getBoard(); + if (!board.isEmpty(move.getRow(), move.getCol())) { + throw new InvalidMoveException("La posición ya está ocupada: (" + move.getRow() + "," + move.getCol() + ")"); + } + + // Validar que el movimiento no es suicidio + validateNotSuicide(board, move, game); + } + + /** + * Valida que el movimiento no deja el grupo sin libertades (suicidio). + * Un movimiento es suicidio si: + * - Coloca una piedra que quedaría sin libertades + * - Y no captura piedras enemigas que restauren libertades + */ + private void validateNotSuicide(Board board, Move move, Game game) throws InvalidMoveException { + Board testBoard = board.clone(); + + // Colocar la piedra (color: 1 para jugador 0, 2 para jugador 1) + int color = game.getCurrentPlayerIndex() == 0 ? 1 : 2; + testBoard.placeStone(move.getRow(), move.getCol(), color); + + // Capturar grupos enemigos sin libertades + testBoard.captureGroupsWithoutLiberties(); + + // Si el grupo del jugador sigue sin libertades, es suicidio + if (testBoard.countLibertiesForGroup(move.getRow(), move.getCol()) == 0) { + throw new InvalidMoveException("Movimiento es suicidio: la piedra quedaría sin libertades"); + } + } +} + + diff --git a/src/test/java/es/iesquevedo/model/BoardTest.java b/src/test/java/es/iesquevedo/model/BoardTest.java new file mode 100644 index 0000000..e7dfc3a --- /dev/null +++ b/src/test/java/es/iesquevedo/model/BoardTest.java @@ -0,0 +1,163 @@ +package es.iesquevedo.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BoardTest { + + private Board board; + + @BeforeEach + void setUp() { + board = new Board(); + } + + @Test + void testInitialBoardIsEmpty() { + for (int r = 0; r < 9; r++) { + for (int c = 0; c < 9; c++) { + assertEquals(0, board.getCell(r, c), "Tablero debe estar vacío inicialmente"); + } + } + } + + @Test + void testPlaceAndRemoveStone() { + board.placeStone(0, 0, 1); // Colocar piedra negra + assertEquals(1, board.getCell(0, 0)); + + board.removeStone(0, 0); + assertEquals(0, board.getCell(0, 0)); + } + + @Test + void testIsValid() { + assertTrue(board.isValid(0, 0)); + assertTrue(board.isValid(8, 8)); + assertFalse(board.isValid(-1, 0)); + assertFalse(board.isValid(9, 0)); + assertFalse(board.isValid(0, -1)); + assertFalse(board.isValid(0, 9)); + } + + @Test + void testIsEmpty() { + assertTrue(board.isEmpty(0, 0)); + + board.placeStone(0, 0, 1); + assertFalse(board.isEmpty(0, 0)); + } + + @Test + void testGetNeighbors() { + // Centro: debe tener 4 vecinos + var neighbors = board.getNeighbors(4, 4); + assertEquals(4, neighbors.size()); + + // Esquina: debe tener 2 vecinos + neighbors = board.getNeighbors(0, 0); + assertEquals(2, neighbors.size()); + } + + @Test + void testCountLibertiesForSingleStone() { + board.placeStone(4, 4, 1); + int liberties = board.countLibertiesForGroup(4, 4); + assertEquals(4, liberties, "Piedra aislada debe tener 4 libertades"); + } + + @Test + void testCountLibertiesForConnectedGroup() { + // Grupo de 2 piedras conectadas + board.placeStone(4, 4, 1); + board.placeStone(4, 5, 1); + + int liberties = board.countLibertiesForGroup(4, 4); + assertEquals(6, liberties, "Grupo de 2 debe tener 6 libertades (sin contar duplicadas)"); + } + + @Test + void testCountLibertiesWithBlockedGroup() { + // Piedra negra rodeada parcialmente + board.placeStone(4, 4, 1); + board.placeStone(3, 4, 2); // Blanco arriba + board.placeStone(5, 4, 2); // Blanco abajo + + int liberties = board.countLibertiesForGroup(4, 4); + assertEquals(2, liberties, "Piedra con 2 lados bloqueados debe tener 2 libertades"); + } + + @Test + void testGetGroup() { + // Crear grupo de 3 piedras conectadas (L-shape) + board.placeStone(4, 4, 1); + board.placeStone(4, 5, 1); + board.placeStone(5, 5, 1); + + Set group = board.getGroup(4, 4); + assertEquals(3, group.size(), "Grupo debe tener 3 piedras"); + assertTrue(group.contains("4,4")); + assertTrue(group.contains("4,5")); + assertTrue(group.contains("5,5")); + } + + @Test + void testCaptureGroupWithoutLiberties() { + // Rodear completamente un grupo blanco + // . . W . . + // . N N N . + // . W W W . + board.placeStone(0, 2, 2); // Blanco arriba + board.placeStone(1, 1, 1); // Negro izq + board.placeStone(1, 2, 1); // Negro centro + board.placeStone(1, 3, 1); // Negro der + board.placeStone(2, 1, 2); // Blanco izq + board.placeStone(2, 2, 2); // Blanco centro + board.placeStone(2, 3, 2); // Blanco der + + // Cerrar: Negro coloca en (0,3) capturando Blanco + board.placeStone(0, 3, 1); + board.placeStone(1, 0, 1); // Pared izquierda + board.placeStone(0, 1, 1); // Pared arriba-izq + + int captured = board.captureGroupsWithoutLiberties(); + assertTrue(captured > 0, "Debe haber capturado piedras"); + + // Verificar que las piedras blancas fueron removidas + assertEquals(0, board.getCell(0, 2), "Piedra blanca (0,2) debe estar capturada"); + } + + @Test + void testBoardClone() { + board.placeStone(0, 0, 1); + board.placeStone(1, 1, 2); + + Board cloned = board.clone(); + assertEquals(board.getCell(0, 0), cloned.getCell(0, 0)); + assertEquals(board.getCell(1, 1), cloned.getCell(1, 1)); + + // Modificar el clon no debe afectar el original + cloned.placeStone(2, 2, 1); + assertEquals(0, board.getCell(2, 2), "Original no debe ser afectado"); + assertEquals(1, cloned.getCell(2, 2), "Clon debe tener el cambio"); + } + + @Test + void testBoardEquals() { + Board board2 = new Board(); + assertTrue(board.equals(board2), "Dos tableros vacíos deben ser iguales"); + + board.placeStone(0, 0, 1); + assertFalse(board.equals(board2), "Tableros con diferentes estados deben ser distintos"); + + board2.placeStone(0, 0, 1); + assertTrue(board.equals(board2), "Tableros con mismo estado deben ser iguales"); + } +} + diff --git a/src/test/java/es/iesquevedo/service/GameServiceTest.java b/src/test/java/es/iesquevedo/service/GameServiceTest.java index b1168d5..d4d4a90 100644 --- a/src/test/java/es/iesquevedo/service/GameServiceTest.java +++ b/src/test/java/es/iesquevedo/service/GameServiceTest.java @@ -4,6 +4,7 @@ import es.iesquevedo.exception.PlayerNotInTurnException; import es.iesquevedo.model.Game; import es.iesquevedo.model.GameState; +import es.iesquevedo.model.Move; import es.iesquevedo.model.Player; import es.iesquevedo.service.impl.GameServiceImpl; import org.junit.jupiter.api.BeforeEach; @@ -102,8 +103,9 @@ void testExecuteMoveValidatesPlayerTurn() { final Game startedGame = gameService.startGame(game.getId()); // Intenta mover con player2 pero es turno de player1 + Move move = new Move(player2.getId(), 0, 0); assertThrows(PlayerNotInTurnException.class, - () -> gameService.executeMove(startedGame.getId(), player2.getId(), null)); + () -> gameService.executeMove(startedGame.getId(), player2.getId(), move)); } @Test @@ -115,8 +117,9 @@ void testExecuteMoveValidatesPlayerAlive() { // Marca player1 como muerto startedGame.getCurrentPlayer().setAlive(false); + Move move = new Move(player1.getId(), 0, 0); assertThrows(InvalidMoveException.class, - () -> gameService.executeMove(startedGame.getId(), player1.getId(), null)); + () -> gameService.executeMove(startedGame.getId(), player1.getId(), move)); } @Test diff --git a/src/test/java/es/iesquevedo/service/impl/GameServiceImplExecuteMoveTest.java b/src/test/java/es/iesquevedo/service/impl/GameServiceImplExecuteMoveTest.java new file mode 100644 index 0000000..5c57e67 --- /dev/null +++ b/src/test/java/es/iesquevedo/service/impl/GameServiceImplExecuteMoveTest.java @@ -0,0 +1,147 @@ +package es.iesquevedo.service.impl; + +import es.iesquevedo.model.Game; +import es.iesquevedo.model.GameState; +import es.iesquevedo.model.Move; +import es.iesquevedo.model.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GameServiceImplExecuteMoveTest { + + private GameServiceImpl gameService; + private Game game; + private Player player1; + private Player player2; + + @BeforeEach + void setUp() { + gameService = new GameServiceImpl(); + player1 = new Player("p1", "Negro"); + player2 = new Player("p2", "Blanco"); + + game = gameService.createGame("testGame", player1); + gameService.joinGame(game.getId(), player2); + game = gameService.startGame(game.getId()); + } + + @Test + void testPlaceStoneChangesBoard() { + Move move = new Move("p1", 0, 0); + Game result = gameService.executeMove(game.getId(), "p1", move); + + assertEquals(1, result.getBoard().getCell(0, 0)); // Negro (1) en (0,0) + assertEquals(1, result.getMoves().size()); + assertEquals(1, result.getCurrentPlayerIndex()); // Cambió a Blanco + } + + @Test + void testDoublPassEndsGame() { + // Negro pasa + Move pass1 = new Move("p1", true); + game = gameService.executeMove(game.getId(), "p1", pass1); + assertEquals(1, game.getConsecutivePasses()); + assertEquals(GameState.IN_PROGRESS, game.getState()); + + // Blanco pasa + Move pass2 = new Move("p2", true); + game = gameService.executeMove(game.getId(), "p2", pass2); + assertEquals(GameState.FINISHED, game.getState()); + assertNotNull(game.getWinnerPlayerId()); + } + + @Test + void testCaptureRemovesPieces() { + // Crear una situación de captura correcta: + // Rodear una piedra blanca solitaria en (3,3) con piedras negras + // N en (2,3) + Move m1 = new Move("p1", 2, 3); + game = gameService.executeMove(game.getId(), "p1", m1); + + // B en (3,3) - la piedra que será capturada + Move m2 = new Move("p2", 3, 3); + game = gameService.executeMove(game.getId(), "p2", m2); + + // N en (4,3) + Move m3 = new Move("p1", 4, 3); + game = gameService.executeMove(game.getId(), "p1", m3); + + // B pasa + Move m4 = new Move("p2", true); + game = gameService.executeMove(game.getId(), "p2", m4); + + // N en (3,2) + Move m5 = new Move("p1", 3, 2); + game = gameService.executeMove(game.getId(), "p1", m5); + + // B pasa + Move m6 = new Move("p2", true); + game = gameService.executeMove(game.getId(), "p2", m6); + + // N en (3,4) y captura la piedra blanca en (3,3) + Move m7 = new Move("p1", 3, 4); + game = gameService.executeMove(game.getId(), "p1", m7); + + // Verificar que la piedra blanca fue capturada + assertEquals(0, game.getBoard().getCell(3, 3), + "La piedra blanca en (3,3) debe estar capturada (rodeada por Negro)"); + } + + @Test + void testConsecutivePassesResetAfterMove() { + // Negro pasa + Move pass1 = new Move("p1", true); + game = gameService.executeMove(game.getId(), "p1", pass1); + assertEquals(1, game.getConsecutivePasses()); + + // Blanco juega (no pasa) + Move move = new Move("p2", 0, 0); + game = gameService.executeMove(game.getId(), "p2", move); + assertEquals(0, game.getConsecutivePasses(), "Los pases consecutivos deben resetear con un movimiento"); + } + + @Test + void testMoveRecordedInHistory() { + Move move1 = new Move("p1", 0, 0); + game = gameService.executeMove(game.getId(), "p1", move1); + assertEquals(1, game.getMoves().size()); + + Move move2 = new Move("p2", 1, 1); + game = gameService.executeMove(game.getId(), "p2", move2); + assertEquals(2, game.getMoves().size()); + } + + @Test + void testTurnAlternatesCorrectly() { + assertEquals(0, game.getCurrentPlayerIndex(), "Debe empezar con Negro (índice 0)"); + + Move move1 = new Move("p1", 0, 0); + game = gameService.executeMove(game.getId(), "p1", move1); + assertEquals(1, game.getCurrentPlayerIndex(), "Debe cambiar a Blanco (índice 1)"); + + Move move2 = new Move("p2", 1, 1); + game = gameService.executeMove(game.getId(), "p2", move2); + assertEquals(0, game.getCurrentPlayerIndex(), "Debe cambiar a Negro (índice 0)"); + } + + @Test + void testPassDoesNotPlaceStone() { + Move pass = new Move("p1", true); + game = gameService.executeMove(game.getId(), "p1", pass); + + // El tablero debe seguir vacío + for (int r = 0; r < 9; r++) { + for (int c = 0; c < 9; c++) { + assertEquals(0, game.getBoard().getCell(r, c), "Tablero debe estar vacío después de pase"); + } + } + } +} + + + diff --git a/src/test/java/es/iesquevedo/service/impl/InazumaGoMoveValidatorTest.java b/src/test/java/es/iesquevedo/service/impl/InazumaGoMoveValidatorTest.java new file mode 100644 index 0000000..ca8399a --- /dev/null +++ b/src/test/java/es/iesquevedo/service/impl/InazumaGoMoveValidatorTest.java @@ -0,0 +1,112 @@ +package es.iesquevedo.service.impl; + +import es.iesquevedo.exception.InvalidMoveException; +import es.iesquevedo.exception.PlayerNotInTurnException; +import es.iesquevedo.model.Board; +import es.iesquevedo.model.Game; +import es.iesquevedo.model.GameState; +import es.iesquevedo.model.Move; +import es.iesquevedo.model.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class InazumaGoMoveValidatorTest { + + private InazumaGoMoveValidator validator; + private Game game; + private Player player1; + private Player player2; + + @BeforeEach + void setUp() { + validator = new InazumaGoMoveValidator(); + player1 = new Player("p1", "Negro"); + player2 = new Player("p2", "Blanco"); + game = new Game("test", player1); + game.addPlayer(player2); + game.start(); + } + + @Test + void testValidPlyacementMove() { + Move move = new Move("p1", 0, 0); + assertDoesNotThrow(() -> validator.validateMove(game, move)); + } + + @Test + void testValidPassMove() { + Move move = new Move("p1", true); + assertDoesNotThrow(() -> validator.validateMove(game, move)); + } + + @Test + void testNotPlayerTurnThrows() { + Move move = new Move("p2", 0, 0); // Blanco intenta jugar pero es turno de Negro + assertThrows(PlayerNotInTurnException.class, () -> validator.validateMove(game, move)); + } + + @Test + void testGameNotInProgressThrows() { + game.setState(GameState.FINISHED); + Move move = new Move("p1", 0, 0); + assertThrows(InvalidMoveException.class, () -> validator.validateMove(game, move)); + } + + @Test + void testPositionOutOfBoundsThrows() { + Move move = new Move("p1", 9, 0); // Fuera del tablero 9x9 + assertThrows(InvalidMoveException.class, () -> validator.validateMove(game, move)); + + Move move2 = new Move("p1", -1, 5); + assertThrows(InvalidMoveException.class, () -> validator.validateMove(game, move2)); + } + + @Test + void testOccupiedPositionThrows() { + Board board = game.getBoard(); + board.placeStone(0, 0, 1); // Colocar piedra negra + + Move move = new Move("p1", 0, 0); + assertThrows(InvalidMoveException.class, () -> validator.validateMove(game, move)); + } + + @Test + void testSuicideMoveThrows() { + // Crear una situación de suicidio: piedra rodeada sin libertades + Board board = game.getBoard(); + // Rodear posición (2,2) de manera que no haya libertades + board.placeStone(1, 2, 2); // Blanco arriba + board.placeStone(3, 2, 2); // Blanco abajo + board.placeStone(2, 1, 2); // Blanco izquierda + board.placeStone(2, 3, 2); // Blanco derecha + + Move move = new Move("p1", 2, 2); // Negro intenta colocar en centro rodeado + assertThrows(InvalidMoveException.class, () -> validator.validateMove(game, move)); + } + + @Test + void testCaptureMovesAllowed() { + // Crear situación donde Negro captura: piedra blanca rodeada + Board board = game.getBoard(); + board.placeStone(0, 1, 2); // Blanco en (0,1) + board.placeStone(1, 0, 1); // Negro en (1,0) + board.placeStone(1, 2, 1); // Negro en (1,2) + board.placeStone(2, 1, 1); // Negro en (2,1) + + // Negro coloca en (0,0) capturando a Blanco + Move move = new Move("p1", 0, 0); + assertDoesNotThrow(() -> validator.validateMove(game, move)); + } + + @Test + void testSecondPlayerTurnAfterFirst() { + game.nextTurn(); // Cambiar a turno de Blanco + + Move move = new Move("p2", 1, 1); + assertDoesNotThrow(() -> validator.validateMove(game, move)); + } +} +