Модуль:StatEngine

Материал из ЧТМ
Перейти к навигации Перейти к поиску
Документация Документация

Модуль является основным вычислительным блоком, отвечающим за обработку и анализ игровой статистики. Модуль использует данные о матчах и правила из Config для расчёта всех показателей игроков и команд, а также для определения победителей в различных номинациях.

Структура модуля

Основной модуль ядра, предназначенный для массовой обработки данных. За один проход анализирует всю базу данных по матчам за год и извлекает из неё всю необходимую информацию: голы, передачи, сыгранные матчи, карточки и т.д. Такой подход обеспечивает высокую скорость обработки, глобальный и самый быстрый сбор статистики.

Содержит блок «Harvester» (Комбайн), который проходит по базе данных матчей ровно один раз и извлекает абсолютно всю статистику, раскладывая её по заранее созданным пустым массивам.

Основные функции:

  • Определяет, за какую команду играл человек в конкретном матче.
  • Считает сыгранные матчи (в поле и на воротах).
  • Суммирует все типы голов, передачи, карточки, сухие матчи, пенальти.
  • Высчитывает продвинутую статистику (показатель «Плюс/Минус», ценность голов, командную статистику).

Создаёт три итоговых массива данных:

  • Players — суммарная статистика по игрокам.
  • Teams — статистика по командам (очки, победы, разница мячей).
  • PlayerTeam — статистика конкретного игрока за конкретную команду.
  • Может собирать данные как за один указанный год, так и за всю историю (циклом по всем годам).

См. также


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

-- ==========================================
-- Модуль:StatEngine - версия 2.2
-- ==========================================
-- полностью переписан
-- Если кому-то понадобится версия 1.0, то она доступна здесь:
-- [[Служебная:Permalink/64779]]
-- ==========================================

local StatEngine = {}

local Config = require('Module:Config') -- подрубаем технический модуль заранее

-- ==========================================
-- СЕКЦИЯ КОМБАЙНА (HARVESTER) - V1.0
-- (Новая архитектура одного прохода)
-- ==========================================

local Harvester = {}

-- === 1. ОПТОВЫЙ ОПРЕДЕЛИТЕЛЬ КОМАНД ===
function Harvester.get_all_teams(match)
    local player_teams = {}
    local function mark(p, t) if p and p ~= "none" and not player_teams[p] then player_teams[p] = t end end

    if match.squad1 then
        if match.squad1.starters then for _,p in ipairs(match.squad1.starters) do mark(p,1) end end
        if match.squad1.substitutes then for _,p in ipairs(match.squad1.substitutes) do mark(p,1) end end
    end
    if match.squad2 then
        if match.squad2.starters then for _,p in ipairs(match.squad2.starters) do mark(p,2) end end
        if match.squad2.substitutes then for _,p in ipairs(match.squad2.substitutes) do mark(p,2) end end
    end
    if match.neutral_gk and match.neutral_gk.starters then for _,p in ipairs(match.neutral_gk.starters) do mark(p,0) end end

    if match.subs then for _, sub in ipairs(match.subs) do mark(sub.player_in, sub.team); mark(sub.player_out, sub.team) end end
    if match.goals then for _, g in ipairs(match.goals) do mark(g.scorer, g.team); mark(g.assist, g.team); if g.own_scorer then mark(g.own_scorer, (g.team==1) and 2 or 1) end end end
    
    if match.mvp and match.mvp.player then mark(match.mvp.player, match.mvp.team or 0) end
    
    if match.cards then for _, c in ipairs(match.cards) do mark(c.player, c.team) end end
    if match.missed_pens then for _, mp in ipairs(match.missed_pens) do mark(mp.taker, mp.team); mark(mp.goalie, (mp.team==1) and 2 or 1) end end
    if match.shootout then for _, shot in ipairs(match.shootout) do mark(shot.taker, shot.team); mark(shot.goalie, (shot.team==1) and 2 or 1) end end
    
    return player_teams
end

-- === 2. ФАБРИКА ПУСТЫХ СТАТИСТИК ===
function Harvester.create_empty_stats()
    return {
        matches_total = 0, matches_field = 0, matches_goalie = 0,
        goals = {total=0, head=0, heel=0, free_kick=0, goalie=0, penalty=0, hat_trick=0, poker=0, penta=0, hexa=0},
        assists = {total=0, hat_trick=0, poker=0, penta=0},
        own_goals = 0, clearances = 0,
        cards = {yellow=0, red=0},
        mvp = {is_mvp=0, is_goalie_mvp=0},
        clean_sheets = 0, plus_minus = 0,
        weighted_ga = 0,
        penalties = {
            in_game = {u=0,g=0,k=0,w=0,o=0,p=0,c=0}, shootout = {u=0,g=0,k=0,w=0,o=0,p=0,c=0},
            saved_as_goalie = 0, caused_pens = 0
        },
        advanced = {
            goal_points = 0, assist_points = 0, winning_goals = 0, winning_assists = 0,
            field_team_goals = 0, field_total_goals = 0, field_team_actions = 0, field_total_actions = 0
        },
        avg = { goals_num = 0, goals_den = 0, assists_num = 0, assists_den = 0 }
    }
end

-- === 3. ОПТОВЫЙ СБОРЩИК ГОЛОВ И АССИСТОВ ===
function Harvester.extract_goals(match_id, match)
    local year = Config.utils.get_tournament_year(match_id)
    local events = {} 

    local function get_p(name)
        if not events[name] then events[name] = Harvester.create_empty_stats() end
        return events[name]
    end

    if match.goals then
        for _, goal in ipairs(match.goals) do
            if goal.scorer then
                local p = get_p(goal.scorer)
                if year >= Config.eras.goals then
                    p.goals.total = p.goals.total + 1
                    if goal.goal_type == "голова" and year >= Config.eras.head_goals then p.goals.head = p.goals.head + 1 end
                    if (goal.goal_type == "пятка" or goal.goal_type2 == "пятка") and year >= Config.eras.heel_goals then p.goals.heel = p.goals.heel + 1 end
                    if goal.goal_type == "штрафной" and year >= Config.eras.free_kick_goals then p.goals.free_kick = p.goals.free_kick + 1 end
                    if goal.goal_type == "вратарский" and year >= Config.eras.goalie_goals then p.goals.goalie = p.goals.goalie + 1 end
                    if (goal.goal_type == "пенальти" or goal.goal_type2 == "пенальти") and year >= Config.eras.pens_scored then p.goals.penalty = p.goals.penalty + 1 end
                end
            end
            if goal.assist and year >= Config.eras.assists then get_p(goal.assist).assists.total = get_p(goal.assist).assists.total + 1 end
            if goal.own_scorer and year >= Config.eras.own_goals then get_p(goal.own_scorer).own_goals = get_p(goal.own_scorer).own_goals + 1 end
        end
    end

    for p_name, p_stats in pairs(events) do
        if year >= Config.eras.mega_tricks then
            local g = p_stats.goals.total
            if g == 3 then p_stats.goals.hat_trick = 1 elseif g == 4 then p_stats.goals.poker = 1
            elseif g == 5 then p_stats.goals.penta = 1 elseif g >= 6 then p_stats.goals.hexa = 1 end
        end
        if year >= Config.eras.assist_mega_tricks then
            local a = p_stats.assists.total
            if a == 3 then p_stats.assists.hat_trick = 1 elseif a == 4 then p_stats.assists.poker = 1
            elseif a >= 5 then p_stats.assists.penta = 1 end
        end
    end

    return events
end

-- === 4. ОПТОВЫЙ СБОРЩИК МАТЧЕЙ (Амплуа) ===
function Harvester.extract_matches(match_id, match, player_teams)
    local year = Config.utils.get_tournament_year(match_id)
    local events = {}

    local function in_official_squad(p, sq)
        if not sq then return false end
        if sq.starters and Config.utils.has_value(sq.starters, p) then return true end
        if sq.substitutes and Config.utils.has_value(sq.substitutes, p) then return true end
        return false
    end

    for p_name, t_id in pairs(player_teams) do
        events[p_name] = { total = 0, field = 0, goalie = 0 }
        
        if year >= Config.eras.matches then
            if in_official_squad(p_name, match.squad1) or in_official_squad(p_name, match.squad2) or in_official_squad(p_name, match.neutral_gk) then
                events[p_name].total = 1
                
                local is_full_goalie = false
                if (t_id == 1 and match.squad1 and match.squad1.full_match_goalie == p_name) or
                   (t_id == 2 and match.squad2 and match.squad2.full_match_goalie == p_name) then
                    is_full_goalie = true
                elseif t_id == 0 and match.neutral_gk and Config.utils.has_value(match.neutral_gk.starters, p_name) then
                    is_full_goalie = true
                    if match.subs then for _, sub in ipairs(match.subs) do if sub.player_out == p_name then is_full_goalie = false; break end end end
                end
                
                if is_full_goalie then events[p_name].goalie = 1 else events[p_name].field = 1 end
            end
        end
    end
    return events
end

-- === 5. ОПТОВЫЙ СБОРЩИК СОБЫТИЙ (MVP, Карточки, Выносы) ===
function Harvester.extract_events(match_id, match)
    local year = Config.utils.get_tournament_year(match_id)
    local events = {}
    
    local function get_p(name)
        if not events[name] then events[name] = Harvester.create_empty_stats() end
        return events[name]
    end

    if match.mvp and match.mvp.player and year >= Config.eras.mvp then
        local p = get_p(match.mvp.player)
        p.mvp.is_mvp = 1
        if match.mvp.role == "вратарь" then p.mvp.is_goalie_mvp = 1 end
    end
    if match.cards and year >= Config.eras.cards then
        for _, c in ipairs(match.cards) do
            if c.player then
                local p = get_p(c.player)
                if c.color == "yellow" then p.cards.yellow = p.cards.yellow + 1 end
                if c.color == "red" then p.cards.red = p.cards.red + 1 end
            end
        end
    end
    if match.clearances and year >= Config.eras.clearances then
        for _, cl in ipairs(match.clearances) do
            if cl.player then get_p(cl.player).clearances = get_p(cl.player).clearances + 1 end
        end
    end
    return events
