Files
JokerComboAdvisor/CLAUDE.md
T
funman300 bf0672a438 Correct the push-auth note: libsecret, not the tea token
Pushes authenticate through the global libsecret credential helper. The
tea login has auth_method oauth and no token at all, so it cannot create
repos or call the API - it was never what authenticated pushes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:11:49 -07:00

210 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**:
```sh
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 `wants` tag 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_info` and `THEME_TABS` cover 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_INERT` on 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 as `C:\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). `git push` just works: it authenticates through the global libsecret
credential helper (`credential.helper=/usr/lib/git-core/git-credential-libsecret`).
Never copy a token into the repo or the remote URL. Commit per feature.
- The `tea` CLI is installed and logged in as `funman300`, but its stored login has
`auth_method: oauth` and **no token**, so `tea` cannot create repos or call the
API — it is not the thing that authenticates pushes, despite what this file used
to say. Creating a new remote repo needs the web UI (or a token added to `tea`).
## 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 and the
partner highlight (hovering a joker juices its owned partners ~0.9s;
partner set cached on the card as `jca_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 by
`config.sell_advisor`.
- Public API for other mods: `JCA.register(key, gives, wants, opts)`,
`JCA.register_pair(a, b, blurb)` (sets `JCA._pairs_dirty` so 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 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 (plus a text-only Progress tab with discovery
completion bars) that render real `Card` objects in `CardArea`s
(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. Cards must be `start_materialize`d
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's `i>1 or j>1` silence flag); tab defs
call `begin_page()` so the counter spans the page, not one row. 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, DNA) only `give`; pure payoffs only `want`.
- 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.lua`
enforces both directions; run it after every `synergies.lua` edit.
- 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 never affect scores. Known traps are warning-only data:
`CLASHES` (pairwise, red "Clashes -" tooltip line vs owned jokers) and
`CAUTIONS` (per-joker, buy-area "Caution:" line) in `synergies.lua`, same
~34-char blurb budget as `PAIRS`. 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 a `j_smeared` +
`j_flower_pot` clash that was exactly backwards: Flower Pot fills its four
suit slots with a greedy `if/elseif` chain (`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 in `functions/state_events.lua`
(`before` → per-card `individual``joker_main`): Vampire strips enhancements
in `before`, so every enhancement payoff that reads them in `individual`
Golden Ticket, Glass, Stone, Driver's License — is looking at cards it has
already eaten.
- The UI hooks used to be the untested surface. They are now covered in the real
game by the smoke harness (see below). If a tooltip regression is reported,
run it, then suspect the `aut.info` entry format and check the lovely log.
## In-game smoke tests
`lua test.lua` stubs the game, so it cannot tell you that vanilla accepted the
tooltip format or that a card is actually animating. The smoke harness runs the
real engine — real `Card` objects, the real `Card:generate_UIBox_ability_table`
pipeline, a real overlay `UIBox` — with no clicking:
```sh
~/Documents/BalatroSmoke/run.sh JokerComboAdvisor
```
It boots the game, runs `scenarios/JokerComboAdvisor.lua`, quits, and prints a
pass/fail report (non-zero exit on failure). Close Balatro first; it needs a clean
boot. Scenarios live in the harness repo, not here.
It covers the surfaces this repo's tests cannot reach: the hover tooltip naming a
partner, the clash and caution lines, discovery on emplace, and — the reason it
exists — that catalog cards really do `start_materialize` on tab build *and* on
page switch (asserted via `card.dissolve > 0` in the live UIBox).
The harness suppresses every save path and verifies from outside that no discovery
state leaked, so running it can never spoil the catalog. Do not "fix" that by
reversing writes after the fact — a crashed scenario would skip the reversal.