Модуль:Турнирная сетка: различия между версиями

Материал из Наро-Фоминский Футбол
Перейти к навигации Перейти к поиску
Создан модуль
 
Добавлена возможность скрывать матч за третье место
 
(не показаны 2 промежуточные версии этого же участника)
Строка 1: Строка 1:
-- Module:Bracket
-- Вывод турнирной сетки на выбывание (single elimination) с матчем за 3-е место.
-- Поддерживает "баи" (проход без игры) и 1 или 2 матча на стадию, с датами.
local p = {}
local p = {}


-------------------------------------------------------------
-- Функция для генерации блока одной игры
-- Утилиты
local function buildMatch(args, roundPrefix, matchNum)
-------------------------------------------------------------
    local prefix = roundPrefix .. "-" .. matchNum
 
    local date = args[prefix .. "-date"] or ""
local function trim(s)
    local team1 = args[prefix .. "-team1"] or ""
if s == nil then return nil end
    local team2 = args[prefix .. "-team2"] or ""
s = mw.text.trim(tostring(s))
    local score1 = args[prefix .. "-score1"] or ""
if s == '' then return nil end
    local score2 = args[prefix .. "-score2"] or ""
return s
    local score1_2 = args[prefix .. "-score1-2"] -- Для второго матча (если 2 игры)
end
    local score2_2 = args[prefix .. "-score2-2"] -- Для второго матча (если 2 игры)
 
local function toNum(s)
if s == nil then return nil end
return tonumber(s)
end
 
local function getArg(args, ...)
for _, key in ipairs({ ... }) do
local v = args[key]
if v ~= nil and mw.text.trim(tostring(v)) ~= '' then
return v
end
end
return nil
end
 
local function yesno(s, default)
if s == nil then return default end
s = mw.text.trim(tostring(s)):lower()
if s == '' then return default end
if s == 'да' or s == 'yes' or s == 'true' or s == '1' then return true end
if s == 'нет' or s == 'no' or s == 'false' or s == '0' then return false end
return default
end
 
-------------------------------------------------------------
-- Названия раундов
-------------------------------------------------------------
 
local function buildAutoRoundNames(n)
local names = {}
for i = 1, n - 1 do
local denom = 2 ^ (n - i)
if denom == 2 then
names[i] = '1/2 финала'
else
names[i] = '1/' .. tostring(denom) .. ' финала'
end
end
names[n] = 'Финал'
return names
end
 
local function parseRoundNames(args)
local raw = getArg(args, 'раунды', 'rounds')
if raw then
local names = {}
for part in mw.text.gsplit(raw, ',') do
local t = trim(part)
if t then table.insert(names, t) end
end
if #names > 0 then return names end
end
local cnt = toNum(getArg(args, 'раундов', 'rounds_count'))
if cnt and cnt >= 1 then
return buildAutoRoundNames(math.floor(cnt))
end
return nil
end
 
-------------------------------------------------------------
-- Чтение данных одной пары/матча
-------------------------------------------------------------
 
local function readMatch(args, prefix, defaultLegs)
local m = {}
m.team1 = trim(args[prefix .. 'к1'])
m.team2 = trim(args[prefix .. 'к2'])
m.score1 = trim(args[prefix .. 'сч1'])
m.score2 = trim(args[prefix .. 'сч2'])
m.score1_2 = trim(args[prefix .. 'сч1_2'])
m.score2_2 = trim(args[prefix .. 'сч2_2'])
m.date1 = trim(args[prefix .. 'дата'])
m.date2 = trim(args[prefix .. 'дата_2'])
m.legs = toNum(args[prefix .. 'игр']) or defaultLegs
m.winnerOverride = trim(args[prefix .. 'победитель'])
m.note = trim(args[prefix .. 'примечание'])
return m
end
 
-------------------------------------------------------------
-- Определение победителя по ГОТОВЫМ (разрешённым) именам команд
-------------------------------------------------------------
 
local function simpleWinner(m)
if m.winnerOverride == '1' or m.winnerOverride == '2' then
return tonumber(m.winnerOverride)
end
if m.team1 and not m.team2 then return 1 end
if m.team2 and not m.team1 then return 2 end
if not (m.team1 and m.team2) then return nil end
 
