Skip to content

Commit

Permalink
Merge pull request #63 from TDT4240-14/Add-heartbeat/circuitbreak-to-…
Browse files Browse the repository at this point in the history
…lobby-and-setup

Add heartbeat/circuitbreak to lobby and setup
  • Loading branch information
benjamls authored Apr 12, 2026
2 parents a9a3144 + e3e753a commit 2177d4b
Show file tree
Hide file tree
Showing 8 changed files with 975 additions and 176 deletions.
95 changes: 68 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,76 @@
# regicidechess

A [libGDX](https://libgdx.com/) project generated with [gdx-liftoff](https://github.com/libgdx/gdx-liftoff).
# Regicide Chess

This project was generated with a template including simple application launchers and an `ApplicationAdapter` extension that draws libGDX logo.
Regicide Chess is a hybrid chess game for Android that combines classic chess rules with a regicide twist. Built with **LibGDX** and **Firebase**, it supports real-time multiplayer matches, letting players challenge friends or opponents online.

## Platforms
## Project Structure

- `core`: Main module with the application logic shared by all platforms.
- `lwjgl3`: Primary desktop platform using LWJGL3; was called 'desktop' in older docs.
- `android`: Android mobile platform. Needs Android SDK.
The project follows the standard LibGDX multi-module layout:

## Gradle
```
RegicideChess/
├── android/ # Android-specific module
│ └── .../firebase/
│ └── AndroidFirebase.java # Main Firebase interface
├── core/ # Platform-independent game logic
│ ├── database/ # Firebase data handling
│ ├── input/ # Input processing
│ ├── model/ # Game models
│ ├── network/ # Networking utilities
│ ├── screens/ # Game screens
│ │ ├── Lobby/ # Lobby creation/joining
│ │ ├── Game/ # Main gameplay
│ │ ├── MainMenu/ # Main menu
│ │ └── Setup/ # Setup pieces
│ ├── states/ # Game state management
│ └── utils/ # Helper classes
├── assets/ # Sprites, fonts
└── ... # Gradle build files, etc.
```

This project uses [Gradle](https://gradle.org/) to manage dependencies.
The Gradle wrapper was included, so you can run Gradle tasks using `gradlew.bat` or `./gradlew` commands.
Useful Gradle tasks and flags:
- **android/**: Contains Android-specific code, notably `AndroidFirebase.java` which acts as the central hub for all Firebase operations.
- **core/**: All shared game logic, UI screens, input handling, and networking abstractions. This module is independent of Android and can be reused on other platforms.
- **assets/**: Images (piece sprites)

- `--continue`: when using this flag, errors will not stop the tasks from running.
- `--daemon`: thanks to this flag, Gradle daemon will be used to run chosen tasks.
- `--offline`: when using this flag, cached dependency archives will be used.
- `--refresh-dependencies`: this flag forces validation of all dependencies. Useful for snapshot versions.
- `android:lint`: performs Android project validation.
- `build`: builds sources and archives of every project.
- `cleanEclipse`: removes Eclipse project data.
- `cleanIdea`: removes IntelliJ project data.
- `clean`: removes `build` folders, which store compiled classes and built archives.
- `eclipse`: generates Eclipse project data.
- `idea`: generates IntelliJ project data.
- `lwjgl3:jar`: builds application's runnable jar, which can be found at `lwjgl3/build/libs`.
- `lwjgl3:run`: starts the application.
- `test`: runs unit tests (if any).
## Compilation and Execution

Note that most tasks that are not specific to a single project can be run with `name:` prefix, where the `name` should be replaced with the ID of a specific project.
For example, `core:clean` removes `build` folder only from the `core` project.
### For End Users (Play Store / APK)

1. Download the `RegicideChess.apk` from the [Releases Page](https://git.ntnu.no/TDT4240-14/TDT4240/-/releases)
2. Copy it to your Android device and install it. You may need to enable **Install from unknown sources** in your device settings.
3. Tap the app icon to launch.

### For Developers (Build from Source)

#### Prerequisites
- [Android Studio](https://developer.android.com/studio) (with Android SDK and build tools)
- Java 11 or higher

#### Steps
1. Clone the repository:
```bash
git clone https://git.ntnu.no/TDT4240-14/TDT4240.git
```
2. Open the project in **Android Studio**.
3. Wait for Gradle sync to complete (it will automatically download all dependencies, including LibGDX and Firebase SDKs).
4. Build the APK:
- Go to the top menu: `Build``Build Bundle(s) / APK(s)``Build APK(s)`.
5. The debug APK will be generated at:
```
app/build/outputs/apk/debug/
```

#### Running on an Emulator or Device
- Connect your Android device via USB or start an emulator.
- Click the **Run** button in Android Studio.

## Testing Multiplayer with BlueStacks

Since multiplayer requires two devices, we recommend using [BlueStacks](https://www.bluestacks.com) (a free Android emulator for PC) to run multiple instances.

1. Install BlueStacks and open the **Multi‑Instance Manager**.
2. Create two separate instances (e.g., `Instance 1` and `Instance 2`).
3. Launch both instances.
4. Drag the `RegicideChess.apk` file into each BlueStacks window to install the game.
5. On one instance, create a new lobby. On the other, join using the displayed **Game ID**.
6. Play a full match – this simulates two real devices and helps catch multiplayer bugs.
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,27 @@ public void getOpponentBoard(String gameId, boolean localIsWhite, FirebaseAPI.Ca
.addOnFailureListener(e -> onBoard.call(new int[0][0]));
}

private ValueEventListener bothReadyListener;

public void listenForBothSetupReady(String gameId, Runnable onBothReady) {
DatabaseReference ref = db.child("games").child(gameId).child("bothReady");
ValueEventListener listener = new ValueEventListener() {

// Remove existing listener if it exists to prevent duplicates
if (bothReadyListener != null) {
ref.removeEventListener(bothReadyListener);
}

bothReadyListener = new ValueEventListener() {
@Override public void onDataChange(DataSnapshot s) {
Boolean bothReady = s.getValue(Boolean.class);
if (bothReady != null && bothReady) {
ref.removeEventListener(this);
bothReadyListener = null; // Clear reference
onBothReady.run();
}
}
@Override public void onCancelled(DatabaseError e) {}
};
ref.addValueEventListener(listener);
ref.addValueEventListener(bothReadyListener);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,31 @@ public interface Listener {
private boolean selfConnected = true; // mirrors .info/connected
private boolean forfeitWritten = false; // prevent double-write to gameOver

/**
* @param selfConnectedAtStart Pass the connection state inherited from SetupNetworkHandler
* (via isSelfConnected()). This seeds selfConnected correctly so
* the initial .info/connected = true callback — which Firebase
* always fires immediately on listener attach — is not mistaken
* for a reconnect after a disconnect.
* Pass {@code true} for a normal start; pass the actual state
* when transitioning from SetupScreen.
*/
public GameNetworkHandler(String gameId, Player localPlayer, Listener listener,
Image connectionIcon, Label connectionLabel, FirebaseAPI api) {
Image connectionIcon, Label connectionLabel,
FirebaseAPI api, boolean selfConnectedAtStart) {
this.gameId = gameId;
this.localPlayer = localPlayer;
this.listener = listener;
this.connectionIcon = connectionIcon;
this.connectionLabel = connectionLabel;
this.api = api;
this.selfConnected = selfConnectedAtStart;
}

/** Convenience overload for cases where we know the device is connected (e.g. tests). */
public GameNetworkHandler(String gameId, Player localPlayer, Listener listener,
Image connectionIcon, Label connectionLabel, FirebaseAPI api) {
this(gameId, localPlayer, listener, connectionIcon, connectionLabel, api, true);
}

// ─────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -241,8 +258,12 @@ private void startOpponentMoveListener() {
}

private void startGameOverListener() {
api.listenForGameOver(gameId, reason ->
Gdx.app.postRunnable(() -> listener.onGameOver(reason)));
api.listenForGameOver(gameId, reason -> {
// "cancel:..." is written by SetupNetworkHandler during the setup phase.
// Ignore it — GameScreen only handles "forfeit:..." and "disconnect:...".
if (reason == null || reason.startsWith("cancel:")) return;
Gdx.app.postRunnable(() -> listener.onGameOver(reason));
});
}

private void startHeartbeatAndLatency() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,34 +106,52 @@ public void showOpponentDisconnected() {
overlayWrapper.setVisible(true);
}

/**
* Called when a forfeit/disconnect reason arrives from Firebase.
* reason format: "forfeit:{loserColor}" | "disconnect:{droppedColor}"
*/
public void showForfeitReceived(String reason) {
// reason = "forfeit:{loserColor}" or "disconnect:{disconnectedColor}"
String myColor = localPlayer.isWhite() ? "white" : "black";
String opponentColor = localPlayer.isWhite() ? "black" : "white";
boolean iDisconnected = reason.equals("disconnect:" + myColor);
boolean opponentDisconnected = reason.equals("disconnect:" + opponentColor);
boolean opponentForfeited = reason.equals("forfeit:" + opponentColor);

if (iDisconnected) {
if (reason.equals("disconnect:" + myColor)) {
// I was the one who dropped and couldn't reconnect in time
overlayTitle.setText("You Lost");
overlayBody.setText("You disconnected.");
} else if (opponentDisconnected) {
overlayBody.setText("You disconnected and could not reconnect in time.");
} else if (reason.equals("disconnect:" + opponentColor)) {
// Opponent dropped and didn't reconnect
overlayTitle.setText("You Win!");
overlayBody.setText("Opponent disconnected.");
} else if (opponentForfeited) {
} else if (reason.equals("forfeit:" + opponentColor)) {
// Opponent pressed Forfeit
overlayTitle.setText("You Win!");
overlayBody.setText("Opponent forfeited!");
} else {
// "forfeit:myColor" — I voluntarily forfeited
overlayBody.setText("Opponent forfeited.");
} else if (reason.equals("forfeit:" + myColor)) {
// My own forfeit echoed back from Firebase — isGameOver guard in GameScreen
// should prevent this path, but handle it defensively.
overlayTitle.setText("You Lost");
overlayBody.setText("You forfeited.");
} else {
// Unrecognised reason (e.g. stale "cancel:" from setup — should be filtered
// in GameNetworkHandler, but guard here too).
return;
}
overlayConfirmBtn.setText("Back to Menu");
overlayCancelBtn.setVisible(false);
onOverlayConfirm = listener::onGameOverBack;
overlayWrapper.setVisible(true);
}

/** Shown immediately to the local player after they confirm the forfeit dialog. */
public void showSelfForfeited() {
overlayTitle.setText("You Lost");
overlayBody.setText("You forfeited.");
overlayConfirmBtn.setText("Back to Menu");
overlayCancelBtn.setVisible(false);
onOverlayConfirm = listener::onGameOverBack;
overlayWrapper.setVisible(true);
}

public void showPromotion() {
promotionOverlay.setVisible(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,18 @@ public class GameScreen implements Screen,
// Constructor
// ─────────────────────────────────────────────────────────────────────────

/**
* @param selfConnectedAtStart Connection state at the moment of transition, taken from
* SetupNetworkHandler.isSelfConnected(). Seeding this correctly
* prevents the initial .info/connected = true callback that
* Firebase fires on listener attach from being misread as a
* reconnect after a disconnect, which would cancel the grace timer.
* Use the single-arg overload (defaults to true) when constructing
* GameScreen outside of a setup→game transition (e.g. in tests).
*/
public GameScreen(Game game, SpriteBatch batch,
Board board, Player localPlayer, int boardSize, String gameId,
FirebaseAPI api) {
FirebaseAPI api, boolean selfConnectedAtStart) {
this.game = game;
this.batch = batch;
this.localPlayer = localPlayer;
Expand Down Expand Up @@ -146,7 +155,8 @@ public GameScreen(Game game, SpriteBatch batch,

overlayManager = new GameOverlayManager(stage, skin, localPlayer, this);
networkHandler = new GameNetworkHandler(gameId, localPlayer, this,
connectionIcon, connectionLabel, api);
connectionIcon, connectionLabel, api,
selfConnectedAtStart);
boardRenderer = new GameBoardRenderer(batch, localPlayer, boardSize);
boardRenderer.computeGeometry(V_WIDTH, V_HEIGHT, TOP_BAR_HEIGHT, STATUS_BAR_HEIGHT);

Expand Down Expand Up @@ -390,7 +400,9 @@ public void onForfeitConfirmed() {
stopHeartbeatTimer();
forfeitBtn.setVisible(false);
networkHandler.signalForfeit();
overlayManager.showGameOver(false);
// Show "You forfeited" immediately — the Firebase echo of our own
// forfeit signal must NOT trigger onGameOver again (isGameOver guards it).
overlayManager.showSelfForfeited();
}

@Override
Expand Down
Loading

0 comments on commit 2177d4b

Please sign in to comment.