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.
Project contributors:
- Sebastian Taylor ([email protected])
- Victor Reynolds ([email protected])
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
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.
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 │
└────────────┘ └───────────────┘
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.
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.
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.
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.
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.
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 -versionAssuming 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-openjfxThen 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:$PATHAnd reload your shell:
source ~/.bashrc # if using bash
source ~/.zshrc # if using zshThe 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.
Navigate to the ctf-server directory.
To compile and run directly:
mvn install
mvn clean compile exec:javaTo package as a JAR:
mvn clean package
java -jar target/ctf-server-1.0-SNAPSHOT.jarCommand-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:SEEDwhere 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.
Navigate to the ctf-game directory.
To compile and run directly:
mvn install
mvn clean compile javafx:runTo package as a JAR:
mvn clean packageTo run the packaged JAR:
On Windows or macOS:
java -jar target/ctf-game-1.0-SNAPSHOT.jarOn 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.jarIt 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
- 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).