·Java, JavaFX, TCP +3

Mesos

Distributed Java implementation of the Mesos board game, with TUI, JavaFX GUI, TCP and RMI networking, multi-game support, reconnection handling, and MySQL rankings.

The source repository is temporarily private and will be linked publicly once it can be published: T-vaccari/ing-sw-2026-Vaccari-Zorloni-Viggiano-Zanatta.

Mesos

This repository contains the Java implementation of the Mesos board game, developed as the final project for the 2025-2026 Software Engineering course at Politecnico di Milano.

The project was developed by group GC3, composed of the following members:

  • Vaccari Tommaso
  • Viggiano Luca
  • Zanatta Tommaso
  • Zorloni Fabio

The application was designed as a distributed client-server system following the Model-View-Controller pattern. The server maintains the game model, manages the application logic, and coordinates matches; the clients allow players to interact with the game through either a textual interface or a graphical interface.

Implemented Requirements

Compared to the evaluation table provided together with the specification, the project implements:

Complete Rules + TUI + GUI + RMI + Socket + 3 Advanced Features

In particular, the following were implemented:

  • Complete Rules: the game supports the complete rules of Mesos, not the advanced ones.
  • TUI: the client can be launched with a textual interface.
  • GUI: the client can be launched with a JavaFX graphical interface.
  • TCP Socket: client and server can communicate through the TCP protocol.
  • Java RMI: client and server can communicate through Java Remote Method Invocation.
  • Advanced Feature - Multiple Matches: the server can manage multiple matches at the same time.
  • Advanced Feature - Disconnection Resilience: a disconnected client can reconnect and continue the match. If a player disconnects, their turn is skipped. If only one connected player remains, a timer starts, and when it expires the only remaining connected player is declared the winner.
  • Advanced Feature - Database with player ranking

Project Architecture

This section explains the high-level architecture of the project, then the related subsections explain the applied patterns and implementation details in depth.

The main design pattern of the project is the Distributed Model View Controller, that is, a version of MVC in which the View is on another machine from the Controller and the Model.

Recall that the classic MVC pattern is an architectural pattern that separates the data model of an application (model) from the graphical representation (view) and from the control logic (controller). The idea is to have the View take care of the logic for presenting information and collecting input from the user, while the controller takes care of coordinating the Model to handle the user's requests. Finally, the Model is responsible for maintaining the state and domain logic. In general, there are several variants of non-distributed MVC, with some differences in how the view interacts with the other two components. In particular, here we implemented the version in which the view is notified by the Model after an update; therefore the view does not know the Model, and from a certain point of view the Model does not know the view, because the observer pattern is applied: the model has references to the view in the form of listener interfaces. Below is an illustrated diagram of the pattern:

MVC.png

The user interacts with the View. Imagine that they request a service; the flow is the following:

  1. The view contacts the controller following the user input.
  2. The controller performs an initial validation of the request and knows, based on the received request, what to request from the model. Here a first distinction can be made between thin and fat controllers, depending on how much domain logic is moved inside the controller. In general, in this implementation we always opted for thin controllers. After validating the request, the controller asks the model for the actual service; the model consequently reacts and updates its internal state.
  3. After the model has modified its internal state and applied the domain logic, it must notify the related virtual views. There can be more than one, since multiple users can use the same model. In our application of the pattern, we chose not to allow the model to know the views directly, but instead to have subscribed listeners, which are concretely implemented by the virtual views.

The idea now is to take the classic pattern and adapt it to a context in which the view is on the user's machine, while the model and controller are on the server. It is therefore necessary to add a network layer for information transport, while hiding from the blocks of the pattern the fact that there is a network in between. In other words, to obtain an architecture as clean as possible, the network must be hidden; it must not be an integral part of this model. To hide the presence of the network, common contracts were introduced, which locally represent the remote objects with which each side must interact. On the client, the user interface does not communicate directly with the network, but uses a ServerProxy, which represents the remote server and exposes the application services that the client can request. In this sense, the remote proxy pattern was applied. Still on the client, the VirtualServer represents the channel through which messages from the server arrive to the local user interface. In fact, it exposes the services that the server can request on the client. On the server, the VirtualView instead represents the client's remote user interface: it receives commands coming from the client and allows the server to notify updates toward that client. It exposes the services that the client can request from the server.

A symmetrical ServerProxy was not introduced on the server side, because the server does not need to invoke a variety of application services on the client. In that communication direction, the VirtualView is enough, as it keeps the reference to the remote client and offers the single point through which notifications can be sent to it.

To better clarify the architecture, this illustrated diagram can be useful:

project-architecture.png

Communication Protocol

The project adopts a distributed MVC architecture: view, controller, and model are in different processes, so some interactions cross the network. So that this separation does not compromise the cleanliness of the architecture, the network must be transparent to the application logic: neither the view nor the model should know whether they are communicating through TCP or RMI.

To achieve this objective, the system introduces two contracts that locally simulate the presence of the remote counterpart: each one pretends to be the object that its own side would like to have nearby:

  • VirtualView (resides on the server): for the model, it is the client's view. The model registers it as a listener through the Observer pattern and calls notifyUpdate() on it exactly as it would with a local view; the concrete VirtualView is responsible for serializing and transporting the update across the network toward the real client.

  • VirtualServer (resides on the client): for the UI, it is the receiving side of the server. It receives the messages that arrive from the network and delivers them to the local view, which knows nothing about the underlying transport.

