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..21e740a --- /dev/null +++ b/.idea/copilotDiffState.xml @@ -0,0 +1,49 @@ + + + + + + \ 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..ca48305 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); // 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..b40725d 100644 --- a/src/main/java/es/iesquevedo/MainGUI.java +++ b/src/main/java/es/iesquevedo/MainGUI.java @@ -1,7 +1,8 @@ package es.iesquevedo; -import es.iesquevedo.controller.LoginController; -import es.iesquevedo.service.auth.AuthServiceMock; +import es.iesquevedo.config.AppConfig; +import es.iesquevedo.service.impl.MainServiceImpl; +import es.iesquevedo.ui.MainScreenController; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -17,27 +18,27 @@ public class MainGUI extends Application { @Override public void start(Stage primaryStage) { try { - // Cargar FXML de Login con su controlador - 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); - primaryStage.setTitle("InazumaGo - Login"); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/MainScreen.fxml")); + Parent root = loader.load(); + + MainScreenController controller = loader.getController(); + String firebaseUrl = System.getenv("FIREBASE_URL"); + var repository = AppConfig.createMainRepository(firebaseUrl); + var mainService = new MainServiceImpl(repository); + controller.setService(mainService); + + Scene scene = new Scene(root, 700, 500); + primaryStage.setTitle("InazumaGo - Pantalla Principal"); 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 principal cargada"); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error al iniciar la aplicación", e); } } + + 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..d403057 --- /dev/null +++ b/src/main/java/es/iesquevedo/controller/GameController.java @@ -0,0 +1,456 @@ +package es.iesquevedo.controller; + +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 int player1Score = 0; + private int player2Score = 0; + private long player1TimeMs = 0; + private long player2TimeMs = 0; + private boolean isPlayer1Turn = true; + private Stone[][] board; + private AnimationTimer gameTimer; + private long lastTime = 0; + private boolean gameEnded = false; + + enum Stone { + EMPTY, BLACK, WHITE + } + + @FXML + public void initialize() { + LOGGER.log(Level.INFO, "GameController inicializado"); + board = new Stone[BOARD_SIZE][BOARD_SIZE]; + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + board[i][j] = Stone.EMPTY; + } + } + + updatePlayerInfo(); + drawBoard(); + boardCanvas.setOnMouseClicked(this::onBoardClick); + startGameTimer(); + } + + private void updatePlayerInfo() { + player1NameLabel.setText(player1Name + " (Negro)"); + player2NameLabel.setText(player2Name + " (Blanco)"); + updateScores(); + updateCurrentTurn(); + } + + private void updateScores() { + player1ScoreLabel.setText("Puntos: " + player1Score); + player2ScoreLabel.setText("Puntos: " + player2Score); + } + + private void updateCurrentTurn() { + String turn = isPlayer1Turn ? player1Name : player2Name; + 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 actuales (usa drawStone) + for (int r = 0; r < BOARD_SIZE; r++) { + for (int c = 0; c < BOARD_SIZE; c++) { + if (board[r][c] != Stone.EMPTY) { + drawStone(gc, r, c, board[r][c]); + } + } + } + } + + private void drawStone(GraphicsContext gc, int row, int col, Stone stone) { + double x = getIntersectionX(col); + double y = getIntersectionY(row); + double radius = CELL_SIZE / 2 - 3; + + if (stone == Stone.BLACK) { + // 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 { + // 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) { + statusLabel.setText("Partida finalizada"); + 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 (board[row][col] != Stone.EMPTY) { + statusLabel.setText("Posición ocupada"); + return; + } + + Stone stone = isPlayer1Turn ? Stone.BLACK : Stone.WHITE; + board[row][col] = stone; + + if (isPlayer1Turn) { + player1Score++; + } else { + player2Score++; + } + + isPlayer1Turn = !isPlayer1Turn; + updateScores(); + updateCurrentTurn(); + drawBoard(); + + statusLabel.setText("Piedra colocada"); + LOGGER.log(Level.INFO, "Piedra colocada en [" + row + "," + col + "]"); + } + + @FXML + private void onPassTurn() { + if (gameEnded) { + statusLabel.setText("Partida finalizada"); + return; + } + + isPlayer1Turn = !isPlayer1Turn; + updateCurrentTurn(); + statusLabel.setText((isPlayer1Turn ? player1Name : player2Name) + " pasó su turno"); + LOGGER.log(Level.INFO, "Turno pasado"); + } + + @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; + String winner = isPlayer1Turn ? player2Name : player1Name; + String loser = isPlayer1Turn ? player1Name : player2Name; + statusLabel.setText(winner + " ganó. " + loser + " se rindió"); + LOGGER.log(Level.INFO, loser + " se rindió. Ganador: " + winner); + } + + @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 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 (isPlayer1Turn) { + 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) { + this.player1Score = score1; + this.player2Score = score2; + 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/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/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>() {}.getType()); + return games != null ? List.copyOf(games.values()) : List.of(); + } else { + LOGGER.log(Level.WARNING, "Error al listar games: " + response.code()); + throw new IOException("HTTP " + response.code()); + } + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error en listGames: " + e.getMessage()); + throw new RuntimeException(e); } }); - return future; } @Override - public String addMovesListener(String gameId, Consumer> listener) { - DatabaseReference ref = database.getReference("games/" + gameId + "/moves"); - ValueEventListener vel = new ValueEventListener() { - @Override - public void onDataChange(DataSnapshot snapshot) { - GenericTypeIndicator> t = new GenericTypeIndicator<>() { - }; - List moves = snapshot.getValue(t); - listener.accept(moves); + public CompletableFuture getGame(String gameId) { + return CompletableFuture.supplyAsync(() -> { + try { + String url = firebaseUrl + "/games/" + gameId + 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) { + String body = response.body().string(); + if ("null".equals(body)) { + return null; + } + GameDto gameDto = gson.fromJson(body, GameDto.class); + LOGGER.log(Level.INFO, "Game obtenido: " + gameId); + return gameDto; + } else if (response.code() == 404) { + return null; + } else { + LOGGER.log(Level.WARNING, "Error al obtener game: " + response.code()); + throw new IOException("HTTP " + response.code()); + } + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error en getGame: " + e.getMessage()); + throw new RuntimeException(e); } + }); + } - @Override - public void onCancelled(DatabaseError error) { - // Handle error, perhaps log or notify + @Override + public CompletableFuture updateGame(String gameId, GameDto game) { + return CompletableFuture.supplyAsync(() -> { + try { + String url = firebaseUrl + "/games/" + gameId + 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) + .patch(body) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { + GameDto updated = gson.fromJson(response.body().string(), GameDto.class); + LOGGER.log(Level.INFO, "Game actualizado: " + gameId); + return updated; + } else { + LOGGER.log(Level.WARNING, "Error al actualizar game: " + response.code()); + throw new IOException("HTTP " + response.code()); + } + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error en updateGame: " + e.getMessage()); + throw new RuntimeException(e); } - }; - ref.addValueEventListener(vel); - String id = "listener-" + gameId + "-" + System.currentTimeMillis(); - listeners.put(id, vel); - return id; + }); } @Override - public String findDefaultName() { - DatabaseReference ref = database.getReference("config/defaultName"); - final String[] result = new String[1]; - CountDownLatch latch = new CountDownLatch(1); - ref.addListenerForSingleValueEvent(new ValueEventListener() { - @Override - public void onDataChange(DataSnapshot snapshot) { - result[0] = snapshot.getValue(String.class); - latch.countDown(); + public CompletableFuture deleteGame(String gameId) { + return CompletableFuture.runAsync(() -> { + try { + String url = firebaseUrl + "/games/" + gameId + SUFFIX; + if (idToken != null) { + url += "?auth=" + idToken; + } + Request request = new Request.Builder() + .url(url) + .delete() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + LOGGER.log(Level.WARNING, "Error al eliminar game: " + response.code()); + throw new IOException("HTTP " + response.code()); + } + LOGGER.log(Level.INFO, "Game eliminado: " + gameId); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error en deleteGame: " + e.getMessage()); + throw new RuntimeException(e); } + }); + } + + @Override + public CompletableFuture writeMoveMultiPath(String gameId, MoveDto payload) { + return CompletableFuture.runAsync(() -> { + try { + // Construir URL para PATCH multi-path + String url = firebaseUrl + "/games/" + gameId + SUFFIX; + if (idToken != null) { + url += "?auth=" + idToken; + } + + // Crear payload con estructura PATCH + Map updates = new HashMap<>(); + updates.put("moves", payload.getMoves()); + updates.put("timestamp", payload.getTimestamp()); + updates.put("gameVersion", payload.getGameVersion()); + + String json = gson.toJson(updates); + RequestBody body = RequestBody.create(json, JSON); + Request request = new Request.Builder() + .url(url) + .patch(body) + .build(); - @Override - public void onCancelled(DatabaseError error) { - result[0] = "DefaultPlayer"; - latch.countDown(); + try (Response response = httpClient.newCall(request).execute()) { + if (response.code() == 403) { + throw new IOException("Movimiento rechazado por reglas del servidor (403)"); + } + if (!response.isSuccessful()) { + LOGGER.log(Level.WARNING, "Error al escribir movimientos: " + response.code()); + throw new IOException("HTTP " + response.code()); + } + LOGGER.log(Level.INFO, "Movimientos guardados para partida: " + gameId); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error en writeMoveMultiPath: " + e.getMessage()); + throw new RuntimeException(e); } }); - try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return "DefaultPlayer"; - } - return result[0] != null ? result[0] : "DefaultPlayer"; } - // Método adicional para pruebas con WireMock (no está en la interfaz) - public boolean patchMultiPath(String path, Map updates) throws IOException { - // TODO: Implementar llamada HTTP real con OkHttp - return true; + @Override + public String addMovesListener(String gameId, Consumer> listener) { + // Simulación básica: guardar listener en mapa para tests + // En producción, implementar SSE (Server-Sent Events) o WebSocket + String listenerId = "listener-" + UUID.randomUUID(); + movesListeners.put(listenerId, listener); + LOGGER.log(Level.INFO, "Listener añadido para partida: " + gameId); + return listenerId; + } + + public void removeMovesListener(String gameId, String listenerId) { + movesListeners.remove(listenerId); + LOGGER.log(Level.INFO, "Listener removido: " + listenerId); + } + + @Override + public String findDefaultName() { + return "FirebasePlayer"; + } + + /** + * Método auxiliar para notificar a listeners (uso interno en tests). + */ + protected void notifyMovesListeners(List moves) { + movesListeners.values().forEach(listener -> listener.accept(moves)); } -} +} \ No newline at end of file diff --git a/src/main/java/es/iesquevedo/repository/firebase/GameEventRepository.java b/src/main/java/es/iesquevedo/repository/firebase/GameEventRepository.java index 6277927..3552831 100644 --- a/src/main/java/es/iesquevedo/repository/firebase/GameEventRepository.java +++ b/src/main/java/es/iesquevedo/repository/firebase/GameEventRepository.java @@ -32,6 +32,7 @@ */ public class GameEventRepository { private static final String EVENTS_PATH = "game_events"; + private static final String FIELD_GAME_ID = "gameId"; private final FirebaseDatabase database; private final String baseUrl; @@ -114,7 +115,7 @@ public Query getGameEventsReference(String gameId) { throw new IllegalStateException("getGameEventsReference solo está disponible en modo Firebase SDK"); } return database.getReference(EVENTS_PATH) - .orderByChild("gameId") + .orderByChild(FIELD_GAME_ID) .equalTo(gameId); } @@ -143,10 +144,13 @@ private CompletableFuture recordEventHttp(EventType eventType, String game "Error al grabar evento " + eventType.getValue() + " (HTTP " + response.statusCode() + "): " + response.body() ); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Hilo interrumpido al grabar evento " + eventType.getValue(), e); } catch (RuntimeException e) { throw e; } catch (Exception e) { - throw new RuntimeException(e); + throw new IllegalStateException("Error al grabar evento " + eventType.getValue(), e); } }); } @@ -188,10 +192,13 @@ private CompletableFuture> fetchGameEventsHttp(String gameId) throw new IllegalStateException( "Error al recuperar eventos (HTTP " + response.statusCode() + "): " + response.body() ); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Hilo interrumpido al recuperar eventos", e); } catch (RuntimeException e) { throw e; } catch (Exception e) { - throw new RuntimeException(e); + throw new IllegalStateException("Error al recuperar eventos", e); } }); } @@ -200,7 +207,7 @@ private CompletableFuture> fetchGameEventsFirebase(String gam CompletableFuture> future = new CompletableFuture<>(); try { Query query = database.getReference(EVENTS_PATH) - .orderByChild("gameId") + .orderByChild(FIELD_GAME_ID) .equalTo(gameId); query.addListenerForSingleValueEvent(new ValueEventListener() { @@ -233,7 +240,7 @@ public void onCancelled(DatabaseError error) { private Map buildEventData(EventType eventType, String gameId, Object payload) { Map eventData = new HashMap<>(); eventData.put("type", eventType.getValue()); - eventData.put("gameId", gameId); + eventData.put(FIELD_GAME_ID, gameId); eventData.put("timestamp", System.currentTimeMillis()); eventData.put("payload", payload); return eventData; @@ -241,7 +248,7 @@ private Map buildEventData(EventType eventType, String gameId, O private String normalizeBaseUrl(String value) { if (value == null || value.isBlank()) { - throw new IllegalArgumentException("La base URL no puede ser nula o vacía"); + throw new IllegalArgumentException("La base URL no puede ser nula o vacía "); } return value.endsWith("/") ? value.substring(0, value.length() - 1) : value; } diff --git a/src/main/java/es/iesquevedo/service/AuthService.java b/src/main/java/es/iesquevedo/service/AuthService.java new file mode 100644 index 0000000..5dfe283 --- /dev/null +++ b/src/main/java/es/iesquevedo/service/AuthService.java @@ -0,0 +1,28 @@ +package es.iesquevedo.service; + +import java.util.concurrent.CompletableFuture; + +/** + * Servicio de autenticación para obtener tokens de Firebase. + */ +public interface AuthService { + + /** + * Login con email y contraseña. + * + * @param email correo del usuario + * @param password contraseña + * @return CompletableFuture con token de autenticación + */ + CompletableFuture login(String email, String password); + + /** + * Obtiene el token actual (si existe y no ha expirado). + */ + String getCurrentToken(); + + /** + * Logout y limpia token. + */ + void logout(); +} diff --git a/src/main/java/es/iesquevedo/service/GameService.java b/src/main/java/es/iesquevedo/service/GameService.java index 53692b3..300e072 100644 --- a/src/main/java/es/iesquevedo/service/GameService.java +++ b/src/main/java/es/iesquevedo/service/GameService.java @@ -83,4 +83,3 @@ public interface GameService { */ Game getGame(String gameId); } - diff --git a/src/main/java/es/iesquevedo/service/impl/AuthServiceImpl.java b/src/main/java/es/iesquevedo/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..260306d --- /dev/null +++ b/src/main/java/es/iesquevedo/service/impl/AuthServiceImpl.java @@ -0,0 +1,50 @@ +package es.iesquevedo.service.impl; + +import es.iesquevedo.service.AuthService; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implementación mock de AuthService para desarrollo. + * En producción, se usaría Firebase Authentication real. + */ +public class AuthServiceImpl implements AuthService { + private static final Logger LOGGER = Logger.getLogger(AuthServiceImpl.class.getName()); + private String currentToken; + private String currentEmail; + + @Override + public CompletableFuture login(String email, String password) { + return CompletableFuture.supplyAsync(() -> { + // Validaciones mínimas (en prod, verificar contra Firebase Auth) + if (email == null || email.isBlank()) { + throw new IllegalArgumentException("Email no puede estar vacío"); + } + if (password == null || password.length() < 6) { + throw new IllegalArgumentException("Contraseña debe tener al menos 6 caracteres"); + } + + // Generar token mock (en prod sería de Firebase) + this.currentToken = "dev-token-" + UUID.randomUUID(); + this.currentEmail = email; + + LOGGER.log(Level.INFO, "Login exitoso: " + email); + return this.currentToken; + }); + } + + @Override + public String getCurrentToken() { + return currentToken; + } + + @Override + public void logout() { + this.currentToken = null; + this.currentEmail = null; + LOGGER.log(Level.INFO, "Logout realizado"); + } +} diff --git a/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java b/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java index 7826364..3a0f58a 100644 --- a/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java +++ b/src/main/java/es/iesquevedo/service/impl/GameServiceImpl.java @@ -137,4 +137,3 @@ public void reset() { games.clear(); } } - diff --git a/src/main/java/es/iesquevedo/service/impl/MainServiceImpl.java b/src/main/java/es/iesquevedo/service/impl/MainServiceImpl.java index 2b4f2a7..0a6ae55 100644 --- a/src/main/java/es/iesquevedo/service/impl/MainServiceImpl.java +++ b/src/main/java/es/iesquevedo/service/impl/MainServiceImpl.java @@ -21,7 +21,7 @@ public MainServiceImpl(MainRepository repository) { @Override public String greet() { String name = repository.findDefaultName(); - if (name == null || name.isBlank()) { + if (name == null || name.trim().isEmpty()) { throw new NotFoundException("Default player name not found"); } return name.endsWith("!") ? "Hello, " + name : "Hello, " + name + "!"; diff --git a/src/main/java/es/iesquevedo/ui/MainScreenController.java b/src/main/java/es/iesquevedo/ui/MainScreenController.java new file mode 100644 index 0000000..8779944 --- /dev/null +++ b/src/main/java/es/iesquevedo/ui/MainScreenController.java @@ -0,0 +1,90 @@ +package es.iesquevedo.ui; + +import es.iesquevedo.service.MainService; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.scene.control.Alert.AlertType; + +import java.util.Arrays; +import java.util.List; + +public class MainScreenController { + + @FXML + private ListView playersListView; + + @FXML + private Button randomMatchButton; + + private MainService mainService; + + // Called by MainGUI after loading FXML + public void setService(MainService mainService) { + this.mainService = mainService; + // Once service is set, we could load real data from Firebase + // For now, load mock players + loadMockPlayers(); + } + + @FXML + public void initialize() { + // Initialization if needed; actual data loading happens in setService + } + + private void loadMockPlayers() { + // TODO: Replace with real data from Firebase via MainService / PlayerRepository + List mockPlayers = Arrays.asList("Jugador1", "Jugador2", "Jugador3", "Jugador4", "Jugador5"); + playersListView.getItems().setAll(mockPlayers); + } + + @FXML + private void onRandomMatch() { + // Simulate random matchmaking + showInfo("Emparejamiento Aleatorio", "Buscando oponente..."); + // In a real implementation, you would call a matchmaking service and navigate to game screen. + // For demo, we simulate a found match after 1 second. + new Thread(() -> { + try { Thread.sleep(1000); } catch (InterruptedException e) {} + Platform.runLater(() -> { + showInfo("Partida encontrada", "Has sido emparejado con OponenteAleatorio. ¡Comienza la partida!"); + // Here you would load the game board screen + }); + }).start(); + } + + @FXML + private void onRefreshPlayers() { + loadMockPlayers(); + showInfo("Actualizar", "Lista de jugadores actualizada."); + } + + @FXML + private void onChallengeSelected() { + String selected = playersListView.getSelectionModel().getSelectedItem(); + if (selected == null) { + showWarning("Atención", "Debes seleccionar un jugador de la lista."); + } else { + showInfo("Retar", "Has retado a " + selected + ". Esperando respuesta..."); + // Here you would send a challenge via Firebase + } + } + + private void showInfo(String title, String message) { + Alert alert = new Alert(AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + private void showWarning(String title, String message) { + Alert alert = new Alert(AlertType.WARNING); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } +} \ No newline at end of file diff --git a/src/main/java/es/iesquevedo/util/EmailUtils.java b/src/main/java/es/iesquevedo/util/EmailUtils.java new file mode 100644 index 0000000..4bb3726 --- /dev/null +++ b/src/main/java/es/iesquevedo/util/EmailUtils.java @@ -0,0 +1,18 @@ +package es.iesquevedo.util; + +public final class EmailUtils { + private EmailUtils() {} + + /** + * Valida el formato básico del email (asegura presencia de '@' y dominio con TLD). + * No pretende ser una validación RFC completa, sino una comprobación práctica. + */ + public static boolean isValidEmail(String email) { + if (email == null) return false; + email = email.trim(); + // Regex simple y efectivo que exige: usuario@dominio.tld (TLD >= 2 caracteres) + String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + return email.matches(emailRegex); + } +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a79588d..bcad1f4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,58 +1,16 @@ # Aplicacin: InazumaGoPrevio -# Propiedades de ejemplo +# Propiedades de configuracin + +# App app.name=InazumaGoPrevio app.environment=development -# ============================================================================ -# FIREBASE REALTIME DATABASE CONFIGURATION -# ============================================================================ -# URL base del proyecto Firebase RTDB -firebase.rtdb.url=https://your-project.firebaseio.com - -# Timeouts de conexin (en milisegundos) -firebase.rtdb.timeout.connect=10000 -firebase.rtdb.timeout.read=30000 -firebase.rtdb.timeout.write=30000 - -# Configuracin de reintentos HTTP -firebase.http.retry.max-attempts=3 -firebase.http.retry.backoff-multiplier=2.0 -firebase.http.retry.initial-delay-ms=500 - -# ============================================================================ -# FIREBASE FIRESTORE CONFIGURATION -# ============================================================================ -# Endpoint de Firestore -firebase.firestore.endpoint=https://firestore.googleapis.com/v1 -firebase.firestore.timeout=30000 -firebase.firestore.retry-attempts=3 - -# ============================================================================ -# WIREMOCK CONFIGURATION (Testing) -# ============================================================================ -# Puerto y URL base de WireMock para tests -wiremock.server.port=8080 -wiremock.server.baseurl=http://localhost:8080 - -# ============================================================================ -# GAME EVENTS CONFIGURATION -# ============================================================================ -# Configuracin de eventos de juego para sincronizacin con Firebase -# Tipos de eventos soportados: game.start, game.move, game.end -game.events.enabled=true -game.events.database-path=game_events -game.events.async-processing=true -game.events.executor-threads=2 - -# ============================================================================ -# AUTHENTICATION -# ============================================================================ -# Token de autenticacin (obtenido dinmicamente desde Firebase Auth, no hardcodear) -# firebase.auth.token=${FIREBASE_ID_TOKEN} +# Firebase Configuration +firebase.url=https://inazumago-default-rtdb.firebaseio.com +firebase.timeout.seconds=30 -# ============================================================================ -# NOTA DE SEGURIDAD -# ============================================================================ -# NO guardes claves privadas de Firebase en este archivo. -# Usa variables de entorno o sistemas de gestin de secretos para datos sensibles. +# Authentication +firebase.auth.token=${FIREBASE_AUTH_TOKEN:} +# Logging +logging.level=INFO diff --git a/src/main/resources/fxml/Game.fxml b/src/main/resources/fxml/Game.fxml new file mode 100644 index 0000000..2bcf68a --- /dev/null +++ b/src/main/resources/fxml/Game.fxml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + +