diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js new file mode 100644 index 0000000..0888376 --- /dev/null +++ b/traque-back/admin_socket.js @@ -0,0 +1,149 @@ +import { io, game } from "./index.js"; +import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; + +import { config } from "dotenv"; +config() + +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; + +/** + * Send a message to all logged in admin sockets + * @param {String} event The event name + * @param {String} data The data to send + */ +export function secureAdminBroadcast(event, data) { + loggedInSockets.forEach(s => { + io.of("admin").to(s).emit(event, data); + }); +} + +//Array of logged in sockets +let loggedInSockets = []; +export function initAdminSocketHandler() { + //Admin namespace + io.of("admin").on("connection", (socket) => { + //Flag to check if the user is logged in, defined for each socket + let loggedIn = false; + + socket.on("disconnect", () => { + console.log("user disconnected"); + //Remove the socket from the logged in sockets array + loggedInSockets = loggedInSockets.filter(s => s !== socket.id); + }); + + socket.on("logout", () => { + loggedInSockets = loggedInSockets.filter(s => s !== socket.id); + }) + + //User is attempting to log in + socket.on("login", (password) => { + if (password === ADMIN_PASSWORD && !loggedIn) { + //Attempt successful + socket.emit("login_response", true); + loggedInSockets.push(socket.id); + loggedIn = true; + //Send the current state + socket.emit("game_state", game.state) + //Other settings that need initialization + socket.emit("zone_settings", game.zone.zoneSettings) + + } else { + //Attempt unsuccessful + socket.emit("login_response", false); + } + }); + + socket.on("set_zone_settings", (settings) => { + if (!loggedIn) { + socket.emit("error", "Not logged in"); + return; + } + if (!game.setZoneSettings(settings)) { + socket.emit("error", "Error changing zone"); + socket.emit("zone_settings", game.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", game.zone.zoneSettings) + } + + }) + + //User is attempting to add a new team + socket.on("add_team", (teamName) => { + if (!loggedIn) { + socket.emit("error", "Not logged in"); + return; + } + if (game.addTeam(teamName)) { + secureAdminBroadcast("teams", game.teams); + } else { + socket.emit("error", "Error adding team"); + } + }); + + //User is attempting to remove a team + socket.on("remove_team", (teamId) => { + if (!loggedIn) { + socket.emit("error", "Not logged in"); + return; + } + if (game.removeTeam(teamId)) { + secureAdminBroadcast("teams", game.teams); + } else { + socket.emit("error", "Error removing team"); + } + }); + + //User is attempting to change the game state + socket.on("change_state", (state) => { + if (!loggedIn) { + socket.emit("error", "Not logged in"); + return; + } + if (game.setState(state)) { + secureAdminBroadcast("game_state", game.state); + playersBroadcast("game_state", game.state) + } else { + socket.emit("error", "Error setting state"); + } + }); + + //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"); + return; + } + if (game.reorderTeams(newOrder)) { + secureAdminBroadcast("teams", game.teams); + } else { + socket.emit("error", "Error reordering teams"); + } + }); + + socket.on("update_team", (teamId, newTeam) => { + if (!loggedIn) { + socket.emit("error", "Not logged in"); + return; + } + if (game.updateTeam(teamId, newTeam)) { + secureAdminBroadcast("teams", game.teams); + sendUpdatedTeamInformations(teamId) + } + }) + + //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"); + return; + } + socket.emit("teams", game.teams); + }); + + }); + + +} \ No newline at end of file diff --git a/traque-back/game.js b/traque-back/game.js index 49bdfa2..d3a77cf 100644 --- a/traque-back/game.js +++ b/traque-back/game.js @@ -1,5 +1,5 @@ import { isInCircle } from "./map_utils.js"; -import { ZoneManager } from "./zoneManager.js"; +import { ZoneManager } from "./zone_manager.js"; const GameState = { SETUP: "setup", @@ -10,6 +10,8 @@ const GameState = { export default class Game { constructor(onUpdateZone,onUpdateNewZone) { + //Number of penalties needed to be eliminated + this.MAX_PENALTIES = 3; this.teams = []; this.state = GameState.SETUP; this.zone = new ZoneManager(onUpdateZone, onUpdateNewZone) @@ -62,6 +64,7 @@ export default class Game { startingArea: null, ready: false, captured: false, + penalties: 0, }); this.updateTeamChasing(); return true; @@ -151,16 +154,33 @@ export default class Game { return true; } - capture(teamId, captureCode) { + /** + * Request a capture initiated by the team with id teamId (the one trying to capture) + * If the captureCode match, the team chased by teamId will be set to captured + * And the chase chain will be updated + * @param {Number} teamId The id of the capturing team + * @param {Number} captureCode The code sent by the capturing that only the captured team know, used to verify the authenticity of the capture + * @returns {Boolean} if the capture has been successfull or not + */ + requestCapture(teamId, captureCode) { let enemyTeam = this.getTeam(this.getTeam(teamId).chasing) if(enemyTeam.captureCode == captureCode) { - enemyTeam.captured = true; + this.capture(enemyTeam); this.updateTeamChasing(); return true; } return false; } + /** + * Set a team to captured and update the chase chain + * @param {Number} teamId the Id of the captured team + */ + capture(teamId) { + this.getTeam(teamId).captured = true + this.updateTeamChasing(); + } + /** * Change the settings of the Zone manager * The game should not be in PLAYING or FINISHED state diff --git a/traque-back/index.js b/traque-back/index.js index 56cc448..44eced3 100644 --- a/traque-back/index.js +++ b/traque-back/index.js @@ -3,9 +3,10 @@ import { Server } from "socket.io"; import Game from "./game.js"; import { config } from "dotenv"; import { readFileSync } from "fs"; +import { initAdminSocketHandler, secureAdminBroadcast } from "./admin_socket.js"; +import { initTeamSocket, playersBroadcast } from "./team_socket.js"; //extract admin password from .env file config(); -const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; const HOST = process.env.HOST; const PORT = process.env.PORT; @@ -20,282 +21,27 @@ httpsServer.listen(PORT, HOST, () => { //set cors to allow all origins -const io = new Server(httpsServer, { +export const io = new Server(httpsServer, { cors: { origin: "*", methods: ["GET", "POST"] } }); - -/** - * Send a message to all logged in admin sockets - * @param {String} event The event name - * @param {String} data The data to send - */ -function secureAdminBroadcast(event, data) { - loggedInSockets.forEach(s => { - io.of("admin").to(s).emit(event, data); - }); -} - -/** - * Send a socket message to all the players of a team - * @param {String} teamId The team that will receive the message - * @param {String} event Event name - * @param {*} data The payload - */ -function teamBroadcast(teamId, event, data) { - for (let socketId of game.getTeam(teamId).sockets) { - io.of("player").to(socketId).emit(event, data) - } -} - -/** - * Send a message to all logged in players - * @param {String} event Event name - * @param {String} data payload - */ -function playersBroadcast(event, data) { - for (let 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 - */ -function logoutPlayer(id) { - for (let team of game.teams) { - team.sockets = team.sockets.filter((sid) => sid != id); - } -} - //Zone update broadcast function, called by the game object function onUpdateNewZone(newZone) { - console.log("new_zone", newZone) playersBroadcast("new_zone", newZone) secureAdminBroadcast("new_zone", newZone) } function onUpdateZone(zone) { - console.log("zone update",zone); playersBroadcast("zone", zone) secureAdminBroadcast("zone", zone) } -const game = new Game(onUpdateZone, onUpdateNewZone); - -//Array of logged in sockets -let loggedInSockets = []; +export const game = new Game(onUpdateZone, onUpdateNewZone); -//Admin namespace -io.of("admin").on("connection", (socket) => { - //Flag to check if the user is logged in, defined for each socket - let loggedIn = false; - - socket.on("disconnect", () => { - console.log("user disconnected"); - //Remove the socket from the logged in sockets array - loggedInSockets = loggedInSockets.filter(s => s !== socket.id); - }); - - socket.on("logout", () => { - loggedInSockets = loggedInSockets.filter(s => s !== socket.id); - }) - - //User is attempting to log in - socket.on("login", (password) => { - if (password === ADMIN_PASSWORD && !loggedIn) { - //Attempt successful - socket.emit("login_response", true); - loggedInSockets.push(socket.id); - loggedIn = true; - //Send the current state - socket.emit("game_state", game.state) - //Other settings that need initialization - socket.emit("zone_settings", game.zone.zoneSettings) - - } else { - //Attempt unsuccessful - socket.emit("login_response", false); - } - }); - - socket.on("set_zone_settings", (settings) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if(!game.setZoneSettings(settings)) { - socket.emit("error", "Error changing zone"); - socket.emit("zone_settings", game.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", game.zone.zoneSettings) - } - - }) - - //User is attempting to add a new team - socket.on("add_team", (teamName) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.addTeam(teamName)) { - secureAdminBroadcast("teams", game.teams); - } else { - socket.emit("error", "Error adding team"); - } - }); - - //User is attempting to remove a team - socket.on("remove_team", (teamId) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.removeTeam(teamId)) { - secureAdminBroadcast("teams", game.teams); - } else { - socket.emit("error", "Error removing team"); - } - }); - - //User is attempting to change the game state - socket.on("change_state", (state) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.setState(state)) { - secureAdminBroadcast("game_state", game.state); - playersBroadcast("game_state", game.state) - } else { - socket.emit("error", "Error setting state"); - } - }); - - //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"); - return; - } - if (game.reorderTeams(newOrder)) { - secureAdminBroadcast("teams", game.teams); - } else { - socket.emit("error", "Error reordering teams"); - } - }); - - socket.on("update_team", (teamId, newTeam) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - if (game.updateTeam(teamId, newTeam)) { - secureAdminBroadcast("teams", game.teams); - sendUpdatedTeamInformations(teamId) - } - }) - - //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"); - return; - } - socket.emit("teams", game.teams); - }); - -}); - - -function sendUpdatedTeamInformations(teamId) { - let team = game.getTeam(teamId) - team.sockets.forEach(socketId => { - io.of("player").to(socketId).emit("update_team", { - name: team.name, - enemyLocation: team.enemyLocation, - currentLocation: team.currentLocation, - lastSentLocation: team.lastSentLocation, - captureCode: team.captureCode, - startingArea: team.startingArea, - ready: team.ready, - captured: team.captured - }) - }) -} - -io.of("player").on("connection", (socket) => { - let teamId = null; - console.log("a user connected"); - - socket.on("disconnect", () => { - console.log("user disconnected"); - logoutPlayer(socket.id) - }); - - socket.on("login", (loginTeamId) => { - if (game.getTeam(loginTeamId) === undefined) { - socket.emit("login_response", false); - return; - } - logoutPlayer(socket.id) - teamId = loginTeamId; - let team = game.getTeam(loginTeamId); - team.sockets.push(socket.id); - sendUpdatedTeamInformations(loginTeamId); - socket.emit("login_response", true); - socket.emit("game_state", game.state) - }); - - socket.on("logout", () => { - logoutPlayer(socket.id); - }) - - socket.on("update_position", (position) => { - // Only the first player to connect to the team socket can update the current position - // 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.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"); - return; - } - game.updateTeamChasing(); - teamBroadcast(teamId, "update_team", { enemyLocation: team.enemyLocation }); - }); - - socket.on('capture', (captureCode) => { - let capturedTeam = game.getTeam(teamId).chasing - if(game.capture(teamId, captureCode)) { - sendUpdatedTeamInformations(teamId) - sendUpdatedTeamInformations(capturedTeam) - secureAdminBroadcast("teams", game.teams); - }else { - socket.emit("error", "Incorrect code") - } - }) -}); +initAdminSocketHandler(); +initTeamSocket(); \ No newline at end of file diff --git a/traque-back/penalty_controller.js b/traque-back/penalty_controller.js new file mode 100644 index 0000000..44681af --- /dev/null +++ b/traque-back/penalty_controller.js @@ -0,0 +1,27 @@ +import { game } from "."; +import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket"; + +export class PenaltyController { + constructor(game) { + + } + + addPenalty(teamId) { + let team = game.getTeam(teamId); + if(team.captured) { + return; + } + team.penalties++; + if(team.penalties == game.MAX_PENALTIES) { + game.requestCapture(team.chased); + sendUpdatedTeamInformations(teamId); + sendUpdatedTeamInformations(team.chased); + teamBroadcast(teamId, "warning", "You have been eliminated (reason: too many penalties)") + teamBroadcast(team.chased, "success", "The team you are chasing has changed") + } + } + + watchZone() { + + } +} \ No newline at end of file diff --git a/traque-back/team_socket.js b/traque-back/team_socket.js new file mode 100644 index 0000000..03c7a0b --- /dev/null +++ b/traque-back/team_socket.js @@ -0,0 +1,119 @@ +import { secureAdminBroadcast } from "./admin_socket.js"; +import { io, game } from "./index.js"; + +/** + * Send a socket message to all the players of a team + * @param {String} teamId The team that will receive the message + * @param {String} event Event name + * @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) + } +} + +/** + * Send a message to all logged in players + * @param {String} event Event name + * @param {String} data payload + */ +export function playersBroadcast(event, data) { + for (let 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 + */ +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) + team.sockets.forEach(socketId => { + io.of("player").to(socketId).emit("update_team", { + name: team.name, + enemyLocation: team.enemyLocation, + currentLocation: team.currentLocation, + lastSentLocation: team.lastSentLocation, + captureCode: team.captureCode, + startingArea: team.startingArea, + ready: team.ready, + captured: team.captured + }) + }) +} +export function initTeamSocket() { + + io.of("player").on("connection", (socket) => { + let teamId = null; + console.log("a user connected"); + + socket.on("disconnect", () => { + console.log("user disconnected"); + logoutPlayer(socket.id) + }); + + socket.on("login", (loginTeamId) => { + if (game.getTeam(loginTeamId) === undefined) { + socket.emit("login_response", false); + return; + } + logoutPlayer(socket.id) + teamId = loginTeamId; + let team = game.getTeam(loginTeamId); + team.sockets.push(socket.id); + sendUpdatedTeamInformations(loginTeamId); + socket.emit("login_response", true); + socket.emit("game_state", game.state) + }); + + socket.on("logout", () => { + logoutPlayer(socket.id); + }) + + socket.on("update_position", (position) => { + // Only the first player to connect to the team socket can update the current position + // 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.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"); + return; + } + game.updateTeamChasing(); + teamBroadcast(teamId, "update_team", { enemyLocation: team.enemyLocation }); + }); + + socket.on('capture', (captureCode) => { + let capturedTeam = game.getTeam(teamId).chasing + if (game.requestCapture(teamId, captureCode)) { + sendUpdatedTeamInformations(teamId) + sendUpdatedTeamInformations(capturedTeam) + secureAdminBroadcast("teams", game.teams); + } else { + socket.emit("error", "Incorrect code") + } + }) + }); +} \ No newline at end of file diff --git a/traque-back/zoneManager.js b/traque-back/zone_manager.js similarity index 96% rename from traque-back/zoneManager.js rename to traque-back/zone_manager.js index d7f5164..9c0f898 100644 --- a/traque-back/zoneManager.js +++ b/traque-back/zone_manager.js @@ -137,11 +137,9 @@ export class ZoneManager { * Wait for the appropriate duration before starting a new zone reduction if needed */ setNextZone() { - console.log("Setting next zone", this.currentZoneCount, this.zoneSettings.reductionCount, this.zoneSettings.reductionInterval) //At this point, nextZone == currentZone, we need to update the next zone, the raidus decrement, and start a timer before the next shrink //last zone if (this.currentZoneCount == this.zoneSettings.reductionCount) { - console.log("last zone reached") this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min)) this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.min)) } else if (this.currentZoneCount == this.zoneSettings.reductionCount - 1) { @@ -150,7 +148,6 @@ export class ZoneManager { this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval) this.currentZoneCount++; } else if (this.currentZoneCount < this.zoneSettings.reductionCount) { - console.log("started timer for next zone") this.nextZone.center = this.getRandomNextCenter(this.nextZone.radius * this.shrinkFactor) this.nextZone.radius *= this.shrinkFactor; this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone)) @@ -170,11 +167,8 @@ export class ZoneManager { */ startShrinking() { const startTime = new Date(); - console.log("started shrinking") this.updateIntervalId = setInterval(() => { - console.log("Shrink tick") const completed = ((new Date() - startTime) / (1000 * 60)) / this.zoneSettings.reductionDuration; - console.log(completed) this.currentZone.radius = map(completed, 0, 1, this.currentStartZone.radius, this.nextZone.radius) this.currentZone.center.lat = map(completed,0,1, this.currentStartZone.center.lat, this.nextZone.center.lat) this.currentZone.center.lng = map(completed,0,1, this.currentStartZone.center.lng, this.nextZone.center.lng)