These contracts have a double role: at the architectural level they are the communication points between client and server; at the implementation level they allow TCP and RMI to offer the same services without the application logic having to know which protocol is being used.

However, TCP and RMI have different technical constraints that prevent the same interfaces from being used in both cases. In the TCP case, the interfaces are normal Java interfaces. In the RMI case, instead, the remote interfaces must extend Remote and their methods must declare RemoteException, because RMI cannot guarantee that a remote call succeeds as a local call would.

This difference introduces two contract levels in the code:

  • Application level - VirtualView and VirtualServer describe the services independently of the protocol used.
  • RMI level - RmiServerService and RmiClientService expose equivalent services with the technical constraints required by RMI.

The functional alignment between the two levels is therefore:

  • VirtualView <-> RmiServerService: client -> server commands
  • VirtualServer <-> RmiClientService: server -> client notifications

RMI requires an additional contract, RmiConnectionService, which does not have a corresponding element at the application level. It is used by the client to open the initial connection, obtain the stub of its server-side session, and pass its own callback for notifications to the server. It is the entry point of all RMI communication, before individual sessions exist for each client.

Messages and Payloads Used by the Protocol

In the code, message and payload must be distinguished. The payload contains the actual application data: the nickname chosen by the client, the lobby id, the selected card, the updated game state. The message is the envelope that adds the information needed to interpret that payload.

The concrete shape of these types is mainly dictated by the TCP problem. Only a JSON string arrives on the socket: the receiver does not know in advance which concrete payload class to reconstruct, because at the contract level the payload is seen only through the ClientPayload or ServerPayload interfaces. To solve this problem, every message includes a type field, which acts as a discriminator: the receiver first reads the type, then asks GSON to deserialize the payload into the corresponding concrete class.

The wrapper therefore exists in two variants, one for each direction:

  • ClientMessage, with a ClientMessageType and a payload implementing ClientPayload, for client -> server messages
  • ServerMessage, with a ServerMessageType and a payload implementing ServerPayload, for server -> client messages

RMI does not have this problem: objects are passed directly through remote invocation, without explicit JSON serialization. However, it reuses the same payloads, so TCP and RMI share the same data contract at the application level and the domain logic does not depend on the chosen protocol.

The payloads are organized hierarchically with respect to the game phase:

  • pre-lobby: payloads used during authentication and lobby discovery
  • lobby: payloads used during lobby creation and management
  • in-game: payloads used during the match

And with respect to the communication direction:

  • client -> server: package clienttoserver, root ClientPayload
  • server -> client: package servertoclient, root ServerPayload

Client Payloads

Server Payloads

Some payloads, such as LobbySummaryPayload, BoardPayload, PlayerPayload, CardPayload, OfferSpacePayload, StartingBlockPayload, TribePayload, TurnOrderPayload, and TurnStatePayload, are not sent directly as main messages but are support payloads nested inside larger payloads, used to structure complex data such as the board state or the current turn.

TCP Protocol

TCP is a transport protocol based on a bidirectional communication channel: the socket, uniquely identified by the tuple (source IP, source port, destination IP, destination port). Once the connection is established, the two endpoints can write and read data on the same channel in both directions.

From the data point of view, however, the socket exposes a continuous stream of bytes: there is no natural boundary between one message and the next. It is therefore the application's responsibility to define a framing protocol. In the project, the choice is simple: each application message is serialized to JSON and written to the socket as a single line of text. On the other side, the message is read with readLine(), so one line corresponds to one complete application message.

The read with readLine() is blocking: the thread remains suspended until a complete line arrives or the connection closes. For this reason, reading cannot happen on the main thread: both server and client dedicate a separate thread to receiving messages, so the rest of the application can keep operating while the reading thread is waiting.

while ((message = reader.readLine()) != null) {
    handleMessage(message);
}

Writing instead happens only when there is a command or notification to send, so it does not require a dedicated thread:

writer.println(serializedMessage);

Server-Side Flow

On the server, a ConnectionAcceptor is created, which opens the ServerSocket on the configured port and runs on a dedicated thread waiting for new connections:

ConnectionAcceptor acceptor = new ConnectionAcceptor(tcpPort, serverConnectionLifecycle, router);
Thread acceptorThread = new Thread(acceptor, "mesos-connection-acceptor");
acceptorThread.start();

For each client that connects, the ConnectionAcceptor creates a dedicated VirtualViewTCP and starts it on its own thread:

Socket clientSocket = this.serverSocket.accept();
VirtualViewTCP virtualView = new VirtualViewTCP(clientSocket, this.serverConnectionLifecycle, this.router);
Thread virtualViewThread = new Thread(virtualView, "mesos-virtual-view-" + clientSocket.getPort());
virtualViewThread.start();

Each VirtualViewTCP is therefore the session of a single client: it runs on its own thread, reads incoming messages from that client, and can send notifications back to it.

Client-Side Flow

On the client, ServerProxyTCP opens the socket toward the server and prepares the writer for outgoing messages. At the same time, it starts a VirtualServerTCP on a dedicated thread, which keeps listening for incoming messages from the server:

