/Balatro-Preview

Simulate the score you will get on the selected cards!

Primary LanguageLuaGNU General Public License v3.0GPL-3.0

Divvy's Preview for Balatro

Version GitHub Downloads (all assets, all releases) GitHub License GitHub Issues or Pull Requests

Simulate the score and the dollars that you will get by playing the selected cards!

👉 Installation

  1. Install Steamodded
  2. Download the latest release of this mod, here.
  3. Copy the downloaded folder to C:\Users\[USER]\AppData\Roaming\Balatro\
  4. Launch the game!

👉 Features

  • Score and dollar preview without side-effects!
  • Updates in real-time when changing selected cards, changing joker order, or using consumables!
  • Can preview score even when cards are face-down!
  • Perfectly predicts random effects...
  • ... or not! There is an option to show the minimum and maximum possible scores when probabilities are involved!

Caution

This mod is currently incompatible with other mods. I am in the process of kick-starting the process of making the popular mods compatible. If you are a mod creator, please help me out by reading the Mod Compatibility section below.

👉 See It In Action

Demonstration for the preview updating in real-time.

 

Demonstration for the preview being hidden with face-down cards.

 

Demonstration for the preview updating in real-time, pay attention to the dollars.

👉 Mod Compatibility

By default, this mod only simulates vanilla jokers. To support modded jokers, it is necessary to tell this mod how to simulate them, which requires writing a new function for each modded joker. Unfortunately, this is the best way I could see to bypass all animations and side-effects, to calculate exact/min/max previews simultaneously, and to make the simulation efficient. It will be up to the other mod developers to ensure that the preview is accurate for their mods.

▶️ How to make your mod compatible?

To begin with, an example of the basic structure for making modded jokers compatible is as follows:

if DV and DV.SIM then -- Check that Divvy's Simulation is loaded

   DV.SIM.JOKERS.simulate_[JOKERID1] = function(joker, context)
      if context.cardarea == G.jokers and context.before and not context.blueprint then
         -- Upgrade joker, or simulate any other 'before' effects
      elseif context.cardarea == G.jokers and context.global then
         -- Simulate main effect application
      end
   end

   DV.SIM.JOKERS.simulate_[JOKERID2] = function(joker, context)
      if context.cardarea == G.play and context.individual then
         -- Simulate joker effect on each played card
      elseif context.cardarea == G.hand and context.individual then
         -- Simulate joker effect on each held card
      end
   end

   -- All other jokers...
end

If you've created modded jokers before, then the structure of each function should be familiar. The only big differences are: the repetition of if context.cardarea ... for each function, and the new context.global property which I introduced to specify when the global joker effects are being applied (as opposed to per-card effects). You should specify context.global whenever your joker's effect was in the else branch of all contexts. The best way to get a feel for all this is to look at the examples down below.

Important

The simulation code must use my mod's custom functions, all of which are listed below. This is necessary because I use a stripped-down version of all objects, namely Card, which in turn means that the default functions like Card:get_id() may cause errors. Again, this is a consequence of avoiding animations and side-effects.

Lastly, you don't have to write functions for all modded jokers — only those that affect score or money during a played hand. For instance, here is a sample of jokers that I ignore in the vanilla game:

  • "Four Fingers", because it does not affect the score nor the money directly;
  • "Trading Card", because its effect is applied after a discard, not after a play;
  • "Marble Joker", because its effect applies during blind selection, not during a play;
  • "Delayed Gratification", because its effect applies after the round ends, not during a play.

If in doubt, feel free to ask for help on Discord!

▶️ What are the custom functions?

