From 816be22411eb783f67c2ab9afa85186c2c29919b Mon Sep 17 00:00:00 2001 From: Pretzal Date: Thu, 25 Dec 2025 07:25:47 -0600 Subject: [PATCH 1/2] Fixing cards with multiple suits not counting in a Spectrum --- utilities/misc_functions.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/utilities/misc_functions.lua b/utilities/misc_functions.lua index fd933857..aa1971e0 100644 --- a/utilities/misc_functions.lua +++ b/utilities/misc_functions.lua @@ -286,7 +286,6 @@ function PB_UTIL.get_unique_suits(scoring_hand, bypass_debuff, flush_calc) for suit, count in pairs(suits) do if card:is_suit(suit, bypass_debuff, flush_calc) and count == 0 then suits[suit] = count + 1 - break end end end From 9ee1265ab91bb78fa53ca001ca04440a4bc669cd Mon Sep 17 00:00:00 2001 From: Pretzal Date: Thu, 25 Dec 2025 18:44:15 -0600 Subject: [PATCH 2/2] Adds in a bipartite matching algorithm because we should be better. --- paperback.lua | 1 + utilities/hopcroft_karp.lua | 133 +++++++++++++++++++++++++++++++++++ utilities/misc_functions.lua | 55 +++++---------- 3 files changed, 150 insertions(+), 39 deletions(-) create mode 100644 utilities/hopcroft_karp.lua diff --git a/paperback.lua b/paperback.lua index 8318e266..b8c05f53 100644 --- a/paperback.lua +++ b/paperback.lua @@ -6,6 +6,7 @@ SMODS.load_file("utilities/misc_functions.lua")() SMODS.load_file("utilities/ui.lua")() SMODS.load_file("utilities/hooks.lua")() SMODS.load_file("utilities/cross-mod.lua")() +SMODS.load_file("utilities/hopcroft_karp.lua")() -- Load the atlases SMODS.load_file("content/atlas.lua")() diff --git a/utilities/hopcroft_karp.lua b/utilities/hopcroft_karp.lua new file mode 100644 index 00000000..a4ff6a5b --- /dev/null +++ b/utilities/hopcroft_karp.lua @@ -0,0 +1,133 @@ +INF = 2147483647 +NIL = 0 + +--- Based on https://www.geeksforgeeks.org/dsa/hopcroft-karp-algorithm-for-maximum-matching-set-2-implementation/ +--- Converted to Lua by your local Pretzal :) + +--- A class to represent Bipartite graph for Hopcroft +--- Karp implementation +BipGraph = Object:extend() +--- Constructor +function BipGraph:init(m, n) + --- m and n are number of vertices on left + --- and right sides of Bipartite Graph + self.__m = m + self.__n = n + --- adj[u] stores adjacents of left side + --- vertex 'u'. The value of u ranges from 1 to m. + --- 0 is used for dummy vertex + self.__adj = {} + for k = 0, m do + self.__adj[k] = {} + end +end + +--- To add edge from u to v and v to u +function BipGraph:addEdge(u, v) + self.__adj[u][#self.__adj[u] + 1] = v --- Add u to v’s list. +end + +--- Returns true if there is an augmenting path, else returns +--- false +function BipGraph:bfs() + Q = {} + --- First layer of vertices (set distance as 0) + for u = 1, self.__m do + --- If this is a free vertex, add it to queue + if self.__pairU[u] == NIL then + --- u is not matched + self.__dist[u] = 0 + Q[#Q + 1] = u + --- Else set distance as infinite so that this vertex + --- is considered next time + else + self.__dist[u] = INF + end + end + --- Initialize distance to NIL as infinite + self.__dist[NIL] = INF + --- Q is going to contain vertices of left side only. + while #Q > 0 do + --- Dequeue a vertex + u = table.remove(Q, 1) + --- If this node is not NIL and can provide a shorter path to NIL + if self.__dist[u] < self.__dist[NIL] then + --- Get all adjacent vertices of the dequeued vertex u + + for _, v in pairs(self.__adj[u]) do + --- If pair of v is not considered so far + --- (v, pairV[V]) is not yet explored edge. + if self.__dist[self.__pairV[v]] == INF then + --- Consider the pair and add it to queue + self.__dist[self.__pairV[v]] = self.__dist[u] + 1 + Q[#Q + 1] = self.__pairV[v] + end + end + end + end + --- If we could come back to NIL using alternating path of distinct + --- vertices then there is an augmenting path + return self.__dist[NIL] ~= INF +end + +--- Returns true if there is an augmenting path beginning with free vertex u +function BipGraph:dfs(u) + if u ~= NIL then + --- Get all adjacent vertices of the dequeued vertex u + for _, v in pairs(self.__adj[u]) do + if self.__dist[self.__pairV[v]] == (self.__dist[u] + 1) then + --- If dfs for pair of v also returns true + if self:dfs(self.__pairV[v]) then + self.__pairV[v] = u + self.__pairU[u] = v + return true + end + end + end + --- If there is no augmenting path beginning with u. + self.__dist[u] = INF + return false + end + return true +end + +function BipGraph:hopcroftKarp() + --- pairU[u] stores pair of u in matching where u + --- is a vertex on left side of Bipartite Graph. + --- If u doesn't have any pair, then pairU[u] is NIL + self.__pairU = {} + for k = 0, self.__m do + self.__pairU[k] = 0 + end + + --- pairV[v] stores pair of v in matching. If v + --- doesn't have any pair, then pairU[v] is NIL + self.__pairV = {} + for k = 0, self.__n do + self.__pairV[k] = 0 + end + + --- dist[u] stores distance of left side vertices + --- dist[u] is one more than dist[u'] if u is next + --- to u'in augmenting path + self.__dist = {} + for k = 0, self.__m do + self.__dist[k] = 0 + end + --- Initialize result + local result = 0 + + --- Keep updating the result while there is an + --- augmenting path. + while self:bfs() do + --- Find a free vertex + for u = 1, self.__m do + --- If current vertex is free and there is + --- an augmenting path from current vertex + if self.__pairU[u] == NIL and self:dfs(u) then + result = result + 1 + end + end + end + return result +end diff --git a/utilities/misc_functions.lua b/utilities/misc_functions.lua index aa1971e0..4ef05bb8 100644 --- a/utilities/misc_functions.lua +++ b/utilities/misc_functions.lua @@ -268,49 +268,26 @@ end --- debuffed wild cards are considered their original suit only ---@return integer function PB_UTIL.get_unique_suits(scoring_hand, bypass_debuff, flush_calc) - -- Set each suit's count to 0 - local suits = {} - - for k, _ in pairs(SMODS.Suits) do - suits[k] = 0 - end - - -- NOTE greedy algorithm is technically wrong for cards with weird suit combos, - -- for example a card with suit A+B might count for A, blocking another card - -- that can only be A - -- (a bipartite matching algorithm would work) - - -- First we cover all the non Wild Cards in the hand - for _, card in ipairs(scoring_hand) do - if not SMODS.has_any_suit(card) then - for suit, count in pairs(suits) do - if card:is_suit(suit, bypass_debuff, flush_calc) and count == 0 then - suits[suit] = count + 1 - end - end - end - end - - -- Then we cover Wild Cards, filling the missing suits - for _, card in ipairs(scoring_hand) do - if SMODS.has_any_suit(card) then - for suit, count in pairs(suits) do - if card:is_suit(suit, bypass_debuff, flush_calc) and count == 0 then - suits[suit] = count + 1 - break - end + local suit_count = 0 + for _ in pairs(SMODS.Suits) do + suit_count = suit_count + 1 + end + -- Initilize a bipartite matching algorithm because math is tight + local b = BipGraph(#scoring_hand, suit_count) + + for card_index, card in ipairs(scoring_hand) do + local suit_index = 0 + for suit, _ in pairs(SMODS.Suits) do + suit_index = suit_index + 1 + if card:is_suit(suit, bypass_debuff, flush_calc) then + -- Add edges for each card based on suits + b:addEdge(card_index, suit_index) end end end - -- Count the amount of suits that were found - local num_suits = 0 - - for _, v in pairs(suits) do - if v > 0 then num_suits = num_suits + 1 end - end - - return num_suits + -- Gets maximum number of matches. + return b:hopcroftKarp() end --- Creates and opens the specified booster pack, the same way a Tag would do it