Модуль:StatEngine: различия между версиями
Burato (обсуждение | вклад) для раскраски ячеек в +/- |
Burato (обсуждение | вклад) Версия 2.2 |
||
| Строка 1: | Строка 1: | ||
-- ========================================== | -- ========================================== | ||
-- Модуль:StatEngine - версия 2 | -- Модуль:StatEngine - версия 2.2 | ||
-- ========================================== | -- ========================================== | ||
-- полностью переписан | -- полностью переписан | ||
Версия от 13:09, 17 апреля 2026
Модуль является основным вычислительным блоком, отвечающим за обработку и анализ игровой статистики. Модуль использует данные о матчах и правила из 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