--- 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(G.hand.highlighted, 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