Module:MatchGroup/Util
From Liquipedia Commons Wiki
--- -- @Liquipedia -- wiki=commons -- page=Module:MatchGroup/Util -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- local Array = require('Module:Array') local FnUtil = require('Module:FnUtil') local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local StringUtils = require('Module:StringUtils') local Table = require('Module:Table') local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') local MatchGroupCoordinates = Lua.import('Module:MatchGroup/Coordinates') local WikiSpecific = Lua.import('Module:Brkts/WikiSpecific') local TBD_DISPLAY = '<abbr title="To Be Decided">TBD</abbr>' local nilIfEmpty = StringUtils.nilIfEmpty --[[ Non-display utility functions for brackets, matchlists, matches, opponents, games, and etc in the new bracket framework. Display related functions go in Module:MatchGroup/Display/Helper. ]] local MatchGroupUtil = {types = {}} ---@class MatchGroupUtilLowerEdge ---@field lowerMatchIndex number ---@field opponentIndex number MatchGroupUtil.types.LowerEdge = TypeUtil.struct({ lowerMatchIndex = 'number', opponentIndex = 'number', }) ---@alias AdvanceBg 'up'|'stayup'|'stay'|'staydown'|'down' MatchGroupUtil.types.AdvanceBg = TypeUtil.literalUnion('up', 'stayup', 'stay', 'staydown', 'down') ---@class MatchGroupUtilAdvanceSpot ---@field bg AdvanceBg ---@field matchId string? ---@field type string? MatchGroupUtil.types.AdvanceSpot = TypeUtil.struct({ bg = MatchGroupUtil.types.AdvanceBg, matchId = 'string?', type = TypeUtil.literalUnion('advance', 'custom', 'qualify'), }) ---@class MatchGroupUtilBracketBracketData ---@field coordinates MatchGroupUtilMatchCoordinates ---@field advanceSpots MatchGroupUtilAdvanceSpot[] ---@field bracketResetMatchId string? ---@field header string? ---@field lowerEdges MatchGroupUtilLowerEdge[]? ---@field lowerMatchIds string[] ---@field qualLose boolean? ---@field qualLoseLiteral string? ---@field qualSkip number? ---@field qualWin boolean? ---@field qualWinLiteral string? ---@field skipRound number? ---@field thirdPlaceMatchId string? ---@field title string? ---@field type 'bracket' ---@field upperMatchId string? ---@field matchId string? ---@field qualifiedHeader boolean? MatchGroupUtil.types.BracketBracketData = TypeUtil.struct({ advanceSpots = TypeUtil.array(MatchGroupUtil.types.AdvanceSpot), bracketResetMatchId = 'string?', header = 'string?', inheritedHeader = 'string?', lowerEdges = TypeUtil.array(MatchGroupUtil.types.LowerEdge), lowerMatchIds = TypeUtil.array('string'), qualLose = 'boolean?', qualLoseLiteral = 'string?', qualSkip = 'number?', qualWin = 'boolean?', qualifiedHeader = 'boolean?', qualWinLiteral = 'string?', skipRound = 'number?', thirdPlaceMatchId = 'string?', title = 'string?', type = TypeUtil.literal('bracket'), upperMatchId = 'string?', }) ---@class MatchGroupUtilMatchCoordinates ---@field depth number ---@field depthCount number ---@field matchIndexInRound number ---@field rootIndex number ---@field roundCount number ---@field roundIndex number ---@field sectionCount number ---@field sectionIndex number ---@field semanticDepth number ---@field semanticRoundIndex number MatchGroupUtil.types.MatchCoordinates = TypeUtil.struct({ depth = 'number', depthCount = 'number', matchIndexInRound = 'number', rootIndex = 'number', roundCount = 'number', roundIndex = 'number', sectionCount = 'number', sectionIndex = 'number', semanticDepth = 'number', semanticRoundIndex = 'number', }) ---@class MatchGroupUtilMatchlistBracketData ---@field header string? ---@field title string? ---@field dateHeader boolean? ---@field type 'matchlist' ---@field matchId string? MatchGroupUtil.types.MatchlistBracketData = TypeUtil.struct({ header = 'string?', title = 'string?', dateHeader = 'boolean?', type = TypeUtil.literal('matchlist'), }) ---@alias MatchGroupUtilBracketData MatchGroupUtilMatchlistBracketData|MatchGroupUtilBracketBracketData MatchGroupUtil.types.BracketData = TypeUtil.union( MatchGroupUtil.types.MatchlistBracketData, MatchGroupUtil.types.BracketBracketData ) ---@class standardPlayer ---@field displayName string? ---@field flag string? ---@field pageName string? ---@field team string? ---@field extradata table? ---@field pageIsResolved boolean? MatchGroupUtil.types.Player = TypeUtil.struct({ displayName = 'string?', flag = 'string?', pageName = 'string?', team = 'string?', extradata = 'table?', }) ---@class standardOpponent ---@field advanceBg string? ---@field advances boolean? ---@field icon string? ---@field icondark string? ---@field name string? ---@field placement number? ---@field placement2 number? ---@field players standardPlayer[]? ---@field score number? ---@field score2 number? ---@field status string? ---@field status2 string? ---@field template string? ---@field type OpponentType ---@field team string? MatchGroupUtil.types.Opponent = TypeUtil.struct({ advanceBg = 'string?', advances = 'boolean?', icon = 'string?', name = 'string?', placement = 'number?', placement2 = 'number?', players = TypeUtil.array(MatchGroupUtil.types.Player), score = 'number?', score2 = 'number?', status = 'string?', status2 = 'string?', template = 'string?', type = 'string', }) ---@class GameOpponent ---@field name string? ---@field players standardPlayer[] ---@field template string? ---@field type string MatchGroupUtil.types.GameOpponent = TypeUtil.struct({ name = 'string?', players = TypeUtil.optional(TypeUtil.array(MatchGroupUtil.types.Player)), template = 'string?', type = 'string', }) ---@alias ResultType 'default'|'draw'|'np' MatchGroupUtil.types.ResultType = TypeUtil.literalUnion('default', 'draw', 'np') ---@alias WalkoverType 'L'|'FF'|'DQ' MatchGroupUtil.types.Walkover = TypeUtil.literalUnion('L', 'FF', 'DQ') ---@class MatchGroupUtilGame ---@field comment string? ---@field game string? ---@field header string? ---@field length number? ---@field map string? ---@field mapDisplayName string? ---@field mode string? ---@field participants table ---@field resultType ResultType? ---@field scores number[] ---@field subgroup number? ---@field type string? ---@field vod string? ---@field walkover WalkoverType? ---@field winner integer? ---@field extradata table? MatchGroupUtil.types.Game = TypeUtil.struct({ comment = 'string?', game = 'string?', header = 'string?', length = 'number?', map = 'string?', mapDisplayName = 'string?', mode = 'string?', participants = 'table', resultType = TypeUtil.optional(MatchGroupUtil.types.ResultType), scores = TypeUtil.array('number'), subgroup = 'number?', type = 'string?', vod = 'string?', walkover = TypeUtil.optional(MatchGroupUtil.types.Walkover), winner = 'number?', extradata = 'table?', }) ---@class MatchGroupUtilMatch ---@field bracketData MatchGroupUtilBracketData ---@field comment string? ---@field date string ---@field dateIsExact boolean ---@field finished boolean ---@field game string? ---@field games MatchGroupUtilGame[] ---@field links table ---@field matchId string? ---@field mode string? ---@field opponents standardOpponent[] ---@field resultType ResultType? ---@field stream table ---@field type string? ---@field vod string? ---@field walkover WalkoverType? ---@field winner string? ---@field extradata table? ---@field timestamp number ---@field bestof number? MatchGroupUtil.types.Match = TypeUtil.struct({ bracketData = MatchGroupUtil.types.BracketData, comment = 'string?', date = 'string', dateIsExact = 'boolean', finished = 'boolean', game = 'string?', games = TypeUtil.array(MatchGroupUtil.types.Game), links = 'table', matchId = 'string?', mode = 'string', opponents = TypeUtil.array(MatchGroupUtil.types.Opponent), resultType = 'string?', stream = 'table', type = 'string?', vod = 'string?', walkover = 'string?', winner = 'number?', extradata = 'table?', }) ---@class standardTeamProps ---@field bracketName string ---@field displayName string ---@field pageName string? ---@field shortName string MatchGroupUtil.types.Team = TypeUtil.struct({ bracketName = 'string', displayName = 'string', pageName = 'string?', shortName = 'string', }) ---@class MatchGroupUtilMatchlist ---@field matches MatchGroupUtilMatch[] ---@field matchesById table<string, MatchGroupUtilMatch> ---@field type 'matchlist' MatchGroupUtil.types.Matchlist = TypeUtil.struct({ bracketDatasById = TypeUtil.table('string', MatchGroupUtil.types.BracketData), matches = TypeUtil.array(MatchGroupUtil.types.Match), matchesById = TypeUtil.table('string', MatchGroupUtil.types.Match), type = TypeUtil.literal('matchlist'), }) ---@class MatchGroupUtilBracket ---@field bracketDatasById table<string, MatchGroupUtilBracketBracketData> ---@field coordinatesByMatchId table<string, MatchGroupUtilMatchCoordinates> ---@field matches MatchGroupUtilMatch[] ---@field matchesById table<string, MatchGroupUtilMatch> ---@field rootMatchIds string[] ---@field rounds string[][] ---@field sections string[][] ---@field type 'bracket' MatchGroupUtil.types.Bracket = TypeUtil.struct({ bracketDatasById = TypeUtil.table('string', MatchGroupUtil.types.BracketData), coordinatesByMatchId = TypeUtil.table('string', MatchGroupUtil.types.MatchCoordinates), matches = TypeUtil.array(MatchGroupUtil.types.Match), matchesById = TypeUtil.table('string', MatchGroupUtil.types.Match), rootMatchIds = TypeUtil.array('string'), rounds = TypeUtil.array(TypeUtil.array('string')), sections = TypeUtil.array(TypeUtil.array('string')), type = TypeUtil.literal('bracket'), }) ---@alias MatchGroupUtilMatchGroup MatchGroupUtilBracket|MatchGroupUtilMatchlist MatchGroupUtil.types.MatchGroup = TypeUtil.union( MatchGroupUtil.types.Matchlist, MatchGroupUtil.types.Bracket ) ---Fetches all matches in a matchlist or bracket. Tries to read from page variables before fetching from LPDB. ---Returns a list of records ordered lexicographically by matchId. ---@param bracketId string ---@return table[] function MatchGroupUtil.fetchMatchRecords(bracketId) local varData = Variables.varDefault('match2bracket_' .. bracketId) if varData then return (Json.parse(varData)) end return mw.ext.LiquipediaDB.lpdb( 'match2', { conditions = '([[namespace::0]] or [[namespace::>0]]) AND [[match2bracketid::' .. bracketId .. ']]', order = 'match2id ASC', limit = 5000, } ) end MatchGroupUtil.fetchMatchGroup = FnUtil.memoize(function(bracketId) local matchRecords = MatchGroupUtil.fetchMatchRecords(bracketId) return MatchGroupUtil.makeMatchGroup(matchRecords) end) ---Creates a match group structure from its match records. Returns a value of type MatchGroupUtil.types.MatchGroup. ---@param matchRecords table[] ---@return MatchGroupUtilMatchGroup function MatchGroupUtil.makeMatchGroup(matchRecords) local type = matchRecords[1] and matchRecords[1].match2bracketdata.type or 'matchlist' if type == 'matchlist' then return MatchGroupUtil.makeMatchlistFromRecords(matchRecords) elseif type == 'bracket' then return MatchGroupUtil.makeBracketFromRecords(matchRecords) else error('Invalid match2bracketdata.type: ' .. type .. '. Expected matchlist or bracket.') end end ---@param matchRecords table[] ---@return MatchGroupUtilMatchlist function MatchGroupUtil.makeMatchlistFromRecords(matchRecords) local matches = Array.map(matchRecords, WikiSpecific.matchFromRecord) local matchesById = Table.map(matches, function(_, match) return match.matchId, match end) local bracketDatasById = Table.mapValues(matchesById, function(match) return match.bracketData end) return { bracketDatasById = bracketDatasById, matches = matches, matchesById = matchesById, type = 'matchlist', } end ---@param matchRecords table[] ---@return MatchGroupUtilBracket function MatchGroupUtil.makeBracketFromRecords(matchRecords) local matches = Array.map(matchRecords, WikiSpecific.matchFromRecord) --[[@as MatchGroupUtilMatch[] ]] local matchesById = Table.map(matches, function(_, match) return match.matchId, match end) local bracketDatasById = Table.mapValues(matchesById, function(match) return match.bracketData end) local firstCoordinates = matches[1] and matches[1].bracketData.coordinates if not firstCoordinates then MatchGroupUtil.backfillUpperMatchIds(bracketDatasById) end local bracket = { bracketDatasById = bracketDatasById, coordinatesByMatchId = Table.mapValues(matchesById, function(match) return match.bracketData.coordinates end), matches = matches, matchesById = matchesById, rootMatchIds = MatchGroupUtil.computeRootMatchIds(bracketDatasById), type = 'bracket', } if firstCoordinates then Table.mergeInto(bracket, { rounds = MatchGroupCoordinates.getRoundsFromCoordinates(bracket), sections = MatchGroupCoordinates.getSectionsFromCoordinates(bracket), }) else MatchGroupUtil.backfillCoordinates(bracket) end MatchGroupUtil.populateAdvanceSpots(bracket) return bracket end ---Returns an array of all the IDs of root matches. The matches are sorted in display order. ---@param bracketDatasById table<string, MatchGroupUtilBracketData> ---@return string[] function MatchGroupUtil.computeRootMatchIds(bracketDatasById) -- Matches without upper matches local rootMatchIds = {} for matchId, bracketData in pairs(bracketDatasById) do if not bracketData.upperMatchId and not StringUtils.endsWith(matchId, 'RxMBR') then table.insert(rootMatchIds, matchId) end end Array.sortInPlaceBy(rootMatchIds, function(matchId) local coordinates = bracketDatasById[matchId].coordinates return coordinates and {coordinates.rootIndex} or {-1, matchId} end) return rootMatchIds end ---Populate bracketData.upperMatchId if it is missing. This can happen if the bracket template is missing data. ---@param bracketDatasById table<string, MatchGroupUtilBracketData> function MatchGroupUtil.backfillUpperMatchIds(bracketDatasById) local upperMatchIds = MatchGroupCoordinates.computeUpperMatchIds(bracketDatasById) for matchId, bracketData in pairs(bracketDatasById) do bracketData.upperMatchId = upperMatchIds[matchId] end end ---Populate bracketData.coordinates if it is missing. ---This can happen if the bracket template has not been recently purged. ---@param matchGroup MatchGroupUtilBracket function MatchGroupUtil.backfillCoordinates(matchGroup) local bracketCoordinates = MatchGroupCoordinates.computeCoordinates(matchGroup) Table.mergeInto(matchGroup, bracketCoordinates) for matchId, bracketData in pairs(matchGroup.bracketDatasById) do bracketData.coordinates = bracketCoordinates.coordinatesByMatchId[matchId] end end ---Fetches all matches in a matchlist or bracket. ---Returns a list of structurally typed matches lexicographically ordered by matchId. ---@param bracketId string ---@return MatchGroupUtilMatch[] function MatchGroupUtil.fetchMatches(bracketId) return MatchGroupUtil.fetchMatchGroup(bracketId).matches end ---Returns a match struct for use in a bracket display or match summary popup. The bracket display and match summary ---popup expects that the finals match also include results from the bracket reset match. ---@param bracketId string ---@param matchId string ---@return MatchGroupUtilMatch, MatchGroupUtilMatch? function MatchGroupUtil.fetchMatchForBracketDisplay(bracketId, matchId) local bracket = MatchGroupUtil.fetchMatchGroup(bracketId) local match = bracket.matchesById[matchId] local bracketResetMatch = match and match.bracketData.bracketResetMatchId and bracket.matchesById[match.bracketData.bracketResetMatchId] return match, bracketResetMatch end ---Converts a match record to a structurally typed table with the appropriate data types for field values. ---The match record is either a match created in the store bracket codepath (WikiSpecific.processMatch), ---or a record fetched from LPDB (MatchGroupUtil.fetchMatchRecords). ---The returned match struct is used in various display components (Bracket, MatchSummary, etc) --- ---This is the implementation used on wikis by default. Wikis may specify a different conversion by setting ---WikiSpecific.matchFromRecord. Refer to the starcraft2 wiki as an example. ---@param record table ---@return MatchGroupUtilMatch function MatchGroupUtil.matchFromRecord(record) local extradata = MatchGroupUtil.parseOrCopyExtradata(record.extradata) local opponents = Array.map(record.match2opponents, MatchGroupUtil.opponentFromRecord) local bracketData = MatchGroupUtil.bracketDataFromRecord(Json.parseIfString(record.match2bracketdata)) if bracketData.type == 'bracket' then bracketData.lowerEdges = bracketData.lowerEdges or MatchGroupUtil.autoAssignLowerEdges(#bracketData.lowerMatchIds, #opponents) end return { bestof = tonumber(record.bestof) or 0, bracketData = bracketData, comment = nilIfEmpty(Table.extract(extradata, 'comment')), extradata = extradata, date = record.date, dateIsExact = Logic.readBool(record.dateexact), finished = Logic.readBool(record.finished), game = record.game, games = Array.map(record.match2games, MatchGroupUtil.gameFromRecord), links = Json.parseIfString(record.links) or {}, matchId = record.match2id, liquipediatier = record.liquipediatier, liquipediatiertype = record.liquipediatiertype, mode = record.mode, opponents = opponents, parent = record.parent, resultType = nilIfEmpty(record.resulttype), stream = Json.parseIfString(record.stream) or {}, timestamp = tonumber(Table.extract(extradata, 'timestamp')), tournament = record.tournament, type = nilIfEmpty(record.type) or 'literal', vod = nilIfEmpty(record.vod), walkover = nilIfEmpty(record.walkover), winner = tonumber(record.winner), } end ---@param data table? ---@return MatchGroupUtilBracketData function MatchGroupUtil.bracketDataFromRecord(data) if not data then return {} end if data.type == 'bracket' then local advanceSpots = data.advancespots or MatchGroupUtil.computeAdvanceSpots(data) return { advanceSpots = advanceSpots, bracketResetMatchId = nilIfEmpty(data.bracketreset), coordinates = data.coordinates and MatchGroupUtil.indexTableFromRecord(data.coordinates), header = nilIfEmpty(data.header), inheritedHeader = nilIfEmpty(data.inheritedheader), lowerEdges = data.loweredges and Array.map(data.loweredges, MatchGroupUtil.indexTableFromRecord), lowerMatchIds = data.lowerMatchIds or MatchGroupUtil.computeLowerMatchIdsFromLegacy(data), qualifiedHeader = nilIfEmpty(data.qualifiedheader), qualLose = advanceSpots[2] and advanceSpots[2].type == 'qualify', qualLoseLiteral = nilIfEmpty(data.qualloseLiteral), qualSkip = tonumber(data.qualskip) or data.qualskip == 'true' and 1 or 0, qualWin = advanceSpots[1] and advanceSpots[1].type == 'qualify', qualWinLiteral = nilIfEmpty(data.qualwinLiteral), skipRound = tonumber(data.skipround) or data.skipround == 'true' and 1 or 0, thirdPlaceMatchId = nilIfEmpty(data.thirdplace), type = 'bracket', upperMatchId = nilIfEmpty(data.upperMatchId), } else return { dateHeader = nilIfEmpty(data.dateheader), header = nilIfEmpty(data.header), inheritedHeader = nilIfEmpty(data.inheritedheader), matchIndex = nilIfEmpty(data.matchIndex), title = nilIfEmpty(data.title), type = 'matchlist', } end end ---@param bracketData MatchGroupUtilBracketData ---@return table function MatchGroupUtil.bracketDataToRecord(bracketData) local coordinates = bracketData.coordinates return { bracketreset = bracketData.bracketResetMatchId, bracketsection = coordinates and MatchGroupUtil.sectionIndexToString(coordinates.sectionIndex, coordinates.sectionCount), coordinates = coordinates and MatchGroupUtil.indexTableToRecord(coordinates), header = bracketData.header, lowerMatchIds = bracketData.lowerMatchIds, loweredges = bracketData.lowerEdges and Array.map(bracketData.lowerEdges, MatchGroupUtil.indexTableToRecord), quallose = bracketData.qualLose and 'true' or nil, qualloseLiteral = bracketData.qualLoseLiteral, qualskip = bracketData.qualSkip ~= 0 and bracketData.qualSkip or nil, qualwin = bracketData.qualWin and 'true' or nil, qualwinLiteral = bracketData.qualWinLiteral, skipround = bracketData.skipRound ~= 0 and bracketData.skipRound or nil, thirdplace = bracketData.thirdPlaceMatchId, tolower = bracketData.lowerMatchIds[#bracketData.lowerMatchIds], toupper = bracketData.lowerMatchIds[#bracketData.lowerMatchIds - 1], type = bracketData.type, upperMatchId = bracketData.upperMatchId, } end ---@param record table ---@return standardOpponent function MatchGroupUtil.opponentFromRecord(record) local extradata = MatchGroupUtil.parseOrCopyExtradata(record.extradata) return { advanceBg = nilIfEmpty(Table.extract(extradata, 'bg')), advances = Logic.readBoolOrNil(Table.extract(extradata, 'advances')), extradata = extradata, icon = nilIfEmpty(record.icon), name = nilIfEmpty(record.name), placement = tonumber(record.placement), players = Array.map(record.match2players, MatchGroupUtil.playerFromRecord), score = tonumber(record.score), status = record.status, template = nilIfEmpty(record.template), type = nilIfEmpty(record.type) or 'literal', } end ---@param args table ---@return table function MatchGroupUtil.createOpponent(args) return { extradata = args.extradata or {}, icon = args.icon, name = args.name, placement = args.placement, players = args.players or {}, score = args.score, status = args.status, template = args.template, type = args.type or 'literal', } end ---@param record table ---@return standardPlayer function MatchGroupUtil.playerFromRecord(record) local extradata = MatchGroupUtil.parseOrCopyExtradata(record.extradata) return { displayName = record.displayname, extradata = extradata, flag = nilIfEmpty(record.flag), pageName = record.name, } end ---@param record table ---@return MatchGroupUtilGame function MatchGroupUtil.gameFromRecord(record) local extradata = MatchGroupUtil.parseOrCopyExtradata(record.extradata) return { comment = nilIfEmpty(Table.extract(extradata, 'comment')), date = record.date, extradata = extradata, game = record.game, header = nilIfEmpty(Table.extract(extradata, 'header')), length = record.length, map = nilIfEmpty(record.map), mapDisplayName = nilIfEmpty(Table.extract(extradata, 'displayname')), mode = nilIfEmpty(record.mode), participants = Json.parseIfString(record.participants) or {}, resultType = nilIfEmpty(record.resulttype), scores = Json.parseIfString(record.scores) or {}, subgroup = tonumber(record.subgroup), type = nilIfEmpty(record.type), vod = nilIfEmpty(record.vod), walkover = nilIfEmpty(record.walkover), winner = tonumber(record.winner), } end ---@param data table ---@return string[] function MatchGroupUtil.computeLowerMatchIdsFromLegacy(data) local lowerMatchIds = {} if nilIfEmpty(data.toupper) then table.insert(lowerMatchIds, data.toupper) end if nilIfEmpty(data.tolower) then table.insert(lowerMatchIds, data.tolower) end return lowerMatchIds end ---Auto compute lower edges, which encode the connector lines between lower matches and this match. ---@param lowerMatchCount integer ---@param opponentCount integer ---@return {lowerMatchIndex: integer, opponentIndex: integer}[] function MatchGroupUtil.autoAssignLowerEdges(lowerMatchCount, opponentCount) local lowerEdges = {} if lowerMatchCount <= opponentCount then -- More opponents than lower matches: connect lower matches to opponents near the middle. local skip = math.ceil((opponentCount - lowerMatchCount) / 2) for lowerMatchIndex = 1, lowerMatchCount do table.insert(lowerEdges, { lowerMatchIndex = lowerMatchIndex, opponentIndex = lowerMatchIndex + skip, }) end else -- More lower matches than opponents: The excess lower matches are all connected to the final opponent. for lowerMatchIndex = 1, lowerMatchCount do table.insert(lowerEdges, { lowerMatchIndex = lowerMatchIndex, opponentIndex = math.min(lowerMatchIndex, opponentCount), }) end end return lowerEdges end ---Computes just the advance spots that can be determined from a match bracket data. ---More are found in populateAdvanceSpots. ---@param data table ---@return table<1|2, {bg: string, type: string, matchId: string}> function MatchGroupUtil.computeAdvanceSpots(data) local advanceSpots = {} if data.upperMatchId then advanceSpots[1] = {bg = 'up', type = 'advance', matchId = data.upperMatchId} end if nilIfEmpty(data.winnerto) then advanceSpots[1] = {bg = 'up', type = 'custom', matchId = data.winnerto} end if nilIfEmpty(data.loserto) then advanceSpots[2] = {bg = 'stayup', type = 'custom', matchId = data.loserto} end if Logic.readBool(data.qualwin) then advanceSpots[1] = Table.merge(advanceSpots[1], {bg = 'up', type = 'qualify'}) end if Logic.readBool(data.quallose) then advanceSpots[2] = Table.merge(advanceSpots[2], {bg = 'stayup', type = 'qualify'}) end return advanceSpots end ---@param bracket MatchGroupUtilBracket function MatchGroupUtil.populateAdvanceSpots(bracket) if #bracket.matches == 0 then return end -- Loser of semifinals play in third place match local firstBracketData = bracket.bracketDatasById[bracket.rootMatchIds[1]] local thirdPlaceMatchId = firstBracketData.thirdPlaceMatchId if thirdPlaceMatchId and bracket.matchesById[thirdPlaceMatchId] then for _, lowerMatchId in ipairs(firstBracketData.lowerMatchIds) do local bracketData = bracket.bracketDatasById[lowerMatchId] bracketData.advanceSpots[2] = bracketData.advanceSpots[2] or {bg = 'stayup', type = 'advance', matchId = thirdPlaceMatchId} end end -- Custom advance spots set via pbg params for _, match in ipairs(bracket.matches) do local pbgs = Array.mapIndexes(function(ix) return Table.extract(match.extradata, 'pbg' .. ix) end) for i = 1, #pbgs do match.bracketData.advanceSpots[i] = Table.merge( match.bracketData.advanceSpots[i], {bg = pbgs[i], type = 'custom'} ) end end end ---Merges a grand finals match with results of its bracket reset match. ---@param match table ---@param bracketResetMatch table ---@return table function MatchGroupUtil.mergeBracketResetMatch(match, bracketResetMatch) local mergedMatch = Table.merge(match, { opponents = {}, games = Table.copy(match.games), }) for ix, opponent in ipairs(match.opponents) do local resetOpponent = bracketResetMatch.opponents[ix] mergedMatch.opponents[ix] = Table.merge(opponent, { score2 = resetOpponent.score, status2 = resetOpponent.status, placement2 = resetOpponent.placement, }) end for _, game in ipairs(bracketResetMatch.games) do table.insert(mergedMatch.games, game) end return mergedMatch end ---Fetches information about a team via mw.ext.TeamTemplate. ---@param template string ---@return table? function MatchGroupUtil.fetchTeam(template) --exception for TBD opponents if string.lower(template) == 'tbd' then return { bracketName = TBD_DISPLAY, displayName = TBD_DISPLAY, pageName = 'TBD', shortName = TBD_DISPLAY, } end local rawTeam = mw.ext.TeamTemplate.raw(template) if not rawTeam then return nil end return { bracketName = rawTeam.bracketname, displayName = rawTeam.name, pageName = rawTeam.page, shortName = rawTeam.shortname, } end ---Parse extradata as a JSON string if read from page variables. Otherwise create a copy if fetched from lpdb. ---The returned extradata table can then be mutated without altering the source. ---@param recordExtradata table|string? ---@return table function MatchGroupUtil.parseOrCopyExtradata(recordExtradata) return type(recordExtradata) == 'string' and Json.parse(recordExtradata) or type(recordExtradata) == 'table' and Table.copy(recordExtradata) or {} end ---Convert 0-based indexes to 1-based ---@param record table ---@return table function MatchGroupUtil.indexTableFromRecord(record) return Table.map(record, function(key, value) if key:match('Index') and type(value) == 'number' then return key, value + 1 else return key, value end end) end ---Convert 1-based indexes to 0-based ---@param coordinates table ---@return table function MatchGroupUtil.indexTableToRecord(coordinates) return Table.map(coordinates, function(key, value) if key:match('Index') and type(value) == 'number' then return key, value - 1 else return key, value end end) end ---@param sectionIndex integer ---@param sectionCount integer ---@return string function MatchGroupUtil.sectionIndexToString(sectionIndex, sectionCount) if sectionIndex == 1 then return 'upper' elseif sectionIndex == sectionCount then return 'lower' else return 'mid' end end ---Splits a matchId like h5HXaqbSVP_R02-M002 into the bracket ID h5HXaqbSVP and the base match ID R02-M002. ---@param matchId string ---@return string?, string? function MatchGroupUtil.splitMatchId(matchId) return matchId:match('^(.-)_([%w-]+)$') end ---Converts R01-M003 to R1M3 ---@param matchId string ---@return string function MatchGroupUtil.matchIdToKey(matchId) if matchId == 'RxMBR' or matchId == 'RxMTP' then return matchId end local round, matchInRound = matchId:match('^R(%d+)%-M(%d+)$') return 'R' .. tonumber(round) .. 'M' .. tonumber(matchInRound) end ---Converts R1M3 to R01-M003 ---@param matchKey string ---@return string function MatchGroupUtil.matchIdFromKey(matchKey) if matchKey == 'RxMBR' or matchKey == 'RxMTP' then return matchKey end local round, matchInRound = matchKey:match('^R(%d+)M(%d+)$') if round and matchInRound then -- Bracket format return 'R' .. string.format('%02d', round) .. '-M' .. string.format('%03d', matchInRound) else -- Matchlist format return string.format('%04d', matchKey) end end return MatchGroupUtil