Bang Average Football logo

Custom Content & Steam Workshop

Guides for Bang Average Football


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