Skip to content
133 changes: 133 additions & 0 deletions lua/wikis/commons/StageWinningsCalculation.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
-- @Liquipedia
-- page=Module:StageWinningsCalculation
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

local Lua = require('Module:Lua')

local Array = Lua.import('Module:Array')
local Opponent = Lua.import('Module:Opponent/Custom')
local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom')
local TournamentStructure = Lua.import('Module:TournamentStructure')

local Condition = Lua.import('Module:Condition')
local ConditionTree = Condition.Tree
local ConditionNode = Condition.Node
local Comparator = Condition.Comparator
local BooleanOperator = Condition.BooleanOperator
local ColumnName = Condition.ColumnName

local StageWinningsCalculation = {}

---@param props {matchGroupsSpecProps: table<string, string>, startDate: integer?, endDate: integer?, mode: string,
---startValue: number, valuePerWin: number, valueByScore: table<string, number>?}
---@return {opponent: standardOpponent, matchWins: integer, matchLosses: integer, gameWins: integer,
---gameLosses: integer, winnings: number, scoreDetails: table<string, integer>}[]
function StageWinningsCalculation.run(props)
local matches = mw.ext.LiquipediaDB.lpdb('match2', {
conditions = StageWinningsCalculation._buildConditions(props),
query = 'match2opponents, winner',
limit = 5000
})
matches = Array.filter(matches, function(match)
return #match.match2opponents == 2
end)

local byName = {}

Array.forEach(matches, function(match)
match.opponents = Array.map(match.match2opponents, Opponent.fromMatch2Record)
Array.forEach(match.opponents, function(opponent, opponentIndex)
local identifier = Opponent.toName(opponent)
opponent.name = identifier
opponent.score = match.match2opponents[opponentIndex].score
opponent.status = match.match2opponents[opponentIndex].status
byName[identifier] = byName[identifier] or {
opponent = opponent,
scoreDetails = {},
matchWins = 0,
matchLosses = 0,
gameWins = 0,
gameLosses = 0,
winnings = 0,
}
end)

local winnerId = tonumber(match.winner)
if winnerId ~= 1 and winnerId ~= 2 then return end
local loserId = 3 - winnerId

local winner = match.opponents[winnerId]
local loser = match.opponents[loserId]

local winnerScore = OpponentDisplay.InlineScore(winner)
local loserScore = OpponentDisplay.InlineScore(loser)

local score = winnerScore .. '-' .. loserScore
local reversedScore = loserScore .. '-' .. winnerScore

byName[winner.name].scoreDetails[score] = (byName[winner.name].scoreDetails[score] or 0) + 1
byName[loser.name].scoreDetails[reversedScore] = (byName[loser.name].scoreDetails[reversedScore] or 0) + 1

byName[winner.name].matchWins = byName[winner.name].matchWins + 1
byName[loser.name].matchLosses = byName[loser.name].matchLosses + 1

byName[winner.name].gameWins = byName[winner.name].gameWins + (tonumber(winner.score) or 0)
byName[loser.name].gameLosses = byName[loser.name].gameLosses + (tonumber(winner.score) or 0)
byName[winner.name].gameLosses = byName[winner.name].gameLosses + (tonumber(loser.score) or 0)
byName[loser.name].gameWins = byName[loser.name].gameWins + (tonumber(loser.score) or 0)
end)

local opponents = Array.extractValues(byName)

Array.forEach(opponents, function(opponent)
if props.mode == 'matchWins' then
opponent.winnings = props.startValue + opponent.matchWins * props.valuePerWin
return
elseif props.mode == 'gameWins' then
opponent.winnings = props.startValue + opponent.gameWins * props.valuePerWin
return
end
-- case: props.mode == 'scores'
local winnings = props.startValue
for score, count in pairs(opponent.scoreDetails) do
winnings = winnings + (props.valueByScore[score] or 0) * count
end
opponent.winnings = winnings
end)

Array.sortInPlaceBy(opponents, function(opponent)
return {- opponent.winnings, - opponent.matchWins, - opponent.gameWins, Opponent.toName(opponent)}
end)

return opponents

end

---@param props {matchGroupsSpecProps: table<string, string>, startDate: integer?, endDate: integer?}
---@return string
function StageWinningsCalculation._buildConditions(props)
local conditions = ConditionTree(BooleanOperator.all):add{
ConditionNode(ColumnName('finished'), Comparator.eq, '1'),
ConditionNode(ColumnName('status'), Comparator.neq, 'notplayed'),
ConditionNode(ColumnName('winner'), Comparator.neq, ''),
TournamentStructure.getMatch2Filter(
TournamentStructure.readMatchGroupsSpec(props.matchGroupsSpecProps)
or TournamentStructure.currentPageSpec()
),
}

