Module:TournamentStructure
From Liquipedia Commons Wiki
--- -- @Liquipedia -- wiki=commons -- page=Module:TournamentStructure -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- local Array = require('Module:Array') local FnUtil = require('Module:FnUtil') local Namespace = require('Module:Namespace') local String = require('Module:StringUtils') local Table = require('Module:Table') local TypeUtil = require('Module:TypeUtil') local FULL_PAGENAME = mw.title.getCurrentTitle().prefixedText local TournamentStructure = {types = {}} TournamentStructure.types.MatchGroupsSpec = TypeUtil.struct{ matchGroupIds = TypeUtil.array('string'), pageNames = TypeUtil.array('string'), } --- Fetches match groups and GroupTableLeague data point records grouped by tournament stage ---@param spec {matchGroupIds: table, pageNames: table} ---@return table function TournamentStructure.fetchStages(spec) return TournamentStructure.groupByStage( TournamentStructure.fetchGroupTables(spec), TournamentStructure.fetchBrackets(spec), spec ) end --- Extracts a match group spec from an arguments table. The match group spec is formed using tournamentX= --- and matchGroupIdX= params, and is used by LPDB fetch functions to know which match groups to fetch. --- Namespace and tournament stage are suppored for pages names. Namespace is supported for match group IDs. --- Example of template arguments: --- |tournament1=PiG Sty Festival --- |tournament2=StayAtHome Story Cup/4#Group Stage 2 --- |tournament3=User:(16thSq) Kuro/Master Swan Open/64 --- |matchGroupId1=Z1lDMZPiGA --- |matchGroupId2=Liquipedia_wnbxUh4Vm1 ---@param args table ---@return table? function TournamentStructure.readMatchGroupsSpec(args) local matchGroupIds = {args.id} table.insert(matchGroupIds, args.matchGroupId) for _, id in Table.iter.pairsByPrefix(args, 'matchGroupId') do table.insert(matchGroupIds, id) end local pageNames = {} table.insert(pageNames, args.tournament) for _, pageName in Table.iter.pairsByPrefix(args, 'tournament') do table.insert(pageNames, pageName) end local function resolve(rawPageName) local namespaceName, basePageName, stageName = TournamentStructure._splitPageName(rawPageName) -- args.ns is deprecated if not namespaceName and args.ns then namespaceName = Namespace.nameFromId(args.ns) end local pageName = String.isNotEmpty(basePageName) and TournamentStructure._createPageName(namespaceName, basePageName) or FULL_PAGENAME local redirectedPage = mw.title.new(TournamentStructure._resolveRedirect(pageName)) redirectedPage.fragment = stageName or '' assert(redirectedPage, 'Invalid page name "' .. pageName .. '"') return redirectedPage.fullText end if #matchGroupIds ~= 0 or #pageNames ~= 0 then return { matchGroupIds = matchGroupIds, pageNames = Array.map(pageNames, resolve), } else return nil end end ---@return {matchGroupIds: {}, pageNames: {[1]: string}} function TournamentStructure.currentPageSpec() return { matchGroupIds = {}, pageNames = {FULL_PAGENAME}, } end TournamentStructure._resolveRedirect = FnUtil.memoize(function(pageName) return mw.ext.TeamLiquidIntegration.resolve_redirect(pageName) end) --- Sorts given group tables and brackets for a given spec into stages --- --- Limitations: --- - Stages cannot span multiple tournament pages --- - The stageName field (as specified by {{Stage|...}}) --- - A stage is contigious within a page --- - A stage cannot consist of both matchlists and brackets --- --- Stages are ordered by the match2bracketindex page variable. For stages originating from different pages, --- the tournamentX arg determines the ordering of pages, hence stage order. ---@param groupTables table ---@param brackets table ---@param spec {matchGroupIds: table, pageNames: table} ---@return table function TournamentStructure.groupByStage(groupTables, brackets, spec) local function getStageKey(recordGroup) return { recordGroup[1].stageIndex or -1, recordGroup[1].pagename, TournamentStructure.getStageName(recordGroup) or 'default', TournamentStructure.isGroupTable(recordGroup) and 'groupTable' or 'bracket' } end local stageIndexes = Table.map(spec.pageNames, function(stageIndex, pageName) return pageName, stageIndex end) local currentPage = FULL_PAGENAME stageIndexes[currentPage] = stageIndexes[currentPage] or 1000 local getSortKey = FnUtil.memoize(function(recordGroup) -- gsub needed to match how pagenames are set up in spec via `TournamentStructure.readMatchGroupsSpec` local basePageName = recordGroup[1].pagename:gsub('_', ' ') local stageName = TournamentStructure.getStageName(recordGroup) local namespaceName = String.nilIfEmpty(Namespace.nameFromId(recordGroup[1].namespace)) local pageName = TournamentStructure._createPageName(namespaceName, basePageName, stageName) local wholePageName = TournamentStructure._createPageName(namespaceName, basePageName) local stageIndex = recordGroup[1].stageIndex or stageIndexes[pageName] or stageIndexes[wholePageName] return TournamentStructure.isGroupTable(recordGroup) and { stageIndex or -1, recordGroup[1].extradata.bracketIndex or -1, 0, tonumber(recordGroup[1].standingsindex) or -1, } or { stageIndex or -1, tonumber((recordGroup[1].match2bracketdata or {}).bracketindex) or -1, 1, 0, } end) local recordGroups = Array.extend(groupTables, brackets) recordGroups = Array.filter(recordGroups, function(recordGroup) return Table.isNotEmpty(recordGroup) end) Array.sortInPlaceBy(recordGroups, getSortKey) return Array.groupAdjacentBy(recordGroups, getStageKey) end --- Checks if a given data set "recordGroup" is a group table (standings table) or not ---@param recordGroup table ---@return boolean function TournamentStructure.isGroupTable(recordGroup) return recordGroup[1].standingsindex ~= nil end --- Retrieves the stage name from a data set (either bracket or standings table) ---@param recordGroup table ---@return string? function TournamentStructure.getStageName(recordGroup) return TournamentStructure.isGroupTable(recordGroup) and recordGroup[1].extradata.stageName or String.nilIfEmpty((recordGroup[1].match2bracketdata or {}).sectionheader) end --- Builds a filter (condition string) for a given matchGroupId ---@param matchGroupId string ---@return string function TournamentStructure.getMatchGroupFilter(matchGroupId) local namespaceName = matchGroupId:match('^(%w+)_') local clauses = Array.extend( namespaceName and ('[[namespace::' .. Namespace.idFromName(namespaceName) .. ']]') or nil, '[[match2bracketid::' .. matchGroupId .. ']]' ) return table.concat(clauses, ' AND ') end --- Builds a filter (condition string) for a given matchGroupType and pageName ---@param matchGroupType string ---@param pageName string ---@return string function TournamentStructure.getPageNameFilter(matchGroupType, pageName) local namespaceName, basePageName, stageName = TournamentStructure._splitPageName(pageName) local clauses = Array.extend( namespaceName and ('[[namespace::' .. Namespace.idFromName(namespaceName) .. ']]') or nil, ('[[pagename::' .. basePageName:gsub('%s', '_') .. ']]'), stageName and (matchGroupType == 'bracket') and ('[[match2bracketdata_sectionheader::' .. stageName .. ']]') or nil, stageName and (matchGroupType == 'standingstable') and ('[[extradata_stagename::' .. stageName .. ']]') or nil ) return table.concat(clauses, ' AND ') end --- Fetches brackets (matches) for a given filter (condition string). ---@param filter string ---@return table function TournamentStructure.fetchBracketsFromFilter(filter) return mw.ext.LiquipediaDB.lpdb('match2', { conditions = filter .. ' AND [[match2bracketdata_type::bracket]]', limit = 5000, }) end --- Fetches groups (standings tables) for a given filter (condition string). ---@param filter string ---@return table function TournamentStructure.fetchGroupsFromFilter(filter) return mw.ext.LiquipediaDB.lpdb('standingstable', { query = 'namespace, pagename, standingsindex, title, extradata, matches, type, config', conditions = filter, limit = 5000, }) end --- Fetch group table results from standings table. ---@param spec {matchGroupIds: table, pageNames: table} ---@return table function TournamentStructure.fetchGroupTables(spec) local pageData = Array.flatten(Array.map(spec.pageNames, function(pageName, groupIndex) return Array.map( TournamentStructure.fetchGroupsFromFilter(TournamentStructure.getPageNameFilter('standingstable', pageName)), function(standingsGroup) standingsGroup.stageIndex = groupIndex return standingsGroup end) end)) local groups = Array.filter(pageData, Table.isNotEmpty) groups = Array.map(groups, TournamentStructure.fetchGroupTableEntries) return groups end --- Fetches standings entries belonging to a given group (standings table) ---@param group table ---@return table function TournamentStructure.fetchGroupTableEntries(group) local groupExtradata = group.extradata or {} local roundIndex = groupExtradata.roundcount if not roundIndex then return {} end local records = mw.ext.LiquipediaDB.lpdb('standingsentry', { conditions = '[[standingsindex::' .. group.standingsindex .. ']] AND ' .. '[[pagename::' .. group.pagename .. ']] AND [[roundindex::' .. roundIndex .. ']]', limit = '100', query = 'scoreboard, currentstatus, extradata, opponenttype, ' .. 'opponentname, opponenttemplate, opponentplayers, placement' }) local sortFunction = function(record1, record2) local value1 = tonumber(record1.extradata.slotindex) or tonumber(record1.placement) or -1 local value2 = tonumber(record2.extradata.slotindex) or tonumber(record2.placement) or -1 return value1 < value2 or value1 == value2 and record1.opponentname < record2.opponentname end table.sort(records, sortFunction) local placeMapping = groupExtradata.placemapping if placeMapping then for _, record in pairs(records) do local sortValue = tonumber(record.extradata.slotindex) or tonumber(record.placement) sortValue = placeMapping[sortValue] or sortValue record.extradata.slotindex = sortValue record.placement = tostring(sortValue) end end return TournamentStructure._mergeGroupEntriesIntoGroup(records, group) end --- Merges groupEntries into a group ---@param entries table ---@param group table ---@return table function TournamentStructure._mergeGroupEntriesIntoGroup(entries, group) local transformedGroup = {} for _, entry in ipairs(entries) do local opponent = require('Module:OpponentLibraries').Opponent.fromLpdbStruct(entry) local finished = group.extradata.finished or group.extradata.groupfinished local extradata = { placeRange = entry.extradata.placerange, placeRangeIsExact = entry.extradata.placerangeisexact, showMatchDraws = (group.config or {}).hasdraws or group.extradata.hasdraw, stageName = group.extradata.stagename, slotIndex = tonumber(entry.extradata.slotindex), groupFinished = finished, finished = finished, enddate = group.extradata.enddate or group.extradata.endtime, endTime = group.extradata.enddate or group.extradata.endtime, bracketIndex = group.extradata.bracketindex, } table.insert(transformedGroup, Table.merge(group, { opponent = opponent, extradata = extradata, scoreboard = entry.scoreboard, currentstatus = entry.currentstatus, matches = group.matches, placement = TournamentStructure._groupPlacement(finished, entry.extradata.slotindex, entry.placement), type = group.type, hasDraw = group.extradata.hasdraw, hasOvertime = group.extradata.hasovertime, })) end return transformedGroup end --- Determines the group placement to be used in further processing depending if the group is finished or not ---@param finished boolean? ---@param slotIndex string|number|nil ---@param placement string|number|nil ---@return number? function TournamentStructure._groupPlacement(finished, slotIndex, placement) if finished then return tonumber(placement) or tonumber(slotIndex) end return tonumber(slotIndex) or tonumber(placement) end --- Converts a match group spec to a standing record filter. Returns a filter string for use in LPDB queries. ---@param spec {matchGroupIds: table, pageNames: table} ---@return string function TournamentStructure.getGroupTableFilter(spec) local whereClauses = Array.map(spec.pageNames, function(pageName) return TournamentStructure.getPageNameFilter('standingstable', pageName) end) return '(' .. table.concat(whereClauses, ' OR ') .. ')' end --- Fetches bracket data (matches) for a given match group spec. ---@param spec {matchGroupIds: table, pageNames: table} ---@return table function TournamentStructure.fetchBrackets(spec) local idData = Array.map(spec.matchGroupIds, function(matchGroupId) return TournamentStructure.fetchBracketsFromFilter(TournamentStructure.getMatchGroupFilter(matchGroupId)) end) local pageData = Array.flatten(Array.map(spec.pageNames, function(pageName, stageIndex) local groupedData = Array.groupBy(Array.map( TournamentStructure.fetchBracketsFromFilter(TournamentStructure.getPageNameFilter('bracket', pageName)), function(bracketMatch) bracketMatch.stageIndex = stageIndex return bracketMatch end), function(record) return record.match2bracketid end) return groupedData end)) return Array.extend(idData, pageData) end --- Converts a match group spec to a match2 record filter. Returns a filter string for use in LPDB queries. ---@param spec {matchGroupIds: table, pageNames: table} ---@return string function TournamentStructure.getMatch2Filter(spec) local whereClauses = Array.extend( Array.map(spec.matchGroupIds, TournamentStructure.getMatchGroupFilter), Array.map(spec.pageNames, function(pageName) return TournamentStructure.getPageNameFilter('bracket', pageName) end) ) return '(' .. table.concat(whereClauses, ' OR ') .. ')' end --- Splits a page name into a namespace, base, and stage. ---@param pageName string ---@return string?, string, string? function TournamentStructure._splitPageName(pageName) local title = mw.title.new(pageName) assert(title, 'Invalid pagename "' .. pageName .. '"') return String.nilIfEmpty(title.nsText), title.text, String.nilIfEmpty(title.fragment) end --- Joins given namespace, base page name, and stage into a page name. ---@param namespaceName string? ---@param basePageName string ---@param stageName string? ---@return string function TournamentStructure._createPageName(namespaceName, basePageName, stageName) if String.isEmpty(basePageName) then return '' end return mw.title.makeTitle(namespaceName or '', basePageName, stageName).fullText end return TournamentStructure