From 674b3efc3ab52753a8f70608f565018e84641048 Mon Sep 17 00:00:00 2001 From: Hymmel Date: Tue, 10 Feb 2026 13:17:43 +0100 Subject: [PATCH] a --- README.md | 43 ++++ build_client.sh | 5 + client/build.sh | 14 ++ client/pom.xml | 35 ++++ .../java/com/lona/tictactoe/client/Main.java | 196 ++++++++++++++++++ docker-compose.yml | 30 +++ server/Dockerfile | 11 + server/pom.xml | 37 ++++ .../lona/tictactoe/server/ClientHandler.java | 114 ++++++++++ .../lona/tictactoe/server/GameInstance.java | 138 ++++++++++++ .../lona/tictactoe/server/GameManager.java | 23 ++ .../com/lona/tictactoe/server/GameServer.java | 25 +++ .../lona/tictactoe/server/ClientHandler.class | Bin 0 -> 4558 bytes .../lona/tictactoe/server/GameInstance.class | Bin 0 -> 3908 bytes .../lona/tictactoe/server/GameManager.class | Bin 0 -> 1670 bytes .../lona/tictactoe/server/GameServer.class | Bin 0 -> 2278 bytes web-client/Dockerfile | 13 ++ web-client/pom.xml | 35 ++++ .../com/lona/tictactoe/web/Application.java | 11 + .../tictactoe/web/GameWebSocketHandler.java | 47 +++++ .../com/lona/tictactoe/web/TcpSession.java | 81 ++++++++ .../com/lona/tictactoe/web/WebController.java | 12 ++ .../lona/tictactoe/web/WebSocketConfig.java | 23 ++ .../src/main/resources/static/css/style.css | 191 +++++++++++++++++ .../src/main/resources/static/js/app.js | 155 ++++++++++++++ .../src/main/resources/templates/index.html | 61 ++++++ 26 files changed, 1300 insertions(+) create mode 100644 README.md create mode 100644 build_client.sh create mode 100755 client/build.sh create mode 100644 client/pom.xml create mode 100644 client/src/main/java/com/lona/tictactoe/client/Main.java create mode 100644 docker-compose.yml create mode 100644 server/Dockerfile create mode 100644 server/pom.xml create mode 100644 server/src/main/java/com/lona/tictactoe/server/ClientHandler.java create mode 100644 server/src/main/java/com/lona/tictactoe/server/GameInstance.java create mode 100644 server/src/main/java/com/lona/tictactoe/server/GameManager.java create mode 100644 server/src/main/java/com/lona/tictactoe/server/GameServer.java create mode 100644 server/target/classes/com/lona/tictactoe/server/ClientHandler.class create mode 100644 server/target/classes/com/lona/tictactoe/server/GameInstance.class create mode 100644 server/target/classes/com/lona/tictactoe/server/GameManager.class create mode 100644 server/target/classes/com/lona/tictactoe/server/GameServer.class create mode 100644 web-client/Dockerfile create mode 100644 web-client/pom.xml create mode 100644 web-client/src/main/java/com/lona/tictactoe/web/Application.java create mode 100644 web-client/src/main/java/com/lona/tictactoe/web/GameWebSocketHandler.java create mode 100644 web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java create mode 100644 web-client/src/main/java/com/lona/tictactoe/web/WebController.java create mode 100644 web-client/src/main/java/com/lona/tictactoe/web/WebSocketConfig.java create mode 100644 web-client/src/main/resources/static/css/style.css create mode 100644 web-client/src/main/resources/static/js/app.js create mode 100644 web-client/src/main/resources/templates/index.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..56a31b0 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# TicTacToe Project + +This project contains a TCP Game Server, a Java Swing Client, and a Spring Boot Web Client. + +## Structure + +- `server/`: Java TCP Server (Port 1870). With Anticheat (Server-side validation). +- `client/`: Standalone Java Swing Client. Connects to `dokploy.lona-development.org:1870`. +- `web-client/`: Spring Boot Web Application. Acts as a proxy to the TCP Server via WebSocket. + +## Running with Docker Compose + +To start the Server and Web Client: + +```bash +docker-compose up --build +``` + +- **Server** runs on port `1870`. +- **Web Client** runs on `http://localhost:8080`. + +## Building the Java Client + +To build the standalone Java client: + +```bash +./build_client.sh +``` + +To run it: + +```bash +java -jar client/target/client-1.0-SNAPSHOT.jar +``` + +**Note:** The Java client is hardcoded to connect to `dokploy.lona-development.org`. Ensure this domain resolves to your server IP (or adds `127.0.0.1 dokploy.lona-development.org` to `/etc/hosts` for local testing). + +## Game Protocol + +- **CREATE**: Starts a new game. Server returns a 6-character code. +- **JOIN <CODE>**: Joins an existing game. +- **MOVE <ROW> <COL>**: Places your symbol. +- **SURRENDER**: Forfeits the game. diff --git a/build_client.sh b/build_client.sh new file mode 100644 index 0000000..e661a78 --- /dev/null +++ b/build_client.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd client +mvn clean package +echo "Client built: client/target/client-1.0-SNAPSHOT.jar" +echo "To run: java -jar client/target/client-1.0-SNAPSHOT.jar" diff --git a/client/build.sh b/client/build.sh new file mode 100755 index 0000000..0486014 --- /dev/null +++ b/client/build.sh @@ -0,0 +1,14 @@ +#!/bin/bash +echo "Building TicTacToe Standalone Client..." +mvn clean package + +if [ $? -eq 0 ]; then + echo "--------------------------------------------------" + echo "Build Successful!" + echo "You can run the client with:" + echo "java -jar target/client-1.0-SNAPSHOT.jar" + echo "--------------------------------------------------" +else + echo "Build Failed!" + exit 1 +fi diff --git a/client/pom.xml b/client/pom.xml new file mode 100644 index 0000000..b7f8b4a --- /dev/null +++ b/client/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + com.lona.tictactoe + client + 1.0-SNAPSHOT + + 17 + 17 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + com.lona.tictactoe.client.Main + + + + + + + + + diff --git a/client/src/main/java/com/lona/tictactoe/client/Main.java b/client/src/main/java/com/lona/tictactoe/client/Main.java new file mode 100644 index 0000000..c91af14 --- /dev/null +++ b/client/src/main/java/com/lona/tictactoe/client/Main.java @@ -0,0 +1,196 @@ +package com.lona.tictactoe.client; + +import javax.swing.*; +import java.awt.*; +import java.io.*; +import java.net.Socket; + +public class Main extends JFrame { + private Socket socket; + private PrintWriter out; + private BufferedReader in; + + private CardLayout cardLayout = new CardLayout(); + private JPanel mainPanel = new JPanel(cardLayout); + + // Menu Components + private JTextField joinCodeField = new JTextField(10); + + // Game Components + private JButton[] buttons = new JButton[9]; + private JLabel statusLabel = new JLabel("Connecting..."); + private JLabel codeLabel = new JLabel("Code: -"); + private boolean myTurn = false; + private char mySymbol = ' '; // assigned by server implied turn + + public Main() { + super("TicTacToe Client"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(400, 500); + setLocationRelativeTo(null); + + initUI(); + setContentPane(mainPanel); + + connectToServer(); + } + + private void initUI() { + // --- MENU PANEL --- + JPanel menuPanel = new JPanel(new GridBagLayout()); + JButton createBtn = new JButton("Create Game"); + JButton joinBtn = new JButton("Join Game"); + + createBtn.addActionListener(e -> send("CREATE")); + joinBtn.addActionListener(e -> { + String code = joinCodeField.getText().trim(); + if (!code.isEmpty()) + send("JOIN " + code); + }); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.gridx = 0; + gbc.gridy = 0; + menuPanel.add(createBtn, gbc); + + gbc.gridy = 1; + menuPanel.add(new JLabel("Enter Code:"), gbc); + gbc.gridy = 2; + menuPanel.add(joinCodeField, gbc); + gbc.gridy = 3; + menuPanel.add(joinBtn, gbc); + + // --- GAME PANEL --- + JPanel gamePanel = new JPanel(new BorderLayout()); + + JPanel topPanel = new JPanel(new GridLayout(2, 1)); + topPanel.add(statusLabel); + topPanel.add(codeLabel); + gamePanel.add(topPanel, BorderLayout.NORTH); + + JPanel boardPanel = new JPanel(new GridLayout(3, 3)); + for (int i = 0; i < 9; i++) { + int finalI = i; + buttons[i] = new JButton(""); + buttons[i].setFont(new Font("Arial", Font.BOLD, 40)); + buttons[i].setFocusPainted(false); + buttons[i].addActionListener(e -> { + int r = finalI / 3; + int c = finalI % 3; + send("MOVE " + r + " " + c); + }); + boardPanel.add(buttons[i]); + } + gamePanel.add(boardPanel, BorderLayout.CENTER); + + JButton surrenderBtn = new JButton("Surrender / Give Up"); + surrenderBtn.addActionListener(e -> send("SURRENDER")); + gamePanel.add(surrenderBtn, BorderLayout.SOUTH); + + mainPanel.add(menuPanel, "MENU"); + mainPanel.add(gamePanel, "GAME"); + } + + private void connectToServer() { + new Thread(() -> { + try { + // Using the specific domain provided + socket = new Socket("dokploy.lona-development.org", 1870); + out = new PrintWriter(socket.getOutputStream(), true); + in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + SwingUtilities.invokeLater(() -> statusLabel.setText("Connected to Server.")); + + String line; + while ((line = in.readLine()) != null) { + processMessage(line); + } + } catch (IOException e) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(this, "Connection Error: " + e.getMessage()); + statusLabel.setText("Disconnected."); + }); + } + }).start(); + } + + private void send(String msg) { + if (out != null) + out.println(msg); + } + + private void processMessage(String msg) { + SwingUtilities.invokeLater(() -> handleMessage(msg)); + } + + private void handleMessage(String msg) { + String[] parts = msg.split(" "); + String cmd = parts[0]; + + switch (cmd) { + case "GAME_CREATED": + cardLayout.show(mainPanel, "GAME"); + if (parts.length > 1) { + codeLabel.setText("Code: " + parts[1]); + mySymbol = 'X'; // Creator starts as X usually + statusLabel.setText("Game Created. Waiting for opponent..."); + } + break; + case "JOIN_SUCCESS": + cardLayout.show(mainPanel, "GAME"); + mySymbol = 'O'; // Joiner is O + statusLabel.setText("Joined Game."); + codeLabel.setText("Code: " + joinCodeField.getText()); + break; + case "START": + statusLabel.setText("Game Started! X goes first."); + break; + case "BOARD": // BOARD X O X ... + String boardStr = msg.substring(6); // remove "BOARD " + for (int i = 0; i < 9 && i < boardStr.length(); i++) { + char c = boardStr.charAt(i); + buttons[i].setText(String.valueOf(c)); + } + break; + case "TURN": + if (parts.length > 1) { + char turn = parts[1].charAt(0); + if (turn == mySymbol) { + statusLabel.setText("Your Turn (" + mySymbol + ")"); + } else { + statusLabel.setText("Opponent's Turn (" + turn + ")"); + } + } + break; + case "WIN": + String winner = msg.substring(4); + JOptionPane.showMessageDialog(this, "Winner: " + winner); + resetGame(); + break; + case "DRAW": + JOptionPane.showMessageDialog(this, "Draw!"); + resetGame(); + break; + case "ERROR:": + if (msg.contains("It is not your turn")) { + statusLabel.setText("Not your turn!"); + } else { + JOptionPane.showMessageDialog(this, msg); + } + break; + } + } + + private void resetGame() { + cardLayout.show(mainPanel, "MENU"); + for (JButton btn : buttons) + btn.setText(""); + statusLabel.setText("Connected."); + codeLabel.setText("Code: -"); + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> new Main().setVisible(true)); + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5d62538 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.8' + +services: + server: + build: ./server + container_name: tictactoe-server + restart: unless-stopped + ports: + - "1870:1870" + networks: + - tictactoe-net + + web: + build: ./web-client + container_name: tictactoe-web + restart: unless-stopped + ports: + - "8080:8080" + environment: + - GAME_SERVER_HOST=server + - GAME_SERVER_PORT=1870 + - SERVER_PORT=8080 + depends_on: + - server + networks: + - tictactoe-net + +networks: + tictactoe-net: + driver: bridge diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..c7e9331 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,11 @@ +FROM maven:3.8.5-openjdk-17 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:17-jre +WORKDIR /app +COPY --from=build /app/target/server-1.0-SNAPSHOT.jar app.jar +EXPOSE 1870 +CMD ["java", "-jar", "app.jar"] diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000..d6bacda --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + com.lona.tictactoe + server + 1.0-SNAPSHOT + + 17 + 17 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + com.lona.tictactoe.server.GameServer + + + + + + + + + diff --git a/server/src/main/java/com/lona/tictactoe/server/ClientHandler.java b/server/src/main/java/com/lona/tictactoe/server/ClientHandler.java new file mode 100644 index 0000000..5337ffe --- /dev/null +++ b/server/src/main/java/com/lona/tictactoe/server/ClientHandler.java @@ -0,0 +1,114 @@ +package com.lona.tictactoe.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; + +public class ClientHandler implements Runnable { + private final Socket client; + private PrintWriter out; + private BufferedReader in; + private String gameCode; + + public ClientHandler(Socket client) { + this.client = client; + } + + @Override + public void run() { + try { + out = new PrintWriter(client.getOutputStream(), true); + in = new BufferedReader(new InputStreamReader(client.getInputStream())); + + String line; + while ((line = in.readLine()) != null) { + // Command processing + String[] parts = line.split(" "); + String cmd = parts[0].toUpperCase(); + + switch (cmd) { + case "CREATE": + String newCode = GameManager.createGame(this); + this.gameCode = newCode; + out.println("GAME_CREATED " + newCode); + break; + + case "JOIN": + if (parts.length < 2) { + out.println("ERROR: Missing game code"); + break; + } + String code = parts[1]; + GameInstance game = GameManager.getGame(code); + if (game != null) { + if (game.join(this)) { + this.gameCode = code; + out.println("JOIN_SUCCESS"); + } else { + out.println("ERROR: Game full or already started"); + } + } else { + out.println("ERROR: Game not found"); + } + break; + + case "MOVE": + if (gameCode == null) { + out.println("ERROR: Not in a game"); + break; + } + GameInstance g = GameManager.getGame(gameCode); + if (g == null) { + out.println("ERROR: Game ended"); + break; + } + if (parts.length < 3) { + out.println("ERROR: Missing coordinates"); + break; + } + try { + int r = Integer.parseInt(parts[1]); + int c = Integer.parseInt(parts[2]); + g.move(this, r, c); + } catch (NumberFormatException e) { + out.println("ERROR: Invalid coordinates"); + } + break; + + case "SURRENDER": + if (gameCode != null) { + GameInstance active = GameManager.getGame(gameCode); + if (active != null) + active.surrender(this); + } + break; + + default: + out.println("ERROR: Unknown command"); + } + } + } catch (IOException e) { + System.err.println("Client disconnected unexpectedly: " + e.getMessage()); + } finally { + if (gameCode != null) { + GameInstance g = GameManager.getGame(gameCode); + if (g != null) + g.disconnect(this); + GameManager.removeGame(gameCode); + } + try { + client.close(); + } catch (IOException e) { + // ignore + } + } + } + + public void sendMessage(String msg) { + if (out != null) { + out.println(msg); + } + } +} diff --git a/server/src/main/java/com/lona/tictactoe/server/GameInstance.java b/server/src/main/java/com/lona/tictactoe/server/GameInstance.java new file mode 100644 index 0000000..1a8f1ff --- /dev/null +++ b/server/src/main/java/com/lona/tictactoe/server/GameInstance.java @@ -0,0 +1,138 @@ +package com.lona.tictactoe.server; + +import java.io.PrintWriter; +import java.util.Arrays; + +public class GameInstance { + private final String code; + private ClientHandler playerX; // Creator + private ClientHandler playerO; // Joiner + private final char[][] board = new char[3][3]; + private char currentTurn = 'X'; + private boolean finished = false; + + public GameInstance(String code, ClientHandler creator) { + this.code = code; + this.playerX = creator; + for (char[] row : board) { + Arrays.fill(row, ' '); + } + } + + public synchronized boolean join(ClientHandler joiner) { + if (playerO != null) + return false; + this.playerO = joiner; + broadcast("START X"); + sendBoard(); + return true; + } + + public synchronized void move(ClientHandler player, int x, int y) { + if (finished || playerO == null) + return; + + char symbol = (player == playerX) ? 'X' : 'O'; + + // --- ANTICHEAT / VALIDATION --- + // 1. Check if correct turn + if (symbol != currentTurn) { + player.sendMessage("ERROR: It is not your turn!"); + return; + } + + // 2. Check bounds + if (x < 0 || x > 2 || y < 0 || y > 2) { + player.sendMessage("ERROR: Invalid coordinates!"); + return; + } + + // 3. Check if cell is empty + if (board[x][y] != ' ') { + player.sendMessage("ERROR: Cell occupied!"); + return; + } + + // Apply move + board[x][y] = symbol; + sendBoard(); + + if (checkWin(symbol)) { + finished = true; + broadcast("WIN " + symbol); + createEndState(); + } else if (checkDraw()) { + finished = true; + broadcast("DRAW"); + createEndState(); + } else { + currentTurn = (currentTurn == 'X') ? 'O' : 'X'; + broadcast("TURN " + currentTurn); + } + } + + public synchronized void surrender(ClientHandler player) { + if (finished) + return; + finished = true; + char loser = (player == playerX) ? 'X' : 'O'; + char winner = (loser == 'X') ? 'O' : 'X'; + broadcast("WIN " + winner + " (Surrender)"); + createEndState(); + } + + public synchronized void disconnect(ClientHandler player) { + if (finished) + return; + finished = true; + broadcast("ERROR: Opponent disconnected"); + createEndState(); + } + + private void createEndState() { + // cleanup if needed + } + + private boolean checkWin(char s) { + // Rows & Cols + for (int i = 0; i < 3; i++) { + if (board[i][0] == s && board[i][1] == s && board[i][2] == s) + return true; + if (board[0][i] == s && board[1][i] == s && board[2][i] == s) + return true; + } + // Diagonals + if (board[0][0] == s && board[1][1] == s && board[2][2] == s) + return true; + if (board[0][2] == s && board[1][1] == s && board[2][0] == s) + return true; + return false; + } + + private boolean checkDraw() { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (board[i][j] == ' ') + return false; + } + } + return true; + } + + private void sendBoard() { + StringBuilder sb = new StringBuilder("BOARD "); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + sb.append(board[i][j]); + } + } + broadcast(sb.toString()); + } + + private void broadcast(String msg) { + if (playerX != null) + playerX.sendMessage(msg); + if (playerO != null) + playerO.sendMessage(msg); + } +} diff --git a/server/src/main/java/com/lona/tictactoe/server/GameManager.java b/server/src/main/java/com/lona/tictactoe/server/GameManager.java new file mode 100644 index 0000000..f39c063 --- /dev/null +++ b/server/src/main/java/com/lona/tictactoe/server/GameManager.java @@ -0,0 +1,23 @@ +package com.lona.tictactoe.server; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.UUID; + +public class GameManager { + private static final ConcurrentHashMap games = new ConcurrentHashMap<>(); + + public static String createGame(ClientHandler creator) { + String code = UUID.randomUUID().toString().substring(0, 6).toUpperCase(); + GameInstance game = new GameInstance(code, creator); + games.put(code, game); + return code; + } + + public static GameInstance getGame(String code) { + return games.get(code); + } + + public static void removeGame(String code) { + games.remove(code); + } +} diff --git a/server/src/main/java/com/lona/tictactoe/server/GameServer.java b/server/src/main/java/com/lona/tictactoe/server/GameServer.java new file mode 100644 index 0000000..19a6635 --- /dev/null +++ b/server/src/main/java/com/lona/tictactoe/server/GameServer.java @@ -0,0 +1,25 @@ +package com.lona.tictactoe.server; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class GameServer { + public static final int PORT = 1870; + private static final ExecutorService pool = Executors.newFixedThreadPool(100); + + public static void main(String[] args) { + System.out.println("Starting TicTacToe Server on port " + PORT + "..."); + try (ServerSocket listener = new ServerSocket(PORT)) { + while (true) { + Socket client = listener.accept(); + System.out.println("Client connected: " + client.getRemoteSocketAddress()); + pool.execute(new ClientHandler(client)); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/server/target/classes/com/lona/tictactoe/server/ClientHandler.class b/server/target/classes/com/lona/tictactoe/server/ClientHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..8a815f9f6c1159ab70148207ab3bf3dfbaeaeb5c GIT binary patch literal 4558 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 literal 0 HcmV?d00001 diff --git a/server/target/classes/com/lona/tictactoe/server/GameServer.class b/server/target/classes/com/lona/tictactoe/server/GameServer.class new file mode 100644 index 0000000000000000000000000000000000000000..3d5d71d84d1bcb07c7ea4b3e642e69acdb247ab8 GIT binary patch literal 2278 zcma)7>sAw26#fnbCW(XLA|STf*ditd80I3v)`a`?VOsJ!vOG4)4oP9ZafBV~e{(koO4**y2vw=2-VO!QL zPx{~`$AdKO=W3Ug-T-#6B?%Tls1)RMIU@*;$wWm(8+CE)G3v|T&rH8 zd1zQn<03vaaES`@!gDAxF$qF(U|pzXQIkqUS4}xiAR1JM69>JuWaRV;J~u#|i!nSx z-ozJ}qFgsMrx(|#obg)lv~=w#5cH2V}mz zy6=##qH*K7)Iq)oD6I(hSE)*r>ZFlG_%igwR3bXk%%As!|=Jq;Z{)S@@Xw!xPdTL{k>R@;R z+R^B5nOU@IX~(%e483ZS3Qe>sDc~KvOKS=zaVj!qLrBm$P<@E^de7|QEGx13Cm6fk zV=U~!S{U2Khm#2s4kdPR;SnxB##Mmy9^%*&%-$YK?BT}m_^J)}pZ`-D`x6FPd|h}t zU-%2j(b9c%6-IwWvM{=fO5tg#u#0aWP}+mUpKEljZu&)9lMwW|LlT|s2!#k7rPB;) z_E5OJm_Q$<(2W`NV~w=#5ds>GbCdvuj~{WD) + 4.0.0 + com.lona.tictactoe + web-client + 1.0-SNAPSHOT + + org.springframework.boot + spring-boot-starter-parent + 3.2.2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/web-client/src/main/java/com/lona/tictactoe/web/Application.java b/web-client/src/main/java/com/lona/tictactoe/web/Application.java new file mode 100644 index 0000000..8674795 --- /dev/null +++ b/web-client/src/main/java/com/lona/tictactoe/web/Application.java @@ -0,0 +1,11 @@ +package com.lona.tictactoe.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/web-client/src/main/java/com/lona/tictactoe/web/GameWebSocketHandler.java b/web-client/src/main/java/com/lona/tictactoe/web/GameWebSocketHandler.java new file mode 100644 index 0000000..4351233 --- /dev/null +++ b/web-client/src/main/java/com/lona/tictactoe/web/GameWebSocketHandler.java @@ -0,0 +1,47 @@ +package com.lona.tictactoe.web; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class GameWebSocketHandler extends TextWebSocketHandler { + + @Value("${game.server.host:localhost}") + private String serverHost; + + @Value("${game.server.port:1870}") + private int serverPort; + + private final Map sessions = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + TcpSession tcp = new TcpSession(session, serverHost, serverPort); + sessions.put(session, tcp); + tcp.start(); + } + + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + TcpSession tcp = sessions.get(session); + if (tcp != null) { + tcp.send(message.getPayload()); + } + } + + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + TcpSession tcp = sessions.remove(session); + if (tcp != null) { + tcp.close(); + } + } +} diff --git a/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java b/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java new file mode 100644 index 0000000..39efa55 --- /dev/null +++ b/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java @@ -0,0 +1,81 @@ +package com.lona.tictactoe.web; + +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public class TcpSession extends Thread { + private final WebSocketSession session; + private final String host; + private final int port; + private Socket socket; + private volatile PrintWriter out; + private BufferedReader in; + private volatile boolean running = true; + private final BlockingQueue sendQueue = new LinkedBlockingQueue<>(); + + public TcpSession(WebSocketSession session, String host, int port) { + this.session = session; + this.host = host; + this.port = port; + } + + @Override + public void run() { + try { + socket = new Socket(host, port); + out = new PrintWriter(socket.getOutputStream(), true); + in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + // Flush queue + String queued; + while ((queued = sendQueue.poll()) != null) { + out.println(queued); + } + + String line; + while (running && (line = in.readLine()) != null) { + if (session.isOpen()) { + session.sendMessage(new TextMessage(line)); + } + } + } catch (IOException e) { + // connection lost or failed + e.printStackTrace(); + } finally { + close(); + } + } + + public void send(String msg) { + if (out != null) { + out.println(msg); + } else { + sendQueue.offer(msg); + // Double check in case we connected while queueing + if (out != null) { + String queued; + while ((queued = sendQueue.poll()) != null) { + out.println(queued); + } + } + } + } + + public void close() { + running = false; + try { + if (socket != null && !socket.isClosed()) + socket.close(); + } catch (IOException e) { + // ignore + } + } +} diff --git a/web-client/src/main/java/com/lona/tictactoe/web/WebController.java b/web-client/src/main/java/com/lona/tictactoe/web/WebController.java new file mode 100644 index 0000000..fb3495e --- /dev/null +++ b/web-client/src/main/java/com/lona/tictactoe/web/WebController.java @@ -0,0 +1,12 @@ +package com.lona.tictactoe.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class WebController { + @GetMapping("/") + public String index() { + return "index"; + } +} diff --git a/web-client/src/main/java/com/lona/tictactoe/web/WebSocketConfig.java b/web-client/src/main/java/com/lona/tictactoe/web/WebSocketConfig.java new file mode 100644 index 0000000..129ebfc --- /dev/null +++ b/web-client/src/main/java/com/lona/tictactoe/web/WebSocketConfig.java @@ -0,0 +1,23 @@ +package com.lona.tictactoe.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + private final GameWebSocketHandler handler; + + public WebSocketConfig(GameWebSocketHandler handler) { + this.handler = handler; + } + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(handler, "/game") + .setAllowedOrigins("*"); + } +} diff --git a/web-client/src/main/resources/static/css/style.css b/web-client/src/main/resources/static/css/style.css new file mode 100644 index 0000000..1b04c69 --- /dev/null +++ b/web-client/src/main/resources/static/css/style.css @@ -0,0 +1,191 @@ +:root { + --bg-color: #09090b; + --card-bg: #18181b; + --primary: #2563eb; + --secondary: #db2777; + --accent: #10b981; + --text: #e4e4e7; + --text-muted: #a1a1aa; + --border: #27272a; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +body { + margin: 0; + padding: 0; + font-family: 'Roboto', sans-serif; + background-color: var(--bg-color); + color: var(--text); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; +} + +.container { + width: 100%; + max-width: 500px; + padding: 20px; + text-align: center; +} + +h1 { + font-family: 'Orbitron', sans-serif; + font-size: 3rem; + margin-bottom: 20px; + background: linear-gradient(to right, var(--primary), var(--secondary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 0 30px rgba(37, 99, 235, 0.5); +} + +.status-bar { + margin-bottom: 20px; + padding: 10px; + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 8px; + font-size: 0.9rem; + color: var(--text-muted); +} + +.panel { + background: var(--card-bg); + padding: 30px; + border-radius: 12px; + border: 1px solid var(--border); + box-shadow: var(--shadow); +} + +.hidden { + display: none !important; +} + +.btn { + padding: 12px 24px; + border: none; + border-radius: 6px; + font-family: 'Orbitron', sans-serif; + font-weight: 700; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + width: 100%; + margin: 5px 0; +} + +.btn:active { + transform: scale(0.98); +} + +.primary { + background: var(--primary); + color: white; + box-shadow: 0 0 15px rgba(37, 99, 235, 0.4); +} + +.secondary { + background: var(--card-bg); /* Use card background for join button */ + color: white; /* Ensure text is visible */ + border: 1px solid var(--border); /* Add a border to distinguish it */ +} + +/* Add hover effect for secondary button too */ +.secondary:hover { + background: #27272a; /* Slightly lighter on hover */ +} + +.danger { + background: #ef4444; + color: white; + margin-top: 20px; +} + +input[type="text"] { + width: 100%; + box-sizing: border-box; /* This ensures padding is included in width */ + padding: 12px; + margin: 10px 0; + background: #27272a; + border: 1px solid var(--border); + border-radius: 6px; + color: white; + font-family: 'Orbitron', sans-serif; + text-align: center; + font-size: 1.2rem; + letter-spacing: 2px; +} + +input[type="text"]:focus { + outline: 2px solid var(--primary); +} + +.divider { + color: var(--text-muted); + margin: 15px 0; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Game Interface */ +.game-info { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + background: #27272a; + padding: 10px; + border-radius: 8px; +} + +.info-item { + display: flex; + flex-direction: column; +} + +.info-item .label { + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; +} + +.info-item .value { + font-family: 'Orbitron', sans-serif; + font-size: 1.1rem; + color: white; +} + +.board { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + aspect-ratio: 1; + margin: 0 auto; + max-width: 350px; +} + +.cell { + background: #27272a; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 3rem; + font-family: 'Orbitron', sans-serif; + cursor: pointer; + transition: background 0.2s; + user-select: none; +} + +.cell:hover { + background: #3f3f46; +} + +.cell.x { + color: var(--secondary); /* Pink for X */ + text-shadow: 0 0 10px rgba(219, 39, 119, 0.6); +} + +.cell.o { + color: var(--accent); /* Green for O */ + text-shadow: 0 0 10px rgba(16, 185, 129, 0.6); +} diff --git a/web-client/src/main/resources/static/js/app.js b/web-client/src/main/resources/static/js/app.js new file mode 100644 index 0000000..e6c6033 --- /dev/null +++ b/web-client/src/main/resources/static/js/app.js @@ -0,0 +1,155 @@ +const menuPanel = document.getElementById('menu'); +const gamePanel = document.getElementById('game'); +const statusDiv = document.getElementById('connection-status'); +const createBtn = document.getElementById('create-btn'); +const joinBtn = document.getElementById('join-btn'); +const joinInput = document.getElementById('join-code'); +const displayCode = document.getElementById('display-code'); +const turnStatus = document.getElementById('turn-status'); +const surrenderBtn = document.getElementById('surrender-btn'); +const cells = document.querySelectorAll('.cell'); + +let mySymbol = ''; +let ws = null; +let currentCode = ''; + +function connect() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${protocol}//${window.location.host}/game`); + + ws.onopen = () => { + statusDiv.textContent = 'Connected to Server'; + statusDiv.style.color = '#10b981'; + }; + + ws.onclose = () => { + statusDiv.textContent = 'Disconnected. Reconnecting...'; + statusDiv.style.color = '#ef4444'; + setTimeout(connect, 2000); + }; + + ws.onmessage = (event) => { + handleMessage(event.data); + }; +} + +function send(msg) { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(msg); + } +} + +createBtn.addEventListener('click', () => { + send('CREATE'); +}); + +joinBtn.addEventListener('click', () => { + const code = joinInput.value.trim(); + if (code) send(`JOIN ${code}`); +}); + +surrenderBtn.addEventListener('click', () => { + if (confirm('Are you sure you want to surrender?')) { + send('SURRENDER'); + } +}); + +cells.forEach(cell => { + cell.addEventListener('click', () => { + const r = cell.dataset.r; + const c = cell.dataset.c; + send(`MOVE ${r} ${c}`); + }); +}); + +function handleMessage(msg) { + console.log("Received:", msg); + const parts = msg.split(' '); + const cmd = parts[0]; + + switch (cmd) { + case 'GAME_CREATED': + currentCode = parts[1]; + mySymbol = 'X'; + enterGame(); + updateStatus("Waiting for opponent..."); + break; + case 'JOIN_SUCCESS': + currentCode = joinInput.value; + mySymbol = 'O'; + enterGame(); + updateStatus("Connected! Game starting soon..."); + break; + case 'START': + updateStatus("Game Started! X goes first."); + break; + case 'BOARD': + const boardStr = msg.substring(6); + updateBoard(boardStr); + break; + case 'TURN': + const turn = parts[1]; + if (turn === mySymbol) { + updateStatus(`Your Turn (${mySymbol})`, true); + } else { + updateStatus(`Opponent's Turn (${turn})`, false); + } + break; + case 'WIN': + const winner = msg.substring(4); + alert(`Game Over! Winner: ${winner}`); + resetGame(); + break; + case 'DRAW': + alert("Game Over! It's a draw!"); + resetGame(); + break; + case 'ERROR:': + alert(msg); + break; + } +} + +function enterGame() { + menuPanel.classList.add('hidden'); + gamePanel.classList.remove('hidden'); + displayCode.textContent = currentCode; + // Clear board + cells.forEach(c => { + c.textContent = ''; + c.className = 'cell'; + }); +} + +function resetGame() { + menuPanel.classList.remove('hidden'); + gamePanel.classList.add('hidden'); + currentCode = ''; + mySymbol = ''; + joinInput.value = ''; + statusDiv.textContent = 'Connected to Server'; + statusDiv.style.color = '#10b981'; +} + +function updateBoard(boardStr) { + cells.forEach((cell, i) => { + if (i < boardStr.length) { + const char = boardStr.charAt(i); + cell.textContent = char === ' ' ? '' : char; + cell.className = 'cell'; + if (char === 'X') cell.classList.add('x'); + if (char === 'O') cell.classList.add('o'); + } + }); +} + +function updateStatus(text, isAction = false) { + turnStatus.textContent = text; + if (isAction) { + turnStatus.style.color = '#10b981'; // Green for 'your turn' + } else { + turnStatus.style.color = '#a1a1aa'; + } +} + +connect(); diff --git a/web-client/src/main/resources/templates/index.html b/web-client/src/main/resources/templates/index.html new file mode 100644 index 0000000..d29f35e --- /dev/null +++ b/web-client/src/main/resources/templates/index.html @@ -0,0 +1,61 @@ + + + + + + TicTacToe + + + + +
+

TicTacToe

+ +
Connecting to server...
+ + + + + + +
+ + + +