end

-- === 5.1 ОПТОВЫЙ СБОРЩИК ПЛЮС/МИНУС ===
function Harvester.extract_plus_minus(match_id, match, player_teams)
    local year = Config.utils.get_tournament_year(match_id)
    local pm_data = {}
    if year < Config.eras.plus_minus then return pm_data end

    local function is_starter(p, t_id)
        local sq = (t_id == 1) and match.squad1 or match.squad2
        if sq and sq.starters and Config.utils.has_value(sq.starters, p) then return true end
        return false
    end

    for p_name, t_id in pairs(player_teams) do
        if t_id == 1 or t_id == 2 then
            local pm = 0
            local is_on = is_starter(p_name, t_id)
            local s1_in, s2_in = 0, 0

            local function calc_stint(s1_out, s2_out)
                local diff1 = tonumber(s1_out) - tonumber(s1_in)
                local diff2 = tonumber(s2_out) - tonumber(s2_in)
                if t_id == 1 then pm = pm + (diff1 - diff2)
                elseif t_id == 2 then pm = pm + (diff2 - diff1) end
            end

            if match.subs then
                for _, sub in ipairs(match.subs) do
                    local cur_s1, cur_s2 = Config.utils.parse_score(sub.score)
                    if sub.player_in == p_name and not is_on then
                        is_on = true; s1_in, s2_in = cur_s1, cur_s2
                    elseif sub.player_out == p_name and is_on then
                        calc_stint(cur_s1, cur_s2); is_on = false
                    end
                end
            end
            
            if is_on then calc_stint(match.score1 or 0, match.score2 or 0) end
            pm_data[p_name] = pm
        end
    end
    return pm_data
end

-- === 5.2 ОПТОВЫЙ СБОРЩИК ПРОДВИНУТЫХ ГОЛОВ (Ценность и Победные) ===
function Harvester.extract_advanced(match_id, match, match_res, player_teams, match_roles)
    local adv_data = {}
    if not match_res then return adv_data end

    local res1, res2 = match_res[1], match_res[2]
    local val1 = (res1.scored > 0) and (res1.pts / res1.scored) or 0
    local val2 = (res2.scored > 0) and (res2.pts / res2.scored) or 0
    local win_target1 = (res1.scored > res2.scored) and (res2.scored + 1) or -1
    local win_target2 = (res2.scored > res1.scored) and (res1.scored + 1) or -1

    local count1, count2 = 0, 0
    local total_act, t1_act, t2_act = 0, 0, 0

    local function get_a(p)
        if not adv_data[p] then adv_data[p] = { gp=0, ap=0, wg=0, wa=0, ftg=0, ftotg=0, fta=0, ftota=0 } end
        return adv_data[p]
    end

    if match.goals then
        for _, g in ipairs(match.goals) do
            total_act = total_act + 1; if g.assist then total_act = total_act + 1 end
            local is_win, g_val = false, 0
            
            if g.team == 1 then
                count1 = count1 + 1; t1_act = t1_act + 1; if g.assist then t1_act = t1_act + 1 end
                if count1 == win_target1 then is_win = true end
                g_val = val1
            elseif g.team == 2 then
                count2 = count2 + 1; t2_act = t2_act + 1; if g.assist then t2_act = t2_act + 1 end
                if count2 == win_target2 then is_win = true end
                g_val = val2
            end

            if g.scorer then
                local a = get_a(g.scorer)
                a.gp = a.gp + g_val; if is_win then a.wg = a.wg + 1 end
            end
            if g.assist then
                local a = get_a(g.assist)
                a.ap = a.ap + g_val; if is_win then a.wa = a.wa + 1 end
            end
        end
    end

    for p_name, t_id in pairs(player_teams) do
        local a = get_a(p_name)
        if match_roles[p_name] and match_roles[p_name].field == 1 then
            a.ftotg = res1.scored + res2.scored
            a.ftota = total_act
            if t_id == 1 then a.ftg = res1.scored; a.fta = t1_act
            elseif t_id == 2 then a.ftg = res2.scored; a.fta = t2_act end
        end
    end

    return adv_data
end

-- === 5.3 ОПТОВЫЙ СБОРЩИК ВРАТАРЕЙ И ПЕНАЛЬТИ ===
function Harvester.extract_goalies_and_pens(match_id, match, player_teams)
    local year = Config.utils.get_tournament_year(match_id)
    local gp_data = {}

    local function get_p(name)
        if not gp_data[name] then
            gp_data[name] = { clean_sheets = 0, weighted_ga = 0, penalties = { in_game = {u=0,g=0,k=0,w=0,o=0,p=0,c=0}, shootout = {u=0,g=0,k=0,w=0,o=0,p=0,c=0}, saved_as_goalie = 0, caused_pens = 0 } }
        end
        return gp_data[name]
    end

    if year >= Config.eras.clean_sheets or year >= Config.eras.gaa then
        for p_name, t_id in pairs(player_teams) do
            local is_full_goalie = false
            if (t_id == 1 and match.squad1 and match.squad1.full_match_goalie == p_name) or
               (t_id == 2 and match.squad2 and match.squad2.full_match_goalie == p_name) then
                is_full_goalie = true
            elseif t_id == 0 and match.neutral_gk and Config.utils.has_value(match.neutral_gk.starters, p_name) then
                is_full_goalie = true
                if match.subs then for _, sub in ipairs(match.subs) do if sub.player_out == p_name then is_full_goalie = false; break end end end
            end

            if is_full_goalie then
                local conceded = 0; local multiplier = 1
                if t_id == 1 then conceded = match.score2 or 0 elseif t_id == 2 then conceded = match.score1 or 0 elseif t_id == 0 then
                    conceded = (match.score1 or 0) + (match.score2 or 0)
                    local n_count = 0; if match.neutral_gk and match.neutral_gk.starters then for _ in ipairs(match.neutral_gk.starters) do n_count = n_count + 1 end end
                    if n_count == 1 then multiplier = 0.5 end
                end

                local p = get_p(p_name)
                if conceded == 0 and year >= Config.eras.clean_sheets then p.clean_sheets = 1 end
                if year >= Config.eras.gaa then p.weighted_ga = conceded * multiplier end
            end
        end
    end

    if year >= Config.eras.pens_scored then
        local function map_res(res, t)
            t.u = t.u + 1
            if res == "гол" then t.g = t.g + 1 elseif res == "вратарь" then t.k = t.k + 1 elseif res == "мимо" then t.w = t.w + 1 elseif res == "выше" then t.o = t.o + 1 elseif res == "штанга" then t.p = t.p + 1 elseif res == "перекладина" then t.c = t.c + 1 end
        end
        if match.goals then for _, goal in ipairs(match.goals) do if goal.goal_type == "пенальти" or goal.goal_type2 == "пенальти" then
            local p = get_p(goal.scorer); p.penalties.in_game.u = p.penalties.in_game.u + 1; p.penalties.in_game.g = p.penalties.in_game.g + 1
            if goal.fouler and year >= Config.eras.caused_pens then get_p(goal.fouler).penalties.caused_pens = get_p(goal.fouler).penalties.caused_pens + 1 end
        end end end
        if match.missed_pens then for _, pen in ipairs(match.missed_pens) do map_res(pen.result, get_p(pen.taker).penalties.in_game)
            if pen.result == "вратарь" and pen.goalie then get_p(pen.goalie).penalties.saved_as_goalie = get_p(pen.goalie).penalties.saved_as_goalie + 1 end
            if pen.fouler and year >= Config.eras.caused_pens then get_p(pen.fouler).penalties.caused_pens = get_p(pen.fouler).penalties.caused_pens + 1 end
        end end
        if match.shootout then for _, shot in ipairs(match.shootout) do map_res(shot.result, get_p(shot.taker).penalties.shootout)
            if shot.result == "вратарь" and shot.goalie then get_p(shot.goalie).penalties.saved_as_goalie = get_p(shot.goalie).penalties.saved_as_goalie + 1 end
        end end
    end
    return gp_data
end

