Folder unification, added description and image

This commit is contained in:
kmirass
2025-03-26 01:59:12 +01:00
parent 4ea3a78220
commit 27f19c8310
17 changed files with 692 additions and 1365 deletions

420
EngineSimulate.lua Normal file
View File

@@ -0,0 +1,420 @@
--- Original: Divvy's Simulation for Balatro - Engine.lua
--
-- The heart of this library: it replicates the game's score evaluation.
function FN.SIM.run()
local null_ret = {score = {min=0, exact=0, max=0}, dollars = {min=0, exact=0, max=0}}
if #G.hand.highlighted < 1 then return null_ret end
FN.SIM.init()
FN.SIM.manage_state("SAVE")
FN.SIM.update_state_variables()
if not FN.SIM.simulate_blind_debuffs() then
FN.SIM.simulate_joker_before_effects()
FN.SIM.add_base_chips_and_mult()
FN.SIM.simulate_blind_effects()
FN.SIM.simulate_scoring_cards()
FN.SIM.simulate_held_cards()
FN.SIM.simulate_joker_global_effects()
FN.SIM.simulate_consumable_effects()
FN.SIM.simulate_deck_effects()
else -- Only Matador at this point:
FN.SIM.simulate_all_jokers(G.jokers, {debuffed_hand = true})
end
FN.SIM.manage_state("RESTORE")
return FN.SIM.get_results()
end
function FN.SIM.init()
-- Reset:
FN.SIM.running = {
min = {chips = 0, mult = 0, dollars = 0},
exact = {chips = 0, mult = 0, dollars = 0},
max = {chips = 0, mult = 0, dollars = 0},
reps = 0
}
-- Fetch metadata about simulated play:
local hand_name, _, poker_hands, scoring_hand, _ = G.FUNCS.get_poker_hand_info(G.hand.highlighted)
FN.SIM.env.scoring_name = hand_name
-- Identify played cards and extract necessary data:
FN.SIM.env.played_cards = {}
FN.SIM.env.scoring_cards = {}
local is_splash_joker = next(find_joker("Splash"))
table.sort(G.hand.highlighted, function(a, b) return a.T.x < b.T.x end) -- Sorts by positional x-value to mirror card order!
for _, card in ipairs(G.hand.highlighted) do
local is_scoring = false
for _, scoring_card in ipairs(scoring_hand) do
-- Either card is scoring because it's part of the scoring hand,
-- or there is Splash joker, or it's a Stone Card:
if card.sort_id == scoring_card.sort_id
or is_splash_joker
or card.ability.effect == "Stone Card"
then
is_scoring = true
break
end
end
local card_data = FN.SIM.get_card_data(card)
table.insert(FN.SIM.env.played_cards, card_data)
if is_scoring then table.insert(FN.SIM.env.scoring_cards, card_data) end
end
-- Identify held cards and extract necessary data:
FN.SIM.env.held_cards = {}
for _, card in ipairs(G.hand.cards) do
-- Highlighted cards are simulated as played cards:
if not card.highlighted then
local card_data = FN.SIM.get_card_data(card)
table.insert(FN.SIM.env.held_cards, card_data)
end
end
-- Extract necessary joker data:
FN.SIM.env.jokers = {}
for _, joker in ipairs(G.jokers.cards) do
local joker_data = {
-- P_CENTER keys for jokers have the form j_NAME, get rid of j_
id = joker.config.center.key:sub(3, #joker.config.center.key),
ability = copy_table(joker.ability),
edition = copy_table(joker.edition),
rarity = joker.config.center.rarity,
debuff = joker.debuff
}
table.insert(FN.SIM.env.jokers, joker_data)
end
-- Extract necessary consumable data:
FN.SIM.env.consumables = {}
for _, consumable in ipairs(G.consumeables.cards) do
local consumable_data = {
-- P_CENTER keys have the form x_NAME, get rid of x_
id = consumable.config.center.key:sub(3, #consumable.config.center.key),
ability = copy_table(consumable.ability)
}
table.insert(FN.SIM.env.consumables, consumable_data)
end
-- Set extensible context template:
FN.SIM.get_context = function(cardarea, args)
local context = {
cardarea = cardarea,
full_hand = FN.SIM.env.played_cards,
scoring_name = hand_name,
scoring_hand = FN.SIM.env.scoring_cards,
poker_hands = poker_hands
}
for k, v in pairs(args) do
context[k] = v
end
return context
end
end
function FN.SIM.get_card_data(card_obj)
return {
rank = card_obj.base.id,
suit = card_obj.base.suit,
base_chips = card_obj.base.nominal,
ability = copy_table(card_obj.ability),
edition = copy_table(card_obj.edition),
seal = card_obj.seal,
debuff = card_obj.debuff,
lucky_trigger = {}
}
end
function FN.SIM.get_results()
local FNSR = FN.SIM.running
local min_score = math.floor(FNSR.min.chips * FNSR.min.mult)
local exact_score = math.floor(FNSR.exact.chips * FNSR.exact.mult)
local max_score = math.floor(FNSR.max.chips * FNSR.max.mult)
return {
score = {min = min_score, exact = exact_score, max = max_score},
dollars = {min = FNSR.min.dollars, exact = FNSR.exact.dollars, max = FNSR.max.dollars}
}
end
--
-- GAME STATE MANAGEMENT:
--
function FN.SIM.manage_state(save_or_restore)
local FNSO = FN.SIM.orig
if save_or_restore == "SAVE" then
FNSO.random_data = copy_table(G.GAME.pseudorandom)
FNSO.hand_data = copy_table(G.GAME.hands)
return
end
if save_or_restore == "RESTORE" then
G.GAME.pseudorandom = FNSO.random_data
G.GAME.hands = FNSO.hand_data
return
end
end
function FN.SIM.update_state_variables()
-- Increment poker hand played this run/round:
local hand_info = G.GAME.hands[FN.SIM.env.scoring_name]
hand_info.played = hand_info.played + 1
hand_info.played_this_round = hand_info.played_this_round + 1
end
--
-- MACRO LEVEL:
--
function FN.SIM.simulate_scoring_cards()
for _, scoring_card in ipairs(FN.SIM.env.scoring_cards) do
FN.SIM.simulate_card_in_context(scoring_card, G.play)
end
end
function FN.SIM.simulate_held_cards()
for _, held_card in ipairs(FN.SIM.env.held_cards) do
FN.SIM.simulate_card_in_context(held_card, G.hand)
end
end
function FN.SIM.simulate_joker_global_effects()
for _, joker in ipairs(FN.SIM.env.jokers) do
if joker.edition then -- Foil and Holo:
if joker.edition.chips then FN.SIM.add_chips(joker.edition.chips) end
if joker.edition.mult then FN.SIM.add_mult(joker.edition.mult) end
end
FN.SIM.simulate_joker(joker, FN.SIM.get_context(G.jokers, {global = true}))
-- Joker-on-joker effects (eg. Blueprint):
FN.SIM.simulate_all_jokers(G.jokers, {other_joker = joker})
if joker.edition then -- Poly:
if joker.edition.x_mult then FN.SIM.x_mult(joker.edition.x_mult) end
end
end
end
function FN.SIM.simulate_consumable_effects()
for _, consumable in ipairs(FN.SIM.env.consumables) do
if consumable.ability.set == "Planet" and not consumable.debuff then
if G.GAME.used_vouchers.v_observatory and consumable.ability.consumeable.hand_type == FN.SIM.env.scoring_name then
FN.SIM.x_mult(G.P_CENTERS.v_observatory.config.extra)
end
end
end
end
function FN.SIM.add_base_chips_and_mult()
local played_hand_data = G.GAME.hands[FN.SIM.env.scoring_name]
FN.SIM.add_chips(played_hand_data.chips)
FN.SIM.add_mult(played_hand_data.mult)
end
function FN.SIM.simulate_joker_before_effects()
for _, joker in ipairs(FN.SIM.env.jokers) do
FN.SIM.simulate_joker(joker, FN.SIM.get_context(G.jokers, {before = true}))
end
end
function FN.SIM.simulate_blind_effects()
if G.GAME.blind.disabled then return end
if G.GAME.blind.name == "The Flint" then
local function flint(data)
local half_chips = math.floor(data.chips/2 + 0.5)
local half_mult = math.floor(data.mult/2 + 0.5)
data.chips = mod_chips(math.max(half_chips, 0))
data.mult = mod_mult(math.max(half_mult, 1))
end
flint(FN.SIM.running.min)
flint(FN.SIM.running.exact)
flint(FN.SIM.running.max)
else
-- Other blinds do not impact scoring; refer to Blind:modify_hand(..)
end
end
function FN.SIM.simulate_deck_effects()
if G.GAME.selected_back.name == 'Plasma Deck' then
local function plasma(data)
local sum = data.chips + data.mult
local half_sum = math.floor(sum/2)
data.chips = mod_chips(half_sum)
data.mult = mod_mult(half_sum)
end
plasma(FN.SIM.running.min)
plasma(FN.SIM.running.exact)
plasma(FN.SIM.running.max)
else
-- Other decks do not impact scoring; refer to Back:trigger_effect(..)
end
end
function FN.SIM.simulate_blind_debuffs()
local blind_obj = G.GAME.blind
if blind_obj.disabled then return false end
-- The following are part of Blind:press_play()
if blind_obj.name == "The Hook" then
blind_obj.triggered = true
for _ = 1, math.min(2, #FN.SIM.env.held_cards) do
-- TODO: Identify cards-in-hand that can affect score, simulate with/without them for min/max
local selected_card, card_key = pseudorandom_element(FN.SIM.env.held_cards, pseudoseed('hook'))
table.remove(FN.SIM.env.held_cards, card_key)
for _, joker in ipairs(FN.SIM.env.jokers) do
-- Note that the cardarea argument is largely arbitrary (used for FN.SIM.JOKERS),
-- I use G.hand because The Hook discards from the hand
FN.SIM.simulate_joker(joker, FN.SIM.get_context(G.hand, {discard = true, other_card = selected_card}))
end
end
end
if blind_obj.name == "The Tooth" then
blind_obj.triggered = true
FN.SIM.add_dollars((-1) * #FN.SIM.env.played_cards)
end
-- The following are part of Blind:debuff_hand(..)
if blind_obj.name == "The Arm" then
blind_obj.triggered = false
local played_hand_name = FN.SIM.env.scoring_name
if G.GAME.hands[played_hand_name].level > 1 then
blind_obj.triggered = true
-- NOTE: Important to save/restore G.GAME.hands here
-- NOTE: Implementation mirrors level_up_hand(..)
local played_hand_data = G.GAME.hands[played_hand_name]
played_hand_data.level = math.max(1, played_hand_data.level - 1)
played_hand_data.mult = math.max(1, played_hand_data.s_mult + (played_hand_data.level-1) * played_hand_data.l_mult)
played_hand_data.chips = math.max(0, played_hand_data.s_chips + (played_hand_data.level-1) * played_hand_data.l_chips)
end
return false -- IMPORTANT: Avoid duplicate effects from Blind:debuff_hand() below
end
if blind_obj.name == "The Ox" then
blind_obj.triggered = false
if FN.SIM.env.scoring_name == G.GAME.current_round.most_played_poker_hand then
blind_obj.triggered = true
FN.SIM.add_dollars(-G.GAME.dollars)
end
return false -- IMPORTANT: Avoid duplicate effects from Blind:debuff_hand() below
end
return blind_obj:debuff_hand(FN.SIM.env.played_cards, FN.SIM.env.poker_hands, FN.SIM.env.scoring_name, true)
end
--
-- MICRO LEVEL (CARDS):
--
function FN.SIM.simulate_card_in_context(card, cardarea)
-- Reset and collect repetitions:
FN.SIM.running.reps = 1
if card.seal == "Red" then FN.SIM.add_reps(1) end
FN.SIM.simulate_all_jokers(cardarea, {other_card = card, repetition = true})
-- Apply effects:
for _ = 1, FN.SIM.running.reps do
FN.SIM.simulate_card(card, FN.SIM.get_context(cardarea, {}))
FN.SIM.simulate_all_jokers(cardarea, {other_card = card, individual = true})
end
end
function FN.SIM.simulate_card(card_data, context)
-- Do nothing if debuffed:
if card_data.debuff then return end
if context.cardarea == G.play then
-- Chips:
if card_data.ability.effect == "Stone Card" then
FN.SIM.add_chips(card_data.ability.bonus + (card_data.ability.perma_bonus or 0))
else
FN.SIM.add_chips(card_data.base_chips + card_data.ability.bonus + (card_data.ability.perma_bonus or 0))
end
-- Mult:
if card_data.ability.effect == "Lucky Card" then
local exact_mult, min_mult, max_mult = FN.SIM.get_probabilistic_extremes(pseudorandom("nope"), 5, card_data.ability.mult, 0)
FN.SIM.add_mult(exact_mult, min_mult, max_mult)
-- Careful not to overwrite `card_data.lucky_trigger` outright:
if exact_mult > 0 then card_data.lucky_trigger.exact = true end
if min_mult > 0 then card_data.lucky_trigger.min = true end
if max_mult > 0 then card_data.lucky_trigger.max = true end
else
FN.SIM.add_mult(card_data.ability.mult)
end
-- XMult:
if card_data.ability.x_mult > 1 then
FN.SIM.x_mult(card_data.ability.x_mult)
end
-- Dollars:
if card_data.seal == "Gold" then
FN.SIM.add_dollars(3)
end
if card_data.ability.p_dollars > 0 then
if card_data.ability.effect == "Lucky Card" then
local exact_dollars, min_dollars, max_dollars = FN.SIM.get_probabilistic_extremes(pseudorandom("notthistime"), 15, card_data.ability.p_dollars, 0)
FN.SIM.add_dollars(exact_dollars, min_dollars, max_dollars)
-- Careful not to overwrite `card_data.lucky_trigger` outright:
if exact_dollars > 0 then card_data.lucky_trigger.exact = true end
if min_dollars > 0 then card_data.lucky_trigger.min = true end
if max_dollars > 0 then card_data.lucky_trigger.max = true end
else
FN.SIM.add_dollars(card_data.ability.p_dollars)
end
end
-- Edition:
if card_data.edition then
if card_data.edition.chips then FN.SIM.add_chips(card_data.edition.chips) end
if card_data.edition.mult then FN.SIM.add_mult(card_data.edition.mult) end
if card_data.edition.x_mult then FN.SIM.x_mult(card_data.edition.x_mult) end
end
elseif context.cardarea == G.hand then
if card_data.ability.h_mult > 0 then
FN.SIM.add_mult(card_data.ability.h_mult)
end
if card_data.ability.h_x_mult > 0 then
FN.SIM.x_mult(card_data.ability.h_x_mult)
end
end
end
--
-- MICRO LEVEL (JOKERS):
--
function FN.SIM.simulate_all_jokers(cardarea, context_args)
for _, joker in ipairs(FN.SIM.env.jokers) do
FN.SIM.simulate_joker(joker, FN.SIM.get_context(cardarea, context_args))
end
end
function FN.SIM.simulate_joker(joker_obj, context)
-- Do nothing if debuffed:
if joker_obj.debuff then return end
local joker_simulation_function = FN.SIM.JOKERS["simulate_" .. joker_obj.id]
if joker_simulation_function then joker_simulation_function(joker_obj, context) end
end