if m.legs and m.legs >= 2 then
local a1, b1 = toNum(m.score1), toNum(m.score1_2)
local a2, b2 = toNum(m.score2), toNum(m.score2_2)
if a1 and b1 and a2 and b2 then
local agg1, agg2 = a1 + b1, a2 + b2
if agg1 > agg2 then return 1
elseif agg2 > agg1 then return 2
else return nil end
end
return nil
else
local s1, s2 = toNum(m.score1), toNum(m.score2)
if s1 and s2 then
if s1 > s2 then return 1
elseif s2 > s1 then return 2
else return nil end
end
return nil
end
end
 
-------------------------------------------------------------
-- Взаимно-рекурсивные функции: какая команда стоит в слоте
-- и кто побеждает в слоте (с учётом "проросших" из предыдущего
-- раунда имён, а не только явно указанных).
-------------------------------------------------------------
 
local resolveTeam
local matchWinner
 
resolveTeam = function(matches, r, s, i)
if r == 1 then
return matches[1][s]['team' .. i]
end
 
local prevSlot = (i == 1) and (2 * s - 1) or (2 * s)
local prevName1 = resolveTeam(matches, r - 1, prevSlot, 1)
local prevName2 = resolveTeam(matches, r - 1, prevSlot, 2)
 
if not (prevName1 or prevName2) then
-- В предыдущем раунде в этой позиции матча вообще не было —
-- значит это "бай": команда указывается явно на текущем раунде.
return matches[r][s]['team' .. i]
end
 
local w = matchWinner(matches, r - 1, prevSlot)
if w == 1 then
return prevName1
elseif w == 2 then
return prevName2
else
-- Матч ещё не сыгран/не завершён — но, возможно, тут явно
-- заранее указано имя (например, известная заявка) — как fallback.
return matches[r][s]['team' .. i]
end
end
 
matchWinner = function(matches, r, s)
local raw = matches[r][s]
local eff = {
team1 = resolveTeam(matches, r, s, 1),
team2 = resolveTeam(matches, r, s, 2),
score1 = raw.score1,
score2 = raw.score2,
score1_2 = raw.score1_2,
score2_2 = raw.score2_2,
legs = raw.legs,
winnerOverride = raw.winnerOverride,
}
return simpleWinner(eff)
end
 
local function resolveLoser(matches, n, i)
-- Раунд n-1 всегда состоит ровно из 2 слотов (полуфинал) —
-- отсюда автоматически берутся участники матча за 3-е место.
local w = matchWinner(matches, n - 1, i)
if not w then return nil end
local loserIdx = (w == 1) and 2 or 1
return resolveTeam(matches, n - 1, i, loserIdx)
end


local function displayName(matches, r, s, i)
    -- Если команды не указаны, возвращаем пустой блок-заполнитель,  
local name = resolveTeam(matches, r, s, i)
    -- чтобы сохранить правильное расстояние в сетке
if name then return name end
    if team1 == "" and team2 == "" and date == "" then
if r == 1 then
        return mw.html.create('div')
return '' -- пустой слот первого раунда - там просто нет игры
            :addClass('bracket-match-empty')
end
            :cssText('min-height: 65px; margin: 8px 0;')
return '<span class="bracket-tbd">—</span>'
    end
end


-------------------------------------------------------------
    local matchDiv = mw.html.create('div')
-- Форматирование счёта/даты
        :addClass('bracket-match')
-------------------------------------------------------------
        :cssText('border: 1px solid #aaa; border-radius: 4px; margin: 8px 0; background: #fff; overflow: hidden; font-size: 14px;')
 
local function formatDate(d)
if not d then return '' end
return ' <small class="bracket-date">(' .. d .. ')</small>'
end


local function formatScore(m, teamIndex)
    if date ~= "" then
local mainKey = 'score' .. teamIndex
        matchDiv:wikitext('<div style="background: #f0f0f0; padding: 2px 8px; font-size: 12px; color: #555; border-bottom: 1px solid #ddd;">' .. date .. '</div>')
local legKey = 'score' .. teamIndex .. '_2'
    end
local parts = {}


if m.legs and m.legs >= 2 then
    -- Внутренняя функция для создания строки команды
if m[mainKey] or m.date1 then
    local function createTeamRow(team, score, score2, isLast)
table.insert(parts, (m[mainKey] or '?') .. formatDate(m.date1))
        local style = 'display: flex; justify-content: space-between; padding: 5px 8px;'
end
        if not isLast then
if m[legKey] or m.date2 then
            style = style .. ' border-bottom: 1px solid #eee;'
table.insert(parts, (m[legKey] or '?') .. formatDate(m.date2))
        end
end
       
