From 72da84c3d606697e61b27b3814e00215fa5bd785 Mon Sep 17 00:00:00 2001 From: MaciejkaG Date: Sat, 2 Mar 2024 18:28:33 +0100 Subject: [PATCH] Major changes - Huge improvements in UI design and animations - Improvements in code organisation (separated multiple functions into an util file) - Multiple bug fixes and improvements to stability - Client side events improved --- index.js | 92 ++++++++++++---------- public/assets/css/main.css | 19 ++++- public/assets/js/socket-game.js | 7 +- public/assets/js/spa_lib.js | 6 +- utils/battleships.js | 131 ++++++++++++++++++++++++++++++++ views/board.handlebars | 2 +- views/index.handlebars | 7 +- 7 files changed, 210 insertions(+), 54 deletions(-) create mode 100644 utils/battleships.js diff --git a/index.js b/index.js index ad53467..212ff4d 100644 --- a/index.js +++ b/index.js @@ -3,10 +3,11 @@ import { createServer } from 'node:http'; import { Server } from 'socket.io'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4, validate } from 'uuid'; import session from "express-session"; import { engine } from 'express-handlebars'; import { createClient } from 'redis'; +import * as bships from './utils/battleships.js' const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -27,6 +28,8 @@ redis.on('error', err => console.log('Redis Client Error', err)); await redis.connect(); redis.flushDb(); +const GInfo = new bships.GameInfo(redis, io); + app.set('trust proxy', 1); const sessionMiddleware = session({ secret: uuidv4(), @@ -69,7 +72,7 @@ app.get("/game", async (req, res) => { const game = await redis.json.get(`game:${req.query.id}`); if (req.session.nickname == null) { res.redirect("/setup"); - } else if (req.query.id == null || game == null || game.state == "expired") { + } else if (req.query.id == null || game == null || game.state == "expired" || req.session.activeGame == null) { res.status(400).send('badGameId'); } else { res.render('board'); @@ -89,7 +92,7 @@ io.on('connection', async (socket) => { return; } - if (!await isPlayerInGame(socket)) { + if (!await GInfo.isPlayerInGame(socket)) { socket.on('whats my nick', (callback) => { callback(session.nickname); }); @@ -132,18 +135,19 @@ io.on('connection', async (socket) => { // Teraz utwórz objekt partii w trakcie w bazie Redis const gameId = uuidv4(); redis.json.set(`game:${gameId}`, '$', { + hostId: opp.id, state: "pregame", - boards: { - host: { // typ 2 to trójmasztowiec pozycja i obrót na planszy które pola zostały trafione + boards: [ + { // typ 2 to trójmasztowiec pozycja i obrót na planszy które pola zostały trafione ships: [], // zawiera np. {type: 2, posX: 3, posY: 4, rot: 2, hits: [false, false, true]} // pozycja na planszy czy strzał miał udział w zatopieniu statku? shots: [], // zawiera np. {posX: 3, posY: 5, sunk: true} }, - guest: { + { ships: [], shots: [], } - }, + ], nextPlayer: 0, }); @@ -193,12 +197,12 @@ io.on('connection', async (socket) => { }); socket.on('disconnecting', () => { - if (isPlayerInRoom(socket)) { + if (bships.isPlayerInRoom(socket)) { io.to(socket.rooms[1]).emit("player left"); } }); } else { - const playerGame = await getPlayerGameData(socket); + const playerGame = await GInfo.getPlayerGameData(socket); if (playerGame.data.state === 'pregame') { socket.join(playerGame.id); @@ -213,22 +217,33 @@ io.on('connection', async (socket) => { let UTCTs = Math.floor((new Date()).getTime() / 1000 + 90); io.to(playerGame.id).emit('turn update', { turn: 0, phase: "preparation", timerToUTC: UTCTs }); - io.to(playerGame.id).emit(); + bships.timer(90, () => GInfo.endPrepPhase(socket)); } } - // socket.on('shoot', async () => { - // const playerGame = await getPlayerGameData(socket); + socket.on('place ship', async (type, posX, posY, rot) => { + const playerGame = await GInfo.getPlayerGameData(socket); - // if (playerGame.state === 'action') { + if (playerGame.state === 'preparation') { - // } - // }); + } + }); + + socket.on('shoot', async () => { + const playerGame = await GInfo.getPlayerGameData(socket); + + if (playerGame.state === 'action') { + if (bships.checkTurn(playerGame, socket.id)) { + + } + } + }); socket.on('disconnecting', async () => { - io.to(playerGame.id).emit("player left"); - - redis.json.del(`game:${playerGame.id}`); + const playerGame = await GInfo.getPlayerGameData(socket); + if (playerGame !== null) { + AFKEnd(playerGame.id); + } }); } }); @@ -241,35 +256,32 @@ function genID() { return Math.floor(100000 + Math.random() * 900000).toString(); } -// async function emitToParty(partyuuid) { -// const party = gameData.find((element) => element.partyId===partyuuid); +function resetUserGame(req) { + req.session.reload((err) => { + if (err) return socket.disconnect(); -// if (party!==null) { -// party.members.forEach(socketId => { - -// io.to(socketId).emit(); -// }); -// } -// } - -function isPlayerInRoom(socket) { - return !socket.rooms.size === 1; + req.session.activeGame = null; + req.session.save(); + }); } -async function isPlayerInGame(socket) { - const game = await redis.json.get(`game:${socket.session.activeGame}`); - return game != null; +function endGame(gameId) { + const members = [...roomMemberIterator(gameId)]; + for (let i = 0; i < members.length; i++) { + const sid = members[i][0]; + const socket = io.sockets.sockets.get(sid); + resetUserGame(socket.request); + socket.leave(gameId); + } + + redis.json.del(`game:${gameId}`); } -async function getPlayerGameData(socket) { - const game = await redis.json.get(`game:${socket.session.activeGame}`); - return game == null ? null : {id: socket.session.activeGame, data: game}; +function AFKEnd(gameId) { + io.to(gameId).emit("player left"); + endGame(gameId); } function roomMemberIterator(id) { return io.sockets.adapter.rooms.get(id).entries(); -} - -function getPlayerRoom(socket) { - return socket.rooms.entries()[1] === undefined ? null : socket.rooms.entries()[1][0]; } \ No newline at end of file diff --git a/public/assets/css/main.css b/public/assets/css/main.css index 8dd3830..8070464 100644 --- a/public/assets/css/main.css +++ b/public/assets/css/main.css @@ -1,9 +1,20 @@ -body { - transition: opacity 0.1s; -} - .container { display: none; + transition: opacity 0.175s; +} + +@keyframes OutAnim { + from {transform: translateY(0)} + to {transform: translateY(5%);} +} + +@keyframes InAnim { + from {transform: translateY(-5%)} + to {transform: translateY(0%);} +} + +.header { + text-align: center; } /* index */ diff --git a/public/assets/js/socket-game.js b/public/assets/js/socket-game.js index 41b7e4b..e588abe 100644 --- a/public/assets/js/socket-game.js +++ b/public/assets/js/socket-game.js @@ -18,7 +18,12 @@ socket.on("player idx", (idx) => { }); socket.on('turn update', (turnData) => { - turnData.turn == playerIdx ? $("#whosTurn").html("Ty") : $("#whosTurn").html("Przeciwnik"); + if (turnData.phase === "preparation") { + $("#whosTurn").html("Faza przygotowań"); + } else { + turnData.turn === playerIdx ? $("#whosTurn").html("Twoja tura") : $("#whosTurn").html("Tura przeciwnika"); + } + timerDestination = turnData.timerToUTC; gamePhase = turnData.phase; diff --git a/public/assets/js/spa_lib.js b/public/assets/js/spa_lib.js index 02da062..a3e93ba 100644 --- a/public/assets/js/spa_lib.js +++ b/public/assets/js/spa_lib.js @@ -1,10 +1,10 @@ var activeView; function switchView(viewContainerId, useReplaceState=false) { - $(`.container`).css("opacity", 0); + $(`.container`).css({ opacity: 0, animation: "OutAnim 0.2s 1 ease" }); setTimeout(() => { $(`.container`).css("display", "none"); - $(`.container#${viewContainerId}`).css({"display": "flex", "opacity": "1"}); + $(`.container#${viewContainerId}`).css({ display: "flex", opacity: 1, animation: "InAnim 0.2s 1 ease" }); let path = $(`.container#${viewContainerId}`).data("path"); let title = $(`.container#${viewContainerId}`).data("title"); if (useReplaceState) { @@ -14,7 +14,7 @@ function switchView(viewContainerId, useReplaceState=false) { } activeView = viewContainerId; - }, 150); + }, 200); } function lockUI(doLock) { diff --git a/utils/battleships.js b/utils/battleships.js new file mode 100644 index 0000000..5a0de0a --- /dev/null +++ b/utils/battleships.js @@ -0,0 +1,131 @@ +// export class Client { +// constructor(clientId, clientSecret, redirectUri) { +// this.clientId = clientId; +// this.clientSecret = clientSecret; +// this.redirectUri = redirectUri; +// } +// getAccessToken(code) { +// } +// } + +export class GameInfo { + constructor(redis, io) { + this.redis = redis; + this.io = io; + } + + async isPlayerInGame(socket) { + const game = await this.redis.json.get(`game:${socket.session.activeGame}`); + return game != null; + } + + async getPlayerGameData(socket) { + const game = await this.redis.json.get(`game:${socket.session.activeGame}`); + return game == null ? null : { id: socket.session.activeGame, data: game }; + } + + async endPrepPhase(socket) { + const gameId = socket.session.activeGame; + const key = `game:${gameId}`; + + await this.redis.json.set(key, '$.state', 'action'); + await this.redis.json.set(key, '$.nextPlayer', 0); + + const UTCTs = Math.floor((new Date()).getTime() / 1000 + 30); + this.io.to(gameId).emit('turn update', { turn: 0, phase: "action", timerToUTC: UTCTs }); + } + + async passTurn(socket) { + const gameId = socket.session.activeGame; + const key = `game:${gameId}`; + + await this.redis.json.set(key, '$.state', 'action'); + let nextPlayer = await this.redis.json.get(key, '$.nextPlayer'); + nextPlayer = nextPlayer === 0 ? 1 : 0; + await this.redis.json.set(key, '$.nextPlayer', nextPlayer); + + const UTCTs = Math.floor((new Date()).getTime() / 1000 + 30); + this.io.to(gameId).emit('turn update', { turn: 0, phase: "action", timerToUTC: UTCTs }); + } +} + +export function isPlayerInRoom(socket) { + return !socket.rooms.size === 1; +} + +var lastTimeChange = new Date().getTime(); + +export function timer(time, callback) { + let localLastChange = lastTimeChange; + + let timeout = setTimeout(callback, time * 1000); + + let interval = setInterval(() => { + if (timeout._destroyed) { + // timer is finished, stop monitoring turn changes + clearInterval(interval); + } + if (localLastChange != lastTimeChange) { + // timer has been reset + clearTimeout(timeout); + clearInterval(interval); + } + }, 200); +} + +export function resetTimers() { + lastTimeChange = -lastTimeChange; +} + +// export function getShipsLeft(data, playerIdx) { +// let shipsLeft = [4, 3, 2, 1]; + +// const playerShips = shipsLeft.boards[playerIdx].ships; + +// playerShips.forEach(ship => { +// var isSunk = true; +// ship.hits.every(isHit => { +// isSunk = isHit; +// return isHit; +// }); +// switch (ship.type) { +// case 0: +// shipsLeft[0]--; +// break; + +// default: +// break; +// } +// }); +// } + +export function getShipsAvailable(data, playerIdx) { + let shipsLeft = [4, 3, 2, 1]; + + const playerShips = shipsLeft.boards[playerIdx].ships; + + playerShips.forEach(ship => { + shipsLeft[ship.type]--; + }); + + return shipsLeft; +} + +export function checkShot(data, playerIdx) { + playerIdx = playerIdx === 0 ? 1 : 0 + + data.boards[playerIdx] +} + +export function checkTurn(data, playerId) { + // Check if it's player's turn + if (playerId == data.hostId) { + return data.nextPlayer === 0; + } else { + return data.nextPlayer === 1; + } +} + +// timer(5, () => { +// console.log("out of time"); +// }); \ No newline at end of file diff --git a/views/board.handlebars b/views/board.handlebars index 1ae2aa9..a86c3f6 100644 --- a/views/board.handlebars +++ b/views/board.handlebars @@ -18,7 +18,7 @@

R Obrót statku

B Zamiana planszy

-

Ruch: Przeciwnik

+

diff --git a/views/index.handlebars b/views/index.handlebars index eac4fcd..3fa608f 100644 --- a/views/index.handlebars +++ b/views/index.handlebars @@ -1,6 +1,7 @@ +

Statki