Module:Opponent

From Liquipedia Commons Wiki

Documentation (view - edit)

Module:Opponent/doc


---
-- @Liquipedia
-- wiki=commons
-- page=Module:Opponent
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

local Array = require('Module:Array')
local Flags = require('Module:Flags')
local Logic = require('Module:Logic')
local Lua = require('Module:Lua')
local String = require('Module:StringUtils')
local Table = require('Module:Table')
local TeamTemplate = require('Module:TeamTemplate')
local TypeUtil = require('Module:TypeUtil')

local PlayerExt = Lua.import('Module:Player/Ext/Custom')

local BYE = 'bye'

--[[
Structural type representation of an opponent.

Examples:
{type = Opponent.solo, players = {displayName = 'Neeb'}}
{type = Opponent.team, template = 'alpha x 2020'}
{type = Opponent.literal, name = 'B2'}

See Opponent.types.Opponent for the exact encoding scheme and required fields.

- For opponent display components, see Module:OpponentDisplay.
- For input from wikicode, use {{1Opponent|...}}, {{TeamOpponent|...}} etc.
- Used by: PrizePool, GroupTableLeague, match2 Matchlist/Bracket,
StarcraftMatchSummary
- Wikis may add additional wiki-specific fields to the opponent representation.

]]
local Opponent = {types = {}}

---@enum OpponentType
local OpponentTypes = {
	team = 'team',
	solo = 'solo',
	duo = 'duo',
	trio = 'trio',
	quad = 'quad',
	literal = 'literal',
}

Opponent.team = OpponentTypes.team
Opponent.solo = OpponentTypes.solo
Opponent.duo = OpponentTypes.duo
Opponent.trio = OpponentTypes.trio
Opponent.quad = OpponentTypes.quad
Opponent.literal = OpponentTypes.literal

Opponent.partyTypes = {Opponent.solo, Opponent.duo, Opponent.trio, Opponent.quad}
Opponent.types = Array.extend(Opponent.partyTypes, {Opponent.team, Opponent.literal}) --[[@as table]]

---@enum PartySize
Opponent.partySizes = {
	solo = 1,
	duo = 2,
	trio = 3,
	quad = 4,
}

Opponent.types.Player = TypeUtil.struct({
	displayName = 'string',
	flag = 'string?',
	pageName = 'string?',
	team = 'string?',
})

Opponent.types.TeamOpponent = TypeUtil.struct({
	template = 'string',
	type = TypeUtil.literal(Opponent.team),
})

Opponent.types.PartyOpponent = TypeUtil.struct({
	players = TypeUtil.array(Opponent.types.Player),
	type = TypeUtil.literalUnion(unpack(Opponent.partyTypes)),
})

Opponent.types.LiteralOpponent = TypeUtil.struct({
	name = 'string',
	type = TypeUtil.literal(Opponent.literal),
})

Opponent.types.Opponent = TypeUtil.union(
	Opponent.types.TeamOpponent,
	Opponent.types.PartyOpponent,
	Opponent.types.LiteralOpponent
)

---Checks if the provided opponent type is a party type
---@param type OpponentType?
---@return boolean
function Opponent.typeIsParty(type)
	return Opponent.partySizes[type] ~= nil
end

---Returns the player count for a party type, or nil otherwise.
---
---example: Opponent.partySize(Opponent.duo) == 2
---@param type OpponentType?
---@return PartySize?
function Opponent.partySize(type)
	return Opponent.partySizes[type]
end

---Creates a blank literal opponent, or a blank opponent of the specified type
---@param type OpponentType?
---@return standardOpponent
function Opponent.blank(type)
	if type == Opponent.team then
		return {type = type, template = 'tbd'}
	elseif Opponent.typeIsParty(type) then
		local partySize = Opponent.partySize(type) --[[@as integer]]
		return {
			type = type,
			players = Array.map(
				Array.range(1, partySize),
				function(_) return {displayName = ''} end
			),
		}
	else
		return {type = Opponent.literal, name = ''}
	end
end

---Creates a blank TBD opponent, or a TBD opponent of the specified type
---@param type OpponentType?
---@return standardOpponent
function Opponent.tbd(type)
	if type == Opponent.team then
		return {type = type, template = 'tbd'}
	elseif Opponent.typeIsParty(type) then
		local partySize = Opponent.partySize(type) --[[@as integer]]
		return {
			type = type,
			players = Array.map(
				Array.range(1, partySize),
				function(_) return {displayName = 'TBD'} end
			),
		}
	else
		return {type = Opponent.literal, name = 'TBD'}
	end
end

---Checks whether an opponent is TBD
---@param opponent standardOpponent
---@return boolean
function Opponent.isTbd(opponent)
	if opponent.type == Opponent.team then
		return opponent.template == 'tbd'

			-- The following can't occur in valid opponents, but we check for them anyway
			or opponent.name == 'TBD'
			or String.isEmpty(opponent.template)

	elseif opponent.type == Opponent.literal then
		return true

	else
		return Array.any(opponent.players, Opponent.playerIsTbd)
	end
end