local a1, b1 = toNum(m.score1), toNum(m.score1_2)
        local row = mw.html.create('div'):cssText(style)
local a2, b2 = toNum(m.score2), toNum(m.score2_2)
        if team == "" then team = "&nbsp;" end
if a1 and b1 and a2 and b2 then
       
local agg = (teamIndex == 1) and (a1 + b1) or (a2 + b2)
        row:wikitext('<span style="flex-grow: 1;">' .. team .. '</span>')
table.insert(parts, '<b>' .. agg .. '</b>')
       
end
        local scoreSpan = mw.html.create('span'):cssText('font-weight: bold; margin-left: 10px; min-width: 15px; text-align: right;')
else
        scoreSpan:wikitext(score ~= "" and score or "&nbsp;")
if m[mainKey] then
        row:node(scoreSpan)
table.insert(parts, m[mainKey] .. formatDate(m.date1))
elseif m.date1 then
table.insert(parts, formatDate(m.date1))
end
end


if m.note then
        -- Если есть второй матч (доп. параметр), выводим его рядом
table.insert(parts, '<small>' .. m.note .. '</small>')
        if score2 then
end
            local score2Span = mw.html.create('span'):cssText('color: #888; font-size: 12px; margin-left: 5px; min-width: 15px; text-align: right;')
            score2Span:wikitext(score2)
            row:node(score2Span)
        end


return table.concat(parts, '<br />')
        return row
end
    end


-------------------------------------------------------------
    matchDiv:node(createTeamRow(team1, score1, score1_2, false))
-- Геометрия таблицы: сколько строк (rowspan) занимает
    matchDiv:node(createTeamRow(team2, score2, score2_2, true))
-- ячейка команды раунда r, и с какой строки начинается.
-- Классическая схема "таблица с объединением ячеек",
-- как в спортивных сетках Википедии.
-------------------------------------------------------------


local function blockInfo(row, r)
    return matchDiv
local blockSize = 2 ^ r
local half = 2 ^ (r - 1)
local idx0 = row - 1
local slot = math.floor(idx0 / blockSize) + 1
local posInBlock = idx0 % blockSize
local teamIndex, offset
if posInBlock < half then
teamIndex, offset = 1, posInBlock
else
teamIndex, offset = 2, posInBlock - half
end
local isStart = (offset == 0)
return slot, teamIndex, isStart, half
end
end


-------------------------------------------------------------
function p.bracket(frame)
-- Основная функция
    local args = frame:getParent().args
-------------------------------------------------------------
   
    -- Количество раундов (5 = от 1/16 до финала, 4 = от 1/8 и т.д.)
    local rounds = tonumber(args.rounds) or 5
   
    -- Названия раундов
    local rdNames = {
        [1] = args["RD1-name"] or "1/16 финала",
        [2] = args["RD2-name"] or "1/8 финала",
        [3] = args["RD3-name"] or "1/4 финала",
        [4] = args["RD4-name"] or "Полуфинал",
        [5] = args["RD5-name"] or "Финал"
    }
    local finalName = args["Final-name"] or "Финал"
    local thirdName = args["3rd-name"] or "Матч за 3-е место"
    local isThird = args["3rd"]


function p._main(args)
    local container = mw.html.create('div'):addClass('bracket-container')
local roundNames = parseRoundNames(args)
    container:cssText('display: flex; flex-direction: row; justify-content: center; align-items: stretch; width: 100%; font-family: sans-serif; overflow-x: auto;')
if not roundNames then
return '<strong class="error">Module:Bracket — не заданы раунды (параметр «раунды» или «раундов»).</strong>'
end
local n = #roundNames


local defaultLegs = toNum(getArg(args, 'игр_на_стадии', 'legs')) or 1
    -- ЛЕВАЯ СТОРОНА
local title = trim(getArg(args, 'название', 'title'))
    local leftSide = mw.html.create('div'):addClass('bracket-left')
local showChampion = yesno(getArg(args, 'чемпион', 'champion'), true)
    leftSide:cssText('display: flex; flex-direction: row;')


-- Строим структуру пар для всех раундов и слотов
    for r = 1, rounds - 1 do
local matches = {}
        local roundDiv = mw.html.create('div'):addClass('bracket-round')
for r = 1, n do
        roundDiv:cssText('display: flex; flex-direction: column; width: 170px; min-width: 150px; margin: 0 5px;')
matches[r] = {}
       
local slots = 2 ^ (n - r)
        local headerDiv = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 15px; border-bottom: 2px solid #333; padding-bottom: 5px;')
