Chase the audit's leads: verify, implement, or reject

Every lead in AUDIT.md, traced to the running source and then settled.

Ceremonial Dagger DESTROYS the joker to its right at blind select
(card.lua:2945) and skips Eternals. The card says so; what it cannot say
is WHICH of your jokers is about to die. JCA.dagger_victim names it in
the tooltip -- "WILL DESTROY Baron at the next blind" -- while there is
still time to reorder, and stays quiet when the neighbour is Eternal.

Four jokers that scored 0 against all 150 now have partners, all verified
in card.lua rather than recalled:
* Burglar + Ramen: Ramen decays per discarded card and is DESTROYED at x1
  (SMODS.destroy_cards). Burglar zeroes discards, so it never decays and
  never dies. Same for Burglar + Green Joker, which is only punished by
  discarding.
* Trading Card + Erosion: Erosion pays per card MISSING from the deck,
  and Trading Card permanently destroys the card it discards.
* Chaos the Clown + Flash Card: a free reroll every shop, and a joker
  that grows per reroll. Both were inert.

Erosion is the mirror of card_gen, so it now clashes with every joker
that permanently adds a card: DNA, Marble, Certificate.

Rejected, with the reasoning recorded so nobody "fixes" it later:
Card Sharp and Supernova reward replaying a hand and Obelisk punishes it,
but neither destroys the other's precondition -- the player's hand choice
does. That is an opposed incentive, not an opposed effect. The clash list
only earns trust while every entry is mechanical.

The audit's own silent-check was wrong: it flagged Square Joker, which
has been paired with Four Fingers all along, and I duplicated that pair
before the suite caught it. Silence now means no tags AND no famous pair.

