fix(app): proxy photo opaque + EXIF strip + WebP (C4), radius_eff communes immenses (A1), variante no-frwiki (A3), cache wiki 24h
This commit is contained in:
@@ -74,7 +74,14 @@ export function getRandomLieu(options: {
|
||||
return pool[Math.floor(Math.random() * pool.length)];
|
||||
}
|
||||
|
||||
/** Anti-triche : ne garde que les champs publiques pour /api/round. */
|
||||
/** Largeurs (px) servies par le proxy photo /api/photo/[id]?w=. */
|
||||
export const PHOTO_LARGEURS = [400, 800, 1200] as const;
|
||||
|
||||
/**
|
||||
* Anti-triche : ne garde que les champs publiques pour /api/round.
|
||||
* L'URL photo passe par le proxy serveur (/api/photo/<uuid>) : l'URL Wikimedia
|
||||
* Commons d'origine contient le nom du fichier, qui révèle souvent le lieu (C4).
|
||||
*/
|
||||
export function sanitizeRound(lieu: Lieu): RoundPlace {
|
||||
return {
|
||||
id: lieu.id,
|
||||
@@ -82,15 +89,41 @@ export function sanitizeRound(lieu: Lieu): RoundPlace {
|
||||
nom_creole: lieu.nom_creole,
|
||||
categorie: lieu.categorie,
|
||||
difficulte: lieu.difficulte,
|
||||
photo: lieu.photo,
|
||||
photo: {
|
||||
url: `/api/photo/${lieu.id}`,
|
||||
srcset: {
|
||||
'400': `/api/photo/${lieu.id}?w=400`,
|
||||
'800': `/api/photo/${lieu.id}?w=800`,
|
||||
'1200': `/api/photo/${lieu.id}?w=1200`
|
||||
},
|
||||
credit: lieu.photo.credit,
|
||||
alt: lieu.photo.alt
|
||||
},
|
||||
indices: lieu.indices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rayon effectif (km) de la courbe de score (audit A1).
|
||||
* Les communes guyanaises sont immenses (Saint-Laurent-du-Maroni ≈ 4 321 km²) :
|
||||
* avec le seul rayon de difficulté, un guess à quelques dizaines de km y est
|
||||
* injustement quasi nul. Quand `aire_commune_km2` est renseigné, le rayon est
|
||||
* au moins celui du disque de même aire que la commune : sqrt(aire / π).
|
||||
* Champ optionnel : sans lui, on retombe sur le rayon de difficulté.
|
||||
*/
|
||||
export function rayonEffectifKm(lieu: Lieu): number {
|
||||
const base = RAYONS[lieu.difficulte];
|
||||
const aire = lieu.aire_commune_km2;
|
||||
if (typeof aire === 'number' && Number.isFinite(aire) && aire > 0) {
|
||||
return Math.max(base, Math.sqrt(aire / Math.PI));
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scoring recalculé côté serveur :
|
||||
* score = round(5000 × e^(−d/r)), r selon difficulté ;
|
||||
* pénalité −10 % par indice ; bonus temps ≤ +20 % ; total plafonné à 5000.
|
||||
* score = round(5000 × e^(−d/r_eff)) ; pénalité −10 % par indice ;
|
||||
* bonus temps ≤ +20 % ; total plafonné à 5000.
|
||||
*/
|
||||
export function scoreGuess(
|
||||
lieu: Lieu,
|
||||
@@ -99,7 +132,8 @@ export function scoreGuess(
|
||||
timeMs?: number
|
||||
): GuessResult {
|
||||
const distanceKm = haversineKm(guess, lieu.coordonnees);
|
||||
const base = Math.round(MAX_POINTS * Math.exp(-distanceKm / RAYONS[lieu.difficulte]));
|
||||
const radiusEff = rayonEffectifKm(lieu);
|
||||
const base = Math.round(MAX_POINTS * Math.exp(-distanceKm / radiusEff));
|
||||
const penalises = base * Math.max(0, 1 - 0.1 * Math.max(0, hintsUsed));
|
||||
let total = penalises;
|
||||
if (typeof timeMs === 'number' && Number.isFinite(timeMs) && timeMs >= 0) {
|
||||
@@ -109,6 +143,7 @@ export function scoreGuess(
|
||||
return {
|
||||
distanceKm: Math.round(distanceKm * 10) / 10,
|
||||
score,
|
||||
radiusEffKm: Math.round(radiusEff * 10) / 10,
|
||||
coordonnees: lieu.coordonnees,
|
||||
commune: lieu.commune,
|
||||
region: lieu.region,
|
||||
@@ -123,6 +158,7 @@ export function skipResult(lieu: Lieu): GuessResult {
|
||||
return {
|
||||
distanceKm: null,
|
||||
score: 0,
|
||||
radiusEffKm: null,
|
||||
coordonnees: lieu.coordonnees,
|
||||
commune: lieu.commune,
|
||||
region: lieu.region,
|
||||
@@ -132,10 +168,14 @@ export function skipResult(lieu: Lieu): GuessResult {
|
||||
};
|
||||
}
|
||||
|
||||
/** Catalogue Explorer : coords arrondies à 0.05° (mode sans score, fiches autorisées). */
|
||||
/**
|
||||
* Catalogue Explorer : coords arrondies à 0.05° (mode sans score, fiches autorisées).
|
||||
* L'`id` (uuid) n'est volontairement PAS exposé : il permettrait de corréler
|
||||
* l'id d'un round avec la commune du lieu (fuite de réponse, audit C4).
|
||||
* L'Explorer n'a pas besoin d'ids stables (le front utilise l'indice du tableau).
|
||||
*/
|
||||
export function catalogueExplorer(): ExplorerLieu[] {
|
||||
return lieux.map((l) => ({
|
||||
id: l.id,
|
||||
nom: l.nom,
|
||||
nom_creole: l.nom_creole,
|
||||
region: l.region,
|
||||
|
||||
Reference in New Issue
Block a user