From 2c3a751d68e43085ae858073a540f0d8204e02cb Mon Sep 17 00:00:00 2001 From: Francin Vincent Date: Mon, 6 Apr 2026 23:16:36 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20enforce=20GUI=E2=86=92Logic?= =?UTF-8?q?=E2=86=92Database=20layer=20separation=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route all database calls through state classes instead of calling DatabaseManager directly from screen helpers. - LobbyState: add validateLobby, listenForOpponentReady, startGame, listenForGameStart wrapper methods - SetupState: add confirmSetup, unconfirmSetup, listenForBothSetupReady, getOpponentBoard, getFirebaseApi wrapper methods - LobbyValidator: route through LobbyState instead of DatabaseManager - LobbyFlowController: route through LobbyState - SetupFlowController: route through SetupState - GameNetworkHandler: accept FirebaseAPI via constructor injection - Thread LobbyState through MainMenuScreen→MainMenuUI→JoinGamePanel - Thread FirebaseAPI through SetupScreen→GameScreen→GameNetworkHandler Result: zero DatabaseManager references in screens/ package --- .../screens/game/GameNetworkHandler.java | 23 +++++++++---------- .../screens/game/GameScreen.java | 6 +++-- .../screens/lobby/LobbyFlowController.java | 14 +++++------ .../screens/lobby/LobbyScreen.java | 2 +- .../screens/mainmenu/JoinGamePanel.java | 5 ++-- .../screens/mainmenu/LobbyValidator.java | 8 ++++--- .../screens/mainmenu/MainMenuScreen.java | 9 ++++---- .../screens/mainmenu/MainMenuUI.java | 5 ++-- .../screens/setup/SetupFlowController.java | 9 ++++---- .../screens/setup/SetupScreen.java | 3 ++- .../regicidechess/states/LobbyState.java | 17 ++++++++++++++ .../regicidechess/states/SetupState.java | 22 ++++++++++++++++++ 12 files changed, 84 insertions(+), 39 deletions(-) diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java b/core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java index 4a70618..04fd300 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java +++ b/core/src/main/java/com/group14/regicidechess/screens/game/GameNetworkHandler.java @@ -5,7 +5,7 @@ import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.ui.Label; -import com.group14.regicidechess.database.DatabaseManager; +import com.group14.regicidechess.database.FirebaseAPI; import com.group14.regicidechess.model.Move; import com.group14.regicidechess.model.Player; @@ -44,18 +44,20 @@ public interface Listener { private final String gameId; private final Player localPlayer; private final Listener listener; + private final FirebaseAPI api; // Connection UI refs — updated directly here to keep GameScreen clean private final Image connectionIcon; private final Label connectionLabel; public GameNetworkHandler(String gameId, Player localPlayer, Listener listener, - Image connectionIcon, Label connectionLabel) { + Image connectionIcon, Label connectionLabel, FirebaseAPI api) { this.gameId = gameId; this.localPlayer = localPlayer; this.listener = listener; this.connectionIcon = connectionIcon; this.connectionLabel = connectionLabel; + this.api = api; } // ───────────────────────────────────────────────────────────────────────── @@ -74,18 +76,18 @@ public void start() { /** Saves a completed move (with optional promotion) to Firebase. */ public void saveMove(Move move) { - DatabaseManager.getInstance().getApi().saveMove(gameId, move, () -> {}); + api.saveMove(gameId, move, () -> {}); } /** Sends a heartbeat pulse. Call every HEARTBEAT_INTERVAL seconds from render(). */ public void sendHeartbeat() { - DatabaseManager.getInstance().getApi().sendHeartbeat(gameId, localPlayer.isWhite()); + api.sendHeartbeat(gameId, localPlayer.isWhite()); } /** Signals that this player has forfeited. */ public void signalForfeit() { String loser = localPlayer.isWhite() ? "white" : "black"; - DatabaseManager.getInstance().getApi().signalGameOver(gameId, "forfeit:" + loser); + api.signalGameOver(gameId, "forfeit:" + loser); } // ───────────────────────────────────────────────────────────────────────── @@ -98,8 +100,7 @@ public void signalForfeit() { * Own echoed moves are filtered out via coords[4]. */ private void startOpponentMoveListener() { - DatabaseManager.getInstance().getApi() - .listenForOpponentMove(gameId, coords -> { + api.listenForOpponentMove(gameId, coords -> { boolean moveIsWhite = coords.length > 4 && coords[4] == 1; if (moveIsWhite == localPlayer.isWhite()) return; // own echo @@ -112,17 +113,15 @@ private void startOpponentMoveListener() { } private void startGameOverListener() { - DatabaseManager.getInstance().getApi() - .listenForGameOver(gameId, reason -> + api.listenForGameOver(gameId, reason -> Gdx.app.postRunnable(() -> listener.onGameOver(reason))); } private void startHeartbeat() { // Send our first beat immediately - DatabaseManager.getInstance().getApi().sendHeartbeat(gameId, localPlayer.isWhite()); + api.sendHeartbeat(gameId, localPlayer.isWhite()); - DatabaseManager.getInstance().getApi() - .listenForHeartbeat( + api.listenForHeartbeat( gameId, !localPlayer.isWhite(), // watch opponent's heartbeat HEARTBEAT_TIMEOUT_MS, diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java b/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java index 14cd5e5..91cfc54 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java +++ b/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java @@ -18,6 +18,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.viewport.FitViewport; +import com.group14.regicidechess.database.FirebaseAPI; import com.group14.regicidechess.input.ScreenInputHandler; import com.group14.regicidechess.model.Board; import com.group14.regicidechess.model.Move; @@ -97,7 +98,8 @@ public class GameScreen implements Screen, // ───────────────────────────────────────────────────────────────────────── public GameScreen(Game game, SpriteBatch batch, - Board board, Player localPlayer, int boardSize, String gameId) { + Board board, Player localPlayer, int boardSize, String gameId, + FirebaseAPI api) { this.game = game; this.batch = batch; this.localPlayer = localPlayer; @@ -129,7 +131,7 @@ public GameScreen(Game game, SpriteBatch batch, overlayManager = new GameOverlayManager(stage, skin, localPlayer, this); networkHandler = new GameNetworkHandler(gameId, localPlayer, this, - connectionIcon, connectionLabel); + connectionIcon, connectionLabel, api); boardRenderer = new GameBoardRenderer(batch, localPlayer, boardSize); boardRenderer.computeGeometry(V_WIDTH, V_HEIGHT, TOP_BAR_HEIGHT, STATUS_BAR_HEIGHT); diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java index 93d7d20..882840a 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java @@ -2,7 +2,7 @@ package com.group14.regicidechess.screens.lobby; import com.badlogic.gdx.Gdx; -import com.group14.regicidechess.database.DatabaseManager; +import com.group14.regicidechess.states.LobbyState; /** * Manages Firebase listeners and flow between host and joiner. @@ -16,10 +16,12 @@ public interface FlowListener { void onError(String message); } + private final LobbyState lobbyState; private final FlowListener listener; private String activeGameId; - public LobbyFlowController(FlowListener listener) { + public LobbyFlowController(LobbyState lobbyState, FlowListener listener) { + this.lobbyState = lobbyState; this.listener = listener; } @@ -29,8 +31,7 @@ public LobbyFlowController(FlowListener listener) { */ public void listenForJoiner(String gameId) { this.activeGameId = gameId; - DatabaseManager.getInstance().getApi() - .listenForOpponentReady(gameId, + lobbyState.listenForOpponentReady(gameId, () -> Gdx.app.postRunnable(() -> { if (listener != null) { listener.onJoinerArrived(); @@ -43,7 +44,7 @@ public void listenForJoiner(String gameId) { * Writes status = "started" to Firebase. */ public void signalGameStart(String gameId) { - DatabaseManager.getInstance().getApi().startGame(gameId); + lobbyState.startGame(gameId); } /** @@ -52,8 +53,7 @@ public void signalGameStart(String gameId) { */ public void listenForGameStart(String gameId) { this.activeGameId = gameId; - DatabaseManager.getInstance().getApi() - .listenForGameStart(gameId, + lobbyState.listenForGameStart(gameId, () -> Gdx.app.postRunnable(() -> { if (listener != null) { listener.onGameStarted(); diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java index 8259b72..52dfb98 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java @@ -64,7 +64,7 @@ public LobbyScreen(Game game, SpriteBatch batch, LobbyMode mode, Lobby lobby) { stateManager.setPrefetchedLobby(lobby); } - this.flowController = new LobbyFlowController(createFlowListener()); + this.flowController = new LobbyFlowController(stateManager.getLobbyState(), createFlowListener()); this.stateManager.setListener(createStateListener()); // LibGDX setup diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java index 959ef26..384526a 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/JoinGamePanel.java @@ -9,6 +9,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextField; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.group14.regicidechess.model.Lobby; +import com.group14.regicidechess.states.LobbyState; /** * Join game panel UI component. @@ -33,10 +34,10 @@ public interface JoinPanelListener { private boolean visible = false; - public JoinGamePanel(Skin skin, JoinPanelListener listener) { + public JoinGamePanel(Skin skin, LobbyState lobbyState, JoinPanelListener listener) { this.skin = skin; this.listener = listener; - this.validator = new LobbyValidator(createValidationListener()); + this.validator = new LobbyValidator(lobbyState, createValidationListener()); buildPanel(); } diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java index 33d1d87..14177d8 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/LobbyValidator.java @@ -2,8 +2,8 @@ package com.group14.regicidechess.screens.mainmenu; import com.badlogic.gdx.Gdx; -import com.group14.regicidechess.database.DatabaseManager; import com.group14.regicidechess.model.Lobby; +import com.group14.regicidechess.states.LobbyState; /** * Validates lobby existence in Firebase before navigating. @@ -17,10 +17,12 @@ public interface ValidationListener { void onValidationError(String message); } + private final LobbyState lobbyState; private final ValidationListener listener; private boolean isValidating = false; - public LobbyValidator(ValidationListener listener) { + public LobbyValidator(LobbyState lobbyState, ValidationListener listener) { + this.lobbyState = lobbyState; this.listener = listener; } @@ -48,7 +50,7 @@ public void validate(String gameId) { isValidating = true; - DatabaseManager.getInstance().getApi().fetchLobby(trimmedId, + lobbyState.validateLobby(trimmedId, // Lobby found lobby -> Gdx.app.postRunnable(() -> { isValidating = false; diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java index dbe5e68..c9ab26c 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuScreen.java @@ -13,6 +13,7 @@ import com.group14.regicidechess.model.Lobby; import com.group14.regicidechess.screens.lobby.LobbyMode; import com.group14.regicidechess.screens.lobby.LobbyScreen; +import com.group14.regicidechess.states.LobbyState; import com.group14.regicidechess.states.MainMenuState; import com.group14.regicidechess.utils.ResourceManager; @@ -36,6 +37,7 @@ public class MainMenuScreen implements Screen, ScreenInputHandler.ScreenInputObs // State and UI private final MainMenuState mainMenuState; + private final LobbyState lobbyState; private final MainMenuUI mainMenuUI; public MainMenuScreen(Game game, SpriteBatch batch) { @@ -44,6 +46,7 @@ public MainMenuScreen(Game game, SpriteBatch batch) { // Initialize state this.mainMenuState = new MainMenuState(); + this.lobbyState = new LobbyState(); mainMenuState.enter(); // LibGDX setup @@ -56,7 +59,7 @@ public MainMenuScreen(Game game, SpriteBatch batch) { inputHandler.addObserver(this); // Build UI with listener - this.mainMenuUI = new MainMenuUI(skin, createUIListener()); + this.mainMenuUI = new MainMenuUI(skin, lobbyState, createUIListener()); // Add UI to stage stage.addActor(mainMenuUI.build()); @@ -97,9 +100,7 @@ public void onBack() { private void fetchLobbyAndNavigate(String gameId) { mainMenuUI.showError("Loading..."); - com.group14.regicidechess.database.DatabaseManager.getInstance() - .getApi() - .fetchLobby(gameId, + lobbyState.validateLobby(gameId, // Success - lobby found lobby -> Gdx.app.postRunnable(() -> { mainMenuUI.clearError(); diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java index 4222be7..ed1ab45 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java @@ -8,6 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Align; +import com.group14.regicidechess.states.LobbyState; /** * Builds and manages the main menu UI. @@ -30,10 +31,10 @@ public interface MainMenuUIListener { private TextButton joinBtn; private Label mainErrorLabel; - public MainMenuUI(Skin skin, MainMenuUIListener listener) { + public MainMenuUI(Skin skin, LobbyState lobbyState, MainMenuUIListener listener) { this.skin = skin; this.listener = listener; - this.joinPanel = new JoinGamePanel(skin, createJoinPanelListener()); + this.joinPanel = new JoinGamePanel(skin, lobbyState, createJoinPanelListener()); } public Table build() { diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java index 99dc3f5..dcdc9b6 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java @@ -2,7 +2,6 @@ package com.group14.regicidechess.screens.setup; import com.badlogic.gdx.Gdx; -import com.group14.regicidechess.database.DatabaseManager; import com.group14.regicidechess.model.Board; import com.group14.regicidechess.model.Player; import com.group14.regicidechess.states.SetupState; @@ -65,7 +64,7 @@ public void confirm() { listener.onUploadComplete(); int[][] encoded = SetupBoardCodec.encode(setupState.getBoard()); - DatabaseManager.getInstance().getApi().confirmSetup( + setupState.confirmSetup( gameId, localPlayer.isWhite(), encoded, @@ -83,7 +82,7 @@ public void unconfirm() { setupState.getPlayer().resetReady(); isConfirmed = false; - DatabaseManager.getInstance().getApi().unconfirmSetup( + setupState.unconfirmSetup( gameId, localPlayer.isWhite(), () -> Gdx.app.postRunnable(() -> { @@ -99,7 +98,7 @@ public void unconfirm() { } private void listenForBothReady() { - DatabaseManager.getInstance().getApi().listenForBothSetupReady( + setupState.listenForBothSetupReady( gameId, () -> Gdx.app.postRunnable(() -> { listener.onBothReady(); @@ -113,7 +112,7 @@ private void fetchOpponentBoardWithRetry() { } private void fetchOpponentBoard() { - DatabaseManager.getInstance().getApi().getOpponentBoard( + setupState.getOpponentBoard( gameId, localPlayer.isWhite(), opponentBoard -> Gdx.app.postRunnable(() -> { diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java index d64d1f3..e490d1c 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java @@ -304,7 +304,8 @@ public void onOpponentBoardFetched(com.group14.regicidechess.model.Board finalBo finalBoard, localPlayer, setupState.getBoardSize(), - gameId)); + gameId, + setupState.getFirebaseApi())); } catch (Exception e) { Gdx.app.error("SetupScreen", "Error navigating to GameScreen: " + e.getMessage(), e); showStatus("Error starting game: " + e.getMessage()); diff --git a/core/src/main/java/com/group14/regicidechess/states/LobbyState.java b/core/src/main/java/com/group14/regicidechess/states/LobbyState.java index 660e7d5..66c884c 100644 --- a/core/src/main/java/com/group14/regicidechess/states/LobbyState.java +++ b/core/src/main/java/com/group14/regicidechess/states/LobbyState.java @@ -1,6 +1,7 @@ package com.group14.regicidechess.states; import com.group14.regicidechess.database.DatabaseManager; +import com.group14.regicidechess.database.FirebaseAPI; import com.group14.regicidechess.model.Lobby; import com.group14.regicidechess.model.Player; @@ -64,6 +65,22 @@ public void setPrefetchedLobby(Lobby prefetched) { } } + public void validateLobby(String gameId, FirebaseAPI.Callback onSuccess, FirebaseAPI.Callback onError) { + DatabaseManager.getInstance().getApi().fetchLobby(gameId, onSuccess, onError); + } + + public void listenForOpponentReady(String gameId, Runnable onReady) { + DatabaseManager.getInstance().getApi().listenForOpponentReady(gameId, onReady); + } + + public void startGame(String gameId) { + DatabaseManager.getInstance().getApi().startGame(gameId); + } + + public void listenForGameStart(String gameId, Runnable onStart) { + DatabaseManager.getInstance().getApi().listenForGameStart(gameId, onStart); + } + public Lobby getLobby() { return lobby; } public Player getPlayerOne() { return playerOne; } public Player getPlayerTwo() { return playerTwo; } diff --git a/core/src/main/java/com/group14/regicidechess/states/SetupState.java b/core/src/main/java/com/group14/regicidechess/states/SetupState.java index 46485ea..c48f641 100644 --- a/core/src/main/java/com/group14/regicidechess/states/SetupState.java +++ b/core/src/main/java/com/group14/regicidechess/states/SetupState.java @@ -1,6 +1,8 @@ // File: core/src/main/java/com/group14/regicidechess/states/SetupState.java package com.group14.regicidechess.states; +import com.group14.regicidechess.database.DatabaseManager; +import com.group14.regicidechess.database.FirebaseAPI; import com.group14.regicidechess.model.Board; import com.group14.regicidechess.model.Player; import com.group14.regicidechess.model.pieces.ChessPiece; @@ -155,6 +157,26 @@ private boolean isInHomeZone(int row) { public int getHomeRowMin() { return homeRowMin; } public int getHomeRowMax() { return homeRowMax; } + public FirebaseAPI getFirebaseApi() { + return DatabaseManager.getInstance().getApi(); + } + + public void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess) { + DatabaseManager.getInstance().getApi().confirmSetup(gameId, isWhite, board, onSuccess); + } + + public void unconfirmSetup(String gameId, boolean isWhite, Runnable onSuccess, FirebaseAPI.Callback onError) { + DatabaseManager.getInstance().getApi().unconfirmSetup(gameId, isWhite, onSuccess, onError); + } + + public void listenForBothSetupReady(String gameId, Runnable onBothReady) { + DatabaseManager.getInstance().getApi().listenForBothSetupReady(gameId, onBothReady); + } + + public void getOpponentBoard(String gameId, boolean localIsWhite, FirebaseAPI.Callback onBoard) { + DatabaseManager.getInstance().getApi().getOpponentBoard(gameId, localIsWhite, onBoard); + } + /** * @deprecated Use isReadyForConfirm() for UI readiness, or isPlayerReady() for Firebase readiness. */