The following are the core functions for manipulating the simulated chips and mult. You will usually just use one argument to manipulate all chips and mult equally (ie. for exact/min/max preview), like DV.SIM.add_mult(3). However, if your joker has a chance element to it, you will have to specify all three arguments.

  • DV.SIM.add_chips(exact, [min], [max])
  • DV.SIM.add_mult(exact, [min], [max])
  • DV.SIM.x_mult(exact, [min], [max])
  • DV.SIM.add_dollars(exact, [min], [max])
  • DV.SIM.add_reps(n)
    • This adds n repetitions for the current played or held card; see examples below.
  • DV.SIM.get_probabilistic_extremes(random_value, odds, reward, default)
    • This is a helper function for getting the exact, min, and max values from your joker, if it relies on chance.
    • It assumes that your joker uses the standard approach to chance: random_value < probability/odds.
    • Its main purpose is to account for guaranteed probabilities, like "2 in 2 chance", which would mean that exact = min = max.
[CLICK ME] Jokers relying on `t_chips`, `t_mult`, or `s_mult`:

If your modded joker leverages the game's built-in properties for chips or mult (based on hand type or suit), then you can use the following functions:

  • DV.SIM.JOKERS.add_type_chips(joker, context)
  • DV.SIM.JOKERS.add_type_mult(joker, context)
  • DV.SIM.JOKERS.add_suit_mult(joker, context)
DV.SIM.JOKERS.simulate_lusty_joker = function(joker, context)
   DV.SIM.JOKERS.add_suit_mult(joker, context)
end


DV.SIM.JOKERS.simulate_jolly = function(joker, context)
    DV.SIM.JOKERS.add_type_mult(joker, context)
end

DV.SIM.JOKERS.simulate_sly = function(joker, context)
    DV.SIM.JOKERS.add_type_chips(joker, context)
end
[CLICK ME] Jokers relying on automatic `x_mult` application:

If your modded joker leverages the game's built-in x-mult calculation, then you can use the following function:

  • DV.SIM.JOKERS.x_mult_if_global(joker, context)

However, only do this if you know what you are doing. If in doubt, have a look at the function definition, here.

DV.SIM.JOKERS.simulate_madness = function(joker, context)
    DV.SIM.JOKERS.x_mult_if_global(joker, context)
end
DV.SIM.JOKERS.simulate_bloodstone = function(joker, context)
   if context.cardarea == G.play and context.individual then
      if DV.SIM.is_suit(context.other_card, "Hearts") and not context.other_card.debuff then
         local exact_xmult, min_xmult, max_xmult = DV.SIM.get_probabilistic_extremes(pseudorandom("bloodstone"), joker.ability.extra.odds, joker.ability.extra.Xmult, 1)
         DV.SIM.x_mult(exact_xmult, min_xmult, max_xmult)
      end
   end
end

The following drop-downs contain all available properties, and below them are the new property retrieval functions.

[CLICK ME] Available card properties:
local card_data = {
   rank = card_obj.base.id,                -- Number 2-14 (where 11-14 is Jack through Ace)
   suit = card_obj.base.suit,              -- "Spades", "Hearts", "Clubs", or "Diamonds"
   base_chips = card_obj.base.nominal,     -- Number 2-10 (default number of chips scored)
   ability = copy_table(card_obj.ability), -- Mirrors Card object
   edition = copy_table(card_obj.edition), -- Mirrors Card object
   seal = card_obj.seal,                   -- "Red", "Purple", "Blue", or "Gold"
   debuff = card_obj.debuff,               -- Boolean
   lucky_trigger = {}                      -- Holds values for exact/min/max triggers
}
[CLICK ME] Available joker properties:
local joker_data = {
   id = [...],
   ability = copy_table(joker.ability), -- Mirrors Card object
   edition = copy_table(joker.edition), -- Mirrors Card object
   rarity = joker.config.center.rarity  -- Number 1-4 (Common, Uncommon, Rare, Legendary)
}
  • DV.SIM.get_rank(card_data)
    • Returns the card's rank (2-14) or a unique negative value for Stone Cards.
  • DV.SIM.is_rank(card_data, ranks)
    • Check for a single rank by using a number argument: DV.SIM.is_rank(card, 9)
    • Check for multiple ranks by using a table argument: DV.SIM.is_rank(card, {11, 12, 13})
  • DV.SIM.is_face(card_data)
    • Checks for ranks 11, 12, 13, taking into account Pareidolia.
  • DV.SIM.is_suit(card_data, suit, [ignore_debuff])
    • Checks for suit, taking into account Stone Cards, Wild Cards, and Smeared Joker.
    • Usually returns false if card is debuffed, unless ignore_debuff == true.
