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

Материал из Наро-Фоминский Футбол
Перейти к навигации Перейти к поиску
Нет описания правки
Добавлен вывод ЖК, и реализована возможность списывать очки
Строка 1: Строка 1:
local p = {}
local p = {}
-- =========================================================
-- НАСТРОЙКИ
-- =========================================================
-- Виды карточек в порядке вывода столбцов.
-- key — часть имени параметров: matchN_<key> и <key>_КОД.
local CARD_TYPES = {
{
key = "yellow",
title = "Жёлтые карточки",
background = "#ffd200"
},
{
key = "yellowred",
title = "Удаления за вторую жёлтую карточку",
background = "linear-gradient(135deg, #ffd200 50%, #e30613 50%)"
},
{
key = "red",
title = "Прямые красные карточки",
background = "#e30613"
}
}
-- Сколько снятий очков можно привязать к одному матчу:
-- matchN_deduction, matchN_deduction2 … matchN_deduction5
local MAX_DEDUCTIONS = 5


-- =========================================================
-- =========================================================
Строка 33: Строка 62:


return n
return n
end
-- Число для текста: 3 → «3», 1.5 → «1,5»
local function formatNumber(n)
if n == math.floor(n) then
return string.format("%d", n)
end
return (string.gsub(tostring(n), "%.", ","))
end
-- Форма слова после числа: 1 очко, 2 очка, 5 очков
local function plural(n, one, few, many)
n = math.abs(n)
if n ~= math.floor(n) then
return few
end
local lastTwo = n % 100
local last = n % 10
if lastTwo >= 11 and lastTwo <= 14 then
return many
elseif last == 1 then
return one
elseif last >= 2 and last <= 4 then
return few
end
return many
end
-- Пара значений «хозяева:гости», например «2:1»
local function parsePair(value)
local home, away =
trim(value):match("^(%d+)%s*:%s*(%d+)$")
if home and away then
return tonumber(home), tonumber(away)
end
return nil
end
-- Метка сноски: 1 → a, 2 → b … 26 → z, 27 → aa
local function noteLabel(index)
local label = ""
while index > 0 do
local rest = (index - 1) % 26
label = string.char(97 + rest) .. label
index = math.floor((index - 1) / 26)
end
return label
end
end


Строка 104: Строка 193:
goalsAgainst = 0,
goalsAgainst = 0,


points = 0
points = 0,
 
-- Сколько очков снято всего
deducted = 0,
 
-- Сноски о снятии очков
notes = {},
 
-- Карточки, собранные по матчам
matchCards = {
yellow = 0,
yellowred = 0,
red = 0
},
 
-- Итоговые карточки (из матчей или из параметров команды)
yellow = 0,
yellowred = 0,
red = 0
}
}
end
end
Строка 127: Строка 234:


return element
return element
end
-- Значок карточки для заголовка столбца.
-- Рисуется средствами CSS, поэтому не нужны файлы изображений.
local function cardIcon(cardType)
return mw.html.create("span")
:addClass("football-card")
:addClass("football-card-" .. cardType.key)
:attr("title", cardType.title)
:css("display", "inline-block")
:css("width", "0.7em")
:css("height", "0.95em")
:css("vertical-align", "middle")
:css("border-radius", "0.12em")
:css("border", "1px solid rgba(0, 0, 0, 0.35)")
:css("background", cardType.background)
:wikitext("&#8203;")
end
end


Строка 150: Строка 275:
local pointsLoss =
local pointsLoss =
number(args.points_loss, 0)
number(args.points_loss, 0)
-- -----------------------------------------------------
-- ЗАГОЛОВКИ РАЗДЕЛОВ
-- -----------------------------------------------------
-- 3 → «=== Заголовок ===» (по умолчанию),
-- 2 → «== Заголовок ==», 4 → «==== Заголовок ====»
local headingLevel =
math.floor(number(args.heading_level, 3))
headingLevel =
math.max(2, math.min(6, headingLevel))
local function heading(text)
local marks = string.rep("=", headingLevel)
-- Заголовок вики-разметки должен стоять
-- на отдельной строке
return "\n" .. marks .. " " .. text .. " " .. marks .. "\n"
end




Строка 186: Строка 332:
table.insert(teamOrder, code)
table.insert(teamOrder, code)
end
end
end
-- Префикс якорей для сносок. Он должен быть уникальным
-- на странице, поэтому по умолчанию зависит от списка команд.
local idPrefix = trim(args.id)
if idPrefix == "" then
idPrefix = "football"
if mw.hash and mw.hash.hashValue then
local ok, hash = pcall(
mw.hash.hashValue,
"crc32b",
teamsParameter
)
if ok and hash then
idPrefix = idPrefix .. "-" .. hash
end
end
else
-- Символы, которые ломают якорь или ссылку [[#…]]
idPrefix = string.gsub(idPrefix, "[%s%[%]{}|#<>\"'&]+", "_")
end
-- -----------------------------------------------------
-- УЧЁТ РЕЗУЛЬТАТА И СНЯТИЯ ОЧКОВ
-- -----------------------------------------------------
-- Добавляет результат сыгранного матча в статистику.
-- teamsMap — итоговые команды или команды «по турам».
local function addResult(teamsMap, match)
local homeTeam = teamsMap[match.home]
local awayTeam = teamsMap[match.away]
homeTeam.played = homeTeam.played + 1
awayTeam.played = awayTeam.played + 1
homeTeam.goalsFor =
homeTeam.goalsFor + match.homeGoals
homeTeam.goalsAgainst =
homeTeam.goalsAgainst + match.awayGoals
awayTeam.goalsFor =
awayTeam.goalsFor + match.awayGoals
awayTeam.goalsAgainst =
awayTeam.goalsAgainst + match.homeGoals
if match.homeGoals > match.awayGoals then
homeTeam.wins = homeTeam.wins + 1
awayTeam.losses = awayTeam.losses + 1
homeTeam.points =
homeTeam.points + pointsWin
awayTeam.points =
awayTeam.points + pointsLoss
elseif match.homeGoals < match.awayGoals then
awayTeam.wins = awayTeam.wins + 1
homeTeam.losses = homeTeam.losses + 1
awayTeam.points =
awayTeam.points + pointsWin
homeTeam.points =
homeTeam.points + pointsLoss
else
homeTeam.draws = homeTeam.draws + 1
awayTeam.draws = awayTeam.draws + 1
homeTeam.points =
homeTeam.points + pointsDraw
awayTeam.points =
awayTeam.points + pointsDraw
end
end
-- Снимает очки, привязанные к матчу
-- (независимо от того, сыгран ли он)
local function applyDeductions(teamsMap, match)
for _, deduction in ipairs(match.deductions) do
local team = teamsMap[deduction.team]
team.points =
team.points - deduction.points
team.deducted =
team.deducted + deduction.points
end
end
end


Строка 194: Строка 454:


local matches = {}
local matches = {}
-- Все снятия очков в порядке матчей
local deductions = {}
-- Указаны ли где-нибудь карточки
local hasCards = false
local matchNumber = 1
local matchNumber = 1


Строка 205: Строка 472:


local parts = split(value, ";")
local parts = split(value, ";")
 
local tour
local tour
local round
local round
Строка 220: Строка 487:
away = trim(parts[4])
away = trim(parts[4])
score = trim(parts[5])
score = trim(parts[5])
-- Формат с номером круга:
-- Формат с номером круга:
-- круг;хозяева;гости;счёт
-- круг;хозяева;гости;счёт
Строка 283: Строка 551:
homeGoals = parsed.homeGoals,
homeGoals = parsed.homeGoals,
awayGoals = parsed.awayGoals,
awayGoals = parsed.awayGoals,
score = parsed.score
score = parsed.score,
 
deductions = {}
}
}
-- -------------------------------------------------
-- КАРТОЧКИ В МАТЧЕ
-- -------------------------------------------------
-- matchN_yellow, matchN_yellowred, matchN_red
-- в формате «хозяева:гости»
for _, cardType in ipairs(CARD_TYPES) do
local paramName =
"match" .. matchNumber .. "_" .. cardType.key
local raw = trim(args[paramName])
if raw ~= "" then
local homeCards, awayCards = parsePair(raw)
if homeCards == nil then
return "'''Ошибка:''' параметр ''" ..
paramName ..
"'' имеет неправильный формат «" ..
raw ..
"». Укажите карточки хозяев и гостей " ..
"через двоеточие, например ''2:1''."
end
local homeCardsTable = teams[home].matchCards
local awayCardsTable = teams[away].matchCards
homeCardsTable[cardType.key] =
homeCardsTable[cardType.key] + homeCards
awayCardsTable[cardType.key] =
awayCardsTable[cardType.key] + awayCards
hasCards = true
end
end
-- -------------------------------------------------
-- СНЯТИЕ ОЧКОВ ЗА МАТЧ
-- -------------------------------------------------
-- matchN_deduction = код;очки;причина
for i = 1, MAX_DEDUCTIONS do
local paramName =
"match" .. matchNumber .. "_deduction"
if i > 1 then
paramName = paramName .. i
end
local raw = trim(args[paramName])
if raw ~= "" then
local deductionParts = split(raw, ";")
local code = deductionParts[1]
-- Допускается и «1,5», и «1.5»
local amount = tonumber(
(string.gsub(deductionParts[2] or "", ",", "."))
)
if code ~= home and code ~= away then
return "'''Ошибка:''' в параметре ''" ..
paramName ..
"'' должна быть указана команда матча " ..
matchNumber .. " ('''" .. home ..
"''' или '''" .. away ..
"'''), а указано «" .. code .. "»."
end
if amount == nil or
  amount == 0 or
  amount ~= amount or
  math.abs(amount) == math.huge then
return "'''Ошибка:''' в параметре ''" ..
paramName ..
"'' не указано, сколько очков снято."
end
-- Причина — всё, что после второй «;»
local reason =
table.concat(deductionParts, "; ", 3)
reason = string.gsub(reason, "[%s%.]+$", "")
local deduction = {
team = code,
points = math.abs(amount),
reason = reason,
match = match
}
table.insert(match.deductions, deduction)
table.insert(deductions, deduction)
end
end


table.insert(matches, match)
table.insert(matches, match)
Строка 294: Строка 681:


if parsed.played then
if parsed.played then
addResult(teams, match)
end


local homeTeam = teams[home]
applyDeductions(teams, match)
local awayTeam = teams[away]


homeTeam.played = homeTeam.played + 1
awayTeam.played = awayTeam.played + 1


homeTeam.goalsFor =
matchNumber = matchNumber + 1
homeTeam.goalsFor + parsed.homeGoals
end


homeTeam.goalsAgainst =
homeTeam.goalsAgainst + parsed.awayGoals


awayTeam.goalsFor =
-- =====================================================
awayTeam.goalsFor + parsed.awayGoals
-- КАРТОЧКИ КОМАНД
-- =====================================================


awayTeam.goalsAgainst =
-- Параметры yellow_КОД, yellowred_КОД, red_КОД задают
awayTeam.goalsAgainst + parsed.homeGoals
-- итог команды. Если указан хотя бы один из них, карточки
-- команды берутся только из этих параметров (неуказанные
-- считаются нулём), а карточки из матчей не учитываются.


for _, code in ipairs(teamOrder) do


if parsed.homeGoals > parsed.awayGoals then
local team = teams[code]


homeTeam.wins = homeTeam.wins + 1
local override = {}
awayTeam.losses = awayTeam.losses + 1
local hasOverride = false


homeTeam.points =
for _, cardType in ipairs(CARD_TYPES) do
homeTeam.points + pointsWin


awayTeam.points =
local paramName = cardType.key .. "_" .. code
awayTeam.points + pointsLoss
local raw = trim(args[paramName])


if raw ~= "" then


elseif parsed.homeGoals < parsed.awayGoals then
if not raw:match("^%d+$") then


awayTeam.wins = awayTeam.wins + 1
return "'''Ошибка:''' параметр ''" ..
homeTeam.losses = homeTeam.losses + 1
paramName ..
"'' должен быть целым неотрицательным " ..
"числом, а указано «" .. raw .. "»."


awayTeam.points =
end
awayTeam.points + pointsWin


homeTeam.points =
override[cardType.key] = tonumber(raw)
homeTeam.points + pointsLoss
hasOverride = true


end


else
end


homeTeam.draws = homeTeam.draws + 1
for _, cardType in ipairs(CARD_TYPES) do
awayTeam.draws = awayTeam.draws + 1
 
homeTeam.points =
homeTeam.points + pointsDraw
 
awayTeam.points =
awayTeam.points + pointsDraw


if hasOverride then
team[cardType.key] =
override[cardType.key] or 0
else
team[cardType.key] =
team.matchCards[cardType.key]
end
end


end
end


if hasOverride then
hasCards = true
end


matchNumber = matchNumber + 1
end
end


Строка 378: Строка 769:
-- Возвращает статистику команды только в матчах
-- Возвращает статистику команды только в матчах
-- против команд из указанной группы.
-- против команд из указанной группы.
-- Если задан lastTour, учитываются только матчи
-- с номером тура не больше lastTour.
-- Снятие очков на личные встречи не влияет.


local function headToHead(code, group)
local function headToHead(code, group, lastTour)


