diff --git a/.idea/copilotDiffState.xml b/.idea/copilotDiffState.xml
new file mode 100644
index 0000000..b12a13e
--- /dev/null
+++ b/.idea/copilotDiffState.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
\ No newline at end of file
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 040fbb0..f48171a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,9 +12,6 @@
21
21
UTF-8
- 0.0.6
- 12.0.1
- windows-x86_64
@@ -22,17 +19,12 @@
org.openjfx
javafx-controls
- ${javafx.version}
+ 21
org.openjfx
javafx-fxml
- ${javafx.version}
-
-
- org.openjfx
- javafx-graphics
- ${javafx.version}
+ 21
@@ -54,10 +46,18 @@
9.8.0
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.11.0
+
+
+
com.google.code.gson
gson
- 2.11.0
+ 2.10.1
@@ -77,15 +77,6 @@
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.11.0
-
- 21
- 21
-
-
org.apache.maven.plugins
maven-surefire-plugin
@@ -94,20 +85,9 @@
false
**/MainTest.java
- **/FirebaseGameRepositoryStub.java
- **/FirebaseMainRepositoryTest.java
- **/LoginTokenFlowIntegrationTest.java
-
- org.openjfx
- javafx-maven-plugin
- 0.0.8
-
- es.iesquevedo.MainGUI
-
-
org.jacoco
jacoco-maven-plugin
diff --git a/src/main/java/es/iesquevedo/config/AppConfig.java b/src/main/java/es/iesquevedo/config/AppConfig.java
index da3b99e..48e2f60 100644
--- a/src/main/java/es/iesquevedo/config/AppConfig.java
+++ b/src/main/java/es/iesquevedo/config/AppConfig.java
@@ -30,7 +30,7 @@ public static MainRepository createMainRepository(String firebaseUrl) {
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/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/Game.java b/src/main/java/es/iesquevedo/model/Game.java
index 66fe899..6913e8f 100644
--- a/src/main/java/es/iesquevedo/model/Game.java
+++ b/src/main/java/es/iesquevedo/model/Game.java
@@ -82,4 +82,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/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/firebase/FirebaseMainRepository.java b/src/main/java/es/iesquevedo/repository/firebase/FirebaseMainRepository.java
index 4e4ada9..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