Ajout de stats + corrections

This commit is contained in:
Sébastien Rivière
2025-06-22 01:34:59 +02:00
parent ab81a5351c
commit 7d541159cd
12 changed files with 246 additions and 170 deletions

View File

@@ -1,14 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"command": "sudo npm start",
"name": "Run npm start",
"request": "launch",
"type": "node-terminal"
},
]
}

View File

@@ -53,7 +53,7 @@ export function initAdminSocketHandler() {
loggedInSockets.push(socket.id);
loggedIn = true;
// Send the current state
socket.emit("game_state", game.state)
socket.emit("game_state", {state: game.state, startDate: game.startDate})
// Other settings that need initialization
socket.emit("penalty_settings", penaltyController.settings)
socket.emit("game_settings", game.settings)
@@ -61,7 +61,8 @@ export function initAdminSocketHandler() {
socket.emit("zone", zone.currentZone)
socket.emit("new_zone", {
begin: zone.currentStartZone,
end: zone.nextZone
end: zone.nextZone,
endDate: zone.nextZoneDate,
})
} else {
// Attempt unsuccessful
@@ -143,10 +144,7 @@ export function initAdminSocketHandler() {
socket.emit("error", "Not logged in");
return;
}
if (game.setState(state)) {
secureAdminBroadcast("game_state", game.state);
playersBroadcast("game_state", game.state)
} else {
if (!game.setState(state)) {
socket.emit("error", "Error setting state");
}
});

View File

@@ -3,11 +3,11 @@ This module manages the main game state, the teams, the settings and the game lo
*/
import { secureAdminBroadcast } from "./admin_socket.js";
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { isInCircle } from "./map_utils.js";
import { isInCircle, getDistanceFromLatLon } from "./map_utils.js";
import timeoutHandler from "./timeoutHandler.js";
import penaltyController from "./penalty_controller.js";
import zoneManager from "./zone_manager.js";
import { writePosition, writeCapture, writeSeePosition } from "./trajectory.js";
import trajectory from "./trajectory.js";
/**
* The possible states of the game
@@ -24,6 +24,8 @@ export default {
teams: [],
//Current state of the game
state: GameState.SETUP,
// Date since gameState switched to PLAYING
startDate: null,
//Settings of the game
settings: {
loserEndGameMessage: "",
@@ -48,41 +50,63 @@ export default {
* @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) {
// Checks is the newState is a Gamestate
if (Object.values(GameState).indexOf(newState) == -1) return false;
// Match case
switch (newState) {
case GameState.SETUP:
trajectory.stop();
zoneManager.reset();
penaltyController.stop();
timeoutHandler.endAllSendPositionTimeout();
}
//Game reset
if (newState == GameState.SETUP) {
for (let team of this.teams) {
team.outOfZone = false;
team.penalties = 0;
team.captured = false;
team.enemyLocation = null;
team.enemyName = null;
team.currentLocation = null;
team.lastSentLocation = null;
team.distance = null;
team.finishDate = null;
team.nCaptures = 0;
team.nSentLocation = 0;
team.nObserved = 0;
}
this.startDate = null;
this.updateTeamChasing();
break;
case GameState.PLACEMENT:
trajectory.stop();
zoneManager.reset();
penaltyController.stop();
timeoutHandler.endAllSendPositionTimeout();
this.startDate = null;
break;
case GameState.PLAYING:
if (!zoneManager.start()) {
return false;
}
trajectory.start();
penaltyController.start();
this.initLastSentLocations();
this.startDate = Date.now();
break;
case GameState.FINISHED:
for (const team of this.teams) {
if (!team.finishDate) team.finishDate = Date.now();
}
trajectory.stop();
penaltyController.stop();
zoneManager.reset();
timeoutHandler.endAllSendPositionTimeout();
break;
}
// Update the state
this.state = newState;
secureAdminBroadcast("game_state", {state: newState, startDate: this.startDate});
playersBroadcast("game_state", newState);
secureAdminBroadcast("teams", this.teams);
return true;
},
@@ -128,6 +152,18 @@ export default {
ready: false,
captured: false,
penalties: 0,
outOfZone: false,
outOfZoneDeadline: null,
distance: 0,
finishDate: null,
nCaptures: 0,
nSentLocation: 0,
nObserved: 0,
phoneModel: null,
phoneName: null,
battery: null,
ping: null,
nConnected: 0,
});
this.updateTeamChasing();
return true;
@@ -156,7 +192,7 @@ export default {
updateTeamChasing() {
if (this.playingTeamCount() <= 2) {
if (this.state == GameState.PLAYING) {
this.finishGame();
this.setState(GameState.FINISHED);
}
return false;
}
@@ -230,8 +266,9 @@ export default {
if (!team || !location) {
return false;
}
if (team.currentLocation) team.distance += Math.floor(getDistanceFromLatLon({lat: location[0], lng: location[1]}, {lat: team.currentLocation[0], lng: team.currentLocation[1]}));
// Update of events of the game
writePosition(Date.now(), teamId, location[0], location[1]);
trajectory.writePosition(Date.now(), teamId, location[0], location[1]);
// Update of currentLocation
team.currentLocation = location;
// Update of ready (true if the team is in the starting area)
@@ -247,13 +284,14 @@ export default {
* Initialize the last sent location of the teams to their starting location
*/
initLastSentLocations() {
// Update of lastSentLocation
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);
}
// Update of enemyLocation now we have the lastSentLocation of the enemy
for (const team of this.teams) {
team.enemyLocation = this.getTeam(team.chasing).lastSentLocation;
sendUpdatedTeamInformations(team.id);
@@ -267,12 +305,15 @@ export default {
*/
sendLocation(teamId) {
const team = this.getTeam(teamId);
const enemyTeam = this.getTeam(team.chasing);
if (!team || !team.currentLocation) {
return false;
}
team.nSentLocation++;
enemyTeam.nObserved++;
const dateNow = Date.now();
// Update of events of the game
writeSeePosition(dateNow, teamId, team.chasing);
trajectory.writeSeePosition(dateNow, teamId, team.chasing);
// Update of locationSendDeadline
team.locationSendDeadline = dateNow + penaltyController.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
timeoutHandler.setSendPositionTimeout(team.id, team.locationSendDeadline);
@@ -283,6 +324,7 @@ export default {
if (teamChasing) team.enemyLocation = teamChasing.lastSentLocation;
// Sending new infos to the team
sendUpdatedTeamInformations(team.id);
sendUpdatedTeamInformations(enemyTeam.id);
return true;
},
@@ -315,8 +357,9 @@ export default {
if (!enemyTeam || enemyTeam.captureCode != captureCode) {
return false;
}
team.nCaptures++;
// Update of events of the game
writeCapture(Date.now(), teamId, enemyTeam.id);
trajectory.writeCapture(Date.now(), teamId, enemyTeam.id);
// Update of capture and chasing cycle
this.capture(enemyTeam.id);
// Sending new infos to the teams
@@ -330,7 +373,9 @@ export default {
* @param {Number} teamId the Id of the captured team
*/
capture(teamId) {
this.getTeam(teamId).captured = true;
const team = this.getTeam(teamId);
team.captured = true;
team.finishDate = Date.now();
timeoutHandler.endSendPositionTimeout(teamId);
this.updateTeamChasing();
},
@@ -352,22 +397,10 @@ export default {
}
zoneManager.udpateSettings(newSettings);
if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) {
zoneManager.reset()
if (!zoneManager.start()) {
this.setState(GameState.SETUP);
return false;
}
}
return true;
},
/**
* 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);
},
}
}

View File

@@ -5,7 +5,6 @@ import { config } from "dotenv";
import { initAdminSocketHandler } from "./admin_socket.js";
import { initTeamSocket } from "./team_socket.js";
import { initPhotoUpload } from "./photo.js";
import { initTrajectories } from "./trajectory.js";
config();
const HOST = process.env.HOST;
@@ -29,4 +28,3 @@ export const io = new Server(httpServer, {
initAdminSocketHandler();
initTeamSocket();
initPhotoUpload();
initTrajectories();

View File

@@ -14,7 +14,7 @@ function degToRad(deg) {
* @returns the distance between the two positions in meters
* @see https://gist.github.com/miguelmota/10076960
*/
function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
export function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
var R = 6371; // Radius of the earth in km
var dLat = degToRad(lat2 - lat1);
var dLon = degToRad(lon2 - lon1);

View File

@@ -121,17 +121,21 @@ export default {
if (!isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] }, zone.currentZone.center, zone.currentZone.radius)) {
//The team was not previously out of the zone
if (!this.outOfBoundsSince[team.id]) {
this.outOfBoundsSince[team.id] = new Date();
teamBroadcast(team.id, "warning", `You left the zone, you have ${this.settings.allowedTimeOutOfZone} minutes to get back in the marked area.`)
} else if (new Date() - this.outOfBoundsSince[team.id] > this.settings.allowedTimeOutOfZone * 60 * 1000) {
this.addPenalty(team.id)
this.outOfBoundsSince[team.id] = new Date();
} else if (Math.abs(new Date() - this.outOfBoundsSince[team.id] - (this.settings.allowedTimeOutOfZone - 1) * 60 * 1000) < 100) {
teamBroadcast(team.id, "warning", `You left the zone, you have 1 minutes to get back in the marked area.`)
this.outOfBoundsSince[team.id] = Date.now();
team.outOfZone = true;
team.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000;
secureAdminBroadcast("teams", game.teams)
} else if (Date.now() - this.outOfBoundsSince[team.id] > this.settings.allowedTimeOutOfZone * 60 * 1000) {
this.addPenalty(team.id);
this.outOfBoundsSince[team.id] = Date.now();
team.outOfZoneDeadline = this.outOfBoundsSince[team.id] + this.settings.allowedTimeOutOfZone * 60 * 1000;
secureAdminBroadcast("teams", game.teams)
}
} else {
if (this.outOfBoundsSince[team.id]) {
team.outOfZone = false;
delete this.outOfBoundsSince[team.id];
secureAdminBroadcast("teams", game.teams)
}
}
})
@@ -146,15 +150,15 @@ export default {
//If the team has not sent their location for more than the allowed period, automatically send it and add a penalty
if (team.captured) { return }
if (team.locationSendDeadline == null) {
team.locationSendDeadline = Number(new Date()) + this.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
team.locationSendDeadline = Number(Date.now()) + this.settings.allowedTimeBetweenPositionUpdate * 60 * 1000;
return;
}
if (new Date() > team.locationSendDeadline) {
if (Date.now() > team.locationSendDeadline) {
this.addPenalty(team.id);
game.sendLocation(team.id);
sendUpdatedTeamInformations(team.id);
secureAdminBroadcast("teams", game.teams)
} else if (Math.abs(new Date() - team.locationSendDeadline - 60 * 1000) < 100) {
} else if (Math.abs(Date.now() - team.locationSendDeadline - 60 * 1000) < 100) {
teamBroadcast(team.id, "warning", `You have one minute left to udpate your location.`)
}
})

