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

Материал из ЧТМ
Перейти к навигации Перейти к поиску
обновление
Нет описания правки
 
(не показано 12 промежуточных версий 3 участников)
Строка 53: Строка 53:
-- 2. РУЧНОЙ КОНТРОЛЬ АКТИВНЫХ КОМАНД (КОСТЫЛЬ)
-- 2. РУЧНОЙ КОНТРОЛЬ АКТИВНЫХ КОМАНД (КОСТЫЛЬ)
-- =========================================================
-- =========================================================
-- Сюда вписываются команды, которые продолжают выступления на текущем ЧТМ
--[[
-- Чтобы не писать сложнейшие алгоритмы для группового раунда
Сюда вписываются команды, которые продолжают выступления на текущем ЧТМ,
чтобы не писать сложнейшие алгоритмы для группового раунда
--]]
Config.active_teams_list = {}  
Config.active_teams_list = {}  


Строка 132: Строка 134:
     cards = 2038
     cards = 2038
}
}
-- Обычно все подсчёты идут именно с указанных годов.
--[[
-- Но есть одно важнейшее исключение:
Обычно все подсчёты идут именно с указанных годов.
-- Несмотря на эти цифры, абсолютно во всех финальных матчах
Но есть одно важнейшее исключение:
-- известны все составы и ассистенты. Как правило, в общую
Несмотря на эти цифры, абсолютно во всех финальных матчах
-- статистику эти показатели не входят, но когда подсчитывается
известны все составы и ассистенты. Как правило, в общую
-- что-то связанное именно с финалами или чемпионскими титулами,
статистику эти показатели не входят, но когда подсчитывается
-- то их тоже надо считать. Поэтому когда создаются алгоритмы подсчёта
что-то связанное именно с финалами или чемпионскими титулами,
-- для чего-то, связанного с финалами, то ни в коем случае не надо
то их тоже надо считать. Поэтому когда создаются алгоритмы подсчёта
-- связывать подсчёты с сыгранными матчами, иначе показатели финалов
для чего-то, связанного с финалами, то ни в коем случае не надо
-- с 2006 до 2018 не попадут в статистику.
связывать подсчёты с сыгранными матчами, иначе показатели финалов
с 2006 до 2018 не попадут в статистику.
--]]


-- =========================================================
-- =========================================================
Строка 166: Строка 170:


     ["own_goals"]          = { start = 2006, anti_prize = true, get_val = function(stats) return stats.own_goals end },
     ["own_goals"]          = { start = 2006, anti_prize = true, get_val = function(stats) return stats.own_goals end },
     ["head_goals"]         = { start = 2022, get_val = function(stats) return stats.goals.head end },
     ["head_goals"] = { -- не записывались в конкретных матчах на ЧТМ-2022 и ЧТМ-2026,
     ["heel_goals"]         = { start = 2026, get_val = function(stats) return stats.goals.heel end },
    -- а просто подсчитывались суммарно
        start = 2022,
        get_val = function(stats) return stats.goals.head end,
        adjustments = {
            players = {
                ["Диман"] = { [2022] = 3, [2026] = 4 },
                ["Антон"] = { [2026] = 1 },
                ["Макс"] = { [2026] = 1 },
            }
        }
    },
     ["heel_goals"] = { -- не записывались в конкретных матчах на ЧТМ-2026,
    -- а просто подсчитывались суммарно
        start = 2026,
        get_val = function(stats) return stats.goals.heel end,
        adjustments = {
            players = {
                ["Геныч"] = { [2026] = 2 },
                ["Макс"] = { [2026] = 1 },
                ["Серёга"] = { [2026] = 1 },
            }
        }
    },
     ["free_kick_goals"]    = { start = 2026, get_val = function(stats) return stats.goals.free_kick end },
     ["free_kick_goals"]    = { start = 2026, get_val = function(stats) return stats.goals.free_kick end },
     ["goalie_goals"]      = { start = 2022, get_val = function(stats) return stats.goals.goalie end },
     ["goalie_goals"]      = { start = 2022, get_val = function(stats) return stats.goals.goalie end },
Строка 178: Строка 204:
     },
     },
     ["assist_mega_tricks"] = { start = 2026, get_val = function(stats) return stats.assists.hat_trick + stats.assists.poker + stats.assists.penta end },
     ["assist_mega_tricks"] = { start = 2026, get_val = function(stats) return stats.assists.hat_trick + stats.assists.poker + stats.assists.penta end },
     ["clearances"]         = { start = 2022, get_val = function(stats) return stats.clearances end },
     ["clearances"] = { -- не записывались в конкретных матчах на ЧТМ-2022 и ЧТМ-2026
    -- а просто подсчитывались суммарно
        start = 2022,
        get_val = function(stats) return stats.clearances end,
        adjustments = {
            players = {
                ["Диман"] = { [2022] = 4, [2026] = 11 },
                ["Макс"] = { [2026] = 5 },
                ["Геныч"] = { [2026] = 2 },
                ["Антон"] = { [2026] = 1 },
                ["Ринат"] = { [2026] = 1 },
                ["Диман Е."] = { [2026] = 1 },
                ["Эльнур"] = { [2026] = 1 },
            }
        }
    },
     ["yellow_cards"]      = { start = 2038, anti_prize = true, get_val = function(stats) return stats.cards.yellow end },
     ["yellow_cards"]      = { start = 2038, anti_prize = true, get_val = function(stats) return stats.cards.yellow end },
     ["red_cards"]          = { start = 2038, anti_prize = true, get_val = function(stats) return stats.cards.red end },
     ["red_cards"]          = { start = 2038, anti_prize = true, get_val = function(stats) return stats.cards.red end },
