Skip to content

Instantly share code, notes, and snippets.

@PhiBabin
Created December 29, 2025 22:14
Show Gist options
  • Select an option

  • Save PhiBabin/947485050b3036fd6dbdd7bcb1d5d8cd to your computer and use it in GitHub Desktop.

Select an option

Save PhiBabin/947485050b3036fd6dbdd7bcb1d5d8cd to your computer and use it in GitHub Desktop.

Revisions

  1. PhiBabin created this gist Dec 29, 2025.
    939 changes: 939 additions & 0 deletions game_autocolors_improved.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,939 @@
    local gadget = gadget ---@type Gadget

    function gadget:GetInfo()
    return {
    name = "AutoColorPicker",
    desc = "Automatically assigns colors to teams",
    author = "Damgam, Born2Crawl (color palette), kafka42",
    date = "2021",
    license = "GNU GPL, v2 or later",
    layer = -100,
    enabled = true,
    }
    end

    local math_pow = math.pow

    local anonymousMode = Spring.GetModOptions().teamcolors_anonymous_mode
    local gaiaTeamID = Spring.GetGaiaTeamID()
    local teamList = Spring.GetTeamList()
    local allyTeamList = Spring.GetAllyTeamList()
    local allyTeamCount = #allyTeamList - 1
    local isSurvival = Spring.Utilities.Gametype.IsPvE()

    local survivalColorNum = 1 -- Starting from color #1
    local survivalColorVariation = 0 -- Current color variation
    local allyTeamNum = 0
    local teamSizes = {}

    local myAllyTeamID, myTeamID
    if not gadgetHandler:IsSyncedCode() then
    myAllyTeamID = Spring.GetMyAllyTeamID()
    myTeamID = Spring.GetMyTeamID()
    end

    -- Special colors
    local armBlueColor = "#004DFF" -- Armada Blue
    local corRedColor = "#FF1005" -- Cortex Red
    local scavPurpColor = "#6809A1" -- Scav Purple
    local raptorOrangeColor = "#CC8914" -- Raptor Orange
    local gaiaGrayColor = "#7F7F7F" -- Gaia Grey
    local legGreenColor = "#0CE818" -- Legion Green

    -- NEW IceXuick Colors V6
    local ffaColors = {
    "#004DFF", -- 1
    "#FF1005", -- 2
    "#0CE908", -- 3
    "#FFD200", -- 4
    "#F80889", -- 5
    "#09F5F5", -- 6
    "#FF6107", -- 7
    "#F190B3", -- 8
    "#097E1C", -- 9
    "#C88B2F", -- 10
    "#7CA1FF", -- 11
    "#9F0D05", -- 12
    "#3EFFA2", -- 13
    "#F5A200", -- 14
    "#C4A9FF", -- 15
    "#0B849B", -- 16
    "#B4FF39", -- 17
    "#FF68EA", -- 18
    "#D8EEFF", -- 19
    "#689E3D", -- 20
    "#B04523", -- 21
    "#FFBB7C", -- 22
    "#3475FF", -- 23
    "#DD783F", -- 24
    "#FFAAF3", -- 25
    "#4A4376", -- 26
    "#773A01", -- 27
    "#B7EA63", -- 28
    "#764A4A", -- 29
    "#7EB900", -- 30
    }
    -- delete excess so a table shuffe wont use the colors added on the bottom
    if #ffaColors > #teamList-1 then
    for i = #teamList, #ffaColors do
    ffaColors[i] = nil
    end
    end

    -- Tailwind v4 color palette (Mostly brightness 200 to 800)
    -- Note: the official color palette used P3 colors (which are meant for HDR displays), this is the fallback RGB colors.
    local gradients = {
    blue = {{190, 219, 255}, {142, 197, 255}, {81, 162, 255}, {43, 127, 255}, {21, 93, 252}, {20, 71, 230}, {25, 60, 184}},
    cyan = {{162, 244, 253}, {83, 234, 253}, {0, 211, 242}, {0, 184, 219}, {0, 146, 184}, {0, 117, 149}, {0, 95, 120}},
    violet = {{221, 214, 255}, {196, 180, 255}, {166, 132, 255}, {142, 81, 255}, {127, 34, 254}, {112, 8, 231}, {93, 14, 192}, {77, 23, 154}},
    fusia = {{246, 207, 255}, {244, 168, 255}, {237, 106, 255}, {225, 42, 251}, {200, 0, 222}, {168, 0, 183}, {138, 1, 148}},
    pink = {{253, 165, 213}, {251, 100, 182}, {246, 51, 154}, {230, 0, 118}, {198, 0, 92}, {163, 0, 76}, {134, 16, 67}},
    red = {{255, 201, 201}, {255, 162, 162}, {255, 100, 103}, {251, 44, 54}, {231, 0, 11}, {193, 0, 7}, {159, 7, 18}},
    orange = {{255, 214, 167}, {255, 184, 106}, {255, 137, 4}, {255, 105, 0}, {245, 73, 0}, {202, 53, 0}},
    yellow = {{255, 240, 133}, {255, 223, 32}, {253, 199, 0}, {240, 177, 0}, {208, 135, 0}, {166, 95, 0}, {137, 75, 0}},
    green = {{185, 248, 207}, {123, 241, 168}, {5, 223, 114}, {0, 201, 80}, {0, 166, 62}, {0, 130, 54}, {1, 102, 48}},
    lime = {{236, 252, 202}, {216, 249, 153}, {187, 244, 81}, {154, 230, 0}, {124, 207, 0}, {94, 165, 0}, {73, 125, 0}, {60, 99, 0}, {53, 83, 14}},
    teal = {{150, 247, 228}, {70, 236, 213}, {0, 213, 190}, {0, 187, 167}, {0, 150, 137}, {0, 120, 111}, {0, 95, 90}},
    sky = {{184, 230, 254}, {116, 212, 255}, {0, 188, 255}, {0, 166, 244}, {0, 132, 209}, {0, 105, 168}, {0, 89, 138}},
    amber = {{254, 230, 133}, {255, 210, 48}, {255, 185, 0}, {254, 154, 0}, {225, 113, 0}, {187, 77, 0}, {151, 60, 0}}, -- very similar to orange, so only use it in the 4 teams case
    }

    local allGrandientNames = {"blue", "red", "green", "yellow", "fusia", "teal", "orange", "pink", "lime", "violet", "cyan", "sky"}

    local gradientGroupPerNumTeams = {
    -- One team
    {
    {"blue", "sky", "violet", "green", "lime"}, -- cold
    },
    -- 2 teams
    {
    {"blue", "sky", "violet", "green", "lime"}, -- cold
    {"red", "pink", "fusia", "orange", "yellow"}, -- warm
    },
    -- 3 teams
    {
    {"blue", "sky", "violet"}, -- blue
    {"red", "orange", "yellow"}, -- red
    {"green", "lime", "teal"}, -- green
    },
    -- 4 teams
    {
    {"blue", "sky", "violet"}, -- blue
    {"red", "pink", "fusia"}, -- red pink
    {"green", "lime", "teal"}, -- green
    {"orange", "yellow"}, -- yellow orange
    },
    -- 5 teams
    {
    {"blue", "sky"}, -- blue
    {"red", "pink"}, -- red pink
    {"green", "lime"}, -- green
    {"orange", "yellow"},
    {"violet", "fusia"},
    },
    -- 6 teams
    {
    {"blue", "sky"}, -- blue
    {"red", "pink"}, -- red pink
    {"green", "lime"}, -- green
    {"orange", "yellow"},
    {"violet", "fusia"},
    {"teal", "cyan"},
    },
    }



    local survivalColors = {
    "#0B3EF3", -- 1
    "#FF1005", -- 2
    "#0CE908", -- 3
    "#ffab8c", -- 4
    "#09F5F5", -- 5
    "#FCEEA4", -- 6
    "#097E1C", -- 7
    "#F190B3", -- 8
    "#F80889", -- 9
    "#3EFFA2", -- 10
    "#911806", -- 11
    "#7CA1FF", -- 12
    "#3c7a74", -- 13
    "#B04523", -- 14
    "#B4FF39", -- 15
    "#773A01", -- 16
    "#D8EEFF", -- 17
    "#689E3D", -- 18
    "#0B849B", -- 19
    "#FFD200", -- 20
    "#971C48", -- 21
    "#4A4376", -- 22
    "#764A4A", -- 23
    "#4F2684", -- 24
    }

    local teamColors = {
    { -- One Team (not possible)
    { -- First Team
    "#004DFF", -- Armada Blue
    },
    },

    { -- Two Teams (40 colors)
    { -- First Team (Cool)
    "#0B3EF3", --1
    "#0CE908", --2
    "#00f5e5", --3
    "#6941f2", --4
    "#8fff94", --5
    "#1b702f", --6
    "#7cc2ff", --7
    "#a294ff", --8
    "#0B849B", --9
    "#689E3D", --10
    "#4F2684", --11
    "#2C32AC", --12
    "#6968A0", --13
    "#D8EEFF", --14
    "#3475FF", --15
    "#7EB900", --16
    "#4A4376", --17
    "#B7EA63", --18
    "#C4A9FF", --19
    "#37713A", --20
    },
    { -- Second Team (Warm)
    "#FF1005", --1
    "#FFD200", --2
    "#FF6107", --3
    "#F80889", --4
    "#FCEEA4", --5
    "#8a2828", --6
    "#F190B3", --7
    "#C88B2F", --8
    "#B04523", --9
    "#FFBB7C", --10
    "#A35274", --11
    "#773A01", --12
    "#F5A200", --13
    "#BBA28B", --14
    "#971C48", --15
    "#FF68EA", --16
    "#DD783F", --17
    "#FFAAF3", --18
    "#764A4A", --19
    "#9F0D05", --20
    },
    },

    { -- Three Teams (24 colors)
    { -- First Team (Blue)
    "#004DFF", -- 1
    "#09F5F5", -- 2
    "#7CA1FF", -- 3
    "#2C32AC", -- 4
    "#D8EEFF", -- 5
    "#0B849B", -- 6
    "#3C7AFF", -- 7
    "#5F6492", -- 8
    },
    { -- Second Team (Red)
    "#FF1005", -- 1
    "#FF6107", -- 2
    "#FFD200", -- 3
    "#FF6058", -- 4
    "#FFBB7C", -- 5
    "#C88B2F", -- 6
    "#F5A200", -- 7
    "#9F0D05", -- 8
    },
    { -- Third Team (Green)
    "#0CE818", -- 1
    "#B4FF39", -- 2
    "#097E1C", -- 3
    "#3EFFA2", -- 4
    "#689E3D", -- 5
    "#7EB900", -- 6
    "#B7EA63", -- 7
    "#37713A", -- 8
    },
    },

    { -- Four Teams (24 colors)
    { -- First Team (Blue)
    "#004DFF", -- 1
    "#7CA1FF", -- 2
    "#D8EEFF", -- 3
    "#09F5F5", -- 4
    "#3475FF", -- 5
    "#0B849B", -- 6
    },
    { -- Second Team (Red)
    "#FF1005", -- 1
    "#FF6107", -- 2
    "#FF6058", -- 3
    "#B04523", -- 4
    "#F80889", -- 5
    "#971C48", -- 6
    },
    { -- Third Team (Green)
    "#0CE818", -- 1
    "#B4FF39", -- 2
    "#097E1C", -- 3
    "#3EFFA2", -- 4
    "#689E3D", -- 5
    "#7EB900", -- 6
    },
    { -- Fourth Team (Yellow)
    "#FFD200", -- 1
    "#F5A200", -- 2
    "#FCEEA4", -- 3
    "#FFBB7C", -- 4
    "#BBA28B", -- 5
    "#C88B2F", -- 6
    },
    },

    { -- Five Teams (25 colors)
    { -- First Team (Blue)
    "#004DFF", -- 1
    "#7CA1FF", -- 2
    "#D8EEFF", -- 3
    "#09F5F5", -- 4
    "#3475FF", -- 5
    },
    { -- Second Team (Red)
    "#FF1005", -- 1
    "#FF6107", -- 2
    "#FF6058", -- 3
    "#B04523", -- 4
    "#9F0D05", -- 5
    },
    { -- Third Team (Green)
    "#0CE818", -- 1
    "#B4FF39", -- 2
    "#097E1C", -- 3
    "#3EFFA2", -- 4
    "#689E3D", -- 5
    },
    { -- Fourth Team (Yellow)
    "#FFD200", -- 1
    "#F5A200", -- 2
    "#FCEEA4", -- 3
    "#FFBB7C", -- 4
    "#C88B2F", -- 5
    },
    { -- Fifth Team (Fuchsia)
    "#F80889", -- 1
    "#FF68EA", -- 2
    "#FFAAF3", -- 3
    "#AA0092", -- 4
    "#701162", -- 5
    },
    },

    { -- Six Teams (24 colors)
    { -- First Team (Blue)
    "#004DFF", -- 1
    "#7CA1FF", -- 2
    "#D8EEFF", -- 3
    "#2C32AC", -- 4
    },
    { -- Second Team (Red)
    "#FF1005", -- 1
    "#FF6058", -- 2
    "#B04523", -- 3
    "#9F0D05", -- 4
    },
    { -- Third Team (Green)
    "#0CE818", -- 1
    "#B4FF39", -- 2
    "#097E1C", -- 3
    "#3EFFA2", -- 4
    },
    { -- Fourth Team (Yellow)
    "#FFD200", -- 1
    "#F5A200", -- 2
    "#FCEEA4", -- 3
    "#9B6408", -- 4
    },
    { -- Fifth Team (Fuchsia)
    "#F80889", -- 1
    "#FF68EA", -- 2
    "#FFAAF3", -- 3
    "#971C48", -- 4
    },
    { -- Sixth Team (Orange)
    "#FF6107", -- 1
    "#FFBB7C", -- 2
    "#DD783F", -- 3
    "#773A01", -- 4
    },
    },

    { -- Seven Teams (21 colors)
    { -- First Team (Blue)
    "#004DFF", -- 1
    "#7CA1FF", -- 2
    "#2C32AC", -- 3
    },
    { -- Second Team (Red)
    "#FF1005", -- 1
    "#FF6058", -- 2
    "#9F0D05", -- 3
    },
    { -- Third Team (Green)
    "#0CE818", -- 1
    "#B4FF39", -- 2
    "#097E1C", -- 3
    },
    { -- Fourth Team (Yellow)
    "#FFD200", -- 1
    "#F5A200", -- 2
    "#FCEEA4", -- 3
    },
    { -- Fifth Team (Fuchsia)
    "#F80889", -- 1
    "#FF68EA", -- 2
    "#FFAAF3", -- 3
    },
    { -- Sixth Team (Orange)
    "#FF6107", -- 1
    "#FFBB7C", -- 2
    "#DD783F", -- 3
    },
    { -- Seventh Team (Cyan)
    "#09F5F5", -- 1
    "#0B849B", -- 2
    "#D8EEFF", -- 3
    },
    },

    { -- Eight Teams (24 colors)
    { -- First Team (Blue)
    "#004DFF", -- 1
    "#7CA1FF", -- 2
    "#2C32AC", -- 3
    },
    { -- Second Team (Red)
    "#FF1005", -- 1
    "#FF6058", -- 2
    "#9F0D05", -- 3
    },
    { -- Third Team (Green)
    "#0CE818", -- 1
    "#B4FF39", -- 2
    "#097E1C", -- 3
    },
    { -- Fourth Team (Yellow)
    "#FFD200", -- 1
    "#F5A200", -- 2
    "#FCEEA4", -- 3
    },
    { -- Fifth Team (Fuchsia)
    "#F80889", -- 1
    "#FF68EA", -- 2
    "#971C48", -- 3
    },
    { -- Sixth Team (Orange)
    "#FF6107", -- 1
    "#FFBB7C", -- 2
    "#DD783F", -- 3
    },
    { -- Seventh Team (Cyan)
    "#09F5F5", -- 1
    "#0B849B", -- 2
    "#D8EEFF", -- 3
    },
    { -- Eigth Team (Purple)
    "#872DFA", -- 1
    "#6809A1", -- 2
    "#C4A9FF", -- 3
    },
    },
    }

    local r = math.random(1,100000)
    math.randomseed(1) -- make sure the next sequence of randoms can be reproduced
    local teamRandoms = {}
    for i = 1, #teamList do
    teamRandoms[teamList[i]] = { math.random(), math.random(), math.random() }
    end
    math.randomseed(r)


    local iconDevModeColors = {
    armblue = armBlueColor,
    corred = corRedColor,
    scavpurp = scavPurpColor,
    raptororange = raptorOrangeColor,
    gaiagray = gaiaGrayColor,
    leggren = legGreenColor,
    }
    local iconDevMode = Spring.GetModOptions().teamcolors_icon_dev_mode
    local iconDevModeColor = iconDevModeColors[iconDevMode]

    local function interpolateGradient(gradient, percentage)
    if percentage >= 1.0 then
    return gradient[#gradient]
    end
    local colorPercentage = percentage * (#gradient - 1)
    local colorA = gradient[math.floor(colorPercentage) + 1]
    local colorB = gradient[math.floor(colorPercentage) + 2]
    local lerpRatio = colorPercentage - math.floor(colorPercentage)
    return {
    math.clamp(math.floor((colorB[1] - colorA[1]) * lerpRatio + colorA[1]), 0, 255),
    math.clamp(math.floor((colorB[2] - colorA[2]) * lerpRatio + colorA[2]), 0, 255),
    math.clamp(math.floor((colorB[3] - colorA[3]) * lerpRatio + colorA[3]), 0, 255)}
    end

    local function shuffleTable(Table)
    local originalTable = {}
    table.append(originalTable, Table)
    local shuffledTable = {}
    if #originalTable > 0 then
    repeat
    local r = math.random(#originalTable)
    table.insert(shuffledTable, originalTable[r])
    table.remove(originalTable, r)
    until #originalTable == 0
    else
    shuffledTable = originalTable
    end
    return shuffledTable
    end

    local function shuffleAllColors()
    ffaColors = shuffleTable(ffaColors)
    survivalColors = shuffleTable(survivalColors)
    for i = 1, #teamColors do
    for j = 1, #teamColors[i] do
    teamColors[i][j] = shuffleTable(teamColors[i][j])
    end
    end
    end

    local function hex2RGB(hex)
    hex = hex:gsub("#", "")
    return { tonumber("0x" .. hex:sub(1, 2)), tonumber("0x" .. hex:sub(3, 4)), tonumber("0x" .. hex:sub(5, 6)) }
    end

    -- we don't want to use FFA colors for TeamFFA, because we want each team to have its own color theme
    local useFFAColors = Spring.Utilities.Gametype.IsFFA() and not Spring.Utilities.Gametype.IsTeams()
    if not useFFAColors and not teamColors[allyTeamCount] and not isSurvival then -- Edge case for TeamFFA with more than supported number of teams
    useFFAColors = true
    end

    local teamColorsTable = {}
    local trueTeamColorsTable = {} -- run first as if we were specs so when we become specs, we can restore the true intended team colors
    local trueFfaColors = table.copy(ffaColors) -- run first as if we were specs so when we become specs, we can restore the true intended ffa colors
    local trueSurvivalColors = table.copy(survivalColors) -- run first as if we were specs so when we become specs, we can restore the true intended survival colors

    local function setupTeamColor(teamID, allyTeamID, isAI, localRun)
    if iconDevModeColor then
    teamColorsTable[teamID] = {
    r = hex2RGB(iconDevModeColor)[1],
    g = hex2RGB(iconDevModeColor)[2],
    b = hex2RGB(iconDevModeColor)[3],
    }

    -- Simple Team Colors
    elseif localRun and
    (Spring.GetConfigInt("SimpleTeamColors", 0) == 1 or (anonymousMode == "allred" and not mySpecState))
    then
    Spring.Echo("Simple team color")
    local brightnessVariation = 0
    local maxColorVariation = 0
    if Spring.GetConfigInt("SimpleTeamColorsUseGradient", 0) == 1 then
    local totalEnemyDimmingCount = 0
    for allyTeamID, count in pairs(dimmingCount) do
    if allyTeamID ~= myAllyTeamID then
    totalEnemyDimmingCount = totalEnemyDimmingCount + count
    end
    end
    brightnessVariation = (0.7 - ((1 / #Spring.GetTeamList(allyTeamID)) * dimmingCount[allyTeamID])) * 255
    brightnessVariation = brightnessVariation * math.min((#Spring.GetTeamList(allyTeamID) * 0.8)-1, 1) -- dont change brightness too much in tiny teams
    maxColorVariation = 60
    end
    local color = hex2RGB(ffaColors[allyTeamID+1] or '#333333')
    if teamID == gaiaTeamID then
    brightnessVariation = 0
    maxColorVariation = 0
    color = hex2RGB(gaiaGrayColor)
    elseif teamID == myTeamID then
    brightnessVariation = 0
    maxColorVariation = 0
    color = {Spring.GetConfigInt("SimpleTeamColorsPlayerR", 0), Spring.GetConfigInt("SimpleTeamColorsPlayerG", 77), Spring.GetConfigInt("SimpleTeamColorsPlayerB", 255)}
    elseif allyTeamID == myAllyTeamID then
    color = {Spring.GetConfigInt("SimpleTeamColorsAllyR", 0), Spring.GetConfigInt("SimpleTeamColorsAllyG", 255), Spring.GetConfigInt("SimpleTeamColorsAllyB", 0)}
    elseif allyTeamID ~= myAllyTeamID then
    color = {Spring.GetConfigInt("SimpleTeamColorsEnemyR", 255), Spring.GetConfigInt("SimpleTeamColorsEnemyG", 16), Spring.GetConfigInt("SimpleTeamColorsEnemyB", 5)}
    end
    color[1] = math.min(color[1] + brightnessVariation, 255) + ((teamRandoms[teamID][1] * (maxColorVariation * 2)) - maxColorVariation)
    color[2] = math.min(color[2] + brightnessVariation, 255) + ((teamRandoms[teamID][2] * (maxColorVariation * 2)) - maxColorVariation)
    color[3] = math.min(color[3] + brightnessVariation, 255) + ((teamRandoms[teamID][3] * (maxColorVariation * 2)) - maxColorVariation)
    teamColorsTable[teamID] = {
    r = color[1],
    g = color[2],
    b = color[3],
    }

    elseif isAI and string.find(isAI, "Scavenger") then
    print("Scavenger team color")
    teamColorsTable[teamID] = {
    r = hex2RGB(scavPurpColor)[1],
    g = hex2RGB(scavPurpColor)[2],
    b = hex2RGB(scavPurpColor)[3],
    }
    elseif isAI and string.find(isAI, "Raptor") then
    print("Raptor team color")
    teamColorsTable[teamID] = {
    r = hex2RGB(raptorOrangeColor)[1],
    g = hex2RGB(raptorOrangeColor)[2],
    b = hex2RGB(raptorOrangeColor)[3],
    }
    elseif teamID == gaiaTeamID then
    Spring.Echo("gaia team color")
    teamColorsTable[teamID] = {
    r = hex2RGB(gaiaGrayColor)[1],
    g = hex2RGB(gaiaGrayColor)[2],
    b = hex2RGB(gaiaGrayColor)[3],
    }

    elseif isSurvival and survivalColors[#Spring.GetTeamList()-2] then
    print("survivor team color")
    teamColorsTable[teamID] = {
    r = hex2RGB(survivalColors[survivalColorNum])[1]
    + math.random(-survivalColorVariation, survivalColorVariation),
    g = hex2RGB(survivalColors[survivalColorNum])[2]
    + math.random(-survivalColorVariation, survivalColorVariation),
    b = hex2RGB(survivalColors[survivalColorNum])[3]
    + math.random(-survivalColorVariation, survivalColorVariation),
    }
    survivalColorNum = survivalColorNum + 1 -- Will start from the next color next time

    -- Use procedural colors
    elseif
    -- Number of player in last non-gaia team is larger than one and there is not a color palette for it
    (#Spring.GetTeamList(allyTeamCount-1) > 1 and (not teamColors[allyTeamCount] or not teamColors[allyTeamCount][1][#Spring.GetTeamList(allyTeamCount-1)]))
    -- or There is more than 29 players (ignores gaia)
    or #Spring.GetTeamList() -1 > #ffaColors
    then
    local totalNumAllyTeams = allyTeamCount
    local overallNumPlayerPerTeam = #Spring.GetTeamList(allyTeamCount-1)
    local numPlayerInTeam = #Spring.GetTeamList(allyTeamID)
    local nthPlayerInTeam = dimmingCount[allyTeamID] - 1
    local useGradientGroup = not (not gradientGroupPerNumTeams[totalNumAllyTeams])

    local color = {125, 125, 125}

    -- If gradient groups are used, each ally team is assigned N gradients
    if useGradientGroup then
    local teamGradientGroup = gradientGroupPerNumTeams[totalNumAllyTeams][allyTeamID + 1]

    local colorWithinGradient = color
    -- (unlikely edge case) If there are more gradient than player, just take the middle color of the gradient
    if numPlayerInTeam <= #teamGradientGroup then
    local gradient = gradients[teamGradientGroup[nthPlayerInTeam + 1]]
    local percentageWithinGradient = 0.5
    colorWithinGradient = interpolateGradient(gradient, percentageWithinGradient)
    else -- Regular case, where we sample thru the group of gradient
    local playerGradientPercentage = nthPlayerInTeam / numPlayerInTeam
    local playerGradientId = math.floor(playerGradientPercentage * #teamGradientGroup)
    local playerGradientName = teamGradientGroup[playerGradientId+1]
    local gradient = gradients[playerGradientName]

    local startGradientPercentage = playerGradientId / #teamGradientGroup
    local endGradientPercentage = (playerGradientId + 1) / #teamGradientGroup
    local percentageWithinGradient = (playerGradientPercentage - startGradientPercentage) / (endGradientPercentage - startGradientPercentage)
    colorWithinGradient = interpolateGradient(gradient, percentageWithinGradient)
    end
    -- local colorWithinGradient = gradient[math.floor(percentageWithinGradient * #gradient) + 1]
    color[1] = colorWithinGradient[1]
    color[2] = colorWithinGradient[2]
    color[3] = colorWithinGradient[3]
    else -- Each ally team use one gradient, this gradient might be share with other ally team(s) (e.g one team is bright green, another is dark green)
    local gradientId = math.floor(allyTeamID % #allGrandientNames)
    local gradName = allGrandientNames[gradientId + 1]
    local gradient = gradients[gradName]
    -- How many ally team share the same gradient?
    local numAllyTeamWithGradient = math.floor(totalNumAllyTeams / #allGrandientNames)
    -- Not all gradients will share the same number of ally team
    if gradientId < totalNumAllyTeams % #allGrandientNames then
    numAllyTeamWithGradient = numAllyTeamWithGradient + 1
    end
    local nthAllyTeamWithGradient = math.floor(allyTeamID / #allGrandientNames)
    -- If there is only a single player in a team, take the color at the center of the gradient
    local playerGradientPercentage = 0.0
    if numPlayerInTeam <= 1 then
    playerGradientPercentage = 0.5
    -- Otherwise, we distribute the players amount the gradient
    else
    playerGradientPercentage = nthPlayerInTeam / (numPlayerInTeam-1)
    local percentagePerPlayer = 1.0 / (numPlayerInTeam-1)

    -- When there are fewer than 5 players in an ally team, the brighness change between player is large, so instead of sampling the entire gradient
    -- we only sample near the middle of the gradient.
    local maxPerPlayerPercentage = 0.25
    if percentagePerPlayer > maxPerPlayerPercentage then
    -- Basically, space every player by 25% of the gradient, but the range is centered on the middle of the gradient
    playerGradientPercentage = maxPerPlayerPercentage * nthPlayerInTeam + (1.0 - maxPerPlayerPercentage * numPlayerInTeam) / 2.0
    end
    end

    -- If multiple ally team share the same gradient, create a gap between the end of one team's and the start of the next one
    if numAllyTeamWithGradient > 1 and nthAllyTeamWithGradient + 1 ~= numAllyTeamWithGradient then
    playerGradientPercentage = 0.9 * playerGradientPercentage
    end
    local gradientPercentage = playerGradientPercentage / numAllyTeamWithGradient + nthAllyTeamWithGradient / numAllyTeamWithGradient
    local colorWithinGradient = interpolateGradient(gradient, gradientPercentage)
    color[1] = colorWithinGradient[1]
    color[2] = colorWithinGradient[2]
    color[3] = colorWithinGradient[3]
    end
    teamColorsTable[teamID] = {
    r = color[1],
    g = color[2],
    b = color[3],
    }

    -- auto ffa gradient colored for huge player games
    elseif useFFAColors or
    -- or Number of player in last non-gaia team is larger than one and there is not a color palette for it
    (#Spring.GetTeamList(allyTeamCount-1) > 1 and (not teamColors[allyTeamCount] or not teamColors[allyTeamCount][1][#Spring.GetTeamList(allyTeamCount-1)]))
    -- or There is more than 29 players (ignores gaia)
    or #Spring.GetTeamList() > 30
    -- or the number of players in the last non-gaia team is one and there is more than team than ffaColor
    or (#Spring.GetTeamList(allyTeamCount-1) == 1 and not ffaColors[allyTeamCount])
    then
    Spring.Echo("Random team color useFFAColors =", useFFAColors, " #Spring.GetTeamList() =", #Spring.GetTeamList(), " #Spring.GetTeamList(allyTeamCount-1)=", #Spring.GetTeamList(allyTeamCount-1))
    local color = hex2RGB(ffaColors[allyTeamID+1] or '#333333')

    -- maxIteration = floor((num_ally_team-1) / 30)
    local maxIterations = math.floor((allyTeamID+1)/(#ffaColors))
    -- local maxIterations = math.floor((allyTeamID+1) / #ffaColors)
    -- dimmingcount here is you're the Nth player of this team to process
    -- So if you're the Nth player of a team with M players:
    -- brightnessVariation = (0.6 - N / M) * 255 , so the range is in -0.4*255 to 0.6*255 or -102 to 153
    local brightnessVariation = (0.6 - ((1 / #Spring.GetTeamList(allyTeamID)) * dimmingCount[allyTeamID])) * 255
    -- brightnessVariation *= math.min(M*0.7 - 1, 1)
    -- For M == 1 -> -0.3
    -- For M == 2 -> 0.4
    -- For M >= 3 -> 1
    brightnessVariation = brightnessVariation * math.min((#Spring.GetTeamList(allyTeamID) * 0.7)-1, 1) -- dont change brightness too much in tiny teams

    -- Basically maxColorVariation gets lower and lower as the number of team increase
    -- maxColorVariation = 120 / max(1, num_team)
    -- So for #P team 1 2 3 4 5 6 7 8
    -- maxColorVariation = 120, 60, 40, 30, 24, 20, 17, 15...
    local maxColorVariation = (120 / math.max(1, allyTeamCount-1))
    if #Spring.GetTeamList(allyTeamID) == 1 then
    brightnessVariation = 0
    maxColorVariation = 0
    end
    -- Basically if there are more player than the table of ffacolors
    -- This is make no sense, because this is trying to add a per team variation, instead of a per player variation
    if maxIterations > 1 then
    -- iteration = 1 + math.floor((allyTeamID+1) / 30 )
    -- iteration = 1 + (allyTeamID+1) // 30
    local iteration = 1 + math.floor((allyTeamID+1)/(#ffaColors))
    -- ffacolor = (allyTeamID+1) - (#ffaColors*(iteration-1)) + 1
    -- = (allyTeamID+1) - (30*((allyTeamID+1) // 30)) + 1
    -- = (allyTeamID+1) % 30
    local ffaColor = (allyTeamID+1) - (#ffaColors*(iteration-1)) + 1
    if iteration ~= 1 then
    color = hex2RGB(ffaColors[ffaColor])
    end
    if iteration == 1 then
    color[1] = color[1] + 40
    color[2] = color[2] + 40
    color[3] = color[3] + 40
    elseif iteration == 2 then
    color[1] = color[1] - 70
    color[2] = color[2] - 70
    color[3] = color[3] - 70
    elseif iteration == 3 then
    color[1] = color[1] + 130
    color[2] = color[2] + 130
    color[3] = color[3] + 130
    end
    end
    if teamID == gaiaTeamID then
    brightnessVariation = 0
    maxColorVariation = 0
    color = hex2RGB(gaiaGrayColor)
    end
    -- clamp(floor( r + brightnessVariation + 2 * random * maxColorVariation - maxColorVariation)
    -- ), 0, 255)
    -- so:
    -- clamp(floor( r + brightnessVariation + (2 * random - 1.0) * maxColorVariation)
    -- ), 0, 255)
    -- So:
    -- clamp(floor( r + brightnessVariation + random(-1., 1.) * maxColorVariation)
    -- ), 0, 255)
    -- brightnessVariation is between -0.4*255 to 0.6*255 for large team
    color[1] = math.clamp(math.floor(color[1] + brightnessVariation + ((teamRandoms[teamID][1] * (maxColorVariation * 2)) - maxColorVariation)), 0, 255)
    color[2] = math.clamp(math.floor(color[2] + brightnessVariation + ((teamRandoms[teamID][2] * (maxColorVariation * 2)) - maxColorVariation)), 0, 255)
    color[3] = math.clamp(math.floor(color[3] + brightnessVariation + ((teamRandoms[teamID][3] * (maxColorVariation * 2)) - maxColorVariation)), 0, 255)
    teamColorsTable[teamID] = {
    r = color[1],
    g = color[2],
    b = color[3],
    }
    else
    Spring.Echo("Palette team color")
    if not teamSizes[allyTeamID] then
    allyTeamNum = allyTeamNum + 1
    teamSizes[allyTeamID] = { allyTeamNum, 1, 0 } -- Team number, Starting color number, Color variation
    end

    if teamColors[allyTeamCount] -- If we have the color set for this number of teams
    and teamColors[allyTeamCount][teamSizes[allyTeamID][1]]
    then -- And this team number exists in the color set
    if not teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]] then -- If we have no color for this player anymore
    teamSizes[allyTeamID][2] = 1 -- Starting from the first color again..
    end

    -- Assigning R,G,B values with specified color variations
    teamColorsTable[teamID] = {
    r = hex2RGB(teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]])[1]
    + math.random(-teamSizes[allyTeamID][3], teamSizes[allyTeamID][3]),
    g = hex2RGB(teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]])[2]
    + math.random(-teamSizes[allyTeamID][3], teamSizes[allyTeamID][3]),
    b = hex2RGB(teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]])[3]
    + math.random(-teamSizes[allyTeamID][3], teamSizes[allyTeamID][3]),
    }
    teamSizes[allyTeamID][2] = teamSizes[allyTeamID][2] + 1 -- Will start from the next color next time

    else
    Spring.Echo("[AUTOCOLORS] Error: Team Colors Table is broken or missing for this allyteam set")
    teamColorsTable[teamID] = {
    r = 255,
    g = 255,
    b = 255,
    }
    end
    end
    end

    local function setupAllTeamColors(localRun)
    survivalColorNum = 1 -- Starting from color #1
    survivalColorVariation = 0 -- Current color variation
    allyTeamNum = 0
    teamSizes = {}

    dimmingCount = {}
    for _, allyTeamID in ipairs(Spring.GetAllyTeamList()) do
    dimmingCount[allyTeamID] = 0
    end
    for i = 1, #teamList do
    local teamID = teamList[i]
    local allyTeamID = select(6, Spring.GetTeamInfo(teamID))
    dimmingCount[allyTeamID] = dimmingCount[allyTeamID] + 1
    local isAI = Spring.GetTeamLuaAI(teamID)
    setupTeamColor(teamID, allyTeamID, isAI, localRun)
    end
    end
    setupAllTeamColors(false)
    trueTeamColorsTable = table.copy(teamColorsTable) -- store the true team colors so we can restore them when we become a spec


    if gadgetHandler:IsSyncedCode() then --- NOTE: STUFF DONE IN SYNCED IS FOR REPLAY WEBSITE

    local AutoColors = {}
    for i = 1, #teamList do
    local teamID = teamList[i]
    AutoColors[i] = {
    teamID = teamID,
    r = trueTeamColorsTable[teamID].r,
    g = trueTeamColorsTable[teamID].g,
    b = trueTeamColorsTable[teamID].b,
    }
    end
    Spring.SendLuaRulesMsg("AutoColors" .. Json.encode(AutoColors))


    else -- UNSYNCED

    local myPlayerID = Spring.GetLocalPlayerID()
    local mySpecState = Spring.GetSpectatingState()

    if anonymousMode == "local" then
    shuffleAllColors()
    end
    if anonymousMode == "local" or Spring.GetConfigInt("SimpleTeamColors", 0) == 1 then
    setupAllTeamColors(true)
    end

    local function isDiscoEnabled()
    return anonymousMode == "disco" and not mySpecState
    end

    -- shuffle colors for all teams except ourselves
    local function discoShuffle(myTeamID)
    -- store own color and do regular shuffle
    local myColor = teamColorsTable[myTeamID]
    shuffleAllColors()
    setupAllTeamColors(true)

    -- swap color with any team that might have been assigned own color
    local teamIDToSwapWith = nil
    for teamID, color in pairs(teamColorsTable) do
    if myColor.r == color.r and myColor.g == color.g and myColor.b == color.b then
    teamIDToSwapWith = teamID
    break
    end
    end
    if teamIDToSwapWith ~= nil then
    teamColorsTable[teamIDToSwapWith] = teamColorsTable[myTeamID]
    end

    -- restore own color
    teamColorsTable[myTeamID] = myColor
    end

    local function updateTeamColors()
    if isDiscoEnabled() then
    discoShuffle(Spring.GetMyTeamID())
    end
    for teamID, color in pairs(teamColorsTable) do
    Spring.SetTeamColor(teamID, color.r / 255, color.g / 255, color.b / 255)
    end
    end
    updateTeamColors()

    local discoTimer = 0
    local discoTimerThreshold = 2 * 60 -- shuffle every 2 minutes with disco mode enabled
    function gadget:Update()
    if isDiscoEnabled() then
    discoTimer = discoTimer + Spring.GetLastUpdateSeconds()
    if discoTimer > discoTimerThreshold then
    discoTimer = 0
    updateTeamColors()
    end
    elseif Spring.GetConfigInt("UpdateTeamColors", 0) == 1 then
    setupAllTeamColors(true)
    updateTeamColors()
    Spring.SetConfigInt("UpdateTeamColors", 0)
    Spring.SetConfigInt("SimpleTeamColors_Reset", 0)
    end
    end

    function gadget:PlayerChanged(playerID)
    if playerID ~= myPlayerID then
    return
    end
    myAllyTeamID = Spring.GetMyAllyTeamID()
    local prevMyTeamID = myTeamID
    myTeamID = Spring.GetMyTeamID()
    if mySpecState and prevMyTeamID ~= myTeamID and Spring.GetConfigInt("SimpleTeamColors", 0) == 1 then
    Spring.SetConfigInt("UpdateTeamColors", 1)
    end
    if mySpecState ~= Spring.GetSpectatingState() then
    mySpecState = Spring.GetSpectatingState()
    teamColorsTable = table.copy(trueTeamColorsTable)
    ffaColors = table.copy(trueFfaColors)
    survivalColors = table.copy(trueSurvivalColors)
    Spring.SetConfigInt("UpdateTeamColors", 1)
    end
    end
    end
    396 changes: 396 additions & 0 deletions plot_ffa_color.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,396 @@
    # pip install matplotlib lupa colormath Pillow

    # Lua runtime
    from lupa.lua54 import LuaRuntime, LuaSyntaxError, LuaError

    # Figure visualization
    import matplotlib.pyplot as plt
    import matplotlib.gridspec as gridspec
    import numpy as np
    import pandas as pd
    from PIL import Image

    # Only used for computing the color divergence
    from colormath.color_objects import sRGBColor, LabColor
    from colormath.color_conversions import convert_color
    from colormath.color_diff import delta_e_cie2000


    # Fix bug in asscalar of numpy
    def patch_asscalar(a):
    return a.item()
    setattr(np, "asscalar", patch_asscalar)


    def get_player_colors(number_teams, number_players):
    """Runs the game_autocolors.lua with the input number of team and number of players and returns the color of each player/team."""

    lua = LuaRuntime(unpack_returned_tuples=True)
    player_ids = list(range(0, number_players))
    team_id_to_players = {}
    number_player_per_team = number_players // number_teams
    current_team = []
    teams = {}
    player_to_team_id = {}
    for player_id in player_ids:
    current_team.append(player_id)
    allies_team_id = len(teams)
    player_to_team_id[player_id] = allies_team_id
    if len(current_team) >= number_player_per_team:
    teams[allies_team_id] = current_team
    current_team = []
    if len(current_team) > 0:
    teams[allies_team_id] = current_team

    # Gaia is always the last player and its part of its own team
    gaia_id = number_players
    teams[allies_team_id+1] = [gaia_id]
    player_to_team_id[gaia_id] = allies_team_id+1
    player_ids.append(gaia_id)
    # print(teams)
    # print(list(teams.keys()))

    # Add input variables to the script
    allTeams = "{" + ','.join([str(id) for id in player_ids]) + "}"
    alliesTeamsToTeam = str(list(teams.values())).replace(':', '=').replace('[', '{').replace(']', '}')
    allAlliesTeamsId = str(list(teams.keys())).replace('[', '{').replace(']', '}')
    teamToAliesTeamId = str(list(player_to_team_id.values())).replace('[', '{').replace(']', '}')
    input_player_table = f"""
    gaia_id = {gaia_id}
    allTeams = {allTeams}
    alliesTeamsToTeam = {alliesTeamsToTeam}
    allAlliesTeamsId = {allAlliesTeamsId}
    teamToAliesTeamId = {teamToAliesTeamId}
    """
    # print(input_player_table)

    # This is the minimum amount of mock to get game_autocolors.lua to run
    lua.require('math')
    preampule = '''
    colorByTeamIdOut = {}
    local Gametype = {}
    function Gametype:new ()
    return {}
    end
    function Gametype:IsPvE ()
    return false
    end
    function Gametype:IsFFA ()
    return false
    end
    function Gametype:IsTeams ()
    return true
    end
    local Spring = {Utilities = {Gametype = Gametype}}
    function Spring:new ()
    return {}
    end
    function Spring.Echo(...)
    print("spring>", ...)
    end
    function Spring:GetModOptions()
    return {
    teamcolors_icon_dev_mode = 'disabled',
    teamcolors_anonymous_mode = 'disabled',
    }
    end
    function Spring:GetGaiaTeamID()
    return gaia_id
    end
    function Spring:GetMyTeamID()
    return allTeams[1]
    end
    function Spring:GetLocalPlayerID()
    return allTeams[1]
    end
    function Spring:GetMyAllyTeamID()
    return teamToAliesTeamId[allTeams[1]]
    end
    -- List of players
    function Spring.GetTeamList(teamId)
    local teamList = {}
    local filterTeamId = -1
    if teamId ~= nil then
    filterTeamId = teamId
    end
    for i, teamID in ipairs(allTeams) do
    if filterTeamId < 0 or teamToAliesTeamId[i] == filterTeamId then
    table.insert(teamList, teamID)
    end
    end
    return teamList
    end
    -- List of teams
    function Spring:GetAllyTeamList()
    return allAlliesTeamsId -- {0, 1}
    end
    function Spring.GetTeamLuaAI(teamID)
    return nil
    end
    function Spring:GetSpectatingState()
    return false, false, false
    end
    function IsPvE()
    return false
    end
    function Spring.GetTeamInfo(teamID, getTeamKeys)
    local numberleader = 0
    local numberisDead = 0
    local numberhasAI = 0
    local stringside = "FooBar"
    local numberallyTeam = teamToAliesTeamId[teamID+1]
    local numberincomeMultiplier = 1.0
    local customTeamKeys = {}
    return teamID, numberleader, numberisDead, numberhasAI, stringside, numberallyTeam, numberincomeMultiplier, customTeamKeys
    end
    function Spring:GetConfigInt(field, default)
    local options = {
    UpdateTeamColors = 0,
    SimpleTeamColors_Reset = 0,
    SimpleTeamColors = 0,
    }
    if options[field] ~= nil then
    return options[field]
    end
    return default
    end
    function Spring.SetTeamColor(teamID, r, g, b)
    -- todo
    -- print("f", teamID, r, g, b)
    color = { r, g, b }
    colorByTeamIdOut[teamID] = color
    end
    local gadgetHandler = {}
    function gadgetHandler:IsSyncedCode()
    return False
    end
    local gadget = {}
    function math.pow(a, b)
    return a ^ b
    end
    function table:copy(foo)
    return foo
    end
    function math.clamp(low, n, high) return math.min(math.max(n, low), high) end
    '''
    # Execute game_autocolors.lua
    filepath = 'luarules/gadgets/game_autocolors.lua'
    # filepath = 'luarules/gadgets/game_autocolors_improved.lua'
    with open(filepath) as file:
    full_code = input_player_table + preampule + file.read()
    try:
    lua.execute(full_code)
    # When lua fails, print the line which cause the error
    except (LuaSyntaxError, LuaError) as err:
    err_str = str(err)
    if '[string "<python>"]:' in err_str:
    line_num = int(err_str.partition('[string "<python>"]:')[2].partition(':')[0])
    lines = full_code.split('\n')
    for i in range(line_num-5, line_num+5):
    if i +1 == line_num:
    print(f"{i+1}>\t{lines[i]}")
    else:
    print(f"{i+1}:\t{lines[i]}")
    raise err

    # Extract output color of each player
    player_to_color = {}
    for team_id, rgb_table in dict(lua.globals().colorByTeamIdOut).items():
    # print(f"team {team_id} : ", list(dict(rgb_table).values()))
    player_to_color[team_id] = (*list(dict(rgb_table).values()),)
    team_to_colors = {}
    for allies_team_id, players in teams.items():
    colors = [player_to_color[id] for id in players]
    team_to_colors[allies_team_id] = colors
    return team_to_colors
    #num_players = 80
    # num_player_per_team = 8
    # num_team = 5

    # Plot some num player / team + number of team permutation as their own figure
    for num_player_per_team, num_team in [(10, 3), (8, 8), (30, 2)]:
    num_players = num_player_per_team*num_team
    team_to_colors = get_player_colors(num_team, num_players)
    # Remove the last team, since this is Gaia
    team_to_colors = {k:v for k, v in team_to_colors.items() if k != len(team_to_colors)- 1}
    # print(team_to_colors)
    fig, axs = plt.subplots(nrows=len(team_to_colors))
    fig.subplots_adjust(top=0.90, bottom=0.1, left=0.1, right=0.99,
    wspace=0.05)
    fig.suptitle(f'Autocolor for {num_player_per_team} players / team with {num_team} ally teams ({num_player_per_team * num_team} players in total)', fontsize=12)

    for ax, (team_id, player_colors) in zip(axs, team_to_colors.items()):
    # Convert the ordered list of colors to 1 x N x 3 array
    color_img = np.array([player_colors])

    # Draw color map
    ax.imshow(color_img, aspect='auto', interpolation="none")

    # Add side text
    pos = list(ax.get_position().bounds)
    x_text = pos[0] - 0.01
    y_text = pos[1] + pos[3]/2.
    fig.text(x_text, y_text, f"Team {team_id}", va='center', ha='right', fontfamily="Monospace", fontsize=10)

    # Turn off *all* ticks & spines, not just the ones with color maps.
    for ax in axs.flat:
    ax.set_axis_off()

    plt.savefig(f"autocolor_{num_player_per_team}pt_{num_team}t.png")


    # Compute all color for all team number and number of team permutation

    num_team_options = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 20, 24, 30, 50, 60, 120]
    num_player_per_team_options = [1, 2, 3, 4, 5, 8, 10, 12, 20, 30, 50, 60]
    # num_team_options = [7]
    # num_player_per_team_options = [8]

    big_color_table = []
    for num_team in num_team_options:
    grid_row = []
    for num_player_per_team in num_player_per_team_options:
    num_players = num_player_per_team * num_team
    if num_players > 255:
    grid_row.append([])
    continue

    team_to_colors = get_player_colors(num_team, num_players)
    grid_row.append(team_to_colors)
    big_color_table.append(grid_row)

    # Compute LABCIE delta E color difference metric (very slow)
    enable_diff_metrics = False
    if enable_diff_metrics:
    color_diff_table = {"num_player_per_team": [], "num_team": [], "min_same_team_divergence": [], "min_enemy_team_divergence": []}
    for y, num_team in enumerate(num_team_options):
    for x, num_player_per_team in enumerate(num_player_per_team_options):
    num_players = num_player_per_team * num_team
    if num_players > 255:
    continue
    # Skip everything because this has N^4 complexity
    # if (not (num_player_per_team == 30 and num_team == 4)) and (not (num_player_per_team == 10 and num_team == 9)):
    # continue
    print(f"{num_player_per_team=}, {num_team=}")
    team_to_colors = big_color_table[y][x]

    color_diff_table["num_player_per_team"].append(num_player_per_team)
    color_diff_table["num_team"].append(num_team)

    # For each color in each team we compute the color distance to every other color in the same team
    min_divergence = None
    min_color = None
    for team_id, player_colors in team_to_colors.items():
    for i, color in enumerate(player_colors):
    srgb_color = sRGBColor(*color)
    lab_color = convert_color(srgb_color, LabColor)
    for j in range(i+1, len(player_colors)):
    other_color = player_colors[j]
    srgb_other_color = sRGBColor(*other_color)
    lab_other_color = convert_color(srgb_other_color, LabColor)

    delta_e = delta_e_cie2000(lab_color, lab_other_color)
    if min_divergence is None or min_divergence > delta_e:
    min_divergence = delta_e
    min_color = (color, other_color)
    print("min_divergence same team", min_divergence, min_color)
    color_diff_table["min_same_team_divergence"].append(min_divergence)

    min_divergence = None
    for team_id, player_colors in team_to_colors.items():
    for i, color in enumerate(player_colors):
    srgb_color = sRGBColor(*color)
    lab_color = convert_color(srgb_color, LabColor)
    for other_team_id, other_player_colors in team_to_colors.items():
    if other_team_id == team_id:
    continue
    for i, other_color in enumerate(other_player_colors):
    srgb_other_color = sRGBColor(*other_color)
    lab_other_color = convert_color(srgb_other_color, LabColor)

    delta_e = delta_e_cie2000(lab_color, lab_other_color)
    if min_divergence is None or min_divergence > delta_e:
    min_divergence = delta_e
    min_color = (color, other_color)
    print("min_divergence different team", min_divergence, min_color)
    color_diff_table["min_enemy_team_divergence"].append(min_divergence)
    df = pd.DataFrame(color_diff_table)
    pd.set_option('display.max_rows', 500)
    pd.set_option('display.max_columns', 500)
    pd.set_option('display.width', 150)
    print("Same team divergence:")
    print(df.pivot_table(index="num_team", columns="num_player_per_team", values='min_same_team_divergence'))
    print("Enemy team divergence:")
    print(df.pivot_table(index="num_team", columns="num_player_per_team", values='min_enemy_team_divergence'))


    # Plot the huge figure of all possible team and number of player team variation

    plt.style.use('classic')
    fig = plt.figure(figsize=(4*len(num_player_per_team_options), 10*len(num_team_options)))
    outer = gridspec.GridSpec(nrows=len(num_team_options), ncols=len(num_player_per_team_options), figure=fig)
    # fig, axes = plt.subplots(nrows=len(num_team_options), ncols=len(num_player_per_team_options), figsize=(100, 100))
    for y, num_team in enumerate(num_team_options):
    for x, num_player_per_team in enumerate(num_player_per_team_options):
    num_players = num_player_per_team * num_team
    if num_players > 255:
    continue
    raw_team_to_colors = big_color_table[y][x]

    # team_to_colors = get_player_colors(num_team, num_players)
    # Remove the last team, since this is Gaia
    team_to_colors = {k:v for k, v in raw_team_to_colors.items() if k != len(raw_team_to_colors)- 1}
    print(f"{num_team=} {num_player_per_team=}")

    inner = gridspec.GridSpecFromSubplotSpec(nrows=len(team_to_colors), ncols=1,
    subplot_spec=outer[y, x], wspace=0.1, hspace=0.1)
    for j, (team_id, player_colors) in enumerate(team_to_colors.items()):
    ax = fig.add_subplot(inner[j])
    # ax = plt.Subplot(fig, inner[j])
    # First team colorbar
    if j == 0:
    # First row of the figure
    if y == 0:
    ax.text(0.7*x, -0.55, f"{num_player_per_team}", fontsize=50, horizontalalignment='center', verticalalignment='bottom')
    ax.set_title(f"{num_player_per_team} players / team with {num_team} ally teams ({num_player_per_team * num_team} players in total)", fontsize=6, color="gray")
    # Middle colorbar of the first column of figures
    if x == 0 and j == len(team_to_colors) // 2:
    ax.text(-0.8, 0.0, f"{num_team}", fontsize=50, horizontalalignment='right', verticalalignment='center')

    # Convert the ordered list of player colors to 1 x N x 3 array
    color_img = np.array([player_colors])

    # Draw color map
    ax.imshow(color_img, aspect='auto', interpolation="none")

    # ax.set_axis_off()
    ax.spines[['left', 'right','bottom', 'top']].set_visible(False)
    ax.get_xaxis().set_ticks([])
    ax.get_yaxis().set_ticks([])

    if len(team_to_colors) >= 20:
    if len(team_to_colors) < 100 or team_id % 4 == 0:
    ax.set_ylabel(f"{team_id}", fontfamily="Monospace", fontsize=5 if len(team_to_colors) > 24 else 8)
    else:
    fontsize = 10
    if num_team >= 10:
    fontsize = 8
    ax.set_ylabel(f"Team {team_id}", fontfamily="Monospace", fontsize=fontsize)

    fig.supxlabel("Number of Player in each team", y=0.91, fontsize=100, va="bottom")
    fig.supylabel("Number of Team", fontsize=100)
    img_path = "huge_team_img.png"
    plt.savefig(img_path)

    # Create thumbnail
    with Image.open(img_path) as im:
    im.thumbnail((512,-1), Image.Resampling.LANCZOS)

    # Save the new thumbnail image
    im.save(img_path.replace(".png", "_thumb.png"), "PNG")