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);
}
