longevity: le cinque curve del questionario, verificate contro l oracolo
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Curve di normalizzazione del questionario Longevity: portano una risposta
|
||||
* grezza a un punteggio 0-100.
|
||||
*
|
||||
* `clamp`, `lerp`, `curvaCampana`, `curvaDecrescente`, `curvaCrescenteConPlateau`
|
||||
* e `curvaDirettaX10` sono il porting letterale di `clamp`, `_lerp`,
|
||||
* `score_bell_curve`, `score_decreasing`, `score_increasing_plateau` e
|
||||
* `score_direct_x10` dell'oracolo del cliente
|
||||
* (`tests/longevity/riferimento/isl_scoring_engine.py`, sezione «Questionario:
|
||||
* curve generiche»). Non toccare quella logica: è verificata riga per riga
|
||||
* contro `riferimento.json`.
|
||||
*
|
||||
* `curvaDirettaX10Invertita` e `curvaGradini` NON sono nell'oracolo Python:
|
||||
* vengono dal prototipo HTML del cliente (`scale10_inv`, `decstep`/`incstep`)
|
||||
* e non hanno un oracolo di riferimento — sono coperte solo dai test scritti
|
||||
* a mano in `tests/longevity/motore-curve.test.ts`.
|
||||
*/
|
||||
|
||||
/** Limita x all'intervallo [lo, hi]. Come `clamp` nell'oracolo. */
|
||||
export function clamp(x: number, lo = 0, hi = 100): number {
|
||||
return Math.max(lo, Math.min(hi, x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolazione lineare fra (x0, y0) e (x1, y1). Il fattore t è limitato a
|
||||
* [0, 1]: la curva non esce mai oltre y0/y1, anche se x è fuori da [x0, x1].
|
||||
* Come `_lerp` nell'oracolo.
|
||||
*/
|
||||
export function lerp(x: number, x0: number, x1: number, y0: number, y1: number): number {
|
||||
if (x1 === x0) return y0;
|
||||
let t = (x - x0) / (x1 - x0);
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
return y0 + t * (y1 - y0);
|
||||
}
|
||||
|
||||
/** Arrotonda a una cifra decimale, come `round(x, 1)` in Python. */
|
||||
function arrotonda1(x: number): number {
|
||||
return Math.round(x * 10) / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Curva a campana: punteggio massimo (100) fra peakLow e peakHigh, decresce
|
||||
* verso low/high ai lati. Come `score_bell_curve` nell'oracolo.
|
||||
*/
|
||||
export function curvaCampana(v: number, low: number, peakLow: number, peakHigh: number, high: number): number {
|
||||
if (v >= peakLow && v <= peakHigh) return 100;
|
||||
if (v < peakLow) return clamp(arrotonda1(lerp(v, low, peakLow, 10, 100)));
|
||||
return clamp(arrotonda1(lerp(v, peakHigh, high, 100, 10)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Curva decrescente: 100 a `best`, 0 a `worst`. Come `score_decreasing`
|
||||
* nell'oracolo — con `lerp` che limita t a [0,1], non scende sotto 0 né sale
|
||||
* sopra 100 fuori da [best, worst].
|
||||
*/
|
||||
export function curvaDecrescente(v: number, best: number, worst: number): number {
|
||||
return clamp(arrotonda1(lerp(v, best, worst, 100, 0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Curva crescente con plateau: 20 a `worst`, 100 da `plateauStart` in poi.
|
||||
* Come `score_increasing_plateau` nell'oracolo.
|
||||
*/
|
||||
export function curvaCrescenteConPlateau(v: number, worst: number, plateauStart: number): number {
|
||||
return clamp(arrotonda1(lerp(v, worst, plateauStart, 20, 100)));
|
||||
}
|
||||
|
||||
/** Punteggio diretto: valore 0-10 moltiplicato per 10. Come `score_direct_x10` nell'oracolo. */
|
||||
export function curvaDirettaX10(v: number): number {
|
||||
return clamp(arrotonda1(v * 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Complemento della diretta: valore 0-10, punteggio decrescente. Assente
|
||||
* nell'oracolo Python (è la quinta curva, quella che al motore del cliente
|
||||
* manca): serve a q_calo_pomeridiano, e nel prototipo HTML è `scale10_inv`,
|
||||
* cioè (10 - v) * 10.
|
||||
*/
|
||||
export function curvaDirettaX10Invertita(v: number): number {
|
||||
return clamp(arrotonda1((10 - v) * 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Curva a gradini: interpola fra coppie [valore, punteggio] ordinate per
|
||||
* valore crescente, esattamente come `decstep`/`incstep` nel prototipo HTML
|
||||
* del cliente (assente nell'oracolo Python).
|
||||
*
|
||||
* Se v è zero e zeroVal è definito, vince zeroVal. Altrimenti si interpola a
|
||||
* tratti fra i gradini, partendo da un punto d'ancoraggio (0, 100) per le
|
||||
* curve decrescenti o (0, 20) per le crescenti, e restando sull'ultimo
|
||||
* gradino oltre la sua ascissa (tipicamente un valore-soglia molto alto che
|
||||
* funge da "infinito").
|
||||
*/
|
||||
export function curvaGradini(
|
||||
v: number,
|
||||
steps: [number, number][],
|
||||
zeroVal: number | undefined,
|
||||
decrescente: boolean
|
||||
): number {
|
||||
if (v === 0 && zeroVal !== undefined) return zeroVal;
|
||||
|
||||
const ancoraggio: [number, number] = [0, decrescente ? 100 : 20];
|
||||
const punti: [number, number][] = [ancoraggio, ...steps];
|
||||
|
||||
for (let i = 0; i < punti.length - 1; i++) {
|
||||
const [x0, y0] = punti[i];
|
||||
const [x1, y1] = punti[i + 1];
|
||||
if (v <= x1) {
|
||||
return clamp(arrotonda1(lerp(v, x0, x1, y0, y1)));
|
||||
}
|
||||
}
|
||||
// v oltre l'ultimo gradino: resta sull'ultimo valore.
|
||||
return punti[punti.length - 1][1];
|
||||
}
|
||||
Reference in New Issue
Block a user