local result = {
local result = {
Строка 390: Строка 784:


for _, match in ipairs(matches) do
for _, match in ipairs(matches) do
local inRange = true
if lastTour then
local tourNumber = tonumber(match.tour)
inRange =
tourNumber ~= nil and tourNumber <= lastTour
end


if match.played and
if match.played and
  inRange and
  group[match.home] and
  group[match.home] and
  group[match.away] then
  group[match.away] then
local scored
local conceded


if match.home == code then
if match.home == code then
scored = match.homeGoals
conceded = match.awayGoals
elseif match.away == code then
scored = match.awayGoals
conceded = match.homeGoals
end


result.goalsFor =
if scored then
result.goalsFor + match.homeGoals
 
result.goalsAgainst =
result.goalsAgainst + match.awayGoals
 
if match.homeGoals > match.awayGoals then
 
result.points =
result.points + pointsWin
 
result.wins =
result.wins + 1
 
elseif match.homeGoals == match.awayGoals then
 
result.points =
result.points + pointsDraw
 
else
 
result.points =
result.points + pointsLoss
 
end
 
 
elseif match.away == code then


result.goalsFor =
result.goalsFor =
result.goalsFor + match.awayGoals
result.goalsFor + scored


result.goalsAgainst =
result.goalsAgainst =
result.goalsAgainst + match.homeGoals
result.goalsAgainst + conceded


if match.awayGoals > match.homeGoals then
if scored > conceded then


result.points =
result.points =
Строка 440: Строка 826:
result.wins + 1
result.wins + 1


elseif match.awayGoals == match.homeGoals then
elseif scored == conceded then


result.points =
result.points =
Строка 502: Строка 888:
end
end


-- =====================================================
-- Выделение победителей и проигравших
-- =====================================================
local highlights = {}
-- Есть ли текст хотя бы у одного хайлайта
local hasHighlightText = false
for i = 1, 10 do
local hl = trim(args["highlight" .. i])
if hl == nil or trim(hl) == "" then
break
end
local parts = split(hl, ";")
local highlight = {
place = tonumber(parts[1]),
count = tonumber(parts[2]),
color = parts[3] or "",
text = parts[4] or ""
}
if highlight.text ~="" then
hasHighlightText = true
end
table.insert(highlights, highlight)
end
-- -----------------------------------------------------
-- Режим отображение шахматки
-- -----------------------------------------------------
-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые
-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг
local resultsMode = 0
if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then
if args["resultsMode"] == "round" then
resultsMode = 1
end
end


-- -----------------------------------------------------
-- -----------------------------------------------------
Строка 547: Строка 893:
-- -----------------------------------------------------
-- -----------------------------------------------------


local function compareTeams(a, b)
-- Возвращает функцию сравнения для table.sort.
-- teamsMap — команды, среди которых ищутся равные по очкам
-- (для критерия «личные»); lastTour — последний
-- учитываемый тур (nil — все матчи).


for _, criterion in ipairs(criteria) do
local function makeComparator(teamsMap, lastTour)


-- ---------------------------------------------
return function(a, b)
-- ОЧКИ
-- ---------------------------------------------


if criterion == "очки" then
for _, criterion in ipairs(criteria) do
 
if a.points ~= b.points then
return a.points > b.points
end
 
 
-- ---------------------------------------------
-- РАЗНИЦА МЯЧЕЙ
-- ---------------------------------------------
 
elseif criterion == "разница" then
 
if a.goalDifference ~= b.goalDifference then
return a.goalDifference > b.goalDifference
end
 
 
-- ---------------------------------------------
-- ЗАБИТЫЕ МЯЧИ
-- ---------------------------------------------
 
elseif criterion == "забитые" then
 
if a.goalsFor ~= b.goalsFor then
return a.goalsFor > b.goalsFor
end
 
 
-- ---------------------------------------------
-- ПОБЕДЫ
-- ---------------------------------------------
 
elseif criterion == "победы" then
 
if a.wins ~= b.wins then
return a.wins > b.wins
end
 
 
-- ---------------------------------------------
-- ИГРЫ
-- ---------------------------------------------
 
elseif criterion == "игры" then
 
if a.played ~= b.played then
return a.played > b.played
end
 
 
-- ---------------------------------------------
-- ЛИЧНЫЕ
-- ---------------------------------------------
 
elseif criterion == "личные" then
 
-- Определяем всех команд с таким же
-- количеством очков, как у a и b.
 
local group = {}
 
for _, code in ipairs(teamOrder) do
 
local team = teams[code]
 
if team.points == a.points then
group[code] = true
end
 
end
 
local ha = headToHead(a.code, group)
local hb = headToHead(b.code, group)
 
if ha.points ~= hb.points then
return ha.points > hb.points
end
 
if ha.goalDifference ~= hb.goalDifference then
return ha.goalDifference > hb.goalDifference
end
 
if ha.goalsFor ~= hb.goalsFor then
return ha.goalsFor > hb.goalsFor
end
 
if ha.wins ~= hb.wins then
return ha.wins > hb.wins
end
 
 
-- ---------------------------------------------
-- НАЗВАНИЕ
-- ---------------------------------------------
 
elseif criterion == "название" then
 
local aname =
mw.text.unstrip(a.name)
 
local bname =
mw.text.unstrip(b.name)
 
if aname ~= bname then
return aname < bname
end
 
end
 
end
 
 
-- Последний резервный критерий —
-- код команды.
 
return a.code < b.code
end
 
-- =====================================================
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================
 
local function buildProgress()
 
local progress = {}
 
-- Собираем список туров
local tours = {}
 
for _, match in ipairs(matches) do
 
local tourNumber = tonumber(match.tour)
 
if tourNumber and not tours[tourNumber] then
tours[tourNumber] = true
end
 
end
 
-- Последовательно рассчитываем таблицу
-- после каждого тура
local currentTeams = {}
 
for _, code in ipairs(teamOrder) do
currentTeams[code] = newTeam(
code,
teams[code].name
)
end
 
 
-- Вспомогательная функция личных встреч
local function progressHeadToHead(code, group, lastTour)
 
local result = {
points = 0,
goalsFor = 0,
goalsAgainst = 0,
goalDifference = 0,
wins = 0
}
 
for _, match in ipairs(matches) do
 
local tourNumber = tonumber(match.tour)
 
if match.played and
  tourNumber and
  tourNumber <= lastTour and
  group[match.home] and
  group[match.away] then
 
if match.home == code then
 
result.goalsFor =
result.goalsFor + match.homeGoals
 
result.goalsAgainst =
result.goalsAgainst + match.awayGoals
 
if match.homeGoals > match.awayGoals then
 
result.points =
result.points + pointsWin
 
result.wins =
result.wins + 1
 
elseif match.homeGoals == match.awayGoals then
 
result.points =
result.points + pointsDraw
 
else
 
result.points =
result.points + pointsLoss
 
end
 
 
elseif match.away == code then
 
result.goalsFor =
result.goalsFor + match.awayGoals
 
result.goalsAgainst =
result.goalsAgainst + match.homeGoals
 
if match.awayGoals > match.homeGoals then
 
result.points =
result.points + pointsWin
 
result.wins =
result.wins + 1
 
elseif match.awayGoals == match.homeGoals then
 
result.points =
result.points + pointsDraw
 
else
 
result.points =
result.points + pointsLoss
 
end
 
end
 
end
 
end
 
result.goalDifference =
result.goalsFor - result.goalsAgainst
 
return result
end
 
 
-- Сравнение команд для конкретного тура
local function compareProgressTeams(a, b, lastTour)


for _, criterion in ipairs(criteria) do
-- -----------------------------------------
-- ОЧКИ
-- -----------------------------------------


if criterion == "очки" then
if criterion == "очки" then
Строка 809: Строка 914:
end
end


-- -----------------------------------------
-- РАЗНИЦА МЯЧЕЙ
-- -----------------------------------------


elseif criterion == "разница" then
elseif criterion == "разница" then
Строка 816: Строка 925:
end
end


-- -----------------------------------------
-- ЗАБИТЫЕ МЯЧИ
-- -----------------------------------------


elseif criterion == "забитые" then
elseif criterion == "забитые" then
Строка 823: Строка 936:
end
end


-- -----------------------------------------
-- ПОБЕДЫ
-- -----------------------------------------


elseif criterion == "победы" then
elseif criterion == "победы" then
Строка 830: Строка 947:
end
end


-- -----------------------------------------
-- ИГРЫ
-- -----------------------------------------


elseif criterion == "игры" then
elseif criterion == "игры" then
Строка 837: Строка 958:
end
end


-- -----------------------------------------
-- ЛИЧНЫЕ
-- -----------------------------------------


elseif criterion == "личные" then
elseif criterion == "личные" then
-- Все команды с таким же
-- количеством очков, как у a и b.


local group = {}
local group = {}
Строка 844: Строка 972:
for _, code in ipairs(teamOrder) do
for _, code in ipairs(teamOrder) do


local team = currentTeams[code]
if teamsMap[code].points == a.points then
 
if team.points == a.points then
group[code] = true
group[code] = true
end
end
Строка 852: Строка 978:
end
end


local ha =
local ha = headToHead(a.code, group, lastTour)
progressHeadToHead(a.code, group, lastTour)
local hb = headToHead(b.code, group, lastTour)
 
local hb =
progressHeadToHead(b.code, group, lastTour)


if ha.points ~= hb.points then
if ha.points ~= hb.points then
Строка 874: Строка 997:
end
end


-- -----------------------------------------
-- НАЗВАНИЕ
-- -----------------------------------------


elseif criterion == "название" then
elseif criterion == "название" then
Строка 890: Строка 1017:


end
end
-- Последний резервный критерий —
-- код команды.


return a.code < b.code
return a.code < b.code
end
end


end
-- =====================================================
-- Выделение победителей и проигравших
-- =====================================================
-- highlightN = место;количество;класс;текст
local highlights = {}


-- -------------------------------------------------
-- Есть ли текст хотя бы у одного выделения
-- Обрабатываем туры по порядку
local hasHighlightText = false
-- -------------------------------------------------
 
for i = 1, 10 do


local tourNumbers = {}
local hl = trim(args["highlight" .. i])


for tourNumber in pairs(tours) do
if hl == "" then
table.insert(tourNumbers, tourNumber)
break
end
end


table.sort(tourNumbers)
local parts = split(hl, ";")


local place = tonumber(parts[1])


for _, tourNumber in ipairs(tourNumbers) do
if place then


-- Добавляем матчи данного тура
local highlight = {
for _, match in ipairs(matches) do
place = math.floor(place),
count = math.max(1, math.floor(tonumber(parts[2]) or 1)),
color = parts[3] or "",
-- Текст может сам содержать «;»
text = table.concat(parts, "; ", 4)
}


if tonumber(match.tour) == tourNumber
if highlight.text ~= "" then
  and match.played then
hasHighlightText = true
end


local homeTeam =
table.insert(highlights, highlight)
currentTeams[match.home]


local awayTeam =
end
currentTeams[match.away]


homeTeam.played =
end
homeTeam.played + 1


awayTeam.played =
table.sort(highlights, function(a, b)
awayTeam.played + 1
return a.place < b.place
end)


homeTeam.goalsFor =
homeTeam.goalsFor + match.homeGoals


homeTeam.goalsAgainst =
-- -----------------------------------------------------
homeTeam.goalsAgainst + match.awayGoals
-- Режим отображения шахматки
-- -----------------------------------------------------
-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые
-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг
local resultsMode = 0
if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then
if args["resultsMode"] == "round" then
resultsMode = 1
end
end


awayTeam.goalsFor =
awayTeam.goalsFor + match.awayGoals


awayTeam.goalsAgainst =
-- =====================================================
awayTeam.goalsAgainst + match.homeGoals
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================


local function buildProgress()


if match.homeGoals > match.awayGoals then
local progress = {}


homeTeam.wins =
-- Собираем список туров
homeTeam.wins + 1
local tours = {}


awayTeam.losses =
for _, match in ipairs(matches) do
awayTeam.losses + 1


homeTeam.points =
local tourNumber = tonumber(match.tour)
homeTeam.points + pointsWin


awayTeam.points =
if tourNumber then
awayTeam.points + pointsLoss
tours[tourNumber] = true
end


end


elseif match.homeGoals < match.awayGoals then
local tourNumbers = {}


awayTeam.wins =
for tourNumber in pairs(tours) do
awayTeam.wins + 1
table.insert(tourNumbers, tourNumber)
end


homeTeam.losses =
table.sort(tourNumbers)
homeTeam.losses + 1


awayTeam.points =
awayTeam.points + pointsWin


homeTeam.points =
-- Таблица, которая последовательно
homeTeam.points + pointsLoss
-- пересчитывается после каждого тура
local currentTeams = {}


for _, code in ipairs(teamOrder) do
currentTeams[code] = newTeam(
code,
teams[code].name
)
end


else
homeTeam.draws =
homeTeam.draws + 1


awayTeam.draws =
for _, tourNumber in ipairs(tourNumbers) do
awayTeam.draws + 1


homeTeam.points =
-- Добавляем матчи данного тура
homeTeam.points + pointsDraw
for _, match in ipairs(matches) do


awayTeam.points =
if tonumber(match.tour) == tourNumber then
awayTeam.points + pointsDraw


if match.played then
addResult(currentTeams, match)
end
end
-- Снятие очков действует с этого тура
applyDeductions(currentTeams, match)


end
end
Строка 1003: Строка 1161:




-- Создаём таблицу после этого тура
-- Таблица после этого тура
local tourStandings = {}
local tourStandings = {}


Строка 1015: Строка 1173:
table.sort(
table.sort(
tourStandings,
tourStandings,
function(a, b)
makeComparator(currentTeams, tourNumber)
return compareProgressTeams(
a,
b,
tourNumber
)
end
)
)


Строка 1040: Строка 1192:


end
end


-- =====================================================
-- =====================================================
Строка 1051: Строка 1204:
end
end


table.sort(standings, compareTeams)
table.sort(standings, makeComparator(teams, nil))
 
local teamCount = #standings
 


-- =====================================================
-- =====================================================
-- Подсветка
-- Подсветка
-- =====================================================
-- =====================================================
-- Цвет строк
for _, hl in ipairs(highlights) do
for _, hl in ipairs(highlights) do
local team = standings[hl.place]
 
team.highlightCount = hl.count
local first = math.max(hl.place, 1)
team.highlightText = hl.text
local last = math.min(hl.place + hl.count - 1, teamCount)
 
local c = hl.place
for position = first, last do
for i = 1, team.highlightCount do
standings[position].highlightColor = hl.color
standings[c].highlightColor = hl.color
c = c + 1
end
end
 
-- Если у нас есть текст с пометками, то нужно объединить пустые ячейки без текста
end
if hasHighlightText and hl.place + hl.count <= #standings then
 
-- Следующая команда, с которой начинается отсутствие текста
-- Ячейки столбца «Повышение или выбывание».
local nextStanding = standings[hl.place + hl.count]
-- Каждая зона — одна объединённая ячейка с текстом,
-- Определяем количество ячеек без текста
-- места между зонами — объединённые пустые ячейки.
local nextCount = #standings - hl.place
if hasHighlightText then
 
if _ ~= #highlights then
local position = 1
nextCount = hl.place - highlights[_+1].place
local index = 1
 
while position <= teamCount do
 
-- Пропускаем зоны, которые начинаются выше
-- текущей строки (пересекаются с предыдущей)
while highlights[index] and
      highlights[index].place < position do
index = index + 1
end
end
 
if nextCount > 0 then
local hl = highlights[index]
nextStanding.highlightCount = nextCount
local span
nextStanding.highlightText = ""
local text
 
if hl and hl.place == position then
 
span = math.min(hl.count, teamCount - position + 1)
text = hl.text
index = index + 1
 
else
 
local nextPlace = teamCount + 1
 
if hl then
nextPlace = math.min(hl.place, teamCount + 1)
end
 
span = nextPlace - position
text = ""
 
end
end
standings[position].zoneCell = {
text = text,
span = span
}
position = position + span
end
end
end
end
-- =====================================================
-- ПРИМЕЧАНИЯ О СНЯТИИ ОЧКОВ
-- =====================================================
-- Текст примечания, например:
-- «С команды «Спартак» снято 3 очка в 5-м туре
-- (матч «Спартак» — «Локомотив») за неявку на матч.»
local function deductionText(deduction)
local match = deduction.match
local text =
"С команды «" .. teams[deduction.team].name ..
"» снято " .. formatNumber(deduction.points) .. " " ..
plural(deduction.points, "очко", "очка", "очков")
local tourNumber = tonumber(match.tour)
if tourNumber then
text = text .. " в " ..
formatNumber(tourNumber) .. "-м туре"
end
text = text ..
" (матч «" .. teams[match.home].name ..
"» — «" .. teams[match.away].name .. "»)"
if deduction.reason ~= "" then
text = text .. " " .. deduction.reason
end
return text .. "."
end
-- Сноски нумеруются сверху вниз по итоговой таблице
local notes = {}
for _, team in ipairs(standings) do
for _, deduction in ipairs(deductions) do
if deduction.team == team.code then
local label = noteLabel(#notes + 1)
local note = {
label = label,
id = idPrefix .. "-note-" .. label,
refId = idPrefix .. "-ref-" .. label,
text = deductionText(deduction)
}
deduction.note = note
table.insert(notes, note)
table.insert(team.notes, note)
end
end
end
-- Сноска вида [a]. Сноска в итоговой таблице получает id,
-- чтобы из примечания можно было вернуться к ней (↑).
local function noteMarker(note, withAnchor)
local marker =
mw.html.create("sup")
:addClass("reference")
:addClass("football-note-ref")
:wikitext(
"[[#" .. note.id .. "|&#91;" ..
note.label .. "&#93;]]"
)
if withAnchor then
marker:attr("id", note.refId)
end
return marker
end
-- =====================================================
-- =====================================================
-- ТАБЛИЦА ЧЕМПИОНАТА
-- ТАБЛИЦА ЧЕМПИОНАТА
-- =====================================================
-- =====================================================


local root = mw.html.create("div")
local root =
local standingsTitle =
mw.html.create("div")
mw.html.create("div")
:addClass("football-title")
:addClass("football-tables")
:wikitext("'''Турнирная таблица'''")
 
root:node(standingsTitle)




Строка 1121: Строка 1395:
"О"
"О"
}
}
 
if hasHighlightText then
for _, title in ipairs(headings) do
table.insert(headings, "Повышение или выбывание")
 
standingsHeader:node(
cell("th", title)
)
 
end
 
-- Столбцы карточек выводятся, только если
-- карточки где-нибудь указаны
if hasCards then
 
for _, cardType in ipairs(CARD_TYPES) do
 
standingsHeader:node(
mw.html.create("th")
:addClass("football-cards")
:attr("title", cardType.title)
:node(cardIcon(cardType))
)
 
end
 
end
end


for _, heading in ipairs(headings) do
if hasHighlightText then


standingsHeader:node(
standingsHeader:node(
cell("th", heading)
cell("th", "Повышение или выбывание")
)
)


Строка 1143: Строка 1438:
local row =
local row =
mw.html.create("tr")
mw.html.create("tr")
 
if team.color ~= "" then  
if team.highlightColor and team.highlightColor ~= "" then
row:addClass("highlight"):addClass(team.highlightColor)
row:addClass("highlight"):addClass(team.highlightColor)
end
end
Строка 1152: Строка 1447:
)
)


