Модуль:Config

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

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

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

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

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

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

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

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

Список официальных стадий плей-офф (например, «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"]         = { start = 2022, get_val = function(stats) return stats.goals.head end },
    ["heel_goals"]         = { start = 2026, get_val = function(stats) return stats.goals.heel 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 },
    ["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"]         = { start = 2022, get_val = function(stats) return stats.clearances 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 },
    ["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"]       = { start = 2006, get_val = function(stats) return stats.clean_sheets end },
    ["matches"]            = { start = 2022, no_medals = true, get_val = function(stats) return stats.matches_total or 0 end },
    ["field_matches"]      = { start = 2022, no_medals = true, 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
-- Не используй хреновины, которые появились только в Lua 5.2 и далее,
-- в частности оператор goto и метки (::continue::)

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

-- (надоело уже постоянно объяснять, лучше сразу прописать в конфиге, чтобы мозг не плавился от непонимания)

-- Чемпионат третьего мира (сокращённо ЧТМ) — футбольный турнир, организованный группой энтузиастов в микрорайоне Тулака города Волгограда летом 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 — ?

return Config