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:
@@ -88,9 +88,21 @@ Three-layer split; `JCA` is the single global namespace:
|
|||||||
All hook bodies are pcall-wrapped so a scoring bug can never crash a
|
All hook bodies are pcall-wrapped so a scoring bug can never crash a
|
||||||
run — keep it that way.
|
run — keep it that way.
|
||||||
- Sell advisor: `JCA.weakest_link()` (full board + ≥3 jokers + a strict
|
- Sell advisor: `JCA.weakest_link()` (full board + ≥3 jokers + a strict
|
||||||
minimum required, else nil) flags the lowest-total owned joker with a
|
minimum required, else nil) flags the lowest-scoring owned joker with a
|
||||||
red verdict row; suppressed by learning mode, toggled by
|
red verdict row; suppressed by learning mode, toggled by
|
||||||
`config.sell_advisor`.
|
`config.sell_advisor`. Its ranking subtracts `CLASH_PENALTY` (odd, so it
|
||||||
|
always breaks a tie) per clash with an owned joker — the joker eating
|
||||||
|
another one's payoff is the one you want to cut. This is the **only**
|
||||||
|
place a clash changes a number; `JCA.score` stays pure, because a trap
|
||||||
|
must never make a card look like a combo.
|
||||||
|
- Copy advisor: `JCA.copy_source(card)` returns what an owned Blueprint or
|
||||||
|
Brainstorm is *actually* copying, from the live board order — Blueprint
|
||||||
|
takes the joker to its right, Brainstorm the leftmost — plus whether
|
||||||
|
vanilla will copy it at all (29 centers carry `blueprint_compat = false`).
|
||||||
|
The tooltip names the target, or says it is copying nothing. Do not
|
||||||
|
rebuild this with the `a and b or c` idiom: for a rightmost Blueprint the
|
||||||
|
right-hand slot is nil and the idiom falls through to the leftmost joker,
|
||||||
|
confidently reporting the wrong card.
|
||||||
- Public API for other mods: `JCA.register(key, gives, wants, opts)`,
|
- Public API for other mods: `JCA.register(key, gives, wants, opts)`,
|
||||||
`JCA.register_pair(a, b, blurb)` (sets `JCA._pairs_dirty` so the
|
`JCA.register_pair(a, b, blurb)` (sets `JCA._pairs_dirty` so the
|
||||||
Combos tab re-sorts), `JCA.register_clash(a, b, warning)`. Unknown
|
Combos tab re-sorts), `JCA.register_clash(a, b, warning)`. Unknown
|
||||||
@@ -147,6 +159,10 @@ Three-layer split; `JCA` is the single global namespace:
|
|||||||
`spectral_gen`): put the same tag in *both* `gives` and `wants` of every member
|
`spectral_gen`): put the same tag in *both* `gives` and `wants` of every member
|
||||||
so members mutually reinforce (2+2 = 4). Pure enablers (e.g. Four Fingers,
|
so members mutually reinforce (2+2 = 4). Pure enablers (e.g. Four Fingers,
|
||||||
Smeared, Pareidolia, DNA) only `give`; pure payoffs only `want`.
|
Smeared, Pareidolia, DNA) only `give`; pure payoffs only `want`.
|
||||||
|
A cluster tag needs all three `TAG_TEXT` phrasings, not just `both`: `both` is
|
||||||
|
only true when *both* jokers sit on both sides of the tag, and `JCA.explain`
|
||||||
|
requires that before using it. Four Fingers does not "reward Straights" — it
|
||||||
|
makes them reachable. The suite enforces the three phrasings.
|
||||||
- A tag needs **both sides to exist, on at least two different jokers**: something
|
- A tag needs **both sides to exist, on at least two different jokers**: something
|
||||||
must give it, something must want it, or it is dead data that can never change a
|
must give it, something must want it, or it is dead data that can never change a
|
||||||
score — and if it is also a theme, an achievement nobody can earn. `lua test.lua`
|
score — and if it is also a theme, an achievement nobody can earn. `lua test.lua`
|
||||||
|
|||||||
@@ -87,8 +87,13 @@ function JCA.explain(hovered, partner)
|
|||||||
for _, t in ipairs(data.tag_order) do
|
for _, t in ipairs(data.tag_order) do
|
||||||
local text = data.tag_text[t]
|
local text = data.tag_text[t]
|
||||||
if text then
|
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
|
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
|
if B.gives[t] and A.wants[t] then
|
||||||
return cluster and text.both or text.give
|
return cluster and text.both or text.give
|
||||||
elseif A.gives[t] and B.wants[t] then
|
elseif A.gives[t] and B.wants[t] then
|
||||||
@@ -152,10 +157,65 @@ function JCA.is_recommended(card)
|
|||||||
return total >= JCA.config.threshold
|
return total >= JCA.config.threshold
|
||||||
end
|
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
|
-- 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
|
-- 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
|
-- up. Returns nil when slots are free, the board is tiny, or every joker
|
||||||
-- scores the same (nothing stands out to cut).
|
-- 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()
|
function JCA.weakest_link()
|
||||||
if not (G.jokers and G.jokers.cards) then return nil end
|
if not (G.jokers and G.jokers.cards) then return nil end
|
||||||
local cards = G.jokers.cards
|
local cards = G.jokers.cards
|
||||||
@@ -164,7 +224,7 @@ function JCA.weakest_link()
|
|||||||
local worst, worst_total, best_total
|
local worst, worst_total, best_total
|
||||||
for _, c in ipairs(cards) do
|
for _, c in ipairs(cards) do
|
||||||
if c.config and c.config.center and c.config.center.set == 'Joker' then
|
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
|
if not worst_total or total < worst_total then
|
||||||
worst, worst_total = c, total
|
worst, worst_total = c, total
|
||||||
end
|
end
|
||||||
@@ -279,6 +339,10 @@ local TAG_LABEL = {
|
|||||||
diamonds = 'Diamonds', spades = 'Spades', clubs = 'Clubs',
|
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.
|
-- Labels for a joker's tag set, in stable data.tag_order order.
|
||||||
local function tag_labels(tag_set)
|
local function tag_labels(tag_set)
|
||||||
local labels = {}
|
local labels = {}
|
||||||
@@ -369,6 +433,26 @@ local function tooltip_rows(card)
|
|||||||
end
|
end
|
||||||
local caution = in_buy_area(card) and JCA.cautions[key]
|
local caution = in_buy_area(card) and JCA.cautions[key]
|
||||||
if caution then text_row('Caution: ' .. caution, G.C.RED) end
|
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
|
-- Sell advisor verdict (owned jokers only; suppressed in learning mode
|
||||||
-- like the other verdicts).
|
-- like the other verdicts).
|
||||||
if JCA.config.sell_advisor and not JCA.config.learning_mode
|
if JCA.config.sell_advisor and not JCA.config.learning_mode
|
||||||
|
|||||||
+37
-15
@@ -54,7 +54,9 @@ j('j_misprint', {'mult'})
|
|||||||
j('j_raised_fist', {'mult'}, {'retrig_held','hand_size'})
|
j('j_raised_fist', {'mult'}, {'retrig_held','hand_size'})
|
||||||
j('j_chaos', {})
|
j('j_chaos', {})
|
||||||
j('j_scary_face', {'chips'}, {'faces','retrig_scoring'})
|
j('j_scary_face', {'chips'}, {'faces','retrig_scoring'})
|
||||||
j('j_abstract', {'mult'})
|
-- Abstract Joker's Mult is the joker count (`#G.jokers.cards`, card.lua:4119), so
|
||||||
|
-- anything that hands you spare Jokers feeds it. Riff-Raff is +6 Mult on its own.
|
||||||
|
j('j_abstract', {'mult'}, {'joker_gen'})
|
||||||
j('j_delayed_grat', {'money'})
|
j('j_delayed_grat', {'money'})
|
||||||
j('j_gros_michel', {'mult'})
|
j('j_gros_michel', {'mult'})
|
||||||
j('j_even_steven', {'mult'}, {'retrig_scoring'})
|
j('j_even_steven', {'mult'}, {'retrig_scoring'})
|
||||||
@@ -67,7 +69,9 @@ j('j_egg', {'sell_value'})
|
|||||||
j('j_runner', {'chips','straight'}, {'straight'})
|
j('j_runner', {'chips','straight'}, {'straight'})
|
||||||
j('j_ice_cream', {'chips'})
|
j('j_ice_cream', {'chips'})
|
||||||
j('j_splash', {'retrig_scoring'}) -- makes every played card score
|
j('j_splash', {'retrig_scoring'}) -- makes every played card score
|
||||||
j('j_blue_joker', {'chips'})
|
-- Blue Joker's Chips scale with the cards left in the deck (`#G.deck.cards`,
|
||||||
|
-- card.lua:4311), so every joker that permanently adds cards grows it.
|
||||||
|
j('j_blue_joker', {'chips'}, {'card_gen'})
|
||||||
j('j_faceless', {'money'}, {'faces','discards_up'})
|
j('j_faceless', {'money'}, {'faces','discards_up'})
|
||||||
j('j_green_joker', {'mult'})
|
j('j_green_joker', {'mult'})
|
||||||
j('j_superposition', {'tarot_gen','straight'}, {'straight'})
|
j('j_superposition', {'tarot_gen','straight'}, {'straight'})
|
||||||
@@ -323,26 +327,44 @@ local TAG_TEXT = {
|
|||||||
probability = {give = 'improves listed odds', want = 'relies on listed odds'},
|
probability = {give = 'improves listed odds', want = 'relies on listed odds'},
|
||||||
money = {give = 'earns money', want = 'scales with your money'},
|
money = {give = 'earns money', want = 'scales with your money'},
|
||||||
tarot_gen = {give = 'creates Tarot cards', want = 'rewards Tarot use'},
|
tarot_gen = {give = 'creates Tarot cards', want = 'rewards Tarot use'},
|
||||||
planet = {both = 'shares the Planet economy'},
|
planet = {both = 'shares the Planet economy',
|
||||||
spectral_gen = {both = 'shares the Spectral theme'},
|
give = 'creates Planet cards', want = 'rewards Planet use'},
|
||||||
|
spectral_gen = {both = 'shares the Spectral theme',
|
||||||
|
give = 'creates Spectral cards', want = 'rewards Spectral use'},
|
||||||
gold_gen = {give = 'creates Gold cards', want = 'pays off Gold cards'},
|
gold_gen = {give = 'creates Gold cards', want = 'pays off Gold cards'},
|
||||||
stone_gen = {give = 'adds Stone cards', want = 'pays off Stone cards'},
|
stone_gen = {give = 'adds Stone cards', want = 'pays off Stone cards'},
|
||||||
enhance_gen = {give = 'enhances cards', want = 'feeds on enhancements'},
|
enhance_gen = {give = 'enhances cards', want = 'feeds on enhancements'},
|
||||||
card_gen = {give = 'adds cards to the deck', want = 'grows as the deck grows'},
|
card_gen = {give = 'adds cards to the deck', want = 'grows as the deck grows'},
|
||||||
joker_gen = {give = 'creates spare Jokers', want = 'profits when Jokers sell'},
|
-- The want text has to fit every wanter: Campfire sells the spares, Swashbuckler
|
||||||
|
-- sums their sell value, Abstract Joker just counts them.
|
||||||
|
joker_gen = {give = 'creates spare Jokers', want = 'rewards spare Jokers'},
|
||||||
sell_value = {give = 'raises sell values', want = 'turns sell value into Mult'},
|
sell_value = {give = 'raises sell values', want = 'turns sell value into Mult'},
|
||||||
copy_target = {give = 'is a prime copy target', want = 'copies your best Joker'},
|
copy_target = {give = 'is a prime copy target', want = 'copies your best Joker'},
|
||||||
suit_flex = {give = 'makes suits count as others', want = 'cares which suits score'},
|
suit_flex = {give = 'makes suits count as others', want = 'cares which suits score'},
|
||||||
pair = {both = 'both reward Pairs'},
|
-- Cluster tags need all three phrasings. `both` is only right when BOTH jokers
|
||||||
two_pair = {both = 'both reward Two Pair'},
|
-- sit on both sides of the tag; a pure enabler (Four Fingers, DNA) gives the
|
||||||
three_kind = {both = 'both reward Three of a Kind'},
|
-- tag without wanting it, and "both reward Straights" would be a lie -- Four
|
||||||
four_kind = {both = 'both reward Four of a Kind'},
|
-- Fingers does not reward Straights, it makes them reachable.
|
||||||
straight = {both = 'both reward Straights'},
|
pair = {both = 'both reward Pairs',
|
||||||
flush = {both = 'both reward Flushes'},
|
give = 'makes Pairs easier', want = 'rewards Pairs'},
|
||||||
hearts = {both = 'both love Hearts'},
|
two_pair = {both = 'both reward Two Pair',
|
||||||
diamonds = {both = 'both love Diamonds'},
|
give = 'makes Two Pair easier', want = 'rewards Two Pair'},
|
||||||
spades = {both = 'both love Spades'},
|
three_kind = {both = 'both reward Three of a Kind',
|
||||||
clubs = {both = 'both love Clubs'},
|
give = 'makes Three of a Kind easier', want = 'rewards Three of a Kind'},
|
||||||
|
four_kind = {both = 'both reward Four of a Kind',
|
||||||
|
give = 'makes Four of a Kind easier', want = 'rewards Four of a Kind'},
|
||||||
|
straight = {both = 'both reward Straights',
|
||||||
|
give = 'makes Straights easier', want = 'rewards Straights'},
|
||||||
|
flush = {both = 'both reward Flushes',
|
||||||
|
give = 'makes Flushes easier', want = 'rewards Flushes'},
|
||||||
|
hearts = {both = 'both love Hearts',
|
||||||
|
give = 'turns cards into Hearts', want = 'rewards Hearts'},
|
||||||
|
diamonds = {both = 'both love Diamonds',
|
||||||
|
give = 'turns cards into Diamonds', want = 'rewards Diamonds'},
|
||||||
|
spades = {both = 'both love Spades',
|
||||||
|
give = 'turns cards into Spades', want = 'rewards Spades'},
|
||||||
|
clubs = {both = 'both love Clubs',
|
||||||
|
give = 'turns cards into Clubs', want = 'rewards Clubs'},
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Order in which tags are considered when picking the one reason to show;
|
-- Order in which tags are considered when picking the one reason to show;
|
||||||
|
|||||||
@@ -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))
|
ok(members[t] or ENGINE_TAGS[t], ('TAG_ORDER tag %q is used by at least one joker'):format(t))
|
||||||
end
|
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')
|
section('Data: themes are reachable')
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user