this.socket = new Socket(serverHost, port);
this.writer = new PrintWriter(this.socket.getOutputStream(), true);
this.virtualServer = new VirtualServerTCP(this.socket, clientView, this);
this.virtualServerThread = new Thread(this.virtualServer, "mesos-client-server-message-receiver");
this.virtualServerThread.start();

Inside VirtualServerTCP, the reader is prepared and the reading loop starts:

BufferedReader reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));

while (this.running && (serializedMessage = reader.readLine()) != null) {
    this.handleMessage(serializedMessage);
}

flusso TCP

In summary, the concrete TCP flow is:

  • the server starts the ConnectionAcceptor on a dedicated thread
  • for each client that connects, a VirtualViewTCP is created on its own thread
  • the client opens the socket and starts VirtualServerTCP on a thread dedicated to receiving
  • client -> server: ServerProxyTCP serializes and writes to the writer
  • server -> client: VirtualViewTCP serializes and writes, VirtualServerTCP reads and delivers to the view

GSON

GSON is the library used to transform Java objects into JSON strings and vice versa. The serialization of an outgoing message is direct:

ClientMessage message = new ClientMessage(ClientMessageType.AUTHENTICATE, new AuthenticationPayload("tom"));
String serializedMessage = GSON.toJson(message);

Incoming deserialization is more articulated because the payload field is declared as an interface (ClientPayload or ServerPayload) and GSON does not know which concrete class to instantiate. The type therefore acts as a discriminator: the message is first read as a JsonObject to extract the type, then the payload is deserialized into the corresponding concrete class. This process is visible in the server-side ClientCommandHandler:

JsonObject root = GSON.fromJson(rawMessage, JsonObject.class);
ClientMessageType type = ClientMessageType.valueOf(root.get("type").getAsString());
JsonElement payloadElement = root.get("payload");

ClientPayload payload = switch (type) {
    case AUTHENTICATE -> GSON.fromJson(payloadElement, AuthenticationPayload.class);
    case CREATE_LOBBY  -> GSON.fromJson(payloadElement, CreateLobbyPayload.class);
    case MOVE_TOTEM   -> GSON.fromJson(payloadElement, MoveTotemPayload.class);
    // other types mapped to their respective payload classes
};

RMI Protocol

RMI is a Java abstraction for distributed communication: it allows one JVM to invoke methods on objects that reside in another JVM, with the same syntax as a local call. Unlike TCP, the application code does not open sockets, does not serialize JSON lines, and does not maintain readLine() loops: these aspects remain present at the transport level but are hidden by the mechanism of stubs and remote interfaces.

The architectural difference compared to TCP mainly concerns the server -> client direction. In TCP, the server can write to the client at any moment because the bidirectional stream is always open. In RMI, there is no stream: everything is a method invocation on remote objects. To notify the client, the server needs something it can call: for this reason the client must export a remote object, the callback, and pass it to the server during the connection.

RMI.png

Connection

The server publishes an object of type RmiConnectionEntryPoint in the RMI registry, which implements RmiConnectionService and is registered with the name MesosServer. The registry acts as a name service: it allows the client to discover the server entry point without having to know the stub directly.

RmiConnectionService rmiEntryPoint = new RmiConnectionEntryPoint(controller, router);
LocateRegistry.createRegistry(port);
Registry registry = LocateRegistry.getRegistry(port);
registry.rebind("MesosServer", rmiEntryPoint);

The client obtains the entry point stub through lookup:

String registryUrl = "rmi://" + serverHost + ":" + rmiPort + "/MesosServer";
RmiConnectionService connectionService =
        (RmiConnectionService) Naming.lookup(registryUrl);

Before calling connect(...), the client exports its own RMI callback, VirtualServerRMI: this is the remote object that the server will call to deliver notifications to the client.

this.callbackReceiver = new VirtualServerRMI(clientView, exportCallback);
this.serverStub = connectionService.connect(this.callbackReceiver);
this.callbackReceiver.attachServerProxy(this);

The call to connect(...) arrives on the server in RmiConnectionEntryPoint, which receives the callback and creates a dedicated VirtualViewRMI for that session:

public RmiServerService connect(RmiClientService clientCallback) throws RemoteException {
    return new VirtualViewRMI(this.serverConnectionLifecycle, clientCallback, this.router);
}

VirtualViewRMI is the remote session of the individual client on the server side: it registers the connection and stores the received callback.

this.clientCallback = clientCallback;
this.sessionId = this.serverConnectionLifecycle.registerConnection(this);

The value returned by connect(...) is a stub of type RmiServerService, which the client will use from now on to send commands, without going through the registry anymore.

Message Exchange

Client -> server: ServerProxyRMI invokes a method on the RmiServerService stub; the call arrives in VirtualViewRMI.handleClientMessage(...). The message is received as a Java object and directly forwarded to the application router:

this.serverStub.handleClientMessage(message);
ClientPayload payload = clientMessage.payload;
this.router.routeMessage(this.sessionId, payload);

Server -> client: the model notifies VirtualViewRMI through the Observer pattern. VirtualViewRMI invokes the method on the client's remote callback:

this.clientCallback.handleMessage(
        new ServerMessage(ServerMessageType.AUTHENTICATION_RESULT, authenticationResultPayload)
);

