diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js index 1d4d58d..feb151c 100644 --- a/traque-back/admin_socket.js +++ b/traque-back/admin_socket.js @@ -2,6 +2,8 @@ import { io, game, penaltyController } from "./index.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; import { config } from "dotenv"; +import { currentZone, initZone, removeZone } from "./zone_manager.js"; +import { GameState } from "./game.js"; config() const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; @@ -47,12 +49,7 @@ export function initAdminSocketHandler() { //Other settings that need initialization socket.emit("penalty_settings", penaltyController.settings) socket.emit("game_settings", game.settings) - socket.emit("zone_settings", game.zone.zoneSettings) - socket.emit("zone", game.zone.currentZone) - socket.emit("new_zone", { - begin: game.zone.currentStartZone, - end: game.zone.nextZone - }) + socket.emit("zone", currentZone) } else { //Attempt unsuccessful @@ -72,20 +69,26 @@ export function initAdminSocketHandler() { playersBroadcast("game_settings", game.settings); }) - socket.on("set_zone_settings", (settings) => { + socket.on("set_zone", (zone) => { 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) + if(game.state != GameState.PLAYING) { + initZone(zone) + }else { + socket.emit("error", "Game is not in setup state") } - }) + socket.on("remove_zone", (zone, time) => { + if (!loggedIn) { + socket.emit("error", "Not logged in"); + return; + } + removeZone(zone, time) + }); + socket.on("set_penalty_settings", (settings) => { if (!loggedIn) { socket.emit("error", "Not logged in"); diff --git a/traque-back/game.js b/traque-back/game.js index 9e9d6f3..f424137 100644 --- a/traque-back/game.js +++ b/traque-back/game.js @@ -2,7 +2,7 @@ import { secureAdminBroadcast } from "./admin_socket.js"; import { penaltyController } from "./index.js"; import { isInCircle } from "./map_utils.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; -import { ZoneManager } from "./zone_manager.js"; +import { resetZone } from "./zone_manager.js"; export const GameState = { SETUP: "setup", @@ -12,10 +12,9 @@ export const GameState = { } export default class Game { - constructor(onUpdateZone, onUpdateNewZone) { + constructor() { this.teams = []; this.state = GameState.SETUP; - this.zone = new ZoneManager(onUpdateZone, onUpdateNewZone) this.settings = { loserEndGameMessage: "", winnerEndGameMessage: "", @@ -36,19 +35,9 @@ export default class Game { //The game has started if (newState == GameState.PLAYING) { penaltyController.start(); - if (!this.zone.ready()) { - return false; - } this.initLastSentLocations(); - this.zone.reset() - //If the zone cannot be setup, reset everything - if (!this.zone.start()) { - this.setState(GameState.SETUP); - return; - } } if (newState != GameState.PLAYING) { - this.zone.reset(); penaltyController.stop(); } //Game reset @@ -235,23 +224,8 @@ export default class Game { 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; - } - return this.zone.udpateSettings(newSettings) - } - finishGame() { this.setState(GameState.FINISHED); - this.zone.reset(); playersBroadcast("game_state", this.state); } } \ No newline at end of file diff --git a/traque-back/index.js b/traque-back/index.js index 88b2a4d..f031bae 100644 --- a/traque-back/index.js +++ b/traque-back/index.js @@ -35,19 +35,7 @@ export const io = new Server(httpsServer, { } }); -//Zone update broadcast function, called by the game object -function onUpdateNewZone(newZone) { - playersBroadcast("new_zone", newZone) - secureAdminBroadcast("new_zone", newZone) -} - -function onUpdateZone(zone) { - playersBroadcast("zone", zone) - secureAdminBroadcast("zone", zone) -} - - -export const game = new Game(onUpdateZone, onUpdateNewZone); +export const game = new Game(); export const penaltyController = new PenaltyController(); diff --git a/traque-back/penalty_controller.js b/traque-back/penalty_controller.js index 88c7f56..54eb31f 100644 --- a/traque-back/penalty_controller.js +++ b/traque-back/penalty_controller.js @@ -3,6 +3,7 @@ import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js"; import { GameState } from "./game.js"; import { secureAdminBroadcast } from "./admin_socket.js"; import { game } from "./index.js"; +import { isInZone } from "./zone_manager.js"; export class PenaltyController { constructor() { @@ -91,10 +92,10 @@ export class PenaltyController { this.game.teams.forEach((team) => { if (team.captured) { return } //All the informations are not ready yet - if (team.currentLocation == null || this.game.zone.currentZone == null) { + if (team.currentLocation == null) { return; } - if (!isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] }, this.game.zone.currentZone.center, this.game.zone.currentZone.radius)) { + if (!isInZone(team.currentLocation)) { //The team was not previously out of the zone if (!this.outOfBoundsSince[team.id]) { this.outOfBoundsSince[team.id] = new Date(); diff --git a/traque-back/team_socket.js b/traque-back/team_socket.js index 23dbddf..5eeb3ac 100644 --- a/traque-back/team_socket.js +++ b/traque-back/team_socket.js @@ -1,5 +1,6 @@ import { secureAdminBroadcast } from "./admin_socket.js"; import { io, game } from "./index.js"; +import { currentZone } from "./zone_manager.js"; /** * Send a socket message to all the players of a team @@ -8,9 +9,13 @@ import { io, game } from "./index.js"; * @param {*} data The payload */ export function teamBroadcast(teamId, event, data) { + if(!game.getTeam(teamId)) { + return false; + } for (let socketId of game.getTeam(teamId).sockets) { io.of("player").to(socketId).emit(event, data) } + return true; } /** @@ -79,11 +84,7 @@ export function initTeamSocket() { socket.emit("login_response", true); socket.emit("game_state", game.state) socket.emit("game_settings", game.settings) - socket.emit("zone", game.zone.currentZone) - socket.emit("new_zone", { - begin: game.zone.currentStartZone, - end: game.zone.nextZone - }) + socket.emit("zone", currentZone) }); socket.on("logout", () => { diff --git a/traque-back/zone_manager.js b/traque-back/zone_manager.js index 31cdb91..fa49c79 100644 --- a/traque-back/zone_manager.js +++ b/traque-back/zone_manager.js @@ -1,199 +1,108 @@ -import { randomCirclePoint } from 'random-location' -import { isInCircle } from './map_utils.js'; -import { map } from './util.js'; +import { playersBroadcast, teamBroadcast } from './team_socket.js'; +import { secureAdminBroadcast } from './admin_socket.js'; -export class ZoneManager { - constructor(onZoneUpdate, onNextZoneUpdate) { - //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 - //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 - this.zoneSettings = { - min: { center: null, radius: null }, - max: { center: null, radius: null }, - reductionCount: 2, - reductionDuration: 1, - reductionInterval: 1, - updateIntervalSeconds: 1, +export let currentZone = [] +let tileSize = 16; + +export class TileNumber { + constructor(x, y) { + this.x = x; + this.y = y; + this.removeDate = null; + } + equals(other) { + return this.x === other.x && this.y === other.y; + } + + removeIn(minutes) { + this.removeDate = Date.now() + 1000 * 60 * minutes + } +} + +export function latLngToTileNumber(pos, tileSize) { + const numTilesX = 2 ** tileSize; + const numTilesY = 2 ** tileSize; + const lngDegrees = pos[1]; + const latRadians = pos[0] * (Math.PI / 180); + return { + x:Math.round(numTilesX * ((lngDegrees + 180) / 360)), + y:Math.round(numTilesY * (1 - Math.log(Math.tan(latRadians) + 1 / Math.cos(latRadians)) / Math.PI) / 2) + }; +} + +export function broadcastZoneState() { + playersBroadcast("zone", currentZone); + secureAdminBroadcast("zone", currentZone); +} + + +/** + * Remove all tiles from the zone + */ +export function resetZone() { + currentZone = []; + broadcastZoneState(); +} + +export function setTileSize(size) { + resetZone(); + tileSize = size; +} + +/** + * Check whether a position is in the zone + * @param {Object} position The position to check + */ +export function isInZone(position) { + let tile = latLngToTileNumber(position, tileSize); + return currentZone.some(square => square.equals(tile)) +} + +/** + * Initialize a zone with a list of tiles + * @param {Array} zone Array of tiles to add + */ +export function initZone(zone) { + currentZone = []; + try { + for (let { x, y } of zone) { + currentZone.push(new TileNumber(x, y)) } - this.nextZoneDecrement = null; - //Live location of the zone - this.currentZone = { center: null, radius: null }; - - //If the zone is shrinking, this is the target of the current shrinking - //If the zone is not shrinking, this will be the target of the next shrinking - this.nextZone = { center: null, radius: null }; - - //Zone at the begining of the shrinking - this.currentStartZone = { center: null, radius: null }; - - this.startDate = null; - this.started = false; - this.updateIntervalId = null; - this.nextZoneTimeoutId = null; - - this.onZoneUpdate = onZoneUpdate; - this.onNextZoneUpdate = onNextZoneUpdate + broadcastZoneState(); + } catch (e) { + console.error(e); + secureAdminBroadcast("error", "Invalid zone format") } +} - /** - * Test if a given configuration object is valid, i.e if all needed values are well defined - * @param {Object} settings Settings object describing a config of a zone manager - * @returns if the config is correct - */ - validateSettings(settings) { - if (settings.reductionCount && (typeof settings.reductionCount != "number" || settings.reductionCount <= 0)) { return false } - if (settings.reductionDuration && (typeof settings.reductionDuration != "number" || settings.reductionDuration < 0)) { return false } - if (settings.reductionInterval && (typeof settings.reductionInterval != "number" || settings.reductionInterval < 0)) { return false } - if (settings.updateIntervalSeconds && (typeof settings.updateIntervalSeconds != "number" || settings.updateIntervalSeconds <= 0)) { return false } - if (settings.max && (typeof settings.max.radius != "number" || typeof settings.max.center.lat != "number" || typeof settings.max.center.lng != "number")) { return false } - if (settings.min && (typeof settings.min.radius != "number" || typeof settings.min.center.lat != "number" || typeof settings.min.center.lng != "number")) { return false } - return true; - } - /** - * Test if the zone manager is ready to start - * @returns true if the zone manager is ready to be started, false otherwise - */ - ready() { - return this.validateSettings(this.zoneSettings); - } - - /** - * Update the settings of the zone, this can be done by passing an object containing the settings to change. - * Unless specified, the durations are in minutes - * Default config : - * `this.zoneSettings = { - * min: {center: null, radius: null}, - * max: {center: null, radius: null}, - * reductionCount: 2, - * reductionDuration: 10, - * reductionInterval: 10, - * updateIntervalSeconds: 10, - * }` - * @param {Object} newSettings The fields of the settings to udpate - * @returns - */ - udpateSettings(newSettings) { - //validate settings - this.zoneSettings = { ...this.zoneSettings, ...newSettings }; - this.nextZoneDecrement = (this.zoneSettings.max.radius - this.zoneSettings.min.radius) / this.zoneSettings.reductionCount; - return true; - } - - /** - * Reinitialize the object and stop all the tasks - */ - reset() { - this.currentZoneCount = 0; - this.started = false; - if (this.updateIntervalId != null) { - clearInterval(this.updateIntervalId); - this.updateIntervalId = null; - } - if (this.nextZoneTimeoutId != null) { - clearTimeout(this.nextZoneTimeoutId); - this.nextZoneTimeoutId = null; - } - } - - /** - * Start the zone reduction sequence - */ - start() { - this.started = true; - this.startDate = new Date(); - //initialize the zone to its max value - this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.max)); - this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.max)); - this.currentZone = JSON.parse(JSON.stringify(this.zoneSettings.max)); - return this.setNextZone(); - - } - - /** - * Get the center of the next zone, this center need to satisfy two properties - * - it needs to be included in the current zone, this means that this new point should lie in the circle of center currentZone.center and radius currentZone.radius - newRadius - * - it needs to include the last zone, which means that the center must lie in the center of center min.center and of radius newRadius - min.radius - * @param newRadius the radius that the new zone will have - * @returns the coordinates of the new center as an object with lat and long fields - */ - getRandomNextCenter(newRadius) { - let ok = false; - let res = null - let tries = 0; - const MAX_TRIES = 100000 - //take a random point satisfying both conditions - while (tries++ < MAX_TRIES && !ok) { - res = randomCirclePoint({ latitude: this.currentZone.center.lat, longitude: this.currentZone.center.lng }, this.currentZone.radius - newRadius); - ok = (isInCircle({ lat: res.latitude, lng: res.longitude }, this.zoneSettings.min.center, newRadius - this.zoneSettings.min.radius)) - } - if(tries>=MAX_TRIES) { - return false; - } - return { - lat: res.latitude, - lng: res.longitude - } - } - - /** - * Compute the next zone satifying the given settings, update the nextZone and currentStartZone - * Wait for the appropriate duration before starting a new zone reduction if needed - */ - setNextZone() { - //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) { - 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) { - this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone)) - this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min)) - this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval) - this.currentZoneCount++; - } else if (this.currentZoneCount < this.zoneSettings.reductionCount) { - this.nextZone.center = this.getRandomNextCenter(this.nextZone.radius - this.nextZoneDecrement) - //Next center cannot be found - if(this.nextZone.center === false) { - console.log("no center") - return false; +/** + * Put a list of tiles in a warning state for a certain amount of time, before removing them + * @param {Array} zone Array of tiles to remove + * @param {Number} time Time before those tiles get removed in minutes + */ +export function removeZone(zone, time) { + for (let tile of zone) { + for (let currentTile of currentZone) { + if (currentTile.equals(tile)) { + currentTile.removeIn(time); } - this.nextZone.radius -= this.nextZoneDecrement; - this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone)) - this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval) - this.currentZoneCount++; } - this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentStartZone))) - this.onNextZoneUpdate({ - begin: JSON.parse(JSON.stringify(this.currentStartZone)), - end: JSON.parse(JSON.stringify(this.nextZone)) - }) - return true; + } + broadcastZoneState(); +} + +setInterval(() => { + let changed = false; + currentZone = currentZone.map(square => { + if (square.removeDate !== null && square.removeDate < Date.now()) { + changed = true; + return null; + } + return square; + }).filter(square => square !== null) + if (changed) { + broadcastZoneState(); } - /* - * Start a task that will run periodically updatinng the zone size, and calling the onZoneUpdate callback - * This will also periodically check if the reduction is over or not - * If the reduction is over this function will call setNextZone - */ - startShrinking() { - const startTime = new Date(); - this.updateIntervalId = setInterval(() => { - const completed = ((new Date() - startTime) / (1000 * 60)) / this.zoneSettings.reductionDuration; - 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) - this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentZone))) - //Zone shrinking is over - if (completed >= 1) { - clearInterval(this.updateIntervalId); - this.updateIntervalId = null; - this.setNextZone(); - return; - } - }, this.zoneSettings.updateIntervalSeconds * 1000); - } +}, 1000); -} \ No newline at end of file