From 8ce561927319f8f61a30e3ec3a8501c514dfdfcd Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 14 Jul 2026 12:36:01 -0700 Subject: [PATCH] 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) --- AUDIT.md | 31 +++++++---------- audit.lua | 95 ++++++++++++++++++++++++++++----------------------- main.lua | 49 ++++++++++++++++++++++++-- synergies.lua | 25 ++++++++++++++ test.lua | 22 ++++++++++++ 5 files changed, 158 insertions(+), 64 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index 5028f57..d08f38c 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -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 diff --git a/audit.lua b/audit.lua index 44d2543..7d8a0dc 100644 --- a/audit.lua +++ b/audit.lua @@ -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 -------------------------------------------------------------------------------- diff --git a/main.lua b/main.lua index 6b3689a..f078627 100644 --- a/main.lua +++ b/main.lua @@ -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 diff --git a/synergies.lua b/synergies.lua index 762291c..45b75a5 100644 --- a/synergies.lua +++ b/synergies.lua @@ -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 diff --git a/test.lua b/test.lua index f3bcddb..2759364 100644 --- a/test.lua +++ b/test.lua @@ -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') --------------------------------------------------------------------------------