The call arrives in VirtualServerRMI.handleMessage(...), which forwards it to the UI MessageRouter:

this.messageRouter.routeMessage(message);

FlussoRMI.png

In summary, the concrete RMI flow is:

  • the server registers RmiConnectionEntryPoint in the registry as MesosServer
  • the client performs lookup and obtains the RmiConnectionService stub
  • the client exports VirtualServerRMI as a callback and calls connect(callback)
  • the server creates a dedicated VirtualViewRMI and returns the RmiServerService stub
  • client -> server: ServerProxyRMI invokes VirtualViewRMI through the stub
  • server -> client: VirtualViewRMI invokes VirtualServerRMI through the callback

Client Architecture

Recall that the client must be able to choose both the type of user interface, which can be textual or graphical, and the type of network protocol, which can be TCP or RMI. The client entry point is the LaunchClient class, which reads from the command line first the UI choice and then the network protocol choice; based on these choices, it instantiates the blocks needed to start the client. In particular, an object controller manages the user interface. This object handles the user's requests and updates the view. The way information is shown depends on the chosen UI. Below is an image of the client architecture, which makes the discussion clearer:

client_architecture.png

The main blocks of the client architecture are:

  • ClientView: the interface representing the methods for modifying the client's view
  • ServerProxy: the interface representing the proxy used to communicate with the server
  • VirtualServer: the interface representing the entry point for messages coming from the server
  • MessageRouter: the class responsible for routing messages received from the server to the view, based on the message type

Clearly these are not all the objects that are created, but they are the main objects for understanding how the client was structured.

These interfaces and classes exist mainly to make the network service and the UI service easily interchangeable at the logic level. In particular, going into detail, the meaning of the interfaces is the following:

  • On the user interface side, regardless of whether it is textual or graphical, it must know how to communicate with the server following a user request, regardless of whether the message then travels via TCP or RMI. For this reason, the ServerProxy contract was created, exposing the methods the client can use to send messages over the network to the server.
  • On the network side, the server must know how to deliver messages to the client, regardless of whether TCP or RMI is the protocol. Therefore the VirtualServer contract exists, exposing the methods the server can use to notify the client, independently of how these messages arrive.
  • On the VirtualServer side, once the message has been received, it is necessary to know which method to call on the view in order to show it to the user. Since each message needs to be shown and handled in a certain way, the logic for choosing the method was centralized in MessageRouter, which chooses which method of the ClientView contract to call based on the payload type. It is used by both TCP and RMI.
  • On the messageRouter side, it must know how to call the view update service to show a given packet, independently of the concrete UI type on the other side. For this reason, the services for showing a given message to the client were fixed in the ClientView interface.

The concrete implementations directly follow these abstractions:

  • ServerProxyTCP and ServerProxyRMI implement ServerProxy, so they expose the same methods to the UI for communicating with the server, changing only the protocol used.
  • VirtualServerTCP and VirtualServerRMI represent the two ways in which the client receives messages from the server. Both receive a ServerMessage and pass it to the MessageRouter.
  • TuiView and GUIView implement ClientView, so the MessageRouter can update the view without knowing whether the user is using TUI or GUI.

So, in summary, the message flow is the following:

  • Outgoing: UI controller -> ServerProxy -> server
  • Incoming: server -> VirtualServer -> MessageRouter -> ClientView

Server Architecture

This section explains the main choices made for the server architecture.

The server was designed from the beginning to manage multiple matches at the same time. For this reason, the application logic was not concentrated in a single controller/model, but was divided according to the main phases in which a user can be:

  • pre-lobby: the user is not authenticated, or is authenticated but is not in any lobby
  • lobby: the user is authenticated and is in a lobby
  • game: the user is inside a match

The idea is therefore to have controllers and models dedicated to the different logical phases. Each controller receives the commands from users who are in the phase it controls and takes care of validating and modifying the state of the related model.

As already explained, however, the server does not communicate directly with a local user interface. The user interface is on the client, so the VirtualView interface was introduced on the server. The VirtualView represents, on the server side, the client's remote user interface and hides from the rest of the server the concrete protocol used to communicate with that client.

The concrete implementations are:

  • VirtualViewTCP, used when the client communicates through TCP sockets
  • VirtualViewRMI, used when the client communicates through RMI

Both implement the same logical contract. This allows the rest of the server to always work against the same concept of remote view, without knowing whether behind that connection there is TCP or RMI.

Incoming, the VirtualView receives a message coming from the client, reconstructs the application payload, and forwards it to the MessageRouter. The VirtualView knows the connection from which the message arrives through sessionId, but it does not decide which controller must handle that command.

This responsibility belongs to the MessageRouter. The router receives:

  • the sessionId of the connection
  • the payload sent by the client

The router chooses the controller to which it forwards the message starting from the runtime state of the user: given the sessionId, the MessageRouter consults the ServerSessionRegistry, retrieves the current phase of the user, and forwards the command to the controller associated with that phase.

Therefore:

  • if the user is in pre-lobby, the command is forwarded to the PreLobbyController
  • if the user is in lobby, the command is forwarded to the controller of the corresponding lobby
  • if the user is in game, the command is forwarded to the controller of the corresponding match

