From a67a167d536f942b76b3275951ea1a89b4c34e9f Mon Sep 17 00:00:00 2001 From: Rikard Blixt Date: Fri, 12 Jun 2026 16:17:32 +0200 Subject: [PATCH] perf: compute standings tiebreaker values once and fix Buchholz complexity Co-Authored-By: Claude Fable 5 --- lua/wikis/commons/Standings/Parser.lua | 5 +- .../commons/Standings/Tiebreaker/Buchholz.lua | 56 +++++++++++++++---- .../Standings/Tiebreaker/Game/Diff.lua | 3 +- .../Standings/Tiebreaker/Game/Rounds/Diff.lua | 3 +- .../Standings/Tiebreaker/Game/WinRate.lua | 5 +- .../Standings/Tiebreaker/Interface.lua | 8 ++- .../Standings/Tiebreaker/Match/Diff.lua | 3 +- .../Standings/Tiebreaker/Match/WinRate.lua | 5 +- 8 files changed, 67 insertions(+), 21 deletions(-) diff --git a/lua/wikis/commons/Standings/Parser.lua b/lua/wikis/commons/Standings/Parser.lua index da58111abc0..a32e03329b4 100644 --- a/lua/wikis/commons/Standings/Parser.lua +++ b/lua/wikis/commons/Standings/Parser.lua @@ -146,9 +146,10 @@ function StandingsParser.calculateTiebreakerValues(opponentsInRound, tiebreakerI return end Array.forEach(opponentsInRound, function(opponent) + local value = tiebreaker:valueOf(opponentsInRound, opponent) opponent.extradata.tiebreakerValues[tiebreakerId] = { - value = tiebreaker:valueOf(opponentsInRound, opponent), - display = tiebreaker:display(opponentsInRound, opponent), + value = value, + display = tiebreaker:display(opponentsInRound, opponent, value), } end) end) diff --git a/lua/wikis/commons/Standings/Tiebreaker/Buchholz.lua b/lua/wikis/commons/Standings/Tiebreaker/Buchholz.lua index 146467a8830..4397c9c59ba 100644 --- a/lua/wikis/commons/Standings/Tiebreaker/Buchholz.lua +++ b/lua/wikis/commons/Standings/Tiebreaker/Buchholz.lua @@ -9,6 +9,7 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') +local FnUtil = Lua.import('Module:FnUtil') local Opponent = Lua.import('Module:Opponent/Custom') @@ -17,23 +18,58 @@ local TiebreakerInterface = Lua.import('Module:Standings/Tiebreaker/Interface') ---@class TiebreakerBuchholz : StandingsTiebreaker local TiebreakerBuchholz = Class.new(TiebreakerInterface) ----@param state TiebreakerOpponent[] +---Build a set of enemy names for an opponent, memoized per opponent entry table. +---Falls back to Opponent.same for renamed-team detection and aliases the result. ---@param opponent TiebreakerOpponent ----@return integer -function TiebreakerBuchholz:valueOf(state, opponent) - local enemies = Array.flatMap(opponent.matches, function(match) +---@return table +local getEnemyNames = FnUtil.memoize(function(opponent) + local ownName = Opponent.toName(opponent.opponent) + local enemyNames = {} + Array.forEach(opponent.matches, function(match) if not match.finished then - return {} + return end - return Array.filter(match.opponents, function (opp) - return not Opponent.same(opp, opponent.opponent) + Array.forEach(match.opponents, function(matchOpponent) + local name = Opponent.toName(matchOpponent) + -- Skip self: by name, or by Opponent.same for renamed-team edge cases + if name == ownName then + return + end + if matchOpponent.type == 'team' and Opponent.same(matchOpponent, opponent.opponent) then + return + end + enemyNames[name] = true end) end) + return enemyNames +end) + +---@param state TiebreakerOpponent[] +---@param opponent TiebreakerOpponent +---@return integer +function TiebreakerBuchholz:valueOf(state, opponent) + local enemyNames = getEnemyNames(opponent) return Array.reduce(state, function(score, groupMember) - local isEnemy = Array.any(enemies, function(enemy) - return Opponent.same(enemy, groupMember.opponent) - end) + local memberName = Opponent.toName(groupMember.opponent) + local isEnemy = enemyNames[memberName] + + -- Fallback for renamed teams: scan match opponents with Opponent.same + if not isEnemy and groupMember.opponent.type == 'team' then + local found = Array.any(opponent.matches, function(match) + if not match.finished then + return false + end + return Array.any(match.opponents, function(matchOpponent) + return Opponent.same(matchOpponent, groupMember.opponent) + end) + end) + if found then + -- Alias so this cost is only paid once per group member + enemyNames[memberName] = true + isEnemy = true + end + end if not isEnemy then return score diff --git a/lua/wikis/commons/Standings/Tiebreaker/Game/Diff.lua b/lua/wikis/commons/Standings/Tiebreaker/Game/Diff.lua index cd24b516de8..95b8cf4a10b 100644 --- a/lua/wikis/commons/Standings/Tiebreaker/Game/Diff.lua +++ b/lua/wikis/commons/Standings/Tiebreaker/Game/Diff.lua @@ -30,8 +30,9 @@ end ---@param state TiebreakerOpponent[] ---@param opponent TiebreakerOpponent +---@param value integer? ---@return string -function TiebreakerGameDiff:display(state, opponent) +function TiebreakerGameDiff:display(state, opponent, value) local games = TiebreakerGameUtil.getGames(opponent) return games.w .. ' - ' .. games.l end diff --git a/lua/wikis/commons/Standings/Tiebreaker/Game/Rounds/Diff.lua b/lua/wikis/commons/Standings/Tiebreaker/Game/Rounds/Diff.lua index 0fb66b43cef..3d43be35416 100644 --- a/lua/wikis/commons/Standings/Tiebreaker/Game/Rounds/Diff.lua +++ b/lua/wikis/commons/Standings/Tiebreaker/Game/Rounds/Diff.lua @@ -30,8 +30,9 @@ end ---@param state TiebreakerOpponent[] ---@param opponent TiebreakerOpponent +---@param value integer? ---@return string -function TiebreakerRoundDiff:display(state, opponent) +function TiebreakerRoundDiff:display(state, opponent, value) local rounds = TiebreakerRoundUtil.getRounds(opponent) return rounds.w .. ' - ' .. rounds.l end diff --git a/lua/wikis/commons/Standings/Tiebreaker/Game/WinRate.lua b/lua/wikis/commons/Standings/Tiebreaker/Game/WinRate.lua index 7857f82855c..761d7a9e831 100644 --- a/lua/wikis/commons/Standings/Tiebreaker/Game/WinRate.lua +++ b/lua/wikis/commons/Standings/Tiebreaker/Game/WinRate.lua @@ -31,13 +31,14 @@ end ---@param state TiebreakerOpponent[] ---@param opponent TiebreakerOpponent +---@param value integer? ---@return string -function TiebreakerGameWinRate:display(state, opponent) +function TiebreakerGameWinRate:display(state, opponent, value) local games = TiebreakerGameUtil.getGames(opponent) if games == 0 then return '-' end - return MathUtil.formatPercentage(self:valueOf(state, opponent), 2) + return MathUtil.formatPercentage(value ~= nil and value or self:valueOf(state, opponent), 2) end return TiebreakerGameWinRate diff --git a/lua/wikis/commons/Standings/Tiebreaker/Interface.lua b/lua/wikis/commons/Standings/Tiebreaker/Interface.lua index ab2ceabb6e1..d7184c47fad 100644 --- a/lua/wikis/commons/Standings/Tiebreaker/Interface.lua +++ b/lua/wikis/commons/Standings/Tiebreaker/Interface.lua @@ -36,9 +36,13 @@ end ---@param state TiebreakerOpponent[] ---@param opponent TiebreakerOpponent +---@param value integer? ---@return string -function StandingsTiebreaker:display(state, opponent) - return tostring(self:valueOf(state, opponent)) +function StandingsTiebreaker:display(state, opponent, value) + if value == nil then + value = self:valueOf(state, opponent) + end + return tostring(value) end ---@return 'full'|'ml'|'h2h' diff --git a/lua/wikis/commons/Standings/Tiebreaker/Match/Diff.lua b/lua/wikis/commons/Standings/Tiebreaker/Match/Diff.lua index a0bbc9ec09e..e9b1a33f9b5 100644 --- a/lua/wikis/commons/Standings/Tiebreaker/Match/Diff.lua +++ b/lua/wikis/commons/Standings/Tiebreaker/Match/Diff.lua @@ -28,8 +28,9 @@ end ---@param state TiebreakerOpponent[] ---@param opponent TiebreakerOpponent +---@param value integer? ---@return string -function TiebreakerMatchDiff:display(state, opponent) +function TiebreakerMatchDiff:display(state, opponent, value) return opponent.match.w .. ' - ' .. opponent.match.l end diff --git a/lua/wikis/commons/Standings/Tiebreaker/Match/WinRate.lua b/lua/wikis/commons/Standings/Tiebreaker/Match/WinRate.lua index 12cfab5c36e..a43a06723d5 100644 --- a/lua/wikis/commons/Standings/Tiebreaker/Match/WinRate.lua +++ b/lua/wikis/commons/Standings/Tiebreaker/Match/WinRate.lua @@ -30,9 +30,10 @@ end ---@param state TiebreakerOpponent[] ---@param opponent TiebreakerOpponent +---@param value integer? ---@return string -function TiebreakerMatchWinRate:display(state, opponent) - return MathUtil.formatPercentage(self:valueOf(state, opponent), 2) +function TiebreakerMatchWinRate:display(state, opponent, value) + return MathUtil.formatPercentage(value ~= nil and value or self:valueOf(state, opponent), 2) end return TiebreakerMatchWinRate