local teamName =  
 
-- Название и сноски о снятии очков
 
local teamName =
cell("td", team.name)
cell("td", team.name)
:css("text-align", "left")
:css("text-align", "left")
 
for _, note in ipairs(team.notes) do
teamName:node(
noteMarker(note, true)
)
end
 
row:node(
row:node(
teamName
teamName
Строка 1163: Строка 1468:
cell("td", team.played)
cell("td", team.played)
)
)


row:node(
row:node(
cell("td", team.wins)
cell("td", team.wins)
)
)


row:node(
row:node(
cell("td", team.draws)
cell("td", team.draws)
)
)


row:node(
row:node(
cell("td", team.losses)
cell("td", team.losses)
)
)


row:node(
row:node(
cell( "td", team.goalsFor)
cell("td", team.goalsFor)
)
)


row:node(
row:node(
cell("td",team.goalsAgainst)
cell("td", team.goalsAgainst)
)
)


local gd
local gd
Строка 1195: Строка 1497:
gd = tostring(team.goalDifference)
gd = tostring(team.goalDifference)
end
end


row:node(
row:node(
Строка 1201: Строка 1502:
)
)


local pointsCell =
cell("td", formatNumber(team.points))
if team.deducted > 0 then
pointsCell:attr(
"title",
"Снято " .. formatNumber(team.deducted) .. " " ..
plural(team.deducted, "очко", "очка", "очков")
)
end


row:node(
row:node(
cell("td", team.points)
pointsCell
)
)
 
if hasHighlightText and team.highlightText ~= nil then
 
if hasCards then
 
for _, cardType in ipairs(CARD_TYPES) do
 
row:node(
cell("td", team[cardType.key], "football-cards")
)
 
end
 
end
 
 
if hasHighlightText and team.zoneCell then
 
local zone =
cell("td", team.zoneCell.text)
 
if team.zoneCell.span > 1 then
zone:attr("rowspan", team.zoneCell.span)
end
 
row:node(
row:node(
cell("td")
zone
:wikitext(team.highlightText)
)
:attr("rowspan", team.highlightCount)
 
)
end
end


Строка 1238: Строка 1573:


root:node(info)
root:node(info)


-- =====================================================
-- =====================================================
-- РЕЗУЛЬТАТЫ
-- ПРИМЕЧАНИЯ ПОД ТАБЛИЦЕЙ
-- =====================================================
-- =====================================================


root:node(mw.html.create("br"))
if #notes > 0 then
 
root:addClass("football-tables")
local notesBlock =
mw.html.create("div")
:addClass("football-info")
:addClass("football-notes")
:wikitext("Примечания:")
 
local list =
notesBlock:tag("ol")
:css("list-style-type", "lower-alpha")
 
for _, note in ipairs(notes) do
 
local item =
list:tag("li")
:attr("id", note.id)
 
item:tag("span")
:addClass("mw-cite-backlink")
:wikitext("[[#" .. note.refId .. "|↑]]")
 
item:wikitext(" ")
 
item:tag("span")
:addClass("reference-text")
:wikitext(note.text)
 
end
 
root:node(notesBlock)
 
end


local resultsTitle =
mw.html.create("div")
:addClass("football-title")
:wikitext("'''Результаты'''")


root:node(resultsTitle)
-- =====================================================
-- РЕЗУЛЬТАТЫ
-- =====================================================
 
root:wikitext(heading("Результаты"))




Строка 1259: Строка 1625:
:addClass("wikitable")
:addClass("wikitable")
:addClass("football-results")
:addClass("football-results")
    :css("text-align", "center")
:css("text-align", "center")




Строка 1292: Строка 1658:
local homeRow =
local homeRow =
mw.html.create("tr")
mw.html.create("tr")
 
local awayRow =
local awayRow =
mw.html.create("tr")
mw.html.create("tr")
 
