UNPKG

sbc-rating-calculator

Version:

A JavaScript library for calculating Squad Building Challenge (SBC) team ratings and finding optimal squad configurations for UT

810 lines (712 loc) 29.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * Helper class for SBC calculations and combinatorial operations */ var SolverHelper_1; var hasRequiredSolverHelper; function requireSolverHelper () { if (hasRequiredSolverHelper) return SolverHelper_1; hasRequiredSolverHelper = 1; 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; }, {}); } } SolverHelper_1 = { SolverHelper }; return SolverHelper_1; } /** * Pre-calculated optimal SBC combinations for ratings 80-92 * Each combination is sorted by rating efficiency (lowest total rating points) * Format: { rating: count } where rating is player rating and count is how many needed */ var OptimalCombinations; var hasRequiredOptimalCombinations; function requireOptimalCombinations () { if (hasRequiredOptimalCombinations) return OptimalCombinations; hasRequiredOptimalCombinations = 1; const OPTIMAL_SBC_COMBINATIONS = { 80: [ { 82: 1, 81: 2, 80: 8 }, // 901 total points { 83: 1, 80: 10 }, // 913 total points { 82: 2, 80: 9 }, // 904 total points { 81: 4, 80: 7 }, // 884 total points { 84: 1, 79: 10 }, // 874 total points { 82: 3, 79: 8 }, // 878 total points ], 81: [ { 82: 3, 81: 8 }, // 894 total points { 83: 1, 81: 10 }, // 893 total points { 84: 1, 80: 10 }, // 884 total points { 82: 5, 80: 6 }, // 890 total points { 83: 2, 80: 9 }, // 886 total points { 85: 1, 79: 10 }, // 875 total points ], 82: [ { 83: 2, 82: 9 }, // 904 total points { 84: 1, 82: 10 }, // 904 total points { 83: 4, 81: 7 }, // 899 total points { 85: 1, 81: 10 }, // 895 total points { 84: 2, 81: 9 }, // 897 total points { 86: 1, 80: 10 }, // 886 total points ], 83: [ { 83: 9, 82: 2 }, // 911 total points (OPTIMAL) { 84: 1, 83: 6, 82: 4 }, // 910 total points { 85: 1, 82: 10 }, // 905 total points { 84: 2, 83: 7, 82: 2 }, // 913 total points { 86: 1, 82: 10 }, // 902 total points { 84: 3, 82: 8 }, // 908 total points ], 84: [ { 84: 11 }, // 924 total points { 85: 1, 84: 8, 83: 2 }, // 924 total points { 86: 1, 83: 10 }, // 916 total points { 85: 2, 84: 7, 83: 2 }, // 924 total points { 87: 1, 83: 10 }, // 917 total points { 85: 3, 83: 8 }, // 919 total points ], 85: [ { 85: 11 }, // 935 total points { 86: 1, 85: 8, 84: 2 }, // 935 total points { 87: 1, 84: 10 }, // 927 total points { 86: 2, 85: 7, 84: 2 }, // 935 total points { 88: 1, 84: 10 }, // 928 total points { 86: 3, 84: 8 }, // 930 total points ], 86: [ { 86: 11 }, // 946 total points { 87: 1, 86: 8, 85: 2 }, // 946 total points { 88: 1, 85: 10 }, // 938 total points { 87: 2, 86: 7, 85: 2 }, // 946 total points { 89: 1, 85: 10 }, // 939 total points { 87: 3, 85: 8 }, // 941 total points ], 87: [ { 87: 11 }, // 957 total points { 88: 1, 87: 8, 86: 2 }, // 957 total points { 89: 1, 86: 10 }, // 949 total points { 88: 2, 87: 7, 86: 2 }, // 957 total points { 90: 1, 86: 10 }, // 950 total points { 88: 3, 86: 8 }, // 952 total points ], 88: [ { 88: 11 }, // 968 total points { 89: 1, 88: 8, 87: 2 }, // 968 total points { 90: 1, 87: 10 }, // 960 total points { 89: 2, 88: 7, 87: 2 }, // 968 total points { 91: 1, 87: 10 }, // 961 total points { 89: 3, 87: 8 }, // 963 total points ], 89: [ { 89: 11 }, // 979 total points { 90: 1, 89: 8, 88: 2 }, // 979 total points { 91: 1, 88: 10 }, // 971 total points { 90: 2, 89: 7, 88: 2 }, // 979 total points { 92: 1, 88: 10 }, // 972 total points { 90: 3, 88: 8 }, // 974 total points ], 90: [ { 90: 11 }, // 990 total points { 91: 1, 90: 8, 89: 2 }, // 990 total points { 92: 1, 89: 10 }, // 982 total points { 91: 2, 90: 7, 89: 2 }, // 990 total points { 93: 1, 89: 10 }, // 983 total points { 91: 3, 89: 8 }, // 985 total points ], 91: [ { 91: 11 }, // 1001 total points { 92: 1, 91: 8, 90: 2 }, // 1001 total points { 93: 1, 90: 10 }, // 993 total points { 92: 2, 91: 7, 90: 2 }, // 1001 total points { 94: 1, 90: 10 }, // 994 total points { 92: 3, 90: 8 }, // 996 total points ], 92: [ { 92: 11 }, // 1012 total points { 93: 1, 92: 8, 91: 2 }, // 1012 total points { 94: 1, 91: 10 }, // 1004 total points { 93: 2, 92: 7, 91: 2 }, // 1012 total points { 95: 1, 91: 10 }, // 1005 total points { 93: 3, 91: 8 }, // 1007 total points ] }; /** * Get optimal combinations for a target rating * @param {number} targetRating - Target squad rating (80-92) * @returns {Array} - Array of optimal combinations sorted by efficiency */ function getOptimalCombinations(targetRating) { if (targetRating < 80 || targetRating > 92) { return []; } return OPTIMAL_SBC_COMBINATIONS[targetRating] || []; } /** * Check if a combination is possible with available cards * @param {Object} combination - Rating combination {rating: count} * @param {Object} availableCounts - Available card counts {rating: count} * @returns {boolean} - True if combination is possible */ function isCombinationPossible(combination, availableCounts) { for (const [rating, needed] of Object.entries(combination)) { const available = availableCounts[parseInt(rating)] || 0; if (available < needed) { return false; } } return true; } /** * Convert combination object to array format for SBC solver * @param {Object} combination - Rating combination {rating: count} * @returns {Array} - Array of individual ratings */ function combinationToArray(combination) { const result = []; for (const [rating, count] of Object.entries(combination)) { for (let i = 0; i < count; i++) { result.push(parseInt(rating)); } } return result; } /** * Calculate total rating points for a combination * @param {Object} combination - Rating combination {rating: count} * @returns {number} - Total rating points */ function calculateCombinationPoints(combination) { let total = 0; for (const [rating, count] of Object.entries(combination)) { total += parseInt(rating) * count; } return total; } OptimalCombinations = { OPTIMAL_SBC_COMBINATIONS, getOptimalCombinations, isCombinationPossible, combinationToArray, calculateCombinationPoints }; return OptimalCombinations; } var src; var hasRequiredSrc; function requireSrc () { if (hasRequiredSrc) return src; hasRequiredSrc = 1; const { SolverHelper } = requireSolverHelper(); const { getOptimalCombinations, isCombinationPossible, combinationToArray, calculateCombinationPoints } = requireOptimalCombinations(); /** * SBC Rating Calculator - A comprehensive library for Squad Building Challenges */ class SBCRatingCalculator { constructor(options = {}) { this.defaultSquadSize = options.defaultSquadSize || 11; this.defaultMaxSolutions = options.defaultMaxSolutions || 50; } /** * Calculate the team rating based on individual player ratings * Always calculates as if there are 11 players, padding missing slots with 0 * @param {number[]} ratings - Array of player ratings * @returns {number} - Calculated team rating */ calculateTeamRating(ratings) { return SolverHelper.getRating(ratings); } /** * Find optimal SBC solutions using pre-calculated combinations (fast method for large inventories) * @param {Object} options - Configuration options * @param {number} options.targetRating - The desired overall squad rating (80-92) * @param {number[]} options.availableRatings - Large array of available player ratings * @param {Object.<number, number>} options.priceByRating - Mapping of player ratings to their prices * @param {number} options.maxSolutions - Maximum number of solutions to return * @returns {Object} - Result object with optimal solutions */ findOptimalSolutions(options) { const { targetRating, availableRatings = [], priceByRating = {}, maxSolutions = 10 } = options; // Get pre-calculated optimal combinations const optimalCombinations = getOptimalCombinations(targetRating); if (optimalCombinations.length === 0) { return { solutionsFound: 0, solutions: [], message: `No optimal combinations available for rating ${targetRating}. Use findSquadSolutions for custom search.` }; } // Count available cards const availableCounts = SolverHelper.countRatings(availableRatings); const solutions = []; for (const combination of optimalCombinations.slice(0, maxSolutions)) { // Check if this combination is possible with available cards if (isCombinationPossible(combination, availableCounts)) { // Calculate price and create solution const ratingArray = combinationToArray(combination); const price = SolverHelper.getPrice(ratingArray, priceByRating); const totalRatingPoints = calculateCombinationPoints(combination); // Convert to squad format const squad = Object.entries(combination).map(([rating, count]) => ({ rating: parseInt(rating), count: count })); solutions.push({ price: price, squad: squad, actualRating: targetRating, totalRatingPoints: totalRatingPoints, efficiency: totalRatingPoints / targetRating, isOptimal: true }); } } return { solutionsFound: solutions.length, solutions: solutions.sort((a, b) => a.totalRatingPoints - b.totalRatingPoints) }; } /** * Calculate possible squad configurations to meet a target rating for an SBC * Prioritizes solutions by rating efficiency (lowest total rating points used) * @param {Object} options - Configuration options * @param {number} options.targetRating - The desired overall squad rating * @param {number[]} options.existingRatings - Ratings of players already in the squad * @param {number[]} options.availableRatings - Ratings of players available to fill the squad * @param {Object.<number, number>} options.priceByRating - Mapping of player ratings to their prices * @param {number} options.squadSize - The total number of players in the squad * @param {number} options.maxSolutions - The maximum number of solutions to return * @param {boolean} options.sortByPrice - If true, sort by price; if false, sort by rating efficiency * @param {boolean} options.useOptimalCombinations - If true, use pre-calculated optimal combinations for speed * @returns {Object} - Result object with solutions found and solution details */ findSquadSolutions(options) { const { targetRating, existingRatings = [], availableRatings = [], priceByRating = {}, squadSize = this.defaultSquadSize, maxSolutions = this.defaultMaxSolutions, sortByPrice = true, useOptimalCombinations = true } = options; // If no existing ratings and target is in optimal range, use fast method if (existingRatings.length === 0 && useOptimalCombinations && targetRating >= 80 && targetRating <= 92 && availableRatings.length > 50) { const optimalResult = this.findOptimalSolutions({ targetRating, availableRatings, priceByRating, maxSolutions }); if (optimalResult.solutionsFound > 0) { return optimalResult; } } // Validate inputs const validation = SolverHelper.validateInputs({ targetRating, existingRatings, availableRatings: availableRatings, squadSize }); if (!validation.valid) { return { error: validation.errors.join(', '), solutionsFound: 0, solutions: [] }; } return calculateSquadSolutions( { targetRating, existingRatings, ratingsToTry: availableRatings, priceByRating }, squadSize, maxSolutions, sortByPrice ); } /** * Find the most efficient squad solutions (lowest rating cost) * @param {Object} options - Same as findSquadSolutions but sorted by efficiency * @returns {Object} - Solutions sorted by rating efficiency */ findMostEfficientSolutions(options) { return this.findSquadSolutions({ ...options, sortByPrice: false }); } /** * Calculate the minimum rating needed for remaining players to reach target * @param {number} targetRating - Desired team rating * @param {number[]} existingRatings - Current player ratings * @param {number} remainingSlots - Number of players still to add * @param {number} squadSize - Total squad size (default: 11) * @returns {number} - Minimum rating needed for each remaining player */ calculateMinimumRatingNeeded(targetRating, existingRatings, remainingSlots, squadSize = 11) { const currentSum = existingRatings.reduce((sum, rating) => sum + rating, 0); const targetSum = targetRating * 11; // Always 11 for EA const remainingSum = targetSum - currentSum; return Math.ceil(remainingSum / remainingSlots); } /** * Validate if a squad configuration can achieve the target rating * @param {number[]} ratings - All player ratings in the squad * @param {number} targetRating - Desired team rating * @param {number} squadSize - Total squad size (default: 11) * @returns {Object} - Validation result with success status and actual rating */ validateSquad(ratings, targetRating, squadSize = 11) { // For validation, we check if it's a full 11-player squad if (ratings.length !== squadSize) { return { valid: false, message: `Squad must have exactly ${squadSize} players`, actualRating: null }; } const actualRating = this.calculateTeamRating(ratings); const valid = actualRating >= targetRating; return { valid, actualRating, targetRating, message: valid ? `Squad achieves target rating (${actualRating} >= ${targetRating})` : `Squad rating too low (${actualRating} < ${targetRating})` }; } /** * Get statistics about available player ratings * @param {number[]} ratings - Array of available player ratings * @returns {Object} - Statistical information about the ratings */ getRatingStatistics(ratings) { if (!ratings || ratings.length === 0) { return { min: 0, max: 0, average: 0, median: 0, count: 0 }; } const sorted = [...ratings].sort((a, b) => a - b); const count = ratings.length; const sum = ratings.reduce((acc, rating) => acc + rating, 0); const average = Math.round(sum / count * 100) / 100; const median = count % 2 === 0 ? (sorted[count / 2 - 1] + sorted[count / 2]) / 2 : sorted[Math.floor(count / 2)]; return { min: sorted[0], max: sorted[count - 1], average, median, count }; } } /** * Calculate possible squad configurations to meet a target rating for an SBC. * Enhanced to prioritize solutions by rating efficiency or price. * * @param {Object} solverOptions - Options to configure the SBC solver. * @param {number} solverOptions.targetRating - The desired overall squad rating. * @param {number[]} solverOptions.existingRatings - Ratings of players already in the squad. * @param {number[]} solverOptions.ratingsToTry - Ratings of players available to fill the squad. * @param {Object.<number, number>} solverOptions.priceByRating - Mapping of player ratings to their prices. * @param {number} SQUAD_SIZE - The total number of players in the squad. * @param {number} MAX_SOLUTIONS_TO_TAKE - The maximum number of solutions to return. * @param {boolean} sortByPrice - If true, sort by price; if false, sort by rating efficiency. * * @returns {Object} - An object containing the number of solutions found and the solutions themselves. * @returns {number} return.solutionsFound - The number of valid squad configurations found. * @returns {Array<Object>} return.solutions - An array of solutions, each with a squad configuration and its cost. */ function calculateSquadSolutions(solverOptions, SQUAD_SIZE, MAX_SOLUTIONS_TO_TAKE, sortByPrice = true) { try { const { targetRating, existingRatings, ratingsToTry, priceByRating } = solverOptions; let solutions = []; if (targetRating > 99 || targetRating < 45) { return { solutionsFound: 0, solutions: [] }; } const remainingSlots = SQUAD_SIZE - existingRatings.length; if (remainingSlots <= 0) { // Check if existing squad already meets the requirement const currentRating = SolverHelper.getRating(existingRatings); if (currentRating >= targetRating) { return { solutionsFound: 1, solutions: [{ price: 0, squad: [], actualRating: currentRating, totalRatingPoints: existingRatings.reduce((a, b) => a + b, 0), efficiency: 0 }] }; } else { return { solutionsFound: 0, solutions: [] }; } } // Count available ratings for better optimization const availableCounts = SolverHelper.countRatings(ratingsToTry); const combinations = SolverHelper.getMultisubsets(ratingsToTry, remainingSlots); for (const combination of combinations) { // Check if we have enough cards of each rating const neededCounts = SolverHelper.countRatings(combination); let hasEnoughCards = true; for (const [rating, needed] of Object.entries(neededCounts)) { if ((availableCounts[rating] || 0) < needed) { hasEnoughCards = false; break; } } if (!hasEnoughCards) continue; const fullSquad = [...existingRatings, ...combination]; const rating = SolverHelper.getRating(fullSquad); if (rating < targetRating) { continue; } const ratingCounts = combination.reduce(function(acc, curr) { acc[curr] = (acc[curr] || 0) + 1; return acc; }, {}); const squad = Object.keys(ratingCounts).map(function(rating) { return { rating: +rating, count: ratingCounts[+rating] }; }); const totalRatingPoints = fullSquad.reduce((a, b) => a + b, 0); const price = SolverHelper.getPrice(combination, priceByRating); // Calculate efficiency (lower is better) const efficiency = totalRatingPoints / Math.max(rating, 1); solutions.push({ price: price, squad: squad, actualRating: rating, totalRatingPoints: totalRatingPoints, efficiency: efficiency, combinationUsed: combination }); } // Sort by price or efficiency if (sortByPrice) { solutions.sort(function(a, b) { // Primary: price, Secondary: efficiency (rating points) if (a.price !== b.price) { return a.price - b.price; } return a.totalRatingPoints - b.totalRatingPoints; }); } else { solutions.sort(function(a, b) { // Primary: total rating points (efficiency), Secondary: price if (a.totalRatingPoints !== b.totalRatingPoints) { return a.totalRatingPoints - b.totalRatingPoints; } return a.price - b.price; }); } if (MAX_SOLUTIONS_TO_TAKE && MAX_SOLUTIONS_TO_TAKE > 0) { solutions = solutions.slice(0, MAX_SOLUTIONS_TO_TAKE); } return { solutionsFound: solutions.length, solutions: solutions }; } catch (error) { return { error: error.message || "An error occurred", solutionsFound: 0, solutions: [] }; } } // CommonJS exports src = { SBCRatingCalculator, calculateSquadSolutions, SolverHelper }; return src; } var srcExports = requireSrc(); var index = srcExports.default; exports.default = index; //# sourceMappingURL=index.js.map