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

Материал из Наро-Фоминский Футбол
Перейти к навигации Перейти к поиску
Создан модуль
 
Переписан модуль
Строка 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 team1 == "" and team2 == "" and date == "" then
if s == '' then return default end
        return mw.html.create('div')
if s == 'да' or s == 'yes' or s == 'true' or s == '1' then return true end
            :addClass('bracket-match-empty')
if s == 'нет' or s == 'no' or s == 'false' or s == '0' then return false end
            :cssText('min-height: 65px; margin: 8px 0;')
return default
    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 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)
    if date ~= "" then
local raw = getArg(args, 'раунды', 'rounds')
        matchDiv:wikitext('<div style="background: #f0f0f0; padding: 2px 8px; font-size: 12px; color: #555; border-bottom: 1px solid #ddd;">' .. date .. '</div>')
if raw then
    end
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 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)


local function readMatch(args, prefix, defaultLegs)
        -- Если есть второй матч (доп. параметр), выводим его рядом
local m = {}
        if score2 then
m.team1 = trim(args[prefix .. 'к1'])
            local score2Span = mw.html.create('span'):cssText('color: #888; font-size: 12px; margin-left: 5px; min-width: 15px; text-align: right;')
m.team2 = trim(args[prefix .. 'к2'])
            score2Span:wikitext(score2)
m.score1 = trim(args[prefix .. 'сч1'])
            row:node(score2Span)
m.score2 = trim(args[prefix .. 'сч2'])
        end
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


-------------------------------------------------------------
        return row
-- Определение победителя по ГОТОВЫМ (разрешённым) именам команд
    end
-------------------------------------------------------------


local function simpleWinner(m)
    matchDiv:node(createTeamRow(team1, score1, score1_2, false))
if m.winnerOverride == '1' or m.winnerOverride == '2' then
    matchDiv:node(createTeamRow(team2, score2, score2_2, true))
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
    return matchDiv
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
end


-------------------------------------------------------------
function p.bracket(frame)
-- Взаимно-рекурсивные функции: какая команда стоит в слоте
    local args = frame:getParent().args
-- и кто побеждает в слоте (с учётом "проросших" из предыдущего
   
-- раунда имён, а не только явно указанных).
    -- Количество раундов (5 = от 1/16 до финала, 4 = от 1/8 и т.д.)
-------------------------------------------------------------
    local rounds = tonumber(args.rounds) or 5
 
   
local resolveTeam
    -- Названия раундов
local matchWinner
    local rdNames = {
 
        [1] = args["RD1-name"] or "1/16 финала",
resolveTeam = function(matches, r, s, i)
        [2] = args["RD2-name"] or "1/8 финала",
if r == 1 then
        [3] = args["RD3-name"] or "1/4 финала",
return matches[1][s]['team' .. i]
        [4] = args["RD4-name"] or "Полуфинал",
end
        [5] = args["RD5-name"] or "Финал"
 
    }
local prevSlot = (i == 1) and (2 * s - 1) or (2 * s)
    local finalName = args["Final-name"] or "Финал"
local prevName1 = resolveTeam(matches, r - 1, prevSlot, 1)
    local thirdName = args["3rd-name"] or "Матч за 3-е место"
local prevName2 = resolveTeam(matches, r - 1, prevSlot, 2)


if not (prevName1 or prevName2) then
    local maxMatches = 2^(rounds - 1)  
-- В предыдущем раунде в этой позиции матча вообще не было —
    local halfMatches = maxMatches / 2
-- значит это "бай": команда указывается явно на текущем раунде.
return matches[r][s]['team' .. i]
end


local w = matchWinner(matches, r - 1, prevSlot)
    local container = mw.html.create('div'):addClass('bracket-container')
if w == 1 then
    container:cssText('display: flex; flex-direction: row; justify-content: center; align-items: stretch; width: 100%; font-family: sans-serif; overflow-x: auto;')
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 leftSide = mw.html.create('div'):addClass('bracket-left')
local eff = {
    leftSide:cssText('display: flex; flex-direction: row;')
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)
    for r = 1, rounds - 1 do
-- Раунд n-1 всегда состоит ровно из 2 слотов (полуфинал) —
        local roundDiv = mw.html.create('div'):addClass('bracket-round')
-- отсюда автоматически берутся участники матча за 3-е место.
        roundDiv:cssText('display: flex; flex-direction: column; width: 170px; min-width: 150px; margin: 0 5px;')
local w = matchWinner(matches, n - 1, i)
       
if not w then return nil end
        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;')
local loserIdx = (w == 1) and 2 or 1
        headerDiv:wikitext(rdNames[r])
return resolveTeam(matches, n - 1, i, loserIdx)
        roundDiv:node(headerDiv)
end


local function displayName(matches, r, s, i)
        local matchesContainer = mw.html.create('div'):cssText('display: flex; flex-direction: column; justify-content: space-around; flex-grow: 1;')
local name = resolveTeam(matches, r, s, i)
       
if name then return name end
        local matchesInRound = 2^(rounds - r - 1)
if r == 1 then
        for m = 1, matchesInRound do
