a
This commit is contained in:
commit
674b3efc3a
26 changed files with 1300 additions and 0 deletions
43
README.md
Normal file
43
README.md
Normal file
|
|
@ -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.
|
||||||
5
build_client.sh
Normal file
5
build_client.sh
Normal file
|
|
@ -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"
|
||||||
14
client/build.sh
Executable file
14
client/build.sh
Executable file
|
|
@ -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
|
||||||
35
client/pom.xml
Normal file
35
client/pom.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.lona.tictactoe</groupId>
|
||||||
|
<artifactId>client</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>com.lona.tictactoe.client.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
196
client/src/main/java/com/lona/tictactoe/client/Main.java
Normal file
196
client/src/main/java/com/lona/tictactoe/client/Main.java
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
|
|
@ -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
|
||||||
11
server/Dockerfile
Normal file
11
server/Dockerfile
Normal file
|
|
@ -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"]
|
||||||
37
server/pom.xml
Normal file
37
server/pom.xml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.lona.tictactoe</groupId>
|
||||||
|
<artifactId>server</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>com.lona.tictactoe.server.GameServer</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
server/src/main/java/com/lona/tictactoe/server/GameInstance.java
Normal file
138
server/src/main/java/com/lona/tictactoe/server/GameInstance.java
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, GameInstance> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
server/target/classes/com/lona/tictactoe/server/GameServer.class
Normal file
BIN
server/target/classes/com/lona/tictactoe/server/GameServer.class
Normal file
Binary file not shown.
13
web-client/Dockerfile
Normal file
13
web-client/Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
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/web-client-1.0-SNAPSHOT.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
ENV GAME_SERVER_HOST=server
|
||||||
|
ENV GAME_SERVER_PORT=1870
|
||||||
|
CMD ["java", "-jar", "app.jar"]
|
||||||
35
web-client/pom.xml
Normal file
35
web-client/pom.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.lona.tictactoe</groupId>
|
||||||
|
<artifactId>web-client</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.2</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<WebSocketSession, TcpSession> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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("*");
|
||||||
|
}
|
||||||
|
}
|
||||||
191
web-client/src/main/resources/static/css/style.css
Normal file
191
web-client/src/main/resources/static/css/style.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
155
web-client/src/main/resources/static/js/app.js
Normal file
155
web-client/src/main/resources/static/js/app.js
Normal file
|
|
@ -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();
|
||||||
61
web-client/src/main/resources/templates/index.html
Normal file
61
web-client/src/main/resources/templates/index.html
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TicTacToe</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;400&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>TicTacToe</h1>
|
||||||
|
|
||||||
|
<div id="connection-status" class="status-bar">Connecting to server...</div>
|
||||||
|
|
||||||
|
<!-- Menu Section -->
|
||||||
|
<div id="menu" class="panel">
|
||||||
|
<h2>Start Game</h2>
|
||||||
|
<div class="menu-controls">
|
||||||
|
<button id="create-btn" class="btn primary">Create New Game</button>
|
||||||
|
<div class="divider">OR</div>
|
||||||
|
<div class="join-group">
|
||||||
|
<input type="text" id="join-code" placeholder="Enter Game Code" maxlength="6">
|
||||||
|
<button id="join-btn" class="btn secondary">Join Game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Section -->
|
||||||
|
<div id="game" class="panel hidden">
|
||||||
|
<div class="game-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Game Code:</span>
|
||||||
|
<span id="display-code" class="value">??????</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Status:</span>
|
||||||
|
<span id="turn-status" class="value">Waiting...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="board" id="board">
|
||||||
|
<!-- Cells generated by JS -->
|
||||||
|
<div class="cell" data-r="0" data-c="0"></div>
|
||||||
|
<div class="cell" data-r="0" data-c="1"></div>
|
||||||
|
<div class="cell" data-r="0" data-c="2"></div>
|
||||||
|
<div class="cell" data-r="1" data-c="0"></div>
|
||||||
|
<div class="cell" data-r="1" data-c="1"></div>
|
||||||
|
<div class="cell" data-r="1" data-c="2"></div>
|
||||||
|
<div class="cell" data-r="2" data-c="0"></div>
|
||||||
|
<div class="cell" data-r="2" data-c="1"></div>
|
||||||
|
<div class="cell" data-r="2" data-c="2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="surrender-btn" class="btn danger">Surrender / Give Up</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Reference in a new issue