-- === 6. УТИЛИТА СЛИЯНИЯ СТАТИСТИКИ ===
local function merge_stats(target, source)
    if not source then return end
    
    target.matches_total = target.matches_total + (source.matches_total or 0)
    target.matches_field = target.matches_field + (source.matches_field or 0)
    target.matches_goalie = target.matches_goalie + (source.matches_goalie or 0)
    
    if source.goals then for k, v in pairs(source.goals) do target.goals[k] = target.goals[k] + v end end
    if source.assists then for k, v in pairs(source.assists) do target.assists[k] = target.assists[k] + v end end
    
    target.own_goals = target.own_goals + (source.own_goals or 0)
    target.clearances = target.clearances + (source.clearances or 0)
    target.clean_sheets = target.clean_sheets + (source.clean_sheets or 0)
    target.weighted_ga = target.weighted_ga + (source.weighted_ga or 0)
    
    if source.cards then
        target.cards.yellow = target.cards.yellow + source.cards.yellow
        target.cards.red = target.cards.red + source.cards.red
    end
    if source.mvp then
        target.mvp.is_mvp = target.mvp.is_mvp + source.mvp.is_mvp
        target.mvp.is_goalie_mvp = target.mvp.is_goalie_mvp + source.mvp.is_goalie_mvp
    end
    
    if source.pm then target.plus_minus = target.plus_minus + source.pm end
    if source.plus_minus then target.plus_minus = target.plus_minus + source.plus_minus end
    
    local adv_src = source.adv or source.advanced
    if adv_src then
        target.advanced.goal_points = target.advanced.goal_points + (adv_src.gp or adv_src.goal_points or 0)
        target.advanced.assist_points = target.advanced.assist_points + (adv_src.ap or adv_src.assist_points or 0)
        target.advanced.winning_goals = target.advanced.winning_goals + (adv_src.wg or adv_src.winning_goals or 0)
        target.advanced.winning_assists = target.advanced.winning_assists + (adv_src.wa or adv_src.winning_assists or 0)
        target.advanced.field_team_goals = target.advanced.field_team_goals + (adv_src.ftg or adv_src.field_team_goals or 0)
        target.advanced.field_total_goals = target.advanced.field_total_goals + (adv_src.ftotg or adv_src.field_total_goals or 0)
        target.advanced.field_team_actions = target.advanced.field_team_actions + (adv_src.fta or adv_src.field_team_actions or 0)
        target.advanced.field_total_actions = target.advanced.field_total_actions + (adv_src.ftota or adv_src.field_total_actions or 0)
    end
    
    if source.penalties then
        for k, v in pairs(source.penalties.in_game) do target.penalties.in_game[k] = target.penalties.in_game[k] + v end
        for k, v in pairs(source.penalties.shootout) do target.penalties.shootout[k] = target.penalties.shootout[k] + v end
        target.penalties.saved_as_goalie = target.penalties.saved_as_goalie + source.penalties.saved_as_goalie
        target.penalties.caused_pens = target.penalties.caused_pens + source.penalties.caused_pens
    end
    
    if source.avg then
        target.avg.goals_num = target.avg.goals_num + source.avg.goals_num
        target.avg.goals_den = target.avg.goals_den + source.avg.goals_den
        target.avg.assists_num = target.avg.assists_num + source.avg.assists_num
        target.avg.assists_den = target.avg.assists_den + source.avg.assists_den
    end
end

-- === 6.1. ФАБРИКА ПУСТЫХ КОМАНДНЫХ СТАТИСТИК ===
function Harvester.create_empty_team_stats()
    return {
        matches = 0, points = 0, scored = 0, conceded = 0, gd = 0,
        w = 0, w_ot = 0, d = 0, l_ot = 0, l = 0
    }
end

-- === 6.2. ОПТОВЫЙ СУДЬЯ МАТЧА ===
function Harvester.evaluate_match(match)
    if not match.team1 or not match.team2 then return nil end
    if match.gates == 0 and not match.shootout_score1 then return nil end

    local s1 = match.score1 or 0
    local s2 = match.score2 or 0

    local res1 = { code = match.team1, scored = s1, conceded = s2, gd = s1 - s2, pts = 0, w=0, w_ot=0, d=0, l_ot=0, l=0 }
    local res2 = { code = match.team2, scored = s2, conceded = s1, gd = s2 - s1, pts = 0, w=0, w_ot=0, d=0, l_ot=0, l=0 }

    if match.shootout_score1 and match.shootout_score2 then
        if match.shootout_score1 > match.shootout_score2 then
            res1.pts, res2.pts = 2, 1; res1.w_ot, res2.l_ot = 1, 1
        elseif match.shootout_score2 > match.shootout_score1 then
            res1.pts, res2.pts = 1, 2; res1.l_ot, res2.w_ot = 1, 1
        else
            res1.pts, res2.pts = 1, 1; res1.d, res2.d = 1, 1
        end
    elseif match.aet then
        if s1 > s2 then res1.pts, res2.pts = 2, 1; res1.w_ot, res2.l_ot = 1, 1
        elseif s2 > s1 then res1.pts, res2.pts = 1, 2; res1.l_ot, res2.w_ot = 1, 1
        else res1.pts, res2.pts = 1, 1; res1.d, res2.d = 1, 1 end
    else
        if s1 > s2 then res1.pts, res2.pts = 3, 0; res1.w, res2.l = 1, 1
        elseif s2 > s1 then res1.pts, res2.pts = 0, 3; res1.l, res2.w = 1, 1
        else res1.pts, res2.pts = 1, 1; res1.d, res2.d = 1, 1 end
    end

    return { [1] = res1, [2] = res2 }
end

-- === 6.3. УТИЛИТА СЛИЯНИЯ ДЛЯ КОМАНД ===
local function merge_team_stats(target, source)
    if not source then return end
    target.matches = target.matches + 1; target.points = target.points + source.pts
    target.scored = target.scored + source.scored; target.conceded = target.conceded + source.conceded
    target.gd = target.gd + source.gd; target.w = target.w + source.w; target.w_ot = target.w_ot + source.w_ot
    target.d = target.d + source.d; target.l_ot = target.l_ot + source.l_ot; target.l = target.l + source.l
end

-- === 7. ГЛАВНЫЙ ДВИГАТЕЛЬ КОМБАЙНА ===
function Harvester.run(year_db, options)
    options = options or { need_players = true, need_teams = false, need_combos = false }
    local Stats = { Players = {}, Teams = {}, PlayerTeam = {} }

    local function get_global_player(name)
        if not Stats.Players[name] then Stats.Players[name] = Harvester.create_empty_stats(); Stats.Players[name].name = name end
        return Stats.Players[name]
    end
    local function get_global_team(code)
        if not Stats.Teams[code] then Stats.Teams[code] = Harvester.create_empty_team_stats(); Stats.Teams[code].code = code end
        return Stats.Teams[code]
    end
    local function get_global_combo(name, team_code)
        local combo_key = name .. "_" .. team_code
        if not Stats.PlayerTeam[combo_key] then Stats.PlayerTeam[combo_key] = Harvester.create_empty_stats(); Stats.PlayerTeam[combo_key].name = name; Stats.PlayerTeam[combo_key].team = team_code end
        return Stats.PlayerTeam[combo_key]
    end

    for match_id, match in pairs(year_db) do
        local player_teams = Harvester.get_all_teams(match)
        local match_roles = Harvester.extract_matches(match_id, match, player_teams)
        local match_goals = Harvester.extract_goals(match_id, match)
        local match_events = Harvester.extract_events(match_id, match)
        local match_pm = Harvester.extract_plus_minus(match_id, match, player_teams)
        local match_gp = Harvester.extract_goalies_and_pens(match_id, match, player_teams)
        
        local match_resolution = nil
        local match_adv = nil
        if options.need_teams or options.need_combos or options.need_players then
            match_resolution = Harvester.evaluate_match(match)
            match_adv = Harvester.extract_advanced(match_id, match, match_resolution, player_teams, match_roles)
            if options.need_teams and match_resolution then
                merge_team_stats(get_global_team(match_resolution[1].code), match_resolution[1])
                merge_team_stats(get_global_team(match_resolution[2].code), match_resolution[2])
            end
        end

        local function get_team_code(t_id)
            if t_id == 1 then return match.team1 or "Неизвестно" elseif t_id == 2 then return match.team2 or "Неизвестно" else return "Нейтрал" end
        end

        for p_name, t_id in pairs(player_teams) do
            local roles = match_roles[p_name] or {total=0, field=0, goalie=0}
            
            if options.need_players then
                local gp = get_global_player(p_name)
                gp.matches_total = gp.matches_total + roles.total
                gp.matches_field = gp.matches_field + roles.field
                gp.matches_goalie = gp.matches_goalie + roles.goalie
                
                local m_g = 0; local m_a = 0
                if match_goals[p_name] then merge_stats(gp, match_goals[p_name]); m_g = match_goals[p_name].goals.total; m_a = match_goals[p_name].assists.total end
                if match_events[p_name] then merge_stats(gp, match_events[p_name]) end
                if match_pm[p_name] ~= nil then merge_stats(gp, { pm = match_pm[p_name] }) end
                if match_adv and match_adv[p_name] then merge_stats(gp, { adv = match_adv[p_name] }) end
                if match_gp[p_name] then merge_stats(gp, match_gp[p_name]) end
                
                local match_year = Config.utils.get_tournament_year(match_id)
                local avg_delta = { goals_num=0, goals_den=0, assists_num=0, assists_den=0 }
                if match_year >= Config.eras.avg_goals then avg_delta.goals_num = m_g; avg_delta.goals_den = roles.field end
                if match_year >= Config.eras.avg_assists then avg_delta.assists_num = m_a; avg_delta.assists_den = roles.field end
                merge_stats(gp, { avg = avg_delta })
            end
            
            if options.need_combos then
                local t_code = get_team_code(t_id)
                local gc = get_global_combo(p_name, t_code)
                gc.matches_total = gc.matches_total + roles.total
                gc.matches_field = gc.matches_field + roles.field
                gc.matches_goalie = gc.matches_goalie + roles.goalie
                
                if match_goals[p_name] then merge_stats(gc, match_goals[p_name]) end
                if match_events[p_name] then merge_stats(gc, match_events[p_name]) end
                if match_pm[p_name] ~= nil then merge_stats(gc, { pm = match_pm[p_name] }) end
                if match_adv and match_adv[p_name] then merge_stats(gc, { adv = match_adv[p_name] }) end
                if match_gp[p_name] then merge_stats(gc, match_gp[p_name]) end
            end
        end
    end
    return Stats
end

-- === 7.5 ГЛОБАЛЬНЫЙ ИСТОРИЧЕСКИЙ СБОРЩИК ===
function Harvester.run_all_time(options)
    options = options or { need_players = true, need_teams = true, need_combos = true }
    local GrandStats = { Players = {}, Teams = {}, PlayerTeam = {} }

    for _, year in ipairs(Config.years) do
        local success, year_db = pcall(require, 'Module:Data/' .. year)
        if success and type(year_db) == "table" then
            local year_stats = Harvester.run(year_db, options)

            if options.need_players then
                for p_name, p_data in pairs(year_stats.Players) do
                    if not GrandStats.Players[p_name] then
                        GrandStats.Players[p_name] = Harvester.create_empty_stats()
                        GrandStats.Players[p_name].name = p_name
                    end
                    merge_stats(GrandStats.Players[p_name], p_data)
                end
            end
            
            if options.need_teams then
                for t_code, t_data in pairs(year_stats.Teams) do
                    if not GrandStats.Teams[t_code] then
                        GrandStats.Teams[t_code] = Harvester.create_empty_team_stats()
                        GrandStats.Teams[t_code].code = t_code
                    end
                    merge_team_stats(GrandStats.Teams[t_code], t_data)
                end
            end

            if options.need_combos then
                for combo_key, c_data in pairs(year_stats.PlayerTeam) do
                    if not GrandStats.PlayerTeam[combo_key] then
                        GrandStats.PlayerTeam[combo_key] = Harvester.create_empty_stats()
                        GrandStats.PlayerTeam[combo_key].name = c_data.name
                        GrandStats.PlayerTeam[combo_key].team = c_data.team
                    end
                    merge_stats(GrandStats.PlayerTeam[combo_key], c_data)
                end
            end
        end
    end
    
    return GrandStats
