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
This commit is contained in:
MaciejkaG 2024-03-02 18:28:33 +01:00
parent dbb3ad0f1d
commit 72da84c3d6
7 changed files with 210 additions and 54 deletions

View File

@ -3,10 +3,11 @@ import { createServer } from 'node:http';
import { Server } from 'socket.io'; import { Server } from 'socket.io';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4, validate } from 'uuid';
import session from "express-session"; import session from "express-session";
import { engine } from 'express-handlebars'; import { engine } from 'express-handlebars';
import { createClient } from 'redis'; import { createClient } from 'redis';
import * as bships from './utils/battleships.js'
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -27,6 +28,8 @@ redis.on('error', err => console.log('Redis Client Error', err));
await redis.connect(); await redis.connect();
redis.flushDb(); redis.flushDb();
const GInfo = new bships.GameInfo(redis, io);
app.set('trust proxy', 1); app.set('trust proxy', 1);
const sessionMiddleware = session({ const sessionMiddleware = session({
secret: uuidv4(), secret: uuidv4(),
@ -69,7 +72,7 @@ app.get("/game", async (req, res) => {
const game = await redis.json.get(`game:${req.query.id}`); const game = await redis.json.get(`game:${req.query.id}`);
if (req.session.nickname == null) { if (req.session.nickname == null) {
res.redirect("/setup"); 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'); res.status(400).send('badGameId');
} else { } else {
res.render('board'); res.render('board');
@ -89,7 +92,7 @@ io.on('connection', async (socket) => {
return; return;
} }
if (!await isPlayerInGame(socket)) { if (!await GInfo.isPlayerInGame(socket)) {
socket.on('whats my nick', (callback) => { socket.on('whats my nick', (callback) => {
callback(session.nickname); callback(session.nickname);
}); });
@ -132,18 +135,19 @@ io.on('connection', async (socket) => {
// Teraz utwórz objekt partii w trakcie w bazie Redis // Teraz utwórz objekt partii w trakcie w bazie Redis
const gameId = uuidv4(); const gameId = uuidv4();
redis.json.set(`game:${gameId}`, '$', { redis.json.set(`game:${gameId}`, '$', {
hostId: opp.id,
state: "pregame", state: "pregame",
boards: { boards: [
host: { // typ 2 to trójmasztowiec pozycja i obrót na planszy które pola zostały trafione { // 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]} 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? // pozycja na planszy czy strzał miał udział w zatopieniu statku?
shots: [], // zawiera np. {posX: 3, posY: 5, sunk: true} shots: [], // zawiera np. {posX: 3, posY: 5, sunk: true}
}, },
guest: { {
ships: [], ships: [],
shots: [], shots: [],
} }
}, ],
nextPlayer: 0, nextPlayer: 0,
}); });
@ -193,12 +197,12 @@ io.on('connection', async (socket) => {
}); });
socket.on('disconnecting', () => { socket.on('disconnecting', () => {
if (isPlayerInRoom(socket)) { if (bships.isPlayerInRoom(socket)) {
io.to(socket.rooms[1]).emit("player left"); io.to(socket.rooms[1]).emit("player left");
} }
}); });
} else { } else {
const playerGame = await getPlayerGameData(socket); const playerGame = await GInfo.getPlayerGameData(socket);
if (playerGame.data.state === 'pregame') { if (playerGame.data.state === 'pregame') {
socket.join(playerGame.id); socket.join(playerGame.id);
@ -213,22 +217,33 @@ io.on('connection', async (socket) => {
let UTCTs = Math.floor((new Date()).getTime() / 1000 + 90); 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('turn update', { turn: 0, phase: "preparation", timerToUTC: UTCTs });
io.to(playerGame.id).emit(); bships.timer(90, () => GInfo.endPrepPhase(socket));
} }
} }
// socket.on('shoot', async () => { socket.on('place ship', async (type, posX, posY, rot) => {
// const playerGame = await getPlayerGameData(socket); 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 () => { socket.on('disconnecting', async () => {
io.to(playerGame.id).emit("player left"); const playerGame = await GInfo.getPlayerGameData(socket);
if (playerGame !== null) {
redis.json.del(`game:${playerGame.id}`); AFKEnd(playerGame.id);
}
}); });
} }
}); });
@ -241,35 +256,32 @@ function genID() {
return Math.floor(100000 + Math.random() * 900000).toString(); return Math.floor(100000 + Math.random() * 900000).toString();
} }
// async function emitToParty(partyuuid) { function resetUserGame(req) {
// const party = gameData.find((element) => element.partyId===partyuuid); req.session.reload((err) => {
if (err) return socket.disconnect();
// if (party!==null) { req.session.activeGame = null;
// party.members.forEach(socketId => { req.session.save();
});
// io.to(socketId).emit();
// });
// }
// }
function isPlayerInRoom(socket) {
return !socket.rooms.size === 1;
} }
async function isPlayerInGame(socket) { function endGame(gameId) {
const game = await redis.json.get(`game:${socket.session.activeGame}`); const members = [...roomMemberIterator(gameId)];
return game != null; 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) { function AFKEnd(gameId) {
const game = await redis.json.get(`game:${socket.session.activeGame}`); io.to(gameId).emit("player left");
return game == null ? null : {id: socket.session.activeGame, data: game}; endGame(gameId);
} }
function roomMemberIterator(id) { function roomMemberIterator(id) {
return io.sockets.adapter.rooms.get(id).entries(); return io.sockets.adapter.rooms.get(id).entries();
} }
function getPlayerRoom(socket) {
return socket.rooms.entries()[1] === undefined ? null : socket.rooms.entries()[1][0];
}

View File

@ -1,9 +1,20 @@
body {
transition: opacity 0.1s;
}
.container { .container {
display: none; 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 */ /* index */

View File

@ -18,7 +18,12 @@ socket.on("player idx", (idx) => {
}); });
socket.on('turn update', (turnData) => { 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; timerDestination = turnData.timerToUTC;
gamePhase = turnData.phase; gamePhase = turnData.phase;

View File

@ -1,10 +1,10 @@
var activeView; var activeView;
function switchView(viewContainerId, useReplaceState=false) { function switchView(viewContainerId, useReplaceState=false) {
$(`.container`).css("opacity", 0); $(`.container`).css({ opacity: 0, animation: "OutAnim 0.2s 1 ease" });
setTimeout(() => { setTimeout(() => {
$(`.container`).css("display", "none"); $(`.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 path = $(`.container#${viewContainerId}`).data("path");
let title = $(`.container#${viewContainerId}`).data("title"); let title = $(`.container#${viewContainerId}`).data("title");
if (useReplaceState) { if (useReplaceState) {
@ -14,7 +14,7 @@ function switchView(viewContainerId, useReplaceState=false) {
} }
activeView = viewContainerId; activeView = viewContainerId;
}, 150); }, 200);
} }
function lockUI(doLock) { function lockUI(doLock) {

131
utils/battleships.js Normal file
View File

@ -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");
// });

View File

@ -18,7 +18,7 @@
<h3 class="controlsOwnBoard"><span class="important">R</span> Obrót statku</h3> <h3 class="controlsOwnBoard"><span class="important">R</span> Obrót statku</h3>
<h3><span class="important">B</span> Zamiana planszy</h3> <h3><span class="important">B</span> Zamiana planszy</h3>
<span class="break"></span> <span class="break"></span>
<h3>Ruch: <span class="dynamic" id="whosTurn">Przeciwnik</span></h3> <h3><span class="dynamic" id="whosTurn"></span></h3>
<h2 class="important" id="timer">∞</h2> <h2 class="important" id="timer">∞</h2>
</div> </div>
<div class="boardContainer"> <div class="boardContainer">

View File

@ -1,6 +1,7 @@
<h1 class="header">Statki</h1>
<div class="container" id="mainMenuView" data-title="Statki" data-path="/"> <div class="container" id="mainMenuView" data-title="Statki" data-path="/">
<div> <div>
<h1>Statki</h1>
<h2>Wybierz tryb gry</h2> <h2>Wybierz tryb gry</h2>
<div class="modes"> <div class="modes">
<div id="pvpMenuButton"> <div id="pvpMenuButton">
@ -17,7 +18,6 @@
<div class="container" id="pvpMenuView" data-title="Statki / PvP" data-path="/pvp"> <div class="container" id="pvpMenuView" data-title="Statki / PvP" data-path="/pvp">
<div> <div>
<h1>Statki</h1>
<h2>PvP</h2> <h2>PvP</h2>
<div class="modes"> <div class="modes">
<div id="createGameButton"> <div id="createGameButton">
@ -34,7 +34,6 @@
<div class="container" id="pvpCreateView" data-title="Statki / PvP / Stwórz" data-path="/pvp/create"> <div class="container" id="pvpCreateView" data-title="Statki / PvP / Stwórz" data-path="/pvp/create">
<div> <div>
<h1>Statki</h1>
<h2>PvP / Stwórz</h2> <h2>PvP / Stwórz</h2>
<div class="modes"> <div class="modes">
<div> <div>
@ -49,7 +48,6 @@
<div class="container" id="pvpJoinView" data-title="Statki / PvP / Dołącz" data-path="/pvp/join"> <div class="container" id="pvpJoinView" data-title="Statki / PvP / Dołącz" data-path="/pvp/join">
<div> <div>
<h1>Statki</h1>
<h2>PvP / Dołącz</h2> <h2>PvP / Dołącz</h2>
<div class="modes"> <div class="modes">
<div> <div>
@ -64,7 +62,6 @@
<div class="container" id="preparingGame" data-title="Statki / PvP / Przygotowywanie" data-path="/pvp/prepairing"> <div class="container" id="preparingGame" data-title="Statki / PvP / Przygotowywanie" data-path="/pvp/prepairing">
<div> <div>
<h1>Statki</h1>
<h2>PvP / Wczytywanie</h2> <h2>PvP / Wczytywanie</h2>
<div class="modes"> <div class="modes">
<div> <div>