/* 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 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"; /** * The possible states of the game */ export const GameState = { SETUP: "setup", PLACEMENT: "placement", PLAYING: "playing", FINISHED: "finished" } 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: "", capturedMessage: "", 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; } //The game has started if (newState == GameState.PLAYING) { penaltyController.start(); if (!zoneManager.ready()) { return false; } this.initLastSentLocations(); zoneManager.reset() //If the zone cannot be setup, reset everything if (!zoneManager.start()) { this.setState(GameState.SETUP); return; } } if (newState != GameState.PLAYING) { zoneManager.reset(); penaltyController.stop(); timeoutHandler.endAllSendPositionTimeout(); } //Game reset if (newState == GameState.SETUP) { for (let team of this.teams) { team.penalties = 0; team.captured = false; team.enemyLocation = null; team.enemyName = null; team.currentLocation = null; team.lastSentLocation = null; } this.updateTeamChasing(); } this.state = newState; 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)) { id = Math.floor(Math.random() * 1_000_000); } 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({ id: id, name: teamName, chasing: null, chased: null, currentLocation: null, lastSentLocation: null, locationSendDeadline: null, enemyLocation: null, enemyName: null, captureCode: this.createCaptureCode(), sockets: [], startingArea: null, ready: false, captured: false, penalties: 0, }); this.updateTeamChasing(); 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) => { if (!t.captured) { res++; } }) 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) { this.finishGame() } return false; } let firstTeam = null; let previousTeam = null for (let i = 0; i < this.teams.length; i++) { if (!this.teams[i].captured) { if (previousTeam != null) { this.teams[i].chased = previousTeam; this.getTeam(previousTeam).chasing = this.teams[i].id; this.getTeam(previousTeam).enemyName = this.teams[i].name; } else { firstTeam = 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) 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) { return { ...t, ...newTeam } } else { return t; } }) this.updateTeamChasing(); penaltyController.checkPenalties(); 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; } writePosition(Date.now(), teamId, location[0], location[1]); team.currentLocation = location; //Update the team ready status if they are in their 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) } return true; }, /** * Initialize the last sent location of the teams to their starting location */ initLastSentLocations() { for (let 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) { team.enemyLocation = this.getTeam(team.chasing).lastSentLocation; sendUpdatedTeamInformations(team.id); } }, /** * 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) { return false; } if (team.currentLocation == null) { return false; } writeSeePosition(Date.now(), teamId, team.chasing); team.locationSendDeadline = Date.now() + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000; timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline); team.lastSentLocation = team.currentLocation; if (this.getTeam(team.chasing) != null) { team.enemyLocation = this.getTeam(team.chasing).lastSentLocation; } sendUpdatedTeamInformations(team.id); 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; } //remove the team from the list this.teams = this.teams.filter(t => t.id !== teamId); this.updateTeamChasing(); timeoutHandler.endSendPositionTimeout(team.id); return true; }, /** * 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 && enemyTeam.captureCode == captureCode) { writeCapture(Date.now(), teamId, enemyTeam.id); this.capture(enemyTeam.id); 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 timeoutHandler.endSendPositionTimeout(teamId); this.updateTeamChasing(); }, /** * Change the settings of the Zone manager * The game should not be in PLAYING or FINISHED state * @param {Object} newSettings The object containing the settings to be changed * @returns false if failed */ setZoneSettings(newSettings) { //cannot change zones while playing if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) { return false; } 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) { return false; } return zoneManager.udpateSettings(newSettings) }, /** * Set the game state as finished, as well as resetting the zone manager */ finishGame() { this.setState(GameState.FINISHED); zoneManager.reset(); timeoutHandler.endAllSendPositionTimeout() playersBroadcast("game_state", this.state); }, }