Skip to content

Ensure documentation is correct #62

Merged
merged 2 commits into from
Apr 21, 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
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,21 @@ BeatBattle/
│ │ ├── LeaderboardController.java
│ │ └── SettingsController.java
│ ├── model/
│ │ ├── domain/ # GameSession, Player, Question, Song, GameRules, …
│ │ ├── domain/ # GameSession, Player, Question, Song, GameRules, AnswerSubmission,
│ │ ├── dto/ # SessionCreationResult, SessionJoinResult
│ │ ├── score/ # ScoreCalculator
│ │ └── services/ # LobbyService, MusicService
│ │ └── services/ # LobbyService, MusicService, QuestionBuilder, SessionSyncService
│ ├── ecs/
│ │ ├── Engine.java
│ │ ├── components/ # TimerComponent, AudioComponent
│ │ ├── systems/ # RoundSystem, AudioSystem
│ │ └── entities/ # Entity, RoundFactory
│ ├── states/ # StateManager + Start/Lobby/InRound/Leaderboard/GameOver states
│ ├── states/ # StateManager + Start/JoinCreate/Lobby/InRound/Leaderboard/GameOver states
│ ├── view/ # One ScreenAdapter per state
│ ├── ui/ # Reusable Scene2D components & dialogs
│ ├── ui/
│ │ ├── components/ # Reusable Scene2D buttons and selectors
│ │ ├── dialog/ # AlertDialogs
│ │ └── style/ # InputFieldStyles
│ ├── utils/ # ScoreCalculator, Strings
│ ├── firebase/ # FirebaseGateway interface + NoOpFirebaseGateway
│ └── audio/ # AudioPlayer interface
└── assets/ # Fonts, images
Expand Down
8 changes: 3 additions & 5 deletions core/src/main/java/group07/beatbattle/BeatBattle.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@
import group07.beatbattle.ui.style.InputFieldStyles;

