Модуль:StatEngine/RatingBuilder: различия между версиями

Материал из ЧТМ
Перейти к навигации Перейти к поиску
Новая страница: «-- ========================================================================= -- Модуль:StatEngine/RatingBuilder -- Автоматический генера...»
 
словарь вынесен в Модуль:Data/RatingCalc
 
(не показаны 33 промежуточные версии 4 участников)
Строка 1: Строка 1:
-- =========================================================================
-- =========================================================================
-- Модуль:StatEngine/RatingBuilder
-- Модуль:StatEngine/RatingBuilder
-- Автоматический генератор рейтинговой базы из Data/Tournaments
-- =========================================================================
-- =========================================================================


local Builder = {}
local Builder = {}
local TeamsDB = require('Модуль:Data/Teams')
local RatingData = require('Модуль:Data/RatingCalc')


-- =========================================================
local ranks = RatingData.ranks
-- 1. ЛЕСТНИЦА РАНГОВ (Единая иерархия достижений)
local manual_overrides = RatingData.manual_overrides
-- Чем выше число, тем выше приоритет результата.
-- =========================================================
local ranks = {
    -- Дно
    ["о-8г-no"] = 1, ["о-7г-no"] = 2, ["о-6г-no"] = 3, ["о-5г-no"] = 4, ["о-4г-no"] = 5, ["о-3г-no"] = 6, ["о1-3г-no"] = 6,
    -- Отбор: Ранние раунды
    ["о1-3г"] = 10, ["о1-2г"] = 11, ["о1-1г"] = 12,
    ["о2-4г"] = 15, ["о2-3г"] = 16, ["о2-2г"] = 17, ["о2-1г"] = 18,
    ["о3-4г"] = 20, ["о3-3г"] = 21, ["о3-2г"] = 22,
    -- Отбор: Высшие раунды и стыки
    ["о-7г"] = 25, ["о-6г"] = 26, ["о-5г"] = 27, ["о-4г"] = 28, ["о-3г"] = 29, ["о-2г"] = 30, ["о-1г"] = 31,
    ["ос-5г"] = 35, ["ос-4г"] = 36, ["ос-3г"] = 37, ["ос-2г"] = 38, ["ос-1г"] = 39,
   
    -- (Здесь позже добавим Лигу Наций и ЧЧМ)
   
    -- Финальные турниры: места (ККо)
    ["8"] = 120, ["7"] = 121, ["6"] = 122, ["5"] = 123,
    -- Финальные турниры: группы
    ["4г"] = 130, ["3г"] = 131, ["2г"] = 132, ["1г"] = 133,
    -- Финальные турниры: плей-офф
    ["в"]  = 140, -- Вылет в 1/8
    ["чф"] = 150, -- Вылет в 1/4
    ["д"]  = 160, -- 4 место (дерево)
    ["б"]  = 170, -- 3 место (бронза)
    ["ф"]  = 180, -- 2 место (финал)
    ["ч"]  = 190  -- 1 место (чемпион)
}


-- =========================================================
-- =========================================================
-- УТИЛИТЫ ДЛЯ ПЛЕЙ-ОФФ
-- УТИЛИТЫ
-- =========================================================
-- =========================================================
-- Определяет победителя и проигравшего в матче плей-офф (1 или 2 матча)
local function get_match_result(m, rounds)
local function get_match_result(m, rounds)
     local t1, t2 = m[1], m[2]
     local t1, t2 = m[1], m[2]
    local g1, g2 = 0, 0
    local p1, p2 = 0, 0
   
     if rounds == 1 then
     if rounds == 1 then
         g1, g2 = m[3] or 0, m[4] or 0
         local g1, g2 = m[3] or 0, m[4] or 0
         p1, p2 = m[6] or 0, m[7] or 0
         if g1 > g2 then return t1, t2 end
        if g2 > g1 then return t2, t1 end
        local p1, p2 = m[6] or 0, m[7] or 0
        if p1 > p2 then return t1, t2 end
        if p2 > p1 then return t2, t1 end
     else
     else
         -- 2 матча (дома и на выезде). Голы суммируем для простоты
         -- 1. Сравниваем общую сумму голов
        -- (правило гостевого гола пока не симулируем, опираемся на пенальти и общую разницу)
         local g1_agg = (m[3] or 0) + (m[5] or 0)
         g1 = (m[3] or 0) + (m[5] or 0)
         local g2_agg = (m[4] or 0) + (m[6] or 0)
         g2 = (m[4] or 0) + (m[6] or 0)
         if g1_agg > g2_agg then return t1, t2 end
         p1, p2 = m[8] or 0, m[9] or 0
        if g2_agg > g1_agg then return t2, t1 end
 
        -- 2. Пенальти (Индексы 8 и 9)
        local p1, p2 = m[8] or 0, m[9] or 0
        if p1 > p2 then return t1, t2 end
        if p2 > p1 then return t2, t1 end
 
        -- 3. ПРАВИЛО ГОСТЕВЫХ ГОЛОВ (Индексы 5 и 4)
        local t1_away = m[5] or 0
        local t2_away = m[4] or 0
        if t1_away > t2_away then return t1, t2 end
        if t2_away > t1_away then return t2, t1 end
     end
     end
    return nil, nil
end


     if g1 > g2 then return t1, t2 end
