Skip to content

Commit

Permalink
Merge branch 'development' into feature/circuit-breaker
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasew authored Apr 8, 2026
2 parents defafb4 + aca5c04 commit 96edb8d
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
public class AndroidFirebase implements FirebaseAPI, API {

private final DatabaseReference db = FirebaseDatabase.getInstance().getReference();
private final List<Runnable> cleanupActions = new ArrayList<>();

private ValueEventListener trackValue(DatabaseReference ref, ValueEventListener listener) {
cleanupActions.add(() -> ref.removeEventListener(listener));
return listener;
}

private ChildEventListener trackChild(DatabaseReference ref, ChildEventListener listener) {
cleanupActions.add(() -> ref.removeEventListener(listener));
return listener;
}

// ── API (Legacy) ──────────────────────────────────────────────────────────

Expand Down Expand Up @@ -74,8 +85,8 @@ public void joinLobby(String gameId, Callback<Lobby> onSuccess, Callback<String>

@Override
public void listenForOpponentReady(String gameId, Runnable onReady) {
db.child("lobbies").child(gameId).child("status")
.addValueEventListener(new ValueEventListener() {
DatabaseReference statusRef = db.child("lobbies").child(gameId).child("status");
statusRef.addValueEventListener(trackValue(statusRef, new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
if ("joined".equals(snapshot.getValue(String.class))) {
Expand All @@ -84,7 +95,7 @@ public void onDataChange(DataSnapshot snapshot) {
}
}
@Override public void onCancelled(DatabaseError e) {}
});
}));
}

@Override
Expand All @@ -94,8 +105,8 @@ public void startGame(String gameId) {

@Override
public void listenForGameStart(String gameId, Runnable onStart) {
db.child("lobbies").child(gameId).child("status")
.addValueEventListener(new ValueEventListener() {
DatabaseReference statusRef = db.child("lobbies").child(gameId).child("status");
statusRef.addValueEventListener(trackValue(statusRef, new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
if ("started".equals(snapshot.getValue(String.class))) {
Expand All @@ -104,13 +115,13 @@ public void onDataChange(DataSnapshot snapshot) {
}
}
@Override public void onCancelled(DatabaseError e) {}
});
}));
}

// ── Setup ─────────────────────────────────────────────────────────────────

@Override
public void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess) {
public void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess, Callback<String> onError) {
String player = isWhite ? "white" : "black";
DatabaseReference gameRef = db.child("games").child(gameId);

Expand Down Expand Up @@ -141,11 +152,12 @@ public void onDataChange(DataSnapshot snapshot) {
}
onSuccess.run();
}
@Override public void onCancelled(DatabaseError e) { onSuccess.run(); }
@Override public void onCancelled(DatabaseError e) { onError.call(e.getMessage()); }
})
)
.addOnFailureListener(e -> onError.call(e.getMessage()))
)
.addOnFailureListener(e -> onSuccess.run());
.addOnFailureListener(e -> onError.call(e.getMessage()));
}

@Override
Expand All @@ -172,8 +184,8 @@ public void getOpponentBoard(String gameId, boolean localIsWhite, Callback<int[]

@Override
public void listenForBothSetupReady(String gameId, Runnable onBothReady) {
db.child("games").child(gameId).child("bothReady")
.addValueEventListener(new ValueEventListener() {
DatabaseReference bothReadyRef = db.child("games").child(gameId).child("bothReady");
bothReadyRef.addValueEventListener(trackValue(bothReadyRef, new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
Boolean bothReady = snapshot.getValue(Boolean.class);
Expand All @@ -183,7 +195,7 @@ public void onDataChange(DataSnapshot snapshot) {
}
}
@Override public void onCancelled(DatabaseError e) {}
});
}));
}

// ── Moves ─────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -221,8 +233,8 @@ public void listenForOpponentMove(String gameId, Callback<int[]> onMove) {
lastExistingKey[0] = child.getKey();
}

db.child("games").child(gameId).child("moves")
.addChildEventListener(new ChildEventListener() {
DatabaseReference movesRef = db.child("games").child(gameId).child("moves");
movesRef.addChildEventListener(trackChild(movesRef, new ChildEventListener() {
private boolean seenStart = (lastExistingKey[0] == null);

@Override
Expand Down Expand Up @@ -257,12 +269,12 @@ public void onChildAdded(DataSnapshot snapshot, String prev) {
@Override public void onChildRemoved(DataSnapshot s) {}
@Override public void onChildMoved (DataSnapshot s, String p) {}
@Override public void onCancelled (DatabaseError e) {}
});
}));
})
.addOnFailureListener(e -> {
// Fallback: attach listener without filtering (original behaviour)
db.child("games").child(gameId).child("moves")
.addChildEventListener(new ChildEventListener() {
DatabaseReference movesRef = db.child("games").child(gameId).child("moves");
movesRef.addChildEventListener(trackChild(movesRef, new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot snapshot, String prev) {
int fromCol = getInt(snapshot, "fromCol");
Expand All @@ -277,7 +289,7 @@ public void onChildAdded(DataSnapshot snapshot, String prev) {
@Override public void onChildRemoved(DataSnapshot s) {}
@Override public void onChildMoved (DataSnapshot s, String p) {}
@Override public void onCancelled (DatabaseError e) {}
});
}));
});
}