The payload is interpreted only later, by the controller that received the command. This is important because the network layer and the router do not contain application logic: they only deliver the command to the correct point of the server based on the session state.

Outgoing, instead, the models do not know the VirtualViews directly. When they need to notify an update toward the client, they only see the Listener contract.

The VirtualViews implement Listener, so they can be registered as recipients of the updates produced by the models. From the model's point of view, however, there is no VirtualViewTCP or VirtualViewRMI: there is only an object implementing Listener and therefore able to receive a notification.

This keeps the model independent from both the network and the concrete type of remote view. The model produces the update and invokes the Listener contract; the concrete VirtualView will then transform that update into a message to send to the client.

In this way, the server keeps three responsibilities separate:

  • the network is managed by the concrete implementations of VirtualView
  • message dispatching is managed by the MessageRouter
  • domain logic is managed by the controllers and models of the different phases

This separation is important because it allows the server to remain independent from the network protocol. Once a message has arrived inside the server as an application payload, the rest of the logic no longer needs to know whether that message arrived through TCP or RMI.

AllServer.png

Threads in Play

This section recaps the threads involved on the server. This makes the reason for some technical decisions clearer. At server startup, the thread running the ConnectionAcceptor is created: the class responsible for accepting incoming connection requests on the ServerSocket. When a client requests a connection on the socket, a VirtualViewTCP is created and runs on a thread. This is due to the fact that continuous socket reading for TCP is blocking, so a dedicated thread is needed. In the RMI case, instead, at server startup a remote object RmiConnectionEntryPoint is created and published in the registry with the name MesosServer. The client performs lookup on the registry and obtains a stub toward this entry point. Once the RMI client has requested the connection through connect(...), a dedicated VirtualViewRMI is created for that connection; it does not run on a thread manually created by our code, but is managed by the RMI infrastructure.

Each server controller runs on its own thread. In the pre-lobby case, that controller is a singleton. For lobbies, there is one controller and one model for each created lobby, and the same applies to games. The reason for this decision will be explained in another section. The gist is that there are many threads, which may need to read and write information characteristic of the user.

Information Management

At runtime, as mentioned, it is necessary to maintain, read, and write information for every user connected to the server. In particular, each connection to the server is uniquely identified by a session-id. All information for a connected user can be retrieved starting from the session-id. Since we need this information to be centralized in a single point that guarantees safety from the concurrency point of view, the ServerSessionRegistry class was created, which therefore represents the only point of the server where users' runtime data must be saved. This class is characterized by internal maps for data retention, and thread-safe methods created specifically to perform safe queries, avoiding race conditions.

Disconnection Management

This section explains at a high level the ideas and mechanisms behind the advanced disconnection resilience feature. First, however, it is necessary to discuss the technical components that allow this feature to exist.

Ping Pong Routine

As mentioned, we already have the class that allows us to centralize information. One of the fundamental pieces of information we save centrally is the last heartbeat response from each client. The idea is to have a verification protocol that allows us to periodically assess which connections are actually alive. This allows us to close connections that remained open, but are effectively hanging, meaning the client crashed and no longer responds. Everything is based on two messages exchanged between client and server: a Ping message from server to client and a Pong message in response from the client to the server. Periodically, with a dedicated routine, a PING message is sent to all connections currently marked as open. Meanwhile, it also checks whether the maximum waiting time since the last received PONG has expired for a given connection. If so, that connection is closed. When the client receives a PING, it replies with a PONG. This PONG is handled by a class responsible for the lifecycle of connections, namely ServerConnectionLifecycle. This heartbeat protocol relies on the already existing communication infrastructure between client and server, and is also agnostic to the low-level communication protocol chosen, thanks to the fact that we use concrete implementations of VirtualView, ServerProxy, and VirtualServer, which hide the entire network layer from us. The PingPongRoutine uses the ClientConnection interface to send the PING toward the client. In this way, the routine does not know the concrete implementations of the virtual views, but only sees ClientConnections. The actual timeout of a session is detected by the PingPongRoutine, which during the periodic check verifies whether a connection has a last PONG that is too old. At that point, it requests intervention from the ServerConnectionLifecycle class, which first closes that connection cleanly. Afterwards, ServerConnectionLifecycle updates the internal states consistently with the phase in which the player was. For lobby and game, the session is not immediately removed from the ServerSessionRegistry. The internal SystemDisconnectCommand carries only the sessionId; the phase controller then uses that session-id to retrieve from the registry the information still needed for cleanup. Only after updating the model and the phase state is the session unregistered.

Disconnection Management in Pre Lobby

If a user disconnects while in the pre-lobby phase, ServerConnectionLifecycle does not need to delegate cleanup to a specific controller. In this phase, in fact, the user does not yet belong to a lobby or a game, so there is no phase state to update inside a dedicated model. For this reason, the lifecycle directly calls ServerSessionRegistry.unregister(sessionId). If the session was authenticated, the registry also removes the association between nickname and session-id, freeing that nickname for a future authentication.

Disconnection Management in Lobby

If a user disconnects while in a lobby, ServerConnectionLifecycle does not directly modify the lobby state, but retrieves the LobbyController associated with the lobby in which the session is located and sends it an internal SystemDisconnectCommand.

