Skip to content

Docs #52

Merged
merged 2 commits into from
Apr 16, 2026
Merged

Docs #52

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 @@ -8,6 +8,12 @@

import group07.beatbattle.audio.AudioPlayer;

/**
* Android AudioPlayer implementation — streams audio from a URL using MediaPlayer.
* Audio is tagged as USAGE_GAME so the system routes it through the game audio stream.
* Playback starts asynchronously via prepareAsync(); mute state is preserved across
* player lifecycle changes (i.e. calling setMuted before play still takes effect).
*/
public class AndroidAudioPlayer implements AudioPlayer {

private final Context context;
Expand All @@ -18,6 +24,11 @@ public AndroidAudioPlayer(Context context) {
this.context = context;
}

/**
* Stops any current track and starts streaming from the given URL.
*
* @param url the HTTP(S) URL of the audio preview to stream
*/
@Override
public void play(String url) {
stop();
Expand All @@ -41,7 +52,9 @@ public void play(String url) {
Gdx.app.error("AndroidAudioPlayer", "Failed to play: " + url, e);
}
}

/**
* Stops any current track
*/
@Override
public void stop() {
if (mediaPlayer != null) {
Expand All @@ -50,7 +63,11 @@ public void stop() {
mediaPlayer = null;
}
}

/**
* Mutes or unmutes the current track. Uses mediaplayer volume control.
*
* @param muted true to mute, false to unmute
*/
@Override
public void setMuted(boolean muted) {
this.muted = muted;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,25 @@
import java.util.Collections;
import java.util.List;

/**
* Fetches song tracks from the Deezer public API for use as quiz questions.
* Hits the global chart endpoint to retrieve up to 50 trending tracks, shuffles them,
* and returns the requested number. Only songs with a preview URL are included.
*
* Note: the chart endpoint always returns the same pool of top-50 songs,
* so variety is limited to shuffle order. Consider the search or genre endpoints for more diversity.
*/
public class DeezerMusicService implements MusicService {

/** Deezer global chart endpoint — returns the current top 50 tracks. */
private static final String CHART_URL = "https://api.deezer.com/chart/0/tracks?limit=50";

/**
* Fetches tracks on a background thread and posts results back to the GL thread.
*
* @param count max number of tracks to return
* @param callback delivers the track list on success, or an error message on failure
*/
@Override
public void fetchTracks(int count, MusicServiceCallback callback) {
new Thread(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

import group07.beatbattle.firebase.FirebaseGateway;

/**
* Android implementation of FirebaseGateway.
* Thin delegation layer, every method forwards to FirestoreSessionRepository.
* Separating this from the repository makes it easy to add more data sources later
* (e.g. analytics, auth) without touching core code.
*/
public class AndroidFirebaseGateway implements FirebaseGateway {

private final FirestoreSessionRepository sessionRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
import group07.beatbattle.firebase.FirebaseGateway;
import group07.beatbattle.model.Player;

/**
* Direct Firestore data-access layer for all session-related operations.
*
* Collections:
* Sessions — top-level game state, pin, round index
* Sessions/Players — per-player name, score, host flag
* Sessions/Questions — round questions stored by the host at game start
* Sessions/Answers — one doc per player per round, used to detect when everyone has answered
*
* Real-time listeners (listenToPlayers, listenToSessionState, listenToRoundAnswerCount)
* stay active until the app process ends or the registration is explicitly removed.
*/
public class FirestoreSessionRepository {

private static final String SESSIONS = "Sessions";
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/java/group07/beatbattle/BeatBattle.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
import group07.beatbattle.ui.components.JoinCreateButton;
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.
*/
public class BeatBattle extends Game {

private final FirebaseGateway firebaseGateway;
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/group07/beatbattle/audio/AudioPlayer.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
package group07.beatbattle.audio;

/**
* Interface for streaming audio previews during a game round.
* Implemented by AndroidAudioPlayer on Android, and NoOpAudioPlayer on desktop.
*/
public interface AudioPlayer {
/** Starts streaming audio from the given URL. Stops any current track first. */
void play(String url);

/** Stops and releases the current track. */
void stop();

/**
* Mutes or unmutes playback without stopping the stream.
*
* @param muted true to mute, false to unmute
*/
void setMuted(boolean muted);

/** Releases all resources. Call when the app is shutting down. */
void dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
import group07.beatbattle.states.StartState;
import group07.beatbattle.states.StateManager;

/**
* Controls the leaderboard screen shown between rounds and at game over.
*
* 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.
*
* Joiner behaviour: joiners do not call onNextRound() or onGameOver() directly —
* they follow the host via the session-state listener started in LobbyController.
*/
public class LeaderboardController {

private final BeatBattle game;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
import group07.beatbattle.view.JoinCreateView;
import group07.beatbattle.view.LobbyView;

/**
* Orchestrates pre-game and inter-round navigation for both the host and joiners.
*
* Host path: onStartGame() fetches tracks from Deezer, builds questions, stores them
* in Firestore, sets state to "in_round", then launches the first RoundController.
*
* Joiner path: onGameStarted() reads questions from Firestore and starts a persistent
* session-state listener that routes the joiner through leaderboard -> next round -> game over,
* driven entirely by what the host writes to Firestore.
*/
public class LobbyController {

private static final int OPTIONS_PER_Q = 4;
Expand Down Expand Up @@ -259,8 +269,8 @@ public void onGameStarted(
}

/**
* Joiners: single Firestore listener that routes through leaderboard
* next round game over, driven by what the host writes.
* Joiners: single Firestore listener that routes through leaderboard ->
* next round -> game over, driven by what the host writes.
*/
private void startJoinerSyncListener(String sessionId, GameSession session) {
// Track the last state+round we handled to avoid duplicate transitions.
Expand Down Expand Up @@ -384,7 +394,7 @@ private void buildAndStartSession(String sessionId, List<Player> sessionPlayers,

game.setCurrentSession(session);

// Store questions update state launch round
// Store questions -> update state -> launch round
game.getFirebaseGateway().storeQuestions(
sessionId,
questionDataList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@
import group07.beatbattle.states.StartState;
import group07.beatbattle.states.StateManager;

/**
* Controls the lifecycle of a single game round.
* On construction creates an ECS round entity, starts audio playback, and (if host)
* starts a Firestore listener that advances the game as soon as 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.
*/
public class RoundController {

private final BeatBattle game;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import group07.beatbattle.ecs.components.TimerComponent;
import group07.beatbattle.model.GameRules;

/**
* Creates ECS round entities used by AudioSystem and RoundSystem.
* Each entity gets an AudioComponent (song preview URL) and a TimerComponent (round duration).
*/
public class RoundFactory {
private static RoundFactory instance;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

import java.util.List;

/**
* 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.
*/
public class AudioSystem extends EntitySystem {
private static AudioSystem instance;
private AudioPlayer audioPlayer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

import group07.beatbattle.model.Player;

/**
* Abstraction layer over the Firebase/Firestore backend.
* All methods are async and deliver results via callback interfaces defined as inner types.
* Keeps Firebase SDK code out of the core module.
* NoOpFirebaseGateway is used for the desktop/LWJGL3 build where Firebase is unavailable.
*
* Firestore data model:
* Sessions/{sessionId}
* Players/{playerId} — name, score, isHost
* Questions/{roundIndex} — songId, songTitle, songArtist, previewUrl, options[]
* Answers/{round}_{player} — roundIndex, playerId
*/
public interface FirebaseGateway {

void gamePinExists(String gamePin, GamePinExistsCallback callback);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package group07.beatbattle.model;

/**
* Immutable record of a player's answer for a single round.
* Stores what they answered, when they answered, whether it was correct, and how many points were awarded.
*/
public class AnswerSubmission {

private final String playerId;
Expand Down
1 change: 1 addition & 0 deletions core/src/main/java/group07/beatbattle/model/GameMode.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package group07.beatbattle.model;

/** Whether the local player is creating a new session or joining an existing one. */
public enum GameMode {
JOIN,
CREATE
Expand Down
1 change: 1 addition & 0 deletions core/src/main/java/group07/beatbattle/model/GameRules.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package group07.beatbattle.model;

/** Central place for game constants shared across systems (e.g. round duration). */
public final class GameRules {

public static final float ROUND_DURATION_SECONDS = 30f;
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/group07/beatbattle/model/GameSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
import java.util.Collections;
import java.util.List;

/**
* Holds all state for an active game session: players, questions, and round progress.
* Created by the host when the game starts and shared (via Firestore) with all joiners.
* currentRoundIndex tracks which question is active and advances via advanceRound().
*/
public class GameSession {

public enum State {
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/group07/beatbattle/model/Leaderboard.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import java.util.Comparator;
import java.util.List;

/**
* Sorted list of LeaderboardEntry objects built from the current player scores.
* Players are ranked highest score first on construction.
*/
public class Leaderboard {

private final List<LeaderboardEntry> entries;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package group07.beatbattle.model;

/**
* A single row on the leaderboard.
* Holds the player's total score, their score for the last round, and their rank position.
*/
public class LeaderboardEntry {

private final String playerId;
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/group07/beatbattle/model/Player.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package group07.beatbattle.model;

/**
* Represents a player in a game session.
* Tracks cumulative score across all rounds and the score earned in the most recent round.
*/
public class Player {

private final String id;
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/group07/beatbattle/model/Question.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import java.util.List;

/**
* A single quiz question for one round.
* Contains the correct song, a list of answer options (shuffled, including the correct title),
* and the round index it belongs to. The correct answer is always the song's title.
*/
public class Question {

private final String id;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package group07.beatbattle.model;

/** Result returned by LobbyService after successfully creating a session. */
public class SessionCreationResult {
private final String sessionId;
private final String gamePin;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package group07.beatbattle.model;

/** Result returned by LobbyService after successfully joining a session. */
public class SessionJoinResult {
private final String sessionId;
private final String gamePin;
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/group07/beatbattle/model/Song.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package group07.beatbattle.model;

/**
* Represents a song fetched from the Deezer API.
* Holds metadata (title, artist, album art) and the 30-second preview URL used for playback.
*/
public class Song {

private final String id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

import group07.beatbattle.model.GameRules;

/**
* Calculates the score awarded for a round based on answer correctness and speed.
*
* Scoring rules:
* - Wrong answer: 0 points.
* - Correct answer within the grace period (first 3 seconds): 1000 points.
* - Correct answer after the grace period: linear decay from 1000 down to 300,
* reaching 300 at the end of the round timer.
*/
public class ScoreCalculator {

private static final int MAX_POINTS = 1000;
private static final int MIN_POINTS = 300;
private static final double ROUND_TIME_SECONDS = GameRules.ROUND_DURATION_SECONDS;

private static final double GRACE_PERIOD_SECONDS = 5.0;
private static final double GRACE_PERIOD_SECONDS = 3.0;

public static int calculateScore(boolean isCorrect, double answerTimeSeconds) {
if (!isCorrect) {
Expand Down Expand Up @@ -38,7 +47,7 @@ public static int calculateScore(boolean isCorrect, double answerTimeSeconds) {

public static void main(String[] args) {
System.out.println(calculateScore(true, 0.0)); // 1000
System.out.println(calculateScore(true, 5.0)); // breaking point for high score
System.out.println(calculateScore(true, 3.0)); // breaking point for high score
System.out.println(calculateScore(true, 15.0)); // midpoint (500)
System.out.println(calculateScore(true, 29.9)); // 300
System.out.println(calculateScore(false, 10.0)); // 0
Expand Down
Loading
Loading