diff --git a/.gitignore b/.gitignore
index 6e637a6..be95506 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,6 @@ target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
-.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..377d957
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml
new file mode 100644
index 0000000..e3a4679
--- /dev/null
+++ b/.idea/copilotDiffState.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TEST_GAME.md b/TEST_GAME.md
new file mode 100644
index 0000000..733ec62
--- /dev/null
+++ b/TEST_GAME.md
@@ -0,0 +1,159 @@
+# 🎮 Prueba de la Vista de Partida - InazumaGo
+
+## Cómo ejecutar y probar la aplicación
+
+### Opción 1: Ejecutar desde el IDE (JetBrains IntelliJ IDEA)
+
+1. Abre el proyecto en JetBrains IDEA
+2. Navega a `src/main/java/es/iesquevedo/MainGUI.java`
+3. Haz clic derecho → "Run 'MainGUI.main()'"
+4. La aplicación mostrará la pantalla de Login
+
+### Opción 2: Ejecutar desde línea de comandos
+
+```bash
+cd C:\Users\Usuario\IdeaProjects\InazumaGo
+mvn compile
+java -cp target/classes es.iesquevedo.MainGUI
+```
+
+---
+
+## Flujo de Prueba
+
+### Paso 1: Pantalla de Login
+- Verás la pantalla de Login con campos de Email y Contraseña
+- Ingresa cualquier email (ej: `usuario@example.com`)
+- Ingresa cualquier contraseña (ej: `password123`)
+- Haz clic en "Iniciar sesión"
+
+### Paso 2: Pantalla de Partida (Nueva Vista)
+Tras login exitoso, verás la nueva vista de partida con:
+
+#### 📊 Encabezado (Top)
+- **Jugador 1 (Negro)**
+ - Nombre: usuario@example.com
+ - Puntos: 0
+ - Tiempo: 00:00
+
+- **VS** (separador visual)
+
+- **Jugador 2 (Blanco)**
+ - Nombre: Oponente
+ - Puntos: 0
+ - Tiempo: 00:00
+
+- **Turno Actual**: Turno: usuario@example.com
+
+#### 🎯 Centro (Canvas)
+- Tablero 19x19 con líneas de cuadrícula
+- Haz clic en cualquier intersección para colocar una piedra
+- Las piedras aparecerán en negro (Jugador 1) o blanco (Jugador 2)
+
+#### 🎮 Controles (Bottom)
+- **Pasar turno**: Cambia de jugador
+- **Deshacer**: No disponible en esta versión
+- **Rendirse**: Marca fin de la partida con ganador
+- **Volver al menú**: Cierra la ventana de partida
+
+---
+
+## Características Implementadas ✅
+
+### Vista FXML (Game.fxml)
+- ✅ Diseño BorderPane con 3 secciones (Top, Center, Bottom)
+- ✅ Encabezado con información de jugadores
+- ✅ Canvas para dibujar el tablero 19x19
+- ✅ Botones de control con eventos onAction
+- ✅ Labels dinámicos para puntuación y tiempo
+
+### Controlador (GameController.java)
+- ✅ Inicialización del tablero vacío (Stone[19][19])
+- ✅ Dibujo del tablero con líneas de cuadrícula
+- ✅ Detección de clics del ratón en el Canvas
+- ✅ Colocación de piedras (negro/blanco)
+- ✅ Control de turnos alternantes
+- ✅ Cálculo de puntuación en tiempo real
+- ✅ Temporizador con AnimationTimer que cuenta segundos/minutos
+- ✅ Métodos para acciones (pasar turno, rendirse)
+- ✅ Inyección de nombres de jugadores
+- ✅ Inyección de puntuaciones iniciales
+
+### Navegación (LoginController.java modificado)
+- ✅ Carga automática de Game.fxml tras login exitoso
+- ✅ Paso de nombres de jugadores al GameController
+- ✅ Cambio del título de la ventana a "InazumaGo - Partida"
+
+---
+
+## Pruebas Recomendadas
+
+### Prueba 1: Colocar piedras
+1. Haz clic en varias intersecciones del tablero
+2. Verifica que aparecen piedras negras (jugador 1) y blancas (jugador 2) alternadamente
+3. Comprueba que no se pueden colocar dos piedras en la misma posición
+
+**Resultado esperado**: Las piedras aparecen correctamente en color y posición
+
+### Prueba 2: Cambio de turno
+1. Coloca una piedra
+2. Verifica que el turno cambió al otro jugador
+3. Comprueba que el color de la siguiente piedra es diferente
+
+**Resultado esperado**: Los turnos se alternan correctamente
+
+### Prueba 3: Puntuación
+1. Coloca varias piedras
+2. Verifica que el contador de puntos aumenta para cada jugador
+
+**Resultado esperado**: Cada piedra colocada suma 1 punto al jugador
+
+### Prueba 4: Temporizador
+1. Espera 10-20 segundos
+2. Verifica que el tiempo avanza (HH:MM)
+3. Coloca una piedra y mira que el temporizador del otro jugador comienza
+
+**Resultado esperado**: El tiempo se actualiza correctamente por segundo
+
+### Prueba 5: Pasar turno
+1. Haz clic en "Pasar turno"
+2. Verifica que cambia el jugador actual sin colocar piedra
+
+**Resultado esperado**: El turno cambia, pero no hay nueva piedra
+
+### Prueba 6: Rendirse
+1. Haz clic en "Rendirse"
+2. Verifica que aparece un mensaje de ganador
+
+**Resultado esperado**: Se muestra "[Ganador] ganó. [Perdedor] se rindió"
+
+---
+
+## Archivos Generados
+
+```
+✅ src/main/resources/fxml/Game.fxml (4,188 bytes)
+✅ src/main/java/es/iesquevedo/controller/GameController.java (8,447 bytes)
+📝 src/main/java/es/iesquevedo/controller/LoginController.java (modificado)
+```
+
+## Estado de Compilación
+
+```
+✅ BUILD SUCCESS
+✅ GameController.class compilado
+✅ GameController$Stone.class compilado
+✅ GameController$1.class compilado (clase interna anónima)
+```
+
+---
+
+## Notas Técnicas
+
+- **Framework**: JavaFX 21.0.2
+- **Compilador**: Maven Compiler Plugin 3.13.0 con `javac`
+- **Versión Java**: 21 (LTS)
+- **Patrón de Diseño**: MVC (Model-View-Controller)
+- **Threading**: AnimationTimer para actualización de UI en tiempo real
+
+
diff --git a/ci/pipeline.yml b/ci/pipeline.yml
index 65feacb..abe7186 100644
--- a/ci/pipeline.yml
+++ b/ci/pipeline.yml
@@ -13,11 +13,12 @@ jobs:
image: eclipse-temurin:21
steps:
- checkout
+ - run: java -version
- restore_cache:
key: maven-cache
paths:
- ~/.m2/repository
- - run: ./mvnw -B -DskipTests=false test
+ - run: ./mvnw -B clean test
- save_cache:
key: maven-cache
paths:
diff --git a/doc/firebase-setup.md b/doc/firebase-setup.md
new file mode 100644
index 0000000..a1ea7ed
--- /dev/null
+++ b/doc/firebase-setup.md
@@ -0,0 +1,149 @@
+# Configuración Firebase para InazumaGo
+
+## Estado Actual
+- ✅ Repositorio HTTP en OkHttp (FirebaseMainRepository) implementado
+- ✅ AuthService mock para desarrollo
+- ✅ Tests básicos pasando (sin dependencia de Firebase real)
+- ⏳ **Falta: Configurar Firebase Console**
+
+---
+
+## Pasos para Configurar Firebase (10-15 min)
+
+### 1. Crear Proyecto en Firebase Console
+1. Ve a **https://console.firebase.google.com**
+2. Click **"Crear proyecto"**
+3. Nombre: `InazumaGo`
+4. Deshabilita Analytics (por ahora, opcional)
+5. Click **"Crear"**
+
+### 2. Habilitar Realtime Database
+1. En el proyecto, ve a **"Realtime Database"** (en el menú lateral)
+2. Click **"Crear base de datos"**
+3. Ubicación: **Europe (europe-west1)** o **us-central1**
+4. Modo: **Start in test mode** (por ahora, abierta para desarrollo)
+5. Click **"Crear"**
+
+Firebase generará una URL como:
+```
+https://inazumago-abc123.firebaseio.com
+```
+
+### 3. Copiar URL a `application.properties`
+
+**Archivo:** `src/main/resources/application.properties`
+
+```properties
+# Firebase URL (sin .json)
+firebase.url=https://inazumago-abc123.firebaseio.com
+firebase.timeout.seconds=30
+firebase.auth.token=
+```
+
+### 4. (Alternativa) Usar Variable de Entorno
+
+```powershell
+# PowerShell
+$env:FIREBASE_URL="https://inazumago-abc123.firebaseio.com"
+```
+
+O en `.env`:
+```
+FIREBASE_URL=https://inazumago-abc123.firebaseio.com
+FIREBASE_AUTH_TOKEN=
+```
+
+### 5. Reglas de Seguridad RTDB (Desarrollo)
+
+**Para DESARROLLO:** En Firebase Console → Realtime Database → Rules
+
+```json
+{
+ "rules": {
+ ".read": true,
+ ".write": true,
+ "games": {
+ "$gameId": {
+ ".validate": "newData.hasChild('players')",
+ "players": {
+ ".validate": "newData.val().length() <= 2"
+ },
+ "moves": {
+ ".validate": "!data.exists() || newData.val().length() >= data.val().length()"
+ }
+ }
+ }
+ }
+}
+```
+
+**Para PRODUCCIÓN:** (después, con autenticación real)
+
+```json
+{
+ "rules": {
+ ".read": "auth != null",
+ ".write": "auth != null",
+ "games": {
+ "$gameId": {
+ ".validate": "newData.hasChildren(['id', 'players', 'status'])",
+ "players": {
+ ".validate": "newData.val().length() >= 2 && newData.val().length() <= 2"
+ },
+ "moves": {
+ ".validate": "root.child('games').child($gameId).child('currentTurn') != null"
+ }
+ }
+ }
+ }
+}
+```
+
+---
+
+## Testing con Firebase Real
+
+**Una vez configurado, Red puede hacer:**
+
+1. Tests WireMock (ya listos en `FirebaseMainRepositoryTest`)
+2. Tests contra Firebase real (descomenta tras configurar):
+
+```java
+@Test
+void testCreateGameAgainstFirebase() throws Exception {
+ GameDto game = new GameDto("game-real-123", "Test",
+ Arrays.asList("p1", "p2"), "IN_PROGRESS", System.currentTimeMillis());
+
+ FirebaseMainRepository repo = new FirebaseMainRepository(
+ System.getenv("FIREBASE_URL")
+ );
+ CompletableFuture result = repo.createGame(game);
+ GameDto created = result.get();
+
+ assertNotNull(created);
+ assertEquals("game-real-123", created.getId());
+}
+```
+
+---
+
+## Checklist para Red
+
+- [ ] Firebase Console: Proyecto creado
+- [ ] RTDB: Base de datos creada (EU o US)
+- [ ] RTDB URL: Copiada a `application.properties`
+- [ ] Reglas: Aplicadas (desarrollo first)
+- [ ] Tests: Ejecutados contra Firebase real (opcional ahora, hacer después)
+
+---
+
+## Próximo Paso (E2-US3)
+
+Cuando Firebase esté configurado, Red implementa:
+- AuthService real (Firebase Authentication)
+- Integración de tokens en peticiones
+- SSE o WebSocket para listeners en tiempo real
+
+**Responsable:** Red Team
+**Estimación:** 1-2 sprints
+**Bloqueador:** Firebase configurado
diff --git a/doc/game-implementation-guide.md b/doc/game-implementation-guide.md
new file mode 100644
index 0000000..33035bf
--- /dev/null
+++ b/doc/game-implementation-guide.md
@@ -0,0 +1,165 @@
+# Guía para Implementar una Partida Jugable (Motor + UI)
+
+## Estado Actual de Red
+✅ FirebaseMainRepository (cliente HTTP OkHttp)
+✅ AuthService (mock para desarrollo)
+✅ 34 tests pasando
+
+---
+
+## Lo que Motor Necesita Hacer
+
+### 1. **Sincronización Optimistic + Rollback** (E2-US4-T3)
+Crear en `GameService`:
+```java
+public CompletableFuture executeMove(String gameId, MoveData move) {
+ // 1. Aplicar movimiento localmente (optimistic)
+ localGame.getMoves().add(move);
+
+ // 2. Enviar a Firebase
+ MovePayload payload = new MovePayload(Arrays.asList(move), System.currentTimeMillis());
+ return mainRepository.writeMoveMultiPath(gameId, payload)
+ .exceptionally(e -> {
+ // 3. Rollback si falla (403, timeout, etc.)
+ localGame.getMoves().remove(move);
+ throw new RuntimeException("Movimiento rechazado: " + e.getMessage());
+ });
+}
+```
+
+### 2. **Reglas de Validación de Movimientos** (E3-US2)
+Crear interfaz `MoveValidator`:
+```java
+public interface MoveValidator {
+ void validateMove(Game game, MoveData move) throws InvalidMoveException;
+}
+```
+
+Implementación:
+```java
+public class InazumaGoMoveValidator implements MoveValidator {
+ @Override
+ public void validateMove(Game game, MoveData move) throws InvalidMoveException {
+ // Validar: turno, posición, acción permitida, etc.
+ if (!isPlayerTurn(game, move.getPlayerId())) {
+ throw new InvalidMoveException("No es tu turno");
+ }
+ if (!isValidPosition(move.getPosition())) {
+ throw new InvalidMoveException("Posición fuera del campo");
+ }
+ }
+}
+```
+
+### 3. **Integración en GameService**
+```java
+public CompletableFuture executeMove(String gameId, MoveData move) {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ moveValidator.validateMove(currentGame, move);
+ // Enviar a Firebase...
+ mainRepository.writeMoveMultiPath(gameId, ...);
+ currentGame.nextTurn(); // Cambiar turno
+ } catch (InvalidMoveException e) {
+ throw new RuntimeException(e);
+ }
+ });
+}
+```
+
+---
+
+## Lo que UI Necesita Hacer
+
+### 1. **Pantalla de Partida**
+Crear `GameController.fxml` + `GameController.java`:
+```java
+@FXML private Label playerNameLabel;
+@FXML private Button kickButton, passButton;
+@FXML private Canvas gameCanvas; // Dibujar campo
+
+private GameService gameService;
+private String currentGameId;
+
+@FXML private void onKickPressed() {
+ MoveData move = new MoveData(playerName, "KICK", selectedPosition);
+ gameService.executeMove(currentGameId, move)
+ .exceptionally(e -> {
+ showError("Movimiento rechazado: " + e.getMessage());
+ return null;
+ });
+}
+```
+
+### 2. **Listeners en Tiempo Real**
+```java
+private void subscribeToMoves(String gameId) {
+ String listenerId = gameService.addMovesListener(gameId, moves -> {
+ // Actualizar UI con nuevos movimientos
+ Platform.runLater(() -> renderMoves(moves));
+ });
+}
+```
+
+### 3. **Flujo de Creación de Partida**
+```
+1. Pantalla de login (usa AuthService)
+2. Pantalla de lobby (listar partidas o crear nueva)
+3. Pantalla de espera (esperando segundo jugador)
+4. Pantalla de partida (turnos, movimientos)
+5. Pantalla de resultado (victoria/derrota/abandono)
+```
+
+---
+
+## Para que Compile Todo Junto
+
+Motor debe crear:
+- [ ] `MoveValidator` interface + `InazumaGoMoveValidator`
+- [ ] Método `executeMove()` en `GameService`
+- [ ] Tests de `MoveValidator` (qué movimientos son válidos)
+
+UI debe crear:
+- [ ] `GameController.fxml` (diseño básico del campo)
+- [ ] `GameController.java` (wiring con GameService)
+- [ ] `AuthenticationController.fxml` + `.java` (login mock)
+- [ ] Actualizar `MainGUI.java` para cargar `AuthenticationController` primero
+
+---
+
+## Orden de Implementación (Rápido)
+
+**Día 1 - Motor:**
+1. `MoveValidator` interface
+2. `InazumaGoMoveValidator` (validar turno, posición)
+3. `executeMove()` en `GameService` (con optimistic update + rollback)
+
+**Día 1 - UI:**
+1. `AuthenticationController` (login mock, reutiliza `AuthServiceImpl`)
+2. `GameController.fxml` (canvas con campo + botones)
+
+**Día 2 - Integración:**
+1. Flujo completo: login → crear partida → jugar
+2. Sync de movimientos (listeners)
+3. Tests E2E con stubs
+
+---
+
+## Firebase (Red) - Paralelo
+Mientras Motor y UI trabajan:
+1. Configura Firebase Console (10 min)
+2. Copia URL a `application.properties`
+3. Escribe reglas RTDB
+
+**No hace falta esperar a nada - Red ya dejó el código listo.**
+
+---
+
+## Checklist Final
+
+- [ ] Motor: `MoveValidator` + `executeMove()`
+- [ ] UI: `AuthenticationController` + `GameController`
+- [ ] Tests: Mínimo 3 tests de validación de movimientos
+- [ ] Firebase: Configurado en Console
+- [ ] Partida: Crear, jugar 2 turnos mínimo, ver resultado
+
diff --git a/pom.xml b/pom.xml
index 5175dcf..f587d8d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,6 +9,11 @@
1.0-SNAPSHOT
+ 21
+ UTF-8
+ 0.0.6
+ 21.0.2
+ windows-x86_64
21
21
UTF-8
@@ -46,10 +51,18 @@
9.8.0
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.11.0
+
+
+
com.google.code.gson
gson
- 2.11.0
+ 2.10.1
@@ -69,6 +82,14 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ ${maven.compiler.release}
+
+
org.apache.maven.plugins
maven-surefire-plugin
diff --git a/src/main/java/es/iesquevedo/GameTest.java b/src/main/java/es/iesquevedo/GameTest.java
new file mode 100644
index 0000000..18848bf
--- /dev/null
+++ b/src/main/java/es/iesquevedo/GameTest.java
@@ -0,0 +1,28 @@
+package es.iesquevedo;
+
+import es.iesquevedo.controller.GameController;
+import java.util.logging.Logger;
+
+public class GameTest {
+ private static final Logger LOGGER = Logger.getLogger(GameTest.class.getName());
+
+ public static void main(String[] args) {
+ LOGGER.info("Iniciando prueba del GameController...");
+
+ try {
+ // Crear instancia del controlador
+ GameController controller = new GameController();
+ LOGGER.info("GameController creado exitosamente");
+
+ // Verificar que se puede llamar a métodos
+ controller.setPlayerNames("Jugador1", "Jugador2");
+ controller.setInitialScores(0, 0);
+ LOGGER.info("Métodos del GameController funcionan correctamente");
+
+ LOGGER.info("✅ Prueba del GameController completada exitosamente");
+ } catch (Exception e) {
+ LOGGER.severe("❌ Error en la prueba del GameController: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/es/iesquevedo/Main.java b/src/main/java/es/iesquevedo/Main.java
index 169342a..60cdad3 100644
--- a/src/main/java/es/iesquevedo/Main.java
+++ b/src/main/java/es/iesquevedo/Main.java
@@ -3,6 +3,7 @@
import es.iesquevedo.config.AppConfig;
import es.iesquevedo.controller.HealthController;
import es.iesquevedo.controller.MainController;
+import es.iesquevedo.repository.MainRepository;
import es.iesquevedo.service.impl.MainServiceImpl;
import es.iesquevedo.ui.HealthUIAdapter;
import es.iesquevedo.ui.UIAdapter;
@@ -18,17 +19,17 @@ public static void main(String[] args) {
String firebaseUrl = System.getenv("FIREBASE_URL");
// Crear repositorio (Firebase si FIREBASE_URL está definido, sino InMemory)
- var repository = AppConfig.createMainRepository(firebaseUrl);
+ MainRepository repository = AppConfig.createMainRepository(firebaseUrl);
// Crear servicio y controlador
- var service = new MainServiceImpl(repository);
- var mainController = new MainController();
+ MainServiceImpl service = new MainServiceImpl(repository);
+ MainController mainController = new MainController();
mainController.setService(service);
// Adaptadores UI
- var ui = new UIAdapter(mainController);
- var healthController = new HealthController();
- var healthUi = new HealthUIAdapter(healthController);
+ UIAdapter ui = new UIAdapter(mainController);
+ HealthController healthController = new HealthController();
+ HealthUIAdapter healthUi = new HealthUIAdapter(healthController);
// Uso simple: saludar y comprobar estado
// Comprobar explícitamente si el nivel está habilitado y usar el formateo incorporado
diff --git a/src/main/java/es/iesquevedo/MainApp.java b/src/main/java/es/iesquevedo/MainApp.java
index 95ca182..307f4aa 100644
--- a/src/main/java/es/iesquevedo/MainApp.java
+++ b/src/main/java/es/iesquevedo/MainApp.java
@@ -3,6 +3,7 @@
import es.iesquevedo.config.AppConfig;
import es.iesquevedo.controller.HealthController;
import es.iesquevedo.controller.MainController;
+import es.iesquevedo.repository.MainRepository;
import es.iesquevedo.service.impl.MainServiceImpl;
import es.iesquevedo.ui.HealthUIAdapter;
import es.iesquevedo.ui.UIAdapter;
@@ -36,17 +37,17 @@ private static void runConsoleMode() {
String firebaseUrl = System.getenv("FIREBASE_URL");
// Crear repositorio (Firebase si FIREBASE_URL está definido, sino InMemory)
- var repository = AppConfig.createMainRepository(firebaseUrl);
+ MainRepository repository = AppConfig.createMainRepository(firebaseUrl);
// Crear servicio y controlador
- var service = new MainServiceImpl(repository);
- var mainController = new MainController();
+ MainServiceImpl service = new MainServiceImpl(repository);
+ MainController mainController = new MainController();
mainController.setService(service);
-
+//error, eliminar este comentario
// Adaptadores UI
- var ui = new UIAdapter(mainController);
- var healthController = new HealthController();
- var healthUi = new HealthUIAdapter(healthController);
+ UIAdapter ui = new UIAdapter(mainController);
+ HealthController healthController = new HealthController();
+ HealthUIAdapter healthUi = new HealthUIAdapter(healthController);
// Uso simple: saludar y comprobar estado
if (LOGGER.isLoggable(Level.INFO)) {
diff --git a/src/main/java/es/iesquevedo/MainGUI.java b/src/main/java/es/iesquevedo/MainGUI.java
index 9c49098..c5de527 100644
--- a/src/main/java/es/iesquevedo/MainGUI.java
+++ b/src/main/java/es/iesquevedo/MainGUI.java
@@ -17,27 +17,27 @@ public class MainGUI extends Application {
@Override
public void start(Stage primaryStage) {
try {
- // Cargar FXML de Login con su controlador
+ // Cargar pantalla de login
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/Login.fxml"));
- Parent root = (Parent) loader.load();
-
- // Obtener el controlador después de cargar el FXML
- LoginController loginController = loader.getController();
-
- // Inyectar el servicio de autenticación (mock para desarrollo)
- loginController.setAuthService(new AuthServiceMock());
-
- // Crear escena y mostrar ventana
- Scene scene = new Scene(root, 600, 400);
+ Parent root = loader.load();
+
+ // Configurar LoginController
+ LoginController controller = loader.getController();
+ controller.setAuthService(new AuthServiceMock());
+
+ Scene scene = new Scene(root, 500, 400);
primaryStage.setTitle("InazumaGo - Login");
primaryStage.setScene(scene);
primaryStage.show();
- if (LOGGER.isLoggable(Level.INFO)) {
- LOGGER.log(Level.INFO, "Aplicación JavaFX iniciada exitosamente (Login)");
- }
+ LOGGER.log(Level.INFO, "Aplicación JavaFX iniciada - Pantalla de login cargada");
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error al iniciar la aplicación", e);
+ e.printStackTrace();
}
}
+
+ public static void main(String[] args) {
+ launch(args);
+ }
}
diff --git a/src/main/java/es/iesquevedo/config/AppConfig.java b/src/main/java/es/iesquevedo/config/AppConfig.java
index f498e7d..48e2f60 100644
--- a/src/main/java/es/iesquevedo/config/AppConfig.java
+++ b/src/main/java/es/iesquevedo/config/AppConfig.java
@@ -27,10 +27,10 @@ private AppConfig() {
* proporciona una URL, se devuelve el repositorio orientado a Firebase.
*/
public static MainRepository createMainRepository(String firebaseUrl) {
- if (firebaseUrl == null || firebaseUrl.isBlank()) {
+ if (firebaseUrl == null || firebaseUrl.trim().isEmpty()) {
return new InMemoryMainRepository();
}
- return new FirebaseMainRepository(firebaseUrl);
+ return new FirebaseMainRepository(firebaseUrl, 30);
}
/**
@@ -44,7 +44,14 @@ public static MainRepository createInMemoryRepository() {
* Atajo para obtener la implementación orientada a Firebase (producción).
*/
public static MainRepository createFirebaseRepository(String firebaseUrl) {
- return new FirebaseMainRepository(firebaseUrl);
+ return new FirebaseMainRepository(firebaseUrl, 30);
+ }
+
+ /**
+ * Crea repositorio Firebase con timeout personalizado.
+ */
+ public static FirebaseMainRepository createFirebaseRepository(String firebaseUrl, int timeoutSeconds) {
+ return new FirebaseMainRepository(firebaseUrl, timeoutSeconds);
}
/**
diff --git a/src/main/java/es/iesquevedo/controller/GameController.java b/src/main/java/es/iesquevedo/controller/GameController.java
new file mode 100644
index 0000000..24b1b19
--- /dev/null
+++ b/src/main/java/es/iesquevedo/controller/GameController.java
@@ -0,0 +1,601 @@
+package es.iesquevedo.controller;
+
+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.impl.InazumaGoMoveValidator;
+import es.iesquevedo.exception.InvalidMoveException;
+import es.iesquevedo.exception.PlayerNotInTurnException;
+
+import javafx.animation.AnimationTimer;
+import javafx.fxml.FXML;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.control.Label;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.LinearGradient;
+import javafx.scene.paint.RadialGradient;
+import javafx.scene.paint.CycleMethod;
+import javafx.scene.paint.Stop;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.text.Font;
+import javafx.scene.text.TextAlignment;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class GameController {
+ private static final Logger LOGGER = Logger.getLogger(GameController.class.getName());
+ private static final int BOARD_SIZE = 9;
+ private static final int CELL_SIZE = 50;
+
+ @FXML private Label player1NameLabel;
+ @FXML private Label player1ScoreLabel;
+ @FXML private Label player1TimeLabel;
+ @FXML private Label player2NameLabel;
+ @FXML private Label player2ScoreLabel;
+ @FXML private Label player2TimeLabel;
+ @FXML private Label currentTurnLabel;
+ @FXML private Label statusLabel;
+ @FXML private Canvas boardCanvas;
+
+ private String player1Name = "Jugador 1";
+ private String player2Name = "Jugador 2";
+ private long player1TimeMs = 0;
+ private long player2TimeMs = 0;
+ private AnimationTimer gameTimer;
+ private long lastTime = 0;
+ private boolean gameEnded = false;
+
+ // Lógica real del juego
+ private Game game;
+ private InazumaGoMoveValidator moveValidator;
+ private boolean moveInProgress = false;
+
+ @FXML
+ public void initialize() {
+ LOGGER.log(Level.INFO, "GameController inicializado");
+
+ // Inicializar validador de movimientos
+ moveValidator = new InazumaGoMoveValidator();
+
+ // Crear juego con dos jugadores locales
+ Player player1 = new Player("1", player1Name);
+ Player player2 = new Player("2", player2Name);
+
+ game = new Game("Local Game", player1);
+ game.addPlayer(player2);
+ game.start();
+
+ updatePlayerInfo();
+ drawBoard();
+ boardCanvas.setOnMouseClicked(this::onBoardClick);
+ startGameTimer();
+ }
+
+ private void updatePlayerInfo() {
+ player1NameLabel.setText(player1Name + " (Negro)");
+ player2NameLabel.setText(player2Name + " (Blanco)");
+ updateScores();
+ updateCurrentTurn();
+ }
+
+ private void updateScores() {
+ // Calcular puntuación simple: contar piedras por color
+ Board board = game.getBoard();
+ int blackStones = 0;
+ int whiteStones = 0;
+
+ for (int r = 0; r < BOARD_SIZE; r++) {
+ for (int c = 0; c < BOARD_SIZE; c++) {
+ int cell = board.getCell(r, c);
+ if (cell == 1) blackStones++;
+ else if (cell == 2) whiteStones++;
+ }
+ }
+
+ // Komi (ventaja blanca): 5.5
+ double blackScore = blackStones;
+ double whiteScore = whiteStones + 5.5;
+
+ player1ScoreLabel.setText(String.format("Puntos: %.1f", blackScore));
+ player2ScoreLabel.setText(String.format("Puntos: %.1f", whiteScore));
+ }
+
+ private void updateCurrentTurn() {
+ Player currentPlayer = game.getCurrentPlayer();
+ String turn = currentPlayer != null ? currentPlayer.getName() : player1Name;
+ currentTurnLabel.setText("Turno: " + turn);
+ }
+
+ private void drawBoard() {
+ GraphicsContext gc = boardCanvas.getGraphicsContext2D();
+ double boardStartX = getBoardStartX();
+ double boardStartY = getBoardStartY();
+ double boardEndX = getBoardEndX();
+ double boardEndY = getBoardEndY();
+ double boardSpan = (BOARD_SIZE - 1) * CELL_SIZE;
+
+ // Fondo de madera con degradado ligero (más realista)
+ LinearGradient wood = new LinearGradient(
+ 0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
+ new Stop(0, Color.web("#D2A679")),
+ new Stop(0.5, Color.web("#C37E3A")),
+ new Stop(1, Color.web("#B06A2E"))
+ );
+ gc.setFill(wood);
+ gc.fillRect(0, 0, boardCanvas.getWidth(), boardCanvas.getHeight());
+
+ // Granulado / vetas finas
+ gc.setStroke(Color.color(0.15, 0.07, 0.03, 0.06));
+ gc.setLineWidth(0.8);
+ for (int i = 0; i < (int) boardCanvas.getWidth(); i += 6) {
+ double offset = (i % 24) * 0.3;
+ gc.beginPath();
+ gc.moveTo(0, (i + offset) % boardCanvas.getHeight());
+ gc.bezierCurveTo(boardCanvas.getWidth() * 0.25, (i + offset + 8) % boardCanvas.getHeight(),
+ boardCanvas.getWidth() * 0.75, (i + offset - 12 + boardCanvas.getHeight()) % boardCanvas.getHeight(),
+ boardCanvas.getWidth(), (i + offset) % boardCanvas.getHeight());
+ gc.stroke();
+ }
+
+ // Marco exterior con esquinas redondeadas perceptuales
+ gc.setStroke(Color.color(0.12, 0.06, 0.03, 1.0));
+ gc.setLineWidth(5);
+ double outerX = boardStartX - 4;
+ double outerY = boardStartY - 4;
+ double outerW = boardSpan + 8;
+ double outerH = boardSpan + 8;
+ gc.strokeRoundRect(outerX, outerY, outerW, outerH, 12, 12);
+
+ // Marco interior menos intenso
+ gc.setStroke(Color.color(0.36, 0.18, 0.08, 0.7));
+ gc.setLineWidth(1.5);
+ gc.strokeRoundRect(boardStartX - 1, boardStartY - 1,
+ boardSpan + 2, boardSpan + 2, 8, 8);
+
+ // Líneas del tablero, ligeramente suavizadas con doble trazo (sombra + línea)
+ for (int i = 0; i < BOARD_SIZE; i++) {
+ double y = getIntersectionY(i);
+ double x = getIntersectionX(i);
+
+ // sombra de la línea horizontal
+ gc.setStroke(Color.color(0, 0, 0, 0.12));
+ gc.setLineWidth(2.0);
+ gc.strokeLine(boardStartX + 0.8, y + 0.8, boardEndX + 0.8, y + 0.8);
+
+ // línea principal
+ gc.setStroke(Color.color(0.06, 0.03, 0.01, 1.0));
+ gc.setLineWidth(1.2);
+ gc.strokeLine(boardStartX, y, boardEndX, y);
+
+ // sombra vertical
+ gc.setStroke(Color.color(0, 0, 0, 0.12));
+ gc.setLineWidth(2.0);
+ gc.strokeLine(x + 0.8, boardStartY + 0.8, x + 0.8, boardEndY + 0.8);
+
+ // línea vertical principal
+ gc.setStroke(Color.color(0.06, 0.03, 0.01, 1.0));
+ gc.setLineWidth(1.2);
+ gc.strokeLine(x, boardStartY, x, boardEndY);
+ }
+
+ // Coordenadas alrededor del tablero
+ drawCoordinates(gc, boardStartX, boardStartY, boardEndX, boardEndY);
+
+ // Puntos hoshizashi mejor integrados
+ gc.setFill(Color.color(0.04, 0.02, 0.01, 1.0));
+ int[] stars = {2, 4, 6};
+ for (int i : stars) {
+ for (int j : stars) {
+ double px = getIntersectionX(i);
+ double py = getIntersectionY(j);
+ gc.fillOval(px - 3, py - 3, 6, 6);
+ gc.setStroke(Color.color(0, 0, 0, 0.18));
+ gc.setLineWidth(0.6);
+ gc.strokeOval(px - 4, py - 4, 8, 8);
+ }
+ }
+
+ // Dibujar piedras del tablero real (Board)
+ Board board = game.getBoard();
+ for (int r = 0; r < BOARD_SIZE; r++) {
+ for (int c = 0; c < BOARD_SIZE; c++) {
+ int cell = board.getCell(r, c);
+ if (cell == 1) { // Negro
+ drawStone(gc, r, c, 1);
+ } else if (cell == 2) { // Blanco
+ drawStone(gc, r, c, 2);
+ }
+ }
+ }
+ }
+
+ private void drawStone(GraphicsContext gc, int row, int col, int stoneColor) {
+ double x = getIntersectionX(col);
+ double y = getIntersectionY(row);
+ double radius = CELL_SIZE / 2 - 3;
+
+ if (stoneColor == 1) { // Negro
+ // sombra proyectada
+ gc.setFill(Color.color(0, 0, 0, 0.28));
+ gc.fillOval(x - radius + 2, y - radius + 3, radius * 1.9, radius * 1.9);
+
+ // gradiente radial para dar volumen
+ RadialGradient blackGrad = new RadialGradient(
+ 45, 0.1,
+ x - radius * 0.15, y - radius * 0.2,
+ radius * 1.05, false, CycleMethod.NO_CYCLE,
+ new Stop(0.0, Color.web("#6e6e6e")),
+ new Stop(0.4, Color.web("#222222")),
+ new Stop(1.0, Color.web("#000000"))
+ );
+ gc.setFill(blackGrad);
+ gc.fillOval(x - radius, y - radius, radius * 2, radius * 2);
+
+ // highlight suave (brillo)
+ RadialGradient shine = new RadialGradient(
+ 45, 0.2,
+ x - radius * 0.25, y - radius * 0.3,
+ radius * 0.6, false, CycleMethod.NO_CYCLE,
+ new Stop(0.0, Color.color(1, 1, 1, 0.9)),
+ new Stop(0.4, Color.color(1, 1, 1, 0.25)),
+ new Stop(1.0, Color.color(1, 1, 1, 0.0))
+ );
+ gc.setGlobalAlpha(1.0);
+ gc.setFill(shine);
+ gc.fillOval(x - radius * 0.2, y - radius * 0.25, radius * 1.2, radius * 1.2);
+
+ // borde ligero para definición
+ gc.setStroke(Color.color(0, 0, 0, 0.45));
+ gc.setLineWidth(0.6);
+ gc.strokeOval(x - radius, y - radius, radius * 2, radius * 2);
+
+ } else if (stoneColor == 2) { // Blanco
+ // piedra blanca: sombra más suave
+ gc.setFill(Color.color(0.06, 0.06, 0.06, 0.14));
+ gc.fillOval(x - radius + 1.8, y - radius + 2.6, radius * 1.95, radius * 1.95);
+
+ // base con degradado radial cálido
+ RadialGradient whiteGrad = new RadialGradient(
+ 45, 0.12,
+ x - radius * 0.12, y - radius * 0.18,
+ radius * 1.05, false, CycleMethod.NO_CYCLE,
+ new Stop(0.0, Color.web("#ffffff")),
+ new Stop(0.6, Color.web("#f2f2f2")),
+ new Stop(1.0, Color.web("#d1d1d1"))
+ );
+ gc.setFill(whiteGrad);
+ gc.fillOval(x - radius, y - radius, radius * 2, radius * 2);
+
+ // borde suave gris
+ gc.setStroke(Color.color(0.6, 0.6, 0.6, 0.6));
+ gc.setLineWidth(0.9);
+ gc.strokeOval(x - radius + 0.4, y - radius + 0.4, radius * 2 - 0.8, radius * 2 - 0.8);
+
+ // highlights blancos
+ RadialGradient whiteShine = new RadialGradient(
+ 45, 0.2,
+ x - radius * 0.2, y - radius * 0.25,
+ radius * 0.5, false, CycleMethod.NO_CYCLE,
+ new Stop(0.0, Color.color(1, 1, 1, 0.95)),
+ new Stop(0.6, Color.color(1, 1, 1, 0.45)),
+ new Stop(1.0, Color.color(1, 1, 1, 0.0))
+ );
+ gc.setFill(whiteShine);
+ gc.fillOval(x - radius * 0.15, y - radius * 0.18, radius * 0.95, radius * 0.95);
+
+ // textura sutil
+ gc.setFill(Color.color(0.8, 0.8, 0.8, 0.05));
+ gc.fillOval(x - radius + 2, y - radius + 2, radius * 1.4, radius * 1.4);
+ }
+ }
+
+ @FXML
+ private void onBoardClick(MouseEvent event) {
+ if (gameEnded || moveInProgress) {
+ return;
+ }
+
+ double x = event.getX();
+ double y = event.getY();
+ double boardStartX = getBoardStartX();
+ double boardStartY = getBoardStartY();
+
+ int col = (int) Math.round((x - boardStartX) / CELL_SIZE);
+ int row = (int) Math.round((y - boardStartY) / CELL_SIZE);
+
+ if (isValidPosition(row, col)) {
+ placeStone(row, col);
+ }
+ }
+
+ private boolean isValidPosition(int row, int col) {
+ return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
+ }
+
+ private void placeStone(int row, int col) {
+ if (moveInProgress || gameEnded) {
+ return;
+ }
+
+ moveInProgress = true;
+
+ try {
+ Player currentPlayer = game.getCurrentPlayer();
+
+ if (currentPlayer == null) {
+ statusLabel.setText("Error: sin jugador actual");
+ moveInProgress = false;
+ return;
+ }
+
+ // Crear movimiento con playerId
+ Move move = new Move(currentPlayer.getId(), row, col);
+
+ // Validar movimiento
+ moveValidator.validateMove(game, move);
+
+ // Guardar estado previo para detectar repetición
+ Board previousBoardState = game.getBoard().clone();
+
+ // Ejecutar movimiento en el tablero
+ Board board = game.getBoard();
+ int playerColor = (game.getCurrentPlayerIndex() == 0) ? 1 : 2; // 1=Negro, 2=Blanco
+ board.placeStone(row, col, playerColor);
+
+ // Capturar grupos enemigos sin libertades
+ int capturedCount = board.captureGroupsWithoutLiberties();
+
+ // Registrar movimiento
+ move.setCapturedCount(capturedCount);
+ game.getMoves().add(move);
+
+ // Detectar repetición de posición (se ignora la reversión por ahora)
+ if (game.getLastBoardState() != null && board.equals(game.getLastBoardState())) {
+ statusLabel.setText("Movimiento rechazado: repetición de posición");
+ moveInProgress = false;
+ return;
+ }
+
+ game.setLastBoardState(previousBoardState);
+
+ // Movimiento exitoso
+ statusLabel.setText("Movimiento realizado" + (capturedCount > 0 ? " (" + capturedCount + " piedras capturadas)" : ""));
+
+ // Cambiar turno
+ game.nextTurn();
+ game.resetConsecutivePasses();
+
+ updatePlayerInfo();
+ drawBoard();
+ moveInProgress = false;
+
+ LOGGER.log(Level.INFO, "Piedra colocada en [" + row + "," + col + "], capturadas: " + capturedCount);
+
+ } catch (InvalidMoveException | PlayerNotInTurnException ex) {
+ statusLabel.setText("Movimiento inválido: " + ex.getMessage());
+ moveInProgress = false;
+ LOGGER.log(Level.WARNING, "Error en movimiento: " + ex.getMessage());
+ } catch (Exception ex) {
+ statusLabel.setText("Error: " + ex.getMessage());
+ moveInProgress = false;
+ LOGGER.log(Level.SEVERE, "Error inesperado en movimiento: " + ex.getMessage());
+ }
+ }
+
+ @FXML
+ private void onPassTurn() {
+ if (gameEnded || moveInProgress) {
+ return;
+ }
+
+ moveInProgress = true;
+
+ try {
+ Player currentPlayer = game.getCurrentPlayer();
+ if (currentPlayer == null) {
+ statusLabel.setText("Error: sin jugador actual");
+ moveInProgress = false;
+ return;
+ }
+
+ // Crear movimiento de PASE
+ Move move = new Move(currentPlayer.getId(), true); // PASE
+
+ // Validar (pasadas sin validación especial)
+ moveValidator.validateMove(game, move);
+
+ // Registrar pase
+ game.getMoves().add(move);
+ game.incrementConsecutivePasses();
+
+ // Cambiar turno
+ game.nextTurn();
+
+ statusLabel.setText(currentPlayer.getName() + " pasó su turno. Pases consecutivos: " + game.getConsecutivePasses());
+
+ // Verificar doble pase = fin de partida
+ if (game.getConsecutivePasses() >= 2) {
+ endGame();
+ }
+
+ updateCurrentTurn();
+ moveInProgress = false;
+
+ LOGGER.log(Level.INFO, "Turno pasado. Pases consecutivos: " + game.getConsecutivePasses());
+ } catch (Exception ex) {
+ statusLabel.setText("Error al pasar: " + ex.getMessage());
+ moveInProgress = false;
+ }
+ }
+
+ @FXML
+ private void onUndo() {
+ statusLabel.setText("Deshacer no disponible en esta versión");
+ LOGGER.log(Level.INFO, "Deshacer solicitado");
+ }
+
+ @FXML
+ private void onSurrender() {
+ gameEnded = true;
+ game.setState(GameState.FINISHED);
+
+ Player winner = game.getPlayers().get((game.getCurrentPlayerIndex() + 1) % 2);
+ Player loser = game.getCurrentPlayer();
+
+ game.setWinnerPlayerId(winner.getId());
+
+ statusLabel.setText(winner.getName() + " ganó. " + loser.getName() + " se rindió");
+ LOGGER.log(Level.INFO, loser.getName() + " se rindió. Ganador: " + winner.getName());
+ }
+
+ @FXML
+ private void onBackToMenu() {
+ stopGameTimer();
+ LOGGER.log(Level.INFO, "Volviendo al menú");
+ // Castear a Stage para poder cerrar la ventana
+ javafx.stage.Stage stage = (javafx.stage.Stage) boardCanvas.getScene().getWindow();
+ stage.close();
+ }
+
+ private void endGame() {
+ gameEnded = true;
+ game.setState(GameState.FINISHED);
+
+ // Calcular puntuación final
+ Board board = game.getBoard();
+ int blackStones = 0;
+ int whiteStones = 0;
+
+ for (int r = 0; r < BOARD_SIZE; r++) {
+ for (int c = 0; c < BOARD_SIZE; c++) {
+ int cell = board.getCell(r, c);
+ if (cell == 1) blackStones++;
+ else if (cell == 2) whiteStones++;
+ }
+ }
+
+ double blackScore = blackStones;
+ double whiteScore = whiteStones + 5.5;
+
+ String winner = blackScore > whiteScore ? player1Name : player2Name;
+ String result = String.format("Partida finalizada. %s gana %.1f - %.1f",
+ winner, Math.max(blackScore, whiteScore), Math.min(blackScore, whiteScore));
+
+ statusLabel.setText(result);
+ LOGGER.log(Level.INFO, result);
+
+ stopGameTimer();
+ }
+
+ private void startGameTimer() {
+ gameTimer = new AnimationTimer() {
+ @Override
+ public void handle(long now) {
+ if (lastTime == 0) {
+ lastTime = now;
+ return;
+ }
+
+ long elapsedNanos = now - lastTime;
+ lastTime = now;
+
+ if (!gameEnded) {
+ if (game.getCurrentPlayerIndex() == 0) {
+ player1TimeMs += elapsedNanos / 1_000_000;
+ } else {
+ player2TimeMs += elapsedNanos / 1_000_000;
+ }
+ updateTimeLabels();
+ }
+ }
+ };
+ gameTimer.start();
+ }
+
+ private void stopGameTimer() {
+ if (gameTimer != null) {
+ gameTimer.stop();
+ }
+ }
+
+ private void updateTimeLabels() {
+ player1TimeLabel.setText("Tiempo: " + formatTime(player1TimeMs));
+ player2TimeLabel.setText("Tiempo: " + formatTime(player2TimeMs));
+ }
+
+ private String formatTime(long ms) {
+ long seconds = ms / 1000;
+ long minutes = seconds / 60;
+ seconds = seconds % 60;
+ return String.format("%02d:%02d", minutes, seconds);
+ }
+
+ public void setPlayerNames(String player1, String player2) {
+ this.player1Name = player1 != null ? player1 : "Jugador 1";
+ this.player2Name = player2 != null ? player2 : "Jugador 2";
+ if (player1NameLabel != null) {
+ updatePlayerInfo();
+ }
+ }
+
+ public void setInitialScores(int score1, int score2) {
+ // Puntuación inicial se calcula del Board, no se establece manualmente
+ if (player1ScoreLabel != null) {
+ updateScores();
+ }
+ }
+
+ private double getBoardStartX() {
+ return (boardCanvas.getWidth() - ((BOARD_SIZE - 1) * CELL_SIZE)) / 2.0;
+ }
+
+ private double getBoardStartY() {
+ return (boardCanvas.getHeight() - ((BOARD_SIZE - 1) * CELL_SIZE)) / 2.0;
+ }
+
+ private double getBoardEndX() {
+ return getBoardStartX() + ((BOARD_SIZE - 1) * CELL_SIZE);
+ }
+
+ private double getBoardEndY() {
+ return getBoardStartY() + ((BOARD_SIZE - 1) * CELL_SIZE);
+ }
+
+ private double getIntersectionX(int col) {
+ return getBoardStartX() + col * CELL_SIZE;
+ }
+
+ private double getIntersectionY(int row) {
+ return getBoardStartY() + row * CELL_SIZE;
+ }
+
+ private void drawCoordinates(GraphicsContext gc, double boardStartX, double boardStartY, double boardEndX, double boardEndY) {
+ String[] letters = {"A", "B", "C", "D", "E", "F", "G", "H", "J"};
+ gc.setFill(Color.color(0.22, 0.12, 0.05, 0.9));
+ gc.setFont(Font.font("System", 13));
+ gc.setTextAlign(TextAlignment.CENTER);
+ gc.setTextBaseline(javafx.geometry.VPos.CENTER);
+
+ double offset = 18;
+ for (int i = 0; i < BOARD_SIZE; i++) {
+ double x = getIntersectionX(i);
+ double y = getIntersectionY(i);
+
+ // Letras arriba y abajo
+ gc.fillText(letters[i], x, boardStartY - offset);
+ gc.fillText(letters[i], x, boardEndY + offset);
+
+ // Números a izquierda y derecha
+ String number = String.valueOf(BOARD_SIZE - i);
+ gc.fillText(number, boardStartX - offset, y);
+ gc.fillText(number, boardEndX + offset, y);
+ }
+ }
+}
+
+
+
diff --git a/src/main/java/es/iesquevedo/controller/LoginController.java b/src/main/java/es/iesquevedo/controller/LoginController.java
index 0dbcabd..307a469 100644
--- a/src/main/java/es/iesquevedo/controller/LoginController.java
+++ b/src/main/java/es/iesquevedo/controller/LoginController.java
@@ -3,6 +3,9 @@
import es.iesquevedo.config.AppState;
import es.iesquevedo.service.auth.AuthService;
import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
@@ -10,6 +13,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
+import es.iesquevedo.util.EmailUtils;
/**
* Controlador para la pantalla de login.
@@ -66,6 +70,13 @@ public void onLoginClicked() {
return;
}
+ // Validar formato de email (asegura que contenga '@' y estructura básica)
+ if (!EmailUtils.isValidEmail(email)) {
+ updateStatus("El email no tiene un formato válido. Ej: usuario@ejemplo.com", "error");
+ LOGGER.log(Level.WARNING, "Intento de login con email inválido: " + email);
+ return;
+ }
+
if (password == null || password.trim().isEmpty()) {
updateStatus("La contraseña no puede estar vacía", "error");
LOGGER.log(Level.WARNING, "Intento de login sin contraseña");
@@ -94,8 +105,8 @@ public void onLoginClicked() {
emailField.clear();
passwordField.clear();
- // Nota: En una app real aquí navegerías a la pantalla principal
- // Por ahora solo mostramos el mensaje de éxito
+ // Navegar a la pantalla de juego
+ navigateToGame(email);
} catch (Exception e) {
updateStatus("✗ Error de login: " + e.getMessage(), "error");
@@ -103,6 +114,32 @@ public void onLoginClicked() {
}
}
+ /**
+ * Navega a la pantalla de juego después del login exitoso.
+ *
+ * @param playerName nombre del jugador autenticado
+ */
+ private void navigateToGame(String playerName) {
+ try {
+ FXMLLoader gameLoader = new FXMLLoader(getClass().getResource("/fxml/Game.fxml"));
+ Parent gameRoot = gameLoader.load();
+
+ GameController gameController = gameLoader.getController();
+ gameController.setPlayerNames(playerName, "Oponente");
+ gameController.setInitialScores(0, 0);
+
+ Scene scene = emailField.getScene();
+ scene.setRoot(gameRoot);
+ javafx.stage.Stage stage = (javafx.stage.Stage) scene.getWindow();
+ stage.setTitle("InazumaGo - Partida");
+
+ LOGGER.log(Level.INFO, "Navegado a pantalla de juego para: " + playerName);
+ } catch (Exception e) {
+ updateStatus("✗ Error al cargar pantalla de juego: " + e.getMessage(), "error");
+ LOGGER.log(Level.SEVERE, "Error al cargar Game.fxml: " + e.getMessage());
+ }
+ }
+
/**
* Manejador del botón "Limpiar" (opcional).
*/
@@ -133,5 +170,7 @@ private void updateStatus(String message, String type) {
statusLabel.setTextFill(Color.BLACK);
}
}
+
+ // ...existing code...
}
diff --git a/src/main/java/es/iesquevedo/exception/InvalidMoveException.java b/src/main/java/es/iesquevedo/exception/InvalidMoveException.java
index 1b7532b..eeacfb6 100644
--- a/src/main/java/es/iesquevedo/exception/InvalidMoveException.java
+++ b/src/main/java/es/iesquevedo/exception/InvalidMoveException.java
@@ -12,4 +12,3 @@ public InvalidMoveException(String message, Throwable cause) {
super(message, cause);
}
}
-
diff --git a/src/main/java/es/iesquevedo/exception/PlayerNotInTurnException.java b/src/main/java/es/iesquevedo/exception/PlayerNotInTurnException.java
index 36ab93c..b75222c 100644
--- a/src/main/java/es/iesquevedo/exception/PlayerNotInTurnException.java
+++ b/src/main/java/es/iesquevedo/exception/PlayerNotInTurnException.java
@@ -12,4 +12,3 @@ public PlayerNotInTurnException(String message, Throwable cause) {
super(message, cause);
}
}
-
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..3a33bde
--- /dev/null
+++ b/src/main/java/es/iesquevedo/model/Board.java
@@ -0,0 +1,222 @@
+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 66fe899..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() {
@@ -82,4 +100,3 @@ public void abandon() {
this.finishedAt = LocalDateTime.now();
}
}
-
diff --git a/src/main/java/es/iesquevedo/model/GameState.java b/src/main/java/es/iesquevedo/model/GameState.java
index 61b8428..a040f94 100644
--- a/src/main/java/es/iesquevedo/model/GameState.java
+++ b/src/main/java/es/iesquevedo/model/GameState.java
@@ -24,4 +24,3 @@ public enum GameState {
*/
ABANDONED
}
-
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..014b3d7
--- /dev/null
+++ b/src/main/java/es/iesquevedo/model/Move.java
@@ -0,0 +1,76 @@
+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/model/Player.java b/src/main/java/es/iesquevedo/model/Player.java
index a9f6ebc..21b89a3 100644
--- a/src/main/java/es/iesquevedo/model/Player.java
+++ b/src/main/java/es/iesquevedo/model/Player.java
@@ -76,4 +76,3 @@ public String toString() {
'}';
}
}
-
diff --git a/src/main/java/es/iesquevedo/repository/MainRepository.java b/src/main/java/es/iesquevedo/repository/MainRepository.java
index e72b521..bcdfc97 100644
--- a/src/main/java/es/iesquevedo/repository/MainRepository.java
+++ b/src/main/java/es/iesquevedo/repository/MainRepository.java
@@ -108,4 +108,3 @@ default CompletableFuture deleteGame(String gameId) {
*/
String findDefaultName();
}
-
diff --git a/src/main/java/es/iesquevedo/repository/firebase/FirebaseHttpClient.java b/src/main/java/es/iesquevedo/repository/firebase/FirebaseHttpClient.java
index 316b432..2cc8176 100644
--- a/src/main/java/es/iesquevedo/repository/firebase/FirebaseHttpClient.java
+++ b/src/main/java/es/iesquevedo/repository/firebase/FirebaseHttpClient.java
@@ -2,34 +2,71 @@
import es.iesquevedo.service.auth.AuthService;
+import java.io.BufferedReader;
import java.io.IOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
/**
* Cliente HTTP minimo para llamadas a Firebase RTDB.
*/
public class FirebaseHttpClient {
private final String baseUrl;
- private final HttpClient httpClient;
private final AuthService authService;
- public FirebaseHttpClient(String baseUrl, HttpClient httpClient, AuthService authService) {
+ public FirebaseHttpClient(String baseUrl, AuthService authService) {
this.baseUrl = baseUrl;
- this.httpClient = httpClient;
this.authService = authService;
}
- public HttpResponse get(String path) throws IOException, InterruptedException {
- HttpRequest.Builder builder = HttpRequest.newBuilder()
- .uri(URI.create(baseUrl + path))
- .GET();
+ public HttpResponse get(String path) throws IOException {
+ URL url = new URL(baseUrl + path);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
- authService.getToken().ifPresent(token -> builder.header("Authorization", "Bearer " + token));
+ if (authService.getToken().isPresent()) {
+ connection.setRequestProperty("Authorization", "Bearer " + authService.getToken().get());
+ }
- return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
+ int statusCode = connection.getResponseCode();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ response.append(line);
+ }
+ reader.close();
+ connection.disconnect();
+
+ return new HttpResponse<>(response.toString(), statusCode);
+ }
+
+ /**
+ * Clase simple para encapsular respuesta HTTP
+ */
+ public static class HttpResponse {
+ private final T body;
+ private final int statusCode;
+
+ public HttpResponse(T body) {
+ this.body = body;
+ this.statusCode = 200; // Default
+ }
+
+ public HttpResponse(T body, int statusCode) {
+ this.body = body;
+ this.statusCode = statusCode;
+ }
+
+ public T body() {
+ return body;
+ }
+
+ public int statusCode() {
+ return statusCode;
+ }
}
}
diff --git a/src/main/java/es/iesquevedo/repository/firebase/FirebaseMainRepository.java b/src/main/java/es/iesquevedo/repository/firebase/FirebaseMainRepository.java
index a801bef..0d698f8 100644
--- a/src/main/java/es/iesquevedo/repository/firebase/FirebaseMainRepository.java
+++ b/src/main/java/es/iesquevedo/repository/firebase/FirebaseMainRepository.java
@@ -1,129 +1,332 @@
package es.iesquevedo.repository.firebase;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import es.iesquevedo.dto.GameDto;
import es.iesquevedo.dto.MoveData;
import es.iesquevedo.dto.MoveDto;
import es.iesquevedo.repository.MainRepository;
-
-import com.google.firebase.database.DatabaseReference;
-import com.google.firebase.database.DataSnapshot;
-import com.google.firebase.database.DatabaseError;
-import com.google.firebase.database.FirebaseDatabase;
-import com.google.firebase.database.GenericTypeIndicator;
-import com.google.firebase.database.ValueEventListener;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+/**
+ * Repositorio para Firebase Realtime Database con cliente HTTP (OkHttp).
+ * Soporta CRUD básico, escritura multi-path (PATCH) y listeners simulados.
+ */
public class FirebaseMainRepository implements MainRepository {
- private final FirebaseDatabase database;
- @SuppressWarnings({"FieldCanBeLocal", "CollectionWithoutInitialCapacity", "MismatchedQueryAndUpdateOfCollection"})
- private final Map listeners = new HashMap<>();
+ private static final Logger LOGGER = Logger.getLogger(FirebaseMainRepository.class.getName());
+ private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
+ private static final String SUFFIX = ".json";
+
+ private final String firebaseUrl;
+ private final OkHttpClient httpClient;
+ private final Gson gson;
+ private final Map>> movesListeners;
+ private final int timeoutSeconds;
+ private String idToken; // Token de autenticación
+ /**
+ * Constructor con URL de Firebase (sin .firebaseio.com, se añade automáticamente).
+ */
public FirebaseMainRepository(String firebaseUrl) {
- this.database = FirebaseDatabase.getInstance(firebaseUrl);
+ this(firebaseUrl, 30);
+ }
+
+ /**
+ * Constructor con URL y timeout configurables.
+ */
+ public FirebaseMainRepository(String firebaseUrl, int timeoutSeconds) {
+ this.firebaseUrl = normalizeUrl(firebaseUrl);
+ this.timeoutSeconds = timeoutSeconds;
+ this.gson = new Gson();
+ this.movesListeners = new ConcurrentHashMap<>();
+ this.httpClient = createHttpClient();
+ this.idToken = null; // Se establece después de autenticar
+ }
+
+ /**
+ * Normaliza la URL de Firebase (garantiza formato correcto).
+ */
+ private String normalizeUrl(String url) {
+ if (url == null || url.isBlank()) {
+ return "https://localhost:9000"; // Fallback para tests con WireMock
+ }
+ url = url.trim();
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ url = "https://" + url;
+ }
+ if (!url.endsWith(".firebaseio.com") && !url.endsWith(".com")) {
+ url = url + ".firebaseio.com";
+ }
+ return url;
}
- /** Constructor para tests: permite inyectar un FirebaseDatabase mockeado. */
- public FirebaseMainRepository(FirebaseDatabase database) {
- this.database = database;
+ /**
+ * Crea cliente HTTP con timeout y configuración.
+ */
+ private OkHttpClient createHttpClient() {
+ return new OkHttpClient.Builder()
+ .connectTimeout(java.time.Duration.ofSeconds(timeoutSeconds))
+ .readTimeout(java.time.Duration.ofSeconds(timeoutSeconds))
+ .writeTimeout(java.time.Duration.ofSeconds(timeoutSeconds))
+ .build();
+ }
+
+ /**
+ * Establece el token de autenticación para futuras peticiones.
+ */
+ public void setIdToken(String idToken) {
+ this.idToken = idToken;
+ }
+
+ /**
+ * Obtiene el token de autenticación actual.
+ */
+ public String getCurrentToken() {
+ return this.idToken;
}
@Override
- public CompletableFuture getGame(String gameId) {
- DatabaseReference ref = database.getReference("games/" + gameId);
- CompletableFuture future = new CompletableFuture<>();
- ref.addListenerForSingleValueEvent(new ValueEventListener() {
- @Override
- public void onDataChange(DataSnapshot snapshot) {
- GameDto game = snapshot.getValue(GameDto.class);
- future.complete(game);
- }
+ public CompletableFuture createGame(GameDto game) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ String url = firebaseUrl + "/games/" + game.getId() + SUFFIX;
+ if (idToken != null) {
+ url += "?auth=" + idToken;
+ }
+ String json = gson.toJson(game);
+ RequestBody body = RequestBody.create(json, JSON);
+ Request request = new Request.Builder()
+ .url(url)
+ .put(body)
+ .build();
- @Override
- public void onCancelled(DatabaseError error) {
- future.completeExceptionally(new Exception(error.getMessage()));
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (response.isSuccessful() && response.body() != null) {
+ GameDto created = gson.fromJson(response.body().string(), GameDto.class);
+ LOGGER.log(Level.INFO, "Game creado: " + game.getId());
+ return created;
+ } else {
+ LOGGER.log(Level.WARNING, "Error al crear game: " + response.code());
+ throw new IOException("HTTP " + response.code());
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "Error en createGame: " + e.getMessage());
+ throw new RuntimeException(e);
}
});
- return future;
}
@Override
- public CompletableFuture writeMoveMultiPath(String gameId, MoveDto payload) {
- DatabaseReference ref = database.getReference("games/" + gameId);
- Map updates = new HashMap<>();
- updates.put("moves", payload.getMoves());
- updates.put("timestamp", payload.getTimestamp());
- updates.put("gameVersion", payload.getGameVersion());
- CompletableFuture future = new CompletableFuture<>();
- ref.updateChildren(updates, (error, ref1) -> {
- if (error == null) {
- future.complete(null);
- } else {
- future.completeExceptionally(new Exception(error.getMessage()));
+ public CompletableFuture> listGames() {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ String url = firebaseUrl + "/games" + SUFFIX;
+ if (idToken != null) {
+ url += "?auth=" + idToken;
+ }
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (response.isSuccessful() && response.body() != null) {
+ // Firebase devuelve Map o null
+ String body = response.body().string();
+ if ("null".equals(body)) {
+ return List.of();
+ }
+ Map games = gson.fromJson(body,
+ new com.google.gson.reflect.TypeToken