---Checks if an opponent is empty
---@param opponent standardOpponent?
---@return boolean
function Opponent.isEmpty(opponent)
	-- if no type is set consider opponent as empty
	return not opponent or not opponent.type
		-- if neither name nor template nor players are set consider the opponent as empty
		or (String.isEmpty(opponent.name) and String.isEmpty(opponent.template) and Logic.isDeepEmpty(opponent.players))
end

---Checks whether an opponent is a BYE Opponent
---@param opponent standardOpponent
---@return boolean
function Opponent.isBye(opponent)
	return string.lower(opponent.name or '') == BYE
		or string.lower(opponent.template or '') == BYE
end

---Checks if a player is a TBD player
---@param player standardPlayer
---@return boolean
function Opponent.playerIsTbd(player)
	return String.isEmpty(player.displayName) or player.displayName:upper() == 'TBD'
end

---Checks if a provided string is an opponent type
---@param type string
---@return boolean
function Opponent.isType(type)
	return Table.includes(Opponent.types, type)
end

---Reads an opponent type.
---If an invalid entry is given returns nil.
---@param type string
---@return OpponentType?
function Opponent.readType(type)
	return Table.includes(Opponent.types, type) and type or nil
end

---Asserts that an arbitary value is a valid representation of an opponent
---@param opponent any
function Opponent.assertOpponent(opponent)
	assert(Opponent.isOpponent(opponent), 'Invalid opponent')
end

---Coerces an arbitary table into an opponent
---@param opponent table
function Opponent.coerce(opponent)
	assert(type(opponent) == 'table')

	opponent.type = Opponent.isType(opponent.type) and opponent.type or Opponent.literal
	if opponent.type == Opponent.literal then
		opponent.name = type(opponent.name) == 'string' and opponent.name or ''
	elseif opponent.type == Opponent.team then
		if String.isEmpty(opponent.template) or type(opponent.template) ~= 'string' then
			opponent.template = 'tbd'
		end
	else
		if type(opponent.players) ~= 'table' then
			opponent.players = {}
		end
		local partySize = Opponent.partySize(opponent.type)
		opponent.players = Array.sub(opponent.players, 1, partySize)
		for _, player in ipairs(opponent.players) do
			if type(player.displayName) ~= 'string' then
				player.displayName = ''
			end
		end
		for i = #opponent.players + 1, partySize do
			opponent.players[i] = {displayName = ''}
		end
	end
end

--[[
Returns the match mode for two or more opponent types.

Example:

Opponent.toMode(Opponent.duo, Opponent.duo) == '2_2'
]]
---@param ... OpponentType
---@return string
function Opponent.toMode(...)
	local modeParts = Array.map(arg, function(opponentType)
		return Opponent.partySize(opponentType) or opponentType
	end)
	return table.concat(modeParts, '_')
end

--[[
Returns the legacy match mode for two or more opponent types.

Used by LPDB placement and tournament records, and smw records.

Example:

Opponent.toLegacyMode(Opponent.duo, Opponent.duo) == '2v2'
]]
---@param ... OpponentType
---@return string
function Opponent.toLegacyMode(...)
	local modeParts = Array.map(arg, function(opponentType)
		return Opponent.partySize(opponentType) or opponentType
	end)
	local mode = table.concat(modeParts, 'v')
	if mode == 'teamvteam' then
		return Opponent.team
	else
		return mode
	end
end

--[[
Resolves the identifiers of an opponent.

For team opponents, this resolves the team template to a particular date. For
party opponents, this fills in players' pageNames using their displayNames,
using data stored in page variables if present.

options.syncPlayer: Whether to fetch player information from variables or LPDB. Disabled by default.
]]
---@param opponent standardOpponent
---@param date string|number|nil
---@param options {syncPlayer: boolean?}?
---@return standardOpponent
function Opponent.resolve(opponent, date, options)
	options = options or {}
	if opponent.type == Opponent.team then
		opponent.template = TeamTemplate.resolve(opponent.template, date) or opponent.template or 'tbd'
		opponent.icon, opponent.icondark = TeamTemplate.getIcon(opponent.template)
	elseif Opponent.typeIsParty(opponent.type) then
		for _, player in ipairs(opponent.players) do
			if options.syncPlayer then
				local savePageVar = not Opponent.playerIsTbd(player)
				PlayerExt.syncPlayer(player, {savePageVar = savePageVar})
				player.team = PlayerExt.syncTeam(
					player.pageName:gsub(' ', '_'),
					player.team,
					{date = date, savePageVar = savePageVar}
				)
			else
				PlayerExt.populatePageName(player)
			end
			if player.team then
				player.team = TeamTemplate.resolve(player.team, date)
			end
		end
	end
	return opponent
end

--[[
Converts a opponent to a name. The name is the same as the one used in the
match2opponent.name field.

Returns nil if the team template does not exist.
]]
---@param opponent standardOpponent
---@return string
function Opponent.toName(opponent)
	if opponent.type == Opponent.team then
		return TeamTemplate.getPageName(opponent.template)
	elseif Opponent.typeIsParty(opponent.type) then
		local pageNames = Array.map(opponent.players, function(player)
			return player.pageName or player.displayName
		end)
		return table.concat(pageNames, ' / ')
	else -- opponent.type == Opponent.literal
		return opponent.name
	end
