/** * ================================================================================ * SiPro SocketArena™ - Assignment: TCP Concurrent Server (Networked Tic-Tac-Toe) * ================================================================================ * COURSE : Distributed Computing & Systems Programming * ASSIGNMENT : Concurrent Multi-Threaded TCP Game Server & Matchmaking Daemon * LANGUAGE : Java (SE 11+) * ARCHITECTURE : Multi-client concurrent server. For every 2 connected players, * the server pairs them and spawns an isolated worker thread to * manage their Tic-Tac-Toe match independently and concurrently. * PROTOCOL : RFC-SIPRO-TTT (ASCII Line-Delimited TCP Protocol) * * STUDENT INSTRUCTIONS: * Complete all 10 fill-in-the-blank sections marked with: * // >>> FILL IN THE BLANK [X]: ... <<< * Do NOT alter the protocol framing or matchmaking logic. * * COMPILATION & EXECUTION: * javac TicTacToeServer.java * java TicTacToeServer * ================================================================================ */ import java.io.*; import java.net.*; import java.util.*; public class TicTacToeServer { public static final String HOST = "0.0.0.0"; public static final int PORT = 9099; // Mutex lock object for thread-safe matchmaking queue private static final Object matchmakingLock = new Object(); // Represents a waiting player before pair matching private static PlayerSession waitingPlayer = null; static class PlayerSession { Socket socket; String name; BufferedReader in; PrintWriter out; PlayerSession(Socket socket, String name) throws IOException { this.socket = socket; this.name = name; this.in = new BufferedReader(new InputStreamReader(socket.getInputStream())); this.out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true); } void send(String msg) { out.println(msg); } String receive() throws IOException { return in.readLine(); } void close() { try { socket.close(); } catch (IOException ignored) {} } } /** * Evaluates 3x3 board state. * Returns: "X", "O", "DRAW" (if full), or null (if game is still active). * Board is represented as a char array of length 9: index 0..8. */ public static String checkWinner(char[] board) { int[][] winLines = { {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, // Rows {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, // Columns {0, 4, 8}, {2, 4, 6} // Diagonals }; for (int[] line : winLines) { char a = board[line[0]]; char b = board[line[1]]; char c = board[line[2]]; if (a != ' ' && a == b && b == c) { return String.valueOf(a); } } boolean hasEmpty = false; for (char cell : board) { if (cell == ' ') { hasEmpty = true; break; } } return hasEmpty ? null : "DRAW"; } /** * Formats 3x3 board as single-line string representation. */ public static String formatBoard(char[] board) { return new String(board); } /** * Dedicated Match Worker handling an isolated 2-player match concurrently. * Player 1 is assigned 'X' (moves first). * Player 2 is assigned 'O' (moves second). */ static class MatchWorker implements Runnable { private final PlayerSession p1; private final PlayerSession p2; private final String matchId; public MatchWorker(PlayerSession p1, PlayerSession p2) { this.p1 = p1; this.p2 = p2; this.matchId = p1.name + "_vs_" + p2.name + "_" + System.currentTimeMillis(); } @Override public void run() { System.out.println("\n[MATCH STARTED: " + matchId + "]"); System.out.println(" -> Player 1 (X): " + p1.name + " (" + p1.socket.getRemoteSocketAddress() + ")"); System.out.println(" -> Player 2 (O): " + p2.name + " (" + p2.socket.getRemoteSocketAddress() + ")"); char[] board = new char[9]; Arrays.fill(board, ' '); char currentTurn = 'X'; // Notify both players match has started p1.send("START X " + p2.name); p2.send("START O " + p1.name); boolean active = true; try { while (active) { // Broadcast current board state to both players String boardStr = formatBoard(board); p1.send("BOARD " + boardStr); p2.send("BOARD " + boardStr); // >>> FILL IN THE BLANK [9]: Evaluate board state using checkWinner(board) <<< // String winner = checkWinner(board); String winner = null; // REPLACE THIS LINE if (winner != null) { if ("DRAW".equals(winner)) { p1.send("MATCH_OVER DRAW The match ended in a stalemate!"); p2.send("MATCH_OVER DRAW The match ended in a stalemate!"); System.out.println("[" + matchId + "] Result: DRAW"); } else if ("X".equals(winner)) { p1.send("MATCH_OVER VICTORY Congratulations! You won as X!"); p2.send("MATCH_OVER DEFEAT Player X has won the match."); System.out.println("[" + matchId + "] Result: " + p1.name + " (X) Won!"); } else { p1.send("MATCH_OVER DEFEAT Player O has won the match."); p2.send("MATCH_OVER VICTORY Congratulations! You won as O!"); System.out.println("[" + matchId + "] Result: " + p2.name + " (O) Won!"); } active = false; break; } // Turn Management PlayerSession activePlayer = (currentTurn == 'X') ? p1 : p2; PlayerSession waitingPeer = (currentTurn == 'X') ? p2 : p1; char activeSymbol = currentTurn; activePlayer.send("YOUR_TURN"); waitingPeer.send("OPPONENT_TURN"); // Receive move command: "MOVE " String cmd = activePlayer.receive(); if (cmd == null) { waitingPeer.send("MATCH_OVER OPPONENT_QUIT Your opponent disconnected."); break; } String[] parts = cmd.trim().split("\\s+"); if (parts.length == 3 && parts[0].equalsIgnoreCase("MOVE")) { try { int r = Integer.parseInt(parts[1]); int c = Integer.parseInt(parts[2]); int idx = r * 3 + c; if (r >= 0 && r <= 2 && c >= 0 && c <= 2 && board[idx] == ' ') { board[idx] = activeSymbol; currentTurn = (currentTurn == 'X') ? 'O' : 'X'; } else { activePlayer.send("INVALID_MOVE Cell already occupied or out of bounds (0-2)"); } } catch (NumberFormatException e) { activePlayer.send("INVALID_MOVE Coordinates must be numeric integers (e.g. MOVE 1 1)"); } } else if (parts.length > 0 && parts[0].equalsIgnoreCase("QUIT")) { waitingPeer.send("MATCH_OVER OPPONENT_QUIT Your opponent quit the game."); break; } else { activePlayer.send("INVALID_MOVE Unrecognized command. Syntax: MOVE "); } } } catch (IOException e) { System.out.println("[" + matchId + "] Network error: " + e.getMessage()); } finally { // >>> FILL IN THE BLANK [10]: Gracefully close both client sockets and cleanup <<< // p1.close(); // p2.close(); // REPLACE THIS LINE WITH PROPER SOCKET TEARDOWN System.out.println("[" + matchId + "] Match completed. Worker thread cleanly terminated.\n"); } } } /** * Master TCP Server Daemon with Multi-Client Concurrency Loop. */ public static void main(String[] args) { System.out.println("===================================================================="); System.out.println(" SiPro SocketArena™ - Concurrent TCP Tic-Tac-Toe Server (Java)"); System.out.println(" Listening on: " + HOST + ":" + PORT); System.out.println(" Concurrency : Multi-Threaded Matchmaking (Worker Thread per Match)"); System.out.println("===================================================================="); ServerSocket serverSocket = null; try { // >>> FILL IN THE BLANK [1]: Instantiate a ServerSocket on PORT (9099) <<< // serverSocket = new ServerSocket(); serverSocket = null; // REPLACE THIS LINE // >>> FILL IN THE BLANK [2]: Configure socket option to reuse local address immediately <<< // serverSocket.setReuseAddress(true); // REPLACE THIS LINE // >>> FILL IN THE BLANK [3]: Bind the ServerSocket to local endpoint (InetSocketAddress) <<< // serverSocket.bind(new InetSocketAddress(PORT)); // REPLACE THIS LINE // >>> FILL IN THE BLANK [4]: Verify backlog queue depth is at least 10 connections <<< // System.out.println("[READY] Listening on port " + PORT + " with backlog 10..."); System.out.println("[READY] Awaiting player connections...\n"); while (true) { // >>> FILL IN THE BLANK [5]: Accept an incoming client TCP connection <<< // Socket clientSocket = serverSocket.accept(); Socket clientSocket = null; // REPLACE THIS LINE if (clientSocket == null) { continue; } System.out.println("[TCP INGRESS] Connection established from " + clientSocket.getRemoteSocketAddress()); // Read registration command: "JOIN " BufferedReader tempIn = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); PrintWriter tempOut = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream()), true); String regMsg = tempIn.readLine(); if (regMsg == null || !regMsg.startsWith("JOIN")) { tempOut.println("ERROR Must send 'JOIN ' as first message."); clientSocket.close(); continue; } String[] regTokens = regMsg.split("\\s+", 2); String playerName = (regTokens.length > 1 && !regTokens[1].isBlank()) ? regTokens[1].trim() : "Player_" + clientSocket.getPort(); System.out.println("[REGISTERED] Player '" + playerName + "' joined."); PlayerSession session = new PlayerSession(clientSocket, playerName); // >>> FILL IN THE BLANK [6]: Acquire thread-safe matchmaking lock synchronized(matchmakingLock) <<< // synchronized (matchmakingLock) { if (true) { // REPLACE WITH synchronized(matchmakingLock) if (waitingPlayer == null) { waitingPlayer = session; session.send("WAIT Waiting for an opponent to join..."); System.out.println(" -> '" + playerName + "' placed in matchmaking queue. Waiting for opponent..."); } else { PlayerSession p1 = waitingPlayer; PlayerSession p2 = session; waitingPlayer = null; // Clear queue for next pair // >>> FILL IN THE BLANK [7]: Instantiate MatchWorker runnable and wrap in Thread <<< // MatchWorker worker = new MatchWorker(p1, p2); // Thread matchThread = new Thread(worker); Thread matchThread = null; // REPLACE THIS LINE // >>> FILL IN THE BLANK [8]: Start the worker thread concurrently <<< // matchThread.start(); // REPLACE THIS LINE System.out.println(" -> Match paired (" + p1.name + " vs " + p2.name + ")! Worker thread active."); } } } } catch (IOException e) { System.err.println("[SERVER ERROR] " + e.getMessage()); } finally { if (serverSocket != null && !serverSocket.isClosed()) { try { serverSocket.close(); } catch (IOException ignored) {} } } } }