local teamHeader =  
local teamHeader =
cell(
cell(
"th",
"th",
Строка 1304: Строка 1670:
teamHeader:attr("rowspan", 2)
teamHeader:attr("rowspan", 2)
teamHeader:css("text-align", "right")
teamHeader:css("text-align", "right")
 
homeRow:node(teamHeader)
homeRow:node(teamHeader)


Строка 1320: Строка 1686:


local homeResultCell =
local homeResultCell =
mw.html.create("td")
mw.html.create("td")
local awayResultCell =
local awayResultCell =
mw.html.create("td")
mw.html.create("td")
Строка 1349: Строка 1715:
:addClass("ui")
:addClass("ui")
:addClass("red")
:addClass("red")
else  
else
homeResultCell
homeResultCell
:addClass("ui")
:addClass("ui")
Строка 1359: Строка 1725:
text = "—"
text = "—"
end
end
 
homeResultCell
homeResultCell
:wikitext(text)
:wikitext(text)
:done()
:done()


-- Если режим отображения по кругам, и игра была во втором круге, то меняем местами гостевую и домашн
-- Если режим отображения по кругам, и игра была во втором круге, то меняем местами гостевую и домашнюю
if resultsMode == 1 and match.round == "2" then
if resultsMode == 1 and match.round == "2" then
changeOrder = true;
changeOrder = true
end
end


Строка 1388: Строка 1754:
:addClass("ui")
:addClass("ui")
:addClass("red")
:addClass("red")
else  
else
awayResultCell
awayResultCell
:addClass("ui")
:addClass("ui")
Строка 1398: Строка 1764:
text = "—"
text = "—"
end
end
 
awayResultCell
awayResultCell
:wikitext(text)
:wikitext(text)
:done()
:done()


-- Если режим отображения по кругам, и игра была во втором круге, то меняем местами гостевую и домашн
-- Если режим отображения по кругам, и игра была в первом круге, то меняем местами гостевую и домашнюю
if resultsMode == 1 and match.round == "1" then
if resultsMode == 1 and match.round == "1" then
changeOrder = true;
changeOrder = true
end
end
end
end
Строка 1425: Строка 1791:
homeRow:node(awayResultCell)
homeRow:node(awayResultCell)
awayRow:node(homeResultCell)
awayRow:node(homeResultCell)
else  
else
homeRow:node(homeResultCell)
homeRow:node(homeResultCell)
awayRow:node(awayResultCell)
awayRow:node(awayResultCell)
Строка 1441: Строка 1807:


root:node(resultsTable)
root:node(resultsTable)
-- =====================================================
-- =====================================================
-- ПРОГРЕСС ПО ТУРАМ
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================
-- =====================================================


if trim(args.show_progress) == "yes" then
local showProgress = trim(args.show_progress) == "yes"


local progress, tourNumbers =
local progress
local tourNumbers
 
if showProgress then
 
progress, tourNumbers =
buildProgress()
buildProgress()




root:node(mw.html.create("br"))
root:wikitext(heading("Положение команд по турам"))
 
 
-- Без номеров туров строить нечего
if #tourNumbers == 0 then


root:node(
mw.html.create("div")
:addClass("football-info")
:wikitext(
"''Нет матчей с номером тура. " ..
"Для этой таблицы матчи нужно указывать " ..
"в формате «тур;круг;хозяева;гости;счёт».''"
)
)
showProgress = false
end


local progressTitle =
end
mw.html.create("div")
:addClass("football-title")
:wikitext("'''Положение команд по турам'''")


root:node(progressTitle)


if showProgress then


local progressTable =
local progressTable =
Строка 1497: Строка 1884:


end
end
end
end
-- -------------------------------------------------
-- Сноски о снятии очков: тур → команда → сноски
-- -------------------------------------------------
local progressNotes = {}
for _, deduction in ipairs(deductions) do
local tourNumber = tonumber(deduction.match.tour)
if tourNumber and deduction.note then
progressNotes[tourNumber] =
progressNotes[tourNumber] or {}
local teamNotes =
progressNotes[tourNumber][deduction.team] or {}
table.insert(teamNotes, deduction.note)
progressNotes[tourNumber][deduction.team] =
teamNotes


end
end
Строка 1521: Строка 1936:


progressTable:node(progressHeader)
progressTable:node(progressHeader)
 
local tourHeader =
local tourHeader =
mw.html.create("tr")
mw.html.create("tr")
Строка 1535: Строка 1950:


end
end
 
progressTable:node(tourHeader)
progressTable:node(tourHeader)


-- -------------------------------------------------
-- -------------------------------------------------
Строка 1573: Строка 1989:
:addClass("highlight")
:addClass("highlight")
:addClass(color)
:addClass(color)
end
-- В туре, где сняты очки, — та же сноска,
-- что и в итоговой таблице
local cellNotes =
progressNotes[tourNumber] and
progressNotes[tourNumber][code]
if cellNotes then
resultCell:addClass("football-deduction")
for _, note in ipairs(cellNotes) do
resultCell:node(
noteMarker(note, false)
)
end


end
end

Версия от 12:58, 24 сентября 2026

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

Турнир, в котором после первого этапа таблица делится на группы, оформляется несколькими таблицами: таблицы групп начинаются с итогов первого этапа — см. «Турнир из нескольких этапов».

Использование

Модуль вызывается напрямую из статьи:

{{#invoke:Football|main| … }}

Модуль читает только параметры самого вызова #invoke. Если нужен шаблон-обёртка, он должен явно передавать в модуль каждый параметр.

Модуль выводит по порядку:

  1. турнирную таблицу — без собственного заголовка (заголовок раздела ставится в статье);
  2. строку «Критерии определения мест» и, если с какой-то команды снимались очки, блок «Примечания»;
  3. раздел «Результаты» с шахматкой;
  4. раздел «Положение команд по турам» — только при show_progress = yes.

Заголовки разделов — обычные вики-заголовки (по умолчанию === … ===), поэтому они попадают в оглавление статьи. Уровень заголовков меняется параметром heading_level.

Быстрый старт

{{#invoke:Турнирная таблица|main
| teams    = СПА;ЦСК;ЗЕН
| name_СПА = Спартак
| name_ЦСК = ЦСКА
| name_ЗЕН = Зенит
| match1   = 1;1;СПА;ЦСК;2:1
| match2   = 2;1;ЦСК;ЗЕН;0:0
| match3   = 3;1;ЗЕН;СПА;1:3
| match4   = 4;2;ЦСК;СПА;
| match5   = 5;2;ЗЕН;ЦСК;
| match6   = 6;2;СПА;ЗЕН;
}}

Несыгранные матчи можно внести заранее с пустым счётом: в шахматке они видны как «—» и начинают учитываться, как только вписан счёт.

Для однокругового турнира добавьте rounds = 1 — см. «Шахматка».

Команды

Параметр Описание
teams Обязательный. Коды команд через «;», например СПА;ЦСК;ЗЕН. Порядок кодов — это порядок строк и столбцов шахматки и строк таблицы по турам; итоговая таблица сортируется сама. Коды должны быть уникальными. Удобнее всего короткие коды из букв и цифр без пробелов: они входят в имена других параметров.
name_КОД Название команды, например name_СПА = Спартак. Можно использовать вики-разметку: name_СПА = [[ФК «Спартак»|Спартак]]. Если название не указано, выводится код.

Матчи

Каждый матч — отдельный параметр: match1, match2 и так далее. Нумерация без пропусков: модуль читает матчи по порядку и останавливается на первом отсутствующем или пустом номере, поэтому матчи после пропуска не учитываются.

Части значения разделяются «;». Допускаются три формата:

Формат Пример Примечание
тур;круг;хозяева;гости;счёт 5;2;СПА;ЛОК;2:2 Полный формат. Номер тура нужен для таблицы положения по турам, номер круга — для режима шахматки resultsMode = round.
круг;хозяева;гости;счёт 2;СПА;ЛОК;2:2 Матч не попадает в таблицу положения по турам.
хозяева;гости;счёт СПА;ЛОК;2:2 Матч не попадает в таблицу положения по турам.

Хозяева и гости указываются кодами из teams. Тур — число, круг — 1 или 2.

Возможные значения счёта:

Значение Что означает Что видно в шахматке
2:1 (можно 2 : 1) Матч сыгран: голы хозяев, двоеточие, голы гостей Счёт и цвет результата
пусто, - или — Матч ещё не сыгран —
перенесён или перенесен Матч перенесён перенесён
отменён или отменен Матч отменён отменён
что-либо другое Счёт не распознан, матч не учитывается ошибка: …

Техническое поражение записывается обычным счётом, например 0:3. Если за матч с команды ещё и сняли очки, это указывается отдельно — см. раздел «Снятие очков».

Очки за результат

Параметр По умолчанию Описание
points_win 3 Очки за победу
points_draw 1 Очки за ничью
points_loss 0 Очки за поражение

Критерии определения мест

Параметры tie1 … tie10 задают критерии в порядке применения: если по первому критерию команды равны, сравнивается второй, и так далее. Если не указан ни один, используется порядок очки → разница → забитые → победы → название. Если команды равны по всем критериям, выше та, чей код раньше по алфавиту.

Значение Выше команда, у которой…
очки больше очков (с учётом снятых и стартовых)
разница лучше разница забитых и пропущенных мячей
забитые больше забитых мячей
победы больше побед
игры больше сыгранных матчей
личные лучше результаты в матчах между всеми командами, набравшими столько же очков: сравниваются очки в этих матчах, затем разница мячей в них, забитые в них и победы в них. Учитываются матчи этой таблицы и матчи h2h_matchN (см. «Личные встречи с учётом первого этапа»); снятые и стартовые очки здесь не учитываются. Критерий имеет смысл только после очки.
название название раньше по алфавиту (сравнивается текст параметра name_КОД как есть, вместе с разметкой)

Стартовые показатели команд (start_КОД) входят во все критерии, кроме личные.

Под таблицей выводится строка «Критерии определения мест: …» с выбранным порядком. Нераспознанные значения модуль пропускает, но показывает в этой строке, так что по ней удобно проверять написание.

Пример: очки, затем личные встречи, затем общая разница мячей.

| tie1 = очки
| tie2 = личные
| tie3 = разница

Цветовые зоны

Параметры highlight1 … highlight10 (без пропусков в нумерации) выделяют группы мест:

highlightN = место;количество;класс;текст

Часть Описание
место Первое место зоны — в нумерации таблицы, то есть с учётом first_place (см. «Нумерация мест»)
количество Сколько мест подряд входит в зону (по умолчанию 1)
класс CSS-класс. Строки зоны получают классы highlight и указанный, например class="highlight green". Сами цвета задаются стилями вики (см. «Оформление»).
текст Подпись зоны (необязательно). Может содержать ссылки и «;».

Если хотя бы у одной зоны есть текст, в таблице появляется столбец «Повышение или выбывание»: подпись выводится одной объединённой ячейкой на всю зону, места между зонами — пустыми объединёнными ячейками. Зоны не должны пересекаться. Зона, которая начинается выше первого места таблицы, обрезается, а зоны целиком за пределами таблицы не выводятся.

| highlight1 = 1;1;green;Выход в Лигу чемпионов
| highlight2 = 2;2;blue;Выход в Лигу конференций
| highlight3 = 15;2;red;Выбывание в Первую лигу

Шахматка

Раздел «Результаты». Строки и столбцы — команды в порядке teams, в ячейке — матч команды строки с командой столбца.

Параметр Значения Описание
rounds 2 (по умолчанию) или 1 Число кругов: 1 — однокруговой турнир
resultsMode round Только для двухкругового турнира: строки по кругам, а не «дома — в гостях»

Двухкруговой турнир (по умолчанию): каждая пара играет по разу на поле каждой из команд, поэтому у команды две строки. В первой — её домашние матчи, во второй — выездные. При resultsMode = round первая строка показывает матчи первого круга, вторая — второго (по номеру круга в matchN).

Однокруговой турнир (rounds = 1): каждая пара играет один раз, поэтому у команды одна строка, а в ячейке — единственный матч пары, на чьём бы поле он ни был. Параметр resultsMode при этом не действует.

Счёт всегда записан как «хозяева:гости», а цвет ячейки показывает результат для команды строки: класс ui green — победа, ui yellow — ничья, ui red — поражение. Например, в однокруговой шахматке матч «Спартак» — ЦСКА 0:2 и в строке «Спартака», и в строке ЦСКА записан как «0:2», но у «Спартака» ячейка красная, а у ЦСКА — зелёная.

Если у пары больше матчей, чем ячеек (например, отменённый матч и переигровка), в ячейке показывается первый сыгранный из них, а если сыгранных нет — первый с пометкой «перенесён», «отменён» или «ошибка: …».

Положение команд по турам

Параметр show_progress = yes добавляет раздел «Положение команд по турам»: для каждого тура — место команды после всех матчей этого и предыдущих туров. Учитываются только матчи с номером тура (полный формат matchN); туры идут по возрастанию, строки — в порядке teams. Места определяются по тем же критериям, что и в итоговой таблице (личные встречи — только по матчам до этого тура включительно). Если ни у одного матча нет номера тура, вместо таблицы выводится пояснение.

Ячейки с определёнными местами можно выделить параметрами progress_highlight1 … progress_highlight10:

progress_highlightN = место;класс — ячейка получает классы highlight и указанный.

| show_progress       = yes
| progress_highlight1 = 1;green
| progress_highlight2 = 16;red

Снятие очков учитывается начиная с тура матча, к которому оно привязано, а ячейка этого тура получает ту же сноску, что и команда в итоговой таблице.

Стартовые показатели (start_КОД) действуют с первого тура таблицы, а матчи h2h_matchN учитываются в критерии личные как сыгранные до первого тура. Места выводятся в нумерации таблицы (с учётом first_place), и в progress_highlightN места указываются в этой же нумерации.

Карточки

Карточки необязательны. Если они не указаны ни в одном матче, ни у одной команды и ни в одних стартовых показателях, столбцов карточек в таблице нет. Если они указаны хотя бы где-нибудь, после столбца «О» появляются три столбца — у всех команд.

Карточки учитываются раздельно, как в подробной футбольной статистике, чтобы вторая жёлтая карточка не засчитывалась дважды:

Столбец Вид Что считается
​ yellow Жёлтые карточки — все предупреждения, кроме второй жёлтой, после которой игрок удалён
​ yellowred Удаления за вторую жёлтую карточку — по одному на каждое такое удаление
​ red Прямые красные карточки

Как записывать разные случаи (один игрок в одном матче):

Что произошло yellow yellowred red
Одна жёлтая карточка 1 0 0
Две жёлтые карточки и удаление 1 1 0
Прямая красная карточка 0 0 1
Жёлтая, затем прямая красная 1 0 1

Общее число удалений команды равно сумме столбцов yellowred и red.

Карточки выводятся для справки и на места в таблице не влияют.

Карточки по матчам

matchN_yellow, matchN_yellowred, matchN_red — карточки в матче N в формате хозяева:гости:

| match1           = 1;1;СПА;ЦСК;2:1
| match1_yellow    = 2:3
| match1_yellowred = 0:1

Здесь у хозяев две жёлтые карточки, у гостей — три жёлтые и одно удаление за вторую жёлтую. Неуказанный вид карточек считается как 0:0. Карточки суммируются по всем матчам, где они указаны, независимо от счёта, и прибавляются к карточкам из стартовых показателей (start_КОД), если они там заданы.

Итог команды

yellow_КОД, yellowred_КОД, red_КОД — итоговое количество карточек команды (целое неотрицательное число). Если у команды указан хотя бы один из этих параметров, все её карточки берутся только из них (неуказанные считаются нулём), а карточки из матчей и стартовых показателей этой команды не учитываются. У остальных команд карточки по-прежнему считаются по матчам.

| yellow_ЗЕН    = 41
| yellowred_ЗЕН = 2
| red_ЗЕН       = 1

Так удобно поступать, если поматчевой статистики нет или итог нужно привести в соответствие с официальными данными.

Снятие очков

Снятие очков привязывается к конкретному матчу:

matchN_deduction = код;очки;причина

Часть Описание
код Команда, с которой сняты очки. Должна быть одной из двух команд матча N.
очки Сколько очков снято: положительное число, можно дробное (1,5).
причина Необязательно. Пишется как продолжение фразы: «за неявку на матч», «по решению КДК». Точка в конце не нужна. Можно использовать ссылки и «;».

Для второго снятия в том же матче (например, с обеих команд) используется matchN_deduction2, и так далее до matchN_deduction5.

| match7           = 4;2;ЦСК;СПА;1:1
| match7_deduction = СПА;3;за участие в матче дисквалифицированного игрока

Результат:

  • в итоговой таблице очки команды уменьшены, у названия стоит сноска вида [a], а при наведении на очки видна подсказка «Снято 3 очка»;
  • под таблицей появляется блок «Примечания» с текстом С команды «Спартак» снято 3 очка в 4-м туре (матч «ЦСКА» — «Спартак») за участие в матче дисквалифицированного игрока. Номер тура упоминается, если он указан в матче;
  • в таблице положения по турам очки снимаются начиная с этого тура, а в ячейке этого тура стоит та же сноска.

Сноски обозначаются буквами a, b, c… сверху вниз по итоговой таблице. Снятие действует независимо от того, сыгран ли матч, и не влияет на критерий личные.

Очки, снятые с команды до начала таблицы (например, в первом этапе турнира), здесь не указываются: они уже входят в её стартовые очки — см. «Стартовые показатели».

Турнир из нескольких этапов

Бывает, что после первого этапа таблица делится на группы — пополам или в другой пропорции, например 6 первых команд играют за 1–6 места, а 8 последних — за 7–14 места, — и команды доигрывают в своих группах. Такой турнир оформляется несколькими таблицами:

  1. первый этап — обычная таблица всех команд (если этап однокруговой — с rounds = 1);
  2. каждая группа второго этапа — отдельная таблица только из команд группы и матчей второго этапа. Итоги первого этапа задаются стартовыми показателями start_КОД, а у групп ниже первой нумерация мест начинается с first_place. Если среди критериев есть личные, в таблицу группы добавляются ещё и матчи первого этапа — h2h_matchN.

Стартовые показатели переписываются из итоговой таблицы первого этапа вручную. Зато так можно описать любой регламент перехода: сохраняются все результаты, очки делятся пополам или учитываются только матчи с командами своей группы.

Полный пример — в разделе «Пример турнира из двух этапов».

Стартовые показатели

start_КОД = И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК — показатели, с которыми команда начинает таблицу. Части идут в том же порядке, что и столбцы итоговой таблицы (без РМ — она вычисляется сама), а за ними карточки:

Часть Что означает Если пусто
И Игры В + Н + П
В, Н, П Победы, ничьи, поражения 0
МЗ, МП Забитые и пропущенные мячи 0
О Очки; могут быть дробными (12,5) и отрицательными Вычисляются по победам, ничьим и поражениям с учётом points_win, points_draw, points_loss
ЖК, 2ЖК, КК Жёлтые карточки, удаления за вторую жёлтую карточку, прямые красные — как yellow, yellowred, red в разделе «Карточки» 0

Все части, кроме О, — целые неотрицательные числа. Если И указано, оно должно быть равно В + Н + П. Последние части можно не писать: при 3 очках за победу start_СПА = 13;9;2;2;27;10 означает то же, что start_СПА = 13;9;2;2;27;10;29.

Стартовые показатели:

  • прибавляются к результатам матчей таблицы и учитываются во всех критериях, кроме личные;
  • в таблице положения по турам действуют с первого тура этой таблицы;
  • в шахматке не видны: в ней только матчи этой таблицы;
  • если в них указаны карточки, те складываются с карточками из матчей, а итог команды (yellow_КОД и т. д.) заменяет и те и другие.

Очки, снятые в первом этапе, уже входят в очки О, и сноски о них в таблице группы не выводятся. Снятие очков за матчи второго этапа указывается как обычно — matchN_deduction.

Если во второй этап переходят не все результаты первого, в start_КОД указывается только то, что переходит. Например, если команды сохраняют половину очков, а игры и мячи считаются заново: start_СПА = ;;;;;;14,5.

Параметры start_КОД для кодов, которых нет в teams, не читаются, поэтому в таблицы всех групп можно вставить один и тот же блок со всеми командами.

Нумерация мест

first_place — номер первого места таблицы, по умолчанию 1. Для группы за 7–14 места указывается first_place = 7: места в турнирной таблице и в таблице положения по турам нумеруются с 7-го. В этой же нумерации указываются места в highlightN и progress_highlightN, например highlight1 = 13;2;red;Выбывание.

Зона, которая начинается выше первого места таблицы, обрезается, а зоны целиком за пределами таблицы не выводятся, поэтому одинаковый набор зон тоже можно вставить в таблицы всех групп.

Личные встречи с учётом первого этапа

В таблице группы критерий личные видит только её матчи, а по регламенту обычно учитываются все матчи сезона между командами. Матчи первого этапа добавляются параметрами h2h_match1, h2h_match2… (без пропусков в нумерации) в тех же форматах, что и matchN. Такие матчи:

  • учитываются только в критерии личные, а в таблице положения по турам — как сыгранные до первого тура;
  • не влияют на показатели команд, шахматку и таблицу по турам: итоги первого этапа уже есть в start_КОД;
  • пропускаются, если в матче играет команда, которой нет в teams, или если матч не сыгран.

Проще всего скопировать в таблицу группы строки matchN из таблицы первого этапа и заменить в них match на h2h_match. Матчи с командами других групп модуль пропустит сам, а получившиеся из карточек и снятий очков строки вроде h2h_match5_yellow не читает — их можно удалить.

Оформление

Параметр Описание
heading_level Уровень заголовков «Результаты» и «Положение команд по турам»: от 2 до 6, по умолчанию 3 (=== Результаты ===). При значении 2 — == Результаты ==.
id Префикс якорей для сносок. По умолчанию вычисляется по списку команд, поэтому обычно его указывать не нужно. Задайте разные id, если на одной странице две таблицы с одинаковым списком команд и со снятием очков. Лучше использовать латиницу, цифры и дефис.

CSS-классы, которые выводит модуль:

Класс Где используется
football-tables Общий блок вокруг всех таблиц
football-standings, football-results, football-progress Итоговая таблица, шахматка, таблица по турам
football-info Строка критериев и блок примечаний
football-notes Блок примечаний
football-note-ref Сноска о снятии очков
football-deduction Ячейка таблицы по турам, в которой сняты очки
football-cards Ячейки столбцов карточек
football-card, football-card-yellow, football-card-yellowred, football-card-red Значки карточек в заголовке
highlight и класс зоны Строки цветовых зон и выделенные ячейки таблицы по турам
ui green, ui yellow, ui red Результаты в шахматке
football-corner, football-team-header, football-diagonal Угловая ячейка, названия команд и диагональ шахматки

Цвета зон и результатов модуль не задаёт — они берутся из стилей вики. Если таких стилей ещё нет, их можно добавить в MediaWiki:Common.css, например:

.football-tables .highlight.green { background-color: #bbf3bb; }
.football-tables .highlight.blue  { background-color: #bbebff; }
.football-tables .highlight.red   { background-color: #ffbbbb; }
.football-results td.ui.green     { background-color: #bbf3bb; }
.football-results td.ui.yellow    { background-color: #ffffbb; }
.football-results td.ui.red       { background-color: #ffbbbb; }
.football-progress td.football-deduction { font-weight: bold; }

Сообщения об ошибках

При ошибке во входных данных модуль вместо таблиц выводит сообщение, начинающееся со слова «Ошибка»:

Сообщение Что исправить
не указан параметр teams Добавьте список команд.
команда с кодом … указана более одного раза Уберите повтор из teams.
матч N имеет неправильный формат В matchN меньше трёх частей через «;».
параметр h2h_matchN имеет неправильный формат В h2h_matchN меньше трёх частей через «;».
в матче N неизвестная команда … Код не входит в teams. Проверьте написание, в том числе не попали ли латинские буквы вместо кириллических.
параметр matchN_yellow имеет неправильный формат … Карточки в матче записываются как число:число (то же для matchN_yellowred и matchN_red).
в параметре matchN_deduction должна быть указана команда матча … Код команды должен совпадать с хозяевами или гостями матча N.
в параметре matchN_deduction не указано, сколько очков снято Вторая часть должна быть ненулевым числом.
параметр yellow_КОД должен быть целым неотрицательным числом … Исправьте итог карточек команды (то же для yellowred_КОД и red_КОД).
параметр rounds может быть равен 1 … или 2 … Укажите 1 для однокругового турнира или уберите параметр.
параметр first_place должен быть целым положительным числом … Укажите номер первого места таблицы, например 7.
в параметре start_КОД больше 10 значений … Частей не больше десяти: И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК.
в параметре start_КОД значение … должно быть целым неотрицательным числом … Исправьте названную часть. Дробными и отрицательными могут быть только очки.
в параметре start_КОД очки (О) должны быть числом … Седьмая часть — число, например 29 или 14,5.
в параметре start_КОД число игр … не равно сумме побед, ничьих и поражений … Проверьте И, В, Н и П или оставьте И пустым — тогда оно вычислится.

Нераспознанный счёт работу модуля не останавливает: такой матч не учитывается, а в шахматке на его месте видно «ошибка: …».

Пример

Четыре команды, шесть туров, карточки, снятие очков, цветовые зоны и таблица положения по турам. Без снятия очков первое место занял бы «Спартак».

{{#invoke:Турнирная таблица|main
| teams    = СПА;ЦСК;ЗЕН;ЛОК
| name_СПА = Спартак
| name_ЦСК = ЦСКА
| name_ЗЕН = Зенит
| name_ЛОК = Локомотив

| match1  = 1;1;СПА;ЦСК;2:1
| match2  = 1;1;ЗЕН;ЛОК;1:1
| match3  = 2;1;ЦСК;ЗЕН;0:2
| match4  = 2;1;ЛОК;СПА;1:3
| match5  = 3;1;СПА;ЗЕН;1:1
| match6  = 3;1;ЦСК;ЛОК;2:0
| match7  = 4;2;ЦСК;СПА;1:1
| match8  = 4;2;ЛОК;ЗЕН;0:1
| match9  = 5;2;ЗЕН;ЦСК;3:1
| match10 = 5;2;СПА;ЛОК;2:2
| match11 = 6;2;ЗЕН;СПА;0:2
| match12 = 6;2;ЛОК;ЦСК;перенесён

| match1_yellow     = 2:3
| match1_yellowred  = 0:1
| match4_yellow     = 3:1
| match7_yellow     = 1:2
| match7_red        = 1:0
| match9_yellow     = 2:2
| match11_yellow    = 1:3
| match11_yellowred = 1:0

| match7_deduction = СПА;3;за участие в матче дисквалифицированного игрока

| highlight1 = 1;1;green;Выход в финальный турнир
| highlight2 = 4;1;red;Выбывание

| show_progress       = yes
| progress_highlight1 = 1;green
| progress_highlight2 = 4;red
}}

Результат:

МестоКомандаИВНПМЗМПРМО​​​Повышение или выбывание
1Зенит632185+311310Выход в финальный турнир
2Спартак[a]6330116+59800
3ЦСКА511358-34611
4Локомотив502349-52300Выбывание
Критерии определения мест: очки → разница → забитые → победы → название
Примечания:
  1. ↑ С команды «Спартак» снято 3 очка в 4-м туре (матч «ЦСКА» — «Спартак») за участие в матче дисквалифицированного игрока.

Результаты

СпартакЦСКАЗенитЛокомотив
Спартак—2:11:12:2
1:10:21:3
ЦСКА1:1—0:22:0
2:13:1перенесён
Зенит0:23:1—1:1
1:10:20:1
Локомотив1:3перенесён0:1—
2:22:01:1

Положение команд по турам

КомандаТур
123456
Спартак1112[a]22
ЦСКА443333
Зенит222111
Локомотив334444

Пример турнира из двух этапов

Шесть команд играют первый этап в один круг, затем три лучшие разыгрывают 1–3 места, а остальные — 4–6 места: в своих группах команды играют в два круга. Все результаты первого этапа сохраняются, а при равенстве очков места определяются по личным встречам за весь сезон.

Стартовые показатели в таблицах групп — это строки итоговой таблицы первого этапа, а в h2h_matchN — матчи первого этапа между командами группы. В группе за 4–6 места «Динамо» и ЦСКА набрали поровну очков и во втором этапе обменялись гостевыми победами 2:1, поэтому «Динамо» выше благодаря победе 2:0 в первом этапе. Без строк h2h_match выше оказался бы ЦСКА — у него лучше разница мячей.

=== Первый этап ===
{{#invoke:Турнирная таблица|main
| teams    = СПА;ЦСК;ЗЕН;ЛОК;ДИН;КРА
| name_СПА = Спартак
| name_ЦСК = ЦСКА
| name_ЗЕН = Зенит
| name_ЛОК = Локомотив
| name_ДИН = Динамо
| name_КРА = Краснодар
| rounds   = 1
| heading_level = 4

| tie1 = очки
| tie2 = личные
| tie3 = разница
| tie4 = забитые

| match1  = 1;1;СПА;КРА;1:1
| match2  = 1;1;ДИН;ЦСК;2:0
| match3  = 1;1;ЗЕН;ЛОК;1:2
| match4  = 2;1;ДИН;СПА;0:0
| match5  = 2;1;КРА;ЛОК;1:0
| match6  = 2;1;ЗЕН;ЦСК;1:1
| match7  = 3;1;СПА;ЛОК;0:1
| match8  = 3;1;ЗЕН;ДИН;3:1
| match9  = 3;1;КРА;ЦСК;2:1
| match10 = 4;1;ЗЕН;СПА;1:3
| match11 = 4;1;ЛОК;ЦСК;2:0
| match12 = 4;1;КРА;ДИН;3:0
| match13 = 5;1;СПА;ЦСК;0:2
| match14 = 5;1;КРА;ЗЕН;1:2
| match15 = 5;1;ЛОК;ДИН;2:1

| highlight1 = 1;3;green;Группа за 1–3 места
| highlight2 = 4;3;blue;Группа за 4–6 места
}}

=== Группа за 1–3 места ===
{{#invoke:Турнирная таблица|main
| teams    = ЛОК;КРА;ЗЕН
| name_ЛОК = Локомотив
| name_КРА = Краснодар
| name_ЗЕН = Зенит
| heading_level = 4

| tie1 = очки
| tie2 = личные
| tie3 = разница
| tie4 = забитые

| start_ЛОК = 5;4;0;1;7;3;12
| start_КРА = 5;3;1;1;8;4;10
| start_ЗЕН = 5;2;1;2;8;8;7

| h2h_match1 = 1;1;ЗЕН;ЛОК;1:2
| h2h_match2 = 2;1;КРА;ЛОК;1:0
| h2h_match3 = 5;1;КРА;ЗЕН;1:2

| match1 = 6;1;ЗЕН;КРА;2:2
| match2 = 7;1;ЗЕН;ЛОК;0:2
| match3 = 8;1;ЛОК;КРА;1:0
| match4 = 9;2;КРА;ЗЕН;0:1
| match5 = 10;2;ЛОК;ЗЕН;2:0
| match6 = 11;2;КРА;ЛОК;0:1

| highlight1 = 1;1;green;Чемпион

| show_progress       = yes
| progress_highlight1 = 1;green
}}

=== Группа за 4–6 места ===
{{#invoke:Турнирная таблица|main
| teams       = СПА;ДИН;ЦСК
| name_СПА    = Спартак
| name_ДИН    = Динамо
| name_ЦСК    = ЦСКА
| first_place = 4
| heading_level = 4

| tie1 = очки
| tie2 = личные
| tie3 = разница
| tie4 = забитые

| start_СПА = 5;1;2;2;4;5;5
| start_ДИН = 5;1;1;3;4;8;4
| start_ЦСК = 5;1;1;3;4;7;4

| h2h_match1 = 1;1;ДИН;ЦСК;2:0
| h2h_match2 = 2;1;ДИН;СПА;0:0
| h2h_match3 = 5;1;СПА;ЦСК;0:2

| match1 = 6;1;ЦСК;ДИН;1:2
| match2 = 7;1;ЦСК;СПА;0:1
| match3 = 8;1;СПА;ДИН;1:2
| match4 = 9;2;ДИН;ЦСК;1:2
| match5 = 10;2;СПА;ЦСК;0:2
| match6 = 11;2;ДИН;СПА;0:1

| highlight1 = 6;1;red;Выбывание

| show_progress       = yes
| progress_highlight1 = 6;red
}}

Результат:

Первый этап

МестоКомандаИВНПМЗМПРМОПовышение или выбывание
1Локомотив540173+412Группа за 1–3 места
2Краснодар531184+410
3Зенит52128807
4Спартак512245-15Группа за 4–6 места
5Динамо511348-44
6ЦСКА511347-34
Критерии определения мест: очки → личные → разница → забитые

Результаты

СпартакЦСКАЗенитЛокомотивДинамоКраснодар
Спартак—0:2—0:1—1:1
—1:3—0:0—
ЦСКА——————
0:21:12:02:02:1
Зенит1:31:1—1:23:1—
————1:2
Локомотив—2:0——2:1—
0:1—1:2—1:0
Динамо0:02:0————
——3:12:13:0
Краснодар—2:11:21:03:0—
1:1————

Группа за 1–3 места

МестоКомандаИВНПМЗМПРМОПовышение или выбывание
1Локомотив440060+612Чемпион
2Зенит411236-34
3Краснодар401325-31
Критерии определения мест: очки → личные → разница → забитые

Результаты

ЛокомотивКраснодарЗенит
Локомотив—1:02:0
0:10:2
Краснодар0:1—0:1
1:02:2
Зенит0:22:2—
2:00:1

Положение команд по турам

КомандаТур
67891011
Локомотив311111
Краснодар222333
Зенит133222

Группа за 4–6 места

МестоКомандаИВНПМЗМПРМОПовышение или выбывание
1ЦСКА420254+16
2Динамо42025506
3Спартак420234-16
Критерии определения мест: очки → личные → разница → забитые

Результаты

СпартакДинамоЦСКА
Спартак—1:20:2
0:10:1
Динамо0:1—1:2
1:21:2
ЦСКА0:11:2—
0:21:2

Положение команд по турам

КомандаТур
67891011
Спартак222233
Динамо111112
ЦСКА333321

local p = {}

-- =========================================================
-- НАСТРОЙКИ
-- =========================================================

-- Виды карточек в порядке вывода столбцов.
-- key — часть имени параметров: matchN_<key> и <key>_КОД.
local CARD_TYPES = {
	{
		key = "yellow",
		title = "Жёлтые карточки",
		background = "#ffd200"
	},
	{
		key = "yellowred",
		title = "Удаления за вторую жёлтую карточку",
		background = "linear-gradient(135deg, #ffd200 50%, #e30613 50%)"
	},
	{
		key = "red",
		title = "Прямые красные карточки",
		background = "#e30613"
	}
}

-- Сколько снятий очков можно привязать к одному матчу:
-- matchN_deduction, matchN_deduction2 … matchN_deduction5
local MAX_DEDUCTIONS = 5


-- =========================================================
-- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
-- =========================================================

local function trim(value)
	if value == nil then
		return ""
	end

	return mw.text.trim(tostring(value))
end


local function split(value, separator)
	local result = {}

	for part in mw.text.gsplit(value, separator, true) do
		table.insert(result, trim(part))
	end

	return result
end


local function number(value, default)
	local n = tonumber(value)

	if n == nil then
		return default
	end

	return n
end


-- Число для текста: 3 → «3», 1.5 → «1,5»
local function formatNumber(n)
	if n == math.floor(n) then
		return string.format("%d", n)
	end

	return (string.gsub(tostring(n), "%.", ","))
end


-- Форма слова после числа: 1 очко, 2 очка, 5 очков
local function plural(n, one, few, many)
	n = math.abs(n)

	if n ~= math.floor(n) then
		return few
	end

	local lastTwo = n % 100
	local last = n % 10

	if lastTwo >= 11 and lastTwo <= 14 then
		return many
	elseif last == 1 then
		return one
	elseif last >= 2 and last <= 4 then
		return few
	end

	return many
end


-- Пара значений «хозяева:гости», например «2:1»
local function parsePair(value)
	local home, away =
		trim(value):match("^(%d+)%s*:%s*(%d+)$")

	if home and away then
		return tonumber(home), tonumber(away)
	end

	return nil
end


-- Метка сноски: 1 → a, 2 → b … 26 → z, 27 → aa
local function noteLabel(index)
	local label = ""

	while index > 0 do
		local rest = (index - 1) % 26
		label = string.char(97 + rest) .. label
		index = math.floor((index - 1) / 26)
	end

	return label
end


-- =========================================================
-- РАЗБОР СЧЁТА
-- =========================================================

local function parseScore(score)
	score = trim(score)

	-- Матч не сыгран
	if score == "" or score == "-" or score == "—" then
		return {
			played = false,
			status = ""
		}
	end

	-- Перенесён
	if score == "перенесён" or score == "перенесен" then
		return {
			played = false,
			status = "перенесён"
		}
	end

	-- Отменён
	if score == "отменён" or score == "отменен" then
		return {
			played = false,
			status = "отменён"
		}
	end

	-- Обычный счёт
	local home, away = score:match("^(%d+)%s*:%s*(%d+)$")

	if home and away then
		return {
			played = true,
			homeGoals = tonumber(home),
			awayGoals = tonumber(away),
			score = home .. ":" .. away
		}
	end

	-- Неизвестный формат
	return {
		played = false,
		status = "ошибка: " .. score
	}
end


-- =========================================================
-- СОЗДАНИЕ КОМАНДЫ
-- =========================================================

local function newTeam(code, name)
	return {
		code = code,
		name = name,

		played = 0,
		wins = 0,
		draws = 0,
		losses = 0,

		goalsFor = 0,
		goalsAgainst = 0,

		points = 0,

		-- Сколько очков снято всего
		deducted = 0,

		-- Сноски о снятии очков
		notes = {},

		-- Карточки, собранные по матчам
		matchCards = {
			yellow = 0,
			yellowred = 0,
			red = 0
		},

		-- Итоговые карточки (из матчей или из параметров команды)
		yellow = 0,
		yellowred = 0,
		red = 0
	}
end


-- =========================================================
-- СОЗДАНИЕ HTML
-- =========================================================

local function cell(tag, content, class)
	local element = mw.html.create(tag)

	if class and class ~= "" then
		element
			:addClass(class)
	end

	if content ~= nil then
		element
			:wikitext(tostring(content))
	end

	return element
end


-- Значок карточки для заголовка столбца.
-- Рисуется средствами CSS, поэтому не нужны файлы изображений.
local function cardIcon(cardType)
	return mw.html.create("span")
		:addClass("football-card")
		:addClass("football-card-" .. cardType.key)
		:attr("title", cardType.title)
		:css("display", "inline-block")
		:css("width", "0.7em")
		:css("height", "0.95em")
		:css("vertical-align", "middle")
		:css("border-radius", "0.12em")
		:css("border", "1px solid rgba(0, 0, 0, 0.35)")
		:css("background", cardType.background)
		:wikitext("&#8203;")
end


-- =========================================================
-- ОСНОВНАЯ ФУНКЦИЯ
-- =========================================================

function p.main(frame)

	local args = frame.args

	-- -----------------------------------------------------
	-- НАСТРОЙКИ ОЧКОВ
	-- -----------------------------------------------------

	local pointsWin =
		number(args.points_win, 3)

	local pointsDraw =
		number(args.points_draw, 1)

	local pointsLoss =
		number(args.points_loss, 0)


	-- -----------------------------------------------------
	-- ЗАГОЛОВКИ РАЗДЕЛОВ
	-- -----------------------------------------------------

	-- 3 → «=== Заголовок ===» (по умолчанию),
	-- 2 → «== Заголовок ==», 4 → «==== Заголовок ====»
	local headingLevel =
		math.floor(number(args.heading_level, 3))

	headingLevel =
		math.max(2, math.min(6, headingLevel))

	local function heading(text)
		local marks = string.rep("=", headingLevel)

		-- Заголовок вики-разметки должен стоять
		-- на отдельной строке
		return "\n" .. marks .. " " .. text .. " " .. marks .. "\n"
	end


	-- -----------------------------------------------------
	-- КОМАНДЫ
	-- -----------------------------------------------------

	local teamsParameter = trim(args.teams)

	if teamsParameter == "" then
		return "'''Ошибка:''' не указан параметр ''teams''."
	end

	local teamCodes = split(teamsParameter, ";")

	local teams = {}
	local teamOrder = {}

	for _, code in ipairs(teamCodes) do

		if code ~= "" then

			if teams[code] then
				return "'''Ошибка:''' команда с кодом '''" ..
					code .. "''' указана более одного раза."
			end

			local name = trim(args["name_" .. code])

			if name == "" then
				name = code
			end

			teams[code] = newTeam(code, name)

			table.insert(teamOrder, code)
		end
	end


	-- Префикс якорей для сносок. Он должен быть уникальным
	-- на странице, поэтому по умолчанию зависит от списка команд.
	local idPrefix = trim(args.id)

	if idPrefix == "" then

		idPrefix = "football"

		if mw.hash and mw.hash.hashValue then

			local ok, hash = pcall(
				mw.hash.hashValue,
				"crc32b",
				teamsParameter
			)

			if ok and hash then
				idPrefix = idPrefix .. "-" .. hash
			end

		end

	else
		-- Символы, которые ломают якорь или ссылку [[#…]]
		idPrefix = string.gsub(idPrefix, "[%s%[%]{}|#<>\"'&]+", "_")
	end


	-- -----------------------------------------------------
	-- УЧЁТ РЕЗУЛЬТАТА И СНЯТИЯ ОЧКОВ
	-- -----------------------------------------------------

	-- Добавляет результат сыгранного матча в статистику.
	-- teamsMap — итоговые команды или команды «по турам».
	local function addResult(teamsMap, match)

		local homeTeam = teamsMap[match.home]
		local awayTeam = teamsMap[match.away]

		homeTeam.played = homeTeam.played + 1
		awayTeam.played = awayTeam.played + 1

		homeTeam.goalsFor =
			homeTeam.goalsFor + match.homeGoals

		homeTeam.goalsAgainst =
			homeTeam.goalsAgainst + match.awayGoals

		awayTeam.goalsFor =
			awayTeam.goalsFor + match.awayGoals

		awayTeam.goalsAgainst =
			awayTeam.goalsAgainst + match.homeGoals


		if match.homeGoals > match.awayGoals then

			homeTeam.wins = homeTeam.wins + 1
			awayTeam.losses = awayTeam.losses + 1

			homeTeam.points =
				homeTeam.points + pointsWin

			awayTeam.points =
				awayTeam.points + pointsLoss


		elseif match.homeGoals < match.awayGoals then

			awayTeam.wins = awayTeam.wins + 1
			homeTeam.losses = homeTeam.losses + 1

			awayTeam.points =
				awayTeam.points + pointsWin

			homeTeam.points =
				homeTeam.points + pointsLoss


		else

			homeTeam.draws = homeTeam.draws + 1
			awayTeam.draws = awayTeam.draws + 1

			homeTeam.points =
				homeTeam.points + pointsDraw

			awayTeam.points =
				awayTeam.points + pointsDraw

		end

	end


	-- Снимает очки, привязанные к матчу
	-- (независимо от того, сыгран ли он)
	local function applyDeductions(teamsMap, match)

		for _, deduction in ipairs(match.deductions) do

			local team = teamsMap[deduction.team]

			team.points =
				team.points - deduction.points

			team.deducted =
				team.deducted + deduction.points

		end

	end


	-- -----------------------------------------------------
	-- МАТЧИ
	-- -----------------------------------------------------

	local matches = {}

	-- Все снятия очков в порядке матчей
	local deductions = {}

	-- Указаны ли где-нибудь карточки
	local hasCards = false

	local matchNumber = 1

	while true do

		local value = args["match" .. matchNumber]

		if value == nil or trim(value) == "" then
			break
		end

		local parts = split(value, ";")

		local tour
		local round
		local home
		local away
		local score

		-- Формат с номером тура:
		-- тур;круг;хозяева;гости;счёт
		if #parts >= 5 then
			tour = trim(parts[1])
			round = trim(parts[2])
			home = trim(parts[3])
			away = trim(parts[4])
			score = trim(parts[5])

		-- Формат с номером круга:
		-- круг;хозяева;гости;счёт
		elseif #parts >= 4 then
			tour = ""
			round = trim(parts[1])
			home = trim(parts[2])
			away = trim(parts[3])
			score = trim(parts[4])

		-- Формат без номера круга:
		-- хозяева;гости;счёт
		elseif #parts >= 3 then
			tour = ""
			round = ""
			home = trim(parts[1])
			away = trim(parts[2])
			score = trim(parts[3])

		else

			return "'''Ошибка:''' матч " ..
				matchNumber ..
				" имеет неправильный формат."

		end


		-- Проверяем команды

		if not teams[home] then

			return "'''Ошибка:''' в матче " ..
				matchNumber ..
				" неизвестная команда '''" ..
				home .. "'''."

		end

		if not teams[away] then

			return "'''Ошибка:''' в матче " ..
				matchNumber ..
				" неизвестная команда '''" ..
				away .. "'''."

		end


		local parsed = parseScore(score)

		local match = {
			number = matchNumber,
			tour = tour,
			round = round,

			home = home,
			away = away,

			played = parsed.played,
			status = parsed.status,

			homeGoals = parsed.homeGoals,
			awayGoals = parsed.awayGoals,
			score = parsed.score,

			deductions = {}
		}


		-- -------------------------------------------------
		-- КАРТОЧКИ В МАТЧЕ
		-- -------------------------------------------------

		-- matchN_yellow, matchN_yellowred, matchN_red
		-- в формате «хозяева:гости»

		for _, cardType in ipairs(CARD_TYPES) do

			local paramName =
				"match" .. matchNumber .. "_" .. cardType.key

			local raw = trim(args[paramName])

			if raw ~= "" then

				local homeCards, awayCards = parsePair(raw)

				if homeCards == nil then

					return "'''Ошибка:''' параметр ''" ..
						paramName ..
						"'' имеет неправильный формат «" ..
						raw ..
						"». Укажите карточки хозяев и гостей " ..
						"через двоеточие, например ''2:1''."

				end

				local homeCardsTable = teams[home].matchCards
				local awayCardsTable = teams[away].matchCards

				homeCardsTable[cardType.key] =
					homeCardsTable[cardType.key] + homeCards

				awayCardsTable[cardType.key] =
					awayCardsTable[cardType.key] + awayCards

				hasCards = true

			end

		end


		-- -------------------------------------------------
		-- СНЯТИЕ ОЧКОВ ЗА МАТЧ
		-- -------------------------------------------------

		-- matchN_deduction = код;очки;причина

		for i = 1, MAX_DEDUCTIONS do

			local paramName =
				"match" .. matchNumber .. "_deduction"

			if i > 1 then
				paramName = paramName .. i
			end

			local raw = trim(args[paramName])

			if raw ~= "" then

				local deductionParts = split(raw, ";")

				local code = deductionParts[1]

				-- Допускается и «1,5», и «1.5»
				local amount = tonumber(
					(string.gsub(deductionParts[2] or "", ",", "."))
				)

				if code ~= home and code ~= away then

					return "'''Ошибка:''' в параметре ''" ..
						paramName ..
						"'' должна быть указана команда матча " ..
						matchNumber .. " ('''" .. home ..
						"''' или '''" .. away ..
						"'''), а указано «" .. code .. "»."

				end

				if amount == nil or
				   amount == 0 or
				   amount ~= amount or
				   math.abs(amount) == math.huge then

					return "'''Ошибка:''' в параметре ''" ..
						paramName ..
						"'' не указано, сколько очков снято."

				end

				-- Причина — всё, что после второй «;»
				local reason =
					table.concat(deductionParts, "; ", 3)

				reason = string.gsub(reason, "[%s%.]+$", "")

				local deduction = {
					team = code,
					points = math.abs(amount),
					reason = reason,
					match = match
				}

				table.insert(match.deductions, deduction)
				table.insert(deductions, deduction)

			end

		end


		table.insert(matches, match)


		-- -------------------------------------------------
		-- СТАТИСТИКА
		-- -------------------------------------------------

		if parsed.played then
			addResult(teams, match)
		end

		applyDeductions(teams, match)


		matchNumber = matchNumber + 1
	end


	-- =====================================================
	-- КАРТОЧКИ КОМАНД
	-- =====================================================

	-- Параметры yellow_КОД, yellowred_КОД, red_КОД задают
	-- итог команды. Если указан хотя бы один из них, карточки
	-- команды берутся только из этих параметров (неуказанные
	-- считаются нулём), а карточки из матчей не учитываются.

	for _, code in ipairs(teamOrder) do

		local team = teams[code]

		local override = {}
		local hasOverride = false

		for _, cardType in ipairs(CARD_TYPES) do

			local paramName = cardType.key .. "_" .. code
			local raw = trim(args[paramName])

			if raw ~= "" then

				if not raw:match("^%d+$") then

					return "'''Ошибка:''' параметр ''" ..
						paramName ..
						"'' должен быть целым неотрицательным " ..
						"числом, а указано «" .. raw .. "»."

				end

				override[cardType.key] = tonumber(raw)
				hasOverride = true

			end

		end

		for _, cardType in ipairs(CARD_TYPES) do

			if hasOverride then
				team[cardType.key] =
					override[cardType.key] or 0
			else
				team[cardType.key] =
					team.matchCards[cardType.key]
			end

		end

		if hasOverride then
			hasCards = true
		end

	end


	-- =====================================================
	-- РАЗНИЦА МЯЧЕЙ
	-- =====================================================

	for _, code in ipairs(teamOrder) do

		local team = teams[code]

		team.goalDifference =
			team.goalsFor - team.goalsAgainst

	end


	-- =====================================================
	-- ЛИЧНЫЕ ВСТРЕЧИ
	-- =====================================================

	-- Возвращает статистику команды только в матчах
	-- против команд из указанной группы.
	-- Если задан lastTour, учитываются только матчи
	-- с номером тура не больше lastTour.
	-- Снятие очков на личные встречи не влияет.

	local function headToHead(code, group, lastTour)

		local result = {
			points = 0,
			goalsFor = 0,
			goalsAgainst = 0,
			goalDifference = 0,
			wins = 0
		}

		for _, match in ipairs(matches) do

			local inRange = true

			if lastTour then
				local tourNumber = tonumber(match.tour)

				inRange =
					tourNumber ~= nil and tourNumber <= lastTour
			end

			if match.played and
			   inRange and
			   group[match.home] and
			   group[match.away] then

				local scored
				local conceded

				if match.home == code then
					scored = match.homeGoals
					conceded = match.awayGoals
				elseif match.away == code then
					scored = match.awayGoals
					conceded = match.homeGoals
				end

				if scored then

					result.goalsFor =
						result.goalsFor + scored

					result.goalsAgainst =
						result.goalsAgainst + conceded

					if scored > conceded then

						result.points =
							result.points + pointsWin

						result.wins =
							result.wins + 1

					elseif scored == conceded then

						result.points =
							result.points + pointsDraw

					else

						result.points =
							result.points + pointsLoss

					end

				end

			end

		end

		result.goalDifference =
			result.goalsFor - result.goalsAgainst

		return result
	end


	-- =====================================================
	-- КРИТЕРИИ ОПРЕДЕЛЕНИЯ МЕСТ
	-- =====================================================

	-- По умолчанию:
	-- 1. очки
	-- 2. разница мячей
	-- 3. забитые мячи
	-- 4. победы
	-- 5. название

	local criteria = {}

	for i = 1, 10 do

		local criterion = trim(args["tie" .. i])

		if criterion ~= "" then
			table.insert(criteria, criterion)
		end

	end

	-- Если критерии не указаны

	if #criteria == 0 then

		criteria = {
			"очки",
			"разница",
			"забитые",
			"победы",
			"название"
		}

	end


	-- -----------------------------------------------------
	-- Сравнение команд
	-- -----------------------------------------------------

	-- Возвращает функцию сравнения для table.sort.
	-- teamsMap — команды, среди которых ищутся равные по очкам
	-- (для критерия «личные»); lastTour — последний
	-- учитываемый тур (nil — все матчи).

	local function makeComparator(teamsMap, lastTour)

		return function(a, b)

			for _, criterion in ipairs(criteria) do

				-- -----------------------------------------
				-- ОЧКИ
				-- -----------------------------------------

				if criterion == "очки" then

					if a.points ~= b.points then
						return a.points > b.points
					end


				-- -----------------------------------------
				-- РАЗНИЦА МЯЧЕЙ
				-- -----------------------------------------

				elseif criterion == "разница" then

					if a.goalDifference ~= b.goalDifference then
						return a.goalDifference > b.goalDifference
					end


				-- -----------------------------------------
				-- ЗАБИТЫЕ МЯЧИ
				-- -----------------------------------------

				elseif criterion == "забитые" then

					if a.goalsFor ~= b.goalsFor then
						return a.goalsFor > b.goalsFor
					end


				-- -----------------------------------------
				-- ПОБЕДЫ
				-- -----------------------------------------

				elseif criterion == "победы" then

					if a.wins ~= b.wins then
						return a.wins > b.wins
					end


				-- -----------------------------------------
				-- ИГРЫ
				-- -----------------------------------------

				elseif criterion == "игры" then

					if a.played ~= b.played then
						return a.played > b.played
					end


				-- -----------------------------------------
				-- ЛИЧНЫЕ
				-- -----------------------------------------

				elseif criterion == "личные" then

					-- Все команды с таким же
					-- количеством очков, как у a и b.

					local group = {}

					for _, code in ipairs(teamOrder) do

						if teamsMap[code].points == a.points then
							group[code] = true
						end

					end

					local ha = headToHead(a.code, group, lastTour)
					local hb = headToHead(b.code, group, lastTour)

					if ha.points ~= hb.points then
						return ha.points > hb.points
					end

					if ha.goalDifference ~= hb.goalDifference then
						return ha.goalDifference > hb.goalDifference
					end

					if ha.goalsFor ~= hb.goalsFor then
						return ha.goalsFor > hb.goalsFor
					end

					if ha.wins ~= hb.wins then
						return ha.wins > hb.wins
					end


				-- -----------------------------------------
				-- НАЗВАНИЕ
				-- -----------------------------------------

				elseif criterion == "название" then

					local aname =
						mw.text.unstrip(a.name)

					local bname =
						mw.text.unstrip(b.name)

					if aname ~= bname then
						return aname < bname
					end

				end

			end


			-- Последний резервный критерий —
			-- код команды.

			return a.code < b.code
		end

	end


	-- =====================================================
	-- Выделение победителей и проигравших
	-- =====================================================

	-- highlightN = место;количество;класс;текст

	local highlights = {}

	-- Есть ли текст хотя бы у одного выделения
	local hasHighlightText = false

	for i = 1, 10 do

		local hl = trim(args["highlight" .. i])

		if hl == "" then
			break
		end

		local parts = split(hl, ";")

		local place = tonumber(parts[1])

		if place then

			local highlight = {
				place = math.floor(place),
				count = math.max(1, math.floor(tonumber(parts[2]) or 1)),
				color = parts[3] or "",
				-- Текст может сам содержать «;»
				text = table.concat(parts, "; ", 4)
			}

			if highlight.text ~= "" then
				hasHighlightText = true
			end

			table.insert(highlights, highlight)

		end

	end

	table.sort(highlights, function(a, b)
		return a.place < b.place
	end)


	-- -----------------------------------------------------
	-- Режим отображения шахматки
	-- -----------------------------------------------------
	-- По умолчанию первая строчка - домашние игры, вторая строчка - гостевые
	-- Если resultsMode = round, то первая строчка - 1 круг, вторая строчка - 2 круг
	local resultsMode = 0
	if args["resultsMode"] ~= nil and args["resultsMode"] ~= "" then
		if args["resultsMode"] == "round" then
			resultsMode = 1
		end
	end


	-- =====================================================
	-- ПРОГРЕСС ПО ТУРАМ
	-- =====================================================

	local function buildProgress()

		local progress = {}

		-- Собираем список туров
		local tours = {}

		for _, match in ipairs(matches) do

			local tourNumber = tonumber(match.tour)

			if tourNumber then
				tours[tourNumber] = true
			end

		end

		local tourNumbers = {}

		for tourNumber in pairs(tours) do
			table.insert(tourNumbers, tourNumber)
		end

		table.sort(tourNumbers)


		-- Таблица, которая последовательно
		-- пересчитывается после каждого тура
		local currentTeams = {}

		for _, code in ipairs(teamOrder) do
			currentTeams[code] = newTeam(
				code,
				teams[code].name
			)
		end


		for _, tourNumber in ipairs(tourNumbers) do

			-- Добавляем матчи данного тура
			for _, match in ipairs(matches) do

				if tonumber(match.tour) == tourNumber then

					if match.played then
						addResult(currentTeams, match)
					end

					-- Снятие очков действует с этого тура
					applyDeductions(currentTeams, match)

				end

			end


			-- Разница мячей
			for _, code in ipairs(teamOrder) do

				local team = currentTeams[code]

				team.goalDifference =
					team.goalsFor - team.goalsAgainst

			end


			-- Таблица после этого тура
			local tourStandings = {}

			for _, code in ipairs(teamOrder) do
				table.insert(
					tourStandings,
					currentTeams[code]
				)
			end

			table.sort(
				tourStandings,
				makeComparator(currentTeams, tourNumber)
			)


			progress[tourNumber] = {}

			for position, team in ipairs(tourStandings) do

				progress[tourNumber][team.code] =
					position

			end

		end


		return progress, tourNumbers

	end


	-- =====================================================
	-- СОРТИРОВКА
	-- =====================================================

	local standings = {}

	for _, code in ipairs(teamOrder) do
		table.insert(standings, teams[code])
	end

	table.sort(standings, makeComparator(teams, nil))

	local teamCount = #standings


	-- =====================================================
	-- Подсветка
	-- =====================================================

	-- Цвет строк
	for _, hl in ipairs(highlights) do

		local first = math.max(hl.place, 1)
		local last = math.min(hl.place + hl.count - 1, teamCount)

		for position = first, last do
			standings[position].highlightColor = hl.color
		end

	end

	-- Ячейки столбца «Повышение или выбывание».
	-- Каждая зона — одна объединённая ячейка с текстом,
	-- места между зонами — объединённые пустые ячейки.
	if hasHighlightText then

		local position = 1
		local index = 1

		while position <= teamCount do

			-- Пропускаем зоны, которые начинаются выше
			-- текущей строки (пересекаются с предыдущей)
			while highlights[index] and
			      highlights[index].place < position do
				index = index + 1
			end

			local hl = highlights[index]
			local span
			local text

			if hl and hl.place == position then

				span = math.min(hl.count, teamCount - position + 1)
				text = hl.text
				index = index + 1

			else

				local nextPlace = teamCount + 1

				if hl then
					nextPlace = math.min(hl.place, teamCount + 1)
				end

				span = nextPlace - position
				text = ""

			end

			standings[position].zoneCell = {
				text = text,
				span = span
			}

			position = position + span
		end

	end


	-- =====================================================
	-- ПРИМЕЧАНИЯ О СНЯТИИ ОЧКОВ
	-- =====================================================

	-- Текст примечания, например:
	-- «С команды «Спартак» снято 3 очка в 5-м туре
	-- (матч «Спартак» — «Локомотив») за неявку на матч.»
	local function deductionText(deduction)

		local match = deduction.match

		local text =
			"С команды «" .. teams[deduction.team].name ..
			"» снято " .. formatNumber(deduction.points) .. " " ..
			plural(deduction.points, "очко", "очка", "очков")

		local tourNumber = tonumber(match.tour)

		if tourNumber then
			text = text .. " в " ..
				formatNumber(tourNumber) .. "-м туре"
		end

		text = text ..
			" (матч «" .. teams[match.home].name ..
			"» — «" .. teams[match.away].name .. "»)"

		if deduction.reason ~= "" then
			text = text .. " " .. deduction.reason
		end

		return text .. "."
	end


	-- Сноски нумеруются сверху вниз по итоговой таблице
	local notes = {}

	for _, team in ipairs(standings) do

		for _, deduction in ipairs(deductions) do

			if deduction.team == team.code then

				local label = noteLabel(#notes + 1)

				local note = {
					label = label,
					id = idPrefix .. "-note-" .. label,
					refId = idPrefix .. "-ref-" .. label,
					text = deductionText(deduction)
				}

				deduction.note = note

				table.insert(notes, note)
				table.insert(team.notes, note)

			end

		end

	end


	-- Сноска вида [a]. Сноска в итоговой таблице получает id,
	-- чтобы из примечания можно было вернуться к ней (↑).
	local function noteMarker(note, withAnchor)

		local marker =
			mw.html.create("sup")
				:addClass("reference")
				:addClass("football-note-ref")
				:wikitext(
					"[[#" .. note.id .. "|&#91;" ..
					note.label .. "&#93;]]"
				)

		if withAnchor then
			marker:attr("id", note.refId)
		end

		return marker
	end


	-- =====================================================
	-- ТАБЛИЦА ЧЕМПИОНАТА
	-- =====================================================

	local root =
		mw.html.create("div")
			:addClass("football-tables")


	local standingsTable =
		mw.html.create("table")
			:addClass("wikitable")
			:addClass("football-standings")
			:css("text-align", "center")


	-- Заголовок

	local standingsHeader =
		mw.html.create("tr")

	local headings = {
		"Место",
		"Команда",
		"И",
		"В",
		"Н",
		"П",
		"МЗ",
		"МП",
		"РМ",
		"О"
	}

	for _, title in ipairs(headings) do

		standingsHeader:node(
			cell("th", title)
		)

	end

	-- Столбцы карточек выводятся, только если
	-- карточки где-нибудь указаны
	if hasCards then

		for _, cardType in ipairs(CARD_TYPES) do

			standingsHeader:node(
				mw.html.create("th")
					:addClass("football-cards")
					:attr("title", cardType.title)
					:node(cardIcon(cardType))
			)

		end

	end

	if hasHighlightText then

		standingsHeader:node(
			cell("th", "Повышение или выбывание")
		)

	end

	standingsTable:node(standingsHeader)


	-- Строки

	for position, team in ipairs(standings) do

		local row =
			mw.html.create("tr")

		if team.highlightColor and team.highlightColor ~= "" then
			row:addClass("highlight"):addClass(team.highlightColor)
		end

		row:node(
			cell("td", position)
		)


		-- Название и сноски о снятии очков

		local teamName =
			cell("td", team.name)
				:css("text-align", "left")

		for _, note in ipairs(team.notes) do
			teamName:node(
				noteMarker(note, true)
			)
		end

		row:node(
			teamName
		)


		row:node(
			cell("td", team.played)
		)

		row:node(
			cell("td", team.wins)
		)

		row:node(
			cell("td", team.draws)
		)

		row:node(
			cell("td", team.losses)
		)

		row:node(
			cell("td", team.goalsFor)
		)

		row:node(
			cell("td", team.goalsAgainst)
		)


		local gd

		if team.goalDifference > 0 then
			gd = "+" .. team.goalDifference
		else
			gd = tostring(team.goalDifference)
		end

		row:node(
			cell("td", gd)
		)


		local pointsCell =
			cell("td", formatNumber(team.points))

		if team.deducted > 0 then

			pointsCell:attr(
				"title",
				"Снято " .. formatNumber(team.deducted) .. " " ..
				plural(team.deducted, "очко", "очка", "очков")
			)

		end

		row:node(
			pointsCell
		)


		if hasCards then

			for _, cardType in ipairs(CARD_TYPES) do

				row:node(
					cell("td", team[cardType.key], "football-cards")
				)

			end

		end


		if hasHighlightText and team.zoneCell then

			local zone =
				cell("td", team.zoneCell.text)

			if team.zoneCell.span > 1 then
				zone:attr("rowspan", team.zoneCell.span)
			end

			row:node(
				zone
			)

		end


		standingsTable:node(row)

	end


	root:node(standingsTable)


	-- =====================================================
	-- ИНФОРМАЦИЯ О КРИТЕРИЯХ
	-- =====================================================

	local criteriaText = table.concat(criteria, " → ")

	local info =
		mw.html.create("div")
			:addClass("football-info")
			:wikitext(
				"Критерии определения мест: " ..
				criteriaText
			)

	root:node(info)


	-- =====================================================
	-- ПРИМЕЧАНИЯ ПОД ТАБЛИЦЕЙ
	-- =====================================================

	if #notes > 0 then

		local notesBlock =
			mw.html.create("div")
				:addClass("football-info")
				:addClass("football-notes")
				:wikitext("Примечания:")

		local list =
			notesBlock:tag("ol")
				:css("list-style-type", "lower-alpha")

		for _, note in ipairs(notes) do

			local item =
				list:tag("li")
					:attr("id", note.id)

			item:tag("span")
				:addClass("mw-cite-backlink")
				:wikitext("[[#" .. note.refId .. "|↑]]")

			item:wikitext(" ")

			item:tag("span")
				:addClass("reference-text")
				:wikitext(note.text)

		end

		root:node(notesBlock)

	end


	-- =====================================================
	-- РЕЗУЛЬТАТЫ
	-- =====================================================

	root:wikitext(heading("Результаты"))


	local resultsTable =
		mw.html.create("table")
			:addClass("wikitable")
			:addClass("football-results")
			:css("text-align", "center")


	-- Заголовок

	local header =
		mw.html.create("tr")

	header:node(
		cell("th", "", "football-corner")
	)

	for _, code in ipairs(teamOrder) do

		header:node(
			cell(
				"th",
				teams[code].name,
				"football-team-header"
			)
		)

	end

	resultsTable:node(header)


	-- Матрица результатов

	for _, homeCode in ipairs(teamOrder) do

		local homeRow =
			mw.html.create("tr")

		local awayRow =
			mw.html.create("tr")

		local teamHeader =
			cell(
				"th",
				teams[homeCode].name,
				"football-team-header"
			)
		teamHeader:attr("rowspan", 2)
		teamHeader:css("text-align", "right")

		homeRow:node(teamHeader)


		for _, awayCode in ipairs(teamOrder) do

			if homeCode == awayCode then
				local diagonal = cell("td", "—", "football-diagonal")
				diagonal:attr("rowspan", 2)
				homeRow:node(
					diagonal
				)

			else

				local homeResultCell =
					mw.html.create("td")
				local awayResultCell =
					mw.html.create("td")

				local homeFound = false
				local awayFound = false

				-- Нужно ли поменять местами гостевую и домашнюю игры
				local changeOrder = false
				-- Ищем ВСЕ матчи между командами
				for _, match in ipairs(matches) do

					if match.home == homeCode and
					   match.away == awayCode then

						homeFound = true

						local text

						if match.played then
							text = match.score
							if match.homeGoals > match.awayGoals then
								homeResultCell
									:addClass("ui")
									:addClass("green")
							elseif match.homeGoals < match.awayGoals then
								homeResultCell
									:addClass("ui")
									:addClass("red")
							else
								homeResultCell
									:addClass("ui")
									:addClass("yellow")
							end
						elseif match.status ~= "" then
							text = match.status
						else
							text = "—"
						end

						homeResultCell
							:wikitext(text)
							:done()

						-- Если режим отображения по кругам, и игра была во втором круге, то меняем местами гостевую и домашнюю
						if resultsMode == 1 and match.round == "2" then
							changeOrder = true
						end

					end

					if match.home == awayCode and
					   match.away == homeCode then

						awayFound = true

						local text

						if match.played then
							text = match.score
							if match.homeGoals < match.awayGoals then
								awayResultCell
									:addClass("ui")
									:addClass("green")
							elseif match.homeGoals > match.awayGoals then
								awayResultCell
									:addClass("ui")
									:addClass("red")
							else
								awayResultCell
									:addClass("ui")
									:addClass("yellow")
							end
						elseif match.status ~= "" then
							text = match.status
						else
							text = "—"
						end

						awayResultCell
							:wikitext(text)
							:done()

						-- Если режим отображения по кругам, и игра была в первом круге, то меняем местами гостевую и домашнюю
						if resultsMode == 1 and match.round == "1" then
							changeOrder = true
						end
					end

					if homeFound and awayFound then
						break
					end
				end


				if not homeFound then
					homeResultCell:wikitext("—")
				end
				if not awayFound then
					awayResultCell:wikitext("—")
				end

				if changeOrder then
					homeRow:node(awayResultCell)
					awayRow:node(homeResultCell)
				else
					homeRow:node(homeResultCell)
					awayRow:node(awayResultCell)
				end

			end

		end

		resultsTable:node(homeRow)
		resultsTable:node(awayRow)

	end


	root:node(resultsTable)


	-- =====================================================
	-- ПРОГРЕСС ПО ТУРАМ
	-- =====================================================

	local showProgress = trim(args.show_progress) == "yes"

	local progress
	local tourNumbers

	if showProgress then

		progress, tourNumbers =
			buildProgress()


		root:wikitext(heading("Положение команд по турам"))


		-- Без номеров туров строить нечего
		if #tourNumbers == 0 then

			root:node(
				mw.html.create("div")
					:addClass("football-info")
					:wikitext(
						"''Нет матчей с номером тура. " ..
						"Для этой таблицы матчи нужно указывать " ..
						"в формате «тур;круг;хозяева;гости;счёт».''"
					)
			)

			showProgress = false

		end

	end


	if showProgress then

		local progressTable =
			mw.html.create("table")
				:addClass("wikitable")
				:addClass("football-progress")
				:css("text-align", "center")


		-- -------------------------------------------------
		-- Цвета мест
		-- -------------------------------------------------

		local progressHighlights = {}

		for i = 1, 10 do

			local value =
				trim(args["progress_highlight" .. i])

			if value ~= "" then

				local parts =
					split(value, ";")

				local place =
					tonumber(parts[1])

				local color =
					parts[2] or ""

				if place and color ~= "" then

					progressHighlights[place] =
						color

				end

			end

		end


		-- -------------------------------------------------
		-- Сноски о снятии очков: тур → команда → сноски
		-- -------------------------------------------------

		local progressNotes = {}

		for _, deduction in ipairs(deductions) do

			local tourNumber = tonumber(deduction.match.tour)

			if tourNumber and deduction.note then

				progressNotes[tourNumber] =
					progressNotes[tourNumber] or {}

				local teamNotes =
					progressNotes[tourNumber][deduction.team] or {}

				table.insert(teamNotes, deduction.note)

				progressNotes[tourNumber][deduction.team] =
					teamNotes

			end

		end


		-- -------------------------------------------------
		-- Заголовок
		-- -------------------------------------------------

		local progressHeader =
			mw.html.create("tr")

		progressHeader:node(
			cell("th", "Команда")
				:attr("rowspan", 2)
		)

		progressHeader:node(
			cell("th", "Тур")
				:attr("colspan", #tourNumbers)
		)

		progressTable:node(progressHeader)

		local tourHeader =
			mw.html.create("tr")

		for _, tourNumber in ipairs(tourNumbers) do

			tourHeader:node(
				cell(
					"th",
					tourNumber
				)
			)

		end

		progressTable:node(tourHeader)


		-- -------------------------------------------------
		-- Строки команд
		-- -------------------------------------------------

		for _, code in ipairs(teamOrder) do

			local row =
				mw.html.create("tr")


			row:node(
				cell(
					"th",
					teams[code].name
				)
			)


			for _, tourNumber in ipairs(tourNumbers) do

				local position =
					progress[tourNumber][code]

				local resultCell =
					cell("td", position)


				local color =
					progressHighlights[position]

				if color and color ~= "" then

					resultCell
						:addClass("highlight")
						:addClass(color)

				end


				-- В туре, где сняты очки, — та же сноска,
				-- что и в итоговой таблице
				local cellNotes =
					progressNotes[tourNumber] and
					progressNotes[tourNumber][code]

				if cellNotes then

					resultCell:addClass("football-deduction")

					for _, note in ipairs(cellNotes) do
						resultCell:node(
							noteMarker(note, false)
						)
					end

				end


				row:node(resultCell)

			end


			progressTable:node(row)

		end


		root:node(progressTable)

	end

	return tostring(root)
end


return p