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
+85
View File
@@ -237,6 +237,91 @@ for _, t in ipairs(data.tag_order) do
ok(members[t] or ENGINE_TAGS[t], ('TAG_ORDER tag %q is used by at least one joker'):format(t))
end
--------------------------------------------------------------------------------
section('Data: tooltip phrasing')
--------------------------------------------------------------------------------
-- A cluster tag needs all three phrasings. "both reward Straights" is only true
-- when BOTH jokers reward them; a pure enabler (Four Fingers, DNA) gives the tag
-- without wanting it and must not be described as rewarding the hand.
for _, t in ipairs(sorted_keys(data.tag_text)) do
local text = data.tag_text[t]
if text.both then
ok(text.give and text.want,
('cluster tag %q has give/want phrasings, not just "both"'):format(t),
'an enabler paired with a payoff would otherwise be told it "rewards" the hand')
end
end
-- Four Fingers ENABLES straights (it does not reward them); Runner rewards them.
eq(JCA.explain('j_four_fingers', 'j_runner'), 'rewards Straights',
'a payoff partner is described as rewarding the hand')
eq(JCA.explain('j_runner', 'j_four_fingers'), 'makes Straights easier',
'an enabler partner is described as enabling, not rewarding')
eq(JCA.explain('j_dna', 'j_family'), 'rewards Four of a Kind',
'The Family rewards Four of a Kind')
eq(JCA.explain('j_family', 'j_dna'), 'makes Four of a Kind easier',
'DNA enables Four of a Kind')
-- Two full cluster members still get the "both" phrasing.
eq(JCA.explain('j_jolly', 'j_sly'), 'both reward Pairs',
'two payoffs still read as "both"')
for _, t in ipairs(sorted_keys(data.theme_info)) do
ok(JCA.tag_label[t], ('theme %q has a TAG_LABEL for learning-mode hints'):format(t))
end
--------------------------------------------------------------------------------
section('Engine: copy jokers are positional')
--------------------------------------------------------------------------------
-- Blueprint copies the joker to its RIGHT, Brainstorm the LEFTMOST. The board
-- order decides what they actually copy, and a copier in the wrong slot is dead.
local bp_board = field('j_baron', 'j_blueprint', 'j_mime')
eq(JCA.copy_source(bp_board[2]), bp_board[3], 'Blueprint copies the joker to its right')
field('j_baron', 'j_mime', 'j_blueprint')
eq(JCA.copy_source(G.jokers.cards[3]), nil, 'a rightmost Blueprint copies nothing')
local bs_board = field('j_baron', 'j_brainstorm', 'j_mime')
eq(JCA.copy_source(bs_board[2]), bs_board[1], 'Brainstorm copies the leftmost joker')
field('j_brainstorm', 'j_baron')
eq(JCA.copy_source(G.jokers.cards[1]), nil, 'a leftmost Brainstorm copies nothing')
field('j_baron', 'j_mime')
eq(JCA.copy_source(G.jokers.cards[1]), nil, 'a non-copier has no copy source')
-- vanilla's own "cannot be copied" flag: 29 jokers carry blueprint_compat = false
-- and yield nothing at all when copied.
local uncopyable = field('j_blueprint', 'j_splash')
uncopyable[2].config.center.blueprint_compat = false
local target, copyable = JCA.copy_source(uncopyable[1])
eq(target, uncopyable[2], 'it still reports what sits in the copied slot')
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: the sell advisor weighs clashes')
--------------------------------------------------------------------------------
-- Clashes must never touch a synergy score...
local pre = JCA.score('j_vampire', 'j_ticket')
ok(pre >= 0, 'a clashing pair still scores by its tags alone')
ok(JCA.clash_blurb[
('j_ticket' < 'j_vampire') and 'j_ticket|j_vampire' or 'j_vampire|j_ticket'],
'Vampire + Golden Ticket is a known clash')
-- ...but on a full board, the joker stuck in a clash is the one to cut, even
-- when a joker with no synergy at all is sitting next to it.
local board = field('j_vampire', 'j_ticket', 'j_baron', 'j_mime')
G.jokers.config.card_limit = 4
local cut = JCA.weakest_link()
ok(cut == board[1] or cut == board[2],
'the sell advisor flags one of the two clashing jokers',
cut and cut.config.center.key or 'nil')
--------------------------------------------------------------------------------
section('Data: themes are reachable')
--------------------------------------------------------------------------------