Skip to content

refactor: enforce GUI→Logic→Database layer separation #61

Merged
merged 1 commit into from
Apr 8, 2026
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

// ─────────────────────────────────────────────────────────────────────────
Expand All @@ -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);
}

// ─────────────────────────────────────────────────────────────────────────
Expand All @@ -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

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

Expand All @@ -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();
Expand All @@ -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);
}

/**
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand All @@ -44,6 +46,7 @@ public MainMenuScreen(Game game, SpriteBatch batch) {

// Initialize state
this.mainMenuState = new MainMenuState();
this.lobbyState = new LobbyState();
mainMenuState.enter();

// LibGDX setup
Expand All @@ -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());
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -83,7 +82,7 @@ public void unconfirm() {
setupState.getPlayer().resetReady();
isConfirmed = false;

DatabaseManager.getInstance().getApi().unconfirmSetup(
setupState.unconfirmSetup(
gameId,
localPlayer.isWhite(),
() -> Gdx.app.postRunnable(() -> {
Expand All @@ -99,7 +98,7 @@ public void unconfirm() {
}

private void listenForBothReady() {
DatabaseManager.getInstance().getApi().listenForBothSetupReady(
setupState.listenForBothSetupReady(
gameId,
() -> Gdx.app.postRunnable(() -> {
listener.onBothReady();
Expand All @@ -113,7 +112,7 @@ private void fetchOpponentBoardWithRetry() {
}

private void fetchOpponentBoard() {
DatabaseManager.getInstance().getApi().getOpponentBoard(
setupState.getOpponentBoard(
gameId,
localPlayer.isWhite(),
opponentBoard -> Gdx.app.postRunnable(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -64,6 +65,22 @@ public void setPrefetchedLobby(Lobby prefetched) {
}
}

public void validateLobby(String gameId, FirebaseAPI.Callback<Lobby> onSuccess, FirebaseAPI.Callback<String> 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; }
Expand Down
Loading