View File

@@ -45,7 +45,6 @@ export function sendUpdatedTeamInformations(teamId) {
name: team.name,
enemyLocation: team.enemyLocation,
enemyName: team.enemyName,
currentLocation: team.currentLocation,
lastSentLocation: team.lastSentLocation,
locationSendDeadline: team.locationSendDeadline,
captureCode: team.captureCode,
@@ -53,6 +52,13 @@ export function sendUpdatedTeamInformations(teamId) {
ready: team.ready,
captured: team.captured,
penalties: team.penalties,
outOfZone: team.outOfZone,
outOfZoneDeadline: team.outOfZoneDeadline,
distance: team.distance,
startDate: game.startDate,
finishDate: team.finishDate,
nCaptures: team.nCaptures,
nSentLocation: team.nSentLocation,
})
})
secureAdminBroadcast("teams", game.teams);
@@ -94,7 +100,8 @@ export function initTeamSocket() {
socket.emit("zone", zone.currentZone);
socket.emit("new_zone", {
begin: zone.currentStartZone,
end: zone.nextZone
end: zone.nextZone,
endDate: zone.nextZoneDate,
})
callback({ isLoggedIn : true, message: "Logged in"});
});
@@ -133,6 +140,29 @@ export function initTeamSocket() {
return;
}
callback({ hasCaptured : true, message: "Capture successful" });
})
});
socket.on("deviceInfo", (infos) => {
if (!teamId) {
return;
}
const team = game.getTeam(teamId);
// Only the first socket shares its infos since he is the one whose location is tracked
if (team.sockets.indexOf(socket.id) == 0) {
team.phoneModel = infos.model;
team.phoneName = infos.name;
}
});
socket.on("batteryUpdate", (batteryLevel) => {
if (!teamId) {
return;
}
const team = game.getTeam(teamId);
// Only the first socket shares its infos since he is the one whose location is tracked
if (team.sockets.indexOf(socket.id) == 0) {
team.battery = batteryLevel;
}
});
});
}