/**
* Root libGDX Game class and central dependency holder for BeatBattle.
* Loads and owns the shared fonts (Montserrat, Orbitron, Oswald).
* Holds platform-injected services: FirebaseGateway, MusicService, AudioPlayer.
* Stores the local player's identity, session membership, and the active GameSession.
* Platform dependencies are injected by AndroidLauncher before create() is called.
* Root LibGDX Game class. Owns shared fonts, platform-injected services
* (FirebaseGateway, MusicService, AudioPlayer), and the local player's session state.
* AndroidLauncher injects platform dependencies before create() is called.
*/
public class BeatBattle extends Game {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,16 @@

/**
* Controls the leaderboard screen shown between rounds and at game over.
* Views that need to react to session cancellation should call
* startSessionStateListener() rather than accessing FirebaseGateway directly.
*
* Host behaviour: onNextRound() writes the new round index to Firestore before
* launching the next RoundController, so joiners are kept in sync.
* onGameOver() writes "game_over" to Firestore then transitions to GameOverState.
* Host: onNextRound() writes the new round index to Firestore before launching the next round,
* and onGameOver() writes "game_over" before transitioning to GameOverState.
*
* Joiner behaviour: joiners do not call onNextRound() or onGameOver() directly —
* they follow the host via the session-state listener started in LobbyController.
* Joiners do not call onNextRound() or onGameOver() directly — they follow the host
* via the session-state listener started in SessionSyncService.
*/
public class LeaderboardController {

/** Callback for views to react to session cancellation without importing FirebaseGateway. */
/** Lets views react to session cancellation without accessing FirebaseGateway directly. */
public interface SessionCancelledCallback {
void onHostLeft();
void onCancelled();
Expand Down Expand Up @@ -85,7 +82,7 @@ public void stopPlayerListener() {
onPlayersChangedCallback = null;
}

/** Returns the leaderboard entry for the local player, or null if not found. */
/** Returns the leaderboard entry for the local player, or null if the player is not on the board. */
public LeaderboardEntry getLocalPlayerEntry() {
String localId = game.getLocalPlayerId();
if (localId == null && !session.getPlayers().isEmpty()) {
Expand Down Expand Up @@ -146,10 +143,7 @@ private void launchNextRound() {
StateManager.getInstance().setState(new InRoundState(game, roundController));
}

/**
* Registers a Firestore session-state listener on behalf of the view.
* Only registers for the host; joiners are already handled by SessionSyncService.
*/
/** Starts listening for session cancellation on behalf of the view (host only). Joiners are already handled by SessionSyncService. */
public void startSessionStateListener(SessionCancelledCallback callback) {
if (!game.isLocalHost()) return;
String sessionId = game.getLocalSessionId();
Expand Down Expand Up @@ -250,11 +244,7 @@ public void onFailure(Exception exception) {
}
}

/**
* Schedules deletion of the session document 5 seconds after the host has cancelled it.
* This gives all connected clients time to receive the cancellation event and show
* the "host left" dialog before the Firestore document disappears.
*/
/** Deletes the session from Firestore after a short delay so all clients can receive the cancellation event first. */
private void scheduleSessionDeletion(String sessionId) {
Timer.schedule(new Timer.Task() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,25 @@
import group07.beatbattle.view.LobbyView;

/**
* Orchestrates pre-game and inter-round navigation for both the host and joiners.
* Handles session creation, joining, leaving, and the transition into the first round.
*
* Host path: onStartGame() fetches tracks from Deezer, delegates to QuestionBuilder
* which builds questions, stores them in Firestore, and sets state to "in_round".
* Host: onStartGame() fetches tracks from Deezer, builds questions via QuestionBuilder,
* stores them in Firestore, and navigates to the first round.
*
* Joiner path: onGameStarted() reads questions from Firestore and delegates to
* SessionSyncService which starts a persistent listener routing the joiner through
* all game states driven by what the host writes to Firestore.
* Joiner: onGameStarted() fetches questions from Firestore and starts SessionSyncService,
* which listens for host state changes and navigates the joiner through all subsequent screens.
*/
public class LobbyController {

private final BeatBattle game;
private int numRounds = 5;

/**
* Callback interface that lets LobbyView react to session state changes
* without importing or knowing about FirebaseGateway.
*/
/** Lets LobbyView react to session state changes without accessing FirebaseGateway directly. */
public interface LobbySessionCallback {
/** Returns the current player roster so the controller can pass it to onGameStarted. */
List<Player> getCurrentPlayers();
/** Called when the host cancelled the session. hostLeft=true means the host left explicitly. */
void onHostCancelled(boolean hostLeft);
/** Called when STATE_IN_ROUND is detected and question fetching begins. */
void onGameStarting();
/** Called when question fetching fails; the listener resets so it can retry. */
void onGameStartFailed();
/** Called when the Firestore listener itself errors. */
void onConnectionError(String error);
}

Expand Down Expand Up @@ -235,10 +226,7 @@ public void onFailure(Exception exception) {
);
}

/**
* Registers a Firestore session-state listener on behalf of LobbyView (joiner only).
* The view is notified through LobbySessionCallback without ever touching FirebaseGateway.
*/
/** Starts listening to session state for a joiner. Notifies the view via LobbySessionCallback when the game starts or is cancelled. */
public void startSessionStateListener(String sessionId, LobbySessionCallback callback) {
final boolean[] gameStarted = {false};

Expand Down Expand Up @@ -286,7 +274,7 @@ public void onFailure(Exception exception) {
);
}

/** Called by the host when they press Start Game. */
/** Called when the host presses Start Game. Fetches tracks from Deezer and builds the session. */
public void onStartGame(String sessionId, List<Player> players, int rounds) {
numRounds = rounds;
game.getMusicService().fetchTracks(numRounds * QuestionBuilder.OPTIONS_PER_Q, new MusicServiceCallback() {
Expand Down Expand Up @@ -318,7 +306,7 @@ public void onFailure(String error) {
});
}

/** Called by joiners when Firebase signals game has started (via startSessionStateListener). */
/** Called when Firebase signals the game has started. Builds the local session from Firestore questions and starts round 0. */
public void onGameStarted(
String sessionId,
List<FirebaseGateway.QuestionData> questionDataList,
Expand Down Expand Up @@ -356,11 +344,7 @@ public void onGameStarted(
StateManager.getInstance().setState(new InRoundState(game, roundController));
}

/**
* Schedules deletion of the session document 5 seconds after the host has cancelled it.
* This gives all connected clients time to receive the cancellation event and show
* the "host left" dialog before the Firestore document disappears.
*/
/** Deletes the session from Firestore after a short delay so all clients can receive the cancellation event first. */
private void scheduleSessionDeletion(String sessionId) {
Timer.schedule(new Timer.Task() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,18 @@

/**
* Controls the lifecycle of a single game round.
* On construction creates a RoundManager (which owns the ECS entity), starts audio
* playback, and (if host) starts a Firestore listener that advances the game as soon
* as all players have answered.
* Creates a RoundManager on construction, which starts audio playback via ECS.
* The host also starts a Firestore listener that triggers early leaderboard transition
* when all players have answered.
*
* Flow:
* 1. Player answers -> onAnswerSubmitted() scores it and records it in Firebase.
* 2. Timer expires (host only) -> onRoundExpired() triggers the leaderboard transition.
* 3. Leaderboard transition fetches fresh scores from Firebase then hands off to LeaderboardController.
* 1. Player answers: onAnswerSubmitted() scores the answer via AnswerSubmission, updates the local player, and records it in Firebase.
* 2. Timer expires (host only): onRoundExpired() triggers transition to the leaderboard.
* 3. Transition fetches updated scores from Firebase before handing off to LeaderboardController.
*/
public class RoundController {

/**
* Callback for GameRoundView to react to session cancellation without
* knowing about Firebase constants or the gateway.
*/
/** Lets GameRoundView react to session cancellation without accessing FirebaseGateway directly. */
public interface SessionCancelledCallback {
void onHostLeft();
void onCancelled();
Expand Down Expand Up @@ -87,11 +84,7 @@ public void setMuted(boolean muted) {
roundManager.setMuted(muted);
}

/**
* Registers a Firestore session-state listener on behalf of GameRoundView.
* Only registers for the host; joiners already have a persistent sync listener
* from SessionSyncService that handles STATE_CANCELLED.
*/
/** Starts listening for session cancellation on behalf of GameRoundView (host only). Joiners are already handled by SessionSyncService. */
public void startSessionStateListener(SessionCancelledCallback callback) {
if (!game.isLocalHost()) return;
String sessionId = game.getLocalSessionId();
Expand Down Expand Up @@ -135,7 +128,7 @@ public void onAnswerSubmitted(String answer) {
recordAnswerInFirebase();
}

/** Called by GameRoundView when the timer runs out (host only). */
/** Called when the round timer runs out. Submits a no-answer if the player never responded, then transitions to the leaderboard. */
public void onRoundExpired() {
if (!playerAnswered) {
Player localPlayer = findLocalPlayer();
Expand Down Expand Up @@ -226,10 +219,7 @@ public void onFailure(Exception exception) {

// --- Private helpers ---

/**
* Host listens for the answer count for this round.
* When every player has answered, advance immediately without waiting for the timer.
*/
/** Host listens for the answer count. When all players have answered, advances to the leaderboard without waiting for the timer. */
private void startAllAnsweredListener() {
String sessionId = game.getLocalSessionId();
if (sessionId == null || sessionId.isBlank()) return;
Expand Down Expand Up @@ -383,11 +373,7 @@ private void launchLeaderboard() {
StateManager.getInstance().setState(new LeaderboardState(game, leaderboardController));
}

/**
* Schedules deletion of the session document 5 seconds after the host has cancelled it.
* This gives all connected clients time to receive the cancellation event and show
* the "host left" dialog before the Firestore document disappears.
*/
/** Deletes the session from Firestore after a short delay so all clients can receive the cancellation event first. */
private void scheduleSessionDeletion(String sessionId) {
Timer.schedule(new Timer.Task() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import group07.beatbattle.states.StateManager;
import group07.beatbattle.view.LobbyView;

/**
* Controls the settings screen. Saves language and round count to persistent preferences
* and reloads the string table when the player applies changes. Navigates back to the
* lobby on back, or to the start screen if no active session exists.
*/
public class SettingsController {

private final BeatBattle game;
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/group07/beatbattle/ecs/Engine.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import java.util.ArrayList;
import java.util.List;

/**
* Singleton ECS engine. Holds all active entities and systems,
* and drives per-frame updates by calling each system's update() method.
*/
public class Engine {
private static Engine instance;
private final List<Entity> entities = new ArrayList<>();
Expand Down
7 changes: 3 additions & 4 deletions core/src/main/java/group07/beatbattle/ecs/RoundManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import group07.beatbattle.ecs.systems.AudioSystem;

/**
* Wraps the ECS lifecycle for a single game round.
* Hides Engine, Entity, and component access from RoundController so
* the controller layer does not depend on ECS internals.
* Manages the ECS entity for a single game round: creates it, starts audio playback,
* and cleans it up when the round ends. Shields RoundController from ECS internals.
*/
public class RoundManager {

Expand All @@ -30,7 +29,7 @@ public float getTotalTime() {
return timer != null ? timer.totalTime : 30f;
}

/** Stops audio playback and removes the entity from the ECS engine. */
/** Stops audio and removes the round entity from the ECS engine. */
public void stop() {
AudioSystem.getInstance().stop(roundEntity);
Engine.getInstance().removeEntity(roundEntity);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package group07.beatbattle.ecs.components;

/** Holds the song preview URL and playback state for a round entity. */
public class AudioComponent implements Component {
public String previewUrl;
public boolean isPlaying;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package group07.beatbattle.ecs.components;

/** Holds the countdown timer for a round. timeRemaining is decremented each frame by RoundSystem. */
public class TimerComponent implements Component {
public float timeRemaining;
public float totalTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.HashMap;
import java.util.Map;

/** An ECS entity identified by a string ID. Holds a set of components keyed by their class. */
public class Entity {
private final String id;
private final Map<Class<? extends Component>, Component> components = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@

/**
* ECS system that controls music preview playback during game rounds.
* Playback is event-driven via play/stop calls rather than running on every tick.
* Mute state is stored here so it persists across round transitions,
* GameRoundView reads it via isMuted() on construction.
* Playback is event-driven: play() and stop() are called explicitly rather than on every tick.
* Mute state persists across round transitions; GameRoundView reads it via isMuted() on construction.
*/
public class AudioSystem extends EntitySystem {
private static AudioSystem instance;
Expand All @@ -30,7 +29,7 @@ public void setAudioPlayer(AudioPlayer audioPlayer) {

@Override
public void update(List<Entity> entities, float delta) {
// playback is event-driven, triggered by play/stop calls
// no per-frame logic — playback is driven by explicit play/stop calls
}

public void play(Entity round) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import group07.beatbattle.ecs.entities.Entity;
import java.util.List;

/** Base class for all ECS systems. update() is called every frame with the full list of active entities. */
public abstract class EntitySystem {
public abstract void update(List<Entity> entities, float delta);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import group07.beatbattle.ecs.entities.Entity;
import java.util.List;

/**
* ECS system that counts down each entity's TimerComponent every frame.
* When a timer reaches zero it fires the RoundExpiredListener once.
*/
public class RoundSystem extends EntitySystem {
private static RoundSystem instance;
private RoundExpiredListener listener;
Expand Down
Loading
Loading