DV.SIM.JOKERS.simulate_walkie_talkie = function(joker, context)
   if context.cardarea == G.play and context.individual then
      if DV.SIM.is_rank(context.other_card, {10, 4}) and not context.other_card.debuff then
         DV.SIM.add_chips(joker.ability.extra.chips)
         DV.SIM.add_mult(joker.ability.extra.mult)
      end
   end
end

The following are the new card manipulation functions. In general, instead of card:set_property(new_property) you will have to write DV.SIM.set_property(card, new_property).

  • DV.SIM.set_ability(card_data, center)
  • DV.SIM.set_edition(card_data, edition)
DV.SIM.JOKERS.simulate_midas_mask = function(joker, context)
   if context.cardarea == G.jokers and context.before and not context.blueprint then
      for _, card in ipairs(context.full_hand) do
         if DV.SIM.is_face(card) then
            DV.SIM.set_ability(card, G.P_CENTERS.m_gold)
         end
      end
   end
end

▶️ Examples

The best source of examples is the default DV.SIM.JOKERS definition, found here.

[CLICK ME] Hack:
DV.SIM.JOKER.simulate_hack = function(joker, context)
   if context.cardarea == G.play and context.repetition then
      if not context.other_card.debuff and DV.SIM.is_rank(context.other_card, {2, 3, 4, 5}) then
         DV.SIM.add_reps(joker.ability.extra)
      end
   end
end
[CLICK ME] Ride The Bus:
DV.SIM.JOKERS.simulate_ride_the_bus = function(joker, context)
   -- Upgrade/Reset, as necessary:
   if context.cardarea == G.jokers and context.before and not context.blueprint then
      local faces = false
      for _, scoring_card in ipairs(context.scoring_hand) do
         if DV.SIM.is_face(scoring_card) then faces = true end
      end
      if faces then
         joker.ability.mult = 0
      else
         joker.ability.mult = joker.ability.mult + joker.ability.extra
      end
   end

   -- Apply mult:
   if context.cardarea == G.jokers and context.global then
      DV.SIM.add_mult(joker.ability.mult)
   end
end
[CLICK ME] Hiker:
DV.SIM.JOKERS.simulate_hiker = function(joker, context)
   if context.cardarea == G.play and context.individual then
      if not context.other_card.debuff then
         context.other_card.ability.perma_bonus = (context.other_card.ability.perma_bonus or 0) + joker.ability.extra
      end
   end
end
[CLICK ME] Business Card:
DV.SIM.JOKERS.simulate_business = function(joker, context)
   if context.cardarea == G.play and context.individual then
      if DV.SIM.is_face(context.other_card) and not context.other_card.debuff then
         local exact_dollars, min_dollars, max_dollars = DV.SIM.get_probabilistic_extremes(pseudorandom("business"), joker.ability.extra, 2, 0)
         DV.SIM.add_dollars(exact_dollars, min_dollars, max_dollars)
      end
   end
end
[CLICK ME] Idol:
DV.SIM.JOKERS.simulate_idol = function(joker, context)
   if context.cardarea == G.play and context.individual then
      if DV.SIM.is_rank(context.other_card, G.GAME.current_round.idol_card.id) and
         DV.SIM.is_suit(context.other_card, G.GAME.current_round.idol_card.suit) and
         not context.other_card.debuff
      then
         DV.SIM.x_mult(joker.ability.extra)
      end
   end
end

If you found this mod useful, consider supporting me!

Buy Me A Coffee