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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user