## game.html
```html=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tic Tac Toe Game</title>
<link rel="stylesheet" href="game.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"
integrity="sha512-zoJXRvW2gC8Z0Xo3lBbao5+AS3g6YWr5ztKqaicua11xHo+AvE1b0lT9ODgrHTmNUxeCw0Ry4BGRYZfXu70weg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
</head>
<body>
<div class="game-container">
<div class="players-info">
<span id="player1-info"></span> vs. <span id="player2-info"></span>
</div>
<table id="game-board">
<tr>
<td data-row="0" data-col="0"></td>
<td data-row="0" data-col="1"></td>
<td data-row="0" data-col="2"></td>
</tr>
<tr>
<td data-row="1" data-col="0"></td>
<td data-row="1" data-col="1"></td>
<td data-row="1" data-col="2"></td>
</tr>
<tr>
<td data-row="2" data-col="0"></td>
<td data-row="2" data-col="1"></td>
<td data-row="2" data-col="2"></td>
</tr>
</table>
<div id="game-info"></div>
</div>
<script src="game.js"></script>
</body>
</html>
```
## game.css
```css=
@import url('https://fonts.googleapis.com/css2?family=Rubik+Iso&family=Sedgwick+Ave+Display&display=swap');
body, html {
margin: 0;
padding: 0;
border: none;
outline: none;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
.game-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #36393f;
color: #ffffff;
}
.players-info {
margin-bottom: 20px;
font-family: 'Sedgwick Ave Display', cursive;
font-size: 28px;
background-color: #2f3136;
padding: 10px 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
color: #dcddde;
}
#player1-info, #player2-info {
font-weight: normal;
padding: 0 10px;
}
#game-info {
font-family: 'Sedgwick Ave Display', cursive;
font-size: 18px;
margin-top: 16px;
background-color: #2F313B;
padding: 15px 25px;
border-radius: 6px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
color: #dcddde;
}
#game-board td {
width: 115px;
height: 115px;
border: 5px solid #40444b;
text-align: center;
vertical-align: middle;
cursor: pointer;
background-color: #1f2025;
}
#game-board td:hover {
background-color: #2d2e35;
transition: all 0.4s;
}
#game-board td img {
width: 100%;
height: 100%;
object-fit: contain;
display: block; /* Remove extra space */
cursor: not-allowed;
}
#game-board tr:first-child td {
border-top: none;
}
#game-board tr:last-child td {
border-bottom: none;
}
#game-board td:first-child {
border-left: none;
}
#game-board td:last-child {
border-right: none;
}
#game-controls {
margin-top: 20px;
}
@keyframes epicAnimation {
0% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.5); opacity: 1; }
100% { transform: scale(1); opacity: 0.5; }
}
#game-info.epic {
animation: epicAnimation;
animation-delay: 0.7s;
animation-duration: 6s;
color: gold;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
```
## game.js
```js=
const socket = io.connect('http://localhost:3000');
const gameBoard = document.getElementById('game-board');
const player1Info = document.getElementById('player1-info');
const player2Info = document.getElementById('player2-info');
const gameInfo = document.getElementById('game-info');
let currentPlayer = 'X';
let gameStarted = false;
let playerSymbol;
let gameOver = false;
// Extract roomId from the URL
const urlParams = new URLSearchParams(window.location.search);
const roomId = urlParams.get('roomId');
socket.emit('check-room-existence', {
roomName: roomId
});
let username = localStorage.getItem('username');
let clientId = localStorage.getItem('clientId');
if (username && clientId) {
socket.emit('reconnect', {
username: username,
clientId: clientId,
roomName: roomId
});
}
if (username) {
console.log(`Emitting join-room with username: ${username} and room: ${roomId}`);
socket.emit('join-room', {
roomName: roomId,
username: username
});
}
gameBoard.addEventListener('click', (e) => {
if (gameOver) {
alert("The game is over!");
return;
}
if (!gameStarted) {
alert("The game hasn't started yet!");
return;
}
if (currentPlayer !== playerSymbol) {
alert("It's not your turn!");
return;
}
const cell = e.target;
if (cell.tagName === 'TD' && !cell.firstChild) {
// Don't create and append the img here. Wait for the server's confirmation.
socket.emit('make-move', {
row: parseInt(cell.dataset.row),
col: parseInt(cell.dataset.col),
symbol: currentPlayer,
roomName: roomId
});
} else {
alert("X or O already in this cell...")
}
});
// LISTENERS
socket.on('game-started', () => {
gameStarted = true;
});
socket.on('player-symbol', (data) => {
playerSymbol = data.symbol;
});
socket.on('room-existence-response', (data) => {
if (data.exists) {
player1Info.textContent = data.player1;
} else {
alert("The room you're trying to join doesn't exist!");
window.location.href = 'index.html';
}
});
socket.on('receive-move', (data) => {
const cell = gameBoard.querySelector(`[data-row="${data.row}"][data-col="${data.col}"]`);
const img = document.createElement('img');
img.src = `images/${data.symbol.toLowerCase()}.png`;
cell.appendChild(img);
currentPlayer = data.nextPlayer;
updateTurnInfo();
});
socket.on('players-ready', (data) => {
console.log('Received players-ready event:', data);
player1Info.textContent = data.player1 || '';
player2Info.textContent = data.player2 || '';
updateTurnInfo();
});
socket.on('disconnect', (reason) => {
console.log('Socket disconnected due to:', reason);
});
socket.on('connect_error', (error) => {
console.log('Socket connection error:', error);
});
socket.on('room-error', (msg) => {
alert("Room error: " + msg);
});
socket.on('game-over', (result) => {
gameOver = true;
if (result === 'DRAW') {
gameInfo.textContent = "DRAW!";
} else {
gameInfo.textContent = `${result === 'X' ? player1Info.textContent.trim() : player2Info.textContent.trim()} WINS!!!`.toUpperCase();
}
partyTime()
gameInfo.classList.add('epic');
});
// functions
function updateTurnInfo() {
if (!player2Info.textContent.trim()) { // Check if player2's name is not set
gameInfo.textContent = "Waiting for second player";
} else if (currentPlayer === 'X') {
gameInfo.textContent = `${player1Info.textContent.trim()}'s turn (X)`;
} else {
gameInfo.textContent = `${player2Info.textContent.trim()}'s turn (O)`;
}
}
function partyTime() {
confetti({
spread: 180
});
// do this for 5 seconds
var duration = 5 * 1000;
var end = Date.now() + duration;
(function frame() {
// launch a few confetti from the left edge
confetti({
particleCount: 7,
angle: 60,
spread: 55,
origin: {
x: 0
}
});
// and launch a few from the right edge
confetti({
particleCount: 7,
angle: 120,
spread: 55,
origin: {
x: 1
}
});
// keep going until we are out of time
if (Date.now() < end) {
requestAnimationFrame(frame);
}
}());
}
```
## Celi script.js (za index.html)
```js=
// document.addEventListener('DOMContentLoaded', function() {
// const lobbiesList = document.getElementById('lobbies-list');
// const sampleRooms = [
// { id: 'room-12345', name: "Alice's Room", status: 'IN_PROGRESS' },
// { id: 'room-67890', name: "Bob's Room", status: 'LOOKING_FOR_PLAYER' },
// { id: 'room-11223', name: "Charlie's Room", status: 'IN_PROGRESS' },
// ];
// sampleRooms.forEach(room => {
// const a = document.createElement('a');
// a.href = `game.html?roomId=${room.id}`;
// a.textContent = room.name + (room.status === 'IN_PROGRESS' ? ' (IN PROGRESS)' : ' (LOOKING FOR PLAYER)');
// lobbiesList.appendChild(a);
// });
// });
const socket = io.connect('http://localhost:3000');
const usernameInput = document.getElementById('username-input');
const setUsernameBtn = document.getElementById('set-username-btn');
const createLobbyBtn = document.getElementById('create-lobby-btn');
const lobbiesList = document.getElementById('lobbies-list');
const usernameStatus = document.getElementById('username-status');
let username = localStorage.getItem('username');
let clientId = localStorage.getItem('clientId');
document.addEventListener('DOMContentLoaded', () => {
if (username && clientId) {
setUsernameStatus(true, username);
usernameInput.value = username;
socket.emit('reclaim-username', { username, clientId });
} else {
setUsernameStatus(false, null);
}
socket.emit('get-rooms');
});
// Button click events
setUsernameBtn.addEventListener('click', () => {
const newUsername = usernameInput.value.trim();
let canChange = true;
if (username) {
canChange = confirm(`Are you sure you want to change your existing username "${username}"?`);
}
if (!canChange) {
return;
}
if (newUsername == username) {
alert("You are trying to change your username to the same name... can't do that");
return;
}
if (newUsername.length < 3) {
alert("Username must be at least 3 characters long");
return;
}
if (newUsername.length > 7) {
alert("Username must be max 7 characters long");
return;
}
const existingClientId = localStorage.getItem('clientId') || null;
socket.emit('set-username', { username: newUsername, clientId: existingClientId });
});
createLobbyBtn.addEventListener('click', () => {
checkIfSocketConnected(socket);
if (username) {
// Prompt the user to select an image
const imageInput = document.createElement('input');
imageInput.type = 'file';
imageInput.accept = 'image/*';
imageInput.onchange = function() {
// Upload the image
const formData = new FormData();
formData.append('lobbyImage', imageInput.files[0]);
fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error);
});
}
return response.json();
})
.then(data => {
socket.emit('create-room', {
username: username,
imageUrl: data.imageUrl
});
})
.catch(error => {
console.error('Error uploading image:', error);
alert(error.message); // Display the error message to the user
});
};
imageInput.click(); // This will open the file dialog
} else {
alert("Please set a username first.");
}
});
// Extra...
toggleSpinner(false); // na zacetku izklopljen
// pocakamo 0.5 sekund...
let canShowSpinner = false;
setTimeout(() => {
canShowSpinner = true;
}, 500);
//// SOCKET LISTENERS
socket.on('connect', () => {
console.log('Socket connected!');
toggleSpinner(false);
});
socket.on('disconnect', (reason) => {
console.log('Socket disconnected due to:', reason);
toggleSpinner(true);
});
socket.on('reconnect', (attemptNumber) => {
console.log('Socket reconnected on attempt:', attemptNumber);
toggleSpinner(false);
});
socket.on('room-error', (msg) => {
alert("Room error: " + msg);
});
socket.on('username-status', (data) => {
if (data.isUnique) {
username = data.username; // Use the username sent from the server
localStorage.setItem('username', username);
// Store the clientId in localStorage
localStorage.setItem('clientId', data.clientId);
setUsernameStatus(true, username);
enableCreateLobbyBtn();
} else {
alert(`Username ${username} is already in use or not available!`);
}
});
socket.on('update-rooms', (rooms) => {
lobbiesList.innerHTML = '';
rooms.forEach(room => {
const a = document.createElement('a');
a.href = `game.html?roomId=${room.id}`;
a.textContent = room.player1 + "'s Room" + (room.status === 'IN_PROGRESS' ? ' (IN PROGRESS)' : ' (LOOKING FOR PLAYER)');
// Add the image next to the lobby name
const img = document.createElement('img');
img.src = "http://localhost:3000" + room.imageUrl;
img.alt = "[Cant load room pic]";
img.className = "lobbyPic"
img.width = 42;
a.prepend(img);
// Add trash icon if the user is the owner of the lobby
if (room.player1 === username) {
const trashWrapper = document.createElement('div');
trashWrapper.className = "trash-wrapper";
const trashIcon = document.createElement('i');
trashIcon.className = "fa fa-trash";
trashIcon.style.color = "red";
trashIcon.addEventListener('click', (e) => {
e.preventDefault();
socket.emit('delete-lobby', room.id);
});
trashWrapper.appendChild(trashIcon);
a.appendChild(trashWrapper);
}
lobbiesList.appendChild(a);
});
});
//// FUNCTIONS
function enableCreateLobbyBtn() {
createLobbyBtn.classList.add('enabled');
createLobbyBtn.disabled = false;
}
function setUsernameStatus(isSet, currentUsername) {
usernameStatus.textContent = isSet ? `Username set (${currentUsername})` : "Username not set";
usernameStatus.style.color = isSet ? "green" : "red";
// Set the tooltip for the createLobbyBtn
createLobbyBtn.setAttribute('title', isSet ? "Be sure to have a lobby pic ready" : "Please set a username first");
}
function checkIfSocketConnected(socket, customAlert = "") {
let connected = socket.connected;
if (!connected) {
if (customAlert) {
alert(customAlert)
} else {
alert("Connection not established / Connection lost with server's socket");
}
}
return connected;
}
function toggleSpinner(toggleON) {
if(toggleON) {
document.querySelector('.spinner-container').style.display = 'flex';
document.querySelector('.main-container').style.display = 'none';
} else {
document.querySelector('.spinner-container').style.display = 'none';
document.querySelector('.main-container').style.display = 'flex';
}
}
```
## HTML za nas HOME page (index.html)
```html=
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Lobbies</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="style.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js" integrity="sha512-zoJXRvW2gC8Z0Xo3lBbao5+AS3g6YWr5ztKqaicua11xHo+AvE1b0lT9ODgrHTmNUxeCw0Ry4BGRYZfXu70weg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<div class="spinner-container">
<h2>Connecting to server</h2>
<div class="spinner"></div>
</div>
<div class="main-container">
<div class="logo-container">
<img src="images/logo.png" alt="Manjka slika" id="site-logo">
</div>
<div class="username-container">
<div id="username-status" class="username-status"></div>
<div class="username-box">
<input type="text" id="username-input" placeholder="Enter username" autocomplete="off">
<button id="set-username-btn">SET</button>
</div>
<button id="create-lobby-btn">CREATE LOBBY</button>
<h4 id="lobbiesListh4">Lobbies List</h4>
<div id="lobbies-list"></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
```
## CSS za lobbies index.html (poimenuj stil.css)
```css=
body {
font-family: Arial, sans-serif;
background-color: #36393f;
margin: 0;
padding: 0;
color: #ffffff;
}
.spinner-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
height: 100vh;
}
.spinner {
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.main-container {
/* display: none; hide it initially */
}
.main-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh; /*full height of the viewport*/
}
.fa-trash {
margin-left: 10px;
transition: all 0.3s;
border: 5px solid transparent;
}
.fa-trash:hover {
background-color: rgba(255, 5, 5, 0.219);
border-color: rgba(255, 5, 5, 0.219);
border-radius: 5px;
}
.trash-wrapper {
display: inline-block;
}
.trash-wrapper:hover .fa-trash {
cursor: default !important;
}
/* Override the <a> tag's cursor when hovering over the trash icon */
a:hover .trash-wrapper:hover {
cursor: default;
}
.logo-container {
position: absolute;
top: 60px;
text-align: center;
}
#site-logo {
width: 260px;
height: auto;
}
.username-container {
text-align: center;
background-color: #2f3136;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.username-status {
font-weight: bold;
font-size: 14px;
margin-bottom: 10px;
}
.username-box {
display: inline-flex;
align-items: center;
margin-bottom: 10px;
}
#username-input {
border: 2px solid #202225;
padding: 10px;
border-radius: 20px 0 0 20px;
outline: none;
width: 200px;
background-color: #40444b;
color: #ffffff;
}
#set-username-btn {
border: 2px solid #202225;
padding: 10px;
border-radius: 0 20px 20px 0;
background-color: #40444b;
transition: background-color 0.3s;
color: #ffffff;
}
#set-username-btn:hover {
background-color: #50555b;
}
#set-username-btn:active {
background-color: #30333b;
}
#create-lobby-btn {
display: block;
margin: 20px auto;
padding: 15px 40px;
border: none;
border-radius: 30px;
background-color: #202225;
cursor: not-allowed;
transition: background-color 0.3s;
color: #ffffff;
opacity: 0.4;
}
#create-lobby-btn.enabled {
background-color: #7289da;
cursor: pointer;
opacity: 1;
}
#create-lobby-btn.enabled:hover {
background-color: #5b6eae;
}
#lobbies-list {
margin-top: 20px;
max-height: 300px;
overflow-y: scroll;
overflow: auto;
border: 1px solid #202225;
border-radius: 10px;
padding: 10px;
background-color: #40444b;
}
a {
text-decoration: none;
color: #7289da;
display: block;
margin: 5px 0;
padding: 5px;
border-radius: 5px;
transition: background-color 0.3s;
}
a:hover {
background-color: #50555b;
}
.lobbyPic {
border-radius: 50%;
transition: border-radius 0.3s;
vertical-align: middle;
margin-right: 8px;
}
a:hover .lobbyPic {
border-radius: 0%;
}
```
# SERVER CODE (server.js)
### run with `npm start` (google what npm is) (before start ... do `npm i`)
```js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
var createError = require('http-errors');
const app = express();
const server = http.createServer(app);
const multer = require('multer');
const rateLimit = require("express-rate-limit");
const fs = require('fs');
const {
unlink
} = require('fs');
const path = require('path');
app.use(express.static(path.join(__dirname, 'public')));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir);
}
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(cors({
origin: "*",
credentials: true
}));
// storage related
// Setup multer storage
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, './uploads/')
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname)
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
// Rate limiting middleware
const uploadLimiter = rateLimit({
windowMs: 5 * 1000, // 5 seconds
max: 1, // 1 request per windowMs
handler: function (req, res) {
res.status(429).json({
error: "Too many images uploaded in the span of 5 seconds, please try again later."
});
}
});
app.use('/upload', uploadLimiter);
// Endpoint to handle image uploads
app.post('/upload', upload.single('lobbyImage'), (req, res, next) => {
if (req.file) {
res.json({
imageUrl: '/uploads/' + req.file.filename
});
} else {
next(new Error('Error uploading image.'));
}
});
app.get('/works', (req, res) => {
res.send("Works.")
})
// Error handling middleware
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
error: 'Image size is too large. Please upload an image smaller than 5MB.'
});
}
return res.status(400).json({
error: 'Error uploading image.'
});
}
// Handle other errors here if needed
res.status(500).json({
error: 'Internal Server Error.'
});
});
///////////////
function setNewUsername(socket, newUsername, clientId) {
// Only delete the old username if it's different from the new one
if (socket.username && socket.username !== newUsername) {
usernames.delete(socket.username);
}
usernames.add(newUsername);
socket.username = newUsername; // Store the new username on the socket
if (!clientId) {
clientId = generateUUID();
}
userMappings[newUsername] = { id: clientId, timestamp: Date.now() };
socket.emit('username-status', { isUnique: true, clientId, username: newUsername });
}
function generateUUID() {
// Simple UUID generator
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function checkWinner(board) {
// Check rows, columns, and diagonals for a winner
for (let i = 0; i < 3; i++) {
if (board[i][0] && board[i][0] === board[i][1] && board[i][0] === board[i][2]) return board[i][0];
if (board[0][i] && board[0][i] === board[1][i] && board[0][i] === board[2][i]) return board[0][i];
}
if (board[0][0] && board[0][0] === board[1][1] && board[0][0] === board[2][2]) return board[0][0];
if (board[0][2] && board[0][2] === board[1][1] && board[0][2] === board[2][0]) return board[0][2];
return null;
}
function getRooms() {
return Object.entries(gameStates).map(([name, state]) => ({
id: name,
player1: state.player1,
name: state.player1 + "'s Room",
status: state.player2 ? 'IN_PROGRESS' : 'LOOKING_FOR_PLAYER',
imageUrl: state.imageUrl
}));
}
/// SOCKET RELATED
const io = socketIo(server, {
cors: {
origin: "*",
methods: ["GET", "POST"],
credentials: true
}
});
const gameStates = {};
const usernames = new Set();
const userRooms = {};
const userMappings = {}; // { username: { id: 'some-uuid', timestamp: Date.now() } }
io.on('connection', (socket) => {
console.log('A user connected');
socket.on('get-rooms', () => {
let rooms = getRooms();
socket.emit('update-rooms', rooms);
});
socket.on('set-username', (data) => {
const newUsername = data.username;
const clientId = data.clientId;
if (usernames.has(newUsername)) {
// If the username is already in use, check if the clientId matches
const existingUser = userMappings[newUsername];
if (existingUser && existingUser.id === clientId && Date.now() - existingUser.timestamp <= 30000) {
// If the clientId matches and the timestamp is within 30 seconds, allow reclaiming the username
setNewUsername(socket, newUsername, clientId);
} else {
socket.emit('username-status', { isUnique: false });
}
} else {
setNewUsername(socket, newUsername, clientId);
}
});
socket.on('reconnect', (data) => {
const { username, clientId, roomName } = data;
const userMapping = userMappings[username];
if (userMapping && userMapping.id === clientId) {
const gameState = gameStates[roomName];
if (gameState) {
if (gameState.player1 === username) {
gameState.player1Connected = true; // Set the connection flag for player1
socket.join(roomName);
console.log(`${username} has rejoined room ${roomName}`);
io.to(roomName).emit('players-ready', {
roomName,
player1: gameState.player1Connected ? gameState.player1 : null,
player2: gameState.player2Connected ? gameState.player2 : null,
currentPlayer: gameState.currentPlayer,
});
socket.emit('player-symbol', { symbol: 'X' });
} else if (gameState.player2 === username) {
gameState.player2Connected = true; // Set the connection flag for player2
socket.join(roomName);
console.log(`${username} has rejoined room ${roomName}`);
io.to(roomName).emit('players-ready', {
roomName,
player1: gameState.player1Connected ? gameState.player1 : null,
player2: gameState.player2Connected ? gameState.player2 : null,
currentPlayer: gameState.currentPlayer,
});
socket.emit('player-symbol', { symbol: 'O' });
}
}
}
});
socket.on('reclaim-username', (data) => {
const usernameToReclaim = data.username;
const clientId = data.clientId;
const userMapping = userMappings[usernameToReclaim];
if (userMapping && userMapping.id === clientId) {
setNewUsername(socket, usernameToReclaim, clientId);
} else if (!usernames.has(usernameToReclaim)) {
// If the username is not in use, set it as a new username
setNewUsername(socket, usernameToReclaim, clientId);
} else {
socket.emit('username-status', { isUnique: false });
}
});
socket.on('create-room', (data) => {
const username = data.username;
const imageUrl = data.imageUrl;
if (!socket.username) {
socket.emit('room-error', 'Please set a username first.');
return;
}
// Check if user already has a room
if (userRooms[socket.username]) {
console.log(`User ${socket.username} already has an active lobby: ${userRooms[socket.username]}`);
socket.emit('room-error', 'You already have an active lobby.');
return;
}
const roomName = 'room-' + Math.random().toString(36).substr(2, 9);
console.log(`Lobby ${roomName} created by ${socket.username}`);
socket.join(roomName);
console.log(`${username} has joined room ${roomName}`);
const clientsInRoom = io.sockets.adapter.rooms.get(roomName);
console.log(`Clients in room ${roomName}:`, [...clientsInRoom]);
gameStates[roomName] = {
player1: username,
player1Connected: true,
player2: null,
player2Connected: false,
board: Array(3).fill(null).map(() => Array(3).fill(null)),
currentPlayer: 'X',
imageUrl: imageUrl,
gameInProgress: false
};
userRooms[socket.username] = roomName; // Store the room against the user's name
io.emit('update-rooms', getRooms());
});
socket.on('delete-lobby', (roomName) => {
if (!gameStates[roomName]) {
socket.emit('room-error', 'The lobby you are trying to delete does not exist.');
return;
}
if (gameStates[roomName].player1 !== socket.username) {
socket.emit('room-error', 'You are not the owner of this lobby.');
return;
}
delete gameStates[roomName];
delete userRooms[socket.username]; // Ensure the user's lobby is deleted from userRooms
console.log(`Lobby ${roomName} deleted by ${socket.username}`);
io.emit('update-rooms', getRooms());
});
socket.on('join-room', (data) => {
const roomName = data.roomName;
const username = data.username;
const gameState = gameStates[roomName];
if (gameState && gameState.gameInProgress) {
socket.emit('room-error', 'Game is already in progress.');
return;
}
// Ensure the client joins the room
socket.join(roomName);
console.log(`Attempting to join room: ${roomName} with username: ${username}`);
if (gameState) {
console.log(`Current state of room ${roomName}:`, gameState);
if (username !== gameState.player1 && !gameState.player2) {
console.log(`Setting player2 as ${username}`);
gameState.player2 = username;
gameState.gameStarted = true;
io.to(roomName).emit('game-started');
io.to(roomName).emit('players-ready', {
roomName,
player1: gameState.player1,
player2: gameState.player2,
currentPlayer: gameState.currentPlayer,
});
socket.emit('player-symbol', { symbol: 'O' });
} else if (username === gameState.player1 && !gameState.player2) {
console.log(`Player1 ${username} rejoining the room.`);
io.to(roomName).emit('players-ready', {
roomName,
player1: gameState.player1,
player2: gameState.player2,
currentPlayer: gameState.currentPlayer,
});
console.log(`Emitted players-ready event to room ${roomName}`);
socket.emit('player-symbol', { symbol: 'X' });
}
} else {
console.log(`Room ${roomName} does not exist.`);
}
});
socket.on('check-room-existence', (data) => {
const roomName = data.roomName;
const gameState = gameStates[roomName];
if (gameState) {
socket.emit('room-existence-response', { exists: true, player1: gameState.player1 });
} else {
socket.emit('room-existence-response', { exists: false });
}
});
socket.on('start-game', async (data) => {
const roomName = data.roomName;
const gameState = gameStates[roomName];
if (gameState) {
gameState.gameInProgress = true;
io.to(roomName).emit('game-started');
}
});
socket.on('make-move', (move) => {
const gameState = gameStates[move.roomName];
if (gameState && gameState.gameStarted && gameState.currentPlayer === move.symbol) {
gameState.board[move.row][move.col] = move.symbol;
gameState.currentPlayer = move.symbol === 'X' ? 'O' : 'X';
io.to(move.roomName).emit('receive-move', {
row: move.row,
col: move.col,
symbol: move.symbol,
nextPlayer: gameState.currentPlayer,
});
const winner = checkWinner(gameState.board);
let wasOver = false;
if (winner) {
io.to(move.roomName).emit('game-over', winner);
wasOver = true;
} else if (gameState.board.every(row => row.every(cell => cell))) {
io.to(move.roomName).emit('game-over', 'DRAW');
wasOver = true;
}
if (wasOver) {
delete userRooms[gameState.player1];
if (gameState.player2) {
delete userRooms[gameState.player2];
}
delete gameStates[move.roomName];
io.emit('update-rooms', getRooms());
}
}
});
socket.on('end-lobby', (roomName) => {
const gameState = gameStates[roomName];
if (gameState) {
// Delete the image associated with the room
const imagePath = path.join(uploadsDir, path.basename(gameState.imageUrl));
unlink(imagePath, (err) => {
if (err) {
console.error('Error deleting the image:', err);
} else {
console.log('Image deleted successfully');
}
});
delete userRooms[gameState.player1];
delete gameStates[roomName];
console.log(`Lobby ${roomName} deleted by ${socket.username}`);
io.emit('update-rooms', getRooms());
socket.leave(roomName);
}
});
socket.on('rematch', (roomName) => {
const gameState = gameStates[roomName];
if (gameState) {
gameState.board = Array(3).fill(null).map(() => Array(3).fill(null));
gameState.currentPlayer = 'X';
io.to(roomName).emit('game-reset');
}
});
socket.on('disconnect', () => {
console.log('A user disconnected');
const roomName = userRooms[socket.username];
const gameState = gameStates[roomName];
if (gameState) {
if (gameState.player1 === socket.username) {
gameState.player1Connected = false;
} else if (gameState.player2 === socket.username) {
gameState.player2Connected = false;
}
}
if (socket.username) {
const userMapping = userMappings[socket.username];
if (userMapping) {
userMapping.timestamp = Date.now();
}
}
});
});
////////////////
/// DB RELATED
// const {
// Pool
// } = require('pg');
// const pool = new Pool({
// host: 'db', // Use service name defined in docker-compose.yml
// port: 5432,
// user: 'root',
// password: 'rootpass',
// database: 'tictactoe',
// });
// Example query
// pool.query('SELECT NOW()', (err, res) => {
// console.log(err, res);
// });
/// REST
// POST endpoint to add game history
// app.post('/add-game-history', (req, res) => {
// // Example request body
// const {
// krogec,
// krizec,
// result,
// koenc
// } = req.body;
// const query = 'INSERT INTO game_history (krogec, krizec, result, koenc) VALUES ($1, $2, $3, $4)';
// const values = [krogec, krizec, result, koenc];
// pool.query(query, values, (err) => {
// if (err) {
// console.error(err);
// res.status(500).send('Error inserting data');
// } else {
// res.status(200).send('Data inserted successfully');
// }
// });
// });
// const createTableQuery = `
// CREATE TABLE IF NOT EXISTS game_history (
// id SERIAL PRIMARY KEY,
// krogec VARCHAR(255),
// krizec VARCHAR(255),
// result VARCHAR(255),
// koenc TIMESTAMP
// );
// `;
// pool.query(createTableQuery, (err) => {
// if (err) {
// console.error('Error creating table:', err);
// } else {
// console.log('Table game_history is ready');
// }
// });
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
```