This commit is contained in:
Hymmel 2026-02-10 13:17:43 +01:00
commit 674b3efc3a
26 changed files with 1300 additions and 0 deletions

43
README.md Normal file
View 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
View 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
View 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
View 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>

View 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
View 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
View 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
View 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>

View file

@ -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);
}
}
}

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

13
web-client/Dockerfile Normal file
View 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
View 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>

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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
}
}
}

View file

@ -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";
}
}

View file

@ -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("*");
}
}

View 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);
}

View 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();

View 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>