Skip to content

Improve circuitbreak #62

Merged
merged 6 commits into from
Apr 10, 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
Binary file modified android/ic_launcher-web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/regicidechess.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/res/drawable-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/res/drawable-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/res/drawable-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/res/drawable-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/res/drawable-xxxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.group14.regicidechess.android;

import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.ValueEventListener;

public class FirebaseConnectionManager {

private final DatabaseReference db;
private final FirebaseUtils utils;

public FirebaseConnectionManager(DatabaseReference db, FirebaseUtils utils) {
this.db = db;
this.utils = utils;
}

public void listenForMyConnection(Runnable onConnected, Runnable onDisconnected) {
DatabaseReference connRef = db.child(".info/connected");
ValueEventListener listener = utils.trackValue(connRef, new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
Boolean connected = s.getValue(Boolean.class);
if (Boolean.TRUE.equals(connected)) onConnected.run();
else onDisconnected.run();
}
@Override public void onCancelled(DatabaseError e) {}
});
}

public void signalReconnected(String gameId, boolean isWhite) {
String color = isWhite ? "white" : "black";
db.child("games").child(gameId).child("disconnectedAt").child(color).onDisconnect().cancel();
db.child("games").child(gameId).child("disconnectedAt").child(color).removeValue();
db.child("games").child(gameId).child("reconnected").child(color).setValue(true);
}

public void listenForOpponentReconnected(String gameId, boolean listenForWhite, Runnable onReconnected) {
String color = listenForWhite ? "white" : "black";
DatabaseReference ref = db.child("games").child(gameId).child("reconnected").child(color);
ValueEventListener listener = utils.trackValue(ref, new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
Boolean val = s.getValue(Boolean.class);
if (Boolean.TRUE.equals(val)) onReconnected.run();
}
@Override public void onCancelled(DatabaseError e) {}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.group14.regicidechess.android;

import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.ServerValue;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;
import com.group14.regicidechess.database.FirebaseAPI;

public class FirebaseGameOverManager {

private final DatabaseReference db;
private final FirebaseUtils utils;

public FirebaseGameOverManager(DatabaseReference db, FirebaseUtils utils) {
this.db = db;
this.utils = utils;
}

public void signalGameOver(String gameId, String reason) {
DatabaseReference ref = db.child("games").child(gameId).child("gameOver");
ref.onDisconnect().cancel();
ref.runTransaction(new Transaction.Handler() {
@Override public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() != null) return Transaction.abort();
currentData.setValue(reason);
return Transaction.success(currentData);
}
@Override public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {}
});
}

public void registerDisconnectGameOver(String gameId, String reason) {
String color = reason.startsWith("disconnect:") ? reason.substring("disconnect:".length()) : reason;
DatabaseReference ref = db.child("games").child(gameId).child("disconnectedAt").child(color);
ref.onDisconnect().setValue(ServerValue.TIMESTAMP);
}

public void listenForGameOver(String gameId, FirebaseAPI.Callback<String> onGameOver) {
DatabaseReference ref = db.child("games").child(gameId).child("gameOver");
ValueEventListener listener = utils.trackValue(ref, new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
String reason = s.getValue(String.class);
if (reason != null) {
ref.removeEventListener(this);
onGameOver.call(reason);
}
}
@Override public void onCancelled(DatabaseError e) {}
});
}

public void listenForOpponentDisconnectedAt(String gameId, String opponentColor, Runnable onDisconnected) {
DatabaseReference ref = db.child("games").child(gameId).child("disconnectedAt").child(opponentColor);
ValueEventListener listener = new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
if (s.getValue() != null) {
ref.removeEventListener(this);
utils.untrackValue(this);
onDisconnected.run();
}
}
@Override public void onCancelled(DatabaseError e) {}
};
utils.trackValue(ref, listener);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.group14.regicidechess.android;

import android.os.Handler;
import android.os.Looper;

