UNPKG

sbc-rating-calculator

Version:

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

394 lines (348 loc) 15.3 kB
const { SolverHelper } = require('./SolverHelper.js'); const { getOptimalCombinations, isCombinationPossible, combinationToArray, calculateCombinationPoints } = require('./OptimalCombinations.js'); /** * 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 module.exports = { SBCRatingCalculator, calculateSquadSolutions, SolverHelper };