Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 184 additions & 30 deletions compatibility/TheOrder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,72 +33,226 @@ 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 = {}

for _, v in ipairs(G.playing_cards) do
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
for _, entry in ipairs(valid_idol_cards) do
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
Expand Down