return '' -- пустой слот первого раунда - там просто нет игры
            matchesContainer:node(buildMatch(args, "RD" .. r, m))
end
        end
return '<span class="bracket-tbd">—</span>'
       
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: space-between; 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-е место
    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))


local function formatDate(d)
    center:node(finalRound)
if not d then return '' end
    center:node(thirdRound)
return ' <small class="bracket-date">(' .. d .. ')</small>'
end


local function formatScore(m, teamIndex)
    -- ПРАВАЯ СТОРОНА
local mainKey = 'score' .. teamIndex
    local rightSide = mw.html.create('div'):addClass('bracket-right')
local legKey = 'score' .. teamIndex .. '_2'
    rightSide:cssText('display: flex; flex-direction: row-reverse;')
local parts = {}


if m.legs and m.legs >= 2 then
    for r = 1, rounds - 1 do
if m[mainKey] or m.date1 then
        local roundDiv = mw.html.create('div'):addClass('bracket-round')
table.insert(parts, (m[mainKey] or '?') .. formatDate(m.date1))
        roundDiv:cssText('display: flex; flex-direction: column; width: 170px; min-width: 150px; margin: 0 5px;')
end
       
if m[legKey] or m.date2 then
        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;')
table.insert(parts, (m[legKey] or '?') .. formatDate(m.date2))
        headerDiv:wikitext(rdNames[r])
end
        roundDiv:node(headerDiv)
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 agg = (teamIndex == 1) and (a1 + b1) or (a2 + b2)
table.insert(parts, '<b>' .. agg .. '</b>')
end
else
if m[mainKey] then
table.insert(parts, m[mainKey] .. formatDate(m.date1))
elseif m.date1 then
table.insert(parts, formatDate(m.date1))
end
end


if m.note then
        local matchesContainer = mw.html.create('div'):cssText('display: flex; flex-direction: column; justify-content: space-around; flex-grow: 1;')
table.insert(parts, '<small>' .. m.note .. '</small>')
       
end
        local matchesInRound = 2^(rounds - r - 1)
        for m = 1, matchesInRound do
            local matchNum = halfMatches + m
            matchesContainer:node(buildMatch(args, "RD" .. r, matchNum))
        end
       
        roundDiv:node(matchesContainer)
        rightSide:node(roundDiv)
    end


return table.concat(parts, '<br />')
    container:node(leftSide)
end
    container:node(center)
 
    container:node(rightSide)
-------------------------------------------------------------
-- Геометрия таблицы: сколько строк (rowspan) занимает
-- ячейка команды раунда r, и с какой строки начинается.
-- Классическая схема "таблица с объединением ячеек",
-- как в спортивных сетках Википедии.
-------------------------------------------------------------
 
local function blockInfo(row, r)
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
 
-------------------------------------------------------------
-- Основная функция
-------------------------------------------------------------
 
function p._main(args)
local roundNames = parseRoundNames(args)
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 showChampion = yesno(getArg(args, 'чемпион', 'champion'), true)
 
-- Строим структуру пар для всех раундов и слотов
local matches = {}
for r = 1, n do
matches[r] = {}
local slots = 2 ^ (n - r)
for s = 1, slots do
matches[r][s] = readMatch(args, 'р' .. r .. '_' .. s .. '_', defaultLegs)
end
end
 
-- Матч за 3-е место
local tpTeam1Given = trim(args['трм_к1'])
local tpTeam2Given = trim(args['трм_к2'])
local showThird = yesno(
getArg(args, 'третье_место', 'third_place'),
(tpTeam1Given ~= nil or tpTeam2Given ~= nil)
)
local thirdName = trim(getArg(args, 'третье_место_название', 'third_place_name'))
or 'Матч за 3-е место'
local tp
if showThird then
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
 
-- Рендер
local root = mw.html.create('div'):addClass('bracket-wrapper')
if title then
root:tag('div'):addClass('bracket-title'):wikitext(title)
end
 
local tbl = root:tag('table'):addClass('bracket-table')
 
local headerRow = tbl:tag('tr'):addClass('bracket-header')
for r = 1, n do
headerRow:tag('th'):attr('colspan', 2):wikitext(roundNames[r])
end
if showChampion then
headerRow:tag('th'):wikitext('Чемпион')
end
 
local baseRows = 2 ^ n
local finalWinner = matchWinner(matches, n, 1)
local championName
if finalWinner then
championName = resolveTeam(matches, n, 1, finalWinner)
end
 
for row = 1, baseRows do
local tr = tbl:tag('tr')
for r = 1, n do
local slot, teamIndex, isStart, rowspan = blockInfo(row, r)
if isStart then
local m = matches[r][slot]
local name = displayName(matches, r, slot, teamIndex)
local w = matchWinner(matches, r, slot)
 
local nameCell = tr:tag('td'):addClass('bracket-team')
if w == teamIndex then
nameCell:addClass('bracket-winner')
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

Версия от 18:53, 17 сентября 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 maxMatches = 2^(rounds - 1) 
    local halfMatches = maxMatches / 2

    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: space-between; 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-е место
    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))

    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 = halfMatches + 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