end


-- ==========================================
-- БЛОК ОБРАТНОЙ СОВМЕСТИМОСТИ (ФАСАД/АДАПТЕР)
-- ==========================================
-- Эти функции оставляют интерфейс старым (вызовы из вики-шаблонов),
-- но внутри используют супер-быстрый Комбайн для точечных задач.

-- 1. Глобальный сборщик статистики ВСЕХ игроков за ОДИН матч
function StatEngine.getAllPlayersMatchStats(match_id, match_data)
    local all_stats = {}
    local player_teams = Harvester.get_all_teams(match_data)

    -- Вызываем экстракторы Комбайна ровно один раз на весь матч
    local match_roles = Harvester.extract_matches(match_id, match_data, player_teams)
    local match_goals = Harvester.extract_goals(match_id, match_data)
    local match_events = Harvester.extract_events(match_id, match_data)
    local match_pm = Harvester.extract_plus_minus(match_id, match_data, player_teams)
    local match_gp = Harvester.extract_goalies_and_pens(match_id, match_data, player_teams)
    local match_res = Harvester.evaluate_match(match_data)
    local match_adv = Harvester.extract_advanced(match_id, match_data, match_res, player_teams, match_roles)

    for p_name, t_id in pairs(player_teams) do
        local adv = match_adv[p_name] or {}
        all_stats[p_name] = {
            team = t_id,
            matches = match_roles[p_name] or {total=0, field=0, goalie=0},
            goals = match_goals[p_name] and match_goals[p_name].goals or {total=0, head=0, heel=0, free_kick=0, goalie=0, penalty=0, hat_trick=0, poker=0, penta=0, hexa=0},
            assists = match_goals[p_name] and match_goals[p_name].assists or {total=0, hat_trick=0, poker=0, penta=0},
            own_goals = match_goals[p_name] and match_goals[p_name].own_goals or 0,
            cards = match_events[p_name] and match_events[p_name].cards or {yellow=0, red=0},
            clearances = match_events[p_name] and match_events[p_name].clearances or 0,
            mvp = match_events[p_name] and match_events[p_name].mvp or {is_mvp=0, is_goalie_mvp=0},
            plus_minus = match_pm[p_name] or nil,
            clean_sheets = match_gp[p_name] and match_gp[p_name].clean_sheets or 0,
            weighted_ga = match_gp[p_name] and match_gp[p_name].weighted_ga or 0,
            penalties = match_gp[p_name] and match_gp[p_name].penalties or {in_game={u=0,g=0,k=0,w=0,o=0,p=0,c=0}, shootout={u=0,g=0,k=0,w=0,o=0,p=0,c=0}, saved_as_goalie=0, caused_pens=0},
            
            -- Маппинг коротких названий Harvester в длинные названия для старых шаблонов
            advanced = {
                goal_points = adv.gp or 0,
                assist_points = adv.ap or 0,
                winning_goals = adv.wg or 0,
                winning_assists = adv.wa or 0,
                field_team_goals = adv.ftg or 0,
                field_total_goals = adv.ftotg or 0,
                field_team_actions = adv.fta or 0,
                field_total_actions = adv.ftota or 0
            }
        }
    end
    return all_stats
end

-- 2. Сборщик статистики ОДНОГО игрока за матч (обёртка вокруг функции выше)
function StatEngine.getPlayerMatchStats(match_id, match_data, player_name)
    local all = StatEngine.getAllPlayersMatchStats(match_id, match_data)
    return all[player_name]
end

-- 3. Швейцарский нож (Умный экспресс-сборщик)
function StatEngine.getCategoryStats(match_id, match_data, category)
    local active_players = Config.getMatchParticipants(match_data)
    local result = {}

    if category == "goals" then
        local extracted = Harvester.extract_goals(match_id, match_data)
        for p, _ in pairs(active_players) do
            local g = extracted[p] and extracted[p].goals or {total=0, head=0, heel=0, free_kick=0, goalie=0, penalty=0, hat_trick=0, poker=0, penta=0, hexa=0}
            local out = {}
            for k,v in pairs(g) do out[k] = v end
            out.own_goals = extracted[p] and extracted[p].own_goals or 0
            result[p] = out
        end
        return result
    elseif category == "assists" then
        local extracted = Harvester.extract_goals(match_id, match_data)
        for p, _ in pairs(active_players) do
            result[p] = extracted[p] and extracted[p].assists or {total=0, hat_trick=0, poker=0, penta=0}
        end
        return result
    elseif category == "cards" then
        local extracted = Harvester.extract_events(match_id, match_data)
        for p, _ in pairs(active_players) do
            result[p] = extracted[p] and extracted[p].cards or {yellow=0, red=0}
        end
        return result
    elseif category == "clearances" then
        local extracted = Harvester.extract_events(match_id, match_data)
        for p, _ in pairs(active_players) do
            result[p] = { total = extracted[p] and extracted[p].clearances or 0 }
        end
        return result
    elseif category == "mvp" then
        local extracted = Harvester.extract_events(match_id, match_data)
        for p, _ in pairs(active_players) do
            result[p] = extracted[p] and extracted[p].mvp or {is_mvp=0, is_goalie_mvp=0}
        end
        return result
    elseif category == "penalties" then
        local player_teams = Harvester.get_all_teams(match_data)
        local extracted = Harvester.extract_goalies_and_pens(match_id, match_data, player_teams)
        for p, _ in pairs(active_players) do
            result[p] = extracted[p] and extracted[p].penalties or {in_game={u=0,g=0,k=0,w=0,o=0,p=0,c=0}, shootout={u=0,g=0,k=0,w=0,o=0,p=0,c=0}, saved_as_goalie=0, caused_pens=0}
        end
        return result
    end
    return {}
end

-- 4. Экспресс-сборщик пенальти
function StatEngine.getMatchPenalties(match_id, match_data)
    return StatEngine.getCategoryStats(match_id, match_data, "penalties")
end

-- 5. Результат команды
function StatEngine.getTeamMatchResult(match_data, team)
    local match_res = Harvester.evaluate_match(match_data)
    if not match_res or (team ~= 1 and team ~= 2) then
        return { points = 0, scored = 0, conceded = 0, gd = 0, w = 0, w_ot = 0, d = 0, l_ot = 0, l = 0 }
    end
    local r = match_res[team]
    return {
        points = r.pts, scored = r.scored, conceded = r.conceded, gd = r.gd,
        w = r.w, w_ot = r.w_ot, d = r.d, l_ot = r.l_ot, l = r.l
    }
end

-- ==========================================
-- БЛОК АГРЕГАЦИИ ИГРОВОГО ДНЯ (ТУРБО-ВЕРСИЯ HARVESTER)
-- ==========================================

