Модуль: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