diff --git a/traque-back/Dockerfile b/traque-back/Dockerfile index 762ad6d..b7966f7 100644 --- a/traque-back/Dockerfile +++ b/traque-back/Dockerfile @@ -13,8 +13,5 @@ RUN npm install # Copy the rest of project files into this image COPY . . -# Expose application port -EXPOSE 3001 - # Start the application CMD npm start diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js index b57e2c5..ffdf7a7 100644 --- a/traque-back/admin_socket.js +++ b/traque-back/admin_socket.js @@ -1,3 +1,8 @@ +/* +This module manages the admin access to the server via websocket. +It receives messages, checks permissions, manages authentication and performs actions by calling functions from other modules. +This module also exposes functions to send messages via socket to all admins +*/ import { io } from "./index.js"; import game from "./game.js" import zone from "./zone_manager.js" diff --git a/traque-back/game.js b/traque-back/game.js index c6e082d..9078d65 100644 --- a/traque-back/game.js +++ b/traque-back/game.js @@ -1,3 +1,6 @@ +/* +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"; @@ -5,6 +8,9 @@ import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js" import penaltyController from "./penalty_controller.js"; import zoneManager from "./zone_manager.js"; +/** + * The possible states of the game + */ export const GameState = { SETUP: "setup", PLACEMENT: "placement", @@ -13,8 +19,11 @@ export const GameState = { } export default { + //List of teams, as objects. To see the fields see the addTeam methods teams : [], + //Current state of the game state : GameState.SETUP, + //Settings of the game settings : { loserEndGameMessage: "", winnerEndGameMessage: "", @@ -22,11 +31,21 @@ export default { waitingMessage: "Jeu en préparation, veuillez patienter." }, + /** + * Update the game settings + * @param {Object} newSettings settings to be updated, can be partial + * @returns true if the settings are applied + */ changeSettings(newSettings) { this.settings = {...this.settings, ...newSettings}; return true; }, + /** + * Change the state of the game to newState and start the necessary processes + * @param {String} newState + * @returns true if the state has been changed + */ setState(newState) { if (Object.values(GameState).indexOf(newState) == -1) { return false; @@ -65,6 +84,10 @@ export default { return true; }, + /** + * Get a new unused team id + * @returns a new unique team id + */ getNewTeamId() { let id = null; while (id === null || this.teams.find(t => t.id === id)) { @@ -73,10 +96,19 @@ export default { return id; }, + /** + * Return a random capture code + * @returns a random 4 digit number + */ createCaptureCode() { return Math.floor(Math.random() * 10000) }, + /** + * Add a new team to the game + * @param {String} teamName the name of the team + * @returns true if the team has been added + */ addTeam(teamName) { let id = this.getNewTeamId(); this.teams.push({ @@ -100,6 +132,10 @@ export default { return true; }, + /** + * Count the number of teams that are not captured + * @returns the number of teams that are not captured + */ playingTeamCount() { let res = 0; this.teams.forEach((t) => { @@ -110,6 +146,12 @@ export default { return res; }, + /** + * Update the chasing chain of the teams based of the ordre of the teams array + * If there are only 2 teams left, the game will end + * This function will update the enemyName, chasing and chased values of each teams + * @returns true if successful + */ updateTeamChasing() { if (this.playingTeamCount() <= 2) { if (this.state == GameState.PLAYING) { @@ -138,15 +180,31 @@ export default { return true; }, + /** + * Rearrange the order of the teams and update the chasing chain + * @param {Array} newOrder An array of teams in the new order + * @returns + */ reorderTeams(newOrder) { this.teams = newOrder; return this.updateTeamChasing(); }, + /** + * Get a team by its ID + * @param {Number} teamId The id of the team + * @returns the team object or undefined if not found + */ getTeam(teamId) { return this.teams.find(t => t.id === teamId); }, + /** + * Update a team's values + * @param {Number} teamId The id of the team to update + * @param {Object} newTeam An object containing the new values of the team, can be partial + * @returns true if the team has been updated + */ updateTeam(teamId, newTeam) { this.teams = this.teams.map((t) => { if (t.id == teamId) { @@ -160,11 +218,20 @@ export default { return true; }, + /** + * + * @param {Number} teamId The ID of the team which location will be updated + * @param {Array} location An array containing in order the latitude and longitude of the new location + * @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) { return false; } @@ -176,8 +243,9 @@ export default { return true; }, - //Make it so that when a team requests the location of a team that has never sent their locaiton - //Their position at the begining of the game is sent + /** + * Initialize the last sent location of the teams to their starting location + */ initLastSentLocations() { for (let team of this.teams) { team.lastSentLocation = team.currentLocation; @@ -186,6 +254,11 @@ export default { } }, + /** + * Get the most recent enemy team's location as well as setting the latest accessible location to the current one + * @param {Number} teamId The ID of the team that will send its location + * @returns true if the location has been sent + */ sendLocation(teamId) { let team = this.getTeam(teamId); if (team == undefined) { @@ -203,6 +276,11 @@ export default { return team; }, + /** + * Remove a team by its ID + * @param {Number} teamId The id of the team to remove + * @returns true if the team has been deleted + */ removeTeam(teamId) { if (this.getTeam(teamId) == undefined) { return false; @@ -254,6 +332,9 @@ export default { return zoneManager.udpateSettings(newSettings) }, + /** + * Set the game state as finished, as well as resetting the zone manager + */ finishGame() { this.setState(GameState.FINISHED); zoneManager.reset(); diff --git a/traque-back/map_utils.js b/traque-back/map_utils.js index bd55cff..1af35a1 100644 --- a/traque-back/map_utils.js +++ b/traque-back/map_utils.js @@ -1,3 +1,10 @@ +/** + * Compute the distance between two points givent their longitude and latitude + * @param {Object} pos1 The first position + * @param {Object} pos2 The second position + * @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 }) { var R = 6371; // Radius of the earth in km var dLat = deg2rad(lat2 - lat1); // deg2rad below @@ -12,10 +19,22 @@ 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 + * @param {Object} center The center of the circle, an object with lat and lng fields + * @param {Number} radius The radius of the circle in meters + * @returns + */ export function isInCircle(position, center, radius) { return getDistanceFromLatLon(position, center) < radius; } \ No newline at end of file diff --git a/traque-back/penalty_controller.js b/traque-back/penalty_controller.js index c6e20fa..2304518 100644 --- a/traque-back/penalty_controller.js +++ b/traque-back/penalty_controller.js @@ -1,3 +1,6 @@ +/* +This module manages the verification of the game rules and the penalties. +*/ import { isInCircle } from "./map_utils.js"; import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js"; import { secureAdminBroadcast } from "./admin_socket.js"; @@ -5,15 +8,22 @@ import game, {GameState} from "./game.js"; import zone from "./zone_manager.js"; export default { + // Object mapping team id to the date they left the zone as a UNIX millisecond timestamp outOfBoundsSince: {}, + // Id of the interval checking the rules checkIntervalId: null, settings: { + //Time in minutes before a team is penalized for not updating their position allowedTimeOutOfZone: 10, + //Time in minutes before a team is penalized for not updating their position allowedTimeBetweenPositionUpdate: 10, //Number of penalties needed to be eliminated maxPenalties: 3 }, + /** + * Start the penalty controller, watch the team positions and apply penalties if necessary + */ start() { this.outOfBoundsSince = {}; if (this.checkIntervalId) { @@ -28,6 +38,9 @@ export default { }, 100); }, + /** + * Stop the penalty controller + */ stop() { this.outOfBoundsSince = {}; if (this.checkIntervalId) { @@ -36,6 +49,11 @@ export default { } }, + /** + * Update the penalty controller settings + * @param {Object} newSettings the object containing the settings to be udpated, can be partial + * @returns true if the settings were updated, false otherwise + */ updateSettings(newSettings) { //Sanitize input if (newSettings.maxPenalties && (isNaN(parseInt(newSettings.maxPenalties)) || newSettings.maxPenalties < 0)) { return false } @@ -72,6 +90,10 @@ export default { secureAdminBroadcast("teams", game.teams) }, + /** + * Check if each team has too many penalties and eliminate them if necessary + * Also send a socket message to the team and the chased team + */ checkPenalties() { for (let team of game.teams) { if (team.penalties >= this.settings.maxPenalties) { @@ -84,6 +106,11 @@ export default { } }, + /** + * Watch the position of each team and apply penalties if necessary + * If a team is out of the zone, a warning will be sent to them + * A warning is also sent one minute before the penalty is applied + */ watchZone() { game.teams.forEach((team) => { if (team.captured) { return } @@ -110,7 +137,10 @@ export default { }) }, - + /** + * Watch the date of the last position update of each team and apply penalties if necessary + * Also send a message one minute before the penalty is applied + */ watchPositionUpdate() { game.teams.forEach((team) => { //If the team has not sent their location for more than the allowed period, automatically send it and add a penalty diff --git a/traque-back/photo.js b/traque-back/photo.js index 5ff5f6d..bfdf629 100644 --- a/traque-back/photo.js +++ b/traque-back/photo.js @@ -1,3 +1,6 @@ +/* +This module manages the handler for uploading photos, as well as serving the correct file on requests based on the team ID and current game state +*/ import { app } from "./index.js"; import multer from "multer"; import fs from "fs"; @@ -10,27 +13,21 @@ const ALLOWED_MIME = [ "image/gif" ] +//Setup multer (the file upload middleware) const storage = multer.diskStorage({ + // Save the file in the uploads directory destination: function (req, file, callback) { callback(null, UPLOAD_DIR); }, + // Save the file with the team ID as the filename filename: function (req, file, callback) { callback(null, req.query.team); } }); -function clean() { - - const files = fs.readdirSync(UPLOAD_DIR); - - for (const file of files) { - const filePath = path.join(UPLOAD_DIR, file); - fs.unlinkSync(filePath); - } -} - const upload = multer({ storage, + //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); @@ -43,13 +40,25 @@ const upload = multer({ }) +//Clean the uploads directory +function clean() { + const files = fs.readdirSync(UPLOAD_DIR); + for (const file of files) { + const filePath = path.join(UPLOAD_DIR, file); + fs.unlinkSync(filePath); + } +} + + export function initPhotoUpload() { clean(); + //App handler for uploading a photo and saving it to a file app.post("/upload", upload.single('file'), (req, res) => { res.set("Access-Control-Allow-Origin", "*"); console.log("upload", req.query) res.send("") }) + //App handler for serving the photo of a team given its secret ID app.get("/photo/my", (req, res) => { let team = game.getTeam(Number(req.query.team)); if (team) { @@ -60,6 +69,7 @@ export function initPhotoUpload() { res.send(400, "Team not found") } }) + //App handler for serving the photo of the team chased by the team given by its secret ID app.get("/photo/enemy", (req, res) => { let team = game.getTeam(Number(req.query.team)); if (team) { diff --git a/traque-back/team_socket.js b/traque-back/team_socket.js index c2e15b4..80b520e 100644 --- a/traque-back/team_socket.js +++ b/traque-back/team_socket.js @@ -1,3 +1,8 @@ +/* +This file manages team access to the server via websocket. +It receives messages, checks permissions, manages authentication and performs actions by calling functions from other modules. +This module also exposes functions to send messages via socket to all teams +*/ import { secureAdminBroadcast } from "./admin_socket.js"; import { io} from "./index.js"; import game from "./game.js"; diff --git a/traque-back/zone_manager.js b/traque-back/zone_manager.js index d00d02f..45c9039 100644 --- a/traque-back/zone_manager.js +++ b/traque-back/zone_manager.js @@ -1,3 +1,7 @@ +/* +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'; @@ -6,11 +10,9 @@ import { secureAdminBroadcast } from './admin_socket.js'; export default { //Setings storing where the zone will start, end and how it should evolve - //The zone will start by staying at its mzx value for reductionInterval minutes + //The zone will start by staying at its max value for reductionInterval minutes //and then reduce during reductionDuration minutes, then wait again... //The reduction factor is such that after reductionCount the zone will be the min zone - //a call to onZoneUpdate will be made every updateIntervalSeconds when the zone is changing - //a call to onNextZoneUpdate will be made when the zone reduction ends and a new next zone is announced zoneSettings: { min: { center: null, radius: null }, max: { center: null, radius: null }, @@ -194,11 +196,13 @@ export default { }, this.zoneSettings.updateIntervalSeconds * 1000); }, + //a call to onNextZoneUpdate will be made when the zone reduction ends and a new next zone is announced onNextZoneUpdate(newZone) { playersBroadcast("new_zone", newZone) secureAdminBroadcast("new_zone", newZone) - }, - + }, + + //a call to onZoneUpdate will be made every updateIntervalSeconds when the zone is changing onZoneUpdate(zone) { playersBroadcast("zone", zone) secureAdminBroadcast("zone", zone)