711 pass locally, 10 in-game.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-14 12:36:01 -07:00
parent 96a00d0d0c
commit 8ce5619273
5 changed files with 158 additions and 64 deletions
+12 -19
View File
@@ -11,11 +11,11 @@ does not get redone from scratch every session.
| | |
|---|---|
| Jokers in the database | 150 |
| Famous pairs | 32 |
| Clashes | 22 |
| Famous pairs | 36 |
| Clashes | 25 |
| Cautions | 15 |
| Themes | 29 |
| Jokers in no famous pair | 108 |
| Jokers in no famous pair | 102 |
## Jokers the advisor is silent about
@@ -25,8 +25,8 @@ answer for some of them -- but it should be a decision, not an accident.
| | count |
|---|---|
| Silent, reviewed, staying that way | 20 |
| Silent, but a lead worth chasing | 12 |
| Silent, reviewed, staying that way | 25 |
| Silent, but a lead worth chasing | 0 |
| **Silent and never looked at** | **0** |
### Leads worth chasing
@@ -34,22 +34,13 @@ answer for some of them -- but it should be a decision, not an accident.
Unverified. Check each against the Lovely dump before acting on it -- a wrong
synergy is worse than a missing one.
- **`j_baseball`** — xMult per Uncommon joker owned. We have no rarity tag, and adding one for a single payoff may not earn its keep.
- **`j_card_sharp`** — xMult when the hand type was already played this round -- it rewards repeating one hand, which is exactly what Obelisk punishes. Clash?
- **`j_ceremonial`** — positional like Blueprint: it DESTROYS the joker to its right. JCA.copy_source already reads board order -- the same machinery could warn "will destroy your Baron" before it eats something irreplaceable.
- **`j_chaos`** — free reroll each shop; see j_flash
- **`j_erosion`** — +Mult per card BELOW 52 in the deck, so it is the inverse of card_gen: DNA/Marble/Certificate permanently shrink it. Likely CLASHES, and possibly a synergy with anything that destroys cards (Trading Card).
- **`j_flash`** — +Mult per shop reroll. Chaos the Clown grants a free reroll each shop, which would give both of them a reason to exist (Chaos is inert today).
- **`j_green_joker`** — +Mult per hand, -Mult per discard: Burglar removes discards entirely. Same shape as the existing Burglar + Mystic Summit pair.
- **`j_obelisk`** — resets on your most-played hand, so it fights every hand-type build. Has a CAUTION already; the pairwise clashes may be worth adding.
- **`j_ramen`** — xMult decays per card discarded, so Burglar (zero discards all run) stops the decay dead. Likely a famous pair.
- **`j_ring_master`** — Showman allows duplicates in the pool. Enables copy/duplicate strategies, but we have no tag for "shop pool" effects and inventing one for a single joker is probably not worth it.
- **`j_square`** — +Chips on exactly-4-card hands; Four Fingers makes 4-card flushes and straights playable. Needs a tag that does not exist yet.
- **`j_supernova`** — Mult scales with times the hand was played; see j_card_sharp
### Reviewed, and deliberately silent
- **`j_baseball`** — xMult per Uncommon joker owned. A rarity tag would have exactly one payoff, and every giver would be "is uncommon" -- noise, not synergy.
- **`j_card_sharp`** — REJECTED as a clash with Obelisk. Card Sharp rewards replaying a hand and Obelisk punishes replaying your MOST played hand -- but neither destroys the other precondition; the player's hand choice does. That is an opposed incentive, not an opposed effect. Flagging it would dilute the clash signal, which only earns trust while every entry is mechanical.
- **`j_cavendish`** — the reward for Gros Michel dying, not a partner
- **`j_ceremonial`** — RESOLVED: it destroys the joker to its right at blind select (card.lua:2945), skipping Eternals. JCA.dagger_victim now names the joker about to die, in the tooltip, while there is still time to reorder.
- **`j_chicot`** — disables the boss blind; no joker interaction
- **`j_credit_card`** — lets you go negative; a money buffer, not a combo
- **`j_diet_cola`** — sell for a Double Tag; tags are outside our model
@@ -62,12 +53,14 @@ synergy is worse than a missing one.
- **`j_luchador`** — sell to disable a boss; no joker interaction
- **`j_misprint`** — random +Mult; interacts with nothing
- **`j_mr_bones`** — death insurance; no joker interaction
- **`j_mystic_summit`** — already paired with Burglar (zero discards, max Mult)
- **`j_obelisk`** — The trap is real but it is a CAUTION ("resets on your most played hand"), not a set of pairwise clashes. See j_card_sharp.
- **`j_popcorn`** — decays; the trap is a CAUTION
- **`j_red_card`** — +Mult per skipped booster; no joker interaction
- **`j_ride_the_bus`** — scales on face-free hands; the traps are CLASHES
- **`j_ring_master`** — Showman allows duplicates in the shop pool. We have no tag for pool effects, and one joker does not justify inventing one.
- **`j_steel_joker`** — wants STEEL cards. The only enhance_gen givers make Gold (Midas) and Stone (Marble), so tagging it enhance_gen would invent a synergy that does not exist. No vanilla joker creates Steel cards. Leave it alone.
- **`j_stuntman`** — big chips, -2 hand size; the trap is a CAUTION
- **`j_supernova`** — REJECTED as an Obelisk clash for the same reason as j_card_sharp
- **`j_throwback`** — xMult per skipped blind; no joker interaction
## Tag health
@@ -127,5 +120,5 @@ mis-tagged, the error is spread across the whole database.
| `j_four_fingers` | 12 |
| `j_seance` | 12 |
Jokers with **no** partner scoring >= 2 anywhere in the game: 30
Jokers with **no** partner scoring >= 2 anywhere in the game: 25
+53 -42
View File
@@ -51,43 +51,47 @@ local VERDICTS = {
'wants GLASS cards, and no vanilla joker creates them. Same trap as ' ..
'Steel Joker: enhance_gen is too coarse a tag to use here.'},
-- Leads worth chasing. NOT verified against the game source yet.
j_ceremonial = {'candidate',
'positional like Blueprint: it DESTROYS the joker to its right. ' ..
'JCA.copy_source already reads board order -- the same machinery could ' ..
'warn "will destroy your Baron" before it eats something irreplaceable.'},
j_erosion = {'candidate',
'+Mult per card BELOW 52 in the deck, so it is the inverse of card_gen: ' ..
'DNA/Marble/Certificate permanently shrink it. Likely CLASHES, and ' ..
'possibly a synergy with anything that destroys cards (Trading Card).'},
j_ramen = {'candidate',
'xMult decays per card discarded, so Burglar (zero discards all run) ' ..
'stops the decay dead. Likely a famous pair.'},
j_green_joker = {'candidate',
'+Mult per hand, -Mult per discard: Burglar removes discards entirely. ' ..
'Same shape as the existing Burglar + Mystic Summit pair.'},
j_flash = {'candidate',
'+Mult per shop reroll. Chaos the Clown grants a free reroll each shop, ' ..
'which would give both of them a reason to exist (Chaos is inert today).'},
j_chaos = {'candidate', 'free reroll each shop; see j_flash'},
j_square = {'candidate',
'+Chips on exactly-4-card hands; Four Fingers makes 4-card flushes and ' ..
'straights playable. Needs a tag that does not exist yet.'},
j_card_sharp = {'candidate',
'xMult when the hand type was already played this round -- it rewards ' ..
'repeating one hand, which is exactly what Obelisk punishes. Clash?'},
j_supernova = {'candidate', 'Mult scales with times the hand was played; see j_card_sharp'},
j_obelisk = {'candidate',
'resets on your most-played hand, so it fights every hand-type build. ' ..
'Has a CAUTION already; the pairwise clashes may be worth adding.'},
j_baseball = {'candidate',
'xMult per Uncommon joker owned. We have no rarity tag, and adding one ' ..
'for a single payoff may not earn its keep.'},
-- Chased and resolved. These now have pairs, clashes or an advisor line, so
-- they no longer appear as silent -- kept here as the record of the decision.
j_ceremonial = {'settled',
'RESOLVED: it destroys the joker to its right at blind select ' ..
'(card.lua:2945), skipping Eternals. JCA.dagger_victim now names the ' ..
'joker about to die, in the tooltip, while there is still time to reorder.'},
j_erosion = {'settled',
'RESOLVED: pays per card missing from the deck (starting_deck_size - ' ..
'#G.playing_cards, card.lua:4318) -- the exact inverse of card_gen. Now ' ..
'clashes with DNA/Marble/Certificate, and pairs with Trading Card, which ' ..
'permanently destroys the card it discards.'},
j_ramen = {'settled',
'RESOLVED: paired with Burglar. It decays per discarded card and is ' ..
'DESTROYED at x1 (card.lua:3175); Burglar zeroes discards, so it never ' ..
'decays and never dies.'},
j_green_joker = {'settled', 'RESOLVED: paired with Burglar (no discards, only gains)'},
j_flash = {'settled', 'RESOLVED: paired with Chaos the Clown (free rerolls grow it)'},
j_chaos = {'settled', 'RESOLVED: paired with Flash Card; no longer inert'},
j_square = {'settled',
'Not actually a gap -- it was paired with Four Fingers all along. It has ' ..
'no TAGS, which is what made the old silent-check flag it. That check now ' ..
'excludes jokers with a famous pair.'},
j_mystic_summit = {'settled', 'already paired with Burglar (zero discards, max Mult)'},
j_ring_master = {'candidate',
'Showman allows duplicates in the pool. Enables copy/duplicate strategies, ' ..
'but we have no tag for "shop pool" effects and inventing one for a single ' ..
'joker is probably not worth it.'},
-- Chased and REJECTED. Do not "fix" these; the reasoning is the point.
j_card_sharp = {'settled',
'REJECTED as a clash with Obelisk. Card Sharp rewards replaying a hand ' ..
'and Obelisk punishes replaying your MOST played hand -- but neither ' ..
'destroys the other precondition; the player\'s hand choice does. That is ' ..
'an opposed incentive, not an opposed effect. Flagging it would dilute the ' ..
'clash signal, which only earns trust while every entry is mechanical.'},
j_supernova = {'settled', 'REJECTED as an Obelisk clash for the same reason as j_card_sharp'},
j_obelisk = {'settled',
'The trap is real but it is a CAUTION ("resets on your most played hand"), ' ..
'not a set of pairwise clashes. See j_card_sharp.'},
j_baseball = {'settled',
'xMult per Uncommon joker owned. A rarity tag would have exactly one ' ..
'payoff, and every giver would be "is uncommon" -- noise, not synergy.'},
j_ring_master = {'settled',
'Showman allows duplicates in the shop pool. We have no tag for pool ' ..
'effects, and one joker does not justify inventing one.'},
}
--------------------------------------------------------------------------------
@@ -136,20 +140,27 @@ for key, e in pairs(JCA.db) do
end
end
-- "Silent": nothing but engine roles to give, and nothing wanted. Such a joker
-- can only ever earn the generic +1, so the advisor has nothing to say about it.
local in_pair = {}
for _, p in ipairs(data.pairs) do in_pair[p[1]] = true; in_pair[p[2]] = true end
-- "Silent": nothing but engine roles to give, nothing wanted, AND no famous pair.
-- Such a joker can only ever earn the generic +1, so the advisor has nothing to
-- say about it against any of the 150.
--
-- The pair check matters: without it this list flags jokers the advisor speaks up
-- about perfectly well (Square Joker has no tags but has been paired with Four
-- Fingers all along), and a lead gets "found" that was never lost.
local silent = {}
for key, e in pairs(JCA.db) do
local has_theme, wants = false, 0
for t in pairs(e.gives) do if not ENGINE[t] then has_theme = true end end
for _ in pairs(e.wants) do wants = wants + 1 end
if not has_theme and wants == 0 then silent[#silent + 1] = key end
if not has_theme and wants == 0 and not in_pair[key] then
silent[#silent + 1] = key
end
end
table.sort(silent)
local in_pair = {}
for _, p in ipairs(data.pairs) do in_pair[p[1]] = true; in_pair[p[2]] = true end
--------------------------------------------------------------------------------
-- Report
--------------------------------------------------------------------------------
+46 -3
View File
@@ -191,6 +191,34 @@ function JCA.copy_source(card)
return target, center.blueprint_compat ~= false
end
-- Ceremonial Dagger is positional too, and it is the dangerous one: when the
-- blind is selected it DESTROYS the joker to its right and eats twice its sell
-- value as Mult (card.lua:2945). The card text says so; what it cannot tell you
-- is *which joker on your board* is about to die. Vanilla skips Eternal jokers,
-- so an Eternal neighbour is safe.
--
-- Returns the joker it will eat (or nil), and whether that joker is safe.
function JCA.dagger_victim(card)
if not (G.jokers and G.jokers.cards and card.config and card.config.center) then
return nil
end
if card.config.center.key ~= 'j_ceremonial' then return nil end
local cards, idx = G.jokers.cards, nil
for i, c in ipairs(cards) do
if c == card then idx = i break end
end
if not idx then return nil end
local target = cards[idx + 1]
if not target or not (target.config and target.config.center) then return nil end
local eternal = target.ability and target.ability.eternal
if SMODS and SMODS.is_eternal then
local ok, res = pcall(SMODS.is_eternal, target, card)
if ok then eternal = res end
end
return target, not not eternal
end
-- Sell advisor: on a full board, the owned joker contributing the least
-- total synergy to the rest is the natural cut when something better shows
-- up. Returns nil when slots are free, the board is tiny, or every joker
@@ -434,10 +462,11 @@ local function tooltip_rows(card)
local caution = in_buy_area(card) and JCA.cautions[key]
if caution then text_row('Caution: ' .. caution, G.C.RED) end
-- Copy jokers: say what this one is copying RIGHT NOW. The board order decides
-- it, and a Blueprint in the wrong slot is a dead card that looks fine. This is
-- an explanation, not a verdict, so learning mode keeps it.
-- Positional jokers: say what this one is doing to its neighbour RIGHT NOW.
-- The board order decides it, so only an owned joker has an answer. These are
-- explanations, not verdicts, so learning mode keeps them.
if card.area == G.jokers then
-- Copy jokers. A Blueprint in the wrong slot is a dead card that looks fine.
local target, copyable = JCA.copy_source(card)
if not target then
if COPIERS[key] then
@@ -451,6 +480,20 @@ local function tooltip_rows(card)
else
text_row('Copying ' .. name_of(target.config.center.key), G.C.GREEN)
end
-- Ceremonial Dagger. This one is about to eat a joker, permanently, at the
-- next blind — worth shouting about while there is still time to reorder.
local victim, safe = JCA.dagger_victim(card)
if victim then
local vname = name_of(victim.config.center.key)
if safe then
text_row(vname .. ' is Eternal - the blade cannot eat it', G.C.GREEN)
else
text_row('WILL DESTROY ' .. vname .. ' at the next blind', G.C.RED)
end
elseif key == 'j_ceremonial' then
text_row('Nothing to its right - the blade goes hungry', G.C.GREEN)
end
end
-- Sell advisor verdict (owned jokers only; suppressed in learning mode
+25
View File
@@ -239,6 +239,25 @@ local PAIRS = {
-- two red + two black cards fill the pot. It is a top-tier combo, and this
-- mod used to warn against it.
{'j_smeared', 'j_flower_pot', 'any 2 red + 2 black fill it'},
-- Burglar zeroes discards_left for the whole run (card.lua:2905). That is a
-- disaster for nine jokers (see CLASHES) but a gift to the two that are
-- PUNISHED by discarding -- same shape as the Burglar + Mystic Summit pair.
-- Ramen is the sharper one: it decays x0.01 per discarded card and is
-- DESTROYED when it reaches x1 (SMODS.destroy_cards, card.lua:3175). No
-- discards means it never decays and never dies.
{'j_burglar', 'j_ramen', 'no discards, no decay'},
{'j_burglar', 'j_green_joker', 'no discards, only gains'},
-- Erosion pays per card MISSING from the deck (starting_deck_size -
-- #G.playing_cards, card.lua:4318), and Trading Card permanently destroys the
-- card it discards (remove = true, card.lua:3214). Thinning the deck is
-- exactly what Erosion is paid for.
{'j_trading', 'j_erosion', 'destroyed cards feed Erosion'},
-- Flash Card gains Mult per shop reroll; Chaos the Clown hands you a free one
-- every shop. Both jokers were inert in this database until now.
{'j_chaos', 'j_flash', 'free rerolls grow Flash Card'},
}
-- Known traps: pairs that actively hurt each other. Warning-only — they
@@ -280,6 +299,12 @@ local CLASHES = {
-- Splash makes every played card score, so a face card that used to be a
-- harmless kicker now lands in the scoring hand and resets the counter.
{'j_splash', 'j_ride_the_bus', 'any played face card resets it'},
-- Erosion is the mirror image of card_gen: it pays per card MISSING from the
-- deck, so every joker that permanently ADDS one shrinks its Mult for good.
{'j_erosion', 'j_dna', 'added cards shrink its Mult'},
{'j_erosion', 'j_marble', 'each Stone shrinks its Mult'},
{'j_erosion', 'j_certificate', 'added cards shrink its Mult'},
}
-- Per-joker cautions for the buy-area tooltip: the joker is fine on its
+22
View File
@@ -302,6 +302,28 @@ eq(copyable, false, 'and flags that vanilla will not copy it (blueprint_compat)'
local fine = field('j_blueprint', 'j_baron')
eq(select(2, JCA.copy_source(fine[1])), true, 'a copyable target reports as copyable')
--------------------------------------------------------------------------------
section('Engine: Ceremonial Dagger names its victim')
--------------------------------------------------------------------------------
-- It destroys the joker to its RIGHT at the next blind, permanently.
local dagger = field('j_baron', 'j_ceremonial', 'j_mime')
local victim, safe = JCA.dagger_victim(dagger[2])
eq(victim, dagger[3], 'it names the joker on its right as the victim')
eq(safe, false, 'and an ordinary joker is not safe from it')
field('j_baron', 'j_mime', 'j_ceremonial')
eq(JCA.dagger_victim(G.jokers.cards[3]), nil, 'a rightmost Dagger eats nothing')
field('j_ceremonial', 'j_baron')
eq(JCA.dagger_victim(G.jokers.cards[2]), nil, 'a non-Dagger has no victim')
-- Vanilla refuses to slice Eternal jokers, so the advisor must not cry wolf.
local eternal = field('j_ceremonial', 'j_baron')
eternal[2].ability = {eternal = true}
local _, is_safe = JCA.dagger_victim(eternal[1])
eq(is_safe, true, 'an Eternal neighbour is reported as safe')
--------------------------------------------------------------------------------
section('Engine: the sell advisor weighs clashes')
--------------------------------------------------------------------------------