function StatEngine.getMatchdayAggregates(year_db, target_matchday)
    local day_players = {}

    local function get_p(name)
        if not day_players[name] then
            day_players[name] = {
                name = name,
                matches_total = 0, matches_non_neutral = 0, matches_field = 0, matches_goalie = 0,
                points_total = 0, gd_total = 0, plus_minus = 0,
                mvp_count = 0, mvp_points = 0, mvp_gd = 0, last_mvp_hist = 0,
                goals = 0, pens_scored = 0, winning_goals = 0, goal_points = 0,
                last_goal_hist = 0, field_team_goals = 0, field_total_goals = 0,
                assists = 0, winning_assists = 0, assist_points = 0, last_assist_hist = 0,
                last_action_hist = 0, field_team_actions = 0, field_total_actions = 0,
                weighted_ga = 0, clean_sheets = 0, pens_saved = 0, mvp_goalie = 0,
                et_as_goalie = 0, last_goalie_hist = 0, faced_opponents = {},
                opp_total_goals = 0, opp_avg_goals = 0
            }
        end
        return day_players[name]
    end

    for match_id, match in pairs(year_db) do
        if match.matchday == target_matchday then
            local num_hist = match.num_hist
            local has_et = match.extra_time or match.aet

            local player_teams = Harvester.get_all_teams(match)
            local match_roles = Harvester.extract_matches(match_id, match, player_teams)
            local match_goals = Harvester.extract_goals(match_id, match)
            local match_pm = Harvester.extract_plus_minus(match_id, match, player_teams)
            local match_res = Harvester.evaluate_match(match)
            local match_adv = Harvester.extract_advanced(match_id, match, match_res, player_teams, match_roles)
            local match_gp = Harvester.extract_goalies_and_pens(match_id, match, player_teams)

            local t1_roster, t2_roster = {}, {}
            for p, t in pairs(player_teams) do
                if t == 1 then table.insert(t1_roster, p)
                elseif t == 2 then table.insert(t2_roster, p)
                elseif t == 0 then table.insert(t1_roster, p); table.insert(t2_roster, p) end
            end

            for p_name, t_id in pairs(player_teams) do
                local p = get_p(p_name)
                local roles = match_roles[p_name] or {total=0, field=0, goalie=0}

                p.matches_total = p.matches_total + roles.total
                if t_id ~= 0 then p.matches_non_neutral = p.matches_non_neutral + 1 end
                p.matches_field = p.matches_field + roles.field
                p.matches_goalie = p.matches_goalie + roles.goalie

                if match_res and t_id > 0 then
                    p.points_total = p.points_total + match_res[t_id].pts
                    p.gd_total = p.gd_total + match_res[t_id].gd
                end
                if match_pm[p_name] then p.plus_minus = p.plus_minus + match_pm[p_name] end

                if match.mvp and match.mvp.player == p_name then
                    p.mvp_count = p.mvp_count + 1
                    if match_res and t_id > 0 then
                        p.mvp_points = p.mvp_points + match_res[t_id].pts
                        p.mvp_gd = p.mvp_gd + match_res[t_id].gd
                    end
                    p.last_mvp_hist = num_hist
                end

                if match_goals[p_name] then
                    p.goals = p.goals + match_goals[p_name].goals.total
                    p.pens_scored = p.pens_scored + match_goals[p_name].goals.penalty
                    p.assists = p.assists + match_goals[p_name].assists.total
                end

                if match_adv and match_adv[p_name] then
                    p.winning_goals = p.winning_goals + match_adv[p_name].wg
                    p.goal_points = p.goal_points + match_adv[p_name].gp
                    p.field_team_goals = p.field_team_goals + match_adv[p_name].ftg
                    p.field_total_goals = p.field_total_goals + match_adv[p_name].ftotg
                    p.field_team_actions = p.field_team_actions + match_adv[p_name].fta
                    p.field_total_actions = p.field_total_actions + match_adv[p_name].ftota
                    p.winning_assists = p.winning_assists + match_adv[p_name].wa
                    p.assist_points = p.assist_points + match_adv[p_name].ap
                end

                local last_g_idx, last_a_idx = 0, 0
                if match.goals then
                    for i, g in ipairs(match.goals) do
                        if g.scorer == p_name then last_g_idx = i end
                        if g.assist == p_name then last_a_idx = i end
                    end
                end
                if last_g_idx > 0 then p.last_goal_hist = math.max(p.last_goal_hist, num_hist * 100 + last_g_idx) end
                if last_a_idx > 0 then p.last_assist_hist = math.max(p.last_assist_hist, num_hist * 100 + last_a_idx) end
                p.last_action_hist = math.max(p.last_goal_hist, p.last_assist_hist)

                if roles.goalie == 1 then
                    if match_gp[p_name] then
                        p.weighted_ga = p.weighted_ga + match_gp[p_name].weighted_ga
                        p.clean_sheets = p.clean_sheets + match_gp[p_name].clean_sheets
                        p.pens_saved = p.pens_saved + match_gp[p_name].penalties.saved_as_goalie
                    end
                    if match.mvp and match.mvp.player == p_name and match.mvp.role == "вратарь" then
                        p.mvp_goalie = p.mvp_goalie + 1
                    end
                    
                    p.last_goalie_hist = num_hist
                    if has_et then p.et_as_goalie = p.et_as_goalie + 1 end

                    local opps = (t_id == 1) and t2_roster or ((t_id == 2) and t1_roster or (function() local o={}; for _,v in ipairs(t1_roster) do table.insert(o,v) end; for _,v in ipairs(t2_roster) do table.insert(o,v) end; return o; end)())
                    for _, opp in ipairs(opps) do
                        if opp ~= p_name then p.faced_opponents[opp] = true end
                    end
                end
            end
        end
    end

    for _, p in pairs(day_players) do
        if p.matches_goalie > 0 then
            local sum_goals, sum_matches = 0, 0
            for opp_name, _ in pairs(p.faced_opponents) do
                local opp_stats = day_players[opp_name]
                if opp_stats then
                    sum_goals = sum_goals + opp_stats.goals
                    sum_matches = sum_matches + opp_stats.matches_field
                end
            end
            p.opp_total_goals = sum_goals
            p.opp_avg_goals = sum_matches > 0 and (sum_goals / sum_matches) or 0
        end
    end

    return day_players
end

-- ==========================================
-- БЛОК МАШИНЫ ДУЭЛЕЙ (ТУРБО-ВЕРСИЯ HARVESTER)
-- ==========================================

function StatEngine.comparePair(playerA, playerB, year_db, target_matchday)
    local h2h = {
        matches = 0, ptsA = 0, ptsB = 0, gdA = 0, gdB = 0, pmA = 0, pmB = 0,
        adv_pts_countA = 0, adv_pts_countB = 0, last_pts_startA = 0, last_pts_startB = 0,
        adv_gd_countA = 0, adv_gd_countB = 0, last_gd_startA = 0, last_gd_startB = 0,
        adv_pm_countA = 0, adv_pm_countB = 0, last_pm_startA = 0, last_pm_startB = 0
    }

    local h2h_matches = {}
    for match_id, match in pairs(year_db) do
        if match.matchday == target_matchday then
            local player_teams = Harvester.get_all_teams(match)
            local teamA = player_teams[playerA]
            local teamB = player_teams[playerB]
            
            if teamA and teamB and teamA > 0 and teamB > 0 and teamA ~= teamB then
                table.insert(h2h_matches, { match_id = match_id, match = match, teamA = teamA, teamB = teamB })
            end
        end
    end

    table.sort(h2h_matches, function(a, b) return a.match.num_hist < b.match.num_hist end)

    local run_ptsA, run_ptsB, run_gdA, run_gdB = 0, 0, 0, 0
    local cur_lead_pts, cur_lead_gd = "Tie", "Tie"

    for _, item in ipairs(h2h_matches) do
        local match = item.match
        local match_res = Harvester.evaluate_match(match)
        
        if match_res then
            local resA = match_res[item.teamA]
            local resB = match_res[item.teamB]
            
            h2h.matches = h2h.matches + 1
            h2h.ptsA = h2h.ptsA + resA.pts
            h2h.ptsB = h2h.ptsB + resB.pts
            h2h.gdA = h2h.gdA + resA.gd
            h2h.gdB = h2h.gdB + resB.gd
            h2h.pmA = h2h.pmA + resA.gd
            h2h.pmB = h2h.pmB + resB.gd

            if not match.shootout_score1 then
                run_ptsA = run_ptsA + resA.pts; run_ptsB = run_ptsB + resB.pts
                local new_lead_pts = "Tie"
                if run_ptsA > run_ptsB then new_lead_pts = "A" elseif run_ptsB > run_ptsA then new_lead_pts = "B" end
                
                if new_lead_pts ~= cur_lead_pts then
                    if new_lead_pts == "A" then h2h.adv_pts_countA = h2h.adv_pts_countA + 1; h2h.last_pts_startA = match.num_hist end
                    if new_lead_pts == "B" then h2h.adv_pts_countB = h2h.adv_pts_countB + 1; h2h.last_pts_startB = match.num_hist end
                    cur_lead_pts = new_lead_pts
                end

                run_gdA = run_gdA + resA.gd; run_gdB = run_gdB + resB.gd
                local new_lead_gd = "Tie"
                if run_gdA > run_gdB then new_lead_gd = "A" elseif run_gdB > run_gdA then new_lead_gd = "B" end
                
                if new_lead_gd ~= cur_lead_gd then
                    if new_lead_gd == "A" then h2h.adv_gd_countA = h2h.adv_gd_countA + 1; h2h.last_gd_startA = match.num_hist end
                    if new_lead_gd == "B" then h2h.adv_gd_countB = h2h.adv_gd_countB + 1; h2h.last_gd_startB = match.num_hist end
                    cur_lead_gd = new_lead_gd
                end
            end
        end
    end

    return h2h
end

-- ==========================================
-- БЛОК СУДЕЙСТВА И ПРИЗОВ (Полный)
-- ==========================================

local function clone_player(p)
    local copy = {}
    for k, v in pairs(p) do copy[k] = v end
    return copy
end

local function assign_olympic_ranks(list, score_func)
    table.sort(list, function(a, b)
        local res = score_func(a, b)
        if res ~= 0 then return res == 1 end
        return a.name < b.name
    end)

    for i, p in ipairs(list) do
        if i == 1 then
            p.rank = 1
        else
            if score_func(list[i-1], p) == 0 then
                p.rank = list[i-1].rank
            else
                p.rank = i
            end
        end
    end
    return list
end

