diff --git a/compatibility/TheOrder.lua b/compatibility/TheOrder.lua index 512b5281..4dc46084 100644 --- a/compatibility/TheOrder.lua +++ b/compatibility/TheOrder.lua @@ -33,6 +33,9 @@ function reset_idol_card() G.GAME.current_round.idol_card.rank = "Ace" G.GAME.current_round.idol_card.suit = "Spades" + -- ---------------------------------------------------------------- + -- Step 1: Build count_map keyed by (value, suit) + -- ---------------------------------------------------------------- local count_map = {} local valid_idol_cards = {} @@ -40,44 +43,199 @@ function reset_idol_card() if v.ability.effect ~= "Stone Card" then local key = v.base.value .. "_" .. v.base.suit if not count_map[key] then - count_map[key] = { count = 0, card = v } + count_map[key] = { + count = 0, + card = v, + value = v.base.value, + suit = v.base.suit, + } table.insert(valid_idol_cards, count_map[key]) end count_map[key].count = count_map[key].count + 1 end end - --failsafe in case all are stone or no cards in deck. Defaults to Ace of Spades + if #valid_idol_cards == 0 then return end - local value_order = {} - for i, rank in ipairs(SMODS.Rank.obj_buffer) do - value_order[rank] = i + -- ---------------------------------------------------------------- + -- Step 2: Build rank ordering from SMODS (positional index) + -- ---------------------------------------------------------------- + local rank_index = {} + for i, rank_key in ipairs(SMODS.Rank.obj_buffer) do + rank_index[rank_key] = i end - local suit_order = {} - for i, suit in ipairs(SMODS.Suit.obj_buffer) do - suit_order[suit] = i + local suit_index = {} + for i, suit_key in ipairs(SMODS.Suit.obj_buffer) do + suit_index[suit_key] = i end - table.sort(valid_idol_cards, function(a, b) - -- Sort by count descending first - if a.count ~= b.count then return a.count > b.count end + -- ---------------------------------------------------------------- + -- Step 3: Aggregate per-rank totals (only ranks present in deck) + -- ---------------------------------------------------------------- + local rank_totals = {} -- rank_key -> total count across all suits + local distinct_cards = 0 -- number of distinct (rank, suit) entries - local a_suit = a.card.base.suit - local b_suit = b.card.base.suit - if suit_order[a_suit] ~= suit_order[b_suit] then return suit_order[a_suit] < suit_order[b_suit] end + for _, entry in ipairs(valid_idol_cards) do + local r = entry.value + rank_totals[r] = (rank_totals[r] or 0) + entry.count + distinct_cards = distinct_cards + 1 + end + + -- Count of distinct ranks present + local distinct_ranks = 0 + for _ in pairs(rank_totals) do + distinct_ranks = distinct_ranks + 1 + end + + local total_cards = 0 + for _, entry in ipairs(valid_idol_cards) do + total_cards = total_cards + entry.count + end + + -- ---------------------------------------------------------------- + -- Step 4: Compute means, rounded to nearest 0.5 + -- (Python: round(x * 2) / 2 — Lua's math.floor with +0.5 trick) + -- ---------------------------------------------------------------- + local function round_to_half(x) + return math.floor(x * 2 + 0.5) / 2 + end + + local function round_to_nearest_05(x) + return math.floor(x * 20 + 0.5) / 20 + end + + local mean_by_card = round_to_half(total_cards / distinct_cards) + local mean_by_number = round_to_half(total_cards / distinct_ranks) + local raw_mean_by_number = total_cards / distinct_ranks + + -- ---------------------------------------------------------------- + -- Step 5: Face / low pools and baselines for Generalized score + -- Face = ranks with .face == true in SMODS + -- Low = ranks with nominal <= 5 and nominal >= 2 + -- (covers 2,3,4,5 in vanilla; adapts to mods) + -- ---------------------------------------------------------------- + local face_pool = 0 + local low_pool = 0 + local face_ranks_present = 0 + local low_ranks_present = 0 + + for rank_key, total in pairs(rank_totals) do + local rank_obj = SMODS.Ranks[rank_key] + if rank_obj then + if rank_obj.face then + face_pool = face_pool + total + face_ranks_present = face_ranks_present + 1 + elseif rank_obj.nominal and rank_obj.nominal >= 2 and rank_obj.nominal <= 5 then + low_pool = low_pool + total + low_ranks_present = low_ranks_present + 1 + end + end + end + + local face_baseline = round_to_nearest_05(raw_mean_by_number * face_ranks_present) + local low_baseline = round_to_nearest_05(raw_mean_by_number * low_ranks_present) + + local W_GEN = 0.05 + local GEN_FLOOR = 0.01 + + -- ---------------------------------------------------------------- + -- Step 6: Off Hit per rank (scaled by 0.5) + -- ---------------------------------------------------------------- + local off_hit_by_rank = {} + for rank_key, total in pairs(rank_totals) do + off_hit_by_rank[rank_key] = 0.5 * math.max(0.0, total - mean_by_number) + end - local a_value = a.card.base.value - local b_value = b.card.base.value - return value_order[a_value] < value_order[b_value] + -- ---------------------------------------------------------------- + -- Step 7: Previous rank (positional wrap: index 1 -> last index) + -- In vanilla obj_buffer: Ace(1) wraps to 2(last), giving + -- the Ace -> King adjacency the Python script intends. + -- ---------------------------------------------------------------- + local function previous_rank_key(rank_key) + local idx = rank_index[rank_key] + if not idx then return nil end + if idx == 1 then + -- wrap to last rank in buffer + return SMODS.Rank.obj_buffer[#SMODS.Rank.obj_buffer] + else + return SMODS.Rank.obj_buffer[idx - 1] + end + end + + -- ---------------------------------------------------------------- + -- Step 8: Generalized score per rank key + -- ---------------------------------------------------------------- + local function generalized_for_rank(rank_key) + local rank_obj = SMODS.Ranks[rank_key] + if not rank_obj then return 0.0 end + if rank_obj.face then + return math.max(GEN_FLOOR, W_GEN * math.max(0.0, face_pool - face_baseline)) + elseif rank_obj.nominal and rank_obj.nominal >= 2 and rank_obj.nominal <= 5 then + return math.max(GEN_FLOOR, W_GEN * math.max(0.0, low_pool - low_baseline)) + end + return 0.0 + end + + -- ---------------------------------------------------------------- + -- Step 9: Compute total score for each distinct (rank, suit) entry + -- ---------------------------------------------------------------- + for _, entry in ipairs(valid_idol_cards) do + local rank_key = entry.value + local suit_key = entry.suit + local card_count = entry.count + + -- Main Hit: 2 * max(0, card_count - mean_by_card) + local main_hit = 2.0 * math.max(0.0, card_count - mean_by_card) + + -- Off Hit: 0.5 * max(0, rank_total - mean_by_number) + local off_hit = off_hit_by_rank[rank_key] or 0.0 + + -- Rank Adjacent: 0.25 * off_hit of the previous rank + local prev_rank = previous_rank_key(rank_key) + local rank_adj = 0.25 * (off_hit_by_rank[prev_rank] or 0.0) + + -- Suit-Matched Adjacent: 0.33 * max(0, neighbor_count - mean_by_card) + -- neighbor = the previous rank of the same suit + local neighbor_count = 0 + if prev_rank then + local neighbor_key = prev_rank .. "_" .. suit_key + if count_map[neighbor_key] then + neighbor_count = count_map[neighbor_key].count + end + end + local suit_matched_adj = 0.33 * math.max(0.0, neighbor_count - mean_by_card) + + -- Generalized + local generalized = generalized_for_rank(rank_key) + + entry.total_score = main_hit + off_hit + suit_matched_adj + rank_adj + generalized + + --[[sendDebugMessage( + string.format( + "(Idol) Score for %s of %s: total=%.4f (main=%.4f off=%.4f suit_adj=%.4f rank_adj=%.4f gen=%.4f)", + rank_key, suit_key, + entry.total_score, main_hit, off_hit, suit_matched_adj, rank_adj, generalized + ) + )]] + end + + -- ---------------------------------------------------------------- + -- Step 10: Sort by score, then weighted random selection by count + -- ---------------------------------------------------------------- + table.sort(valid_idol_cards, function(a, b) + if a.total_score ~= b.total_score then return a.total_score > b.total_score end + if suit_index[a.suit] ~= suit_index[b.suit] then return suit_index[a.suit] < suit_index[b.suit] end + return (rank_index[a.value] or 0) < (rank_index[b.value] or 0) end) - -- Weighted random selection based on count local total_weight = 0 for _, entry in ipairs(valid_idol_cards) do total_weight = total_weight + entry.count end + if total_weight <= 0 then return end + local raw_random = pseudorandom("idol" .. G.GAME.round_resets.ante) local threshold = 0 @@ -85,20 +243,16 @@ function reset_idol_card() threshold = threshold + (entry.count / total_weight) if raw_random < threshold then local idol_card = entry.card - sendDebugMessage( - "(Idol) Selected card " - .. idol_card.base.value - .. " of " - .. idol_card.base.suit - .. " with weight " - .. entry.count - .. " of total " - .. total_weight, - "MULTIPLAYER" - ) + --[[sendDebugMessage( + string.format( + "(Idol) Selected %s of %s (score=%.4f, count=%d, total_weight=%d)", + idol_card.base.value, idol_card.base.suit, + entry.total_score, entry.count, total_weight + ) + )]] G.GAME.current_round.idol_card.rank = idol_card.base.value G.GAME.current_round.idol_card.suit = idol_card.base.suit - G.GAME.current_round.idol_card.id = idol_card.base.id + G.GAME.current_round.idol_card.id = idol_card.base.id break end end