import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.ServerValue;
import com.google.firebase.database.ValueEventListener;
import com.group14.regicidechess.database.FirebaseAPI;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class FirebaseHeartbeatManager {

private final DatabaseReference db;
private final FirebaseUtils utils;

public FirebaseHeartbeatManager(DatabaseReference db, FirebaseUtils utils) {
this.db = db;
this.utils = utils;
}

public void sendLatency(String gameId, boolean isWhite, long latencyMs) {
String player = isWhite ? "white" : "black";
db.child("games").child(gameId).child("latency").child(player).setValue(latencyMs);
}

public void listenForOpponentLatency(String gameId, boolean listenForWhite, FirebaseAPI.Callback<Long> onLatency) {
String player = listenForWhite ? "white" : "black";
DatabaseReference ref = db.child("games").child(gameId).child("latency").child(player);
ValueEventListener listener = utils.trackValue(ref, new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
Long latency = s.getValue(Long.class);
if (latency != null) onLatency.call(latency);
}
@Override public void onCancelled(DatabaseError e) {}
});
}

public void sendHeartbeat(String gameId, boolean isWhite) {
String playerKey = isWhite ? "white" : "black";
long correctedSendTime = utils.serverNow();
Map<String, Object> data = new HashMap<>();
data.put("timestamp", ServerValue.TIMESTAMP);
data.put("serverCorrectedSendTime", correctedSendTime);
data.put("sender", playerKey);
db.child("games").child(gameId).child("heartbeat").child(playerKey).setValue(data);
}

public void listenForHeartbeat(String gameId, boolean listenForWhite,
long timeoutMs, FirebaseAPI.Callback<Long> onHeartbeat, Runnable onTimeout) {
String player = listenForWhite ? "white" : "black";
Handler handler = new Handler(Looper.getMainLooper());
AtomicBoolean currentlyTimedOut = new AtomicBoolean(false);
final long[] lastHeartbeatTime = {utils.serverNow()};

Runnable timeoutRunnable = new Runnable() {
@Override public void run() {
long now = utils.serverNow();
long timeSinceLast = now - lastHeartbeatTime[0];
if (timeSinceLast >= timeoutMs) {
if (!currentlyTimedOut.getAndSet(true)) onTimeout.run();
} else {
currentlyTimedOut.set(false);
}
handler.postDelayed(this, timeoutMs / 3);
}
};
handler.postDelayed(timeoutRunnable, timeoutMs);

DatabaseReference ref = db.child("games").child(gameId).child("heartbeat").child(player);
ValueEventListener listener = utils.trackValue(ref, new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
Object value = s.getValue();
if (value == null) return;
lastHeartbeatTime[0] = utils.serverNow();
currentlyTimedOut.set(false);
long rtt = 0;
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
Object sendTime = map.get("serverCorrectedSendTime");
if (sendTime instanceof Long) {
rtt = utils.serverNow() - (Long) sendTime;
if (rtt < 0) rtt = 0;
}
}
onHeartbeat.call(rtt);
}
@Override public void onCancelled(DatabaseError e) {}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.group14.regicidechess.android;

import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.ValueEventListener;
import com.group14.regicidechess.database.FirebaseAPI;
import com.group14.regicidechess.model.Lobby;

import java.util.HashMap;
import java.util.Map;

public class FirebaseLobbyManager {

private final DatabaseReference db;
private final FirebaseUtils utils;

public FirebaseLobbyManager(DatabaseReference db, FirebaseUtils utils) {
this.db = db;
this.utils = utils;
}

public void createLobby(Lobby lobby, FirebaseAPI.Callback<String> onSuccess, FirebaseAPI.Callback<String> onError) {
String gameId = lobby.getGameId();
Map<String, Object> data = new HashMap<>();
data.put("boardSize", lobby.getBoardSize());
data.put("budget", lobby.getBudget());
data.put("status", "waiting");

db.child("lobbies").child(gameId).setValue(data)
.addOnSuccessListener(v -> onSuccess.call(gameId))
.addOnFailureListener(e -> onError.call(e.getMessage()));
}

public void fetchLobby(String gameId, FirebaseAPI.Callback<Lobby> onSuccess, FirebaseAPI.Callback<String> onError) {
db.child("lobbies").child(gameId).get()
.addOnSuccessListener(snapshot -> {
if (!snapshot.exists()) { onError.call("Lobby not found"); return; }
int boardSize = utils.getInt(snapshot, "boardSize");
int budget = utils.getInt(snapshot, "budget");
onSuccess.call(new Lobby(gameId, boardSize, budget,
System.currentTimeMillis() + 30 * 60 * 1000L));
})
.addOnFailureListener(e -> onError.call(e.getMessage()));
}

public void joinLobby(String gameId, FirebaseAPI.Callback<Lobby> onSuccess, FirebaseAPI.Callback<String> onError) {
db.child("lobbies").child(gameId).get()
.addOnSuccessListener(snapshot -> {
if (!snapshot.exists()) { onError.call("Lobby not found"); return; }
int boardSize = utils.getInt(snapshot, "boardSize");
int budget = utils.getInt(snapshot, "budget");
db.child("lobbies").child(gameId).child("status").setValue("joined");
onSuccess.call(new Lobby(gameId, boardSize, budget,
System.currentTimeMillis() + 30 * 60 * 1000L));
})
.addOnFailureListener(e -> onError.call(e.getMessage()));
}

public void listenForOpponentReady(String gameId, Runnable onReady) {
DatabaseReference ref = db.child("lobbies").child(gameId).child("status");
ValueEventListener listener = new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
if ("joined".equals(s.getValue(String.class))) {
ref.removeEventListener(this);
onReady.run();
}
}
@Override public void onCancelled(DatabaseError e) {}
};
ref.addValueEventListener(listener);
// cleanup will be handled by utils if needed, but we don't track here for simplicity
}