local function get_group_stats(team, stages)
     if g2 > g1 then return t2, t1 end
    local pts, gf, ga = 0, 0, 0
     if p1 > p2 then return t1, t2 end
     for stage_name, s_data in pairs(stages) do
    if p2 > p1 then return t2, t1 end
        if s_data.type == "group" and string.match(stage_name, "Group") then
      
            for _, m in ipairs(s_data.matches) do
     return nil, nil -- Если ничья (в теории такого в плей-офф быть не должно)
                if m[1] == team then
                    local g1, g2 = m[3] or 0, m[4] or 0
                    gf = gf + g1; ga = ga + g2
                    if g1 > g2 then pts = pts + 3 elseif g1 == g2 then pts = pts + 1 end
                elseif m[2] == team then
                    local g1, g2 = m[4] or 0, m[3] or 0
                    gf = gf + g1; ga = ga + g2
                    if g1 > g2 then pts = pts + 3 elseif g1 == g2 then pts = pts + 1 end
                end
            end
        end
    end
    return pts, gf - ga, gf
end
 
local function assign_places_by_stats(team_results, source_key, target_keys, stages)
    local teams = {}
     for t, res in pairs(team_results) do
        if res == source_key then
            local pts, gd, gf = get_group_stats(t, stages)
            table.insert(teams, {name = t, pts = pts, gd = gd, gf = gf})
        end
    end
     if #teams == 0 then return end
    table.sort(teams, function(a, b)
        if a.pts ~= b.pts then return a.pts > b.pts end
        if a.gd ~= b.gd then return a.gd > b.gd end
        if a.gf ~= b.gf then return a.gf > b.gf end
        return a.name < b.name
    end)
     local current_idx = 1
     for i, t_info in ipairs(teams) do
        if i > 1 then
            local prev = teams[i-1]
            if t_info.pts == prev.pts and t_info.gd == prev.gd and t_info.gf == prev.gf then
            else current_idx = current_idx + 1 end
        end
        local key = target_keys[current_idx] or target_keys[#target_keys]
        team_results[t_info.name] = key
    end
end
 
local function update_result(tab, team, new_key)
    if not new_key then return end
    local cur_rank = tab[team] and ranks[tab[team]] or 0
    local new_rank = ranks[new_key] or 0
    if new_rank > cur_rank then tab[team] = new_key end
end
end


-- =========================================================
-- =========================================================
-- ОСНОВНОЙ ГЕНЕРАТОР
-- СПЕЦ-ОБРАБОТЧИК ЛИГИ НАЦИЙ (ЛНа)
-- =========================================================
local function process_lna(stages)
    local team_results = {}
 
    -- 1. ГРУППЫ (1R)
    for stage_name, s_data in pairs(stages) do
        local r, div, reg = stage_name:match("^(%d)R_([A-D])_([^_]+)")
        if r == "1" and s_data.type == "group" then
            local num_teams = #s_data.standings
            for place, row in ipairs(s_data.standings) do
                local team = row[1]
                local key = place .. "г" .. div
 
                -- Халявщики в низших дивизионах (D везде, C в Океании)
                if num_teams == 3 and place == 3 and (div == "D" or (div == "C" and reg == "Oceania")) then
                    local pts = get_group_stats(team, {[stage_name] = s_data})
                    local req_pts = (s_data.number_of_rounds == 1) and 2 or 4
                    if pts < req_pts then key = key .. "-no" end
                end
                update_result(team_results, team, key)
            end
        end
    end
 
    -- 2. ПЛЕЙ-ОФФ ДИВИЗИОНОВ (2R)
    for stage_name, s_data in pairs(stages) do
        local r, div = stage_name:match("^(%d)R_([A-D])_")
        if r == "2" and s_data.type == "knockout" then
            for _, match in ipairs(s_data.matches) do
                local winner, loser = get_match_result(match, s_data.number_of_rounds)
                if winner and loser then
                    if stage_name:match("Quarterfinal") then update_result(team_results, loser, "чф" .. div)
                    elseif stage_name:match("Semifinal") then update_result(team_results, loser, "п" .. div)
                    elseif stage_name:match("Final") then
                        update_result(team_results, loser, "ф" .. div)
                        if div ~= "A" then update_result(team_results, winner, "поб" .. div) end
                    end
                end
            end
        end
    end
 
    -- 3. ГЛОБАЛЬНЫЙ ПЛЕЙ-ОФФ ЗА МЕДАЛИ (3R)
    for stage_name, s_data in pairs(stages) do
        local r = stage_name:match("^(%d)R_")
        if r == "3" and s_data.type == "knockout" then
            for _, match in ipairs(s_data.matches) do
                local winner, loser = get_match_result(match, s_data.number_of_rounds)
                if winner and loser then
                    if stage_name:match("3rdPlace") then update_result(team_results, winner, "б"); update_result(team_results, loser, "д")
                    elseif stage_name:match("Final") then update_result(team_results, winner, "ч"); update_result(team_results, loser, "ф")
                    elseif stage_name:match("Semifinal") then update_result(team_results, loser, "б") -- Если нет 3-го места
                    end
                end
            end
        end
    end
 
    return team_results
end
 
-- =========================================================
-- ОБРАБОТЧИК СТАНДАРТНОГО ТУРНИРА
-- =========================================================
local function process_tournament(full_name, stages, global_playoffs)
    local team_results = {}
    local is_qual = string.match(full_name, "Qual") ~= nil
 
    local max_r_by_region = {}
    for stage_name in pairs(stages) do
        local r, reg = stage_name:match("^(%d+)R_([^_]+)")
        if not r then r = stage_name:match("^(%d+)R_"); reg = "Global" end
        if r then
            r = tonumber(r)
            if reg == "GroupA" or reg:match("Group") or reg == "Playoffs" then reg = "Global" end
            max_r_by_region[reg] = math.max(max_r_by_region[reg] or 0, r)
        end
    end
 
    local function get_qual_key(stage_name, place)
        local r, reg = stage_name:match("^(%d+)R_([^_]+)")
        if not r then r = stage_name:match("^(%d+)R_"); reg = "Global" end
        if reg == "GroupA" or (reg and reg:match("Group")) then reg = "Global" end
        if not r then return string.match(stage_name, "Playoff") and "ос" or "о" end
        r = tonumber(r)
        if r == 0 then return "оп" end
 
        -- ФИКС 2034/2038: Если в глобале раундов больше, чем в регионе, значит отбор многоступенчатый для всех
        local max_r = max_r_by_region[reg] or 1
        if max_r_by_region["Global"] and max_r_by_region["Global"] > max_r then
            max_r = max_r_by_region["Global"]
        end
 
        local prefix = (max_r <= 1) and "о-" or ("о" .. r .. "-")
        if place then return prefix .. place .. "г" end
        if string.match(stage_name, "Playoff") then return "ос" end
        return "о" .. r
    end
 
    for stage_name, s_data in pairs(stages) do
        if s_data.type == "group" and string.match(stage_name, "Group") then
            for place, row in ipairs(s_data.standings) do
                local team = row[1]
                local key = ""
                if not is_qual then key = place .. "г"
                else
                    key = get_qual_key(stage_name, place)
                    if key and key ~= "оп" and #s_data.standings == 3 and place == 3 then
                        local pts = get_group_stats(team, {[stage_name] = s_data})
                        local req_pts = (s_data.number_of_rounds == 1) and 2 or 4
                        if pts < req_pts then key = key .. "-no" end
                    end
                end
                update_result(team_results, team, key)
            end
        end
    end
 
    local has_3rd_place_match = stages["FR_3rdPlace"] ~= nil
    for stage_name, s_data in pairs(stages) do
        if s_data.type == "knockout" then
            local is_playoff = string.match(stage_name, "Playoff") ~= nil
            for _, match in ipairs(s_data.matches) do
                local winner, loser = get_match_result(match, s_data.number_of_rounds)
                if winner and loser then
                    if is_qual then
                        if is_playoff then
                            global_playoffs[winner] = true
                            global_playoffs[loser] = true
                            if not team_results[loser] then update_result(team_results, loser, "ос") end
                        else
                            local key = get_qual_key(stage_name, nil)
                            if key then update_result(team_results, loser, key) end
                        end
                    else
                        if stage_name == "FR_Last 16" or stage_name == "FR_Last16" then update_result(team_results, loser, "в")
                        elseif stage_name == "FR_Quarterfinal" then update_result(team_results, loser, "чф")
                        elseif stage_name == "FR_3rdPlace" then update_result(team_results, winner, "б"); update_result(team_results, loser, "д")
                        elseif stage_name == "FR_Final" then update_result(team_results, winner, "ч"); update_result(team_results, loser, "ф")
                        elseif stage_name == "FR_Semifinal" and not has_3rd_place_match then update_result(team_results, loser, "б")
                        end
                    end
                end
            end
        end
    end
 
    for t in pairs(global_playoffs) do
        if team_results[t] then
            if team_results[t] == "о" or team_results[t] == "оп" or string.match(team_results[t], "^о%d$") then
                update_result(team_results, t, "ос")
            else
                local new_key = string.gsub(team_results[t], "^о%d?%-", "ос-")
                update_result(team_results, t, new_key)
            end
        else
            update_result(team_results, t, "ос")
        end
    end
 
    return team_results
end
 
-- =========================================================
-- ГЛАВНАЯ ФУНКЦИЯ
-- =========================================================
-- =========================================================
function Builder.build_year(year, db_tournaments)
function Builder.build_year(year, db_tournaments)
     local output = {} -- Формат: output["ЧТМ"] = { ["МОН"] = "ч", ... }
     local output = {}  
   
    local global_playoff_participants = {}  
    -- Бежим по всем турнирам года (ЧТМ_2006_Qual, ЧТМ_2006_Final...)
 
     for full_tourney_name, stages in pairs(db_tournaments) do
     for full_tourney_name, stages in pairs(db_tournaments) do
        -- Выделяем базовое имя турнира ("ЧТМ", "КАм", "ККо")
         local base_tourney = string.match(full_tourney_name, "^([^_]+)")
         local base_tourney = string.match(full_tourney_name, "^([^_]+)")
         if not output[base_tourney] then output[base_tourney] = {} end
         if not output[base_tourney] then output[base_tourney] = {} end
          
         if not global_playoff_participants[base_tourney] then global_playoff_participants[base_tourney] = {} end
        -- Временное хранилище достижений команд в ЭТОМ турнире
 
         local team_results = {}
         local team_results = {}
          
 
         -- Вспомогательная функция обновления ранга команды
         if base_tourney == "ЛНа" then
         local function update_team_result(team, new_key)
            team_results = process_lna(stages)
             local current_key = team_results[team]
        else
             local current_rank = current_key and ranks[current_key] or 0
            team_results = process_tournament(full_tourney_name, stages, global_playoff_participants[base_tourney])
            local new_rank = ranks[new_key] or 0
        end
              
 
             if new_rank > current_rank then
         -- ХУКИ
                 team_results[team] = new_key
         if year == 2009 and base_tourney == "КОк" then assign_places_by_stats(team_results, "2г", {"б", "д"}, stages) end
        if base_tourney == "ККо" and (year == 2009 or year == 2017 or year == 2025 or year == 2029) then
             assign_places_by_stats(team_results, "3г", {"5", "6"}, stages)
             assign_places_by_stats(team_results, "4г", {"7", "8"}, stages)
        end
        if year == 2013 and base_tourney == "ККо" then
            if stages["FR_Playoffs"] then
                for _, m in ipairs(stages["FR_Playoffs"].matches) do
                    local _, loser = get_match_result(m, stages["FR_Playoffs"].number_of_rounds)
                    if loser then team_results[loser] = "5" end
                end
            end
            assign_places_by_stats(team_results, "2г", {"6"}, stages)
             assign_places_by_stats(team_results, "3г", {"7", "8"}, stages)
        end
        if year == 2021 and base_tourney == "ККо" then
             for t, res in pairs(team_results) do
                if res == "3г" then team_results[t] = "б" elseif res == "4г" then team_results[t] = "д"
                 elseif res == "5г" then team_results[t] = "5" elseif res == "6г" then team_results[t] = "6" end
             end
             end
         end
         end
 
         if year == 2015 and base_tourney == "КФе" then
        -- 1. ПРОХОД ПО ГРУППАМ
            if stages["FR_Stage2_Losers"] then
         for stage_name, stage_data in pairs(stages) do
                 for _, m in ipairs(stages["FR_Stage2_Losers"].matches) do
            if stage_data.type == "group" then
                     local _, loser = get_match_result(m, stages["FR_Stage2_Losers"].number_of_rounds)
                local num_teams = #stage_data.standings
                     if loser then team_results[loser] = "д" end
               
                end
                 for place, row in ipairs(stage_data.standings) do
            end
                     local team = row[1]
            if stages["FR_Semifinal"] then
                    local key = ""
                for _, m in ipairs(stages["FR_Semifinal"].matches) do
                   
                    local _, loser = get_match_result(m, stages["FR_Semifinal"].number_of_rounds)
                    -- Определяем базовый ключ (финал или квала)
                    if loser then team_results[loser] = "б" end
                     if string.match(full_tourney_name, "Final") then
                        key = tostring(place) .. "г"
                    else
                        key = "о-" .. tostring(place) .. "г"
                       
                        -- ПРОВЕРКА НА ХАЛЯВЩИКОВ (Правило 3-х команд)
                        if num_teams == 3 and place == 3 then
                            local pts = 0
                            for _, m in ipairs(stage_data.matches) do
                                if m[1] == team then
                                    if m[3] > m[4] then pts = pts + 3 elseif m[3] == m[4] then pts = pts + 1 end
                                elseif m[2] == team then
                                    if m[4] > m[3] then pts = pts + 3 elseif m[4] == m[3] then pts = pts + 1 end
                                end
                            end
                            local req_pts = (stage_data.number_of_rounds == 1) and 2 or 4
                            if pts < req_pts then
                                key = key .. "-no"
                            end
                        end
                    end
                    update_team_result(team, key)
                 end
                 end
             end
             end
         end
         end
          
         if year == 2013 and base_tourney == "КЮжАм" then
        -- 2. ПРОХОД ПО ПЛЕЙ-ОФФ (Базовые стадии)
            for t, res in pairs(team_results) do if res == "" then team_results[t] = "б" end end
        for stage_name, stage_data in pairs(stages) do
        end
            if stage_data.type == "knockout" then
 
                for _, match in ipairs(stage_data.matches) do
        for t, k in pairs(team_results) do update_result(output[base_tourney], t, k) end
                    local winner, loser = get_match_result(match, stage_data.number_of_rounds)
    end
                   
 
                    if winner and loser then
    -- ИМПОРТ ОТБОРОВ
                        -- Стандартные финальные стадии ЧТМ и Кубков
    if year == 2009 or year == 2013 or year == 2017 then
                        if stage_name == "FR_Last 16" or stage_name == "FR_Last16" then
        local next_year = year + 1
                            update_team_result(loser, "в")
        local success, next_db = pcall(require, 'Модуль:Data/Tournaments/' .. next_year)
                        elseif stage_name == "FR_Quarterfinal" then
        if success and next_db["ЧТМ_"..next_year.."_Qual"] then
                            update_team_result(loser, "чф")
            local dummy_playoffs = {}
                        elseif stage_name == "FR_3rdPlace" then
            local ctm_results = process_tournament("ЧТМ_"..next_year.."_Qual", next_db["ЧТМ_"..next_year.."_Qual"], dummy_playoffs)
                            update_team_result(winner, "б")
            local conf_to_cup = { ["Америка"] = "КАм", ["Африка"] = "КАф", ["Евразия"] = "КЕв" }
                            update_team_result(loser, "д")
 
                        elseif stage_name == "FR_Final" then
            for team, res_key in pairs(ctm_results) do
                            update_team_result(winner, "ч")
                local t_info = TeamsDB.getTeam(team)
                            update_team_result(loser, "ф")
                if t_info and t_info.conf then
                         end
                    local base_tourney = conf_to_cup[t_info.conf]
                          
                    if base_tourney then
                         -- ИСКЛЮЧЕНИЕ 2006: Бронзу получают оба неудачника полуфинала
                         if not output[base_tourney] then output[base_tourney] = {} end
                        if stage_name == "FR_Semifinal" and string.match(full_tourney_name, "2006") then
                         local final_key = res_key
                             update_team_result(loser, "б")
                         if global_playoff_participants[base_tourney] and global_playoff_participants[base_tourney][team] then
                            if res_key == "о" or res_key == "оп" or string.match(res_key, "^о%d$") then final_key = "ос"
                             else final_key = string.gsub(res_key, "^о%d?%-", "ос-") end
                         end
                         end
                        update_result(output[base_tourney], team, final_key)
                     end
                     end
                 end
                 end
             end
             end
         end
         end
       
    end
        -- 3. СЛИЯНИЕ С ОБЩЕЙ БАЗОЙ
 
        -- Сравниваем результаты этого турнира с уже записанными (например, квала vs финал)
    if manual_overrides[year] then
         for team, res_key in pairs(team_results) do
         for tourney, teams in pairs(manual_overrides[year]) do
             local global_key = output[base_tourney][team]
             if not output[tourney] then output[tourney] = {} end
            local global_rank = global_key and ranks[global_key] or 0
             for team, key in pairs(teams) do output[tourney][team] = key end
            local local_rank = ranks[res_key] or 0
              
            if local_rank > global_rank then
                output[base_tourney][team] = res_key
            end
         end
         end
     end
     end
   
 
     return output
     return output
end
end


return Builder
return Builder

Текущая версия от 23:50, 2 июня 2026

Документация Документация

Сейчас проводятся усиленные тесты на странице RatingBuilder. Пока модуль категорически не готов к использованию, поскольку требует очень серьёзной отладки, но потенциал у него колоссальный.

Скрипты для отладки:

  • StatEngine/Tester — для сравнения со старой БД Data/Rating.
    • Вызов: {{#invoke:StatEngine/Tester|test_year|ГОД}}
  • StatEngine/RatingBuilder/Debug — для тупого вывода всех результатов за указанный год.
    • Вызов: {{#invoke:StatEngine/RatingBuilder/Debug|run|ГОД}}
--- Промежуточные результаты ---
Уникальные значения из фрагмента 1 (только в ""): ['1гB', '1гC', '1гD', '2г', '2гA', '2гB', '2гC', '2гD', '3г', '3гA', '3гB', '3гC', '3гC-no', '3гD', '3гD-no', '4г', '4гA', '4гB', '4гC', '4гD', '5', '5г', '6', '6г', '7', '8', 'б', 'бпмв', 'в', 'д', 'дпмв', 'о-2г', 'о-3г', 'о-3г-1', 'о-3г-no', 'о-4г', 'о-4г-1', 'о-4г-no', 'о-5г', 'о-5г-1', 'о-5г-no', 'о-6г', 'о-6г-1', 'о-6г-2', 'о-6г-no', 'о-7г', 'о-7г-2', 'о-7г-no', 'о-8г-no', 'о1', 'о1-1г', 'о1-2г', 'о1-3г', 'о1-3г-no', 'о1-4г', 'о1-5г', 'о2', 'о2-2г', 'о2-3г', 'о2-4г', 'о2-5г', 'о3', 'о3-3г', 'о3-4г', 'оп', 'ос', 'ос-1г', 'ос-2г', 'ос-3г', 'осп-2г', 'осп-3г', 'осп-4г', 'осп-5г', 'осф-1г', 'осф-2г', 'осф-3г', 'осф-4г', 'осчф-3г', 'осчф-4г', 'пA', 'пB', 'пC', 'пD', 'пмв', 'пмп', 'побB', 'побC', 'побD', 'пол', 'смв-2г', 'смв-3г', 'смв-4г', 'смп-2г', 'смп-3г', 'смп-4г', 'ф', 'фA', 'фB', 'фC', 'фD', 'фпмп', 'ч', 'чф', 'чфC', 'чфD', 'чфпм', 'чфпмв', 'чфпмп', '—']
Уникальные значения из фрагмента 2 (только в [""]): ['1г', '1гA', '1гB', '1гC', '1гD', '2г', '2гA', '2гB', '2гC', '2гD', '3г', '3гA', '3гA-no', '3гB', '3гB-no', '3гC', '3гC-no', '3гD', '3гD-no', '4г', '4гA', '4гB', '4гC', '4гD', '5', '5г', '6', '6г', '7', '8', 'б', 'в', 'д', 'о', 'о-1г', 'о-2г', 'о-3г', 'о-3г-no', 'о-4г', 'о-4г-no', 'о-5г', 'о-5г-no', 'о-6г', 'о-6г-no', 'о-7г', 'о-7г-no', 'о-8г', 'о-8г-no', 'о1', 'о1-1г', 'о1-2г', 'о1-3г', 'о1-3г-no', 'о1-4г', 'о1-5г', 'о1-6г', 'о2', 'о2-1г', 'о2-2г', 'о2-3г', 'о2-4г', 'о2-5г', 'о2-6г', 'о3', 'о3-1г', 'о3-2г', 'о3-3г', 'о3-4г', 'о3-5г', 'о3-6г', 'оп', 'ос', 'ос-1г', 'ос-2г', 'ос-3г', 'ос-4г', 'ос-5г', 'пA', 'пB', 'пC', 'пD', 'побB', 'побC', 'побD', 'ф', 'фA', 'фB', 'фC', 'фD', 'ч', 'чф', 'чфA', 'чфB', 'чфC', 'чфD']

--- Финальный результат ---
Найдено значений: 32
1. бпмв
2. дпмв
3. о-3г-1
4. о-4г-1
5. о-5г-1
6. о-6г-1
7. о-6г-2
8. о-7г-2
9. осп-2г
10. осп-3г
11. осп-4г
12. осп-5г
13. осф-1г
14. осф-2г
15. осф-3г
16. осф-4г
17. осчф-3г
18. осчф-4г
19. пмв
20. пмп
21. пол
22. смв-2г
23. смв-3г
24. смв-4г
25. смп-2г
26. смп-3г
27. смп-4г
28. фпмп
29. чфпм
30. чфпмв
31. чфпмп
32. —

Пожалуйста, добавляйте категории на страницу документации.

-- =========================================================================
-- Модуль:StatEngine/RatingBuilder
-- =========================================================================

local Builder = {}
local TeamsDB = require('Модуль:Data/Teams')
local RatingData = require('Модуль:Data/RatingCalc')

local ranks = RatingData.ranks
local manual_overrides = RatingData.manual_overrides

-- =========================================================
-- УТИЛИТЫ
-- =========================================================
local function get_match_result(m, rounds)
    local t1, t2 = m[1], m[2]
    if rounds == 1 then
        local g1, g2 = m[3] or 0, m[4] or 0
        if g1 > g2 then return t1, t2 end
        if g2 > g1 then return t2, t1 end
        local p1, p2 = m[6] or 0, m[7] or 0
        if p1 > p2 then return t1, t2 end
        if p2 > p1 then return t2, t1 end
    else
        -- 1. Сравниваем общую сумму голов
        local g1_agg = (m[3] or 0) + (m[5] or 0)
        local g2_agg = (m[4] or 0) + (m[6] or 0)
        if g1_agg > g2_agg then return t1, t2 end
        if g2_agg > g1_agg then return t2, t1 end

        -- 2. Пенальти (Индексы 8 и 9)
        local p1, p2 = m[8] or 0, m[9] or 0
        if p1 > p2 then return t1, t2 end
        if p2 > p1 then return t2, t1 end

        -- 3. ПРАВИЛО ГОСТЕВЫХ ГОЛОВ (Индексы 5 и 4)
        local t1_away = m[5] or 0
        local t2_away = m[4] or 0
        if t1_away > t2_away then return t1, t2 end
        if t2_away > t1_away then return t2, t1 end
    end
    return nil, nil
end

local function get_group_stats(team, stages)
    local pts, gf, ga = 0, 0, 0
    for stage_name, s_data in pairs(stages) do
        if s_data.type == "group" and string.match(stage_name, "Group") then
            for _, m in ipairs(s_data.matches) do
                if m[1] == team then
                    local g1, g2 = m[3] or 0, m[4] or 0
                    gf = gf + g1; ga = ga + g2
                    if g1 > g2 then pts = pts + 3 elseif g1 == g2 then pts = pts + 1 end
                elseif m[2] == team then
                    local g1, g2 = m[4] or 0, m[3] or 0
                    gf = gf + g1; ga = ga + g2
                    if g1 > g2 then pts = pts + 3 elseif g1 == g2 then pts = pts + 1 end
                end
            end
        end
    end
    return pts, gf - ga, gf
end

local function assign_places_by_stats(team_results, source_key, target_keys, stages)
    local teams = {}
    for t, res in pairs(team_results) do
        if res == source_key then
            local pts, gd, gf = get_group_stats(t, stages)
            table.insert(teams, {name = t, pts = pts, gd = gd, gf = gf})
        end
    end
    if #teams == 0 then return end
    table.sort(teams, function(a, b)
        if a.pts ~= b.pts then return a.pts > b.pts end
        if a.gd ~= b.gd then return a.gd > b.gd end
        if a.gf ~= b.gf then return a.gf > b.gf end
        return a.name < b.name
    end)
    local current_idx = 1
    for i, t_info in ipairs(teams) do
        if i > 1 then
            local prev = teams[i-1]
            if t_info.pts == prev.pts and t_info.gd == prev.gd and t_info.gf == prev.gf then
            else current_idx = current_idx + 1 end
        end
        local key = target_keys[current_idx] or target_keys[#target_keys]
        team_results[t_info.name] = key
    end
end

local function update_result(tab, team, new_key)
    if not new_key then return end
    local cur_rank = tab[team] and ranks[tab[team]] or 0
    local new_rank = ranks[new_key] or 0
    if new_rank > cur_rank then tab[team] = new_key end
end

-- =========================================================
-- СПЕЦ-ОБРАБОТЧИК ЛИГИ НАЦИЙ (ЛНа)
-- =========================================================
local function process_lna(stages)
    local team_results = {}

    -- 1. ГРУППЫ (1R)
    for stage_name, s_data in pairs(stages) do
        local r, div, reg = stage_name:match("^(%d)R_([A-D])_([^_]+)")
        if r == "1" and s_data.type == "group" then
            local num_teams = #s_data.standings
            for place, row in ipairs(s_data.standings) do
                local team = row[1]
                local key = place .. "г" .. div

                -- Халявщики в низших дивизионах (D везде, C в Океании)
                if num_teams == 3 and place == 3 and (div == "D" or (div == "C" and reg == "Oceania")) then
                    local pts = get_group_stats(team, {[stage_name] = s_data})
                    local req_pts = (s_data.number_of_rounds == 1) and 2 or 4
                    if pts < req_pts then key = key .. "-no" end
                end
                update_result(team_results, team, key)
            end
        end
    end

    -- 2. ПЛЕЙ-ОФФ ДИВИЗИОНОВ (2R)
    for stage_name, s_data in pairs(stages) do
        local r, div = stage_name:match("^(%d)R_([A-D])_")
        if r == "2" and s_data.type == "knockout" then
            for _, match in ipairs(s_data.matches) do
                local winner, loser = get_match_result(match, s_data.number_of_rounds)
                if winner and loser then
                    if stage_name:match("Quarterfinal") then update_result(team_results, loser, "чф" .. div)
                    elseif stage_name:match("Semifinal") then update_result(team_results, loser, "п" .. div)
                    elseif stage_name:match("Final") then
                        update_result(team_results, loser, "ф" .. div)
                        if div ~= "A" then update_result(team_results, winner, "поб" .. div) end
                    end
                end
            end
        end
    end

    -- 3. ГЛОБАЛЬНЫЙ ПЛЕЙ-ОФФ ЗА МЕДАЛИ (3R)
    for stage_name, s_data in pairs(stages) do
        local r = stage_name:match("^(%d)R_")
        if r == "3" and s_data.type == "knockout" then
            for _, match in ipairs(s_data.matches) do
                local winner, loser = get_match_result(match, s_data.number_of_rounds)
                if winner and loser then
                    if stage_name:match("3rdPlace") then update_result(team_results, winner, "б"); update_result(team_results, loser, "д")
                    elseif stage_name:match("Final") then update_result(team_results, winner, "ч"); update_result(team_results, loser, "ф")
                    elseif stage_name:match("Semifinal") then update_result(team_results, loser, "б") -- Если нет 3-го места
                    end
                end
            end
        end
    end

    return team_results
end

-- =========================================================
-- ОБРАБОТЧИК СТАНДАРТНОГО ТУРНИРА
-- =========================================================
local function process_tournament(full_name, stages, global_playoffs)
    local team_results = {}
    local is_qual = string.match(full_name, "Qual") ~= nil

    local max_r_by_region = {}
    for stage_name in pairs(stages) do
        local r, reg = stage_name:match("^(%d+)R_([^_]+)")
        if not r then r = stage_name:match("^(%d+)R_"); reg = "Global" end
        if r then
            r = tonumber(r)
            if reg == "GroupA" or reg:match("Group") or reg == "Playoffs" then reg = "Global" end
            max_r_by_region[reg] = math.max(max_r_by_region[reg] or 0, r)
        end
    end

    local function get_qual_key(stage_name, place)
        local r, reg = stage_name:match("^(%d+)R_([^_]+)")
        if not r then r = stage_name:match("^(%d+)R_"); reg = "Global" end
        if reg == "GroupA" or (reg and reg:match("Group")) then reg = "Global" end
        if not r then return string.match(stage_name, "Playoff") and "ос" or "о" end
        r = tonumber(r)
        if r == 0 then return "оп" end

        -- ФИКС 2034/2038: Если в глобале раундов больше, чем в регионе, значит отбор многоступенчатый для всех
        local max_r = max_r_by_region[reg] or 1
        if max_r_by_region["Global"] and max_r_by_region["Global"] > max_r then
            max_r = max_r_by_region["Global"]
        end

        local prefix = (max_r <= 1) and "о-" or ("о" .. r .. "-")
        if place then return prefix .. place .. "г" end
        if string.match(stage_name, "Playoff") then return "ос" end
        return "о" .. r
    end

    for stage_name, s_data in pairs(stages) do
        if s_data.type == "group" and string.match(stage_name, "Group") then
            for place, row in ipairs(s_data.standings) do
                local team = row[1]
                local key = ""
                if not is_qual then key = place .. "г"
                else
                    key = get_qual_key(stage_name, place)
                    if key and key ~= "оп" and #s_data.standings == 3 and place == 3 then
                        local pts = get_group_stats(team, {[stage_name] = s_data})
                        local req_pts = (s_data.number_of_rounds == 1) and 2 or 4
                        if pts < req_pts then key = key .. "-no" end
                    end
                end
                update_result(team_results, team, key)
            end
        end
    end

    local has_3rd_place_match = stages["FR_3rdPlace"] ~= nil
    for stage_name, s_data in pairs(stages) do
        if s_data.type == "knockout" then
            local is_playoff = string.match(stage_name, "Playoff") ~= nil
            for _, match in ipairs(s_data.matches) do
                local winner, loser = get_match_result(match, s_data.number_of_rounds)
                if winner and loser then
                    if is_qual then
                        if is_playoff then
                            global_playoffs[winner] = true
                            global_playoffs[loser] = true
                            if not team_results[loser] then update_result(team_results, loser, "ос") end
                        else
                            local key = get_qual_key(stage_name, nil)
                            if key then update_result(team_results, loser, key) end
                        end
                    else
                        if stage_name == "FR_Last 16" or stage_name == "FR_Last16" then update_result(team_results, loser, "в")
                        elseif stage_name == "FR_Quarterfinal" then update_result(team_results, loser, "чф")
                        elseif stage_name == "FR_3rdPlace" then update_result(team_results, winner, "б"); update_result(team_results, loser, "д")
                        elseif stage_name == "FR_Final" then update_result(team_results, winner, "ч"); update_result(team_results, loser, "ф")
                        elseif stage_name == "FR_Semifinal" and not has_3rd_place_match then update_result(team_results, loser, "б")
                        end
                    end
                end
            end
        end
    end

    for t in pairs(global_playoffs) do
        if team_results[t] then
            if team_results[t] == "о" or team_results[t] == "оп" or string.match(team_results[t], "^о%d$") then
                update_result(team_results, t, "ос")
            else
                local new_key = string.gsub(team_results[t], "^о%d?%-", "ос-")
                update_result(team_results, t, new_key)
            end
        else
            update_result(team_results, t, "ос")
        end
    end

    return team_results
end

-- =========================================================
-- ГЛАВНАЯ ФУНКЦИЯ
-- =========================================================
function Builder.build_year(year, db_tournaments)
    local output = {} 
    local global_playoff_participants = {} 

    for full_tourney_name, stages in pairs(db_tournaments) do
        local base_tourney = string.match(full_tourney_name, "^([^_]+)")
        if not output[base_tourney] then output[base_tourney] = {} end
        if not global_playoff_participants[base_tourney] then global_playoff_participants[base_tourney] = {} end

        local team_results = {}

        if base_tourney == "ЛНа" then
            team_results = process_lna(stages)
        else
            team_results = process_tournament(full_tourney_name, stages, global_playoff_participants[base_tourney])
        end

        -- ХУКИ
        if year == 2009 and base_tourney == "КОк" then assign_places_by_stats(team_results, "2г", {"б", "д"}, stages) end
        if base_tourney == "ККо" and (year == 2009 or year == 2017 or year == 2025 or year == 2029) then
            assign_places_by_stats(team_results, "3г", {"5", "6"}, stages)
            assign_places_by_stats(team_results, "4г", {"7", "8"}, stages)
        end
        if year == 2013 and base_tourney == "ККо" then
            if stages["FR_Playoffs"] then
                for _, m in ipairs(stages["FR_Playoffs"].matches) do
                    local _, loser = get_match_result(m, stages["FR_Playoffs"].number_of_rounds)
                    if loser then team_results[loser] = "5" end
                end
            end
            assign_places_by_stats(team_results, "2г", {"6"}, stages)
            assign_places_by_stats(team_results, "3г", {"7", "8"}, stages)
        end
        if year == 2021 and base_tourney == "ККо" then
            for t, res in pairs(team_results) do
                if res == "3г" then team_results[t] = "б" elseif res == "4г" then team_results[t] = "д"
                elseif res == "5г" then team_results[t] = "5" elseif res == "6г" then team_results[t] = "6" end
            end
        end
        if year == 2015 and base_tourney == "КФе" then
            if stages["FR_Stage2_Losers"] then
                for _, m in ipairs(stages["FR_Stage2_Losers"].matches) do
                    local _, loser = get_match_result(m, stages["FR_Stage2_Losers"].number_of_rounds)
                    if loser then team_results[loser] = "д" end
                end
            end
            if stages["FR_Semifinal"] then
                for _, m in ipairs(stages["FR_Semifinal"].matches) do
                    local _, loser = get_match_result(m, stages["FR_Semifinal"].number_of_rounds)
                    if loser then team_results[loser] = "б" end
                end
            end
        end
        if year == 2013 and base_tourney == "КЮжАм" then
            for t, res in pairs(team_results) do if res == "3г" then team_results[t] = "б" end end
        end

        for t, k in pairs(team_results) do update_result(output[base_tourney], t, k) end
    end

    -- ИМПОРТ ОТБОРОВ
    if year == 2009 or year == 2013 or year == 2017 then
        local next_year = year + 1
        local success, next_db = pcall(require, 'Модуль:Data/Tournaments/' .. next_year)
        if success and next_db["ЧТМ_"..next_year.."_Qual"] then
            local dummy_playoffs = {}
            local ctm_results = process_tournament("ЧТМ_"..next_year.."_Qual", next_db["ЧТМ_"..next_year.."_Qual"], dummy_playoffs)
            local conf_to_cup = { ["Америка"] = "КАм", ["Африка"] = "КАф", ["Евразия"] = "КЕв" } 

            for team, res_key in pairs(ctm_results) do
                local t_info = TeamsDB.getTeam(team)
                if t_info and t_info.conf then
                    local base_tourney = conf_to_cup[t_info.conf]
                    if base_tourney then
                        if not output[base_tourney] then output[base_tourney] = {} end
                        local final_key = res_key
                        if global_playoff_participants[base_tourney] and global_playoff_participants[base_tourney][team] then
                            if res_key == "о" or res_key == "оп" or string.match(res_key, "^о%d$") then final_key = "ос"
                            else final_key = string.gsub(res_key, "^о%d?%-", "ос-") end
                        end
                        update_result(output[base_tourney], team, final_key)
                    end
                end
            end
        end
    end

    if manual_overrides[year] then
        for tourney, teams in pairs(manual_overrides[year]) do
            if not output[tourney] then output[tourney] = {} end
            for team, key in pairs(teams) do output[tourney][team] = key end
        end
    end

    return output
end

return Builder