The LobbyController processes the command in its own queue and calls disconnectUser(...). At this point, it retrieves nickname and listener of the session from the ServerSessionRegistry, removes the player from the Lobby model through removePlayer(...), and notifies the other clients with notifyPlayerLeft(...). The specific lobby logic therefore remains in the model: if the disconnected player was host, Lobby assigns the host to the first remaining player; if instead the lobby becomes empty, the state becomes CLOSED. In that case, the controller removes both the lobby controller and the lobby model from the ServerSessionRegistry and finally unregisters the disconnected session.

Game Disconnection Management

If a user disconnects while in game, ServerConnectionLifecycle retrieves the GameController associated with the game of the session and sends it an internal SystemDisconnectCommand. Also in this case, the command carries only the session-id, because the session remains registered until the controller has completed disconnection handling.

The GameController processes the command in its own queue and resolves from the ServerSessionRegistry both the listener of the session and the associated Player. As a first step, it removes the listener from the Game model, so that client no longer receives updates. Then it delegates to the model through game.disconnectedPlayer(player).

The domain logic remains in the Game model: the player is marked as not connected in the connectedPlayers map, then the behavior changes based on the current round phase.

During the PLACING_TOTEMS phase, TotemPlacementHandler is used. If the player had already placed the totem, their position on the board is not modified. If instead they were the current player and therefore should have placed the totem at that moment, their turn is skipped and the cursor moves to the next connected player. If the player had not yet reached their turn, nothing is done immediately: the player remains on the starting tile and will be skipped when the cursor reaches their turn, if they are still disconnected.

The case between totem placement and card drawing is handled when the PLAYER_DRAWS phase starts. At that moment, the offer path resolution order is built. If a player placed the totem, disconnects, but reconnects before their draft turn, they can draw normally. If instead they are still disconnected when their draft turn arrives, their turn is closed without drawing: they are moved to the starting tile and their draw quotas are reset.

During the PLAYER_DRAWS phase, CardDraftHandler is used. If the current player disconnects, skipDisconnectedPlayer(...) immediately closes their turn, returns them to the starting tile, and resets their remaining choices. Then advanceToNextDrawablePlayerOrEndStandardDraft() searches for the next connected player who can actually draw, or closes the standard draft phase if there are no others. If instead a player who is not the current one disconnects, the board is not modified immediately: they will be handled when the cursor reaches that player.

During the EXTRA_DRAWS phase, if the player who would be entitled to the extra draw disconnects, the optional draw is skipped and the round moves to the RESOLVING_EFFECTS phase. At the end of the specific handling, Game calls passiveRoundPhaseHandler.advancePassivePhases(), so the match does not remain blocked on an automatic phase or on a player who is no longer connected.

After the model has accepted the disconnection, the controller checks whether the match has ended. If no connected players remain, the model brings the game to the FINISHED state and the controller performs game cleanup through checkGameEndAndCleanup(). If instead the match is not over, the controller records a reconnection slot in the ServerSessionRegistry with recordGameDisconnection(...), removes the old session from the game, and then unregisters it. If only one connected player remains, the last-player timer is also started through startLastPlayerTimer(). If another player reconnects in time, reConnectedPlayer(...) cancels the timer through cancelLastPlayerTimer(). If instead nobody returns before the expiration, the timer inserts a SystemCloseGameCommand into the queue and the match is closed.

Network Layer

The server-side network layer has the task of hiding the concrete protocol from the rest of the application. Controllers and models must not know whether the client is connected through TCP or RMI: they see a connection through common contracts.

The central concept is the VirtualView. At the architectural level, it represents the client's remote view inside the server: it is the object that allows the server to treat the client as if it were a local view. Incoming, it represents the point where client -> server commands arrive; outgoing, it also implements Listener, so it can receive updates from the models and send them to the client. In this way, the model notifies a Listener, not a socket or an RMI stub.

The concrete implementations are VirtualViewTCP and VirtualViewRMI. Both do three things:

  • register a new session in the ServerSessionRegistry through ServerConnectionLifecycle
  • receive messages from the client, reconstruct an application payload, and pass it to the MessageRouter
  • receive server -> client payloads and send them to the client using the concrete protocol

The MessageRouter is the separation point between network and application logic. The virtual view passes it only sessionId and ClientPayload. The router consults the ServerSessionRegistry, understands which phase that session is in, and inserts the command into the queue of the correct controller: PreLobbyController, LobbyController, or GameController.

TCP

In the TCP case, the server starts from ConnectionAcceptor. This class opens a ServerSocket, stays in the accept() loop, and for each new socket creates a dedicated VirtualViewTCP. The VirtualViewTCP is then started on a separate thread, because it must remain in blocking read on the client socket.

Socket clientSocket = this.serverSocket.accept();
VirtualViewTCP virtualView = new VirtualViewTCP(clientSocket, this.serverConnectionLifecycle, this.router);
Thread virtualViewThread = new Thread(virtualView);
virtualViewThread.start();

Inside VirtualViewTCP, the socket channels are prepared. The PrintWriter is used for server -> client messages, while the BufferedReader is used in the client -> server message reading loop.

while ((rawMessage = reader.readLine()) != null) {
    this.commandHandler.handle(rawMessage);
}

