Player Rating Algorithm
Player ratings are automatically calculated from their stats when databases are loaded in the game. All player ratings are calculated using the algorithm below. Ratings are weighted averages of a player's stats, with their three worst stats omitted (ensuring that a striker's rating isn't unduly hindered by poor Tackling or Handling, for example).
Bear in mind that overall player ratings don't really mean anything in-game, except for summarising a player's ability quickly. Individual stats are used for all in-match calculations and transfer prices are calculated using a different algorithm which doesn't scale linearly with the rating algorithm. Also note that goalkeepers and outfield players use different weights to calculate their ratings. In the case where a player can both play in goal and outfield, the higher of the two calculated ratings is used.
Both code samples are identical in functionality, but are provided in both Lua and Javascript for flexibility and ease of reading.
Lua
local GK_RATING_WEIGHTS = { speed = 0.2, control = 0.2, passing = 0.3, shooting = 0.1, power = 0.6, tackling = 0.1, handling = 1, agility = 1, } local OUTFIELD_RATING_WEIGHTS = { speed = 0.7, control = 1, passing = 1, shooting = 1, power = 0.8, tackling = 0.9, handling = 0, agility = 0.8, } function table.find(tbl, element) for key, value in pairs(tbl) do if value == element then return key end end return nil end function table.keys(tbl) local keys = {} for key, _ in pairs(tbl) do table.insert(keys, key) end return keys end function table.values(tbl) local values = {} for _, value in pairs(tbl) do table.insert(values, value) end return values end local function getAverageRatingForWeights(stats, statWeights) local weightedStats = {} for statName, weight in pairs(statWeights) do local rawStatValue = stats[statName] local weightedStatValue = rawStatValue * weight table.insert(weightedStats, { value = weightedStatValue, rawValue = rawStatValue, }) end -- Order the weighted stats from highest to lowest. Ties are broken by -- whichever stat had the higher value before weighting. table.sort(weightedStats, (function (stat1, stat2) if stat1.value == stat2.value then return stat1.rawValue > stat2.rawValue end return stat1.value > stat2.value end)) -- Remove three worst stats. table.remove(weightedStats) table.remove(weightedStats) table.remove(weightedStats) -- Sum the remaining weighted stats and divide them by the sum of the original weights. local statTotal = 0 for _, weightedStat in ipairs(weightedStats) do statTotal = statTotal + weightedStat.value end local orderedWeights = table.values(statWeights) table.sort(orderedWeights) while #orderedWeights > #table.keys(weightedStats) do table.remove(orderedWeights, 1) end local weightTotal = 0 for _, weight in pairs(orderedWeights) do weightTotal = weightTotal + weight end return statTotal / weightTotal end local function getAverageRating(positions, stats) local isGoalkeeper = table.find(positions, "GK") local isOutfieldPlayer = #positions > 1 or positions[1] ~= "GK" local goalkeeperRating = 0 if isGoalkeeper then goalkeeperRating = getAverageRatingForWeights(stats, GK_RATING_WEIGHTS) end local outfieldRating = 0 if isOutfieldPlayer then outfieldRating = getAverageRatingForWeights(stats, OUTFIELD_RATING_WEIGHTS) end return math.max(outfieldRating, goalkeeperRating) end
Javascript
const GK_RATING_WEIGHTS = { speed: 0.2, control: 0.2, passing: 0.3, shooting: 0.1, power: 0.6, tackling: 0.1, handling: 1, agility: 1, }; const OUTFIELD_RATING_WEIGHTS = { speed: 0.7, control: 1, passing: 1, shooting: 1, power: 0.8, tackling: 0.9, handling: 0, agility: 0.8, }; function getAverageRatingForWeights(stats, statWeights) { const weightedStats = []; for (const [statName, weight] of Object.entries(statWeights)) { const rawStatValue = stats[statName]; const weightedStatValue = rawStatValue * weight; weightedStats.push({ value: weightedStatValue, rawValue: rawStatValue, }); } // Order the weighted stats from highest to lowest. Ties are broken by // whichever stat had the higher value before weighting. weightedStats.sort((stat1, stat2) => stat1.value == stat2.value ? stat2.rawValue - stat1.rawValue : stat2.value - stat1.value ); // Remove three worst stats. const topWeightedStats = weightedStats.slice(0, 5); // Sum the remaining weighted stats and divide them by the sum of the original weights. const statTotal = topWeightedStats.reduce( (accumulator, weightedStat) => accumulator + weightedStat.value, 0 ); const statWeightValues = Object.values(statWeights); statWeightValues.sort( (statWeight1, statWeight2) => statWeight2 - statWeight1 ); const orderedWeights = statWeightValues.slice(0, 5); const weightTotal = orderedWeights.reduce( (accumulator, statWeight) => accumulator + statWeight ); return statTotal / weightTotal; } function getAverageRating(positions, stats) { const isGoalkeeper = positions.includes("GK"); const isOutfieldPlayer = positions.length > 1 || positions[0] != "GK"; let goalkeeperRating = 0; if (isGoalkeeper) { goalkeeperRating = getAverageRatingForWeights(stats, GK_RATING_WEIGHTS); } let outfieldRating = 0; if (isOutfieldPlayer) { outfieldRating = getAverageRatingForWeights( stats, OUTFIELD_RATING_WEIGHTS ); } return Math.max(outfieldRating, goalkeeperRating); }