for s = 1, slots do
        headerDiv:wikitext(rdNames[r])
matches[r][s] = readMatch(args, 'р' .. r .. '_' .. s .. '_', defaultLegs)
        roundDiv:node(headerDiv)
end
end


-- Матч за 3-е место
        local matchesContainer = mw.html.create('div'):cssText('display: flex; flex-direction: column; justify-content: space-around; flex-grow: 1;')
local tpTeam1Given = trim(args['трм_к1'])
       
local tpTeam2Given = trim(args['трм_к2'])
        -- Количество матчей на одной стороне в данном раунде
local showThird = yesno(
        local matchesInRound = 2^(rounds - r - 1)
getArg(args, 'третье_место', 'third_place'),
        for m = 1, matchesInRound do
(tpTeam1Given ~= nil or tpTeam2Given ~= nil)
            matchesContainer:node(buildMatch(args, "RD" .. r, m))
)
        end
local thirdName = trim(getArg(args, 'третье_место_название', 'third_place_name'))
       
or 'Матч за 3-е место'
        roundDiv:node(matchesContainer)
local tp
        leftSide:node(roundDiv)
if showThird then
    end
tp = readMatch(args, 'трм_', defaultLegs)
if n >= 2 then
tp.team1 = tp.team1 or resolveLoser(matches, n, 1)
tp.team2 = tp.team2 or resolveLoser(matches, n, 2)
end
end


-- Рендер
    -- ЦЕНТР (Финал и игра за 3-е место)
local root = mw.html.create('div'):addClass('bracket-wrapper')
    local center = mw.html.create('div'):addClass('bracket-center')
if title then
    center:cssText('display: flex; flex-direction: column; justify-content: center; width: 190px; min-width: 170px; margin: 0 10px;')
root:tag('div'):addClass('bracket-title'):wikitext(title)
   
end
    -- Финал
    local finalRound = mw.html.create('div')
    local finalHeader = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 16px; border-bottom: 2px solid #333; padding-bottom: 5px;')
    finalHeader:wikitext(finalName)
    finalRound:node(finalHeader)
    finalRound:node(buildMatch(args, "Final", 1))
   
    -- Матч за 3-е место
    if isThird then
        local thirdRound = mw.html.create('div')
        local thirdHeader = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 15px; border-bottom: 2px solid #aaa; padding-bottom: 5px; margin-top: 20px;')
        thirdHeader:wikitext(thirdName)
        thirdRound:node(thirdHeader)
        thirdRound:node(buildMatch(args, "3rd", 1))
    end


local tbl = root:tag('table'):addClass('bracket-table')
    center:node(finalRound)
    center:node(thirdRound)


local headerRow = tbl:tag('tr'):addClass('bracket-header')
    -- ПРАВАЯ СТОРОНА
for r = 1, n do
    local rightSide = mw.html.create('div'):addClass('bracket-right')
headerRow:tag('th'):attr('colspan', 2):wikitext(roundNames[r])
    rightSide:cssText('display: flex; flex-direction: row-reverse;')
end
if showChampion then
headerRow:tag('th'):wikitext('Чемпион')
end


local baseRows = 2 ^ n
    for r = 1, rounds - 1 do
local finalWinner = matchWinner(matches, n, 1)
        local roundDiv = mw.html.create('div'):addClass('bracket-round')
local championName
        roundDiv:cssText('display: flex; flex-direction: column; width: 170px; min-width: 150px; margin: 0 5px;')
if finalWinner then
       
championName = resolveTeam(matches, n, 1, finalWinner)
        local headerDiv = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 15px; border-bottom: 2px solid #333; padding-bottom: 5px;')
end
        headerDiv:wikitext(rdNames[r])
        roundDiv:node(headerDiv)


for row = 1, baseRows do
        local matchesContainer = mw.html.create('div'):cssText('display: flex; flex-direction: column; justify-content: space-around; flex-grow: 1;')
local tr = tbl:tag('tr')
       
for r = 1, n do
        local matchesInRound = 2^(rounds - r - 1)
local slot, teamIndex, isStart, rowspan = blockInfo(row, r)
        for m = 1, matchesInRound do
if isStart then
            -- ИСПРАВЛЕНИЕ: Правая сторона нумеруется подряд после левой
local m = matches[r][slot]
            local matchNum = matchesInRound + m
local name = displayName(matches, r, slot, teamIndex)
            matchesContainer:node(buildMatch(args, "RD" .. r, matchNum))
