gay
This commit is contained in:
parent
8dc716b4bf
commit
0937fc35de
21 changed files with 238 additions and 146 deletions
|
|
@ -52,6 +52,13 @@ The server creates game sessions identified by a unique 6-character code.
|
||||||
- **JOIN <CODE>**: Joins an existing game. Server returns `JOIN_SUCCESS` or `ERROR`.
|
- **JOIN <CODE>**: Joins an existing game. Server returns `JOIN_SUCCESS` or `ERROR`.
|
||||||
- **MOVE <ROW> <COL>**: Places your symbol (0-2). Validated by server.
|
- **MOVE <ROW> <COL>**: Places your symbol (0-2). Validated by server.
|
||||||
- **SURRENDER**: Forfeits the game immediately.
|
- **SURRENDER**: Forfeits the game immediately.
|
||||||
|
- **LEAVE**: Leaves the current lobby. If empty, the lobby acts as closed.
|
||||||
|
- **RESTART**: Resets the board for the current lobby so players can play again.
|
||||||
|
|
||||||
|
**Server Messages:**
|
||||||
|
- `WIN <SYMBOL>` or `DRAW`
|
||||||
|
- `OPPONENT_LEFT`: Sent when the other player leaves via the LEAVE command.
|
||||||
|
- `RESTART`: Sent when the game is restarted.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public class Main extends JFrame {
|
||||||
private char mySymbol = ' '; // assigned by server implied turn
|
private char mySymbol = ' '; // assigned by server implied turn
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
String host = "dokploy.lona-development.org";
|
String host = "38.242.130.81";
|
||||||
int port = 1870;
|
int port = 1870;
|
||||||
|
|
||||||
if (args.length > 0) {
|
if (args.length > 0) {
|
||||||
|
|
@ -126,9 +126,26 @@ public class Main extends JFrame {
|
||||||
}
|
}
|
||||||
gamePanel.add(boardPanel, BorderLayout.CENTER);
|
gamePanel.add(boardPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
JButton surrenderBtn = new JButton("Surrender / Give Up");
|
// --- BUTTONS PANEL ---
|
||||||
|
JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 10, 0));
|
||||||
|
|
||||||
|
JButton leaveBtn = new JButton("Leave Lobby");
|
||||||
|
leaveBtn.setBackground(Color.GRAY);
|
||||||
|
leaveBtn.setForeground(Color.WHITE);
|
||||||
|
leaveBtn.addActionListener(e -> {
|
||||||
|
send("LEAVE");
|
||||||
|
resetGame();
|
||||||
|
});
|
||||||
|
|
||||||
|
JButton surrenderBtn = new JButton("Surrender");
|
||||||
|
surrenderBtn.setBackground(new Color(220, 53, 69)); // Bootstrap Danger Red
|
||||||
|
surrenderBtn.setForeground(Color.WHITE);
|
||||||
surrenderBtn.addActionListener(e -> send("SURRENDER"));
|
surrenderBtn.addActionListener(e -> send("SURRENDER"));
|
||||||
gamePanel.add(surrenderBtn, BorderLayout.SOUTH);
|
|
||||||
|
buttonPanel.add(leaveBtn);
|
||||||
|
buttonPanel.add(surrenderBtn);
|
||||||
|
|
||||||
|
gamePanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
mainPanel.add(menuPanel, "MENU");
|
mainPanel.add(menuPanel, "MENU");
|
||||||
mainPanel.add(gamePanel, "GAME");
|
mainPanel.add(gamePanel, "GAME");
|
||||||
|
|
@ -155,6 +172,7 @@ public class Main extends JFrame {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
statusLabel.setText("Connection Failed.");
|
statusLabel.setText("Connection Failed.");
|
||||||
JOptionPane.showMessageDialog(this, "Connection Error: " + e.getMessage());
|
JOptionPane.showMessageDialog(this, "Connection Error: " + e.getMessage());
|
||||||
|
// Don't reset to menu if we can't connect, let user see error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
|
|
@ -191,6 +209,12 @@ public class Main extends JFrame {
|
||||||
case "START":
|
case "START":
|
||||||
statusLabel.setText("Game Started! X goes first.");
|
statusLabel.setText("Game Started! X goes first.");
|
||||||
break;
|
break;
|
||||||
|
case "RESTART":
|
||||||
|
// clear board
|
||||||
|
for (JButton btn : buttons)
|
||||||
|
btn.setText("");
|
||||||
|
statusLabel.setText("Game Restarted! X goes first.");
|
||||||
|
break;
|
||||||
case "BOARD": // BOARD X O X ...
|
case "BOARD": // BOARD X O X ...
|
||||||
String boardStr = msg.substring(6); // remove "BOARD "
|
String boardStr = msg.substring(6); // remove "BOARD "
|
||||||
for (int i = 0; i < 9 && i < boardStr.length(); i++) {
|
for (int i = 0; i < 9 && i < boardStr.length(); i++) {
|
||||||
|
|
@ -210,11 +234,29 @@ public class Main extends JFrame {
|
||||||
break;
|
break;
|
||||||
case "WIN":
|
case "WIN":
|
||||||
String winner = msg.substring(4);
|
String winner = msg.substring(4);
|
||||||
JOptionPane.showMessageDialog(this, "Winner: " + winner);
|
statusLabel.setText("Winner: " + winner);
|
||||||
resetGame();
|
int choice = JOptionPane.showConfirmDialog(this,
|
||||||
|
"Winner: " + winner + "\nPlay Again?",
|
||||||
|
"Game Over",
|
||||||
|
JOptionPane.YES_NO_OPTION);
|
||||||
|
|
||||||
|
if (choice == JOptionPane.YES_OPTION) {
|
||||||
|
send("RESTART");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "DRAW":
|
case "DRAW":
|
||||||
JOptionPane.showMessageDialog(this, "Draw!");
|
statusLabel.setText("Draw!");
|
||||||
|
int drawChoice = JOptionPane.showConfirmDialog(this,
|
||||||
|
"Draw!\nPlay Again?",
|
||||||
|
"Game Over",
|
||||||
|
JOptionPane.YES_NO_OPTION);
|
||||||
|
|
||||||
|
if (drawChoice == JOptionPane.YES_OPTION) {
|
||||||
|
send("RESTART");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "OPPONENT_LEFT":
|
||||||
|
JOptionPane.showMessageDialog(this, "Opponent left the game.");
|
||||||
resetGame();
|
resetGame();
|
||||||
break;
|
break;
|
||||||
case "ERROR:":
|
case "ERROR:":
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
#Generated by Maven
|
#Generated by Maven
|
||||||
#Tue Feb 10 13:27:55 CET 2026
|
#Tue Feb 10 14:07:26 CET 2026
|
||||||
artifactId=client
|
artifactId=client
|
||||||
groupId=com.lona.tictactoe
|
groupId=com.lona.tictactoe
|
||||||
version=1.0-SNAPSHOT
|
version=1.0-SNAPSHOT
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -85,6 +85,27 @@ public class ClientHandler implements Runnable {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "RESTART":
|
||||||
|
if (gameCode != null) {
|
||||||
|
GameInstance active = GameManager.getGame(gameCode);
|
||||||
|
if (active != null)
|
||||||
|
active.restart();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "LEAVE":
|
||||||
|
if (gameCode != null) {
|
||||||
|
GameInstance active = GameManager.getGame(gameCode);
|
||||||
|
if (active != null) {
|
||||||
|
active.leave(this);
|
||||||
|
if (active.isEmpty()) {
|
||||||
|
GameManager.removeGame(gameCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.gameCode = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
out.println("ERROR: Unknown command");
|
out.println("ERROR: Unknown command");
|
||||||
}
|
}
|
||||||
|
|
@ -94,9 +115,12 @@ public class ClientHandler implements Runnable {
|
||||||
} finally {
|
} finally {
|
||||||
if (gameCode != null) {
|
if (gameCode != null) {
|
||||||
GameInstance g = GameManager.getGame(gameCode);
|
GameInstance g = GameManager.getGame(gameCode);
|
||||||
if (g != null)
|
if (g != null) {
|
||||||
g.disconnect(this);
|
g.disconnect(this);
|
||||||
GameManager.removeGame(gameCode);
|
if (g.isEmpty()) {
|
||||||
|
GameManager.removeGame(gameCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
client.close();
|
client.close();
|
||||||
|
|
|
||||||
|
|
@ -35,36 +35,30 @@ public class GameInstance {
|
||||||
char symbol = (player == playerX) ? 'X' : 'O';
|
char symbol = (player == playerX) ? 'X' : 'O';
|
||||||
|
|
||||||
// --- ANTICHEAT / VALIDATION ---
|
// --- ANTICHEAT / VALIDATION ---
|
||||||
// 1. Check if correct turn
|
|
||||||
if (symbol != currentTurn) {
|
if (symbol != currentTurn) {
|
||||||
player.sendMessage("ERROR: It is not your turn!");
|
player.sendMessage("ERROR: It is not your turn!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check bounds
|
|
||||||
if (x < 0 || x > 2 || y < 0 || y > 2) {
|
if (x < 0 || x > 2 || y < 0 || y > 2) {
|
||||||
player.sendMessage("ERROR: Invalid coordinates!");
|
player.sendMessage("ERROR: Invalid coordinates!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check if cell is empty
|
|
||||||
if (board[x][y] != ' ') {
|
if (board[x][y] != ' ') {
|
||||||
player.sendMessage("ERROR: Cell occupied!");
|
player.sendMessage("ERROR: Cell occupied!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply move
|
|
||||||
board[x][y] = symbol;
|
board[x][y] = symbol;
|
||||||
sendBoard();
|
sendBoard();
|
||||||
|
|
||||||
if (checkWin(symbol)) {
|
if (checkWin(symbol)) {
|
||||||
finished = true;
|
finished = true;
|
||||||
broadcast("WIN " + symbol);
|
broadcast("WIN " + symbol);
|
||||||
createEndState();
|
|
||||||
} else if (checkDraw()) {
|
} else if (checkDraw()) {
|
||||||
finished = true;
|
finished = true;
|
||||||
broadcast("DRAW");
|
broadcast("DRAW");
|
||||||
createEndState();
|
|
||||||
} else {
|
} else {
|
||||||
currentTurn = (currentTurn == 'X') ? 'O' : 'X';
|
currentTurn = (currentTurn == 'X') ? 'O' : 'X';
|
||||||
broadcast("TURN " + currentTurn);
|
broadcast("TURN " + currentTurn);
|
||||||
|
|
@ -78,19 +72,42 @@ public class GameInstance {
|
||||||
char loser = (player == playerX) ? 'X' : 'O';
|
char loser = (player == playerX) ? 'X' : 'O';
|
||||||
char winner = (loser == 'X') ? 'O' : 'X';
|
char winner = (loser == 'X') ? 'O' : 'X';
|
||||||
broadcast("WIN " + winner + " (Surrender)");
|
broadcast("WIN " + winner + " (Surrender)");
|
||||||
createEndState();
|
}
|
||||||
|
|
||||||
|
public synchronized void restart() {
|
||||||
|
for (char[] row : board)
|
||||||
|
Arrays.fill(row, ' ');
|
||||||
|
currentTurn = 'X';
|
||||||
|
finished = false;
|
||||||
|
broadcast("RESTART");
|
||||||
|
sendBoard();
|
||||||
|
broadcast("TURN " + currentTurn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void leave(ClientHandler player) {
|
||||||
|
if (player == playerX) {
|
||||||
|
// Creator left, if playerO exists, tell them?
|
||||||
|
if (playerO != null) {
|
||||||
|
playerO.sendMessage("OPPONENT_LEFT");
|
||||||
|
}
|
||||||
|
playerX = null;
|
||||||
|
} else if (player == playerO) {
|
||||||
|
if (playerX != null) {
|
||||||
|
playerX.sendMessage("OPPONENT_LEFT");
|
||||||
|
}
|
||||||
|
playerO = null;
|
||||||
|
}
|
||||||
|
// If empty, GameManager will eventually remove if we allow it,
|
||||||
|
// but for now let's rely on GameManager to check if empty logic or just keep it
|
||||||
|
// simple.
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isEmpty() {
|
||||||
|
return playerX == null && playerO == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void disconnect(ClientHandler player) {
|
public synchronized void disconnect(ClientHandler player) {
|
||||||
if (finished)
|
leave(player);
|
||||||
return;
|
|
||||||
finished = true;
|
|
||||||
broadcast("ERROR: Opponent disconnected");
|
|
||||||
createEndState();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createEndState() {
|
|
||||||
// cleanup if needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkWin(char s) {
|
private boolean checkWin(char s) {
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
#Generated by Maven
|
#Generated by Maven
|
||||||
#Tue Feb 10 13:26:46 CET 2026
|
#Tue Feb 10 14:07:14 CET 2026
|
||||||
artifactId=server
|
artifactId=server
|
||||||
groupId=com.lona.tictactoe
|
groupId=com.lona.tictactoe
|
||||||
version=1.0-SNAPSHOT
|
version=1.0-SNAPSHOT
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -84,14 +84,18 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
background: var(--card-bg); /* Use card background for join button */
|
background: var(--card-bg);
|
||||||
color: white; /* Ensure text is visible */
|
/* Use card background for join button */
|
||||||
border: 1px solid var(--border); /* Add a border to distinguish it */
|
color: white;
|
||||||
|
/* Ensure text is visible */
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
/* Add a border to distinguish it */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add hover effect for secondary button too */
|
/* Add hover effect for secondary button too */
|
||||||
.secondary:hover {
|
.secondary:hover {
|
||||||
background: #27272a; /* Slightly lighter on hover */
|
background: #27272a;
|
||||||
|
/* Slightly lighter on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger {
|
.danger {
|
||||||
|
|
@ -102,7 +106,8 @@ h1 {
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box; /* This ensures padding is included in width */
|
box-sizing: border-box;
|
||||||
|
/* This ensures padding is included in width */
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
background: #27272a;
|
background: #27272a;
|
||||||
|
|
@ -180,12 +185,20 @@ input[type="text"]:focus {
|
||||||
background: #3f3f46;
|
background: #3f3f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.cell.x {
|
.cell.x {
|
||||||
color: var(--secondary); /* Pink for X */
|
color: var(--secondary);
|
||||||
|
/* Pink for X */
|
||||||
text-shadow: 0 0 10px rgba(219, 39, 119, 0.6);
|
text-shadow: 0 0 10px rgba(219, 39, 119, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell.o {
|
.cell.o {
|
||||||
color: var(--accent); /* Green for O */
|
color: var(--accent);
|
||||||
|
/* Green for O */
|
||||||
text-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
|
text-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
|
||||||
}
|
}
|
||||||
|
|
@ -6,46 +6,17 @@ const joinBtn = document.getElementById('join-btn');
|
||||||
const joinInput = document.getElementById('join-code');
|
const joinInput = document.getElementById('join-code');
|
||||||
const displayCode = document.getElementById('display-code');
|
const displayCode = document.getElementById('display-code');
|
||||||
const turnStatus = document.getElementById('turn-status');
|
const turnStatus = document.getElementById('turn-status');
|
||||||
const surrenderBtn = document.getElementById('surrender-btn');
|
const leaveBtn = document.getElementById('leave-btn');
|
||||||
const cells = document.querySelectorAll('.cell');
|
|
||||||
|
|
||||||
let mySymbol = '';
|
// ... (other vars)
|
||||||
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 = () => {
|
leaveBtn.addEventListener('click', () => {
|
||||||
statusDiv.textContent = 'Connected to Server';
|
if (confirm('Are you sure you want to leave the lobby?')) {
|
||||||
statusDiv.style.color = '#10b981';
|
send('LEAVE');
|
||||||
};
|
resetGame();
|
||||||
|
|
||||||
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', () => {
|
surrenderBtn.addEventListener('click', () => {
|
||||||
|
|
@ -54,13 +25,7 @@ surrenderBtn.addEventListener('click', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cells.forEach(cell => {
|
// ...
|
||||||
cell.addEventListener('click', () => {
|
|
||||||
const r = cell.dataset.r;
|
|
||||||
const c = cell.dataset.c;
|
|
||||||
send(`MOVE ${r} ${c}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleMessage(msg) {
|
function handleMessage(msg) {
|
||||||
console.log("Received:", msg);
|
console.log("Received:", msg);
|
||||||
|
|
@ -83,6 +48,13 @@ function handleMessage(msg) {
|
||||||
case 'START':
|
case 'START':
|
||||||
updateStatus("Game Started! X goes first.");
|
updateStatus("Game Started! X goes first.");
|
||||||
break;
|
break;
|
||||||
|
case 'RESTART':
|
||||||
|
updateStatus("Game Restarted! X goes first.");
|
||||||
|
cells.forEach(c => {
|
||||||
|
c.textContent = '';
|
||||||
|
c.className = 'cell';
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'BOARD':
|
case 'BOARD':
|
||||||
const boardStr = msg.substring(6);
|
const boardStr = msg.substring(6);
|
||||||
updateBoard(boardStr);
|
updateBoard(boardStr);
|
||||||
|
|
@ -97,11 +69,21 @@ function handleMessage(msg) {
|
||||||
break;
|
break;
|
||||||
case 'WIN':
|
case 'WIN':
|
||||||
const winner = msg.substring(4);
|
const winner = msg.substring(4);
|
||||||
alert(`Game Over! Winner: ${winner}`);
|
setTimeout(() => {
|
||||||
resetGame();
|
if (confirm(`Game Over! Winner: ${winner}\nDo you want to play again?`)) {
|
||||||
|
send('RESTART');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
break;
|
break;
|
||||||
case 'DRAW':
|
case 'DRAW':
|
||||||
alert("Game Over! It's a draw!");
|
setTimeout(() => {
|
||||||
|
if (confirm("Game Over! It's a draw!\nDo you want to play again?")) {
|
||||||
|
send('RESTART');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
break;
|
||||||
|
case 'OPPONENT_LEFT':
|
||||||
|
alert("Opponent has left the lobby.");
|
||||||
resetGame();
|
resetGame();
|
||||||
break;
|
break;
|
||||||
case 'ERROR:':
|
case 'ERROR:':
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>TicTacToe</title>
|
<title>TicTacToe</title>
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<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">
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;400&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>TicTacToe</h1>
|
<h1>TicTacToe</h1>
|
||||||
|
|
@ -26,7 +29,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Section -->
|
|
||||||
<div id="game" class="panel hidden">
|
<div id="game" class="panel hidden">
|
||||||
<div class="game-info">
|
<div class="game-info">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
|
|
@ -52,10 +54,14 @@
|
||||||
<div class="cell" data-r="2" data-c="2"></div>
|
<div class="cell" data-r="2" data-c="2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="surrender-btn" class="btn danger">Surrender / Give Up</button>
|
<div class="game-actions">
|
||||||
|
<button id="leave-btn" class="btn secondary">Leave Lobby</button>
|
||||||
|
<button id="surrender-btn" class="btn danger">Surrender</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -84,14 +84,18 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
background: var(--card-bg); /* Use card background for join button */
|
background: var(--card-bg);
|
||||||
color: white; /* Ensure text is visible */
|
/* Use card background for join button */
|
||||||
border: 1px solid var(--border); /* Add a border to distinguish it */
|
color: white;
|
||||||
|
/* Ensure text is visible */
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
/* Add a border to distinguish it */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add hover effect for secondary button too */
|
/* Add hover effect for secondary button too */
|
||||||
.secondary:hover {
|
.secondary:hover {
|
||||||
background: #27272a; /* Slightly lighter on hover */
|
background: #27272a;
|
||||||
|
/* Slightly lighter on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger {
|
.danger {
|
||||||
|
|
@ -102,7 +106,8 @@ h1 {
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box; /* This ensures padding is included in width */
|
box-sizing: border-box;
|
||||||
|
/* This ensures padding is included in width */
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
background: #27272a;
|
background: #27272a;
|
||||||
|
|
@ -180,12 +185,20 @@ input[type="text"]:focus {
|
||||||
background: #3f3f46;
|
background: #3f3f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.cell.x {
|
.cell.x {
|
||||||
color: var(--secondary); /* Pink for X */
|
color: var(--secondary);
|
||||||
|
/* Pink for X */
|
||||||
text-shadow: 0 0 10px rgba(219, 39, 119, 0.6);
|
text-shadow: 0 0 10px rgba(219, 39, 119, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell.o {
|
.cell.o {
|
||||||
color: var(--accent); /* Green for O */
|
color: var(--accent);
|
||||||
|
/* Green for O */
|
||||||
text-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
|
text-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
|
||||||
}
|
}
|
||||||
|
|
@ -6,46 +6,17 @@ const joinBtn = document.getElementById('join-btn');
|
||||||
const joinInput = document.getElementById('join-code');
|
const joinInput = document.getElementById('join-code');
|
||||||
const displayCode = document.getElementById('display-code');
|
const displayCode = document.getElementById('display-code');
|
||||||
const turnStatus = document.getElementById('turn-status');
|
const turnStatus = document.getElementById('turn-status');
|
||||||
const surrenderBtn = document.getElementById('surrender-btn');
|
const leaveBtn = document.getElementById('leave-btn');
|
||||||
const cells = document.querySelectorAll('.cell');
|
|
||||||
|
|
||||||
let mySymbol = '';
|
// ... (other vars)
|
||||||
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 = () => {
|
leaveBtn.addEventListener('click', () => {
|
||||||
statusDiv.textContent = 'Connected to Server';
|
if (confirm('Are you sure you want to leave the lobby?')) {
|
||||||
statusDiv.style.color = '#10b981';
|
send('LEAVE');
|
||||||
};
|
resetGame();
|
||||||
|
|
||||||
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', () => {
|
surrenderBtn.addEventListener('click', () => {
|
||||||
|
|
@ -54,13 +25,7 @@ surrenderBtn.addEventListener('click', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cells.forEach(cell => {
|
// ...
|
||||||
cell.addEventListener('click', () => {
|
|
||||||
const r = cell.dataset.r;
|
|
||||||
const c = cell.dataset.c;
|
|
||||||
send(`MOVE ${r} ${c}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleMessage(msg) {
|
function handleMessage(msg) {
|
||||||
console.log("Received:", msg);
|
console.log("Received:", msg);
|
||||||
|
|
@ -83,6 +48,13 @@ function handleMessage(msg) {
|
||||||
case 'START':
|
case 'START':
|
||||||
updateStatus("Game Started! X goes first.");
|
updateStatus("Game Started! X goes first.");
|
||||||
break;
|
break;
|
||||||
|
case 'RESTART':
|
||||||
|
updateStatus("Game Restarted! X goes first.");
|
||||||
|
cells.forEach(c => {
|
||||||
|
c.textContent = '';
|
||||||
|
c.className = 'cell';
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'BOARD':
|
case 'BOARD':
|
||||||
const boardStr = msg.substring(6);
|
const boardStr = msg.substring(6);
|
||||||
updateBoard(boardStr);
|
updateBoard(boardStr);
|
||||||
|
|
@ -97,11 +69,21 @@ function handleMessage(msg) {
|
||||||
break;
|
break;
|
||||||
case 'WIN':
|
case 'WIN':
|
||||||
const winner = msg.substring(4);
|
const winner = msg.substring(4);
|
||||||
alert(`Game Over! Winner: ${winner}`);
|
setTimeout(() => {
|
||||||
resetGame();
|
if (confirm(`Game Over! Winner: ${winner}\nDo you want to play again?`)) {
|
||||||
|
send('RESTART');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
break;
|
break;
|
||||||
case 'DRAW':
|
case 'DRAW':
|
||||||
alert("Game Over! It's a draw!");
|
setTimeout(() => {
|
||||||
|
if (confirm("Game Over! It's a draw!\nDo you want to play again?")) {
|
||||||
|
send('RESTART');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
break;
|
||||||
|
case 'OPPONENT_LEFT':
|
||||||
|
alert("Opponent has left the lobby.");
|
||||||
resetGame();
|
resetGame();
|
||||||
break;
|
break;
|
||||||
case 'ERROR:':
|
case 'ERROR:':
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>TicTacToe</title>
|
<title>TicTacToe</title>
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<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">
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;400&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>TicTacToe</h1>
|
<h1>TicTacToe</h1>
|
||||||
|
|
@ -26,7 +29,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Section -->
|
|
||||||
<div id="game" class="panel hidden">
|
<div id="game" class="panel hidden">
|
||||||
<div class="game-info">
|
<div class="game-info">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
|
|
@ -52,10 +54,14 @@
|
||||||
<div class="cell" data-r="2" data-c="2"></div>
|
<div class="cell" data-r="2" data-c="2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="surrender-btn" class="btn danger">Surrender / Give Up</button>
|
<div class="game-actions">
|
||||||
|
<button id="leave-btn" class="btn secondary">Leave Lobby</button>
|
||||||
|
<button id="surrender-btn" class="btn danger">Surrender</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue