Optimisations + lisibilité

This commit is contained in:
Sébastien Rivière
2025-06-18 02:21:32 +02:00
parent c6066bc234
commit 4fd73a35c8
9 changed files with 153 additions and 187 deletions

3
.gitignore vendored
View File

@@ -127,3 +127,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Other
.vscode/

View File

@@ -8,11 +8,10 @@ import game from "./game.js"
import zone from "./zone_manager.js"
import penaltyController from "./penalty_controller.js";
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { sha256 } from "./util.js";
import { createHash } from "crypto";
import { config } from "dotenv";
config()
config();
const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH;
/**
@@ -26,18 +25,18 @@ export function secureAdminBroadcast(event, data) {
});
}
//Array of logged in sockets
// Array of logged in sockets
let loggedInSockets = [];
export function initAdminSocketHandler() {
//Admin namespace
// Admin namespace
io.of("admin").on("connection", (socket) => {
//Flag to check if the user is logged in, defined for each socket
// Flag to check if the user is logged in, defined for each socket
console.log("Connection of an admin");
let loggedIn = false;
socket.on("disconnect", () => {
console.log("Disconnection of an admin");
//Remove the socket from the logged in sockets array
// Remove the socket from the logged in sockets array
loggedInSockets = loggedInSockets.filter(s => s !== socket.id);
});
@@ -45,17 +44,17 @@ export function initAdminSocketHandler() {
loggedInSockets = loggedInSockets.filter(s => s !== socket.id);
})
//User is attempting to log in
// User is attempting to log in
socket.on("login", (password) => {
const hash = sha256(password);
const hash = createHash('sha256').update(password).digest('hex');
if (hash === ADMIN_PASSWORD_HASH && !loggedIn) {
//Attempt successful
// Attempt successful
socket.emit("login_response", true);
loggedInSockets.push(socket.id);
loggedIn = true;
//Send the current state
// Send the current state
socket.emit("game_state", game.state)
//Other settings that need initialization
// Other settings that need initialization
socket.emit("penalty_settings", penaltyController.settings)
socket.emit("game_settings", game.settings)
socket.emit("zone_settings", zone.zoneSettings)
@@ -64,9 +63,8 @@ export function initAdminSocketHandler() {
begin: zone.currentStartZone,
end: zone.nextZone
})
} else {
//Attempt unsuccessful
// Attempt unsuccessful
socket.emit("login_response", false);
}
});
@@ -90,7 +88,7 @@ export function initAdminSocketHandler() {
}
if (!game.setZoneSettings(settings)) {
socket.emit("error", "Error changing zone");
socket.emit("zone_settings", zone.zoneSettings) //Still broadcast the old config to the client who submited an incorrect config to keep the client up to date
socket.emit("zone_settings", zone.zoneSettings) // Still broadcast the old config to the client who submited an incorrect config to keep the client up to date
} else {
secureAdminBroadcast("zone_settings", zone.zoneSettings)
}
@@ -111,7 +109,7 @@ export function initAdminSocketHandler() {
})
//User is attempting to add a new team
// User is attempting to add a new team
socket.on("add_team", (teamName) => {
if (!loggedIn) {
socket.emit("error", "Not logged in");
@@ -124,7 +122,7 @@ export function initAdminSocketHandler() {
}
});
//User is attempting to remove a team
// User is attempting to remove a team
socket.on("remove_team", (teamId) => {
if (!loggedIn) {
socket.emit("error", "Not logged in");
@@ -137,7 +135,7 @@ export function initAdminSocketHandler() {
}
});
//User is attempting to change the game state
// User is attempting to change the game state
socket.on("change_state", (state) => {
if (!loggedIn) {
socket.emit("error", "Not logged in");
@@ -151,9 +149,9 @@ export function initAdminSocketHandler() {
}
});
//Use is sending a new list containing the new order of the teams
//Note that we never check if the new order contains the same teams as the old order, so it behaves more like a setTeams function
//But the frontend should always send the same teams in a different order
// Use is sending a new list containing the new order of the teams
// Note that we never check if the new order contains the same teams as the old order, so it behaves more like a setTeams function
// But the frontend should always send the same teams in a different order
socket.on("reorder_teams", (newOrder) => {
if (!loggedIn) {
socket.emit("error", "Not logged in");
@@ -179,8 +177,8 @@ export function initAdminSocketHandler() {
}
})
//Request an update of the team list
//We only reply to the sender to prevent spam
// Request an update of the team list
// We only reply to the sender to prevent spam
socket.on("get_teams", () => {
if (!loggedIn) {
socket.emit("error", "Not logged in");
@@ -188,8 +186,5 @@ export function initAdminSocketHandler() {
}
socket.emit("teams", game.teams);
});
});
}
}

View File

@@ -2,14 +2,11 @@
This module manages the main game state, the teams, the settings and the game logic
*/
import { secureAdminBroadcast } from "./admin_socket.js";
import { isInCircle } from "./map_utils.js";
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { isInCircle } from "./map_utils.js";
import timeoutHandler from "./timeoutHandler.js";
import penaltyController from "./penalty_controller.js";
import zoneManager from "./zone_manager.js";
import { getDistanceFromLatLon } from "./map_utils.js";
import { writePosition, writeCapture, writeSeePosition } from "./trajectory.js";
/**
@@ -32,7 +29,7 @@ export default {
loserEndGameMessage: "",
winnerEndGameMessage: "",
capturedMessage: "",
waitingMessage: "Jeu en préparation, veuillez patienter."
waitingMessage: ""
},
/**
@@ -106,7 +103,7 @@ export default {
* @returns a random 4 digit number
*/
createCaptureCode() {
return Math.floor(Math.random() * 10000)
return Math.floor(Math.random() * 10000);
},
/**
@@ -115,9 +112,8 @@ export default {
* @returns true if the team has been added
*/
addTeam(teamName) {
let id = this.getNewTeamId();
this.teams.push({
id: id,
id: this.getNewTeamId(),
name: teamName,
chasing: null,
chased: null,
@@ -160,7 +156,7 @@ export default {
updateTeamChasing() {
if (this.playingTeamCount() <= 2) {
if (this.state == GameState.PLAYING) {
this.finishGame()
this.finishGame();
}
return false;
}
@@ -175,13 +171,13 @@ export default {
} else {
firstTeam = this.teams[i].id;
}
previousTeam = this.teams[i].id
previousTeam = this.teams[i].id;
}
}
this.getTeam(firstTeam).chased = previousTeam;
this.getTeam(previousTeam).chasing = firstTeam;
this.getTeam(previousTeam).enemyName = this.getTeam(firstTeam).name;
secureAdminBroadcast("teams", this.teams)
secureAdminBroadcast("teams", this.teams);
return true;
},
@@ -213,7 +209,7 @@ export default {
updateTeam(teamId, newTeam) {
this.teams = this.teams.map((t) => {
if (t.id == teamId) {
return { ...t, ...newTeam }
return { ...t, ...newTeam };
} else {
return t;
}
@@ -230,22 +226,20 @@ export default {
* @returns true if the location has been updated
*/
updateLocation(teamId, location) {
let team = this.getTeam(teamId);
//The ID does not match any team
if (team == undefined) {
return false;
}
//The location sent by the team will be null if the browser call API dooes not succeed
//See issue #19
if (location == null) {
const team = this.getTeam(teamId);
if (!team || !location) {
return false;
}
// Update of events of the game
writePosition(Date.now(), teamId, location[0], location[1]);
// Update of currentLocation
team.currentLocation = location;
//Update the team ready status if they are in their starting area
// Update of ready (true if the team is in the starting area)
if (this.state == GameState.PLACEMENT && team.startingArea && team.startingArea && location) {
team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius)
team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius);
}
// Sending new infos to the team
sendUpdatedTeamInformations(team.id);
return true;
},
@@ -253,14 +247,14 @@ export default {
* Initialize the last sent location of the teams to their starting location
*/
initLastSentLocations() {
for (let team of this.teams) {
for (const team of this.teams) {
team.lastSentLocation = team.currentLocation;
team.locationSendDeadline = Date.now() + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline);
this.getTeam(team.chasing).enemyLocation = team.lastSentLocation;
sendUpdatedTeamInformations(team.id);
}
for (let team of this.teams) {
for (const team of this.teams) {
team.enemyLocation = this.getTeam(team.chasing).lastSentLocation;
sendUpdatedTeamInformations(team.id);
}
@@ -272,22 +266,24 @@ export default {
* @returns true if the location has been sent
*/
sendLocation(teamId) {
let team = this.getTeam(teamId);
if (team == undefined) {
const team = this.getTeam(teamId);
if (!team || !team.currentLocation) {
return false;
}
if (team.currentLocation == null) {
return false;
}
writeSeePosition(Date.now(), teamId, team.chasing);
team.locationSendDeadline = Date.now() + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
const dateNow = Date.now();
// Update of events of the game
writeSeePosition(dateNow, teamId, team.chasing);
// Update of locationSendDeadline
team.locationSendDeadline = dateNow + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline);
// Update of lastSentLocation
team.lastSentLocation = team.currentLocation;
if (this.getTeam(team.chasing) != null) {
team.enemyLocation = this.getTeam(team.chasing).lastSentLocation;
}
// Update of enemyLocation
const teamChasing = this.getTeam(team.chasing);
if (teamChasing) team.enemyLocation = teamChasing.lastSentLocation;
// Sending new infos to the team
sendUpdatedTeamInformations(team.id);
return team;
return true;
},
/**
@@ -296,10 +292,9 @@ export default {
* @returns true if the team has been deleted
*/
removeTeam(teamId) {
if (this.getTeam(teamId) == undefined) {
if (!this.getTeam(teamId)) {
return false;
}
//remove the team from the list
this.teams = this.teams.filter(t => t.id !== teamId);
this.updateTeamChasing();
timeoutHandler.endSendPositionTimeout(team.id);
@@ -315,14 +310,19 @@ export default {
* @returns {Boolean} if the capture has been successfull or not
*/
requestCapture(teamId, captureCode) {
let enemyTeam = this.getTeam(this.getTeam(teamId).chasing)
if (enemyTeam && enemyTeam.captureCode == captureCode) {
writeCapture(Date.now(), teamId, enemyTeam.id);
this.capture(enemyTeam.id);
this.updateTeamChasing();
return true;
const team = this.getTeam(teamId);
const enemyTeam = this.getTeam(team.chasing);
if (!enemyTeam || enemyTeam.captureCode != captureCode) {
return false;
}
return false;
// Update of events of the game
writeCapture(Date.now(), teamId, enemyTeam.id);
// Update of capture and chasing cycle
this.capture(enemyTeam.id);
// Sending new infos to the teams
sendUpdatedTeamInformations(team.id);
sendUpdatedTeamInformations(enemyTeam.id);
return true;
},
/**
@@ -330,7 +330,7 @@ export default {
* @param {Number} teamId the Id of the captured team
*/
capture(teamId) {
this.getTeam(teamId).captured = true
this.getTeam(teamId).captured = true;
timeoutHandler.endSendPositionTimeout(teamId);
this.updateTeamChasing();
},
@@ -349,11 +349,10 @@ export default {
var min = newSettings.min;
var max = newSettings.max;
// The end zone must be included in the start zone
var dist = getDistanceFromLatLon(min.center, max.center);
if (min.radius + dist >= max.radius) {
if (!isInCircle(min.center, max.center, max.radius-min.radius)) {
return false;
}
return zoneManager.udpateSettings(newSettings)
return zoneManager.udpateSettings(newSettings);
},
/**
@@ -362,7 +361,7 @@ export default {
finishGame() {
this.setState(GameState.FINISHED);
zoneManager.reset();
timeoutHandler.endAllSendPositionTimeout()
timeoutHandler.endAllSendPositionTimeout();
playersBroadcast("game_state", this.state);
},
}

View File

@@ -1,19 +1,17 @@
import { createServer } from "http";
import express from "express";
import { Server } from "socket.io";
import { config } from "dotenv";
import { readFileSync } from "fs";
import { initAdminSocketHandler } from "./admin_socket.js";
import { initTeamSocket } from "./team_socket.js";
import { initPhotoUpload } from "./photo.js";
import { initTrajectories } from "./trajectory.js";
//extract admin password from .env file
config();
const HOST = process.env.HOST;
const PORT = process.env.PORT;
export const app = express()
export const app = express();
const httpServer = createServer({}, app);
@@ -21,9 +19,6 @@ httpServer.listen(PORT, HOST, () => {
console.log("Server running on http://" + HOST + ":" + PORT);
});
//set cors to allow all origins
export const io = new Server(httpServer, {
cors: {
origin: "*",

View File

@@ -1,3 +1,12 @@
/**
* Convert a angle from degree to radian
* @param {Number} deg angle in degree
* @returns angle in radian
*/
function degToRad(deg) {
return deg * (Math.PI / 180);
}
/**
* Compute the distance between two points givent their longitude and latitude
* @param {Object} pos1 The first position
@@ -5,13 +14,13 @@
* @returns the distance between the two positions in meters
* @see https://gist.github.com/miguelmota/10076960
*/
export function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
var R = 6371; // Radius of the earth in km
var dLat = deg2rad(lat2 - lat1); // deg2rad below
var dLon = deg2rad(lon2 - lon1);
var dLat = degToRad(lat2 - lat1);
var dLon = degToRad(lon2 - lon1);
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
;
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
@@ -19,15 +28,6 @@ export function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng
return d * 1000;
}
/**
* Convert a angle from degree to radian
* @param {Number} deg angle in degree
* @returns angle in radian
*/
function deg2rad(deg) {
return deg * (Math.PI / 180)
}
/**
* Check if a GPS point is in a circle
* @param {Object} position The position to check, an object with lat and lng fields
@@ -37,4 +37,4 @@ function deg2rad(deg) {
*/
export function isInCircle(position, center, radius) {
return getDistanceFromLatLon(position, center) < radius;
}
}

View File

@@ -13,7 +13,7 @@ const ALLOWED_MIME = [
"image/gif"
]
//Setup multer (the file upload middleware)
// Setup multer (the file upload middleware)
const storage = multer.diskStorage({
// Save the file in the uploads directory
destination: function (req, file, callback) {
@@ -27,7 +27,7 @@ const storage = multer.diskStorage({
const upload = multer({
storage,
//Only upload the file if it is a valid mime type and the team POST parameter is a valid team
// Only upload the file if it is a valid mime type and the team POST parameter is a valid team
fileFilter: function (req, file, callback) {
if (ALLOWED_MIME.indexOf(file.mimetype) == -1) {
callback(null, false);
@@ -39,8 +39,7 @@ const upload = multer({
}
})
//Clean the uploads directory
// Clean the uploads directory
function clean() {
const files = fs.readdirSync(UPLOAD_DIR);
for (const file of files) {
@@ -49,7 +48,6 @@ function clean() {
}
}
export function initPhotoUpload() {
clean();
//App handler for uploading a photo and saving it to a file

View File

@@ -15,8 +15,8 @@ import zone from "./zone_manager.js";
* @param {*} data The payload
*/
export function teamBroadcast(teamId, event, data) {
for (let socketId of game.getTeam(teamId).sockets) {
io.of("player").to(socketId).emit(event, data)
for (const socketId of game.getTeam(teamId).sockets) {
io.of("player").to(socketId).emit(event, data);
}
}
@@ -26,25 +26,19 @@ export function teamBroadcast(teamId, event, data) {
* @param {String} data payload
*/
export function playersBroadcast(event, data) {
for (let team of game.teams) {
for (const team of game.teams) {
teamBroadcast(team.id, event, data);
}
}
/**
* Remove a player from the list of logged in players
* @param {Number} id The id of the player to log out
* Send a socket message to all the players of a team
* @param {String} teamId The team that will receive the message
*/
function logoutPlayer(id) {
for (let team of game.teams) {
team.sockets = team.sockets.filter((sid) => sid != id);
}
}
export function sendUpdatedTeamInformations(teamId) {
let team = game.getTeam(teamId)
const team = game.getTeam(teamId);
if (!team) {
return false;
return;
}
team.sockets.forEach(socketId => {
io.of("player").to(socketId).emit("update_team", {
@@ -61,6 +55,17 @@ export function sendUpdatedTeamInformations(teamId) {
penalties: team.penalties,
})
})
secureAdminBroadcast("teams", game.teams);
}
/**
* Remove a player from the list of logged in players
* @param {Number} id The id of the player to log out
*/
function logoutPlayer(id) {
for (const team of game.teams) {
team.sockets = team.sockets.filter((sid) => sid != id);
}
}
export function initTeamSocket() {
@@ -70,37 +75,33 @@ export function initTeamSocket() {
socket.on("disconnect", () => {
console.log("Disconnection of a player");
logoutPlayer(socket.id)
logoutPlayer(socket.id);
});
socket.on("login", (loginTeamId, callback) => {
let team = game.getTeam(loginTeamId);
if (team === undefined) {
socket.emit("login_response", false);
if (typeof callback === "function") {
callback({ isLoggedIn: false, message: "Login denied" });
}
} else {
logoutPlayer(socket.id)
team.sockets.push(socket.id);
teamId = loginTeamId;
sendUpdatedTeamInformations(loginTeamId);
socket.emit("login_response", true);
socket.emit("game_state", game.state)
socket.emit("game_settings", game.settings)
socket.emit("zone", zone.currentZone)
socket.emit("new_zone", {
begin: zone.currentStartZone,
end: zone.nextZone
})
if (typeof callback === "function") {
callback({ isLoggedIn : true, message: "Logged in"});
}
const team = game.getTeam(loginTeamId);
if (!team) {
callback({ isLoggedIn: false, message: "Login denied" });
return;
}
logoutPlayer(socket.id);
team.sockets.push(socket.id);
teamId = loginTeamId;
sendUpdatedTeamInformations(loginTeamId);
socket.emit("login_response", true);
socket.emit("game_state", game.state);
socket.emit("game_settings", game.settings);
socket.emit("zone", zone.currentZone);
socket.emit("new_zone", {
begin: zone.currentStartZone,
end: zone.nextZone
})
callback({ isLoggedIn : true, message: "Logged in"});
});
socket.on("logout", () => {
logoutPlayer(socket.id);
teamId = null;
})
socket.on("update_position", (position) => {
@@ -108,48 +109,30 @@ export function initTeamSocket() {
// This is done to prevent multiple clients from sending slightly different prosition back and forth
// Making the point jitter on the map
if (!teamId) {
socket.emit("error", "not logged in yet");
return;
}
let team = game.getTeam(teamId)
if (team == undefined) {
logoutPlayer(socket.id);
return;
}
const team = game.getTeam(teamId);
if (team.sockets.indexOf(socket.id) == 0) {
game.updateLocation(teamId, position);
teamBroadcast(teamId, "update_team", { currentLocation: team.currentLocation, ready: team.ready });
secureAdminBroadcast("teams", game.teams);
}
});
socket.on("send_position", () => {
game.sendLocation(teamId);
let team = game.getTeam(teamId);
if (team === undefined) {
socket.emit("error", "Team not found");
if (!teamId) {
return;
}
game.updateTeamChasing();
teamBroadcast(teamId, "update_team", { enemyLocation: team.enemyLocation, locationSendDeadline: team.locationSendDeadline, lastSentLocation: team.lastSentLocation });
secureAdminBroadcast("teams", game.teams)
game.sendLocation(teamId);
});
socket.on("capture", (captureCode, callback) => {
let capturedTeam = game.getTeam(teamId)?.chasing;
if (capturedTeam !== undefined && game.requestCapture(teamId, captureCode)) {
sendUpdatedTeamInformations(teamId);
sendUpdatedTeamInformations(capturedTeam);
secureAdminBroadcast("teams", game.teams);
if (typeof callback === "function") {
callback({ hasCaptured : true, message: "Capture successful" });
}
} else {
socket.emit("error", "Incorrect code");
if (typeof callback === "function") {
callback({ hasCaptured : false, message: "Capture failed" });
}
if (!teamId) {
return;
}
if (!game.requestCapture(teamId, captureCode)) {
callback({ hasCaptured : false, message: "Capture failed" });
return;
}
callback({ hasCaptured : true, message: "Capture successful" });
})
});
}

View File

@@ -1,19 +0,0 @@
import { createHash } from "crypto";
/**
* Scale a value that is known to be in a range to a new range
* for instance map(50,0,100,1000,2000) will return 1500 as 50 is halfway between 0 and 100 and 1500 is halfway through 1000 and 2000
* @param {Number} value value to map
* @param {Number} oldMin minimum value of the number
* @param {Number} oldMax maximum value of the number
* @param {Number} newMin minimum value of the output
* @param {Number} newMax maximum value of the output
* @returns
*/
export function map(value, oldMin, oldMax, newMin, newMax) {
return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin;
}
export function sha256(password) {
return createHash('sha256').update(password).digest('hex');;
}

View File

@@ -1,13 +1,25 @@
/*
This module manages the play area during the game, shrinking it over time based of some settings.
*/
import { randomCirclePoint } from 'random-location'
import { isInCircle } from './map_utils.js';
import { map } from './util.js';
import { playersBroadcast } from './team_socket.js';
import { secureAdminBroadcast } from './admin_socket.js';
/**
* Scale a value that is known to be in a range to a new range
* for instance map(50,0,100,1000,2000) will return 1500 as 50 is halfway between 0 and 100 and 1500 is halfway through 1000 and 2000
* @param {Number} value value to map
* @param {Number} oldMin minimum value of the number
* @param {Number} oldMax maximum value of the number
* @param {Number} newMin minimum value of the output
* @param {Number} newMax maximum value of the output
* @returns
*/
function map(value, oldMin, oldMax, newMin, newMax) {
return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin;
}
export default {
//Setings storing where the zone will start, end and how it should evolve
//The zone will start by staying at its max value for reductionInterval minutes