end

--[[
Parses an argument table of an Opponent input template into an opponent struct.
Returns nil if the input is invalid.

Opponent input templates include Template:TeamOpponent, Template:SoloOpponent,
Template:LiteralOpponent, and etc.

Wikis sometimes provide variants of this function that include wiki specific
transformations.
]]
---@param args table
---@return standardOpponent
function Opponent.readOpponentArgs(args)
	local partySize = Opponent.partySize(args.type)

	if args.type == Opponent.team then
		local template = args.template or args[1]
		return template and {
			type = Opponent.team,
			template = template,
		}

	elseif partySize == 1 then
		local player = {
			displayName = args[1] or args.p1 or args.name or '',
			flag = String.nilIfEmpty(Flags.CountryName(args.flag or args.p1flag)),
			pageName = args.link or args.p1link,
			team = args.team or args.p1team,
		}
		return {type = Opponent.solo, players = {player}}

	elseif partySize then
		local players = Array.map(Array.range(1, partySize), function(playerIndex)
			local playerTeam = args['p' .. playerIndex .. 'team']
			if playerTeam then
				playerTeam = playerTeam
			end
			return {
				displayName = args[playerIndex] or args['p' .. playerIndex] or '',
				flag = String.nilIfEmpty(Flags.CountryName(args['p' .. playerIndex .. 'flag'])),
				pageName = args['p' .. playerIndex .. 'link'],
				team = playerTeam,
			}
		end)
		return {type = args.type, players = players}

	elseif args.type == Opponent.literal then
		return {type = Opponent.literal, name = args.name or args[1] or ''}

	end
	error("Unknown opponent type: " .. args.type)
end

--[[
Creates an opponent struct from a match2opponent record. Returns nil if
unsuccessful.

Wikis sometimes provide variants of this function that include wiki specific
transformations.
]]
---@param record table
---@return standardOpponent
function Opponent.fromMatch2Record(record)
	if record.type == Opponent.team then
		return {type = Opponent.team, template = record.template}

	elseif Opponent.typeIsParty(record.type) then
		return {
			type = record.type,
			players = Array.map(record.match2players, function(playerRecord)
				return {
					displayName = playerRecord.displayname,
					flag = String.nilIfEmpty(Flags.CountryName(playerRecord.flag)),
					pageName = String.nilIfEmpty(playerRecord.name),
				}
			end),
		}

	elseif record.type == Opponent.literal then
		return {type = Opponent.literal, name = record.name or ''}

	end
	error("Unknown opponent type: " .. record.type)
end

---Reads an opponent struct and builds a standings/placement lpdb struct from it
---@param opponent standardOpponent
---@return {opponentname: string, opponenttemplate: string?, opponenttype: OpponentType, opponentplayers: table?}
function Opponent.toLpdbStruct(opponent)
	local storageStruct = {
		opponentname = Opponent.toName(opponent),
		opponenttemplate = opponent.template,
		opponenttype = opponent.type,
	}

	-- Add players for Party Type opponents.
	-- Team's will have their players added via the TeamCard.
	if Opponent.typeIsParty(opponent.type) then
		local players = {}
		for playerIndex, player in ipairs(opponent.players) do
			local prefix = 'p' .. playerIndex

			players[prefix] = player.pageName
			players[prefix .. 'dn'] = player.displayName
			players[prefix .. 'flag'] = player.flag
			players[prefix .. 'team'] = player.team and
				Opponent.toName({type = Opponent.team, template = player.team, players = {}}) or
				nil
			players[prefix .. 'template'] = player.team
		end
		storageStruct.opponentplayers = players
	end

	return storageStruct
end

---Reads a standings or placement lpdb structure and builds an opponent struct from it
---@param storageStruct table
---@return standardOpponent
function Opponent.fromLpdbStruct(storageStruct)
	local partySize = Opponent.partySize(storageStruct.opponenttype)
	if partySize then
		local players = storageStruct.opponentplayers
		local function playerFromLpdbStruct(playerIndex)
			return {
				displayName = players['p' .. playerIndex .. 'dn'],
				flag = Flags.CountryName(players['p' .. playerIndex .. 'flag']),
				pageName = players['p' .. playerIndex],
				team = players['p' .. playerIndex .. 'team'],
			}
		end
		local opponent = {
			players = Array.map(Array.range(1, partySize), playerFromLpdbStruct),
			type = storageStruct.opponenttype,
		}
		return opponent
	elseif storageStruct.opponenttype == Opponent.team then
		return {
			name = storageStruct.opponentname,
			template = storageStruct.opponenttemplate,
			type = Opponent.team,
		}
	elseif storageStruct.opponenttype == Opponent.literal then
		return {
			name = storageStruct.opponentname,
			type = Opponent.literal,
		}
	end
	error("Unknown opponent type: " .. storageStruct.type)
end

return Opponent