Модуль:StatEngine

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

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

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

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

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

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

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

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

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

См. также


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

-- ==========================================
-- Модуль:StatEngine
-- Версия 2.6
-- ==========================================
-- полностью переписан
-- Если кому-то понадобится версия 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
        },
        -- [НОВЫЙ БЛОК ДЛЯ ЗНАЧИМОСТИ]
        awards = {
            golden_spheres = 0, other_spheres = 0,
            golden_shoes = 0, other_shoes = 0,
            golden_assistants = 0, other_assistants = 0,
            best_goalies = 0, superchamps = 0,
            titles = 0, finals_played = 0
        },
        -- [/НОВЫЙ БЛОК]
        avg = { goals_num = 0, goals_den = 0, assists_num = 0, assists_den = 0 },
        megarating = {
            mr_matches = 0, mr_points = 0,
            playoff_mvp = { r16 = 0, qf = 0, sf = 0, final = 0 },
            playoff_wins = { r16 = 0, qf = 0, sf = 0 },
            is_champion = false, best_goal_bonus = false,
            final_goals = 0, final_gold_goals = 0,
            final_assists = 0, final_gold_assists = 0,
            semi_goals = 0, semi_assists = 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.awards then
        target.awards.golden_spheres = target.awards.golden_spheres + source.awards.golden_spheres
        target.awards.other_spheres = target.awards.other_spheres + source.awards.other_spheres
        target.awards.golden_shoes = target.awards.golden_shoes + source.awards.golden_shoes
        target.awards.other_shoes = target.awards.other_shoes + source.awards.other_shoes
        target.awards.golden_assistants = target.awards.golden_assistants + source.awards.golden_assistants
        target.awards.other_assistants = target.awards.other_assistants + source.awards.other_assistants
        target.awards.best_goalies = target.awards.best_goalies + source.awards.best_goalies
        target.awards.superchamps = target.awards.superchamps + source.awards.superchamps
        target.awards.titles = target.awards.titles + source.awards.titles
        target.awards.finals_played = target.awards.finals_played + source.awards.finals_played
    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
    
    if source.megarating then
        target.megarating.mr_matches = target.megarating.mr_matches + source.megarating.mr_matches
        target.megarating.mr_points = target.megarating.mr_points + source.megarating.mr_points
        for k, v in pairs(source.megarating.playoff_mvp) do target.megarating.playoff_mvp[k] = target.megarating.playoff_mvp[k] + v end
        for k, v in pairs(source.megarating.playoff_wins) do target.megarating.playoff_wins[k] = target.megarating.playoff_wins[k] + v end
        target.megarating.is_champion = target.megarating.is_champion or source.megarating.is_champion
        target.megarating.best_goal_bonus = target.megarating.best_goal_bonus or source.megarating.best_goal_bonus
        target.megarating.final_goals = target.megarating.final_goals + source.megarating.final_goals
        target.megarating.final_gold_goals = target.megarating.final_gold_goals + source.megarating.final_gold_goals
        target.megarating.final_assists = target.megarating.final_assists + source.megarating.final_assists
        target.megarating.final_gold_assists = target.megarating.final_gold_assists + source.megarating.final_gold_assists
        target.megarating.semi_goals = target.megarating.semi_goals + source.megarating.semi_goals
        target.megarating.semi_assists = target.megarating.semi_assists + source.megarating.semi_assists
    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
    
    -- Если это сырой результат одного матча (source.matches = nil), прибавляем 1.
    -- Если это уже собранная статистика года/матча, прибавляем её количество.
    target.matches = target.matches + (source.matches or 1)
    
    -- Сырой результат использует .pts, агрегированный — .points
    target.points = target.points + (source.pts or source.points or 0)
    
    target.scored = target.scored + (source.scored or 0)
    target.conceded = target.conceded + (source.conceded or 0)
    target.gd = target.gd + (source.gd or 0)
    target.w = target.w + (source.w or 0)
    target.w_ot = target.w_ot + (source.w_ot or 0)
    target.d = target.d + (source.d or 0)
    target.l_ot = target.l_ot + (source.l_ot or 0)
    target.l = target.l + (source.l or 0)
end

-- === 7.0 АТОМАРНЫЙ СБОРЩИК (ОДИН МАТЧ) ===
-- Это фундамент. Возвращает статистику строго за одну игру.
function Harvester.process_match(match_id, match, options)
    options = options or { need_players = true, need_teams = false, need_combos = false }
    local MatchStats = { Players = {}, Teams = {}, PlayerTeam = {} }

    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
            local t1_code = match_resolution[1].code
            local t2_code = match_resolution[2].code
            MatchStats.Teams[t1_code] = Harvester.create_empty_team_stats(); MatchStats.Teams[t1_code].code = t1_code
            MatchStats.Teams[t2_code] = Harvester.create_empty_team_stats(); MatchStats.Teams[t2_code].code = t2_code
            merge_team_stats(MatchStats.Teams[t1_code], match_resolution[1])
            merge_team_stats(MatchStats.Teams[t2_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 = Harvester.create_empty_stats()
            gp.name = p_name; gp.team_id = t_id -- сохраняем за какую команду играл
            
            gp.matches_total = roles.total
            gp.matches_field = roles.field
            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 })
            
            local st = match.stage or ""
            local is_r16 = (st == "1/8 финала"); local is_qf = (st == "1/4 финала"); local is_sf = (st == "Полуфинал"); local is_final = (st == "Финал")
            
            local mr = { mr_matches = 0, mr_points = 0, playoff_mvp = { r16=0, qf=0, sf=0, final=0 }, playoff_wins = { r16=0, qf=0, sf=0 }, is_champion = false, best_goal_bonus = false, final_goals = 0, final_gold_goals = 0, final_assists = 0, final_gold_assists = 0, semi_goals = 0, semi_assists = 0 }
            if t_id == 1 or t_id == 2 then
                mr.mr_matches = roles.total
                if roles.total > 0 and match_resolution then
                    mr.mr_points = match_resolution[t_id].pts
                    if match_resolution[t_id].pts >= 2 then
                        if is_r16 then mr.playoff_wins.r16 = 1 end
                        if is_qf then mr.playoff_wins.qf = 1 end
                        if is_sf then mr.playoff_wins.sf = 1 end
                        if is_final then mr.is_champion = true end
                    end
                end
            end
            if match.mvp and match.mvp.player == p_name then
                if is_r16 then mr.playoff_mvp.r16 = 1 end; if is_qf then mr.playoff_mvp.qf = 1 end; if is_sf then mr.playoff_mvp.sf = 1 end; if is_final then mr.playoff_mvp.final = 1 end
            end
            if is_final and match.best_goal == p_name then mr.best_goal_bonus = true end
            if is_final then mr.final_goals = m_g; mr.final_assists = m_a; mr.final_gold_goals = (match_adv and match_adv[p_name] and match_adv[p_name].wg or 0); mr.final_gold_assists = (match_adv and match_adv[p_name] and match_adv[p_name].wa or 0) elseif is_sf then mr.semi_goals = m_g; mr.semi_assists = m_a end

            merge_stats(gp, { megarating = mr })
            
            -- [НОВЫЙ БЛОК ДЛЯ ЗНАЧИМОСТИ: УЧАСТИЕ И ТИТУЛЫ В ФИНАЛЕ]
            if match.stage == "Финал" then
                local aw = { golden_spheres = 0, other_spheres = 0, golden_shoes = 0, other_shoes = 0, golden_assistants = 0, other_assistants = 0, best_goalies = 0, superchamps = 0, titles = 0, finals_played = 1 }
                if match_resolution and (t_id == 1 or t_id == 2) and match_resolution[t_id].pts >= 2 then aw.titles = 1 end
                merge_stats(gp, { awards = aw })
            end
            -- [/НОВЫЙ БЛОК]

            MatchStats.Players[p_name] = gp
        end
        
        if options.need_combos then
            local t_code = get_team_code(t_id)
            local combo_key = p_name .. "_" .. t_code
            local gc = Harvester.create_empty_stats()
            gc.name = p_name; gc.team = t_code
            gc.matches_total = roles.total; gc.matches_field = roles.field; 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
            MatchStats.PlayerTeam[combo_key] = gc
        end
    end
    
    -- [НОВЫЙ БЛОК ДЛЯ ЗНАЧИМОСТИ: ГЛОБАЛЬНЫЕ ПРИЗЫ ТУРНИРА]
    if match.stage == "Финал" and options.need_players then
        local function add_aw(p, key)
            if not p or p == "none" then return end
            -- Если игрок не играл в финале, создаем ему пустой профиль чисто для наград
            if not MatchStats.Players[p] then
                MatchStats.Players[p] = Harvester.create_empty_stats()
                MatchStats.Players[p].name = p
            end
            if not MatchStats.Players[p].awards then
                MatchStats.Players[p].awards = {golden_spheres=0, other_spheres=0, golden_shoes=0, other_shoes=0, golden_assistants=0, other_assistants=0, best_goalies=0, superchamps=0, titles=0, finals_played=0}
            end
            MatchStats.Players[p].awards[key] = MatchStats.Players[p].awards[key] + 1
        end

        local function parse_aw(aw_data, key)
            if not aw_data then return end
            if type(aw_data) == "table" then
                for _, p in ipairs(aw_data) do add_aw(p, key) end
            elseif type(aw_data) == "string" then
                add_aw(aw_data, key)
            end
        end

        parse_aw(match.golden_sphere, "golden_spheres")
        parse_aw(match.silver_sphere, "other_spheres")
        parse_aw(match.bronze_sphere, "other_spheres")
        parse_aw(match.wooden_sphere, "other_spheres")
        
        parse_aw(match.golden_shoe, "golden_shoes")
        parse_aw(match.silver_shoe, "other_shoes")
        parse_aw(match.bronze_shoe, "other_shoes")
        parse_aw(match.wooden_shoe, "other_shoes")
        
        parse_aw(match.golden_assistant, "golden_assistants")
        parse_aw(match.silver_assistant, "other_assistants")
        parse_aw(match.bronze_assistant, "other_assistants")
        parse_aw(match.wooden_assistant, "other_assistants")
        
        parse_aw(match.elnur_award, "best_goalies")
        parse_aw(match.superchampions, "superchamps")
    end
    -- [/НОВЫЙ БЛОК]
    
    return MatchStats
end

-- === 7.1 СБОРЩИК ТУРНИРА (ОДИН ГОД) ===
-- Собирает год, опираясь на данные отдельных матчей.
function Harvester.run(year_db, options)
    options = options or { need_players = true, need_teams = false, need_combos = false, keep_matches = false }
    local YearStats = { Players = {}, Teams = {}, PlayerTeam = {}, Matches = {} }

    local function get_p(name) if not YearStats.Players[name] then YearStats.Players[name] = Harvester.create_empty_stats(); YearStats.Players[name].name = name; if options.keep_matches then YearStats.Players[name].matches = {} end end return YearStats.Players[name] end
    local function get_t(code) if not YearStats.Teams[code] then YearStats.Teams[code] = Harvester.create_empty_team_stats(); YearStats.Teams[code].code = code; if options.keep_matches then YearStats.Teams[code].matches = {} end end return YearStats.Teams[code] end
    local function get_c(key, n, t) if not YearStats.PlayerTeam[key] then YearStats.PlayerTeam[key] = Harvester.create_empty_stats(); YearStats.PlayerTeam[key].name = n; YearStats.PlayerTeam[key].team = t; if options.keep_matches then YearStats.PlayerTeam[key].matches = {} end end return YearStats.PlayerTeam[key] end

    for match_id, match in pairs(year_db) do
        -- 1. Считаем этот конкретный матч
        local m_stats = Harvester.process_match(match_id, match, options)
        
        -- Если попросили сохранить матчи глобально - сохраняем
        if options.keep_matches then YearStats.Matches[match_id] = m_stats end

        -- 2. Сливаем данные матча в общую "мясорубку" года
        if options.need_players then
            for p_name, p_data in pairs(m_stats.Players) do
                local gp = get_p(p_name)
                merge_stats(gp, p_data)
                if options.keep_matches then gp.matches[match_id] = p_data end -- Сохраняем стату игрока за этот матч внутри самого игрока!
            end
        end
        if options.need_teams then
            for t_code, t_data in pairs(m_stats.Teams) do
                local gt = get_t(t_code)
                merge_team_stats(gt, t_data)
                if options.keep_matches then gt.matches[match_id] = t_data end
            end
        end
        if options.need_combos then
            for c_key, c_data in pairs(m_stats.PlayerTeam) do
                local gc = get_c(c_key, c_data.name, c_data.team)
                merge_stats(gc, c_data)
                if options.keep_matches then gc.matches[match_id] = c_data end
            end
        end
    end
    
    -- Пост-обработка (Плюс/Минус)
    local year_detected = nil; for match_id, _ in pairs(year_db) do year_detected = Config.utils.get_tournament_year(match_id); if year_detected then break end end
    if year_detected then
        local pm_conf = Config.metrics["plus_minus"]
        if pm_conf and pm_conf.adjustments and pm_conf.adjustments.players then
            if options.need_players then for p_name, gp in pairs(YearStats.Players) do local adj = pm_conf.adjustments.players[p_name] and pm_conf.adjustments.players[p_name][year_detected]; if adj then gp.plus_minus = gp.plus_minus + adj end end end
            if options.need_combos then for combo_key, gc in pairs(YearStats.PlayerTeam) do local adj = pm_conf.adjustments.players[gc.name] and pm_conf.adjustments.players[gc.name][year_detected]; if adj then gc.plus_minus = gc.plus_minus + adj end end end
        end
    end
    return YearStats
end

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

    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.keep_years then GrandStats.Years[year] = year_stats end

            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; if options.keep_years then GrandStats.Players[p_name].years = {} end end
                    merge_stats(GrandStats.Players[p_name], p_data)
                    if options.keep_years then GrandStats.Players[p_name].years[year] = p_data end
                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; if options.keep_years then GrandStats.Teams[t_code].years = {} end end
                    merge_team_stats(GrandStats.Teams[t_code], t_data)
                    if options.keep_years then GrandStats.Teams[t_code].years[year] = t_data end
                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; if options.keep_years then GrandStats.PlayerTeam[combo_key].years = {} end end
                    merge_stats(GrandStats.PlayerTeam[combo_key], c_data)
                    if options.keep_years then GrandStats.PlayerTeam[combo_key].years[year] = c_data end
                end
            end
        end
    end
    return GrandStats
end

-- ==========================================
-- Перенесено из основного модуля на подстраницы:
-- ==========================================
-- [[Модуль:StatEngine/Legacy]]
-- БЛОК ОБРАТНОЙ СОВМЕСТИМОСТИ (ФАСАД/АДАПТЕР)
-- getAllPlayersMatchStats и StatEngine.getPlayerMatchStats
-- Эти функции оставляют интерфейс старым (вызовы из вики-шаблонов),
-- но внутри используют супер-быстрый Комбайн для точечных задач.
-- ==========================================
-- [[Модуль:StatEngine/Matchday]]
-- БЛОК АГРЕГАЦИИ ИГРОВОГО ДНЯ (ТУРБО-ВЕРСИЯ HARVESTER)
-- ==========================================
-- [[Модуль:StatEngine/TournamentAwards]]
-- БЛОК СУДЕЙСТВА И ПРИЗОВ
-- СУДЬЯ БАШМАКОВ И АССИСТЕНТОВ
-- УНИВЕРСАЛЬНЫЙ СУДЬЯ ПРОСТЫХ МЕТРИК
-- ==========================================
-- БЛОК ТЕСТОВ — [[Модуль:StatEngine/sandbox]]
-- ==========================================

StatEngine.Harvester = Harvester

return StatEngine