public void startGame(String gameId) {
db.child("lobbies").child(gameId).child("status").setValue("started");
}

public void listenForGameStart(String gameId, Runnable onStart) {
DatabaseReference ref = db.child("lobbies").child(gameId).child("status");
ValueEventListener listener = new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
if ("started".equals(s.getValue(String.class))) {
ref.removeEventListener(this);
onStart.run();
}
}
@Override public void onCancelled(DatabaseError e) {}
};
ref.addValueEventListener(listener);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.group14.regicidechess.android;

import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.ServerValue;
import com.group14.regicidechess.database.FirebaseAPI;
import com.group14.regicidechess.model.Move;

import java.util.HashMap;
import java.util.Map;

public class FirebaseMoveManager {

private final DatabaseReference db;
private final FirebaseUtils utils;

public FirebaseMoveManager(DatabaseReference db, FirebaseUtils utils) {
this.db = db;
this.utils = utils;
}

public void saveMove(String gameId, Move move, Runnable onSuccess) {
Map<String, Object> data = new HashMap<>();
data.put("fromCol", (int) move.getFrom().x);
data.put("fromRow", (int) move.getFrom().y);
data.put("toCol", (int) move.getTo().x);
data.put("toRow", (int) move.getTo().y);
data.put("player", move.getPlayer().isWhite() ? "white" : "black");
data.put("timestamp", ServerValue.TIMESTAMP);
if (move.getPromotion() != null) {
data.put("promotion", move.getPromotion());
}

db.child("games").child(gameId).child("moves").push()
.setValue(data)
.addOnSuccessListener(v -> onSuccess.run());
}

public void listenForOpponentMove(String gameId, FirebaseAPI.Callback<int[]> onMove) {
long startTime = System.currentTimeMillis();
DatabaseReference movesRef = db.child("games").child(gameId).child("moves");

movesRef.orderByChild("timestamp").startAt(startTime)
.addChildEventListener(utils.trackChild(movesRef, new ChildEventListener() {
@Override public void onChildAdded(DataSnapshot s, String prev) {
int fromCol = utils.getInt(s, "fromCol");
int fromRow = utils.getInt(s, "fromRow");
int toCol = utils.getInt(s, "toCol");
int toRow = utils.getInt(s, "toRow");
String mover = s.child("player").getValue(String.class);
int isWhite = "white".equals(mover) ? 1 : 0;

String promo = s.child("promotion").getValue(String.class);
int promoCode = 0;
if (promo != null) {
switch (promo) {
case "Queen": promoCode = 2; break;
case "Rook": promoCode = 3; break;
case "Bishop": promoCode = 4; break;
case "Knight": promoCode = 5; break;
}
}
onMove.call(new int[]{fromCol, fromRow, toCol, toRow, isWhite, promoCode});
}
@Override public void onChildChanged(DataSnapshot s, String p) {}
@Override public void onChildRemoved(DataSnapshot s) {}
@Override public void onChildMoved(DataSnapshot s, String p) {}
@Override public void onCancelled(DatabaseError e) {}
}));
}
}
Loading