diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml
index 9e9dc17..b5d06bc 100644
--- a/.idea/copilotDiffState.xml
+++ b/.idea/copilotDiffState.xml
@@ -6,9 +6,117 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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));
+ }
+}
+