This commit is contained in:
Hymmel 2026-02-10 13:30:36 +01:00
parent 82b9cfe780
commit 8dc716b4bf
31 changed files with 527 additions and 27 deletions

View file

@ -4,8 +4,8 @@ This project contains a TCP Game Server, a Java Swing Client, and a Spring Boot
## Structure
- `server/`: Java TCP Server (Port 1870). With Anticheat (Server-side validation).
- `client/`: Standalone Java Swing Client. Connects to `dokploy.lona-development.org:1870`.
- `server/`: Java TCP Server (Port 1870). Includes Anticheat logic (validates turns, moves, wins).
- `client/`: Standalone Java Swing Client. Defaults to `dokploy.lona-development.org:1870` but allows custom host/port.
- `web-client/`: Spring Boot Web Application. Acts as a proxy to the TCP Server via WebSocket.
## Running with Docker Compose
@ -16,28 +16,44 @@ To start the Server and Web Client:
docker-compose up --build
```
- **Server** runs on port `1870`.
- **Server** runs on port `1870` (exposed).
- **Web Client** runs on `http://localhost:8080`.
## Building the Java Client
## Building and Running the Standalone Java Client
To build the standalone Java client:
Go to the `client` directory and run the build script:
```bash
./build_client.sh
cd client
chmod +x build.sh
./build.sh
```
To run it:
### Usage
By default, the client attempts to connect to `dokploy.lona-development.org:1870`.
```bash
java -jar client/target/client-1.0-SNAPSHOT.jar
java -jar 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).
**Custom Host/Port (Local Testing):**
If you want to connect to a local server (e.g., running via Docker Compose), pass the host and port as arguments:
## Game Protocol
```bash
java -jar target/client-1.0-SNAPSHOT.jar localhost 1870
```
- **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.
## Game Protocol (TCP 1870)
The server creates game sessions identified by a unique 6-character code.
- **CREATE**: Starts a new game. Server returns `GAME_CREATED <CODE>`.
- **JOIN &lt;CODE&gt;**: Joins an existing game. Server returns `JOIN_SUCCESS` or `ERROR`.
- **MOVE &lt;ROW&gt; &lt;COL&gt;**: Places your symbol (0-2). Validated by server.
- **SURRENDER**: Forfeits the game immediately.
## Troubleshooting
- **Server Connection Refused?** Ensure the server container is running and port 1870 is mapped.
- **Client stuck connecting?** Verify the hostname is reachable. Use `localhost` if running locally.

View file

@ -5,8 +5,11 @@ mvn clean package
if [ $? -eq 0 ]; then
echo "--------------------------------------------------"
echo "Build Successful!"
echo "You can run the client with:"
echo "You can run the client with default settings (dokploy.lona-development.org):"
echo "java -jar target/client-1.0-SNAPSHOT.jar"
echo ""
echo "Or specify a custom host and port (e.g. for local testing):"
echo "java -jar target/client-1.0-SNAPSHOT.jar localhost 1870"
echo "--------------------------------------------------"
else
echo "Build Failed!"

View file

@ -23,16 +23,58 @@ public class Main extends JFrame {
private boolean myTurn = false;
private char mySymbol = ' '; // assigned by server implied turn
public Main() {
public static void main(String[] args) {
String host = "dokploy.lona-development.org";
int port = 1870;
if (args.length > 0) {
host = args[0];
}
if (args.length > 1) {
try {
port = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
System.err.println("Invalid port number provided, using default 1870");
}
}
final String serverHost = host;
final int serverPort = port;
System.out.println("Starting TicTacToe Client...");
System.out.println("Target Server: " + serverHost + ":" + serverPort);
SwingUtilities.invokeLater(() -> {
try {
Main client = new Main(serverHost, serverPort);
client.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
System.err.println("Failed to start GUI: " + e.getMessage());
}
});
}
// Instance variables
private final String serverHost;
private final int serverPort;
public Main(String host, int port) {
super("TicTacToe Client");
this.serverHost = host;
this.serverPort = port;
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(400, 500);
setLocationRelativeTo(null);
initUI();
setContentPane(mainPanel);
connectToServer();
try {
initUI();
setContentPane(mainPanel);
connectToServer();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initUI() {
@ -95,21 +137,24 @@ public class Main extends JFrame {
private void connectToServer() {
new Thread(() -> {
try {
// Using the specific domain provided
socket = new Socket("dokploy.lona-development.org", 1870);
System.out.println("Attempting to connect to " + serverHost + ":" + serverPort);
socket = new Socket(serverHost, serverPort);
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("Connected successfully!");
SwingUtilities.invokeLater(() -> statusLabel.setText("Connected to Server."));
String line;
while ((line = in.readLine()) != null) {
System.out.println("Received: " + line); // Log received messages
processMessage(line);
}
} catch (IOException e) {
System.err.println("Connection Error: " + e.getMessage());
SwingUtilities.invokeLater(() -> {
statusLabel.setText("Connection Failed.");
JOptionPane.showMessageDialog(this, "Connection Error: " + e.getMessage());
statusLabel.setText("Disconnected.");
});
}
}).start();
@ -189,8 +234,4 @@ public class Main extends JFrame {
statusLabel.setText("Connected.");
codeLabel.setText("Code: -");
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new Main().setVisible(true));
}
}

Binary file not shown.

View file

@ -0,0 +1,5 @@
#Generated by Maven
#Tue Feb 10 13:27:55 CET 2026
artifactId=client
groupId=com.lona.tictactoe
version=1.0-SNAPSHOT

View file

@ -0,0 +1 @@
com/lona/tictactoe/client/Main.class

View file

@ -0,0 +1 @@
/home/collin/tictactoe/client/src/main/java/com/lona/tictactoe/client/Main.java

Binary file not shown.

View file

@ -0,0 +1,5 @@
#Generated by Maven
#Tue Feb 10 13:26:46 CET 2026
artifactId=server
groupId=com.lona.tictactoe
version=1.0-SNAPSHOT

View file

@ -0,0 +1,4 @@
com/lona/tictactoe/server/ClientHandler.class
com/lona/tictactoe/server/GameServer.class
com/lona/tictactoe/server/GameInstance.class
com/lona/tictactoe/server/GameManager.class

View file

@ -0,0 +1,4 @@
/home/collin/tictactoe/server/src/main/java/com/lona/tictactoe/server/GameInstance.java
/home/collin/tictactoe/server/src/main/java/com/lona/tictactoe/server/ClientHandler.java
/home/collin/tictactoe/server/src/main/java/com/lona/tictactoe/server/GameServer.java
/home/collin/tictactoe/server/src/main/java/com/lona/tictactoe/server/GameManager.java

Binary file not shown.

Binary file not shown.

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>

View file

@ -0,0 +1,3 @@
artifactId=web-client
groupId=com.lona.tictactoe
version=1.0-SNAPSHOT

View file

@ -0,0 +1,5 @@
com/lona/tictactoe/web/TcpSession.class
com/lona/tictactoe/web/WebSocketConfig.class
com/lona/tictactoe/web/Application.class
com/lona/tictactoe/web/GameWebSocketHandler.class
com/lona/tictactoe/web/WebController.class

View file

@ -0,0 +1,5 @@
/home/collin/tictactoe/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java
/home/collin/tictactoe/web-client/src/main/java/com/lona/tictactoe/web/WebSocketConfig.java
/home/collin/tictactoe/web-client/src/main/java/com/lona/tictactoe/web/GameWebSocketHandler.java
/home/collin/tictactoe/web-client/src/main/java/com/lona/tictactoe/web/Application.java
/home/collin/tictactoe/web-client/src/main/java/com/lona/tictactoe/web/WebController.java

Binary file not shown.

Binary file not shown.