function StatEngine.evaluateMatchdayPrizes(year_db, target_matchday)
    local day_stats = StatEngine.getMatchdayAggregates(year_db, target_matchday)
    local players = {}
    for _, p in pairs(day_stats) do table.insert(players, p) end
    local prizes = { summary = {} }

    local function log_summary(p)
        if not prizes.summary[p.name] then
            prizes.summary[p.name] = { prizes = 0, places = 0, anti_prizes = 0, ranks = { [1]=0, [2]=0, [3]=0, [4]=0 } }
        end
        local s = prizes.summary[p.name]
        if p.prize then s.prizes = s.prizes + 1 end
        if p.prize_place then s.places = s.places + 1 end
        if p.anti_prize then s.anti_prizes = s.anti_prizes + 1 end
        if type(p.rank) == "number" and p.rank >= 1 and p.rank <= 4 then
            s.ranks[p.rank] = s.ranks[p.rank] + 1
        end
    end

    local function allocate_medals(list, is_mvp, is_bad_goalies, skip_anti_prize)
        local rank_counts = {}
        local max_rank = 0

        for _, p in ipairs(list) do
            local r = p.rank
            if type(r) == "number" then
                rank_counts[r] = (rank_counts[r] or 0) + 1
                if r > max_rank then max_rank = r end
            end
        end

        for _, p in ipairs(list) do
            local r = p.rank
            p.prize = false; p.prize_place = false; p.anti_prize = false; p.color = ""

            if type(r) == "number" then
                if not is_bad_goalies then
                    if r == 1 then
                        p.prize_place = true
                        if rank_counts[1] == 1 then p.prize = true; p.color = "background-color:gold;"
                        else p.color = "background-color:lightgreen;" end
                    elseif r == 2 then p.prize_place = true; p.color = "background-color:silver;"
                    elseif r == 3 then p.prize_place = true; p.color = "background-color:rgb(204,153,102);"
                    elseif r == 4 then p.prize_place = true; p.color = "background-color:darkkhaki;"
                    end
                end

                local can_get_antiprize = (max_rank > 1 or is_bad_goalies)
                if not is_mvp and not skip_anti_prize and r == max_rank and rank_counts[max_rank] == 1 and can_get_antiprize then
                    p.anti_prize = true
                    p.color = "background-color:lightsalmon;"
                end
            end
            log_summary(p)
        end
        return list
    end

    -- 1. СУДЬЯ MVP
    local mvp_list = {}; for _, p in ipairs(players) do table.insert(mvp_list, clone_player(p)) end
    prizes.mvp = allocate_medals(assign_olympic_ranks(mvp_list, function(a, b)
        if a.mvp_count ~= b.mvp_count then return a.mvp_count > b.mvp_count and 1 or -1 end
        if a.points_total ~= b.points_total then return a.points_total > b.points_total and 1 or -1 end
        if a.gd_total ~= b.gd_total then return a.gd_total > b.gd_total and 1 or -1 end
        if a.mvp_points ~= b.mvp_points then return a.mvp_points > b.mvp_points and 1 or -1 end
        if a.mvp_gd ~= b.mvp_gd then return a.mvp_gd > b.mvp_gd and 1 or -1 end
        if a.matches_total ~= b.matches_total then return a.matches_total < b.matches_total and 1 or -1 end
        if a.mvp_count > 0 and a.last_mvp_hist ~= b.last_mvp_hist then return a.last_mvp_hist < b.last_mvp_hist and 1 or -1 end
        return 0
    end), true, false, false)

    -- 2. СУДЬЯ БОМБАРДИРОВ
    local scorer_list = {}; for _, p in ipairs(players) do table.insert(scorer_list, clone_player(p)) end
    prizes.scorer = allocate_medals(assign_olympic_ranks(scorer_list, function(a, b)
        local a_has = a.goals > 0; local b_has = b.goals > 0
        if a_has ~= b_has then return a_has and 1 or -1 end
        if a_has then
            if a.goals ~= b.goals then return a.goals > b.goals and 1 or -1 end
            if a.pens_scored ~= b.pens_scored then return a.pens_scored < b.pens_scored and 1 or -1 end
            if a.matches_field ~= b.matches_field then return a.matches_field < b.matches_field and 1 or -1 end
            if a.winning_goals ~= b.winning_goals then return a.winning_goals > b.winning_goals and 1 or -1 end
            if a.goal_points ~= b.goal_points then return a.goal_points > b.goal_points and 1 or -1 end
            if a.last_goal_hist ~= b.last_goal_hist then return a.last_goal_hist < b.last_goal_hist and 1 or -1 end
            return 0
        else
            if a.matches_field ~= b.matches_field then return a.matches_field < b.matches_field and 1 or -1 end
            if a.field_total_goals ~= b.field_total_goals then return a.field_total_goals < b.field_total_goals and 1 or -1 end
            if a.field_team_goals ~= b.field_team_goals then return a.field_team_goals < b.field_team_goals and 1 or -1 end
            return 0
        end
    end), false, false, false)

    -- 3. СУДЬЯ АССИСТЕНТОВ
    local assist_list = {}; for _, p in ipairs(players) do table.insert(assist_list, clone_player(p)) end
    prizes.assistant = allocate_medals(assign_olympic_ranks(assist_list, function(a, b)
        local a_has = a.assists > 0; local b_has = b.assists > 0
        if a_has ~= b_has then return a_has and 1 or -1 end
        if a_has then
            if a.assists ~= b.assists then return a.assists > b.assists and 1 or -1 end
            if a.matches_field ~= b.matches_field then return a.matches_field < b.matches_field and 1 or -1 end
            if a.winning_assists ~= b.winning_assists then return a.winning_assists > b.winning_assists and 1 or -1 end
            if a.assist_points ~= b.assist_points then return a.assist_points > b.assist_points and 1 or -1 end
            if a.last_assist_hist ~= b.last_assist_hist then return a.last_assist_hist < b.last_assist_hist and 1 or -1 end
            return 0
        else
            if a.matches_field ~= b.matches_field then return a.matches_field < b.matches_field and 1 or -1 end
            if a.field_total_goals ~= b.field_total_goals then return a.field_total_goals < b.field_total_goals and 1 or -1 end
            if a.field_team_goals ~= b.field_team_goals then return a.field_team_goals < b.field_team_goals and 1 or -1 end
            return 0
        end
    end), false, false, false)

    -- 4. СУДЬЯ РЕЗУЛЬТАТИВНЫХ (Гол+Пас)
    local prod_list = {}; for _, p in ipairs(players) do table.insert(prod_list, clone_player(p)) end
    prizes.productive = allocate_medals(assign_olympic_ranks(prod_list, function(a, b)
        local a_act = a.goals + a.assists; local b_act = b.goals + b.assists
        local a_has = a_act > 0; local b_has = b_act > 0
        if a_has ~= b_has then return a_has and 1 or -1 end
        if a_has then
            if a_act ~= b_act then return a_act > b_act and 1 or -1 end
            if a.goals ~= b.goals then return a.goals > b.goals and 1 or -1 end
            if a.matches_field ~= b.matches_field then return a.matches_field < b.matches_field and 1 or -1 end
            local a_win_act = a.winning_goals + a.winning_assists; local b_win_act = b.winning_goals + b.winning_assists
            if a_win_act ~= b_win_act then return a_win_act > b_win_act and 1 or -1 end
            if a.winning_goals ~= b.winning_goals then return a.winning_goals > b.winning_goals and 1 or -1 end
            local a_pts = a.goal_points + a.assist_points; local b_pts = b.goal_points + b.assist_points
            if a_pts ~= b_pts then return a_pts > b_pts and 1 or -1 end
            if a.goal_points ~= b.goal_points then return a.goal_points > b.goal_points and 1 or -1 end
            if a.last_action_hist ~= b.last_action_hist then return a.last_action_hist < b.last_action_hist and 1 or -1 end
            return 0
        else
            if a.matches_field ~= b.matches_field then return a.matches_field < b.matches_field and 1 or -1 end
            if a.field_total_actions ~= b.field_total_actions then return a.field_total_actions < b.field_total_actions and 1 or -1 end
            if a.field_team_actions ~= b.field_team_actions then return a.field_team_actions < b.field_team_actions and 1 or -1 end
            return 0
        end
    end), false, false, false)

    -- 5. СУДЬЯ ЭФФЕКТИВНЫХ (Очки)
    local eff_list = {}; for _, p in ipairs(players) do table.insert(eff_list, clone_player(p)) end
    prizes.effective = allocate_medals(assign_olympic_ranks(eff_list, function(a, b)
        if a.points_total ~= b.points_total then return a.points_total > b.points_total and 1 or -1 end
        if a.gd_total ~= b.gd_total then return a.gd_total > b.gd_total and 1 or -1 end
        if a.matches_non_neutral ~= b.matches_non_neutral then return a.matches_non_neutral < b.matches_non_neutral and 1 or -1 end
        local h2h = StatEngine.comparePair(a.name, b.name, year_db, target_matchday)
        if h2h.matches > 0 then
            if h2h.ptsA ~= h2h.ptsB then return h2h.ptsA > h2h.ptsB and 1 or -1 end
            if h2h.gdA ~= h2h.gdB then return h2h.gdA > h2h.gdB and 1 or -1 end
            if h2h.adv_pts_countA ~= h2h.adv_pts_countB then return h2h.adv_pts_countA > h2h.adv_pts_countB and 1 or -1 end
            if h2h.adv_gd_countA ~= h2h.adv_gd_countB then return h2h.adv_gd_countA > h2h.adv_gd_countB and 1 or -1 end
            if h2h.adv_pts_countA > 0 and h2h.last_pts_startA ~= h2h.last_pts_startB then return h2h.last_pts_startA < h2h.last_pts_startB and 1 or -1 end
        end
        return 0
    end), false, false, false)

    -- 6. СУДЬЯ ПОЛЕЗНЫХ (Плюс/Минус)
    local use_list = {}; for _, p in ipairs(players) do table.insert(use_list, clone_player(p)) end
    prizes.useful = allocate_medals(assign_olympic_ranks(use_list, function(a, b)
        if a.plus_minus ~= b.plus_minus then return a.plus_minus > b.plus_minus and 1 or -1 end
        if a.points_total ~= b.points_total then return a.points_total > b.points_total and 1 or -1 end
        if a.gd_total ~= b.gd_total then return a.gd_total > b.gd_total and 1 or -1 end
        if a.matches_non_neutral ~= b.matches_non_neutral then return a.matches_non_neutral < b.matches_non_neutral and 1 or -1 end
        local h2h = StatEngine.comparePair(a.name, b.name, year_db, target_matchday)
        if h2h.matches > 0 then
            if h2h.pmA ~= h2h.pmB then return h2h.pmA > h2h.pmB and 1 or -1 end
            if h2h.ptsA ~= h2h.ptsB then return h2h.ptsA > h2h.ptsB and 1 or -1 end
            if h2h.gdA ~= h2h.gdB then return h2h.gdA > h2h.gdB and 1 or -1 end
            if h2h.adv_gd_countA ~= h2h.adv_gd_countB then return h2h.adv_gd_countA > h2h.adv_gd_countB and 1 or -1 end
            if h2h.adv_gd_countA > 0 and h2h.last_gd_startA ~= h2h.last_gd_startB then return h2h.last_gd_startA < h2h.last_gd_startB and 1 or -1 end
        end
        return 0
    end), false, false, false)

    -- 7. СУДЬЯ ВРАТАРЕЙ
    local goalie_good, goalie_bad = {}, {}
    for _, p in ipairs(players) do
        if p.matches_goalie > 0 then
            local p_cloned = clone_player(p)
            p_cloned.gaa = p_cloned.weighted_ga / p_cloned.matches_goalie
            p_cloned.goalie_score = 10 - p_cloned.gaa + p_cloned.mvp_goalie + p_cloned.clean_sheets + p_cloned.pens_saved
            if p_cloned.gaa <= 2.00 then table.insert(goalie_good, p_cloned) else table.insert(goalie_bad, p_cloned) end
        end
    end
    local goalie_cmp = function(a, b)
        if a.goalie_score ~= b.goalie_score then return a.goalie_score > b.goalie_score and 1 or -1 end
        if a.matches_goalie ~= b.matches_goalie then return a.matches_goalie > b.matches_goalie and 1 or -1 end
        if a.gaa ~= b.gaa then return a.gaa < b.gaa and 1 or -1 end
        if a.opp_avg_goals ~= b.opp_avg_goals then return a.opp_avg_goals > b.opp_avg_goals and 1 or -1 end
        if a.opp_total_goals ~= b.opp_total_goals then return a.opp_total_goals > b.opp_total_goals and 1 or -1 end
        if a.et_as_goalie ~= b.et_as_goalie then return a.et_as_goalie > b.et_as_goalie and 1 or -1 end
        if a.last_goalie_hist ~= b.last_goalie_hist then return a.last_goalie_hist < b.last_goalie_hist and 1 or -1 end
        return 0
    end

    prizes.goalie_good = allocate_medals(assign_olympic_ranks(goalie_good, goalie_cmp), false, false, true)

    prizes.goalie_bad = allocate_medals(assign_olympic_ranks(goalie_bad, goalie_cmp), false, true, false)
    for _, bg in ipairs(prizes.goalie_bad) do bg.rank = "—" end

    return prizes
