Module:MatchGroup/Util/Custom
From Liquipedia Warcraft Wiki
---
-- @Liquipedia
-- page=Module:MatchGroup/Util/Custom
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--
local Lua = require('Module:Lua')
local Array = Lua.import('Module:Array')
local Faction = Lua.import('Module:Faction')
local Logic = Lua.import('Module:Logic')
local Operator = Lua.import('Module:Operator')
local String = Lua.import('Module:StringUtils')
local Table = Lua.import('Module:Table')
local MatchGroupUtil = Lua.import('Module:MatchGroup/Util')
local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util')
local Opponent = Lua.import('Module:Opponent/Custom')
local TEAM_DISPLAY_MODE = 'team'
local UNIFORM_DISPLAY_MODE = 'uniform'
local SCORE_STATUS = MatchGroupInputUtil.STATUS.SCORE
local CustomMatchGroupUtil = Table.deepCopy(MatchGroupUtil)
---@class WarcraftMatchGroupUtilGamePlayer: standardPlayer
---@field matchplayerIndex integer
---@field heroes string[]?
---@field position integer
---@field random boolean
---@class WarcraftMatchGroupUtilGameOpponent:GameOpponent
---@field placement number?
---@field players WarcraftMatchGroupUtilGamePlayer[]
---@field score number?
---@class WarcraftMatchGroupUtilGame: MatchGroupUtilGame
---@field opponents WarcraftMatchGroupUtilGameOpponent[]
---@field offfactions table<integer, string[]>?
---@class WarcraftMatchGroupUtilVeto
---@field by number?
---@field map string
---@class WarcraftMatchGroupUtilSubmatch
---@field games WarcraftMatchGroupUtilGame[]
---@field mode string
---@field opponents WarcraftMatchGroupUtilGameOpponent[]
---@field status string?
---@field subgroup number
---@field winner number?
---@field header string?
---@class WarcraftMatchGroupUtilMatch: MatchGroupUtilMatch
---@field games WarcraftMatchGroupUtilGame[]
---@field opponentMode 'uniform'|'team'
---@field opponents standardOpponent[]
---@field vetoes WarcraftMatchGroupUtilVeto[]
---@field submatches WarcraftMatchGroupUtilSubmatch[]?
---@field casters string?
---@param record table
---@return WarcraftMatchGroupUtilMatch
function CustomMatchGroupUtil.matchFromRecord(record)
local match = MatchGroupUtil.matchFromRecord(record) --[[@as WarcraftMatchGroupUtilMatch]]
-- Add additional fields to opponents
CustomMatchGroupUtil.populateOpponents(match)
-- Adjust game.opponents by looking up game.opponents.players in match.opponents
Array.forEach(match.games, function(game)
game.opponents = CustomMatchGroupUtil.computeGameOpponents(game, match.opponents)
end)
-- Determine whether the match is a team match with different players each game
match.opponentMode = Array.any(match.opponents, function(opponent) return opponent.type == Opponent.team end)
and TEAM_DISPLAY_MODE or UNIFORM_DISPLAY_MODE
local extradata = match.extradata
---@cast extradata table
if match.opponentMode == TEAM_DISPLAY_MODE then
-- Compute submatches
match.submatches = Array.map(
CustomMatchGroupUtil.groupBySubmatch(match.games),
function(games) return CustomMatchGroupUtil.constructSubmatch(games, match) end
)
end
-- Add vetoes
match.vetoes = {}
for vetoIndex = 1, math.huge do
local map = Table.extract(extradata, 'veto' .. vetoIndex)
local by = tonumber(Table.extract(extradata, 'veto' .. vetoIndex .. 'by'))
if not map then break end
table.insert(match.vetoes, {map = map, by = by})
end
return match
end
---Move additional fields from extradata to struct
---@param match WarcraftMatchGroupUtilMatch
function CustomMatchGroupUtil.populateOpponents(match)
local opponents = match.opponents
for _, opponent in ipairs(opponents) do
opponent.placement2 = tonumber(Table.extract(opponent.extradata, 'placement2'))
opponent.score2 = tonumber(Table.extract(opponent.extradata, 'score2'))
opponent.status2 = opponent.score2 and SCORE_STATUS or nil
for _, player in ipairs(opponent.players) do
player.faction = Table.extract(player.extradata, 'faction') or Faction.defaultFaction
end
end
if #opponents == 2 and opponents[1].score2 and opponents[2].score2 then
local scoreDiff = opponents[1].score2 - opponents[2].score2
opponents[1].placement2 = scoreDiff > 0 and 1 or 2
opponents[2].placement2 = scoreDiff < 0 and 1 or 2
end
end
---@param game WarcraftMatchGroupUtilGame
---@param matchOpponents standardOpponent[]
---@return WarcraftMatchGroupUtilGameOpponent[]
function CustomMatchGroupUtil.computeGameOpponents(game, matchOpponents)
return Array.map(game.opponents, function(mapOpponent, opponentIndex)
local players = Array.map(mapOpponent.players or {}, function(player, playerIndex)
if Logic.isEmpty(player) then return end
local matchPlayer = (matchOpponents[opponentIndex].players or {})[playerIndex] or {}
return Table.merge({displayName = 'TBD'}, matchPlayer, {
faction = player.faction,
position = tonumber(player.position),
heroes = player.heroes,
random = player.random,
matchPlayerIndex = playerIndex,
})
end)
return Table.merge(mapOpponent, {players = players})
end)
end
---Group games on the subgroup field to form submatches
---@param matchGames WarcraftMatchGroupUtilGame[]
---@return WarcraftMatchGroupUtilGame[][]
function CustomMatchGroupUtil.groupBySubmatch(matchGames)
-- Group games on adjacent subgroups
local previousSubgroup = nil
local currentGames = nil
local submatchGames = {}
for _, game in ipairs(matchGames) do
if previousSubgroup == nil or previousSubgroup ~= game.subgroup then
currentGames = {}
table.insert(submatchGames, currentGames)
previousSubgroup = game.subgroup
end
---@cast currentGames -nil
table.insert(currentGames, game)
end
return submatchGames
end
---Constructs a submatch object whose properties are aggregated from that of its games.
---@param games WarcraftMatchGroupUtilGame[]
---@param match WarcraftMatchGroupUtilMatch
---@return WarcraftMatchGroupUtilSubmatch
function CustomMatchGroupUtil.constructSubmatch(games, match)
local firstGame = games[1]
local opponents = Table.deepCopy(firstGame.opponents)
local isSubmatch = String.startsWith(firstGame.map or '', 'Submatch')
if isSubmatch then
games = {firstGame}
end
---@param opponent table
---@param opponentIndex integer
local getOpponentScoreAndStatus = function(opponent, opponentIndex)
local statuses = Array.unique(Array.map(games, function(game)
return game.opponents[opponentIndex].status
end))
opponent.status = #statuses == 1 and statuses[1] ~= SCORE_STATUS and statuses[1] or SCORE_STATUS
opponent.score = isSubmatch and opponent.score or Array.reduce(Array.map(games, function(game)
return (game.winner == opponentIndex and 1 or 0)
end), Operator.add)
end
Array.forEach(opponents, getOpponentScoreAndStatus)
local allPlayed = Array.all(games, function (game)
return game.winner ~= nil or game.status == 'notplayed'
end)
local winner = allPlayed and MatchGroupInputUtil.getWinner('', nil, opponents) or nil
Array.forEach(opponents, function(opponent, opponentIndex)
opponent.placement = MatchGroupInputUtil.placementFromWinner('', winner, opponentIndex)
end)
--check the faction of the players
Array.forEach(opponents, function(_, opponentIndex)
CustomMatchGroupUtil._determineSubmatchPlayerFactions(match, games, opponents, opponentIndex)
end)
return {
games = games,
mode = firstGame.mode,
opponents = opponents,
subgroup = firstGame.subgroup,
winner = winner,
header = Table.extract(match.extradata or {}, 'subgroup' .. firstGame.subgroup .. 'header'),
}
end
---@param match WarcraftMatchGroupUtilMatch
---@param games WarcraftMatchGroupUtilGame[]
---@param opponents WarcraftMatchGroupUtilGameOpponent[]
---@param opponentIndex integer
function CustomMatchGroupUtil._determineSubmatchPlayerFactions(match, games, opponents, opponentIndex)
local opponent = opponents[opponentIndex]
local playerFactions = {}
Array.forEach(games, function(game)
for playerIndex, player in pairs(game.opponents[opponentIndex].players) do
playerFactions[playerIndex] = playerFactions[playerIndex] or {}
playerFactions[playerIndex][player.faction] = true
end
end)
local toFaction = function(playerIndex, player)
local isRandom = Array.any(games, function(game)
return game.opponents[opponentIndex].players[playerIndex].random
end)
if isRandom then return Faction.read('r') end
local faction = Table.uniqueKey(playerFactions[playerIndex])
if faction then return faction end
if Table.isNotEmpty(playerFactions[playerIndex]) then
return Faction.read('m')
end
local matchPlayer = match.opponents[opponentIndex].players[player.matchplayerIndex]
return matchPlayer and matchPlayer.faction or Faction.defaultFaction
end
for playerIndex, player in pairs(opponent.players) do
player.faction = toFaction(playerIndex, player)
end
end
---Determines if any player in an opponent is not playing their main faction by comparing them to a reference opponent.
---Returns the factions played if at least one player chose an offfaction or nil if otherwise.
---@param gameOpponent WarcraftMatchGroupUtilGameOpponent
---@param referenceOpponent standardOpponent|WarcraftMatchGroupUtilGameOpponent
---@return string[]?
function CustomMatchGroupUtil.computeOfffactions(gameOpponent, referenceOpponent)
local gameFactions = {}
local hasOfffaction = false
for playerIndex, gamePlayer in ipairs(gameOpponent.players) do
local referencePlayer = referenceOpponent.players[playerIndex]
table.insert(gameFactions, gamePlayer.faction)
hasOfffaction = hasOfffaction or gamePlayer.faction ~= referencePlayer.faction
end
return hasOfffaction and gameFactions or nil
end
return CustomMatchGroupUtil