The ClientCommandHandler class is the TCP-specific helper that takes a JSON line, reads the type field, deserializes the correct payload, and reconstructs a ClientMessage. If the message is a PONG, it is not sent to the application controllers, but is used to update the connection lifecycle. For all other messages, VirtualViewTCP.handleClientMessage(...) is called, which forwards the payload to the MessageRouter.

Outgoing, the flow is the opposite. The model produces a ServerPayload, often built through phase-specific mappers such as PreLobbyPayloadMapper, LobbyPayloadMapper, or GamePayloadMapper. The VirtualViewTCP receives that payload through notifyUpdate(...), wraps it in a ServerMessage with the related ServerMessageType, serializes it with GSON, and writes it to the socket.

RMI

In the RMI case, the first remote object exposed by the server is RmiConnectionEntryPoint. This object implements RmiConnectionService and is published in the registry with the name MesosServer. The client performs lookup of MesosServer, obtains the entry point stub, and calls connect(...), passing its own RMI callback.

public RmiServerService connect(RmiClientService clientCallback) throws RemoteException {
    return new VirtualViewRMI(this.serverConnectionLifecycle, clientCallback, this.router);
}

This call creates a dedicated VirtualViewRMI for that client. The VirtualViewRMI is the RMI equivalent of VirtualViewTCP: it registers a session, stores the client callback, and becomes the server-side stub that the client will use to send subsequent commands.

Incoming, when the client invokes a remote method on the stub, a ClientMessage already reconstructed by RMI arrives. The VirtualViewRMI checks that the message is valid and then passes the payload to the MessageRouter. It does not need to parse JSON and does not need to manually manage a socket, because that work is hidden by the RMI infrastructure.

Outgoing, instead, the VirtualViewRMI uses the remote callback RmiClientService. When it receives a ServerPayload through notifyUpdate(...), it wraps it in a ServerMessage and calls clientCallback.handleMessage(...). So here too, the model only sees a Listener, while the RMI detail remains confined to the virtual view.

Helpers

The main helpers of the network layer are those that make it possible to keep the rest of the server independent from the concrete protocol.

ClientConnection is the minimal contract for a registered client connection. It exposes lifecycle operations, in particular sendPing() and close(). Both VirtualViewTCP and VirtualViewRMI implement this interface: the PingPongRoutine can therefore send pings and close connections without knowing whether TCP or RMI is behind them.

ServerConnectionLifecycle centralizes the management of the life of connections. When a virtual view is created, it registers through registerConnection(...) and obtains a sessionId. When a PONG arrives, the virtual view calls recordPong(...). When a connection must be closed, the lifecycle reads the current phase from the ServerSessionRegistry and decides whether to directly unregister the session or send a SystemDisconnectCommand to the correct lobby/game controller.

PingPongRoutine is the periodic heartbeat routine. Every interval, it reads the session snapshots from the ServerSessionRegistry. If a session has not expired, it sends a ping through ClientConnection.sendPing(). If instead a session has expired, it calls ServerConnectionLifecycle.handleConnectionTimeout(...), which closes the connection and starts the disconnection flow coherent with the user's phase.

ProtocolErrorMapper is used at the boundary of the network layer. If TCP or RMI intercept a malformed message or an error before the command reaches the controllers, the exception is transformed into an ErrorPayload. The actual application errors instead remain the responsibility of controllers and models.

Multi-Match Management

Multi-match management is based on a simple idea: every important application phase has a controller that runs on its own thread and processes commands through a queue. In this way, messages can arrive from different threads, for example TCP threads, RMI callbacks, timeout routines, or other controllers, but the logic of a single phase is still executed in order by a single thread. The controller thread therefore becomes the single serialization point for commands for that phase, protecting the related model from direct concurrent access.

The common contract of internal controller messages is QueuedCommand. Each controller exposes enqueueMessage(...), which adds a command to its own BlockingQueue. The run() method of the controller then waits with commandQueue.take() and processes one command at a time.

QueuedCommand cmd = this.commandQueue.take();

These queues do not contain only messages coming from the network. Client -> server messages are wrapped in ClientPayloadCommand: they contain the sessionId and the ClientPayload reconstructed by the network layer. However, there are also internal commands generated by the server, such as SystemJoinCommand, SystemDisconnectCommand, SystemSetupMatchCommand, and SystemCloseGameCommand. These do not represent direct client requests, but events that the server must make the correct controller process in the same order as the other commands.

The MessageRouter is the point where messages coming from the network enter this system. It receives from the virtual view the sessionId and the payload, reads the current phase of the session from the ServerSessionRegistry, and inserts a ClientPayloadCommand into the queue of the correct controller. If the session is in pre-lobby, it uses the PreLobbyController; if it is in lobby, it retrieves the LobbyController of the specific lobby; if it is in game, it retrieves the GameController of the specific match.

This structure is applied to the three main phases of the server: pre-lobby, lobby, and game. The pre-lobby is global, while lobby and game have dedicated controllers and models for each created instance. The following sections explain the concrete role of each phase and how sessions are moved from one phase to another.

Pre Lobby

The pre-lobby is the global phase of the server, meaning the one where newly connected users are located before entering a lobby or a match. For this reason, there is a single PreLobbyController, created at server startup. The controller owns a command queue and delegates application logic to the PreLobbyModel.