if props.startDate then
conditions:add(ConditionNode(ColumnName('date'), Comparator.ge, props.startDate))
end

if props.endDate then
conditions:add(ConditionNode(ColumnName('date'), Comparator.le, props.endDate))
end

return tostring(conditions)
end

return StageWinningsCalculation
254 changes: 254 additions & 0 deletions lua/wikis/commons/Widget/StageWinnings.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
---
-- @Liquipedia
-- page=Module:Widget/StageWinnings
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

local Lua = require('Module:Lua')

local Array = Lua.import('Module:Array')
local Class = Lua.import('Module:Class')
local Currency = Lua.import('Module:Currency')
local DateExt = Lua.import('Module:Date/Ext')
local FnUtil = Lua.import('Module:FnUtil')
local Logic = Lua.import('Module:Logic')
local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util')
local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom')
local StageWinningsCalculation = Lua.import('Module:StageWinningsCalculation')
local String = Lua.import('Module:StringUtils')
local Table = Lua.import('Module:Table')
local Variables = Lua.import('Module:Variables')

local HtmlWidgets = Lua.import('Module:Widget/Html/All')
local Widget = Lua.import('Module:Widget')
local Widgets = Lua.import('Module:Widget/All')
local WidgetUtil = Lua.import('Module:Widget/Util')

local BASE_CURRENCY = 'USD'

---@class StageWinningProps
---@field tournaments string
---@field ids string?
---@field sdate string|number|osdate|osdateparam?
---@field edate string|number|osdate|osdateparam?
---@field prizeMode 'matchWins'|'gameWins'|'scores'
---@field valueStart number?
---@field valuePerWin number?
---@field n-m number? # n amd m integers
---@field localcurrency string?
---@field width integer?
---@field cutafter integer?
---@field title string?
---@field precision integer?
---@field autoexchange boolean
---@field showMatchWL boolean
---@field showGameWL boolean
---@field showScore boolean

---@class StageWinnings: Widget
---@operator call(table): StageWinnings
---@field props StageWinningProps
local StageWinnings = Class.new(Widget)
StageWinnings.defaultProps = {
tournaments = mw.title.getCurrentTitle().text,
delimiter = ',',
autoexchange = true,
prizeMode = 'matchWins'
}

---@return Widget?
function StageWinnings:render()
local props = self.props

local startDate = DateExt.readTimestamp(props.sdate)
local endDate = DateExt.readTimestamp(props.edate)

local valueByScore = Table.filterByKey(props, function(key)
if key:match('^%d+%-%d+$') ~= nil then
return true
end
local keyParts = Array.parseCommaSeparatedString(key, '-')
return #keyParts == 2 and
Array.all(keyParts, function(keyPart) return Table.includes(MatchGroupInputUtil.STATUS, keyPart) end)
end)
valueByScore = Table.map(valueByScore, function(key, value)
return key, tonumber(value)
end)

assert(props.prizeMode == 'matchWins' or props.prizeMode == 'gameWins' or props.prizeMode == 'scores',
'Invalid prizeMode input')
assert(props.prizeMode ~= 'scores' or Logic.isNotEmpty(valueByScore),
'No values per scores defined')

local opponentList = StageWinningsCalculation.run{
matchGroupsSpecProps = Table.filterByKey(props, function(key)
return String.startsWith(key, 'tournament') or String.startsWith(key, 'matchGroupId')
end) --[[@as table<string, string>]],
startDate = startDate,
endDate = endDate,
mode = props.prizeMode,
valueByScore = valueByScore,
startValue = tonumber(props.valueStart) or 0,
valuePerWin = tonumber(props.valuePerWin) or 0,
}

if Logic.isNotEmpty(props.localcurrency) and Logic.readBool(props.autoexchange) then
Currency.display(props.localcurrency, nil, {setVariables = true})
self.currencyRate = Currency.getExchangeRate{
currency = props.localcurrency,
currencyRate = Variables.varDefault('exchangerate_' .. props.localcurrency:upper()),
date = DateExt.toYmdInUtc(endDate or DateExt.getContextualDateOrNow()),
setVariables = false
}
end