end

	-- 8. СУДЬЯ БАШМАКОВ И АССИСТЕНТОВ (Сложные правила)
-- ==========================================
-- БЛОК 2.1: СУДЬЯ БАШМАКОВ И АССИСТЕНТОВ (Сложные правила)
-- ==========================================

function StatEngine.getTournamentAwards(year, year_db, award_type)
    local raw_data = {} -- Хранилище связок "Игрок_КодКоманды"

    -- ШАГ 1: ОДНОПРОХОДНЫЙ СБОР ВСЕХ ДАННЫХ
    for match_id, match in pairs(year_db) do
        local stage = match.stage or ""
        local is_final = (stage == "Финал")
        local is_semi  = (stage == "Полуфинал")
        local is_qf    = (stage == "1/4 финала")
        local is_r16   = (stage == "1/8 финала")
        
        local t1, t2 = match.team1, match.team2

        if match.goals then
            for i, g in ipairs(match.goals) do
                -- Определяем, чью статистику собираем: авторов голов или ассистентов
                local target_player = nil
                if award_type == "assists" then
                    target_player = g.assist
                else
                    target_player = g.scorer
                end
                
                local team_num = g.team
                local t_code = (team_num == 1) and t1 or ((team_num == 2) and t2 or nil)

                if target_player and t_code then
                    local key = target_player .. "_" .. t_code
                    if not raw_data[key] then
                        raw_data[key] = {
                            player = target_player,
                            team_code = t_code,
                            total = 0, pens = 0,
                            stages = {
                                ["Финал"]      = {t=0, p=0},
                                ["Полуфинал"]  = {t=0, p=0},
                                ["1/4 финала"] = {t=0, p=0},
                                ["1/8 финала"] = {t=0, p=0}
                            },
                            matches_map = {},
                            last_hist = 0
                        }
                    end
                    local d = raw_data[key]

                    -- Учет действия
                    d.total = d.total + 1
                    local is_pen = (g.goal_type == "пенальти" or g.goal_type2 == "пенальти")
                    -- Штрафуем за пенальти только авторов голов. Для ассистентов и других наград это не минус!
					if is_pen and award_type == "goals" then d.pens = d.pens + 1 end

                    -- Учет по стадиям плей-офф
                    if is_final then d.stages["Финал"].t = d.stages["Финал"].t + 1; if is_pen and award_type == "goals" then d.stages["Финал"].p = d.stages["Финал"].p + 1 end
                    elseif is_semi then d.stages["Полуфинал"].t = d.stages["Полуфинал"].t + 1; if is_pen and award_type == "goals" then d.stages["Полуфинал"].p = d.stages["Полуфинал"].p + 1 end
                    elseif is_qf then d.stages["1/4 финала"].t = d.stages["1/4 финала"].t + 1; if is_pen and award_type == "goals" then d.stages["1/4 финала"].p = d.stages["1/4 финала"].p + 1 end
                    elseif is_r16 then d.stages["1/8 финала"].t = d.stages["1/8 финала"].t + 1; if is_pen and award_type == "goals" then d.stages["1/8 финала"].p = d.stages["1/8 финала"].p + 1 end end

                    -- Учет результативности за отдельный матч
                    if not d.matches_map[match_id] then d.matches_map[match_id] = {t=0, p=0} end
                    d.matches_map[match_id].t = d.matches_map[match_id].t + 1
                    if is_pen then d.matches_map[match_id].p = d.matches_map[match_id].p + 1 end

                    -- Хронологический маркер (У кого меньше — тот сделал это раньше)
                    local current_hist = (match.num_hist or 0) * 100 + i
                    if current_hist > d.last_hist then d.last_hist = current_hist end
                end
            end
        end
    end

    -- ИСКЛЮЧЕНИЕ: ЧТМ-2022, Геныч (Киргизия), Золотой Башмак (+4 гола за аннулированный матч за 3 место)
    if year == 2022 and award_type == "goals" then
        local key = "Геныч_КИР"
        if raw_data[key] then
            raw_data[key].total = raw_data[key].total + 4
            -- Записываем этот матч как фантомное событие, чтобы тайбрейкер "результативность за матч" работал
            raw_data[key].matches_map["2022-bronze-phantom"] = {t = 4, p = 0}
            -- Задираем хронологию в небеса (матч был сыгран в конце турнира, при равенстве он должен проигрывать)
            raw_data[key].last_hist = 29300
        end
    end

    -- ШАГ 2: ПОДГОТОВКА МАССИВОВ РЕЗУЛЬТАТИВНОСТИ МАТЧЕЙ
    local list = {}
    for _, d in pairs(raw_data) do
        d.match_arr = {}
        for _, m_stat in pairs(d.matches_map) do table.insert(d.match_arr, m_stat) end
        
        -- Сортируем матчи игрока от самого результативного к наименее результативному
        table.sort(d.match_arr, function(a, b)
            if a.t ~= b.t then return a.t > b.t end -- Сначала по общему числу голов
            local anp, bnp = a.t - a.p, b.t - b.p
            if anp ~= bnp then return anp > bnp end -- Затем по голам без пенальти
            return false
        end)
        table.insert(list, d)
    end

    -- ШАГ 3: МЕХАНИЗМ СРАВНЕНИЯ ПО ВСЕМ 12 КРИТЕРИЯМ
    local function award_compare(a, b)
        -- 1. Наименьшее количество пенальти
        if a.pens ~= b.pens then return a.pens < b.pens and 1 or -1 end
        
        -- 2-9. Стадии плей-офф (Больше действий, затем меньше пенальти)
        local st_order = {"Финал", "Полуфинал", "1/4 финала", "1/8 финала"}
        for _, st in ipairs(st_order) do
            if a.stages[st].t ~= b.stages[st].t then return a.stages[st].t > b.stages[st].t and 1 or -1 end
            if a.stages[st].p ~= b.stages[st].p then return a.stages[st].p < b.stages[st].p and 1 or -1 end
        end

        -- 10-11. Результативность по отдельным матчам
        local max_m = math.max(#a.match_arr, #b.match_arr)
        for i = 1, max_m do
            local am = a.match_arr[i] or {t=0,p=0}
            local bm = b.match_arr[i] or {t=0,p=0}
            if am.t ~= bm.t then return am.t > bm.t and 1 or -1 end
            local anp, bnp = am.t - am.p, bm.t - bm.p
            if anp ~= bnp then return anp > bnp and 1 or -1 end
        end

        -- 12. Исторический приоритет (кто сделал это раньше)
        if a.last_hist ~= b.last_hist then return a.last_hist < b.last_hist and 1 or -1 end
        return 0
    end

    -- Первичная сортировка по сумме, вторичная — по 12 правилам
    table.sort(list, function(a, b)
        if a.total ~= b.total then return a.total > b.total end
        local res = award_compare(a, b)
        if res ~= 0 then return res == 1 end
        return a.player < b.player
    end)

    -- ШАГ 4: СИСТЕМА ВЫДАЧИ ОЛИМПИЙСКИХ МЕДАЛЕЙ (С правилом множественного золота)
    local current_gold_total = nil
    local players_with_gold = {}

    for i, p in ipairs(list) do
        if current_gold_total == nil then current_gold_total = p.total end

        if p.total == current_gold_total then
            -- Человек в топе. Если он еще не получал золото, даем ранг 1
            if not players_with_gold[p.player] then
                p.rank = 1
                players_with_gold[p.player] = true
            else
                -- Игрок делит 1 место сам с собой. Присваиваем фактическое место
                p.rank = i
            end
        else
            -- Остальным присваиваем их фактический индекс в массиве
            p.rank = i
        end

        -- Раскраска
        if p.rank == 1 then p.color = "background-color:gold;"
        elseif p.rank == 2 then p.color = "background-color:silver;"
        elseif p.rank == 3 then p.color = "background-color:rgb(204,153,102);"
        elseif p.rank == 4 then p.color = "background-color:darkkhaki;"
        else p.color = "" end
    end

    return list
end

-- ==========================================
-- БЛОК 2.2: УНИВЕРСАЛЬНЫЙ СУДЬЯ ПРОСТЫХ МЕТРИК
-- ==========================================
function StatEngine.getGenericMetricByTeams(year, year_db, metric_name, options)
    options = options or {}
    local m_conf = Config.metrics[metric_name]
    if not m_conf then return {} end

    local stats = Harvester.run(year_db, { need_players = false, need_teams = false, need_combos = true })
    
    local list = {}
    for combo_key, c_data in pairs(stats.PlayerTeam) do
        local val = m_conf.get_val(c_data)
        local is_valid = false
        
        if type(val) == "number" and val ~= 0 then is_valid = true end
        if type(val) == "table" and (val.num or 0) ~= 0 then is_valid = true end

        if is_valid then
            table.insert(list, {
                player = c_data.name,
                team_code = c_data.team,
                total = val,
                pens = 0
            })
        end
    end

    -- ==========================================
    -- НАСТРОЙКА СОРТИРОВКИ И МЕДАЛЕЙ
    -- ==========================================
    local is_anti = m_conf.anti_prize
    local is_asc = m_conf.sort_asc
    
    -- ИСКЛЮЧЕНИЕ ДЛЯ ПЛЮС/МИНУС (Игнорируем настройки конфига, опираемся на worst)
    if metric_name == "plus_minus" then
        if options.worst then
            is_anti = true  -- Худшие получают цвет лосося
            is_asc = true   -- Сортируем по возрастанию (от -10 до -1, чтобы самые минусы были наверху)
        else
            is_anti = false -- Лучшие получают золото/серебро/бронзу
            is_asc = false  -- Сортируем по убыванию (от +10 до +1)
        end
    else
        -- Стандартная логика для остальных
        if options.worst then
            is_anti = true
            is_asc = not is_asc
        end
    end

    table.sort(list, function(a, b)
        local valA = type(a.total) == "table" and Config.utils.calc_avg(a.total.num, a.total.den) or a.total
        local valB = type(b.total) == "table" and Config.utils.calc_avg(b.total.num, b.total.den) or b.total
        
        if valA ~= valB then
            if is_asc then return valA < valB end
            return valA > valB
        end
        return a.player < b.player
    end)

    if not m_conf.no_medals then
        local current_val = nil
        local current_rank = 0

        for i, p in ipairs(list) do
            local p_val = type(p.total) == "table" and Config.utils.calc_avg(p.total.num, p.total.den) or p.total
            if current_val == nil or p_val ~= current_val then
                current_val = p_val
                current_rank = i
            end
            p.rank = current_rank
        end

        for _, p in ipairs(list) do
            if is_anti then
                if p.rank == 1 then p.color = Config.styles.lightsalmon end
            else
                if p.rank == 1 then p.color = Config.styles.gold
                elseif p.rank == 2 then p.color = Config.styles.silver
                elseif p.rank == 3 then p.color = Config.styles.bronze
                elseif p.rank == 4 then p.color = Config.styles.wood
                else p.color = "" end
            end
        end
    end

    return list
end

-- ==========================================
-- БЛОК ТЕСТОВ
-- ==========================================

-- удалил все тесты, кроме одного, который удобен для проверки всей статистики игроков на одной странице
-- если вдруг кому-то понадобятся старые тесты, то они есть на этой версии страницы:
-- [[Служебная:Permalink/64861]]

function StatEngine.testAllMetrics(frame)
    local stats = Harvester.run_all_time({ need_players = true, need_teams = false, need_combos = false })
    local list = {}
    for _, p in pairs(stats.Players) do
        if p.matches_total > 0 or p.goals.total > 0 or p.assists.total > 0 or p.mvp.is_mvp > 0 or p.own_goals > 0 or p.clean_sheets > 0 or p.cards.yellow > 0 then
            table.insert(list, p)
        end
    end
    table.sort(list, function(a, b) return a.name < b.name end)
    local html = mw.html.create('table'):addClass(Config.styles.classes):attr('border', '1'):css('font-size', '90%')
    local cols = { "Игрок", "Матчи", "В поле", "В ворот.", "Голы", "Ср.Голы", "Головой", "Пяткой", "Штраф.", "Вратар.", "Автоголы", "Мега-трик(Г)", "Ассисты", "Ср.Ассисты", "Мега-трик(А)", "+/-", "Выносы", "ЖК", "КК", "MVP", "MVP(ВР)", "Сухари", "КПГ", "Пен(Забил)", "Пен(Мимо)", "Пен(Сейв)", "Пен(Привоз)", "Поб.Гол", "Поб.Асс", "Очки(Гол)", "Очки(Асс)" }
    local tr_head = html:tag('tr')
    for _, col in ipairs(cols) do tr_head:tag('th'):cssText(Config.styles.header .. ' white-space:nowrap;'):wikitext(col) end
    for _, p in ipairs(list) do
        local tr = html:tag('tr')
        local function td(val, bold)
            local text = tostring(val)
            if bold then text = "'''" .. text .. "'''" end
            tr:tag('td'):cssText(Config.styles.center):wikitext(text)
        end
        td(p.name, true); td(p.matches_total); td(p.matches_field); td(p.matches_goalie)
        td(p.goals.total, true)
        local avg_g = p.avg.goals_den > 0 and (p.avg.goals_num / p.avg.goals_den) or 0
        td(string.format("%.2f", avg_g)); td(p.goals.head); td(p.goals.heel); td(p.goals.free_kick); td(p.goals.goalie); td(p.own_goals)
        td(p.goals.hat_trick + p.goals.poker + p.goals.penta + p.goals.hexa)
        td(p.assists.total, true)
        local avg_a = p.avg.assists_den > 0 and (p.avg.assists_num / p.avg.assists_den) or 0
        td(string.format("%.2f", avg_a)); td(p.assists.hat_trick + p.assists.poker + p.assists.penta)
        local pm_str = tostring(p.plus_minus)
        if p.plus_minus > 0 then pm_str = "+" .. pm_str end
        td(pm_str); td(p.clearances); td(p.cards.yellow); td(p.cards.red)
        td(p.mvp.is_mvp); td(p.mvp.is_goalie_mvp); td(p.clean_sheets)
        local gaa = p.matches_goalie > 0 and (p.weighted_ga / p.matches_goalie) or 0
        td(string.format("%.2f", gaa))
        local pens_scored = p.penalties.in_game.g + p.penalties.shootout.g
        local pens_missed = (p.penalties.in_game.u - p.penalties.in_game.g) + (p.penalties.shootout.u - p.penalties.shootout.g)
        td(pens_scored); td(pens_missed); td(p.penalties.saved_as_goalie); td(p.penalties.caused_pens)
        td(p.advanced.winning_goals); td(p.advanced.winning_assists); td(string.format("%.2f", p.advanced.goal_points)); td(string.format("%.2f", p.advanced.assist_points))
    end
    return frame:preprocess(Config.styles.wiki_templates .. tostring(html))
end

-- ==========================================
--  ТЕСТ БАШМАКОВ И АССИСТЕНТОВ
-- ==========================================
function StatEngine.testAwardSorting(frame)
    local year = tonumber(frame.args.year) or 2046
    local award_type = frame.args.award or "goals" -- "goals" или "assists"
    
    local success, year_db = pcall(require, 'Module:Data/' .. year)
    if not success then return "БД за " .. year .. " год не найдена" end

    local sorted_list = StatEngine.getTournamentAwards(year, year_db, award_type)

    local html = mw.html.create('table'):addClass(Config.styles.classes):attr('border', '1')
    
    -- Рисуем сложную шапку
    local tr_head = html:tag('tr')
    local action_name = (award_type == "assists") and "Ассисты" or "Голы"
    local cols = {"Место", "Игрок (Команда)", action_name, "Пен", "Финал", "1/2", "1/4", "1/8", "Матч 1", "Матч 2", "Матч 3", "Матч 4", "Маркер времени"}
    for _, col in ipairs(cols) do
        tr_head:tag('th'):cssText(Config.styles.header):wikitext(col)
    end

    -- Форматтер ячеек с пенальти (например: "2 (1)", или просто "3")
    local function format_gp(t, p)
        if t == 0 then return "—" end
        if p > 0 then return t .. " (" .. p .. ")" end
        return tostring(t)
    end

    for _, p in ipairs(sorted_list) do
        -- ТЕСТОВЫЙ ФИЛЬТР: Выводим только тех, кто набрал 3 и более действий
        if p.total >= 3 then
            local tr = html:tag('tr')
            
            tr:tag('td'):cssText(Config.styles.center .. p.color):wikitext(tostring(p.rank))
            tr:tag('td'):cssText(Config.styles.center):wikitext(p.player .. " (" .. p.team_code .. ")")
            tr:tag('td'):cssText(Config.styles.center):wikitext("'''" .. p.total .. "'''")
            tr:tag('td'):cssText(Config.styles.center):wikitext(tostring(p.pens))
            
            tr:tag('td'):cssText(Config.styles.center):wikitext(format_gp(p.stages["Финал"].t, p.stages["Финал"].p))
            tr:tag('td'):cssText(Config.styles.center):wikitext(format_gp(p.stages["Полуфинал"].t, p.stages["Полуфинал"].p))
            tr:tag('td'):cssText(Config.styles.center):wikitext(format_gp(p.stages["1/4 финала"].t, p.stages["1/4 финала"].p))
            tr:tag('td'):cssText(Config.styles.center):wikitext(format_gp(p.stages["1/8 финала"].t, p.stages["1/8 финала"].p))
            
            for i = 1, 4 do
                local m = p.match_arr[i] or {t=0,p=0}
                tr:tag('td'):cssText(Config.styles.center):wikitext(format_gp(m.t, m.p))
            end
            
            tr:tag('td'):cssText(Config.styles.center):wikitext(tostring(p.last_hist))
        end
    end

    return frame:preprocess(Config.styles.wiki_templates .. tostring(html))
end

return StatEngine