Files
JokerComboAdvisor/CLAUDE.md
T
funman300 e37b56471b Cap generic mult/xmult bonus per candidate; opt Stencil out of it
Any xMult joker used to pulse 'recommended' on a board of four unrelated
+Mult/+Chips jokers: the generic engine-complement +1 in JCA.score
accumulated once per owned joker in partners_for, reaching the threshold
of 4 with zero listable partners (pairwise scores never hit 2), so the
card pulsed while its own tooltip said 'No synergy with your jokers.'

- partners_for now counts the generic +1 once per candidate. Tag/pair
  scores are even, so the generic bonus can no longer flip a
  recommendation at all; a pulsing card always has a nameable partner.
- Joker Stencil is the one xMult joker the generic rule actively
  misreads (its multiplier grows with EMPTY slots), so entries support
  {no_generic = true} which suppresses the rule in score() and the
  generic fallback lines in explain(). Stencil also gains copy_target:
  its real famous synergy is being copied by Blueprint/Brainstorm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:51:54 -07:00

8.0 KiB
Raw Blame History

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 everything:

luac -p main.lua synergies.lua config.lua

There is no test framework. Test the engine standalone by stubbing the game globals before loading main.lua (the game itself cannot be launched headlessly here):

lua - <<'EOF'
SMODS = { load_file = function(f) return loadfile(f) end,
          current_mod = { config = dofile('config.lua') } }
Card = { generate_UIBox_ability_table = function() end, update = function() end }
G = { UIT = {ROOT='ROOT', R='R', T='T'},
      C = { UI = {TEXT_DARK='dark'}, GREEN='green', CLEAR='clear' },
      STAGES = {RUN=1}, FUNCS = {} }
dofile('main.lua')
print(JCA.score('j_baron', 'j_mime'))            -- pairwise score
-- simulate owned jokers via G.jokers.cards; each card needs
-- { config = { center = { key=..., name=..., set='Joker' } } }
EOF

Expected score tiers: famous explicit pair + tags ≈ 6, hand-type/suit cluster = 4, single tag match = 2, generic mult+xmult complement = 1, unrelated = 0.

In-game verification (this machine)

  • The mod is symlinked into the live Mods folder: ~/.local/share/Balatro/Mods/JokerComboAdvisor -> ~/Documents/Balatro Mod. Edits here go live on next game launch — do not copy files anywhere.
  • Game: Steam/Proton at /mnt/games/Steam/steamapps/common/Balatro (note: second Steam library, not the default paths). 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.yml at 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 gets gives (capabilities it adds) and wants (capabilities that strengthen it) tag lists, converted to sets at the bottom of the file. PAIRS lists 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 over G.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} in synergies.lua (Joker Stencil: its ×Mult grows with empty slots, so "+Mult jokers feed it" is backwards).
    • UI hooks: wraps Card:generate_UIBox_ability_table to append a tooltip entry to aut.info (format: array of rows-of-UIT-nodes plus a .name string — must match what vanilla info_tip_from_rows expects), and wraps Card:update for the ~2.5s pulse on recommended shop cards. Both hook bodies are pcall-wrapped so a scoring bug can never crash a run — keep it that way.
    • Config tab: registered on SMODS.current_mod.config_tab, guarded by if SMODS.current_mod so standalone tests don't need full stubs.
    • Synergy catalog: SMODS.current_mod.extra_tabs adds Combos/Engine/ Economy/Hands tabs that render real Card objects in CardAreas (collection-style, so hover shows each joker's own tooltip). One theme per page; pagers swap the tab_contents UIBox 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. The same tabs open in-run via a "Combos" HUD button (wrapped create_UIBox_HUD, injected into the button_area node under Run Info/Options); the pagers work there because vanilla create_tabs also names its body tab_contents.
    • Discovery: CardArea:emplace hook calls JCA.check_discoveries when a joker lands in G.jokers; famous pairs fielded together persist in config.discovered (saved immediately via SMODS.save_mod_config) and per-run in G.GAME.jca_run_combos. Undiscovered pairs render face-down with a ??? caption in the Combos tab; blurb replaces it once fielded. Themes (every theme_info tag) persist in config.themes_found once 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 in theme_tab_def; toast names come from JCA.theme_names (filled after THEME_TABS, used only at gameplay time).
    • Post-run recap: wraps create_UIBox_game_over/create_UIBox_win and inserts a "Combos fielded this run" row (from G.GAME.jca_run_combos) relative to vanilla button ids (from_game_over; from_game_won or win_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 from TAG_LABEL. synergies.lua also exports theme_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.
  • config.lua — default settings (pulse, owned_tooltip, threshold). Steamodded loads and persists this table itself; read it only via SMODS.current_mod.config (exposed as JCA.config).

Conventions and gotchas

  • Cluster tags (hand types pair/straight/flush/…, suits, planet, spectral_gen): put the same tag in both gives and wants of every member so members mutually reinforce (2+2 = 4). Pure enablers (e.g. Four Fingers, Smeared, Pareidolia) only give; pure payoffs only want.
  • Every wants tag must be given by at least one joker — the standalone test's orphan-tag check enforces this idea; re-run it after database edits.
  • Tooltips teach: JCA.explain(hovered, partner) picks one reason per partner — famous-pair blurb (third element of each PAIRS entry), else the first TAG_ORDER match phrased from TAG_TEXT (give/want/both variants), else the generic mult×xmult line. Every non-engine tag (anything besides mult/chips/xmult) MUST have a TAG_TEXT entry; new PAIRS entries 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.name fallback; anti-synergies are deliberately not modeled.
  • The UI hooks are the only untested-in-game surface; if a tooltip regression is reported, suspect the aut.info entry format first and check the lovely log.