e29bc8ee80
After the binary search converges, also simulate predicted rating at required±1 average. Display the three rows in the modal so the user can see how sharp the requirement is — e.g. whether averaging 1 point lower costs them 1 point of predicted rating or 5.
82 lines
3.0 KiB
JavaScript
82 lines
3.0 KiB
JavaScript
const { calculatePredictedRating, getNextPDGAUpdateDate } = require('./rating-calculator');
|
|
const logger = require('../logger');
|
|
|
|
function calculateRequiredAverage(roundRatings, targetRating, numRounds) {
|
|
if (!Array.isArray(roundRatings) || roundRatings.length === 0) {
|
|
const err = new Error('No round history');
|
|
err.code = 'NO_ROUNDS';
|
|
throw err;
|
|
}
|
|
|
|
const currentPredicted = calculatePredictedRating(roundRatings).rating;
|
|
const nextUpdate = getNextPDGAUpdateDate();
|
|
const syntheticDate = new Date(nextUpdate.getTime() - 24 * 60 * 60 * 1000);
|
|
|
|
const simulate = (R) => {
|
|
const synthetic = [];
|
|
for (let i = 0; i < numRounds; i++) {
|
|
synthetic.push({ rating: R, date: syntheticDate, competition: 'TARGET_SIM' });
|
|
}
|
|
return calculatePredictedRating([...roundRatings, ...synthetic]).rating;
|
|
};
|
|
|
|
let lo = 400;
|
|
let hi = 1200;
|
|
let iterations = 0;
|
|
const maxIterations = 30;
|
|
let exactMatchAvg = null;
|
|
|
|
while (iterations < maxIterations && (hi - lo) >= 0.5) {
|
|
const mid = (lo + hi) / 2;
|
|
const predicted = simulate(mid);
|
|
if (predicted === targetRating) {
|
|
exactMatchAvg = mid;
|
|
// Narrow toward smaller R that still hits target — but break to bound iterations.
|
|
hi = mid;
|
|
} else if (predicted < targetRating) {
|
|
lo = mid;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
iterations++;
|
|
}
|
|
|
|
const candidate = exactMatchAvg !== null ? exactMatchAvg : (lo + hi) / 2;
|
|
const requiredAverage = Math.round(candidate * 10) / 10;
|
|
const simulatedPredicted = simulate(requiredAverage);
|
|
|
|
let warning = null;
|
|
if (requiredAverage >= 1199.5) {
|
|
warning = 'Target may be unreachable with this number of rounds within the simulated range [400, 1200].';
|
|
} else if (requiredAverage <= 400.5) {
|
|
warning = 'Required average is at the lower bound — target may already be exceeded or rounds would drag rating down.';
|
|
} else if (requiredAverage > 1050) {
|
|
warning = 'Required average is extremely high (>1050) — practically very difficult.';
|
|
} else if (requiredAverage < 600) {
|
|
warning = 'Required average is very low (<600) — check that your target is reasonable.';
|
|
} else if (iterations >= maxIterations && Math.abs(simulatedPredicted - targetRating) > 1) {
|
|
warning = 'Could not converge precisely; result is approximate.';
|
|
}
|
|
|
|
const lowerAvg = Math.max(400, Math.round((requiredAverage - 1) * 10) / 10);
|
|
const upperAvg = Math.min(1200, Math.round((requiredAverage + 1) * 10) / 10);
|
|
const sensitivity = {
|
|
lower: { average: lowerAvg, predicted: simulate(lowerAvg) },
|
|
target: { average: requiredAverage, predicted: simulatedPredicted },
|
|
upper: { average: upperAvg, predicted: simulate(upperAvg) }
|
|
};
|
|
|
|
logger.debug(`Target rating calc: target=${targetRating} rounds=${numRounds} → avg=${requiredAverage} (iterations=${iterations}, simulated=${simulatedPredicted})`);
|
|
|
|
return {
|
|
requiredAverage,
|
|
currentPredicted,
|
|
simulatedPredicted,
|
|
iterations,
|
|
warning,
|
|
sensitivity
|
|
};
|
|
}
|
|
|
|
module.exports = { calculateRequiredAverage };
|