diff --git a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java index c2a1eb1..35cc78b 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java @@ -43,4 +43,24 @@ public void addHostToPlayers( ) { sessionRepository.addHostToPlayers(sessionId, hostId, hostName, callback); } + + @Override + public void findSessionIdByGamePin(String gamePin, FindSessionCallback callback) { + sessionRepository.findSessionIdByGamePin(gamePin, callback); + } + + @Override + public void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + AddPlayerCallback callback + ) { + sessionRepository.addPlayerToSession(sessionId, playerId, playerName, callback); + } + + @Override + public void listenToPlayers(String sessionId, PlayersListenerCallback callback) { + sessionRepository.listenToPlayers(sessionId, callback); + } } diff --git a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java index b14d368..e654f65 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java @@ -2,11 +2,16 @@ import com.google.firebase.Timestamp; import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.ListenerRegistration; +import com.google.firebase.firestore.QuerySnapshot; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import group07.beatbattle.firebase.FirebaseGateway; +import group07.beatbattle.model.Player; public class FirestoreSessionRepository { @@ -70,4 +75,89 @@ public void addHostToPlayers( .addOnSuccessListener(unused -> callback.onSuccess()) .addOnFailureListener(callback::onFailure); } + + public void findSessionIdByGamePin( + String gamePin, + FirebaseGateway.FindSessionCallback callback + ) { + firestore.collection(SESSIONS) + .whereEqualTo("gamePin", gamePin) + .limit(1) + .get() + .addOnSuccessListener(snapshot -> handleSessionLookup(snapshot, callback)) + .addOnFailureListener(callback::onFailure); + } + + private void handleSessionLookup( + QuerySnapshot snapshot, + FirebaseGateway.FindSessionCallback callback + ) { + if (snapshot.isEmpty()) { + callback.onFailure(new IllegalArgumentException("Invalid game pin")); + return; + } + String sessionId = snapshot.getDocuments().get(0).getId(); + callback.onSuccess(sessionId); + } + + public void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + FirebaseGateway.AddPlayerCallback callback + ) { + Map player = new HashMap<>(); + player.put("name", playerName); + player.put("score", 0); + player.put("isHost", false); + + firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .document(playerId) + .set(player) + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } + + public ListenerRegistration listenToPlayers( + String sessionId, + FirebaseGateway.PlayersListenerCallback callback + ) { + return firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .addSnapshotListener((snapshot, error) -> { + if (error != null) { + callback.onFailure(error); + return; + } + + if (snapshot == null) { + callback.onPlayersChanged(new ArrayList<>()); + return; + } + + List players = new ArrayList<>(); + + snapshot.getDocuments().forEach(document -> { + String playerId = document.getId(); + String name = document.getString("name"); + if (name == null) { + name = "Unknown"; + } + + Player player = new Player(playerId, name); + + Long scoreValue = document.getLong("score"); + if (scoreValue != null) { + player.setScore(scoreValue.intValue()); + } + + players.add(player); + }); + + callback.onPlayersChanged(players); + }); + } } diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index fe3780e..08c2502 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -8,6 +8,7 @@ import group07.beatbattle.model.Player; import group07.beatbattle.model.Question; import group07.beatbattle.model.SessionCreationResult; +import group07.beatbattle.model.SessionJoinResult; import group07.beatbattle.model.Song; import group07.beatbattle.model.services.LobbyService; import group07.beatbattle.states.InRoundState; @@ -42,23 +43,60 @@ public void onReady( int totalRounds, JoinCreateView view ) { - if (mode != GameMode.CREATE) { - StateManager.getInstance().setState(new LobbyState(game, mode, this)); - return; - } - view.setLoadingState(true); - view.setStatusMessage("Creating session..."); LobbyService lobbyService = new LobbyService(game.getFirebaseGateway()); - lobbyService.createSession( + if (mode == GameMode.CREATE) { + view.setStatusMessage("Creating session..."); + + lobbyService.createSession( + playerName, + gamePin, + totalRounds, + new LobbyService.CreateSessionCallback() { + @Override + public void onSuccess(SessionCreationResult result) { + Gdx.app.postRunnable(() -> { + view.setLoadingState(false); + view.setStatusMessage(""); + StateManager.getInstance().setState( + new LobbyState( + game, + mode, + result.getSessionId(), + result.getGamePin(), + result.getHostName(), + LobbyController.this + ) + ); + }); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> { + view.setLoadingState(false); + view.setStatusMessage( + exception.getMessage() != null + ? exception.getMessage() + : "Failed to create session" + ); + }); + } + } + ); + return; + } + + view.setStatusMessage("Joining session..."); + + lobbyService.joinSession( playerName, gamePin, - totalRounds, - new LobbyService.CreateSessionCallback() { + new LobbyService.JoinSessionCallback() { @Override - public void onSuccess(SessionCreationResult result) { + public void onSuccess(SessionJoinResult result) { Gdx.app.postRunnable(() -> { view.setLoadingState(false); view.setStatusMessage(""); @@ -68,7 +106,7 @@ public void onSuccess(SessionCreationResult result) { mode, result.getSessionId(), result.getGamePin(), - result.getHostName(), + result.getPlayerName(), LobbyController.this ) ); @@ -82,7 +120,7 @@ public void onFailure(Exception exception) { view.setStatusMessage( exception.getMessage() != null ? exception.getMessage() - : "Failed to create session" + : "Failed to join session" ); }); } diff --git a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java index 4ed36d1..dcc30e1 100644 --- a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java @@ -1,5 +1,9 @@ package group07.beatbattle.firebase; +import java.util.List; + +import group07.beatbattle.model.Player; + public interface FirebaseGateway { void gamePinExists(String gamePin, GamePinExistsCallback callback); @@ -20,6 +24,20 @@ void addHostToPlayers( AddHostCallback callback ); + void findSessionIdByGamePin(String gamePin, FindSessionCallback callback); + + void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + AddPlayerCallback callback + ); + + void listenToPlayers( + String sessionId, + PlayersListenerCallback callback + ); + interface GamePinExistsCallback { void onResult(boolean exists); void onFailure(Exception exception); @@ -34,4 +52,19 @@ interface AddHostCallback { void onSuccess(); void onFailure(Exception exception); } + + interface FindSessionCallback { + void onSuccess(String sessionId); + void onFailure(Exception exception); + } + + interface AddPlayerCallback { + void onSuccess(); + void onFailure(Exception exception); + } + + interface PlayersListenerCallback { + void onPlayersChanged(List players); + void onFailure(Exception exception); + } } diff --git a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java index 324afc9..6af3658 100644 --- a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java @@ -4,7 +4,7 @@ public class NoOpFirebaseGateway implements FirebaseGateway { @Override public void gamePinExists(String gamePin, GamePinExistsCallback callback) { - callback.onResult(false); + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } @Override @@ -16,7 +16,7 @@ public void createSession( int totalRounds, CreateSessionCallback callback ) { - callback.onSuccess("desktop-session"); + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } @Override @@ -26,6 +26,29 @@ public void addHostToPlayers( String hostName, AddHostCallback callback ) { - callback.onSuccess(); + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void findSessionIdByGamePin( + String gamePin, + FindSessionCallback callback + ) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + AddPlayerCallback callback + ) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void listenToPlayers(String sessionId, PlayersListenerCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } } diff --git a/core/src/main/java/group07/beatbattle/model/Player.java b/core/src/main/java/group07/beatbattle/model/Player.java index 03c294b..c70a580 100644 --- a/core/src/main/java/group07/beatbattle/model/Player.java +++ b/core/src/main/java/group07/beatbattle/model/Player.java @@ -23,4 +23,8 @@ public void addScore(int points) { this.lastRoundScore = points; this.score += points; } + + public void setScore(int score) { + this.score = score; + } } diff --git a/core/src/main/java/group07/beatbattle/model/SessionJoinResult.java b/core/src/main/java/group07/beatbattle/model/SessionJoinResult.java new file mode 100644 index 0000000..9ea6bd4 --- /dev/null +++ b/core/src/main/java/group07/beatbattle/model/SessionJoinResult.java @@ -0,0 +1,31 @@ +package group07.beatbattle.model; + +public class SessionJoinResult { + private final String sessionId; + private final String gamePin; + private final String playerId; + private final String playerName; + + public SessionJoinResult(String sessionId, String gamePin, String playerId, String playerName) { + this.sessionId = sessionId; + this.gamePin = gamePin; + this.playerId = playerId; + this.playerName = playerName; + } + + public String getSessionId() { + return sessionId; + } + + public String getGamePin() { + return gamePin; + } + + public String getPlayerId() { + return playerId; + } + + public String getPlayerName() { + return playerName; + } +} diff --git a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java index 2763c71..4827d53 100644 --- a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java +++ b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java @@ -1,9 +1,12 @@ package group07.beatbattle.model.services; import java.util.Random; +import java.util.List; import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.model.SessionCreationResult; +import group07.beatbattle.model.SessionJoinResult; +import group07.beatbattle.model.Player; public class LobbyService { @@ -12,6 +15,11 @@ public interface CreateSessionCallback { void onFailure(Exception exception); } + public interface JoinSessionCallback { + void onSuccess(SessionJoinResult result); + void onFailure(Exception exception); + } + public interface GamePinCallback { void onSuccess(String gamePin); void onFailure(Exception exception); @@ -103,6 +111,61 @@ public void onFailure(Exception exception) { ); } + public void joinSession( + String playerName, + String gamePin, + JoinSessionCallback callback + ) { + try { + validatePlayerName(playerName); + validateGamePin(gamePin); + } catch (IllegalArgumentException exception) { + callback.onFailure(exception); + return; + } + + String trimmedPlayerName = playerName.trim(); + String normalizedGamePin = normalizeGamePin(gamePin); + String playerId = generatePlayerId(); + + firebaseGateway.findSessionIdByGamePin( + normalizedGamePin, + new FirebaseGateway.FindSessionCallback() { + @Override + public void onSuccess(String sessionId) { + firebaseGateway.addPlayerToSession( + sessionId, + playerId, + trimmedPlayerName, + new FirebaseGateway.AddPlayerCallback() { + @Override + public void onSuccess() { + callback.onSuccess( + new SessionJoinResult( + sessionId, + normalizedGamePin, + playerId, + trimmedPlayerName + ) + ); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + } + private void generateUniqueGamePin(GamePinCallback callback) { String gamePin = generateGamePin(); @@ -123,19 +186,51 @@ public void onFailure(Exception exception) { }); } + public interface PlayersCallback { + void onPlayersChanged(List players); + void onFailure(Exception exception); + } + + public void listenToPlayers(String sessionId, PlayersCallback callback) { + firebaseGateway.listenToPlayers( + sessionId, + new FirebaseGateway.PlayersListenerCallback() { + @Override + public void onPlayersChanged(List players) { + callback.onPlayersChanged(players); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + } + private void validateHostName(String hostName) { if (hostName == null || hostName.trim().isEmpty()) { throw new IllegalArgumentException("Host name cannot be empty"); } } + private void validatePlayerName(String playerName) { + if (playerName == null || playerName.trim().isEmpty()) { + throw new IllegalArgumentException("Player name cannot be empty"); + } + } + private void validateGamePin(String gamePin) { if (gamePin == null || gamePin.isBlank()) { throw new IllegalArgumentException("Game pin is missing"); } + + String normalized = normalizeGamePin(gamePin); + if (normalized.length() != GAME_PIN_LENGTH) { + throw new IllegalArgumentException("Game pin must be 4 characters"); + } } - private void validateTotalRounds(int totalRounds) { if (totalRounds <= 0) { throw new IllegalArgumentException("Number of rounds must be at least 1"); @@ -145,6 +240,10 @@ private void validateTotalRounds(int totalRounds) { } } + private String normalizeGamePin(String gamePin) { + return gamePin.trim().toUpperCase(); + } + private String generateGamePin() { StringBuilder pinBuilder = new StringBuilder(); @@ -159,4 +258,8 @@ private String generateGamePin() { private String generateHostId() { return "host-" + System.currentTimeMillis(); } + + private String generatePlayerId() { + return "player-" + System.currentTimeMillis(); + } } diff --git a/core/src/main/java/group07/beatbattle/view/JoinCreateView.java b/core/src/main/java/group07/beatbattle/view/JoinCreateView.java index a85a758..e83226e 100644 --- a/core/src/main/java/group07/beatbattle/view/JoinCreateView.java +++ b/core/src/main/java/group07/beatbattle/view/JoinCreateView.java @@ -38,6 +38,7 @@ public class JoinCreateView extends ScreenAdapter { private final LobbyService lobbyService; private TextField playerNameField; + private TextField gameCodeField; private Label statusLabel; private Label gameCodeLabel; private JoinCreateButton readyButton; @@ -68,10 +69,14 @@ public JoinCreateView(BeatBattle game, GameMode mode, LobbyController controller readyButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { + String gamePinValue = mode == GameMode.CREATE + ? generatedGamePin + : gameCodeField.getText(); + controller.onReady( mode, playerNameField.getText(), - generatedGamePin, + gamePinValue, roundSelector.getSelectedRounds(), JoinCreateView.this ); @@ -129,6 +134,10 @@ public void onFailure(Exception exception) { public void setLoadingState(boolean isLoading) { readyButton.setDisabled(isLoading); playerNameField.setDisabled(isLoading); + + if (gameCodeField != null) { + gameCodeField.setDisabled(isLoading); + } } public void setStatusMessage(String message) { @@ -174,9 +183,9 @@ private Label createGeneratedCodeLabel() { } private TextField createCodeInputField() { - TextField codeField = new TextField("", InputFieldStyles.createDefault(game.getMontserratFont())); - codeField.setMessageText("Enter game code"); - return codeField; + gameCodeField = new TextField("", InputFieldStyles.createDefault(game.getMontserratFont())); + gameCodeField.setMessageText("Enter game code"); + return gameCodeField; } @Override diff --git a/core/src/main/java/group07/beatbattle/view/LobbyView.java b/core/src/main/java/group07/beatbattle/view/LobbyView.java index fac418a..45e3125 100644 --- a/core/src/main/java/group07/beatbattle/view/LobbyView.java +++ b/core/src/main/java/group07/beatbattle/view/LobbyView.java @@ -16,9 +16,13 @@ import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.viewport.ScreenViewport; +import java.util.List; + import group07.beatbattle.BeatBattle; import group07.beatbattle.controller.LobbyController; import group07.beatbattle.model.GameMode; +import group07.beatbattle.model.Player; +import group07.beatbattle.model.services.LobbyService; import group07.beatbattle.ui.components.BackButton; import group07.beatbattle.ui.components.SettingsButton; import group07.beatbattle.ui.components.StartGameButton; @@ -29,6 +33,7 @@ public class LobbyView extends ScreenAdapter { private final GameMode mode; private final LobbyController controller; private final Stage stage; + private final LobbyService lobbyService; private final String sessionId; private final String gamePin; @@ -37,6 +42,9 @@ public class LobbyView extends ScreenAdapter { private Label.LabelStyle infoStyle; private Label.LabelStyle titleStyle; + private Table playersGrid; + private Label statusLabel; + public LobbyView(BeatBattle game, GameMode mode, LobbyController controller) { this(game, mode, "", "", "", controller); } @@ -46,6 +54,7 @@ public LobbyView(BeatBattle game, GameMode mode, String sessionId, String gamePi this.mode = mode; this.controller = controller; this.stage = new Stage(new ScreenViewport()); + this.lobbyService = new LobbyService(game.getFirebaseGateway()); this.sessionId = sessionId; this.gamePin = gamePin; this.hostName = hostName; @@ -69,6 +78,35 @@ public LobbyView(BeatBattle game, GameMode mode, String sessionId, String gamePi root.add(createFooter()).expandX().bottom().padBottom(40); stage.addActor(root); + + startListeningToPlayers(); + } + + private void startListeningToPlayers() { + if (sessionId == null || sessionId.isBlank()) { + setStatusMessage("Missing session id"); + return; + } + + lobbyService.listenToPlayers( + sessionId, + new LobbyService.PlayersCallback() { + @Override + public void onPlayersChanged(List players) { + Gdx.app.postRunnable(() -> { + updatePlayersGrid(players); + setStatusMessage(""); + }); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> { + setStatusMessage("Failed to load players"); + }); + } + } + ); } private Table createHeader() { @@ -147,20 +185,53 @@ private Table createPartyBox() { Table container = new Table(); container.setBackground(createRoundedBackground(boxWidth, boxHeight, radius)); - Table grid = new Table(); + Table content = new Table(); + + playersGrid = new Table(); + statusLabel = new Label("Loading players...", infoStyle); + statusLabel.setAlignment(Align.center); + + content.add(playersGrid).expand().center().row(); + content.add(statusLabel).padTop(20); + + container.add(content).expand().fill().pad(60); + + return container; + } + + private void updatePlayersGrid(List players) { + playersGrid.clear(); float cellWidth = 260f; float cellHeight = 60f; - grid.add(createPlayerLabel("Oda")).width(cellWidth).height(cellHeight).pad(15); - grid.add(createPlayerLabel("Lea")).width(cellWidth).height(cellHeight).pad(15).row(); + if (players == null || players.isEmpty()) { + playersGrid.add(createPlayerLabel("No players yet")) + .width(cellWidth) + .height(cellHeight) + .pad(15); + return; + } - grid.add(createPlayerLabel("Milos")).width(cellWidth).height(cellHeight).pad(15); - grid.add(createPlayerLabel("Karya")).width(cellWidth).height(cellHeight).pad(15); + int columnCount = 2; + for (int i = 0; i < players.size(); i++) { + Player player = players.get(i); - container.add(grid).pad(60); + playersGrid.add(createPlayerLabel(player.getName())) + .width(cellWidth) + .height(cellHeight) + .pad(15); - return container; + if ((i + 1) % columnCount == 0) { + playersGrid.row(); + } + } + } + + private void setStatusMessage(String message) { + if (statusLabel != null) { + statusLabel.setText(message); + } } private Label createPlayerLabel(String name) {