Fix enabler phrasing, wake two silent jokers, read the board order

* The tooltip mis-taught every pure enabler. JCA.explain used a cluster
  tag's `both` text whenever EITHER side was a full member, so Four
  Fingers next to Runner claimed "both reward Straights" -- Four Fingers
  does not reward Straights, it makes them reachable. `both` now requires
  both sides, and every cluster tag carries give/want phrasings.

* Blue Joker and Abstract Joker scored 0 against all 150 jokers. Blue
  Joker scales on #G.deck.cards, so it wants card_gen (DNA, Marble,
  Certificate); Abstract scales on #G.jokers.cards, so it wants joker_gen
  (Riff-Raff is +6 Mult on its own). Both verified in card.lua. Steel and
  Glass Joker look like the same fix but are NOT: they need Steel/Glass
  specifically, and the only enhance_gen givers make Gold and Stone, so
  tagging them would invent a synergy that does not exist.

* Copy jokers are positional and the board order was there all along.
  JCA.copy_source reports what an owned Blueprint/Brainstorm is actually
  copying, including when the target is one of the 29 centers vanilla
  refuses to copy (blueprint_compat = false), or when it is copying
  nothing at all.

* The sell advisor now counts clashes when ranking what to cut. JCA.score
  stays pure -- a trap must never make a card look like a combo -- but a
  joker eating another one's payoff is exactly the one to sell.

The suite caught a real bug while writing this: `(mode == 'right') and
cards[idx+1] or cards[1]` falls through to the LEFTMOST joker when the
right-hand slot is nil, so a rightmost Blueprint would have named the
wrong card. 673 pass here, 9 in-game via the smoke harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-14 12:24:33 -07:00
parent bf0672a438
commit e688de0efb
4 changed files with 226 additions and 19 deletions
+86 -2
View File
@@ -87,8 +87,13 @@ function JCA.explain(hovered, partner)
for _, t in ipairs(data.tag_order) do
local text = data.tag_text[t]
if text then
-- "both reward Straights" only reads true when BOTH jokers sit on both
-- sides of the tag. A pure enabler (Four Fingers, DNA) gives it without
-- wanting it, so it gets the give/want phrasing instead — it does not
-- reward the hand, it makes the hand reachable.
local cluster = text.both
and ((A.gives[t] and A.wants[t]) or (B.gives[t] and B.wants[t]))
and (A.gives[t] and A.wants[t])
and (B.gives[t] and B.wants[t])
if B.gives[t] and A.wants[t] then
return cluster and text.both or text.give
elseif A.gives[t] and B.wants[t] then
@@ -152,10 +157,65 @@ function JCA.is_recommended(card)
return total >= JCA.config.threshold
end
-- Copy jokers are positional, and the board order is right there in
-- G.jokers.cards -- so the advisor can name what a copier is ACTUALLY copying
-- rather than guess. Blueprint copies the joker to its right (card.lua:2306);
-- Brainstorm copies the leftmost one (card.lua:2322), which is nothing when it
-- is itself leftmost.
--
-- Returns: the copied card (or nil), and whether vanilla will actually let it be
-- copied -- 29 jokers carry `blueprint_compat = false` and yield nothing at all.
local COPIERS = {j_blueprint = 'right', j_brainstorm = 'leftmost'}
function JCA.copy_source(card)
if not (G.jokers and G.jokers.cards and card.config and card.config.center) then
return nil
end
local mode = COPIERS[card.config.center.key]
if not mode 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
-- NOT the `a and b or c` idiom: for a rightmost Blueprint cards[idx+1] is nil,
-- and the idiom would fall through to `or cards[1]` and confidently report it
-- copying the LEFTMOST joker.
local target
if mode == 'right' then target = cards[idx + 1] else target = cards[1] end
if not target or target == card then return nil end
local center = target.config and target.config.center
if not center then return nil end
-- blueprint_compat is vanilla's own flag for "this joker cannot be copied".
return target, center.blueprint_compat ~= false
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
-- scores the same (nothing stands out to cut).
--
-- Clashes count here, and ONLY here. JCA.score stays pure -- an anti-synergy
-- never changes a synergy score or a recommendation (a trap must never make a
-- card look like a combo). But when picking which joker to cut, a joker that is
-- actively eating another one's payoff is exactly the one you want gone, so a
-- clash with an owned joker is a penalty against keeping it. The penalty is odd
-- so it always breaks a tie against a clashing joker.
local CLASH_PENALTY = 3
local function keep_score(card, cards)
local _, total = JCA.partners_for(card)
local key = card.config.center.key
for _, other in ipairs(cards) do
local center = other ~= card and other.config and other.config.center
if center and JCA.clash_blurb[pair_key(key, center.key)] then
total = total - CLASH_PENALTY
end
end
return total
end
function JCA.weakest_link()
if not (G.jokers and G.jokers.cards) then return nil end
local cards = G.jokers.cards
@@ -164,7 +224,7 @@ function JCA.weakest_link()
local worst, worst_total, best_total
for _, c in ipairs(cards) do
if c.config and c.config.center and c.config.center.set == 'Joker' then
local _, total = JCA.partners_for(c)
local total = keep_score(c, cards)
if not worst_total or total < worst_total then
worst, worst_total = c, total
end
@@ -279,6 +339,10 @@ local TAG_LABEL = {
diamonds = 'Diamonds', spades = 'Spades', clubs = 'Clubs',
}
-- Exposed so the test suite can check every catalog tag has a label: a tag with no
-- TAG_LABEL entry silently vanishes from learning mode's "Looking for:" hints.
JCA.tag_label = TAG_LABEL
-- Labels for a joker's tag set, in stable data.tag_order order.
local function tag_labels(tag_set)
local labels = {}
@@ -369,6 +433,26 @@ local function tooltip_rows(card)
end
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.
if card.area == G.jokers then
local target, copyable = JCA.copy_source(card)
if not target then
if COPIERS[key] then
text_row(key == 'j_brainstorm'
and 'Copying nothing - it IS the leftmost joker'
or 'Copying nothing - no joker to its right', G.C.RED)
end
elseif not copyable then
text_row('Copying ' .. name_of(target.config.center.key)
.. ' - which cannot be copied', G.C.RED)
else
text_row('Copying ' .. name_of(target.config.center.key), G.C.GREEN)
end
end
-- Sell advisor verdict (owned jokers only; suppressed in learning mode
-- like the other verdicts).
if JCA.config.sell_advisor and not JCA.config.learning_mode