View File

@@ -43,23 +43,48 @@ function addLineToFile(teamID, line) {
}
}
// Export functions
export async function initTrajectories() {
function initTrajectories() {
const files = fs.readdirSync(UPLOAD_DIR);
for (const file of files) fs.unlinkSync(path.join(UPLOAD_DIR, file));
}
export function writePosition(date, teamID, lon, lat) {
addLineToFile(teamID, dataToLine(date, "position", lon, lat));
}
// Export functions
export function writeCapture(date, teamID, capturedTeamID) {
export default {
isRecording: false,
start() {
initTrajectories();
this.isRecording = true;
},
stop() {
this.isRecording = false;
},
writePosition(date, teamID, lon, lat) {
if (this.isRecording) {
addLineToFile(teamID, dataToLine(date, "position", lon, lat));
}
},
writeCapture(date, teamID, capturedTeamID) {
if (this.isRecording) {
addLineToFile(teamID, dataToLine(date, "capture", capturedTeamID));
addLineToFile(capturedTeamID, dataToLine(date, "captured", teamID));
}
}
},
export function writeSeePosition(date, teamID, seenTeamID) {
writeSeePosition(date, teamID, seenTeamID) {
if (this.isRecording) {
addLineToFile(teamID, dataToLine(date, "see"));
addLineToFile(seenTeamID, dataToLine(date, "seen"));
}
},
writeOutOfZone(date, teamID, isOutOfZone) {
if (this.isRecording) {
addLineToFile(teamID, dataToLine(date, "zone", isOutOfZone));
}
},
}

View File

@@ -116,6 +116,8 @@ export default {
* Start the zone reduction sequence
*/
start() {
if (!this.ready()) return false;
this.reset();
this.started = true;
this.startDate = new Date();
//initialize the zone to its max value

View File

@@ -1,28 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}

View File

@@ -5,6 +5,7 @@ import useAdmin from '@/hook/useAdmin';
import dynamic from 'next/dynamic';
import { env } from 'next-runtime-env';
import { GameState } from '@/util/gameState';
const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod.CircularAreaPicker), {
ssr: false
@@ -12,8 +13,9 @@ const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod
export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
const teamImage = useRef(null);
const [avgSpeed, setAvgSpeed] = useState(0); // Speed in m/s
const [newTeamName, setNewTeamName] = React.useState('');
const { updateTeam, getTeamName, removeTeam, getTeam, teams } = useAdmin();
const { updateTeam, getTeamName, removeTeam, getTeam, teams, gameState, startDate } = useAdmin();
const [team, setTeam] = useState({});
const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST");
var protocol = "https://";
@@ -32,6 +34,12 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
teamImage.current.src = SERVER_URL + "/photo/my?team=" + selectedTeamId + "&t=" + new Date().getTime();
}, [selectedTeamId, teams])
// Update the average speed
useEffect(() => {
const time = Math.floor((team.finishDate ? team.finishDate - startDate : Date.now() - startDate) / 1000);
setAvgSpeed(team.distance/time);
}, [team.distance, team.finishDate]);
function handleRename(e) {
e.preventDefault();
updateTeam(team.id, { name: newTeamName });
@@ -49,10 +57,19 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
updateTeam(team.id, { penalties: newPenalties });
}
function formatTimeHours(time) {
// time is in seconds
if (!Number.isInteger(time)) return "Inconnue";
if (time < 0) time = 0;
const hours = Math.floor(time / 3600);
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return String(hours).padStart(2,"0") + ":" + String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
}
return (team &&
<div className='flex flex-col w-full h-full'>
<div className='flex flex-row gap-2'>
<div className='flex w-1/2 flex-col gap-2 h-min self-start'>
<div className='flex flex-row w-full h-full gap-2'>
<div className='flex w-1/2 flex-col h-1/2 gap-2'>
<h2 className='text-2xl text-center'>Actions</h2>
<form className='flex flex-row' onSubmit={handleRename}>
<div className='w-4/5'>
@@ -63,37 +80,47 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
</div>
</form>
<div className='flex flex-row'>
<BlueButton onClick={() => updateTeam(team.id, { captured: !team.captured })}>{team.captured ? "Revive" : "Capture"}</BlueButton>
<BlueButton onClick={() => {updateTeam(team.id, { captured: !team.captured }); team.finishDate = Date.now()}}>{team.captured ? "Revive" : "Capture"}</BlueButton>
<RedButton onClick={handleRemove}>Remove</RedButton>
</div>
<p className='text-2xl text-center w-full'>Starting zone</p>
<CircularAreaPicker area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} />
</div>
<div className='flex w-1/2 flex-col space-y-2 h-min self-start'>
<div className='flex w-1/2 flex-col h-min gap-2 items-center'>
<h2 className='text-2xl text-center'>Team details</h2>
<div className='w-3/5'>
<img className='self-stretch' ref={teamImage} onError={(e) => {e.target.src = "/images/missing_image.jpg"}} />
</div>
<div className='flex flex-col gap-3'>
<div>
<p>Secret : {String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, '$1 $2')}</p>
<p>Capture code : {String(team.captureCode).padStart(4, '0')}</p>
</div>
<div>
<p>Chasing : {getTeamName(team.chasing)}</p>
<p>Chased by : {getTeamName(team.chased)}</p>
</div>
{gameState == GameState.PLAYING &&
<div>
<p>Distance: { (team.distance / 1000).toFixed(1) }km</p>
<p>Time : {formatTimeHours(Math.floor((team.finishDate ? team.finishDate - startDate : Date.now() - startDate) / 1000))}</p>
<p>Average speed : {(avgSpeed*3.6).toFixed(1)}km/h</p>
<p>Captures : {team.nCaptures}</p>
<p>Sent location : {team.nSentLocation}</p>
<p>Oberved : {team.nObserved}</p>
</div>
}
<div>
<p>Phone model : {team.phoneModel ?? "Unknown"}</p>
<p>Phone name : {team.phoneName ?? "Unknown"}</p>
<p>Battery: {team.battery ? team.battery + "%" : "Unknown"}</p>
</div>
<div className='flex flex-row'>
<p>Penalties :</p>
<button className='w-7 h-7 mx-4 bg-blue-600 hover:bg-blue-500 text-md ease-out duration-200 text-white shadow-sm rounded' onClick={() => handleAddPenalty(-1)}>-</button>
<p>{team.penalties}</p>
<button className='w-7 h-7 mx-4 bg-blue-600 hover:bg-blue-500 text-md ease-out duration-200 text-white shadow-sm rounded' onClick={() => handleAddPenalty(1)}>+</button>
</div>
<br/>
</div>
</div>
</div>
<div className='flex flex-row'>
<p className='text-2xl text-center w-full'>Starting zone</p>
<p className='text-2xl text-center w-full'>Profile picture</p>
</div>
<div className='flex grow flex-row'>
<div className='w-1/2'>
<CircularAreaPicker area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} />
</div>
<div className='w-1/2'>
<img className='self-stretch' ref={teamImage} onError={(e) => {e.target.src = "/images/missing_image.jpg"}} />
</div>
</div>
</div>

View File

@@ -19,8 +19,9 @@ function AdminProvider({ children }) {
const { adminSocket } = useSocket();
const { loggedIn } = useAdminConnexion();
const [gameState, setGameState] = useState(GameState.SETUP);
const [startDate, setStartDate] = useState(null);
useSocketListener(adminSocket, "game_state", setGameState);
useSocketListener(adminSocket, "game_state", (data) => {setGameState(data.state); setStartDate(data.startDate)});
//Send a request to get the teams when the user logs in
useEffect(() => {
adminSocket.emit("get_teams");
@@ -46,7 +47,7 @@ function AdminProvider({ children }) {
useSocketListener(adminSocket, "zone_start", shrinking);
useSocketListener(adminSocket, "new_zone", waiting);
const value = useMemo(() => ({ zone, zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, isShrinking }), [zoneSettings, teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, isShrinking]);
const value = useMemo(() => ({ zone, zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, isShrinking, startDate }), [zoneSettings, teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, isShrinking, startDate]);
return (
<adminContext.Provider value={value}>
{children}