local w = matchWinner(matches, r, slot)
        end
       
        roundDiv:node(matchesContainer)
        rightSide:node(roundDiv)
    end


local nameCell = tr:tag('td'):addClass('bracket-team')
    container:node(leftSide)
if w == teamIndex then
    container:node(center)
nameCell:addClass('bracket-winner')
    container:node(rightSide)
end
nameCell:wikitext(name)
if rowspan > 1 then nameCell:attr('rowspan', rowspan) end
 
local scoreCell = tr:tag('td'):addClass('bracket-score')
scoreCell:wikitext(formatScore(m, teamIndex))
if rowspan > 1 then scoreCell:attr('rowspan', rowspan) end
end
end
if showChampion and row == 1 then
tr:tag('td'):addClass('bracket-champion')
:attr('rowspan', baseRows)
:wikitext(championName or '—')
end
end
 
if showThird then
local wrap = root:tag('div'):addClass('bracket-third')
wrap:tag('div'):addClass('bracket-third-title'):wikitext(thirdName)
local ttbl = wrap:tag('table'):addClass('bracket-third-table')
local w3 = simpleWinner(tp)
for i = 1, 2 do
local trr = ttbl:tag('tr')
local nameCell = trr:tag('td'):addClass('bracket-team')
if w3 == i then nameCell:addClass('bracket-winner') end
nameCell:wikitext(tp['team' .. i] or '—')
trr:tag('td'):addClass('bracket-score'):wikitext(formatScore(tp, i))
end
end
 
return tostring(root)
end


function p.main(frame)
    return tostring(container)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
return p._main(args)
end
end


return p
return p

Текущая версия от 11:51, 18 сентября 2026

Для документации этого модуля может быть создана страница Модуль:Турнирная сетка/doc

local p = {}

-- Функция для генерации блока одной игры
local function buildMatch(args, roundPrefix, matchNum)
    local prefix = roundPrefix .. "-" .. matchNum
    local date = args[prefix .. "-date"] or ""
    local team1 = args[prefix .. "-team1"] or ""
    local team2 = args[prefix .. "-team2"] or ""
    local score1 = args[prefix .. "-score1"] or ""
    local score2 = args[prefix .. "-score2"] or ""
    local score1_2 = args[prefix .. "-score1-2"] -- Для второго матча (если 2 игры)
    local score2_2 = args[prefix .. "-score2-2"] -- Для второго матча (если 2 игры)

    -- Если команды не указаны, возвращаем пустой блок-заполнитель, 
    -- чтобы сохранить правильное расстояние в сетке
    if team1 == "" and team2 == "" and date == "" then
        return mw.html.create('div')
            :addClass('bracket-match-empty')
            :cssText('min-height: 65px; margin: 8px 0;')
    end

    local matchDiv = mw.html.create('div')
        :addClass('bracket-match')
        :cssText('border: 1px solid #aaa; border-radius: 4px; margin: 8px 0; background: #fff; overflow: hidden; font-size: 14px;')

    if date ~= "" then
        matchDiv:wikitext('<div style="background: #f0f0f0; padding: 2px 8px; font-size: 12px; color: #555; border-bottom: 1px solid #ddd;">' .. date .. '</div>')
    end

    -- Внутренняя функция для создания строки команды
    local function createTeamRow(team, score, score2, isLast)
        local style = 'display: flex; justify-content: space-between; padding: 5px 8px;'
        if not isLast then
            style = style .. ' border-bottom: 1px solid #eee;'
        end
        
        local row = mw.html.create('div'):cssText(style)
        if team == "" then team = "&nbsp;" end
        
        row:wikitext('<span style="flex-grow: 1;">' .. team .. '</span>')
        
        local scoreSpan = mw.html.create('span'):cssText('font-weight: bold; margin-left: 10px; min-width: 15px; text-align: right;')
        scoreSpan:wikitext(score ~= "" and score or "&nbsp;")
        row:node(scoreSpan)

        -- Если есть второй матч (доп. параметр), выводим его рядом
        if score2 then
            local score2Span = mw.html.create('span'):cssText('color: #888; font-size: 12px; margin-left: 5px; min-width: 15px; text-align: right;')
            score2Span:wikitext(score2)
            row:node(score2Span)
        end

        return row
    end

    matchDiv:node(createTeamRow(team1, score1, score1_2, false))
    matchDiv:node(createTeamRow(team2, score2, score2_2, true))

    return matchDiv
end

