sbc-rating-calculator
Version:
A JavaScript library for calculating Squad Building Challenge (SBC) team ratings and finding optimal squad configurations for UT
189 lines (167 loc) • 6.42 kB
JavaScript
/**
* Helper class for SBC calculations and combinatorial operations
*/
class SolverHelper {
/**
* Calculate the number of multisubsets of size n from a set of given length
* @param {number} setLength - Length of the original set
* @param {number} n - Size of multisubsets to generate
* @returns {number} - Number of possible multisubsets
*/
static getMultisubsetsCount(setLength, n) {
if (n === 0 || setLength === 0) return n === 0 ? 1 : 0;
return (
this.factorial(setLength + n - 1) /
(this.factorial(setLength - 1) * this.factorial(n))
);
}
/**
* Calculate factorial of a number
* @param {number} num - Number to calculate factorial for
* @returns {number} - Factorial result
*/
static factorial(num) {
if (num < 0) return 0;
if (num === 0 || num === 1) return 1;
let result = 1;
for (let i = 2; i <= num; i++) {
result *= i;
}
return result;
}
/**
* Calculate team rating
* IMPORTANT: Always calculates squad rating as if there are 11 players total,
* padding missing slots with 0 rating.
* @param {number[]} ratings - Array of individual player ratings
* @returns {number} - Calculated team rating (floored)
*/
static getRating(ratings) {
if (!ratings || ratings.length === 0) return 0;
const paddedRatings = [...ratings];
while (paddedRatings.length < 11) {
paddedRatings.push(0);
}
const sum = paddedRatings.reduce((acc, curr) => acc + curr, 0);
const avg = sum / 11; // Always divide by 11
// Calculate correction factor for players above average
const correctionFactor = paddedRatings.reduce((acc, curr) => {
return curr > avg ? acc + (curr - avg) : acc;
}, 0);
// EA's formula: Round(sum + correction) / 11, then floor
const sumWithCorrection = sum + correctionFactor;
const rounded = Math.round(sumWithCorrection);
return Math.floor(rounded / 11);
}
/**
* Calculate total price for a set of ratings
* @param {number[]} ratings - Array of player ratings
* @param {Object.<number, number>} priceByRating - Mapping of ratings to prices
* @returns {number} - Total price
*/
static getPrice(ratings, priceByRating) {
return ratings.reduce(
(acc, curr) => acc + (priceByRating[curr] || 0),
0
);
}
/**
* Generate all multisubsets of size n from the given set
* @param {Array} set - The original set to generate subsets from
* @param {number} n - Size of each multisubset
* @returns {Generator} - Generator yielding multisubsets
*/
static getMultisubsets(set, n) {
return this.multisubsetsImpl(set, n);
}
/**
* Internal implementation for generating multisubsets
* @param {Array} set - The set to generate from
* @param {number} n - Size of subsets
* @returns {Generator} - Generator yielding multisubsets
*/
static* multisubsetsImpl(set, n) {
if (n === 0) {
yield [];
} else if (set.length > 0) {
const [x, ...rest] = set;
// Include x 0 to n times, then recurse on the rest
for (let i = 0; i <= n; i++) {
yield* this.prependNTimes(x, this.multisubsetsImpl(rest, n - i), i);
}
}
}
/**
* Prepend element 'a' n times to each array in the generator
* @param {*} a - Element to prepend
* @param {Generator} xss - Generator of arrays
* @param {number} n - Number of times to prepend
* @returns {Generator} - Generator yielding arrays with prepended elements
*/
static* prependNTimes(a, xss, n) {
const prefix = Array(n).fill(a);
for (let xs of xss) {
yield [...prefix, ...xs];
}
}
/**
* Validate input parameters for SBC calculations
* @param {Object} params - Parameters to validate
* @returns {Object} - Validation result
*/
static validateInputs(params) {
const { targetRating, existingRatings, availableRatings, squadSize } = params;
const errors = [];
if (typeof targetRating !== 'number' || targetRating < 45 || targetRating > 99) {
errors.push('Target rating must be a number between 45 and 99');
}
if (!Array.isArray(existingRatings)) {
errors.push('Existing ratings must be an array');
} else {
existingRatings.forEach((rating, index) => {
if (typeof rating !== 'number' || rating < 45 || rating > 99) {
errors.push(`Invalid rating at index ${index}: ${rating}`);
}
});
}
if (!Array.isArray(availableRatings)) {
errors.push('Available ratings must be an array');
} else {
availableRatings.forEach((rating, index) => {
if (typeof rating !== 'number' || rating < 45 || rating > 99) {
errors.push(`Invalid available rating at index ${index}: ${rating}`);
}
});
}
if (typeof squadSize !== 'number' || squadSize < 1 || squadSize > 23) {
errors.push('Squad size must be a number between 1 and 23');
}
if (existingRatings && squadSize && existingRatings.length > squadSize) {
errors.push('Existing ratings exceed squad size');
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Get unique ratings from an array
* @param {number[]} ratings - Array of ratings
* @returns {number[]} - Array of unique ratings sorted ascending
*/
static getUniqueRatings(ratings) {
return [...new Set(ratings)].sort((a, b) => a - b);
}
/**
* Count occurrences of each rating
* @param {number[]} ratings - Array of ratings
* @returns {Object.<number, number>} - Object mapping ratings to counts
*/
static countRatings(ratings) {
return ratings.reduce((acc, rating) => {
acc[rating] = (acc[rating] || 0) + 1;
return acc;
}, {});
}
}
module.exports = { SolverHelper };