Expand All @@ -296,12 +308,13 @@ public void listenForHeartbeat(String gameId, boolean listenForWhite,
String player = listenForWhite ? "white" : "black";
Handler handler = new Handler(Looper.getMainLooper());
Runnable timeoutRunnable = onTimeout::run;
cleanupActions.add(() -> handler.removeCallbacks(timeoutRunnable));

// Start timeout timer — reset each time a valid heartbeat arrives
handler.postDelayed(timeoutRunnable, timeoutMs);

db.child("games").child(gameId).child("heartbeat").child(player)
.addValueEventListener(new ValueEventListener() {
DatabaseReference heartbeatRef = db.child("games").child(gameId).child("heartbeat").child(player);
heartbeatRef.addValueEventListener(trackValue(heartbeatRef, new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
Long timestamp = snapshot.getValue(Long.class);
Expand All @@ -319,7 +332,7 @@ public void onDataChange(DataSnapshot snapshot) {
onHeartbeat.call(latency);
}
@Override public void onCancelled(DatabaseError e) {}
});
}));
}

// ── Game over ─────────────────────────────────────────────────────────────
Expand All @@ -331,8 +344,8 @@ public void signalGameOver(String gameId, String reason) {

@Override
public void listenForGameOver(String gameId, Callback<String> onGameOver) {
db.child("games").child(gameId).child("gameOver")
.addValueEventListener(new ValueEventListener() {
DatabaseReference gameOverRef = db.child("games").child(gameId).child("gameOver");
gameOverRef.addValueEventListener(trackValue(gameOverRef, new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
String reason = snapshot.getValue(String.class);
Expand All @@ -342,7 +355,7 @@ public void onDataChange(DataSnapshot snapshot) {
}
}
@Override public void onCancelled(DatabaseError e) {}
});
}));
}

// ─────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -370,4 +383,10 @@ public void unconfirmSetup(String gameId, boolean isWhite, Runnable onSuccess, C
})
.addOnFailureListener(e -> onError.call(e.getMessage()));
}
}

@Override
public void removeAllListeners() {
for (Runnable cleanup : cleanupActions) cleanup.run();
cleanupActions.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public interface FirebaseAPI {
* Saves this player's board layout and marks them as setup-ready.
* Sets games/{gameId}/bothReady = true once BOTH players have called this.
*/
void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess);
void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess, Callback<String> onError);

/**
* Unconfirms the setup, marking this player as not ready.
Expand Down Expand Up @@ -80,9 +80,11 @@ void listenForHeartbeat(String gameId, boolean listenForWhite, long timeoutMs,
void signalGameOver(String gameId, String reason);
void listenForGameOver(String gameId, Callback<String> onGameOver);

void removeAllListeners();

// ─────────────────────────────────────────────────────────────────────────

interface Callback<T> {
void call(T value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
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.network.CircuitBreaker;
import com.group14.regicidechess.database.FirebaseAPI;
import com.group14.regicidechess.model.Move;
import com.group14.regicidechess.model.Player;

Expand Down Expand Up @@ -45,19 +45,21 @@ 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;
private final CircuitBreaker circuitBreaker = new CircuitBreaker(3, 10_000L);

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 @@ -70,6 +72,10 @@ public void start() {
startHeartbeat();
}

public void stop() {
DatabaseManager.getInstance().getApi().removeAllListeners();
}

// ─────────────────────────────────────────────────────────────────────────
// Outgoing
// ─────────────────────────────────────────────────────────────────────────
Expand All @@ -79,18 +85,18 @@ public void saveMove(Move move) {
Gdx.app.log("GameNetworkHandler", "Circuit breaker OPEN — move send blocked");
return;
}
DatabaseManager.getInstance().getApi().saveMove(gameId, move, () -> {});
api.saveMove(gameId, move, () -> {});
}

public void sendHeartbeat() {
if (!circuitBreaker.allowRequest()) return;
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 @@ -103,8 +109,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 @@ -117,16 +122,13 @@ 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() {
sendHeartbeat();

DatabaseManager.getInstance().getApi()
.listenForHeartbeat(
api.listenForHeartbeat(
gameId,
!localPlayer.isWhite(),
HEARTBEAT_TIMEOUT_MS,
Expand Down Expand Up @@ -165,4 +167,4 @@ private void updateConnectionUI(long latencyMs) {
else connectionIcon.setColor(Color.RED);
connectionLabel.setText(latencyMs + " ms");
}
}
}
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 Expand Up @@ -438,12 +440,14 @@ public void resize(int width, int height) {
@Override
public void hide() {
inputHandler.clearObservers();
networkHandler.stop();
inMatchState.exit();
}

@Override
public void dispose() {
networkHandler.stop();
stage.dispose();
boardRenderer.dispose();
}
}
}
Loading

0 comments on commit 96edb8d

Please sign in to comment.