Строка 189: Строка 230:
     ["pens_saved"]        = { start = 2006, get_val = function(stats) return stats.penalties.saved_as_goalie end },
     ["pens_saved"]        = { start = 2006, get_val = function(stats) return stats.penalties.saved_as_goalie end },
     ["caused_pens"]        = { start = 2026, anti_prize = true, get_val = function(stats) return stats.penalties.caused_pens end },
     ["caused_pens"]        = { start = 2026, anti_prize = true, get_val = function(stats) return stats.penalties.caused_pens end },
   
     ["clean_sheets"] = { -- статистика до ЧТМ-2022 не велась, 8 матчей из 14 восстановлены по косвенным точечным признакам
     ["clean_sheets"]       = { start = 2006, get_val = function(stats) return stats.clean_sheets end },
        start = 2006,
     ["matches"]            = { start = 2022, no_medals = true, get_val = function(stats) return stats.matches_total or 0 end },
        get_val = function(stats) return stats.clean_sheets end,
     ["field_matches"]      = { start = 2022, no_medals = true, get_val = function(stats) return stats.matches_field or 0 end },
        adjustments = {
            totals = {
                ["total"] = 6 -- Это надо только к колонке "Всего" добавлять, потому что невозможно установить не только игроков, но даже турниры
            },
            players = {
                ["Диман"] = { [2006] = 2, [2010] = 1, [2014] = 1 },
                ["Макс"] = { [2006] = 1 },
                ["Крюча"] = { [2006] = 1 },
                ["Герыч"] = { [2010] = 1 },
                ["Диман Е."] = { [2014] = 1 },
            }
        }
    },
     ["matches"]            = { start = 2022, get_val = function(stats) return stats.matches_total or 0 end },
     ["field_matches"]      = { start = 2022, get_val = function(stats) return stats.matches_field or 0 end },
     ["plus_minus"]        = { start = 2022, anti_prize = true, get_val = function(stats) return stats.plus_minus end,
     ["plus_minus"]        = { start = 2022, anti_prize = true, get_val = function(stats) return stats.plus_minus end,
         adjustments = {
         adjustments = {
Строка 345: Строка 400:
-- 9. УМНАЯ СОРТИРОВКА (Универсальный тайбрейкер)
-- 9. УМНАЯ СОРТИРОВКА (Универсальный тайбрейкер)
-- =========================================================
-- =========================================================
-- Сначала сравнивает общие показатели. Если они равны, идет по годам  
--[[
-- с конца (от новых к старым). Поощряет "исторический приоритет":  
Сначала сравнивает общие показатели. Если они равны, идет по годам  
-- при равенстве выше ставится тот, кто достиг этого показателя раньше.
с конца (от новых к старым). Поощряет "исторический приоритет":  
при равенстве выше ставится тот, кто достиг этого показателя раньше.
--]]
function Config.tiebreaker(a, b, valid_years, is_avg, sort_asc)
function Config.tiebreaker(a, b, valid_years, is_avg, sort_asc)
     if is_avg then
     if is_avg then
Строка 410: Строка 467:
-- 10. КОНСТРУКТОР ТАБЛИЦ (Table Builder)
-- 10. КОНСТРУКТОР ТАБЛИЦ (Table Builder)
-- =========================================================
-- =========================================================
-- В подавляющем большинстве случаев используется ровно два дизайна
--[[
-- таблиц — разный для несворачиваемых и сворачиваемых.
В подавляющем большинстве случаев используется ровно два дизайна
-- Если не указано иное, то все таблицы по умолчанию
таблиц — разный для несворачиваемых и сворачиваемых.
-- должны лепиться по одному из этих двух шаблонов в зависимости от типа.
Если не указано иное, то все таблицы по умолчанию
-- Иногда могут выключаться сортировка или плавающая шапка,
должны лепиться по одному из этих двух шаблонов в зависимости от типа.
-- но это опционально и всегда оговаривается дополнительно.
Иногда могут выключаться сортировка или плавающая шапка,
но это опционально и всегда оговаривается дополнительно.
--]]
Config.builder = {
Config.builder = {
     -- 1. Создает стандартную таблицу с плавающей шапкой
     -- 1. Создает стандартную таблицу с плавающей шапкой
Строка 526: Строка 585:
-- 11. ЕЩЁ ИСКЛЮЧЕНИЯ
-- 11. ЕЩЁ ИСКЛЮЧЕНИЯ
-- =========================================================
-- =========================================================
-- Для реализации сложной логики, согласно которой Геныч  
--[[
-- не получает 4 гола в матче за 3-е место в общую статистику,
Для реализации сложной логики, согласно которой Геныч  
-- но получает в зачёт голов за Киргизию и Башмак
не получает 4 гола в матче за 3-е место в общую статистику,
но получает в зачёт голов за Киргизию и Башмак
--]]
Config.award_adjustments = {
Config.award_adjustments = {
     -- [год] = { [тип_награды] = { ... } }
     -- [год] = { [тип_награды] = { ... } }
Строка 546: Строка 607:
-- ЗАДОЛБАВШИЕ ОШИБКИ
-- ЗАДОЛБАВШИЕ ОШИБКИ
-- =========================================================
-- =========================================================
-- 1
--[[
-- Пожалуйста, грузи готовые таблицы через return frame:preprocess(Config.styles.wiki_templates .. tostring(html)),
1
-- а не через return Config.styles.wiki_templates .. tostring(html),
Пожалуйста, грузи готовые таблицы через return frame:preprocess(Config.styles.wiki_templates .. tostring(html)),
-- а то <templatestyles src="Шаблон:Плавающая_шапка_таблицы/styles.css" /> сырым кодом вылезает прямо на странице.
а не через return Config.styles.wiki_templates .. tostring(html),
-- 2
а то <templatestyles src="Шаблон:Плавающая_шапка_таблицы/styles.css" /> сырым кодом вылезает прямо на странице.
-- Обзор конкретного кейса от Gemini, но эта проблема возникала  
2
-- уже не единожды при разных обстоятельствах, необходимо всегда обращать на это внимание:
Обзор конкретного кейса от Gemini, но эта проблема возникала  
-- "Голевые передачи: В локальной функции build_complex_aggregate (которая строит простые нумерованные списки)
уже не единожды при разных обстоятельствах, необходимо всегда обращать на это внимание:
-- автор пытался сделать тернарный оператор: local target = (award_type == "assists") and g.assist or g.scorer.
"Голевые передачи: В локальной функции build_complex_aggregate (которая строит простые нумерованные списки)
-- В Lua, если g.assist оказывался nil (то есть пас никто не отдал), условие соскальзывало дальше и записывало  
автор пытался сделать тернарный оператор: local target = (award_type == "assists") and g.assist or g.scorer.
-- в ассистенты автора гола (g.scorer). Из-за этого каждый гол без ассистента плюсовался автору как голевая передача!  
В Lua, если g.assist оказывался nil (то есть пас никто не отдал), условие соскальзывало дальше и записывало  
-- Заменил на классический if-else".
в ассистенты автора гола (g.scorer). Из-за этого каждый гол без ассистента плюсовался автору как голевая передача!  
-- 3
Заменил на классический if-else".
-- Scribunto работает на Lua 5.1
3
-- Не используй хреновины, которые появились только в Lua 5.2 и далее,
Scribunto работает на Lua 5.1.5
-- в частности оператор goto и метки (::continue::)
Не используй хреновины, которые появились только в Lua 5.2 и далее,
в частности оператор goto и метки (::continue::)
4
Пожалуйста, ВСЕГДА обращай внимание на то, что общее число голов/передач и призы лучшему бомбардиру (Золотой Башмак)
и лучшему ассистенту — это РАЗНЫЕ сущности. Общее число голов/передач тупо считается по общей сумме. А призы
бомбардирам и ассистентам раздаются по количеству голов/передач за одну конкретную команду со сложным
собственным тайбрейкером в случае равенства.
5
При загрузке из Модуль:Data/GrandStats.json
Вот что написал Gemini Pro:
"Ах, чёрт возьми! Прости, это классическая и очень коварная ловушка парсера MediaWiki, о которой я на секунду забыл,
когда наводил красоту в коде. Дело в том, что когда mw.text.jsonDecode разворачивает JSON, движок PHP
(на котором работает Вики) видит ключи вроде "2006". PHP автоматически и принудительно превращает любые ключи,
состоящие только из цифр, в числовые индексы (integers). В итоге, когда таблица возвращается в Lua,
ключом становится число 2006, а не строка "2006". В своей первой успешной версии я использовал двойную проверку
tab[year] or tab[tostring(year)], и это спасло ситуацию. Но в версии с медалями я решил «сделать код чище»,
принудительно переведя всё в строки (y_str = tostring(year)), и стал искать только по строкам.
Разумеется, Lua строк не нашёл, вернул nil, и все ячейки оказались пустыми."
--]]


-- ================================
-- ================================
Строка 568: Строка 647:


-- (надоело уже постоянно объяснять, лучше сразу прописать в конфиге, чтобы мозг не плавился от непонимания)
-- (надоело уже постоянно объяснять, лучше сразу прописать в конфиге, чтобы мозг не плавился от непонимания)
--[[
Чемпионат третьего мира (сокращённо ЧТМ) — футбольный турнир, организованный группой энтузиастов в микрорайоне Тулака города Волгограда летом 2006 года.
Начавшись как шутка, соревнование очень быстро разрослось до значительных масштабов, всего в него было вовлечено в разное время не менее ста человек.
Турниры проводились на протяжении 2006—2007 гг. (четыре) и 2011—2013 гг. (также четыре), девятый ЧТМ был начат в 2014 году,
после чего турнир был заморожен на пять с половиной лет. В период 2020—2023 годов соревнование было возобновлено, но игровые дни проходили крайне редко,
поэтому девятый турнир продолжался в общей сложности почти девять лет и был закончен только в апреле 2023 года.
Вскоре стартовал десятый ЧТМ, завершившийся в мае 2024 года, а в октябре был начат одиннадцатый турнир.


-- Чемпионат третьего мира (сокращённо ЧТМ) — футбольный турнир, организованный группой энтузиастов в микрорайоне Тулака города Волгограда летом 2006 года.
Ни для кого не является секретом, что в детстве и юношестве многие любители футбола часто создают воображаемые турниры,
-- Начавшись как шутка, соревнование очень быстро разрослось до значительных масштабов, всего в него было вовлечено в разное время не менее ста человек.
представляя себя в роли лучших мастеров мира, играющих за различные реально существующие команды. Как правило,
-- Турниры проводились на протяжении 2006—2007 гг. (четыре) и 2011—2013 гг. (также четыре), девятый ЧТМ был начат в 2014 году,
соревнования эти довольно скоротечны, часто бывают незавершёнными и ограничиваются топ-уровнем (Лига Чемпионов, Чемпионат Мира).
-- после чего турнир был заморожен на пять с половиной лет. В период 2020—2023 годов соревнование было возобновлено, но игровые дни проходили крайне редко,
-- поэтому девятый турнир продолжался в общей сложности почти девять лет и был закончен только в апреле 2023 года.
-- Вскоре стартовал десятый ЧТМ, завершившийся в мае 2024 года, а в октябре был начат одиннадцатый турнир.
 
-- Ни для кого не является секретом, что в детстве и юношестве многие любители футбола часто создают воображаемые турниры,
-- представляя себя в роли лучших мастеров мира, играющих за различные реально существующие команды. Как правило,
-- соревнования эти довольно скоротечны, часто бывают незавершёнными и ограничиваются топ-уровнем (Лига Чемпионов, Чемпионат Мира).
 
-- Однажды родилась мысль — а почему бы не провести альтернативный чемпионат мира среди самых слабых команд планеты,
-- чтобы всевозможные «банановые республики» получили возможность побороться за мировую корону в матчах с себе подобными?


-- В чемпионате принимают участие команды, названные в честь реальных стран и территорий мира.
Однажды родилась мысль — а почему бы не провести альтернативный чемпионат мира среди самых слабых команд планеты,
-- По раз и навсегда установленному критерию отбора страны с хорошо или хотя бы более-менее прилично развитым уровнем футбола принимать участие в ЧТМ не могут.
чтобы всевозможные «банановые республики» получили возможность побороться за мировую корону в матчах с себе подобными?
-- В турнире не имеют права выступать сборные, хотя бы один раз игравшие в настоящем чемпионате мира, а также команды, реально претендовавшие на это
-- (например, Бахрейн или Иордания, игравшие в стыковых матчах).


-- Также не допускаются к участию сборные, добивавшиеся каких-либо значимых успехов на чемпионатах своих конфедераций
В чемпионате принимают участие команды, названные в честь реальных стран и территорий мира.
-- (например, Судан, дважды в далёкие времена бравший серебро на Кубке Африки). Но из этого правила бывают исключения.
По раз и навсегда установленному критерию отбора страны с хорошо или хотя бы более-менее прилично развитым уровнем футбола принимать участие в ЧТМ не могут.
-- О древних достижениях некоторых команд (Эфиопия, Мьянма) на момент их включения в ЧТМ организаторы соревнований в силу
В турнире не имеют права выступать сборные, хотя бы один раз игравшие в настоящем чемпионате мира, а также команды, реально претендовавшие на это
-- своих на тот момент недостаточно глубоких познаний ещё не знали, а, скажем, выход Гваделупы в полуфинал Золотого Кубка-2007
(например, Бахрейн или Иордания, игравшие в стыковых матчах).
-- и Буркина-Фасо в финал Кубка Африки-2013 произошли уже после их включения в ЧТМ. В этих случаях членство таких команд в федерации не пересматривалось.


-- Турниры проводятся по схеме, идентичной настоящему чемпионату мира. Финальному раунду предшествует отборочный.
Также не допускаются к участию сборные, добивавшиеся каких-либо значимых успехов на чемпионатах своих конфедераций
-- Иногда он полностью или частично играется на самом деле, иногда — просто расписывается. Финальный раунд всегда играется полностью от начала до конца.
(например, Судан, дважды в далёкие времена бравший серебро на Кубке Африки). Но из этого правила бывают исключения.
-- В нём участвуют 32 команды (кроме ЧТМ-2010, когда участников было 24), разделённые на 8 групп, по две команды из каждой группы выходят в плей-офф, где по системе на вылет определяют чемпиона.
О древних достижениях некоторых команд (Эфиопия, Мьянма) на момент их включения в ЧТМ организаторы соревнований в силу
своих на тот момент недостаточно глубоких познаний ещё не знали, а, скажем, выход Гваделупы в полуфинал Золотого Кубка-2007
и Буркина-Фасо в финал Кубка Африки-2013 произошли уже после их включения в ЧТМ. В этих случаях членство таких команд в федерации не пересматривалось.


-- Разумеется, набрать такое количество человек, чтобы полностью сформировать составы всех 32 команд, просто невозможно. Поэтому в ходе турнира все играют за разные сборные.
Турниры проводятся по схеме, идентичной настоящему чемпионату мира. Финальному раунду предшествует отборочный.
-- Перед каждым матчем проводится процедура определения составов. Каждый волен выбрать команду, за которую будет играть в конкретном матче.
Иногда он полностью или частично играется на самом деле, иногда — просто расписывается. Финальный раунд всегда играется полностью от начала до конца.
-- Часто случаются численные перевесы среди желающих, в этих случаях часть составов определяется искусственным образом.
В нём участвуют 32 команды (кроме ЧТМ-2010, когда участников было 24), разделённые на 8 групп, по две команды из каждой группы выходят в плей-офф, где по системе на вылет определяют чемпиона.


-- На первом ЧТМ и в начале отбора ко второму сила команд не регламентировалась, всё определялось случайным образом.
Разумеется, набрать такое количество человек, чтобы полностью сформировать составы всех 32 команд, просто невозможно. Поэтому в ходе турнира все играют за разные сборные.
-- Но довольно быстро составы команд стали формировать, учитывая силу соперников, для чего была введена система рейтинга.
Перед каждым матчем проводится процедура определения составов. Каждый волен выбрать команду, за которую будет играть в конкретном матче.
-- Чем ближе оппоненты находятся друг к другу в рейтинге, тем более равными формируются их составы. И наоборот, чем больше разница рейтинга,
Часто случаются численные перевесы среди желающих, в этих случаях часть составов определяется искусственным образом.
-- тем сильнее должна быть разница в уровне игроков каждой из команд, вплоть до численного перевеса.


-- Разумеется, очень быстро у основных игроков сложились как свои симпатии, так и антипатии. Особенным шиком считается суперчемпионство:
На первом ЧТМ и в начале отбора ко второму сила команд не регламентировалась, всё определялось случайным образом.
-- не просто стать чемпионом третьего мира, то есть выиграть финальный матч, но сделать это в составе команды,
Но довольно быстро составы команд стали формировать, учитывая силу соперников, для чего была введена система рейтинга.
-- которую ты тащил к триумфу на протяжении всего турнира (а то и многих турниров). Такое удавалось Диману (за Доминику-2014 и 2026, а также Индию-2042),
Чем ближе оппоненты находятся друг к другу в рейтинге, тем более равными формируются их составы. И наоборот, чем больше разница рейтинга,
-- Рафу (Монголия-2006 и 2018), Антону (Доминика-2014 и 2026), Генычу (Монголия-2022), Максу (Доминика-2026), Серёге (Индия-2042) и Ринату (Индия-2042).
тем сильнее должна быть разница в уровне игроков каждой из команд, вплоть до численного перевеса.


-- Остроту чемпионату придают именно соперничества между командами, симпатичными тем или иным игрокам. В основном общество расколото на два лагеря:
Разумеется, очень быстро у основных игроков сложились как свои симпатии, так и антипатии. Особенным шиком считается суперчемпионство:
-- сторонников Монголии и сторонников Доминики, но существует также множество других противостояний, если и уступающих по масштабу Битве Титанов, то не так сильно.
не просто стать чемпионом третьего мира, то есть выиграть финальный матч, но сделать это в составе команды,
которую ты тащил к триумфу на протяжении всего турнира (а то и многих турниров). Такое удавалось Диману (за Доминику-2014 и 2026, а также Индию-2042),
Рафу (Монголия-2006 и 2018), Антону (Доминика-2014 и 2026), Генычу (Монголия-2022), Максу (Доминика-2026), Серёге (Индия-2042) и Ринату (Индия-2042).


-- Очень часто бывают случаи врыва массы игроков за команду, которой они совершенно не симпатизируют, только по причине того,
Остроту чемпионату придают именно соперничества между командами, симпатичными тем или иным игрокам. В основном общество расколото на два лагеря:
-- что она противостоит сборной из враждебного лагеря. Или чтобы отомстить команде, выбившей чьего-либо любимца в предыдущем раунде.
сторонников Монголии и сторонников Доминики, но существует также множество других противостояний, если и уступающих по масштабу Битве Титанов, то не так сильно.
-- Или чтобы отомстить людям (группе людей) за вылет собственного фаворита путём выбивания из борьбы его собственного фаворита.
-- В общем, клубок противоречий постоянно запутывается до невозможности и иногда приводит к тому, что ни один из фаворитов до финала не доходит,
-- поскольку их постоянно выщёлкивают изо всех сил антифанаты. Такими были финалы 2010 и 2034, сюда же относится и чемпионство невразумительной Сент-Люсии на ЧТМ-2030.


-- Разночтения в датах объясняются очень просто. Первый ЧТМ состоялся в 2006 году и эта дата является реальной.
Очень часто бывают случаи врыва массы игроков за команду, которой они совершенно не симпатизируют, только по причине того,
-- Чтобы не быть зависимыми от времени и одновременно придать чемпионатам сходство с проведением настоящего мундиаля,
что она противостоит сборной из враждебного лагеря. Или чтобы отомстить команде, выбившей чьего-либо любимца в предыдущем раунде.
-- было решено просто прибавлять четыре воображаемых года по мере проведения каждого следующего турнира.
Или чтобы отомстить людям (группе людей) за вылет собственного фаворита путём выбивания из борьбы его собственного фаворита.
-- По тому же принципу приписывались годы и к прочим соревнованиям. Никаких неудобств это никогда не создавало,
В общем, клубок противоречий постоянно запутывается до невозможности и иногда приводит к тому, что ни один из фаворитов до финала не доходит,
-- кроме того, что приходилось постоянно объяснять, почему это мы играем турнир, скажем, 2022 года, хотя на дворе только 2011-й.
поскольку их постоянно выщёлкивают изо всех сил антифанаты. Такими были финалы 2010 и 2034, сюда же относится и чемпионство невразумительной Сент-Люсии на ЧТМ-2030.


-- ЧТМ Реальные даты
Разночтения в датах объясняются очень просто. Первый ЧТМ состоялся в 2006 году и эта дата является реальной.
-- 2006 июнь—июль 2006
Чтобы не быть зависимыми от времени и одновременно придать чемпионатам сходство с проведением настоящего мундиаля,
-- 2010 август 2006
было решено просто прибавлять четыре воображаемых года по мере проведения каждого следующего турнира.
-- 2014 сентябрь—октябрь 2006
По тому же принципу приписывались годы и к прочим соревнованиям. Никаких неудобств это никогда не создавало,
-- 2018 июнь—август 2007
кроме того, что приходилось постоянно объяснять, почему это мы играем турнир, скажем, 2022 года, хотя на дворе только 2011-й.
-- 2022 май—июнь 2011
-- 2026 август 2011
-- 2030 май—август 2012
-- 2034 апрель—июнь 2013
-- 2038 июль 2014; январь 2020—апрель 2023
-- 2042 август 2023 — май 2024
-- 2046 октябрь 2024 — ?


ЧТМ Реальные даты
2006 июнь—июль 2006
2010 август 2006
2014 сентябрь—октябрь 2006
2018 июнь—август 2007
2022 май—июнь 2011
2026 август 2011
2030 май—август 2012
2034 апрель—июнь 2013
2038 июль 2014; январь 2020—апрель 2023
2042 август 2023 — май 2024
2046 октябрь 2024 — май 2026
--]]
return Config
return Config

Текущая версия от 05:35, 6 июня 2026

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

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

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

Единый список турниров

Перечень всех годов проведения ЧТМ, которые учитываются системой. Добавление нового года в этот список автоматически включает его во все статистические расчёты и таблицы на сайте, без необходимости вносить изменения в других местах.

Единый стиль всех таблиц

Набор правил, определяющих внешний вид таблиц: цвета, рамки, выравнивание текста и т.д. Обеспечивает единообразное визуальное оформление всех статистических таблиц на сайте. Здесь же задана цветовая палитра для призовых мест (золото, серебро, бронза, дерево) и для выделения команд.

Блок фильтров (белые списки стадий)

Список официальных стадий плей-офф (например, «1/8 финала», «Финал»). Позволяет системе отличать матчи основной сетки на выбывание от других игр (например, матчей за 3-е место или стыковых матчей). Это необходимо для корректного расчёта статистики, относящейся только к плей-офф.

Блок эпох (с какого года считается статистика)

Список, который для каждого статистического показателя указывает год, с которого начался его учёт. Гарантирует, что система не будет пытаться считать те данные, которые в определённые годы ещё не собирались. Например, голы (goals) учитываются с 2006 года, а голевые передачи (assists) — только с 2026.

Словарь показателей (для генератора таблиц)

Технический справочник. Для каждого статистического показателя (голы, автоголы, карточки) здесь прописаны правила его извлечения из базы данных. Определяет, как именно система должна рассчитывать каждый показатель, является ли он негативным («антиприз») и нужно ли применять особые корректировки.

Набор инструментов

Вспомогательные технические функции для выполнения стандартных операций. Содержат базовые действия, такие как преобразование счёта из текста в числа, определение года турнира по ID матча или форматирование чисел для вывода.

Универсальный радар (сканер присутствия)

Инструмент для составления полного списка участников матча. Анализирует все данные по игре (составы, голы, карточки и т.д.) и определяет каждого игрока, который принимал в ней участие. Это гарантирует, что ни один участник не будет пропущен при сборе статистики.

Умная сортировка (универсальный тайбрейкер)

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

Конструктор таблиц

Инструменты для автоматической генерации HTML-кода таблиц. Содержит функции для создания заголовка, добавления строк и ячеек, что упрощает и стандартизирует процесс вывода статистических данных на страницы сайта.

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

-- ==========================================
-- Модуль:Config
-- ==========================================

local Config = {}

-- =========================================================
-- 1. ЕДИНЫЙ СПИСОК ТУРНИРОВ
-- =========================================================
-- Добавите сюда 2050 год — и он автоматически появится во всех таблицах на сайте.
-- Нужны два разных массива для двух разных БД.
-- Только годы финальных турниров ЧТМ (для Data/2006, Data/2010 и т. д.):
Config.years = {2006, 2010, 2014, 2018, 2022, 2026, 2030, 2034, 2038, 2042, 2046}
Config.is_latest_finished = true -- Флаг: завершён ли последний ЧТМ в списке?

-- Все годы, в которые проходил хотя бы один любой турнир (для Data/Tournaments/2006 и т. д.):
Config.all_years = {2006, 2009, 2010, 2013, 2014, 2015, 2017, 2018, 2019, 2020, 2021,
					2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032,
					2033, 2034, 2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042, 2043,
					2044, 2045, 2046, 2047, 2048}
Config.rating_year = 2046 -- до какого года подсчитывается актуальный рейтинг
Config.champions_teams = { -- быстрый список чемпионов, чтобы не шарить по всей БД каждый раз
                [2006] = "МОН", -- Монголия
                [2010] = "АФГ", -- Афганистан
                [2014] = "ДОМ", -- Доминика
                [2018] = "МОН", -- Монголия
                [2022] = "МОН", -- Монголия
                [2026] = "ДОМ", -- Доминика
                [2030] = "СЕН", -- Сент-Люсия
                [2034] = "ГВА", -- Гваделупа
                [2038] = "КИР", -- Киргизия
                [2042] = "ИНД", -- Индия
                [2046] = "КИР", -- Киргизия
                -- Каждая команда имеет свой трёхбуквенный код, прописанный в Модуль:Data/Teams
}
Config.champions_players = {-- Все игроки всегда и везде проходят под одним и тем же именем, 
							-- например, Лёша и Лёха (или Диман и Диман Е., или Герыч и Геныч и т. д.) — это два разных человека, 
							-- данное правило для унификации соблюдается абсолютно всегда
                [2006] = {"Глеб", "Диман Е.", "Раф"}, 
                [2010] = {"Антон", "Апож", "Герыч", "Диман", "Ильич", "Крюча"}, 
                [2014] = {"Антон", "Артём", "Геныч", "Диман", "Олег"}, 
                [2018] = {"Андрей", "Артур", "Диман Е.", "Ильич", "Капуста", "Лёша", "Некит Х.", "Раф"}, 
                [2022] = {"Геныч", "Ильич", "Капуста", "Лёха", "Серёга", "Хвича"}, 
                [2026] = {"Антон", "Диман", "Ильич", "Лёха", "Макс", "Стас", "Эльнур"}, 
                [2030] = {"Антон", "Геныч", "Денис", "Макс", "Эльнур"}, 
                [2034] = {"Антон", "Бирюк", "Геныч", "Диман", "Макс", "Тимон"}, 
                [2038] = {"Алишер", "Антон", "Геныч", "Дима Кес.", "Лёха", "Макс"}, 
                [2042] = {"Геныч", "Диман", "Некит", "Ринат", "Серёга"}, 
                [2046] = {"Бирюк", "Геныч", "Диман", "Коля", "Серёга"}, 
}

-- =========================================================
-- 2. РУЧНОЙ КОНТРОЛЬ АКТИВНЫХ КОМАНД (КОСТЫЛЬ)
-- =========================================================
--[[
Сюда вписываются команды, которые продолжают выступления на текущем ЧТМ,
чтобы не писать сложнейшие алгоритмы для группового раунда
--]]
Config.active_teams_list = {} 

-- Технический словарь для моментального поиска
Config.active_teams = {}
for _, t in ipairs(Config.active_teams_list) do
    Config.active_teams[t] = true
end

-- =========================================================
-- 3. ЕДИНЫЙ СТИЛЬ ВСЕХ ТАБЛИЦ (Дизайн)
-- =========================================================

Config.styles = {
    -- Базовые настройки таблиц
    classes = 'article-table sortable ts-stickytableheader',
    border = "1", cellspacing = "1", cellpadding = "1",
    
    -- Классы для сворачиваемых таблиц
    collapsible = 'mw-collapsible',
    collapsed = 'mw-collapsed',
    
    -- Системная подгрузка плавающей шапки (вместо шаблонов)
    wiki_templates = '<templatestyles src="Шаблон:Плавающая_шапка_таблицы/styles.css" />\n',

    -- Технические стили текста
    center = 'text-align:center;',
    center_nowrap = 'text-align:center; white-space:nowrap;',

    -- === ФИРМЕННАЯ ПАЛИТРА ЧТМ ===
    header  = 'background-color:#e0f0ff;', -- Голубой (стандартная шапка)
    
    -- Призовые (можно использовать для 1, 2, 3 и 4 места)
    gold    = 'background-color:gold;',
    silver	= 'background-color:silver;',
    bronze  = 'background-color:rgb(204,153,102);',
    wood	='background-color:darkkhaki;', -- Деревянная медаль ЧТМ!
    
    -- Как правило, для раскраски групп и пар-плей-офф, но бывает и для чего-то ещё
    lightgreen		= 'background-color:lightgreen;', -- проходит дальше
    palegoldenrod	= 'background-color:palegoldenrod;', -- стыковые матчи или что-то в таком духе
    lightyellow		= 'background-color:lightyellow;', -- какие-нибудь призрачные шансы вроде дополнительного турнира
    lightsalmon		= 'background-color:lightsalmon;',

    -- Дополнительные бледные оттенки для подсветок
    group		= 'background-color:silver; text-align:center;', -- Серый (разделитель групп)
    gainsboro	= 'background-color:Gainsboro;',
    peachpuff	= 'background-color:PeachPuff;',
}

-- =========================================================
-- 4. БЛОК ФИЛЬТРОВ (Белые списки стадий)
-- =========================================================
-- Используется как хэш-таблица для мгновенного поиска O(1). 
-- Если стадии здесь нет (например, "Стыковой матч" или "За 3-е место"), она вернет nil (false).
-- Стыковые матчи и матчи за третье место НЕ считаются частью плей-офф!
Config.playoff_stages = {
    ["1/8 финала"] = true,
    ["1/4 финала"] = true,
    ["Полуфинал"]  = true,
    ["Финал"]      = true
}

-- =========================================================
-- 5. БЛОК ЭПОХ (С какого года считается статистика)
-- =========================================================
Config.eras = {
    goals = 2006, mvp = 2006, mega_tricks = 2006,
    pens_scored = 2006, pens_missed = 2006, pens_saved = 2006,
    mvp_goalie = 2006, clean_sheets = 2006, own_goals = 2006,
    matches = 2022, field_matches = 2022, goalie_matches = 2022,
    matchday_prizes = 2022, matchday_places = 2022,
    plus_minus = 2022, head_goals = 2022, clearances = 2022,
    goalie_goals = 2022, gaa = 2022, avg_goals = 2022,
    assists = 2026, avg_assists = 2026, assist_mega_tricks = 2026,
    heel_goals = 2026, free_kick_goals = 2026, caused_pens = 2026,
    cards = 2038
}
--[[
Обычно все подсчёты идут именно с указанных годов.
Но есть одно важнейшее исключение:
Несмотря на эти цифры, абсолютно во всех финальных матчах
известны все составы и ассистенты. Как правило, в общую
статистику эти показатели не входят, но когда подсчитывается
что-то связанное именно с финалами или чемпионскими титулами,
то их тоже надо считать. Поэтому когда создаются алгоритмы подсчёта
для чего-то, связанного с финалами, то ни в коем случае не надо
связывать подсчёты с сыгранными матчами, иначе показатели финалов
с 2006 до 2018 не попадут в статистику.
--]]

-- =========================================================
-- 6. СЛОВАРЬ ПОКАЗАТЕЛЕЙ (Для генератора таблиц)
-- =========================================================
-- ВСЕГДА ОБРАЩАЙ ВНИМАНИЕ НА adjustments!!!
-- Это захардкоженные показатели по отдельным турнирам,
-- которых нет и не может быть в БД.
-- Они должны добавляться во всех суммарных подсчётах
-- как по отдельным турнирам, так и исторической суммы.
Config.metrics = {
    ["goals"] = { 
        start = 2006,
        get_val = function(stats) return stats.goals.total end,
        adjustments = {
            totals = { 
                [2006] = 45 
            },
            players = {
                ["Игрок"] = { [2006] = 0 }, -- пока просто шаблон, ещё не прошёлся по нужным добавлениям
            }
        }
    },

    ["own_goals"]          = { start = 2006, anti_prize = true, get_val = function(stats) return stats.own_goals end },
    ["head_goals"] = {	-- не записывались в конкретных матчах на ЧТМ-2022 и ЧТМ-2026,
    					-- а просто подсчитывались суммарно
        start = 2022,
        get_val = function(stats) return stats.goals.head end,
        adjustments = {
            players = {
                ["Диман"] = { [2022] = 3, [2026] = 4 },
                ["Антон"] = { [2026] = 1 },
                ["Макс"] = { [2026] = 1 },
            }
        }
    },
    ["heel_goals"] = {	-- не записывались в конкретных матчах на ЧТМ-2026,
    					-- а просто подсчитывались суммарно
        start = 2026,
        get_val = function(stats) return stats.goals.heel end,
        adjustments = {
            players = {
                ["Геныч"] = { [2026] = 2 },
                ["Макс"] = { [2026] = 1 },
                ["Серёга"] = { [2026] = 1 },
            }
        }
    },
    ["free_kick_goals"]    = { start = 2026, get_val = function(stats) return stats.goals.free_kick end },
    ["goalie_goals"]       = { start = 2022, get_val = function(stats) return stats.goals.goalie end },
    ["mega_tricks"]        = { start = 2006, get_val = function(stats) return stats.goals.hat_trick + stats.goals.poker + stats.goals.penta + stats.goals.hexa end },
    
    ["assists"] = { 
        start = 2026,
        get_val = function(stats) return stats.assists.total end,
        -- Если позже понадобятся adjustments для ассистов — добавляй сюда в том же формате, что и для goals
    },
    ["assist_mega_tricks"] = { start = 2026, get_val = function(stats) return stats.assists.hat_trick + stats.assists.poker + stats.assists.penta end },
    ["clearances"] = {	-- не записывались в конкретных матчах на ЧТМ-2022 и ЧТМ-2026
    					-- а просто подсчитывались суммарно
        start = 2022,
        get_val = function(stats) return stats.clearances end,
        adjustments = {
            players = {
                ["Диман"] = { [2022] = 4, [2026] = 11 },
                ["Макс"] = { [2026] = 5 },
                ["Геныч"] = { [2026] = 2 },
                ["Антон"] = { [2026] = 1 },
                ["Ринат"] = { [2026] = 1 },
                ["Диман Е."] = { [2026] = 1 },
                ["Эльнур"] = { [2026] = 1 },
            }
        }
    },
    ["yellow_cards"]       = { start = 2038, anti_prize = true, get_val = function(stats) return stats.cards.yellow end },
    ["red_cards"]          = { start = 2038, anti_prize = true, get_val = function(stats) return stats.cards.red end },
    ["mvp"]                = { start = 2006, get_val = function(stats) return stats.mvp.is_mvp end },
    ["mvp_goalie"]         = { start = 2006, get_val = function(stats) return stats.mvp.is_goalie_mvp end },
    
    -- Пенальти
    ["pens_scored"]        = { start = 2006, get_val = function(stats) return stats.penalties.in_game.g + stats.penalties.shootout.g end },
    ["pens_missed"]        = { start = 2006, anti_prize = true, get_val = function(stats) return (stats.penalties.in_game.u - stats.penalties.in_game.g) + (stats.penalties.shootout.u - stats.penalties.shootout.g) end },
    ["pens_saved"]         = { start = 2006, get_val = function(stats) return stats.penalties.saved_as_goalie end },
    ["caused_pens"]        = { start = 2026, anti_prize = true, get_val = function(stats) return stats.penalties.caused_pens end },
    ["clean_sheets"] = {	-- статистика до ЧТМ-2022 не велась, 8 матчей из 14 восстановлены по косвенным точечным признакам
        start = 2006,
        get_val = function(stats) return stats.clean_sheets end,
        adjustments = {
            totals = { 
                ["total"] = 6 -- Это надо только к колонке "Всего" добавлять, потому что невозможно установить не только игроков, но даже турниры
            },
            players = {
                ["Диман"] = { [2006] = 2, [2010] = 1, [2014] = 1 },
                ["Макс"] = { [2006] = 1 },
                ["Крюча"] = { [2006] = 1 },
                ["Герыч"] = { [2010] = 1 },
                ["Диман Е."] = { [2014] = 1 },
            }
        }
    },
    ["matches"]            = { start = 2022, get_val = function(stats) return stats.matches_total or 0 end },
    ["field_matches"]      = { start = 2022, get_val = function(stats) return stats.matches_field or 0 end },
    ["plus_minus"]         = { start = 2022, anti_prize = true, get_val = function(stats) return stats.plus_minus end,
        adjustments = {
            players = { -- тот самый матч на ЧТМ-2022
                ["Геныч"] = { [2022] = 11 },
                ["Антон"] = { [2022] = 11 },
                ["Макс"] = { [2022] = 11 },
                ["Диман"] = { [2022] = 11 },
                ["Леший"] = { [2022] = 11 },
                ["Минкин"] = { [2022] = 11 },
                ["Ринат"] = { [2022] = -11 },
                ["Йося"] = { [2022] = -11 },
                ["Илья Г."] = { [2022] = -11 },
                ["Глеб"] = { [2022] = -11 },
                ["Лёша"] = { [2022] = -11 },
                ["Гусев"] = { [2022] = -11 },
            }
        }
    },
    
    -- Средние показатели (берут данные из специально подготовленного Комбайном массива .avg, где эпохи уже отфильтрованы)
    ["avg_goals"]          = { start = 2022, is_average = true, get_val = function(stats) return { num = stats.avg and stats.avg.goals_num or 0, den = stats.avg and stats.avg.goals_den or 0 } end },
    ["avg_assists"]        = { start = 2026, is_average = true, get_val = function(stats) return { num = stats.avg and stats.avg.assists_num or 0, den = stats.avg and stats.avg.assists_den or 0 } end },
    
    -- Призы (собираются отдельно, но выводим через словарь)
    ["matchday_prizes"]    = { start = 2022, get_val = function(stats) return stats.matchday_prizes or 0 end },
    ["matchday_places"]    = { start = 2022, get_val = function(stats) return stats.matchday_places or 0 end },
}

-- =========================================================
-- 7. НАБОР ИНСТРУМЕНТОВ (Внутренние утилиты)
-- =========================================================
Config.utils = {
    -- Проверяет, есть ли имя в списке
    has_value = function(tab, val)
        if type(tab) ~= "table" then return false end
        for _, value in ipairs(tab) do
            if value == val then return true end
        end
        return false
    end,
    
    -- Превращает строку счета "2:1" в числа
    parse_score = function(score_str)
        if not score_str then return 0, 0 end
        local s1, s2 = string.match(score_str, "(%d+):(%d+)")
        if not s1 or not s2 then return 0, 0 end 
        return tonumber(s1), tonumber(s2)
    end,
    
    -- Достает год из ID матча
    get_tournament_year = function(match_id)
        local year_str = string.match(match_id, "^(%d+)")
        return tonumber(year_str) or 0
    end,
    
    -- Фильтрация матчей (например, только плей-офф)
    match_passes_filter = function(match_data, filter_type)
        if not filter_type or filter_type == "" then return true end -- Если фильтра нет, берем все матчи
        if filter_type == "playoff" then
            -- Мгновенная проверка по словарю
            return Config.playoff_stages[match_data.stage] == true
        end
        return true
    end,
    
    -- Считает среднее (защита от деления на ноль)
    calc_avg = function(v, m)
        if m == 0 then return 0 end
        return v / m
    end,
    
    -- Форматирует среднее значение (с запятой)
    format_avg = function(num, den)
        if den == 0 then return "0,00" end
        return (string.gsub(string.format("%.2f", num / den), "%.", ","))
    end,
    
    -- Форматирует обычное значение (плюс для plus_minus)
    format_val = function(metric, val)
        if metric == "plus_minus" and val > 0 then return "+" .. tostring(val) end
        return tostring(val)
    end,
    
    -- Защита от двойных точек, например, после "Диман Е.")
    smart_dot = function(str)
        if not str or str == "" then return "" end
        local text = str .. "."
        -- Если образовались ровно две точки на конце (буква + точка от инициала + наша точка), меняем на одну
        text = string.gsub(text, "([^%.])%.%.$", "%1.")
        return text
    end,
}

-- =========================================================
-- 8. УНИВЕРСАЛЬНЫЙ РАДАР (Сканер присутствия)
-- =========================================================

-- РАДАР 1: Сканирует ОДИН конкретный матч и выдаёт список всех его участников
function Config.getMatchParticipants(match_data)
    local played = {}
    
    local function mark(name)
        if name then played[name] = true end
    end

    local function scan_squad(sq)
        if not sq then return end
        if sq.starters then for _, p in ipairs(sq.starters) do mark(p) end end
        if sq.substitutes then for _, p in ipairs(sq.substitutes) do mark(p) end end
    end

    -- 1. Ищем в официальных заявках
    scan_squad(match_data.squad1)
    scan_squad(match_data.squad2)
    scan_squad(match_data.neutral_gk)
    
    -- 2. Ищем в событиях (для старых матчей без составов или неучтённых замен)
    if match_data.goals then
        for _, g in ipairs(match_data.goals) do mark(g.scorer); mark(g.assist); mark(g.own_scorer) end
    end
    if match_data.cards then for _, c in ipairs(match_data.cards) do mark(c.player) end end
    if match_data.clearances then for _, c in ipairs(match_data.clearances) do mark(c.player) end end
    if match_data.mvp and match_data.mvp.player then mark(match_data.mvp.player) end
    if match_data.missed_pens then
        for _, pen in ipairs(match_data.missed_pens) do mark(pen.taker); mark(pen.goalie) end
    end
    if match_data.shootout then
        for _, shot in ipairs(match_data.shootout) do mark(shot.taker); mark(shot.goalie) end
    end
    
    return played -- Возвращает таблицу: { ["Диман"] = true, ["Макс"] = true ... }
end

-- РАДАР 2: Проглатывает базу за ГОД (опирается на Радар 1)
-- (ЖЕЛЕЗНОЕ ПРАВИЛО "0 ИЛИ ПУСТОТА")
-- Эта функция проглатывает базу данных за один год и возвращает список всех, 
-- кто хотя бы на секунду вышел на поле или сделал любое действие.
function Config.getParticipants(year_db)
    local year_played = {}
    for _, match_data in pairs(year_db) do
        -- Вызываем Радар 1 для каждого матча
        local match_players = Config.getMatchParticipants(match_data)
        for player_name, _ in pairs(match_players) do
            year_played[player_name] = true
        end
    end
    return year_played
end

-- =========================================================
-- 9. УМНАЯ СОРТИРОВКА (Универсальный тайбрейкер)
-- =========================================================
--[[
Сначала сравнивает общие показатели. Если они равны, идет по годам 
с конца (от новых к старым). Поощряет "исторический приоритет": 
при равенстве выше ставится тот, кто достиг этого показателя раньше.
--]]
function Config.tiebreaker(a, b, valid_years, is_avg, sort_asc)
    if is_avg then
        -- 1. Сравниваем общие средние показатели
        local tA_val = a.total_den == 0 and 0 or (a.total_num / a.total_den)
        local tB_val = b.total_den == 0 and 0 or (b.total_num / b.total_den)
        
        local sA = string.format("%.2f", tA_val)
        local sB = string.format("%.2f", tB_val)
        
        if sA ~= sB then
            if sort_asc then return tonumber(sA) < tonumber(sB) end
            return tonumber(sA) > tonumber(sB)
        end
        
        -- 2. Тайбрейкер по годам (с конца)
        for i = #valid_years, 1, -1 do
            local y = valid_years[i]
            local yA_num = a.years[y] and a.years[y].num or 0
            local yA_den = a.years[y] and a.years[y].den or 0
            local yB_num = b.years[y] and b.years[y].num or 0
            local yB_den = b.years[y] and b.years[y].den or 0
            
            local yA_val = yA_den == 0 and 0 or (yA_num / yA_den)
            local yB_val = yB_den == 0 and 0 or (yB_num / yB_den)
            
            local syA = string.format("%.2f", yA_val)
            local syB = string.format("%.2f", yB_val)
            
            if syA ~= syB then
                -- ЗНАКИ ПЕРЕВЕРНУТЫ: меньше в новом году = больше в старых = выше в таблице
                if sort_asc then return tonumber(syA) > tonumber(syB) end
                return tonumber(syA) < tonumber(syB)
            end
        end
    else
        -- 1. Сравниваем абсолютные общие показатели (Grand Total)
        if a.total_val ~= b.total_val then
            if sort_asc then return a.total_val < b.total_val end
            return a.total_val > b.total_val
        end
        
        -- 2. Тайбрейкер по годам (с конца)
        for i = #valid_years, 1, -1 do
            local y = valid_years[i]
            local valA = a.years[y] or 0
            local valB = b.years[y] or 0
            
            if valA ~= valB then
                -- ЗНАКИ ПЕРЕВЕРНУТЫ: меньше в новом году = больше в старых = выше в таблице
                if sort_asc then return valA > valB end
                return valA < valB
            end
        end
    end
    
    -- 3. Алфавитный порядок как последний аргумент (если статистика идентична)
    return a.name < b.name 
end

-- =========================================================
-- 10. КОНСТРУКТОР ТАБЛИЦ (Table Builder)
-- =========================================================
--[[
В подавляющем большинстве случаев используется ровно два дизайна
таблиц — разный для несворачиваемых и сворачиваемых.
Если не указано иное, то все таблицы по умолчанию
должны лепиться по одному из этих двух шаблонов в зависимости от типа.
Иногда могут выключаться сортировка или плавающая шапка,
но это опционально и всегда оговаривается дополнительно.
--]]
Config.builder = {
    -- 1. Создает стандартную таблицу с плавающей шапкой
    start = function(columns)
        local html = mw.html.create('table')
            :addClass(Config.styles.classes)
            :attr('border', Config.styles.border)
            :attr('cellspacing', Config.styles.cellspacing)
            :attr('cellpadding', Config.styles.cellpadding)

        local headerRow = html:tag('tr')
        for _, col in ipairs(columns) do
            if type(col) == "table" then
                local th = headerRow:tag('th'):wikitext(col.text or "")
                th:cssText(col.style or Config.styles.header)
                if col.colspan then th:attr('colspan', col.colspan) end
                if col.rowspan then th:attr('rowspan', col.rowspan) end
            else
                headerRow:tag('th'):cssText(Config.styles.header):wikitext(tostring(col))
            end
        end
        return html
    end,

    -- 1.5. НОВЫЙ МЕТОД: Создает простую сворачиваемую таблицу
    -- title - заголовок, объединяющий все колонки (например, год)
    -- expanded - boolean (true = развернуто по умолчанию, false/nil = свернуто)
    start_collapsible = function(columns, title, expanded)
        local html = mw.html.create('table')
            :addClass('wikitable')
            :addClass(Config.styles.collapsible)

        -- Если не передано true, таблица по умолчанию скрыта
        if not expanded then
            html:addClass(Config.styles.collapsed)
        end

        -- Высчитываем общее число колонок для объединения заголовка
        local total_colspan = 0
        for _, col in ipairs(columns) do
            total_colspan = total_colspan + (type(col) == "table" and (col.colspan or 1) or 1)
        end

        -- Строка с общим заголовком
        if title then
            html:tag('tr')
                :tag('th')
                :attr('colspan', total_colspan)
                :cssText(Config.styles.header)
                :wikitext(tostring(title))
        end

        -- Основная шапка с названиями колонок
        local headerRow = html:tag('tr')
        for _, col in ipairs(columns) do
            if type(col) == "table" then
                local th = headerRow:tag('th'):wikitext(col.text or "")
                th:cssText(col.style or Config.styles.header)
                if col.colspan then th:attr('colspan', col.colspan) end
                if col.rowspan then th:attr('rowspan', col.rowspan) end
            else
                headerRow:tag('th'):cssText(Config.styles.header):wikitext(tostring(col))
            end
        end
        
        return html
    end,

    -- 2. Универсальная рисовалка строк
    -- Nowrap ВСЕГДА по умолчанию, он используется в 99% случаев
    row = function(html, cells, row_style)
        local tr = html:tag('tr')
        if row_style then tr:cssText(row_style) end
        
        for _, cell in ipairs(cells) do
            if type(cell) == "table" then
                -- Если ячейке нужен особый стиль
                local td = tr:tag('td'):wikitext(cell.text ~= nil and tostring(cell.text) or "")
                -- ЗДЕСЬ ИСПОЛЬЗУЕМ center_nowrap
                td:cssText(cell.style or Config.styles.center_nowrap)
                if cell.colspan then td:attr('colspan', cell.colspan) end
            else
                -- И ЗДЕСЬ ИСПОЛЬЗУЕМ center_nowrap
                tr:tag('td'):cssText(Config.styles.center_nowrap):wikitext(tostring(cell))
            end
        end
        return tr
    end,

    -- УТИЛИТА А: Автоматически генерирует шапки годов: ['06], ['10]...
    years = function(valid_years)
        local res = {}
        for _, y in ipairs(valid_years) do
            table.insert(res, "[[" .. y .. "|'" .. string.sub(tostring(y), 3, 4) .. "]]")
        end
        return res
    end,

    -- УТИЛИТА Б: Склеивает списки колонок в одну длинную шапку
    merge = function(...)
        local res = {}
        for _, arr in ipairs({...}) do
            for _, v in ipairs(arr) do table.insert(res, v) end
        end
        return res
    end
}

-- =========================================================
-- 11. ЕЩЁ ИСКЛЮЧЕНИЯ
-- =========================================================
--[[
Для реализации сложной логики, согласно которой Геныч 
не получает 4 гола в матче за 3-е место в общую статистику,
но получает в зачёт голов за Киргизию и Башмак
--]]
Config.award_adjustments = {
    -- [год] = { [тип_награды] = { ... } }
    [2022] = {
        goals = {
            -- [ключ "Игрок_Команда"] = {добавка к total, добавка к матчам, добавка к last_hist}
            ["Геныч_КИР"] = {
                total = 4,
                match_phantom = {t = 4, p = 0},
                last_hist = 29300
            }
        }
    }
}

-- =========================================================
-- ЗАДОЛБАВШИЕ ОШИБКИ
-- =========================================================
--[[
1
Пожалуйста, грузи готовые таблицы через return frame:preprocess(Config.styles.wiki_templates .. tostring(html)),
а не через return Config.styles.wiki_templates .. tostring(html),
а то <templatestyles src="Шаблон:Плавающая_шапка_таблицы/styles.css" /> сырым кодом вылезает прямо на странице.
2
Обзор конкретного кейса от Gemini, но эта проблема возникала 
уже не единожды при разных обстоятельствах, необходимо всегда обращать на это внимание:
"Голевые передачи: В локальной функции build_complex_aggregate (которая строит простые нумерованные списки)
автор пытался сделать тернарный оператор: local target = (award_type == "assists") and g.assist or g.scorer.
В Lua, если g.assist оказывался nil (то есть пас никто не отдал), условие соскальзывало дальше и записывало 
в ассистенты автора гола (g.scorer). Из-за этого каждый гол без ассистента плюсовался автору как голевая передача! 
Заменил на классический if-else".
3
Scribunto работает на Lua 5.1.5
Не используй хреновины, которые появились только в Lua 5.2 и далее,
в частности оператор goto и метки (::continue::)
4
Пожалуйста, ВСЕГДА обращай внимание на то, что общее число голов/передач и призы лучшему бомбардиру (Золотой Башмак)
и лучшему ассистенту — это РАЗНЫЕ сущности. Общее число голов/передач тупо считается по общей сумме. А призы 
бомбардирам и ассистентам раздаются по количеству голов/передач за одну конкретную команду со сложным
собственным тайбрейкером в случае равенства.
5
При загрузке из Модуль:Data/GrandStats.json
Вот что написал Gemini Pro:
"Ах, чёрт возьми! Прости, это классическая и очень коварная ловушка парсера MediaWiki, о которой я на секунду забыл, 
когда наводил красоту в коде. Дело в том, что когда mw.text.jsonDecode разворачивает JSON, движок PHP
(на котором работает Вики) видит ключи вроде "2006". PHP автоматически и принудительно превращает любые ключи, 
состоящие только из цифр, в числовые индексы (integers). В итоге, когда таблица возвращается в Lua, 
ключом становится число 2006, а не строка "2006". В своей первой успешной версии я использовал двойную проверку 
tab[year] or tab[tostring(year)], и это спасло ситуацию. Но в версии с медалями я решил «сделать код чище», 
принудительно переведя всё в строки (y_str = tostring(year)), и стал искать только по строкам. 
Разумеется, Lua строк не нашёл, вернул nil, и все ячейки оказались пустыми."
--]]

-- ================================
-- КРАТКАЯ СПРАВКА
-- ================================

-- (надоело уже постоянно объяснять, лучше сразу прописать в конфиге, чтобы мозг не плавился от непонимания)
--[[
Чемпионат третьего мира (сокращённо ЧТМ) — футбольный турнир, организованный группой энтузиастов в микрорайоне Тулака города Волгограда летом 2006 года.
Начавшись как шутка, соревнование очень быстро разрослось до значительных масштабов, всего в него было вовлечено в разное время не менее ста человек.
Турниры проводились на протяжении 2006—2007 гг. (четыре) и 2011—2013 гг. (также четыре), девятый ЧТМ был начат в 2014 году,
после чего турнир был заморожен на пять с половиной лет. В период 2020—2023 годов соревнование было возобновлено, но игровые дни проходили крайне редко,
поэтому девятый турнир продолжался в общей сложности почти девять лет и был закончен только в апреле 2023 года.
Вскоре стартовал десятый ЧТМ, завершившийся в мае 2024 года, а в октябре был начат одиннадцатый турнир.

Ни для кого не является секретом, что в детстве и юношестве многие любители футбола часто создают воображаемые турниры,
представляя себя в роли лучших мастеров мира, играющих за различные реально существующие команды. Как правило,
соревнования эти довольно скоротечны, часто бывают незавершёнными и ограничиваются топ-уровнем (Лига Чемпионов, Чемпионат Мира).

Однажды родилась мысль — а почему бы не провести альтернативный чемпионат мира среди самых слабых команд планеты,
чтобы всевозможные «банановые республики» получили возможность побороться за мировую корону в матчах с себе подобными?

В чемпионате принимают участие команды, названные в честь реальных стран и территорий мира.
По раз и навсегда установленному критерию отбора страны с хорошо или хотя бы более-менее прилично развитым уровнем футбола принимать участие в ЧТМ не могут.
В турнире не имеют права выступать сборные, хотя бы один раз игравшие в настоящем чемпионате мира, а также команды, реально претендовавшие на это
(например, Бахрейн или Иордания, игравшие в стыковых матчах).

Также не допускаются к участию сборные, добивавшиеся каких-либо значимых успехов на чемпионатах своих конфедераций
(например, Судан, дважды в далёкие времена бравший серебро на Кубке Африки). Но из этого правила бывают исключения.
О древних достижениях некоторых команд (Эфиопия, Мьянма) на момент их включения в ЧТМ организаторы соревнований в силу
своих на тот момент недостаточно глубоких познаний ещё не знали, а, скажем, выход Гваделупы в полуфинал Золотого Кубка-2007
и Буркина-Фасо в финал Кубка Африки-2013 произошли уже после их включения в ЧТМ. В этих случаях членство таких команд в федерации не пересматривалось.

Турниры проводятся по схеме, идентичной настоящему чемпионату мира. Финальному раунду предшествует отборочный.
Иногда он полностью или частично играется на самом деле, иногда — просто расписывается. Финальный раунд всегда играется полностью от начала до конца.
В нём участвуют 32 команды (кроме ЧТМ-2010, когда участников было 24), разделённые на 8 групп, по две команды из каждой группы выходят в плей-офф, где по системе на вылет определяют чемпиона.

Разумеется, набрать такое количество человек, чтобы полностью сформировать составы всех 32 команд, просто невозможно. Поэтому в ходе турнира все играют за разные сборные.
Перед каждым матчем проводится процедура определения составов. Каждый волен выбрать команду, за которую будет играть в конкретном матче.
Часто случаются численные перевесы среди желающих, в этих случаях часть составов определяется искусственным образом.

На первом ЧТМ и в начале отбора ко второму сила команд не регламентировалась, всё определялось случайным образом.
Но довольно быстро составы команд стали формировать, учитывая силу соперников, для чего была введена система рейтинга.
Чем ближе оппоненты находятся друг к другу в рейтинге, тем более равными формируются их составы. И наоборот, чем больше разница рейтинга,
тем сильнее должна быть разница в уровне игроков каждой из команд, вплоть до численного перевеса.

Разумеется, очень быстро у основных игроков сложились как свои симпатии, так и антипатии. Особенным шиком считается суперчемпионство:
не просто стать чемпионом третьего мира, то есть выиграть финальный матч, но сделать это в составе команды,
которую ты тащил к триумфу на протяжении всего турнира (а то и многих турниров). Такое удавалось Диману (за Доминику-2014 и 2026, а также Индию-2042),
Рафу (Монголия-2006 и 2018), Антону (Доминика-2014 и 2026), Генычу (Монголия-2022), Максу (Доминика-2026), Серёге (Индия-2042) и Ринату (Индия-2042).

Остроту чемпионату придают именно соперничества между командами, симпатичными тем или иным игрокам. В основном общество расколото на два лагеря:
сторонников Монголии и сторонников Доминики, но существует также множество других противостояний, если и уступающих по масштабу Битве Титанов, то не так сильно.

Очень часто бывают случаи врыва массы игроков за команду, которой они совершенно не симпатизируют, только по причине того,
что она противостоит сборной из враждебного лагеря. Или чтобы отомстить команде, выбившей чьего-либо любимца в предыдущем раунде.
Или чтобы отомстить людям (группе людей) за вылет собственного фаворита путём выбивания из борьбы его собственного фаворита.
В общем, клубок противоречий постоянно запутывается до невозможности и иногда приводит к тому, что ни один из фаворитов до финала не доходит,
поскольку их постоянно выщёлкивают изо всех сил антифанаты. Такими были финалы 2010 и 2034, сюда же относится и чемпионство невразумительной Сент-Люсии на ЧТМ-2030.

Разночтения в датах объясняются очень просто. Первый ЧТМ состоялся в 2006 году и эта дата является реальной.
Чтобы не быть зависимыми от времени и одновременно придать чемпионатам сходство с проведением настоящего мундиаля,
было решено просто прибавлять четыре воображаемых года по мере проведения каждого следующего турнира.
По тому же принципу приписывались годы и к прочим соревнованиям. Никаких неудобств это никогда не создавало,
кроме того, что приходилось постоянно объяснять, почему это мы играем турнир, скажем, 2022 года, хотя на дворе только 2011-й.

ЧТМ	Реальные даты
2006	июнь—июль 2006
2010	август 2006
2014	сентябрь—октябрь 2006
2018	июнь—август 2007
2022	май—июнь 2011
2026	август 2011
2030	май—август 2012
2034	апрель—июнь 2013
2038	июль 2014; январь 2020—апрель 2023
2042	август 2023 — май 2024
2046	октябрь 2024 — май 2026
--]]
return Config