Skip to content

tayctl/concurrent-capture

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

238 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Project 24 - Capture The Flag

Abstract

The project is a capture the flag game in a top-down 2D custom pixel art setting. It's a online team-based game with players scattered across two teams. The game takes place in a randomly generated dungeon, where each team is placed in opposite ends, and the flag is located between them. The point of the game is to capture the flag without being gunned down by the opposing team. If you are gunned down by the other team while holding the flag you will drop it for the other team to claim. The first team to capture three flags and return them to their starting area wins.

Contributors

Project contributors:

Contributions:

  • Design of main coordination aspects: Victor & Sebastian
  • Coding of main coordination aspects: Victor & Sebastian
  • Documentation (this README file): Victor & Sebastian
  • Videos: Victor & Sebastian
  • Other aspects (e.g. coding of UI, etc.): Victor & Sebastian

Demo video

https://youtu.be/592BNBte1pw

main coordination challenge

pre-game lobby coordination with distributed state synchronisation

The biggest coordination challenge we tackled was getting the pre-game lobby working properly. We needed to make sure that no two players could have the same name (since that would mean controlling the same character), track who was ready to play, and get everyone to start the game at the same time. It's essentially a distributed consensus problem combined with ensuring names are mutually exclusive.

architecture

We went with a centralised approach using tuple spaces (jspace). The server has a lobbycontroller that keeps track of all player names and their states as the single source of truth. Everything happens through a shared lobby tuple space, join requests and their responses, player states, ready updates, and the game start signal all go through there. On the client side, we have lobbynetworkmanager instances that poll this space to stay updated.

┌─────────────────────────────────────────────────────┐
│                server (coordinator)                 │
│  lobbycontroller                                    │
│    - playernames: set<string>                       │
│    - players: map<string, state>                    │
│    - gamestarted: boolean                           │
│                        │                            │
│                        ▼                            │
│  ┌──────────────────────────────────────────────┐   │
│  │   lobby tuple space (shared memory)          │   │
│  │   - join requests/responses                  │   │
│  │   - player states (lobby_player tuples)      │   │
│  │   - game start signal                        │   │
│  └──────────────────────────────────────────────┘   │
└──────────────────┬─────────────┬────────────────────┘
                   │             │
         ┌─────────┴──┐    ┌─────┴─────────┐
         │  client a  │    │   client b    │
         │  polling   │    │   polling     │
         └────────────┘    └───────────────┘

key coordination mechanisms

mutual exclusion for name validation. We handle all join requests sequentially in a single server thread that runs every 100ms. This completely eliminates race conditions because the server thread itself acts as the critical section. We get first-come-first-served mutual exclusion without needing any explicit locks.

distributed barrier for game start. The game only starts when at least one player from each team (blue and red) is ready. Once that condition is met, the server puts a game_start tuple in the space. We use non-destructive reads (queryp()) so all clients can see the signal without consuming it.

eventual consistency through polling. Instead of pushing updates to clients, we have them poll every 500ms. This means everyone converges to the same view within half a second. It's not instant, but it keeps things simple and half a second delay in a lobby isn't noticeable.

critical race condition and solution

we ran into a nasty race condition during testing. When the game started, the server would immediately begin processing bullets and collisions, but clients still needed time to detect the signal, exit the lobby screen, initialise their game world, and register themselves. if someone fired a bullet before everyone was registered, the server's collision detection would find no players and nothing would happen.

the fix was pretty straightforward; we added a 2-second delay between sending the game start signal and actually starting the gameticker thread. it's not the most elegant solution, but it's simple and it works reliably when you can't guarantee exact timing in a distributed system.

additional synchronisation challenges

Another weird bug we found, was that stationary players would not take damage when shot. It turns out clients were only sending updates to the server when they moved, so if you just stood still, your client never learned that your health had changed on the server side. The fix was to have clients check their server-side health every 50ms regardless of movement, keeping their local state in sync with what the server knows.

relation to course concepts

This implementation uses several concepts from the course tutorials. We're using linda-style tuple spaces (tutorial 1) for all the communication, with generative communication where tuples persist until consumed. The name validation implements mutual exclusion using the global lock pattern from tutorial 2, where the server thread itself acts as the critical section. The game start condition implements barrier synchronisation (also from tutorial 2), where all clients must reach a coordination point before proceeding.

By going with a centralised coordinator design (tutorial 3), we kept things simple and avoided more complex distributed approaches. The polling approach gives us eventual consistency where clients converge to the same view within bounded time.

the hardest part wasn't the coordination primitives themselves, tuple spaces make that pretty clean, but finding and fixing all the subtle timing bugs. Every issue required really understanding when things happen in what order across different machines. We're quite happy with how simple the final design ended up being. By choosing centralised coordination over peer-to-peer, pull-based consistency over push-based, and sequential processing over concurrent, we ended up with something that's easy to understand and debug.

Installation

This project was built for Java 21 and requires JavaFX 21. The installation varies depending on the operating system, so here are some basic instructions.

Windows or MacOS

For the sake of simplicity, we recommend a distribution of Azul Zulu already containing JavaFX which can be found here. Scroll down and select Java 21 (LTS), the operating system that you are using, and ensure that you pick the Java package JDK FX.

To check if java is installed correctly, you can run the command:

java -version

Arch Linux

Assuming that you have an AUR helper installed, install Java 21 and JavaFX 21 from the AUR via the following commands. If you do not have an AUR helper, you should install one. I personally use yay - installation instructions can be found in their official repository.

yay -S jdk21-openjdk java21-openjfx

Then set the following environment variables in your shell configuration (.bashrc or .zshrc):

export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
export PATH=$JAVA_HOME/bin:$PATH

And reload your shell:

source ~/.bashrc  # if using bash
source ~/.zshrc   # if using zsh

Compiling and Running From Source

The project consists of two Java applications: a server (ctf-server) and a game client (ctf-game). Below are instructions for compiling and running each component.

Server

Navigate to the ctf-server directory.

To compile and run directly:

mvn install
mvn clean compile exec:java

To package as a JAR:

mvn clean package
java -jar target/ctf-server-1.0-SNAPSHOT.jar

Command-line arguments:

# Change the server port
java -jar target/ctf-server-1.0-SNAPSHOT.jar --port:PORT

# Change the world generation seed
java -jar target/ctf-server-1.0-SNAPSHOT.jar --seed:SEED

where SEED and PORT are numbers.

Note: This assumes jSpace is installed locally on your machine and that you have opened a port through your systems firewall.

Game Client

Navigate to the ctf-game directory.

To compile and run directly:

mvn install
mvn clean compile javafx:run

To package as a JAR:

mvn clean package

To run the packaged JAR:

On Windows or macOS:

java -jar target/ctf-game-1.0-SNAPSHOT.jar

On Linux (also available in ctf-game/runjar.sh):

java --module-path /usr/lib/jvm/java-21-openjdk/lib \
     --add-modules javafx.controls,javafx.fxml \
     -jar target/ctf-game-1.0-SNAPSHOT.jar

Generate Javadocs Documentation HTML

It is also possible to generate a HTML file containing the documentation of the entire game client or server using the following command.

mvn javadoc:javadoc

References

  • Tutorial 1 (Programming with Spaces)
  • Tutorial 2 (Concurrent Programming with Tuple Spaces)
  • Tutorial 3 (Distributed Programming with Tuple Spaces)
  • Section 4 (Interaction-oriented Programming - Protocols)
  • Section 5 (Task-oriented Programming - Workflows).

About

A 2d top down fast paced ctf shooter!

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages