Перейти к содержанию

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

Убрано добавление гостевых результатов в шахматке
Нет описания правки
 
(не показано 47 промежуточных версий этого же участника)
Строка 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
-- Части параметра start_КОД (стартовые показатели команды)
-- в порядке записи. label — обозначение части
-- в описании формата и в сообщениях об ошибках.
local START_PARTS = {
{ key = "played", label = "И" },
{ key = "wins", label = "В" },
{ key = "draws", label = "Н" },
{ key = "losses", label = "П" },
{ key = "goalsFor", label = "МЗ" },
{ key = "goalsAgainst", label = "МП" },
{ key = "points", label = "О" },
{ key = "yellow", label = "ЖК", card = true },
{ key = "yellowred", label = "2ЖК", card = true },
{ key = "red", label = "КК", card = true }
}
-- Формат параметра start_КОД: «И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК»
local START_FORMAT
do
local labels = {}
for _, part in ipairs(START_PARTS) do
table.insert(labels, part.label)
end
START_FORMAT = table.concat(labels, ";")
end


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


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


Строка 84: Строка 202:
status = "ошибка: " .. score
status = "ошибка: " .. score
}
}
end
-- =========================================================
-- РАЗБОР ЗАПИСИ О МАТЧЕ
-- =========================================================
-- Значение matchN или h2h_matchN в одном из форматов:
-- тур;круг;хозяева;гости;счёт
-- круг;хозяева;гости;счёт
-- хозяева;гости;счёт
-- Возвращает nil, если частей меньше трёх.
local function parseMatchValue(value)
local parts = split(value, ";")
-- Формат с номером тура
if #parts >= 5 then
return {
tour = parts[1],
round = parts[2],
home = parts[3],
away = parts[4],
score = parts[5]
}
end
-- Формат с номером круга
if #parts == 4 then
return {
tour = "",
round = parts[1],
home = parts[2],
away = parts[3],
score = parts[4]
}
end
-- Формат без номера круга
if #parts == 3 then
return {
tour = "",
round = "",
home = parts[1],
away = parts[2],
score = parts[3]
}
end
return nil
end
end


Строка 104: Строка 272:
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
-- Добавляет к показателям команды стартовые
-- (параметр start_КОД), если они указаны
local function addStart(team, start)
if not start then
return
end
team.played = team.played + start.played
team.wins = team.wins + start.wins
team.draws = team.draws + start.draws
team.losses = team.losses + start.losses
team.goalsFor =
team.goalsFor + start.goalsFor
team.goalsAgainst =
team.goalsAgainst + start.goalsAgainst
team.points =
team.points + start.points
for _, cardType in ipairs(CARD_TYPES) do
team.matchCards[cardType.key] =
team.matchCards[cardType.key] + start[cardType.key]
end
end
end


Строка 127: Строка 345:


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: Строка 386:
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: Строка 443:
table.insert(teamOrder, code)
table.insert(teamOrder, code)
end
end
end
-- -----------------------------------------------------
-- СТАРТОВЫЕ ПОКАЗАТЕЛИ
-- -----------------------------------------------------
-- start_КОД = И;В;Н;П;МЗ;МП;О;ЖК;2ЖК;КК — показатели,
-- с которыми команда начинает турнир. Нужны, например,
-- когда после первого этапа таблица делится на группы:
-- таблица каждой группы начинается с итогов первого этапа.
-- Пустая часть считается нулём; пустые И и О вычисляются
-- по победам, ничьим и поражениям. Параметры для кодов,
-- которых нет в teams, не читаются.
-- Возвращает показатели (nil, если параметр не указан)
-- или nil и текст ошибки
local function parseStart(code)
local paramName = "start_" .. code
local raw = trim(args[paramName])
if raw == "" then
return nil
end
local function fail(text)
return nil,
"'''Ошибка:''' в параметре ''" ..
paramName .. "'' " .. text
end
local parts = split(raw, ";")
if #parts > #START_PARTS then
return fail(
"больше " .. #START_PARTS .. " значений. " ..
"Формат: ''" .. START_FORMAT .. "''."
)
end
local start = {
-- Указаны ли карточки
hasCards = false
}
for index, part in ipairs(START_PARTS) do
local value = parts[index] or ""
if part.key == "points" then
-- Очки могут быть дробными и отрицательными:
-- «12,5», «-3» (пустые вычисляются ниже)
if value ~= "" then
local normalized =
string.gsub(value, ",", ".")
normalized =
string.gsub(normalized, "−", "-")
local amount = tonumber(normalized)
if amount == nil or
  amount ~= amount or
  math.abs(amount) == math.huge then
return fail(
"очки (О) должны быть числом, " ..
"а указано «" .. value .. "»."
)
end
start.points = amount
end
elseif value == "" then
-- Пустое число игр вычисляется ниже
if part.key ~= "played" then
start[part.key] = 0
end
elseif value:match("^%d+$") then
start[part.key] = tonumber(value)
if part.card then
start.hasCards = true
end
else
return fail(
"значение " .. part.label ..
" должно быть целым неотрицательным числом, " ..
"а указано «" .. value .. "»."
)
end
end
local results =
start.wins + start.draws + start.losses
if start.played == nil then
start.played = results
elseif start.played ~= results then
return fail(
"число игр (" .. start.played .. ") не равно " ..
"сумме побед, ничьих и поражений (" ..
results .. ")."
)
end
if start.points == nil then
start.points =
start.wins * pointsWin +
start.draws * pointsDraw +
start.losses * pointsLoss
end
return start
end
for _, code in ipairs(teamOrder) do
local start, errorText = parseStart(code)
if errorText then
return errorText
end
if start then
teams[code].start = start
addStart(teams[code], start)
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: Строка 721:


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


Строка 204: Строка 738:
end
end


local parts = split(value, ";")
local fields = parseMatchValue(value)


local round
if not fields then
local home
local away
local score
 
-- Новый формат:
-- круг;хозяева;гости;счёт
if #parts >= 4 then
 
round = trim(parts[1])
home = trim(parts[2])
away = trim(parts[3])
score = trim(parts[4])
 
-- Старый формат:
-- хозяева;гости;счёт
elseif #parts >= 3 then
 
round = ""
home = trim(parts[1])
away = trim(parts[2])
score = trim(parts[3])
 
else


return "'''Ошибка:''' матч " ..
return "'''Ошибка:''' матч " ..
Строка 236: Строка 747:


end
end
local tour = fields.tour
local round = fields.round
local home = fields.home
local away = fields.away
local score = fields.score




Строка 263: Строка 780:
local match = {
local match = {
number = matchNumber,
number = matchNumber,
tour = tour,
round = round,
round = round,


Строка 273: Строка 791:
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)
Строка 284: Строка 921:


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
-- МАТЧИ ДЛЯ ЛИЧНЫХ ВСТРЕЧ
-- =====================================================
 
-- h2h_matchN — матчи, сыгранные до этого турнира (например,
-- в первом этапе), в тех же форматах, что и matchN.
-- Они учитываются только в критерии «личные» и не влияют
-- на показатели команд, шахматку и таблицу по турам.
-- Матчи с командами, которых нет в teams, пропускаются,
-- поэтому можно вставить все матчи первого этапа.
 
local h2hMatches = {}
 
local h2hNumber = 1
 
while true do


awayTeam.goalsAgainst =
local paramName = "h2h_match" .. h2hNumber
awayTeam.goalsAgainst + parsed.homeGoals
local value = trim(args[paramName])


if value == "" then
break
end


if parsed.homeGoals > parsed.awayGoals then
local fields = parseMatchValue(value)


homeTeam.wins = homeTeam.wins + 1
if not fields then
awayTeam.losses = awayTeam.losses + 1


homeTeam.points =
return "'''Ошибка:''' параметр ''" ..
homeTeam.points + pointsWin
paramName ..
"'' имеет неправильный формат."


awayTeam.points =
end
awayTeam.points + pointsLoss


local parsed = parseScore(fields.score)


elseif parsed.homeGoals < parsed.awayGoals then
if parsed.played and
  teams[fields.home] and
  teams[fields.away] then


awayTeam.wins = awayTeam.wins + 1
table.insert(h2hMatches, {
homeTeam.losses = homeTeam.losses + 1
home = fields.home,
away = fields.away,
homeGoals = parsed.homeGoals,
awayGoals = parsed.awayGoals
})


awayTeam.points =
end
awayTeam.points + pointsWin


homeTeam.points =
h2hNumber = h2hNumber + 1
homeTeam.points + pointsLoss
end




else
-- =====================================================
-- КАРТОЧКИ КОМАНД
-- =====================================================
 
-- Параметры 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


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


homeTeam.points =
end
homeTeam.points + pointsDraw


awayTeam.points =
for _, cardType in ipairs(CARD_TYPES) do
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 or
  (team.start and team.start.hasCards) then
hasCards = true
end


matchNumber = matchNumber + 1
end
end


Строка 366: Строка 1062:
-- =====================================================
-- =====================================================


-- Возвращает статистику команды только в матчах
-- Добавляет сыгранный матч к статистике личных встреч
-- против команд из указанной группы.
-- команды code, если обе команды матча входят в группу.
local function addHeadToHeadMatch(result, code, group, match)


local function headToHead(code, group)
if not (group[match.home] and group[match.away]) then
return
end


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


for _, match in ipairs(matches) do
if match.home == code then
scored = match.homeGoals
conceded = match.awayGoals
elseif match.away == code then
scored = match.awayGoals
conceded = match.homeGoals
else
return
end


if match.played and
result.goalsFor =
  group[match.home] and
result.goalsFor + scored
  group[match.away] then


if match.home == code then
result.goalsAgainst =
result.goalsAgainst + conceded


result.goalsFor =
if scored > conceded then
result.goalsFor + match.homeGoals


result.goalsAgainst =
result.points =
result.goalsAgainst + match.awayGoals
result.points + pointsWin


if match.homeGoals > match.awayGoals then
result.wins =
result.wins + 1


result.points =
elseif scored == conceded then
result.points + pointsWin


result.wins =
result.points =
result.wins + 1
result.points + pointsDraw


elseif match.homeGoals == match.awayGoals then
else


result.points =
result.points =
result.points + pointsDraw
result.points + pointsLoss


else
end


result.points =
end
result.points + pointsLoss


end


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


elseif match.away == code then
local function headToHead(code, group, lastTour)


result.goalsFor =
local result = {
result.goalsFor + match.awayGoals
points = 0,
goalsFor = 0,
goalsAgainst = 0,
goalDifference = 0,
wins = 0
}


result.goalsAgainst =
-- Матчи, сыгранные до этого турнира
result.goalsAgainst + match.homeGoals
for _, match in ipairs(h2hMatches) do
addHeadToHeadMatch(result, code, group, match)
end


if match.awayGoals > match.homeGoals then
for _, match in ipairs(matches) do


result.points =
local inRange = true
result.points + pointsWin


result.wins =
if lastTour then
result.wins + 1
local tourNumber = tonumber(match.tour)


elseif match.awayGoals == match.homeGoals then
inRange =
 
tourNumber ~= nil and tourNumber <= lastTour
result.points =
end
result.points + pointsDraw
 
else
 
result.points =
result.points + pointsLoss
 
end
 
end


if match.played and inRange then
addHeadToHeadMatch(result, code, group, match)
end
end


Строка 497: Строка 1199:
-- -----------------------------------------------------
-- -----------------------------------------------------


local function compareTeams(a, b)
-- Возвращает функцию сравнения для 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


for _, criterion in ipairs(criteria) do


-- ---------------------------------------------
-- -----------------------------------------
-- ОЧКИ
-- НАЗВАНИЕ
-- ---------------------------------------------
-- -----------------------------------------


if criterion == "очки" then
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


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


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


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


elseif criterion == "разница" then
-- =====================================================
-- НУМЕРАЦИЯ МЕСТ
-- =====================================================


if a.goalDifference ~= b.goalDifference then
-- first_place — номер первого места таблицы. Например,
return a.goalDifference > b.goalDifference
-- в таблице группы за 7–14 места первая строка — 7-е место.
-- Места в highlightN и progress_highlightN указываются
-- в этой же нумерации.
 
local placeOffset = 0
 
local firstPlace = trim(args.first_place)
 
if firstPlace ~= "" then
 
if not firstPlace:match("^%d+$") or
  tonumber(firstPlace) < 1 then
 
return "'''Ошибка:''' параметр ''first_place'' должен " ..
"быть целым положительным числом, а указано «" ..
firstPlace .. "»."
 
end
 
placeOffset = tonumber(firstPlace) - 1
 
end
 
 
-- =====================================================
-- Выделение победителей и проигравших
-- =====================================================
 
-- highlightN = место;количество;класс;текст
 
local highlights = {}
 
-- Есть ли текст хотя бы у одного выделения
local hasHighlightText = false
 
for i = 1, 15 do
 
local hl = trim(args["highlight" .. i])
 
if hl == "" then
break
end
 
local parts = split(hl, ";")
 
local place = tonumber(parts[1])
 
if place then
 
-- Номер строки таблицы с учётом first_place
local row = math.floor(place) - placeOffset
 
local count =
math.max(1, math.floor(tonumber(parts[2]) or 1))
 
-- Зона, которая начинается выше таблицы,
-- обрезается по первой строке
if row < 1 then
count = count + row - 1
row = 1
end
 
-- Зоны целиком за пределами таблицы пропускаются
if count >= 1 and row <= #teamOrder then
 
local highlight = {
place = row,
count = count,
color = parts[3] or "",
-- Текст может сам содержать «;»
text = table.concat(parts, "; ", 4)
}
 
if highlight.text ~= "" then
hasHighlightText = true
end
end


table.insert(highlights, highlight)


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


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


if a.goalsFor ~= b.goalsFor then
end
return a.goalsFor > b.goalsFor
 
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
 
-- rounds = 1 — однокруговой турнир: у каждой команды в шахматке
-- одна строка, в ячейке — единственный матч пары (resultsMode
-- при этом не действует). rounds = 2 или пусто — двухкруговой.
local roundsParameter = trim(args.rounds)
 
if roundsParameter ~= "" and
  roundsParameter ~= "1" and
  roundsParameter ~= "2" then
 
return "'''Ошибка:''' параметр ''rounds'' может быть равен " ..
"1 (однокруговой турнир) или 2 (двухкруговой), " ..
"а указано «" .. roundsParameter .. "»."
 
end
 
local singleRound = roundsParameter == "1"
 
 
-- =====================================================
-- ПРОГРЕСС ПО ТУРАМ
-- =====================================================
 
local function buildProgress()
 
local progress = {}
 
-- Собираем список туров
local tours = {}


for _, match in ipairs(matches) do


-- ---------------------------------------------
local tourNumber = tonumber(match.tour)
-- ПОБЕДЫ
-- ---------------------------------------------


elseif criterion == "победы" then
if tourNumber then
tours[tourNumber] = true
end


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


local tourNumbers = {}


-- ---------------------------------------------
for tourNumber in pairs(tours) do
-- ИГРЫ
table.insert(tourNumbers, tourNumber)
-- ---------------------------------------------
end


elseif criterion == "игры" then
table.sort(tourNumbers)


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


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


-- ---------------------------------------------
for _, code in ipairs(teamOrder) do
-- ЛИЧНЫЕ
currentTeams[code] = newTeam(
-- ---------------------------------------------
code,
teams[code].name
)


elseif criterion == "личные" then
-- Стартовые показатели действуют с первого тура
addStart(currentTeams[code], teams[code].start)
end


-- Определяем всех команд с таким же
-- количеством очков, как у a и b.


local group = {}
for _, tourNumber in ipairs(tourNumbers) do


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


local team = teams[code]
if tonumber(match.tour) == tourNumber then


if team.points == a.points then
if match.played then
group[code] = true
addResult(currentTeams, match)
end
end
-- Снятие очков действует с этого тура
applyDeductions(currentTeams, match)


end
end


local ha = headToHead(a.code, group)
end
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
for _, code in ipairs(teamOrder) do
end
 
local team = currentTeams[code]
 
team.goalDifference =
team.goalsFor - team.goalsAgainst
 
end


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


if ha.wins ~= hb.wins then
-- Таблица после этого тура
return ha.wins > hb.wins
local tourStandings = {}
end


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


-- ---------------------------------------------
table.sort(
-- НАЗВАНИЕ
tourStandings,
-- ---------------------------------------------
makeComparator(currentTeams, tourNumber)
)


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


local aname =
progress[tourNumber] = {}
mw.text.unstrip(a.name)


local bname =
for position, team in ipairs(tourStandings) do
mw.text.unstrip(b.name)


if aname ~= bname then
progress[tourNumber][team.code] =
return aname < bname
position
end


end
end
Строка 618: Строка 1562:




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


return a.code < b.code
end
end


Строка 635: Строка 1577:
end
end


table.sort(standings, compareTeams)
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
 


-- =====================================================
-- =====================================================
Строка 641: Строка 1739:
-- =====================================================
-- =====================================================


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)




Строка 654: Строка 1748:
:addClass("wikitable")
:addClass("wikitable")
:addClass("football-standings")
:addClass("football-standings")
:css("text-align", "center")




Строка 674: Строка 1769:
}
}


for _, heading in ipairs(headings) do
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(
standingsHeader:node(
cell("th", heading)
cell("th", "Повышение или выбывание")
)
)


Строка 692: Строка 1812:
mw.html.create("tr")
mw.html.create("tr")


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


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


-- Название и сноски о снятии очков
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(
row:node(
cell("td", team.name)
teamName
)
)


Строка 706: Строка 1841:
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
Строка 738: Строка 1870:
gd = tostring(team.goalDifference)
gd = tostring(team.goalDifference)
end
end


row:node(
row:node(
Строка 744: Строка 1875:
)
)


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 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




Строка 773: Строка 1946:


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


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


root:addClass("football-tables")
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)


local resultsTitle =
item:tag("span")
mw.html.create("div")
:addClass("mw-cite-backlink")
:addClass("football-title")
:wikitext("[[#" .. note.refId .. "|↑]]")
:wikitext("'''Результаты'''")
 
item:wikitext(" ")
 
item:tag("span")
:addClass("reference-text")
:wikitext(note.text)
 
end
 
root:node(notesBlock)
 
end
 
 
-- =====================================================
-- РЕЗУЛЬТАТЫ
-- =====================================================


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




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




Строка 820: Строка 2026:


-- Матрица результатов
-- Матрица результатов
-- Матч для ячейки шахматки.
-- eitherVenue = false — только матч на поле homeCode,
-- true — матч этой пары на любом поле (однокруговой турнир).
-- Если подходящих матчей несколько, берётся первый сыгранный,
-- если сыгранных нет — первый с пометкой («перенесён» и т. п.),
-- иначе просто первый.
local function findMatch(homeCode, awayCode, eitherVenue)
local found
local foundRank = 0
for _, match in ipairs(matches) do
local suits =
(match.home == homeCode and match.away == awayCode) or
(eitherVenue and
match.home == awayCode and match.away == homeCode)
if suits then
local rank = 1
if match.played then
rank = 3
elseif match.status ~= "" then
rank = 2
end
if rank > foundRank then
found = match
foundRank = rank
end
end
end
return found
end
-- Ячейка шахматки для команды строки rowCode.
-- Счёт записан как «хозяева:гости»,
-- а цвет показывает результат команды строки.
local function makeResultCell(match, rowCode)
local resultCell =
mw.html.create("td")
if not match then
return resultCell:wikitext("—")
end
if match.played then
local scored = match.homeGoals
local conceded = match.awayGoals
if match.away == rowCode then
scored = match.awayGoals
conceded = match.homeGoals
end
if scored > conceded then
resultCell
:addClass("ui")
:addClass("green")
elseif scored < conceded then
resultCell
:addClass("ui")
:addClass("red")
else
resultCell
:addClass("ui")
:addClass("yellow")
end
resultCell:wikitext(match.score)
elseif match.status ~= "" then
resultCell:wikitext(match.status)
else
resultCell:wikitext("—")
end
return resultCell
end
-- Строк у команды: в двухкруговом турнире две
-- (дома и в гостях или 1-й и 2-й круг), в однокруговом — одна
local rowsPerTeam = 2
if singleRound then
rowsPerTeam = 1
end


for _, homeCode in ipairs(teamOrder) do
for _, homeCode in ipairs(teamOrder) do


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


row:node(
local teamHeader =
cell(
cell(
"th",
"th",
Строка 832: Строка 2139:
"football-team-header"
"football-team-header"
)
)
)
 
if rowsPerTeam > 1 then
teamHeader:attr("rowspan", rowsPerTeam)
end
 
teamHeader:css("text-align", "right")
 
homeRow:node(teamHeader)




Строка 839: Строка 2153:
if homeCode == awayCode then
if homeCode == awayCode then


row:node(
local diagonal = cell("td", "—", "football-diagonal")
cell("td", "—", "football-diagonal")
 
if rowsPerTeam > 1 then
diagonal:attr("rowspan", rowsPerTeam)
end
 
homeRow:node(
diagonal
)
 
elseif singleRound then
 
-- Единственный матч пары, на чьём бы поле он ни был
homeRow:node(
makeResultCell(
findMatch(homeCode, awayCode, true),
homeCode
)
)
)


else
else


local resultCell =
local homeMatch = findMatch(homeCode, awayCode, false)
mw.html.create("td")
local awayMatch = findMatch(awayCode, homeCode, false)
 
local homeResultCell = makeResultCell(homeMatch, homeCode)
local awayResultCell = makeResultCell(awayMatch, homeCode)
 
-- Нужно ли поменять местами гостевую и домашнюю игры:
-- в режиме по кругам первая строка — матч 1-го круга,
-- вторая — матч 2-го круга
local changeOrder =
resultsMode == 1 and (
(homeMatch ~= nil and homeMatch.round == "2") or
(awayMatch ~= nil and awayMatch.round == "1")
)
 
if changeOrder then
homeRow:node(awayResultCell)
awayRow:node(homeResultCell)
else
homeRow:node(homeResultCell)
awayRow:node(awayResultCell)
end
 
end
 
end
 
resultsTable:node(homeRow)
 
if rowsPerTeam > 1 then
resultsTable:node(awayRow)
end
 
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, 15 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)
)


local found = false
progressTable:node(progressHeader)


-- Ищем ВСЕ матчи между командами
local tourHeader =
mw.html.create("tr")


for _, match in ipairs(matches) do
for _, tourNumber in ipairs(tourNumbers) do


if match.home == homeCode and
tourHeader:node(
  match.away == awayCode then
cell(
"th",
tourNumber
)
)


found = true
end


local text
progressTable:node(tourHeader)


if match.played then


text = match.score
-- -------------------------------------------------
-- Строки команд
-- -------------------------------------------------


elseif match.status ~= "" then
for _, code in ipairs(teamOrder) do


text = match.status
local row =
mw.html.create("tr")


else


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


end
for _, tourNumber in ipairs(tourNumbers) do


if match.round ~= "" then
-- Место с учётом first_place
local position =
progress[tourNumber][code] + placeOffset


text = text
local resultCell =
cell("td", position)


end


resultCell
local color =
:wikitext(text)
progressHighlights[position]
:tag("br")
 
:done()
if color and color ~= "" then


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


end
end




if not found then
-- В туре, где сняты очки, — та же сноска,
resultCell:wikitext("—")
-- что и в итоговой таблице
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


Строка 900: Строка 2422:
end
end


end


resultsTable:node(row)
progressTable:node(row)


end
end




root:node(resultsTable)
root:node(progressTable)


end


return tostring(root)
return tostring(root)