Every entry here was checked against the running Steamodded source in Mods/lovely/dump, not from memory. The headline is a correction: the shipped CLASHES table warned that Smeared Joker + Flower Pot "cannot make all four" suits. It is backwards. Flower Pot fills its four suit slots with a greedy if/elseif chain (card.lua:4256) and SMODS.smeared_check makes Hearts answer for Diamonds and Spades for Clubs, so the second Heart falls through into the Diamond slot: any two red + two black cards fill the pot. It is one of the strongest combos in the game and the mod was steering players away from it -- while simultaneously scoring the pair +2 via suit_flex, which should have been the tell. It is now a famous pair. New clashes (4 -> 22), all traced to the mechanism: * Burglar zeroes discards_left on blind select (card.lua:2905), which switches off every discard-gated joker for the rest of the run: Banner, Delayed Gratification, Castle, Yorick, Hit the Road, Burnt, Mail-In Rebate, Faceless, Trading Card, and it wipes the discards Drunkard and Merry Andy hand you. Delayed Gratification is the cruellest -- "no discards used" reads like a synergy, but the code also requires discards_left > 0, so it pays exactly $0. * Vampire strips enhancements in the `before` pass, ahead of the per-card `individual` pass where enhancement payoffs are read, so it eats what Golden Ticket, Glass, Stone Joker, Marble and Driver's License are counting. * Joker Stencil scales on EMPTY slots, so Riff-Raff and Abstract Joker fight it. * Splash makes every played card score, so a face card that used to be a harmless kicker now resets Ride the Bus. New cautions (3 -> 15): the positional traps (Blueprint dead in the rightmost slot, Brainstorm in the leftmost), Mime doing nothing without a held-card payoff, and the decay timers, whose numbers are exact from game.lua (Ice Cream 100/5 = 20 hands, Popcorn 20/4 = 5 rounds). Also corrects the stale game path in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
"Joker Combo Advisor" — a Steamodded (SMODS) mod for Balatro that scores shop/pack jokers against the player's current jokers, shows a "Combo Advisor" tooltip on hover, and pulses strongly recommended cards. Pure Lua, no build step.
Commands
Syntax check, then run the test suite — always both, after every edit:
luac -p main.lua synergies.lua config.lua test.lua
lua test.lua
test.lua stubs the handful of game globals main.lua touches (the game cannot be
launched headlessly here) and then exercises the real engine, the real database and
the real catalog tab builders. It is the only safety net; there is no other harness.
It enforces, among other things:
- Score tiers: famous explicit pair + tags = 6, hand-type/suit cluster = 4, single tag match = 2, generic mult+xmult complement = +1 on top, unrelated = 0.
- No orphan wants — a
wantstag nothing gives is a payoff with no enabler. - No dead gives — a non-engine tag given but never wanted can never change a
score, so it is invisible dead weight. (This caught
hand_level, given by Space and Burnt Joker and wanted by nobody.) - Every theme is discoverable — theme discovery needs a giver and a wanter on two
different fielded jokers, so a theme with fewer than two members is an
achievement no player can ever earn and pins the Progress tab under 100% forever.
(This caught
four_kind, whose only member was The Family.) theme_infoandTHEME_TABScover exactly the same tag set.- Pair/clash/caution keys all exist; every blurb fits its ~34-char row.
- Every card the catalog pages emplace is materialized (see the UI notes below).
- Jokers with no tags at all are listed in
EXPECTED_INERTon purpose — a joker landing there by accident scores 0 against the entire game and says nothing.
To poke at the engine by hand, copy the stub block from the top of test.lua.
In-game verification (this machine)
- The mod is symlinked into the live Mods folder:
~/.local/share/Balatro/Mods/JokerComboAdvisor -> ~/Documents/JokerComboAdvisor(this repo). Edits here go live on next game launch — do not copy files anywhere. Lovely reads that folder asC:\users\steamuser\AppData\Roaming\Balatro\Mods. - Game: Steam/Proton at
~/.local/share/Steam/steamapps/common/Balatro. Lovely injector + Steamodded 1.0.0-beta are already installed; mods are managed via Balatro Mod Manager. - Crash/load logs:
~/.local/share/Balatro/Mods/lovely/log/(newest file). - Remote:
https://git.aleshym.co/funman300/JokerComboAdvisor(private, user's Gitea). Pushes authenticate via the repo-local credential helper, which reads the token from~/.config/tea/config.ymlat push time — never copy the token into the repo or the remote URL. Commit per feature.
Architecture
Three-layer split; JCA is the single global namespace:
synergies.lua— data only. Returns{ jokers = {...}, pairs = {...} }. Each of the 150 vanilla jokers getsgives(capabilities it adds) andwants(capabilities that strengthen it) tag lists, converted to sets at the bottom of the file.PAIRSlists famous explicit combos worth a flat +4.main.lua— engine + game integration:- Scoring:
JCA.score(a, b)= explicit pair bonus (+4) + 2 per give→want tag match in each direction + 1 for the generic "+Mult/+Chips source with an ×Mult joker" complement (second return value flags when that +1 applied).JCA.partners_for(card)aggregates overG.jokers.cards; total ≥JCA.config.threshold⇒ recommended. The generic +1 counts toward the total once per candidate, not per owned joker — since tag/pair scores are even and the threshold is 4, the generic bonus alone can never trigger a recommendation, so a pulsing card always has a nameable partner. Jokers whose multiplier the generic rule misreads opt out with{no_generic = true}insynergies.lua(Joker Stencil: its ×Mult grows with empty slots, so "+Mult jokers feed it" is backwards). - UI hooks: wraps
Card:generate_UIBox_ability_tableto append a tooltip entry toaut.info(format: array of rows-of-UIT-nodes plus a.namestring — must match what vanillainfo_tip_from_rowsexpects), and wrapsCard:updatefor the ~2.5s pulse on recommended shop cards and the partner highlight (hovering a joker juices its owned partners ~0.9s; partner set cached on the card asjca_hl, dropped when hover ends). All hook bodies are pcall-wrapped so a scoring bug can never crash a run — keep it that way. - Sell advisor:
JCA.weakest_link()(full board + ≥3 jokers + a strict minimum required, else nil) flags the lowest-total owned joker with a red verdict row; suppressed by learning mode, toggled byconfig.sell_advisor. - Public API for other mods:
JCA.register(key, gives, wants, opts),JCA.register_pair(a, b, blurb)(setsJCA._pairs_dirtyso the Combos tab re-sorts),JCA.register_clash(a, b, warning). Unknown tags are dropped with a log line, never an error. - Config tab: registered on
SMODS.current_mod.config_tab, guarded byif SMODS.current_modso standalone tests don't need full stubs. - Synergy catalog:
SMODS.current_mod.extra_tabsadds Combos/Engine/ Economy/Hands tabs (plus a text-only Progress tab with discovery completion bars) that render realCardobjects inCardAreas (collection-style, so hover shows each joker's own tooltip). One theme per page; pagers swap thetab_contentsUIBox from an option-cycle callback — the same pattern as SMODS's achievements tab. Because pages contain live Card/CardArea objects, tab definitions must be rebuilt on every call — never cache the node trees. Cards must bestart_materialized as they are emplaced (card_row_node), exactly as every vanilla collection page does — that call is the appear animation, and without it cards pop in with no animation on tab open and page switch. Only the first card of a page build plays the sound (vanilla'si>1 or j>1silence flag); tab defs callbegin_page()so the counter spans the page, not one row. The same tabs open in-run via a "Combos" HUD button (wrappedcreate_UIBox_HUD, injected into thebutton_areanode under Run Info/Options); the pagers work there because vanillacreate_tabsalso names its bodytab_contents. - Discovery:
CardArea:emplacehook callsJCA.check_discoverieswhen a joker lands inG.jokers; famous pairs fielded together persist inconfig.discovered(saved immediately viaSMODS.save_mod_config) and per-run inG.GAME.jca_run_combos. Undiscovered pairs render face-down with a???caption in the Combos tab; blurb replaces it once fielded. Themes (everytheme_infotag) persist inconfig.themes_foundonce two DIFFERENT jokers connect the tag (giver + wanter; a joker carrying both sides needs a second joker). One toast per emplacement, famous pair outranking theme. Badge/progress render intheme_tab_def; toast names come fromJCA.theme_names(filled after THEME_TABS, used only at gameplay time). - Post-run recap: wraps
create_UIBox_game_over/create_UIBox_winand inserts a "Combos fielded this run" row (fromG.GAME.jca_run_combos) relative to vanilla button ids (from_game_over;from_game_wonorwin_cta) via a parent-trail walk — anchor depths differ per screen. - Learning mode (
config.learning_mode): suppresses the pulse and the "Strong pick!" line but never the explanations. No-synergy buy-area tooltips show "Looking for:"/"Offers:" hints fromTAG_LABEL.synergies.luaalso exportstheme_info(one teaching sentence per catalog tag — keep ≤ ~90 chars and cover every THEME_TABS tag). - The real (Lovely-patched) game source is readable at
~/.local/share/Balatro/Mods/lovely/dump/— check it before assuming what a vanilla function returns or expects.
- Scoring:
config.lua— default settings (pulse,owned_tooltip,threshold). Steamodded loads and persists this table itself; read it only viaSMODS.current_mod.config(exposed asJCA.config).
Conventions and gotchas
- Cluster tags (hand types
pair/straight/flush/…, suits,planet,spectral_gen): put the same tag in bothgivesandwantsof every member so members mutually reinforce (2+2 = 4). Pure enablers (e.g. Four Fingers, Smeared, Pareidolia, DNA) onlygive; pure payoffs onlywant. - A tag needs both sides to exist, on at least two different jokers: something
must give it, something must want it, or it is dead data that can never change a
score — and if it is also a theme, an achievement nobody can earn.
lua test.luaenforces both directions; run it after everysynergies.luaedit. - Tooltips teach:
JCA.explain(hovered, partner)picks one reason per partner — famous-pair blurb (third element of eachPAIRSentry), else the firstTAG_ORDERmatch phrased fromTAG_TEXT(give/want/both variants), else the generic mult×xmult line. Every non-engine tag (anything besides mult/chips/xmult) MUST have aTAG_TEXTentry; newPAIRSentries need a blurb under ~34 chars so name + blurb fits one tooltip row. - Vanilla center keys contain intentional misspellings/abbreviations — do not
"fix" them:
j_gluttenous_joker,j_selzer,j_ticket(Golden Ticket),j_trousers(Spare Trousers),j_ring_master(Showman),j_caino(Canio),j_delayed_grat,j_todo_list. - Unknown joker keys (from other mods) must keep scoring 0 — never index
JCA.db[key]without a nil guard. - Display names go through
localize{type='name_text', set='Joker', ...}with a pcall +center.namefallback. - Anti-synergies never affect scores. Known traps are warning-only data:
CLASHES(pairwise, red "Clashes -" tooltip line vs owned jokers) andCAUTIONS(per-joker, buy-area "Caution:" line) insynergies.lua, same ~34-char blurb budget asPAIRS. Learning mode keeps warnings visible. Verify every new trap against the dump before adding it — a warning that is merely plausible is worse than none. The mod once shipped aj_smeared+j_flower_potclash that was exactly backwards: Flower Pot fills its four suit slots with a greedyif/elseifchain (card.lua:4256) and Smeared makes Hearts answer for Diamonds, so any two red + two black cards fill the pot. It is one of the best combos in the game, and the mod was warning players off it. Most traps hinge on the scoring-pipeline order infunctions/state_events.lua(before→ per-cardindividual→joker_main): Vampire strips enhancements inbefore, so every enhancement payoff that reads them inindividual— Golden Ticket, Glass, Stone, Driver's License — is looking at cards it has already eaten. - The UI hooks are the only untested-in-game surface; if a tooltip regression is
reported, suspect the
aut.infoentry format first and check the lovely log.