The commands handled in this phase are those defined by PreLobbyCommandType: authentication, lobby list, lobby creation, and lobby join. The controller receives a ClientPayloadCommand, extracts the payload, and chooses the correct method based on the payload type. If the payload is not allowed in pre-lobby, a phase error is notified to the client.

Authentication is handled by the model through authenticate(...). The model registers the nickname in the ServerSessionRegistry and notifies the client with an AuthenticationResultPayload. If the nickname is invalid or already taken, an ErrorPayload is sent. After a successful authentication, the controller also checks whether that nickname has a reconnection slot toward a match: if one exists and the game is still open, it inserts a SystemJoinCommand into the queue of the related GameController.

The lobby list is obtained from the ServerSessionRegistry through getOpenLobbies(). The model then builds the response payload with PreLobbyPayloadMapper, so the client receives only a lightweight representation of the available lobbies.

Lobby creation always starts from the pre-lobby. The model validates that the session is authenticated and not already in a lobby, then creates the Lobby model through the registry. The controller completes the architectural part: it moves the session to the lobby phase with enterLobby(...), creates a LobbyController, registers it in the ServerSessionRegistry, and starts the thread dedicated to the lobby.

Joining an existing lobby is divided into two steps. First, the PreLobbyModel validates that the lobby exists, is joinable, and that the user can enter. Then the PreLobbyController retrieves the target LobbyController and sends it a SystemJoinCommand. In this way, the actual modification of the lobby does not happen in the pre-lobby thread, but in the thread of the controller that owns that lobby.

Lobby

The lobby is a specific application phase: every open lobby has its own Lobby model and its own LobbyController. The controller runs on a dedicated thread and serializes all commands concerning that lobby. This prevents multiple clients from modifying the same lobby model at the same time.

The LobbyController receives two main kinds of commands. Client commands arrive as ClientPayloadCommand and are interpreted through LobbyCommandType: leaving the lobby and starting the lobby. Internal commands, instead, arrive from the server: SystemJoinCommand when a user must enter and SystemDisconnectCommand when a session in the lobby disconnects.

When a SystemJoinCommand arrives, the controller retrieves nickname and listener from the ServerSessionRegistry, passes them to the Lobby model through addPlayer(...), then updates the registry with enterLobby(...). If insertion succeeds, the model notifies all listeners with notifyPlayerJoined(...).

When a user voluntarily leaves the lobby, the controller calls lobby.removePlayer(...), returns the session to pre-lobby with leaveLobby(...), and notifies the other clients with notifyPlayerLeft(...). If the lobby becomes empty, the controller removes both the LobbyController and the Lobby model from the registry. Disconnection follows similar logic, but at the end the session is unregistered from the registry because it is no longer an active session.

Starting the lobby can only be requested by the host. The validation is in the Lobby model, through startMatch(...). If the lobby can start, the controller creates the Players from the lobby users through LobbyPlayerFactory, creates the Game model through GameFactory, and creates a dedicated GameController for the match.

The lobby -> game transition also updates the ServerSessionRegistry. All lobby sessions are moved to the game phase with enterGame(...); listeners are removed from the lobby and added to the game. The new GameController is registered in the registry, while the lobby controller and model are removed. Finally, the game thread is started and, after a short countdown, a SystemSetupMatchCommand is inserted into the queue to initialize the match.

Game

The game is the phase in which a single match is managed. Each match has its own Game model and its own GameController, registered in the ServerSessionRegistry as the game orchestrator. Here too, the controller runs on a dedicated thread and becomes the single serialization point for modifications to the match model.

The GameController receives client commands and internal commands. Client commands arrive as game payloads and are classified through GameCommandType: MOVE_TOTEM, CHOOSE_CARD, and EXTRA_DRAW. Before calling the model, the controller resolves the Player associated with the session using the registry. Then it delegates the actual logic to the Game model, calling moveTotem(...), chooseCard(...), or extraDrawn(...).

Internal commands are instead used to manage the lifecycle of the match. SystemSetupMatchCommand starts the initial game setup. SystemDisconnectCommand handles the disconnection of a player. SystemJoinCommand is used for the reconnection of an already authenticated player. SystemCloseGameCommand forcibly closes the match, for example when the last-player timer expires. These events also pass through the same queue, so they never modify the model from external threads.

The Game model contains the domain logic of the match. The controller does not decide the round rules: it only acts as an orchestrator between messages, sessions, and model. When the model changes state, it notifies the registered listeners, meaning the virtual views of the clients that are still connected. The payloads sent to clients are built using GamePayloadMapper, so the network receives a representation of the game suitable for transfer, not the model's internal objects directly.

After every relevant application or internal command, the controller calls checkGameEndAndCleanup(). If the game has reached the FINISHED state, the controller marks the game as closing, cancels any timers, moves the still registered sessions out of the game, removes the GameController from the registry, and deletes the reconnection slots associated with that match.

Reconnection is always handled through the GameController queue. When a player reconnects, the controller consumes the reconnection slot from the ServerSessionRegistry, brings the new session back into the game, adds the new listener to the model, and marks the player as connected. If after the reconnection there is no longer only one connected player, the last-player timer is cancelled.

Technologies

  • Java
  • JavaFX
  • TCP
  • RMI
  • MVC
  • MySQL