Add a test suite, and fix the three dead-data bugs it found

There was no test file -- CLAUDE.md described the harness in prose and
referred to an "orphan-tag check" that did not exist. test.lua now stubs
the game globals and exercises the real engine, database and catalog tab
builders. It found three bugs on its first run:

* four_kind was the only theme in the catalog that could never be
  discovered. Theme discovery needs a giver and a wanter on two DIFFERENT
  fielded jokers, and The Family was its sole member -- so the Hands bar
  and the overall Progress bar could never reach 100%. DNA is the real
  enabler (copying a card is how you stack duplicate ranks, the only
  vanilla route to a reliable Four of a Kind), so it now gives the
  matching-hand tags as a pure enabler.

* hand_level was given by Space Joker and Burnt Joker and wanted by
  nobody, so it could never contribute to a score and its "want" tooltip
  text was unreachable. Nothing in vanilla rewards hand levels; the tag
  is dropped rather than faked.

* Invisible Joker had no tags at all, so it scored 0 against the entire
  game. Selling it duplicates one of your Jokers -- the same "stronger
  target, better payoff" logic Blueprint and Brainstorm already model --
  so it now wants copy_target.

The suite guards both directions of the tag contract (no orphan wants, no
dead gives), theme reachability, blurb widths, the inert-joker list, and
the card materialize fix from the previous commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-14 11:15:10 -07:00
parent f8bba13f28
commit 07cada9e49
4 changed files with 447 additions and 36 deletions
+41 -29
View File
@@ -10,38 +10,42 @@ and pulses strongly recommended cards. Pure Lua, no build step.
## Commands
Syntax check everything:
Syntax check, then run the test suite — **always both, after every edit**:
```sh
luac -p main.lua synergies.lua config.lua
luac -p main.lua synergies.lua config.lua test.lua
lua test.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):
`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:
```sh
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
```
- 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.
Expected score tiers: famous explicit pair + tags ≈ 6, hand-type/suit cluster = 4,
single tag match = 2, generic mult+xmult complement = 1, unrelated = 0.
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/Balatro Mod`.
Edits here go live on next game launch — do not copy files anywhere.
`~/.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 `/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.
@@ -96,10 +100,16 @@ Three-layer split; `JCA` is the single global namespace:
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`.
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
@@ -132,9 +142,11 @@ Three-layer split; `JCA` is the single global namespace:
- **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.
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