return Widgets.DataTable{
classes = {'prizepooltable', 'collapsed'},
tableCss = {
['text-align'] = 'center',
['margin-top'] = 0,
['margin-bottom'] = 0,
width = 'auto',
},
tableAttributes = {
['data-cutafter'] = (tonumber(props.cutafter) or 5) + 1, -- +1 due to 2nd headerRow
['data-opentext'] = 'Show remaining participants',
['data-closetext'] = 'Hide remaining participants',
},
children = WidgetUtil.collect(
-- first header
HtmlWidgets.Tr{
children = {
HtmlWidgets.Th{
attributes = {colspan = '100%'},
children = {props.title or 'Group Stage Winnings'},
},
},
},
-- second header
self:_headerRow(),
-- rows
Array.map(opponentList, FnUtil.curry(self._row, self))
),
}
end

---@return Widget
function StageWinnings:_headerRow()
local props = self.props
return HtmlWidgets.Tr{
children = WidgetUtil.collect(
HtmlWidgets.Th{
css = {width = 'auto'},
children = {'Participants'},
},
(Logic.readBool(props.showMatchWL) or props.prizeMode == 'matchWins') and HtmlWidgets.Th{
css = {width = 'auto'},
children = {'Matches'},
} or nil,
(Logic.readBool(props.showGameWL) or props.prizeMode == 'gameWins') and HtmlWidgets.Th{
css = {width = 'auto'},
children = {'Games'},
} or nil,
Logic.readBool(props.showScore) and HtmlWidgets.Th{
css = {width = 'auto'},
children = {'Score Details'},
} or nil,
Logic.isNotEmpty(props.localcurrency) and HtmlWidgets.Th{
css = {width = 'auto'},
children = {Currency.display(props.localcurrency)},
} or nil,
(Logic.readBool(props.autoexchange) or Logic.isEmpty(props.localcurrency)) and HtmlWidgets.Th{
css = {width = 'auto'},
children = {Currency.display(BASE_CURRENCY)},
} or nil
),
}
end

---@param data {opponent: standardOpponent, matchWins: integer, matchLosses: integer, gameWins: integer,
---gameLosses: integer, winnings: number, scoreDetails: table<string, integer>}
---@return Widget
function StageWinnings:_row(data)
local props = self.props

local currencyDisplayConfig = {
displaySymbol = true,
formatValue = true,
displayCurrencyCode = false,
formatPrecision = tonumber(props.precision) or 0,
}

return HtmlWidgets.Tr{
children = WidgetUtil.collect(
HtmlWidgets.Td{
css = {['text-align'] = 'left'},
children = {OpponentDisplay.InlineOpponent{opponent = data.opponent}},
},
(Logic.readBool(props.showMatchWL) or props.prizeMode == 'matchWins') and HtmlWidgets.Td{
css = {width = 'auto'},
children = {
data.matchWins,
'-',
data.matchLosses
},
} or nil,
(Logic.readBool(props.showGameWL) or props.prizeMode == 'gameWins') and HtmlWidgets.Td{
css = {width = 'auto'},
children = {
data.gameWins,
'-',
data.gameLosses
},
} or nil,
Logic.readBool(props.showScore) and HtmlWidgets.Td{
css = {['text-align'] = 'left', width = 'auto'},
children = StageWinnings._detailedScores(data.scoreDetails),
} or nil,
Logic.isNotEmpty(props.localcurrency) and HtmlWidgets.Td{
css = {width = 'auto'},
children = {Currency.display(
props.localcurrency,
data.winnings,
currencyDisplayConfig
)},
} or nil,
(Logic.readBool(props.autoexchange) or Logic.isEmpty(props.localcurrency)) and HtmlWidgets.Td{
css = {width = 'auto'},
children = {Currency.display(
BASE_CURRENCY,
data.winnings * (self.currencyRate or 1),
currencyDisplayConfig
)},
} or nil
),
}
end

---@param scoresTable table<string, integer>
---@return (string|Widget)[]
function StageWinnings._detailedScores(scoresTable)
---@type {wins: integer, losses: integer, count: integer}
local scoreInfos = Array.extractValues(Table.map(scoresTable, function(score, count)
local wins, losses = score:match('(%d+)%-(%d+)')
return score, {wins = tonumber(wins), losses = tonumber(losses), count = count}
end))
table.sort(scoreInfos, function(a, b)
local diffA = a.wins - a.losses
local diffB = b.wins - b.losses
if diffA ~= diffB then
return diffA > diffB
end
return a.wins > b.wins
end)

return Array.interleave(
Array.map(scoreInfos, function(scoreInfo)
return scoreInfo.wins .. '-' .. scoreInfo.losses .. ': ' .. scoreInfo.count .. ' times'
end),
HtmlWidgets.Br{}
)
end

return StageWinnings
Loading