Skip to content

Commit

Permalink
Merge pull request #52 from milospi/docs
Browse files Browse the repository at this point in the history
Docs
  • Loading branch information
mostafas authored Apr 16, 2026
2 parents 07358f0 + b648244 commit ec4e9ec
Show file tree
Hide file tree
Showing 36 changed files with 206 additions and 7 deletions.
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 @@ -15,6 +15,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 @@ -27,6 +27,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 @@ -262,8 +272,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 @@ -393,7 +403,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 @@ -19,6 +19,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 {

String STATE_LOBBY = "lobby";
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

0 comments on commit ec4e9ec

Please sign in to comment.