function p.bracket(frame)
    local args = frame:getParent().args
    
    -- Количество раундов (5 = от 1/16 до финала, 4 = от 1/8 и т.д.)
    local rounds = tonumber(args.rounds) or 5 
    
    -- Названия раундов
    local rdNames = {
        [1] = args["RD1-name"] or "1/16 финала",
        [2] = args["RD2-name"] or "1/8 финала",
        [3] = args["RD3-name"] or "1/4 финала",
        [4] = args["RD4-name"] or "Полуфинал",
        [5] = args["RD5-name"] or "Финал"
    }
    local finalName = args["Final-name"] or "Финал"
    local thirdName = args["3rd-name"] or "Матч за 3-е место"
    local isThird = args["3rd"]

    local container = mw.html.create('div'):addClass('bracket-container')
    container:cssText('display: flex; flex-direction: row; justify-content: center; align-items: stretch; width: 100%; font-family: sans-serif; overflow-x: auto;')

    -- ЛЕВАЯ СТОРОНА
    local leftSide = mw.html.create('div'):addClass('bracket-left')
    leftSide:cssText('display: flex; flex-direction: row;')

    for r = 1, rounds - 1 do
        local roundDiv = mw.html.create('div'):addClass('bracket-round')
        roundDiv:cssText('display: flex; flex-direction: column; width: 170px; min-width: 150px; margin: 0 5px;')
        
        local headerDiv = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 15px; border-bottom: 2px solid #333; padding-bottom: 5px;')
        headerDiv:wikitext(rdNames[r])
        roundDiv:node(headerDiv)

        local matchesContainer = mw.html.create('div'):cssText('display: flex; flex-direction: column; justify-content: space-around; flex-grow: 1;')
        
        -- Количество матчей на одной стороне в данном раунде
        local matchesInRound = 2^(rounds - r - 1)
        for m = 1, matchesInRound do
            matchesContainer:node(buildMatch(args, "RD" .. r, m))
        end
        
        roundDiv:node(matchesContainer)
        leftSide:node(roundDiv)
    end

    -- ЦЕНТР (Финал и игра за 3-е место)
    local center = mw.html.create('div'):addClass('bracket-center')
    center:cssText('display: flex; flex-direction: column; justify-content: center; width: 190px; min-width: 170px; margin: 0 10px;')
    
    -- Финал
    local finalRound = mw.html.create('div')
    local finalHeader = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 16px; border-bottom: 2px solid #333; padding-bottom: 5px;')
    finalHeader:wikitext(finalName)
    finalRound:node(finalHeader)
    finalRound:node(buildMatch(args, "Final", 1))
    
    -- Матч за 3-е место
    if isThird then
        local thirdRound = mw.html.create('div')
        local thirdHeader = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 15px; border-bottom: 2px solid #aaa; padding-bottom: 5px; margin-top: 20px;')
        thirdHeader:wikitext(thirdName)
        thirdRound:node(thirdHeader)
        thirdRound:node(buildMatch(args, "3rd", 1))
    end

    center:node(finalRound)
    center:node(thirdRound)

    -- ПРАВАЯ СТОРОНА
    local rightSide = mw.html.create('div'):addClass('bracket-right')
    rightSide:cssText('display: flex; flex-direction: row-reverse;')

    for r = 1, rounds - 1 do
        local roundDiv = mw.html.create('div'):addClass('bracket-round')
        roundDiv:cssText('display: flex; flex-direction: column; width: 170px; min-width: 150px; margin: 0 5px;')
        
        local headerDiv = mw.html.create('div'):cssText('text-align: center; font-weight: bold; margin-bottom: 15px; font-size: 15px; border-bottom: 2px solid #333; padding-bottom: 5px;')
        headerDiv:wikitext(rdNames[r])
        roundDiv:node(headerDiv)

        local matchesContainer = mw.html.create('div'):cssText('display: flex; flex-direction: column; justify-content: space-around; flex-grow: 1;')
        
        local matchesInRound = 2^(rounds - r - 1)
        for m = 1, matchesInRound do
            -- ИСПРАВЛЕНИЕ: Правая сторона нумеруется подряд после левой
            local matchNum = matchesInRound + m
            matchesContainer:node(buildMatch(args, "RD" .. r, matchNum))
        end
        
        roundDiv:node(matchesContainer)
        rightSide:node(roundDiv)
    end

    container:node(leftSide)
    container:node(center)
    container:node(rightSide)

    return tostring(container)
end

return p