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`.
|
||||
- **MOVE <ROW> <COL>**: Places your symbol (0-2). Validated by server.
|
||||
- **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
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class Main extends JFrame {
|
|||
private char mySymbol = ' '; // assigned by server implied turn
|
||||
|
||||
public static void main(String[] args) {
|
||||
String host = "dokploy.lona-development.org";
|
||||
String host = "38.242.130.81";
|
||||
int port = 1870;
|
||||
|
||||
if (args.length > 0) {
|
||||
|
|
@ -126,9 +126,26 @@ public class Main extends JFrame {
|
|||
}
|
||||
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"));
|
||||
gamePanel.add(surrenderBtn, BorderLayout.SOUTH);
|
||||
|
||||
buttonPanel.add(leaveBtn);
|
||||
buttonPanel.add(surrenderBtn);
|
||||
|
||||
gamePanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
mainPanel.add(menuPanel, "MENU");
|
||||
mainPanel.add(gamePanel, "GAME");
|
||||
|
|
@ -155,6 +172,7 @@ public class Main extends JFrame {
|
|||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("Connection Failed.");
|
||||
JOptionPane.showMessageDialog(this, "Connection Error: " + e.getMessage());
|
||||
// Don't reset to menu if we can't connect, let user see error
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
|
|
@ -191,6 +209,12 @@ public class Main extends JFrame {
|
|||
case "START":
|
||||
statusLabel.setText("Game Started! X goes first.");
|
||||
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 ...
|
||||
String boardStr = msg.substring(6); // remove "BOARD "
|
||||
for (int i = 0; i < 9 && i < boardStr.length(); i++) {
|
||||
|
|
@ -210,11 +234,29 @@ public class Main extends JFrame {
|
|||
break;
|
||||
case "WIN":
|
||||
String winner = msg.substring(4);
|
||||
JOptionPane.showMessageDialog(this, "Winner: " + winner);
|
||||
resetGame();
|
||||
statusLabel.setText("Winner: " + winner);
|
||||
int choice = JOptionPane.showConfirmDialog(this,
|
||||
"Winner: " + winner + "\nPlay Again?",
|
||||
"Game Over",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
|
||||
if (choice == JOptionPane.YES_OPTION) {
|
||||
send("RESTART");
|
||||
}
|
||||
break;
|
||||
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();
|
||||
break;
|
||||
case "ERROR:":
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
#Generated by Maven
|
||||
#Tue Feb 10 13:27:55 CET 2026
|
||||
#Tue Feb 10 14:07:26 CET 2026
|
||||
artifactId=client
|
||||
groupId=com.lona.tictactoe
|
||||
version=1.0-SNAPSHOT
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -85,6 +85,27 @@ public class ClientHandler implements Runnable {
|
|||
}
|
||||
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:
|
||||
out.println("ERROR: Unknown command");
|
||||
}
|
||||
|
|
@ -94,10 +115,13 @@ public class ClientHandler implements Runnable {
|
|||
} finally {
|
||||
if (gameCode != null) {
|
||||
GameInstance g = GameManager.getGame(gameCode);
|
||||
if (g != null)
|
||||
if (g != null) {
|
||||
g.disconnect(this);
|
||||
if (g.isEmpty()) {
|
||||
GameManager.removeGame(gameCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
client.close();
|
||||
} catch (IOException e) {
|
||||
|
|
|
|||
|
|
@ -35,36 +35,30 @@ public class GameInstance {
|
|||
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);
|
||||
|
|
@ -78,19 +72,42 @@ public class GameInstance {
|
|||
char loser = (player == playerX) ? 'X' : 'O';
|
||||
char winner = (loser == 'X') ? 'O' : 'X';
|
||||
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) {
|
||||
if (finished)
|
||||
return;
|
||||
finished = true;
|
||||
broadcast("ERROR: Opponent disconnected");
|
||||
createEndState();
|
||||
}
|
||||
|
||||
private void createEndState() {
|
||||
// cleanup if needed
|
||||
leave(player);
|
||||
}
|
||||
|
||||
private boolean checkWin(char s) {
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
#Generated by Maven
|
||||
#Tue Feb 10 13:26:46 CET 2026
|
||||
#Tue Feb 10 14:07:14 CET 2026
|
||||
artifactId=server
|
||||
groupId=com.lona.tictactoe
|
||||
version=1.0-SNAPSHOT
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -84,14 +84,18 @@ h1 {
|
|||
}
|
||||
|
||||
.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 */
|
||||
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 */
|
||||
background: #27272a;
|
||||
/* Slightly lighter on hover */
|
||||
}
|
||||
|
||||
.danger {
|
||||
|
|
@ -102,7 +106,8 @@ h1 {
|
|||
|
||||
input[type="text"] {
|
||||
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;
|
||||
margin: 10px 0;
|
||||
background: #27272a;
|
||||
|
|
@ -180,12 +185,20 @@ input[type="text"]:focus {
|
|||
background: #3f3f46;
|
||||
}
|
||||
|
||||
.game-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
|
@ -6,46 +6,17 @@ 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');
|
||||
const leaveBtn = document.getElementById('leave-btn');
|
||||
|
||||
let mySymbol = '';
|
||||
let ws = null;
|
||||
let currentCode = '';
|
||||
// ... (other vars)
|
||||
|
||||
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);
|
||||
};
|
||||
leaveBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to leave the lobby?')) {
|
||||
send('LEAVE');
|
||||
resetGame();
|
||||
}
|
||||
|
||||
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', () => {
|
||||
|
|
@ -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) {
|
||||
console.log("Received:", msg);
|
||||
|
|
@ -83,6 +48,13 @@ function handleMessage(msg) {
|
|||
case 'START':
|
||||
updateStatus("Game Started! X goes first.");
|
||||
break;
|
||||
case 'RESTART':
|
||||
updateStatus("Game Restarted! X goes first.");
|
||||
cells.forEach(c => {
|
||||
c.textContent = '';
|
||||
c.className = 'cell';
|
||||
});
|
||||
break;
|
||||
case 'BOARD':
|
||||
const boardStr = msg.substring(6);
|
||||
updateBoard(boardStr);
|
||||
|
|
@ -97,11 +69,21 @@ function handleMessage(msg) {
|
|||
break;
|
||||
case 'WIN':
|
||||
const winner = msg.substring(4);
|
||||
alert(`Game Over! Winner: ${winner}`);
|
||||
resetGame();
|
||||
setTimeout(() => {
|
||||
if (confirm(`Game Over! Winner: ${winner}\nDo you want to play again?`)) {
|
||||
send('RESTART');
|
||||
}
|
||||
}, 100);
|
||||
break;
|
||||
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();
|
||||
break;
|
||||
case 'ERROR:':
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<!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">
|
||||
<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>
|
||||
|
|
@ -26,7 +29,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Section -->
|
||||
<div id="game" class="panel hidden">
|
||||
<div class="game-info">
|
||||
<div class="info-item">
|
||||
|
|
@ -52,10 +54,14 @@
|
|||
<div class="cell" data-r="2" data-c="2"></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>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -84,14 +84,18 @@ h1 {
|
|||
}
|
||||
|
||||
.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 */
|
||||
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 */
|
||||
background: #27272a;
|
||||
/* Slightly lighter on hover */
|
||||
}
|
||||
|
||||
.danger {
|
||||
|
|
@ -102,7 +106,8 @@ h1 {
|
|||
|
||||
input[type="text"] {
|
||||
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;
|
||||
margin: 10px 0;
|
||||
background: #27272a;
|
||||
|
|
@ -180,12 +185,20 @@ input[type="text"]:focus {
|
|||
background: #3f3f46;
|
||||
}
|
||||
|
||||
.game-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
|
@ -6,46 +6,17 @@ 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');
|
||||
const leaveBtn = document.getElementById('leave-btn');
|
||||
|
||||
let mySymbol = '';
|
||||
let ws = null;
|
||||
let currentCode = '';
|
||||
// ... (other vars)
|
||||
|
||||
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);
|
||||
};
|
||||
leaveBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to leave the lobby?')) {
|
||||
send('LEAVE');
|
||||
resetGame();
|
||||
}
|
||||
|
||||
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', () => {
|
||||
|
|
@ -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) {
|
||||
console.log("Received:", msg);
|
||||
|
|
@ -83,6 +48,13 @@ function handleMessage(msg) {
|
|||
case 'START':
|
||||
updateStatus("Game Started! X goes first.");
|
||||
break;
|
||||
case 'RESTART':
|
||||
updateStatus("Game Restarted! X goes first.");
|
||||
cells.forEach(c => {
|
||||
c.textContent = '';
|
||||
c.className = 'cell';
|
||||
});
|
||||
break;
|
||||
case 'BOARD':
|
||||
const boardStr = msg.substring(6);
|
||||
updateBoard(boardStr);
|
||||
|
|
@ -97,11 +69,21 @@ function handleMessage(msg) {
|
|||
break;
|
||||
case 'WIN':
|
||||
const winner = msg.substring(4);
|
||||
alert(`Game Over! Winner: ${winner}`);
|
||||
resetGame();
|
||||
setTimeout(() => {
|
||||
if (confirm(`Game Over! Winner: ${winner}\nDo you want to play again?`)) {
|
||||
send('RESTART');
|
||||
}
|
||||
}, 100);
|
||||
break;
|
||||
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();
|
||||
break;
|
||||
case 'ERROR:':
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<!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">
|
||||
<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>
|
||||
|
|
@ -26,7 +29,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Section -->
|
||||
<div id="game" class="panel hidden">
|
||||
<div class="game-info">
|
||||
<div class="info-item">
|
||||
|
|
@ -52,10 +54,14 @@
|
|||
<div class="cell" data-r="2" data-c="2"></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>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue