From 665bd9b396adb7fbdb26d0d2a63901765a0946f1 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 14 Jul 2026 13:42:50 -0700 Subject: [PATCH] Point a dead copier at its fix: name the best target one reorder away JCA.best_copy_target scans the board for copyable jokers (same truth test as copy_source: blueprint_compat must be true, debuffed excluded, other copiers excluded) and prefers the ones the database marks copy_target. When a Blueprint or Brainstorm reports anything but 'ok', the tooltip now adds the reorder that revives it: 'Fix: slot this just left of Baron' / 'Fix: make Baron your leftmost joker'. Explanation, not verdict, so learning mode keeps it. Co-Authored-By: Claude Fable 5 --- main.lua | 36 ++++++++++++++++++++++++++++++++++++ test.lua | 24 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/main.lua b/main.lua index 5f3dfa3..00e4f60 100644 --- a/main.lua +++ b/main.lua @@ -421,6 +421,30 @@ local function tag_labels(tag_set) return labels end +-- When a copier is copying nothing (or something it cannot use), the fix is a +-- reorder -- so name the best target actually on the board. Copyable means the +-- same thing it means in copy_source: blueprint_compat tested for TRUTH, not +-- debuffed, and not another copier (suggesting a copier would re-ask the same +-- question one slot over). Jokers the database marks copy_target are preferred; +-- the leftmost candidate wins a tie so the suggestion is stable across hovers. +-- Returns the card and whether it carried the copy_target tag. +function JCA.best_copy_target(copier) + if not (G.jokers and G.jokers.cards) then return nil end + local best, best_prime + for _, c in ipairs(G.jokers.cards) do + if c ~= copier and is_joker(c) and not c.debuff + and not COPIERS[c.config.center.key] + and c.config.center.blueprint_compat then + local entry = JCA.db[c.config.center.key] + local prime = not not (entry and entry.gives.copy_target) + if not best or (prime and not best_prime) then + best, best_prime = c, prime + end + end + end + return best, best_prime +end + -- Build gaps: what the current board is shopping for. A tag is a gap when -- some owned joker wants it and no OTHER owned joker gives it (same rule as -- scoring and theme discovery: a joker cannot feed itself). The reverse -- a @@ -574,6 +598,18 @@ local function tooltip_rows(card) and 'Copying nothing - it IS the leftmost joker' or 'Copying nothing - no joker to its right', G.C.RED) end + + -- A dead copier usually has a live fix one reorder away. This is + -- an explanation of the mechanic, so learning mode keeps it. + if status ~= 'ok' then + local best = JCA.best_copy_target(card) + if best then + local bname = name_of(best.config.center.key) + text_row(key == 'j_brainstorm' + and ('Fix: make ' .. bname .. ' your leftmost joker') + or ('Fix: slot this just left of ' .. bname), G.C.GREEN) + end + end end -- Ceremonial Dagger. This one is about to eat a joker, permanently, at the diff --git a/test.lua b/test.lua index a0bddee..daceef6 100644 --- a/test.lua +++ b/test.lua @@ -339,6 +339,30 @@ local ring = field('j_brainstorm', 'j_blueprint') for _, c in ipairs(ring) do c.config.center.blueprint_compat = true end eq(JCA.copy_source(ring[2]), nil, 'a Blueprint/Brainstorm ring resolves to nothing') +-- A dead copier's fix is a reorder, so the advisor names the best target on +-- the board: copyable (blueprint_compat for TRUTH, not debuffed, not another +-- copier), preferring jokers the db marks as prime copy targets. +field('j_hiker', 'j_baron', 'j_blueprint') +for _, c in ipairs(G.jokers.cards) do c.config.center.blueprint_compat = true end +local best, prime = JCA.best_copy_target(G.jokers.cards[3]) +eq(best, G.jokers.cards[2], 'the prime copy target (Baron) beats a plain joker') +eq(prime, true, 'and is reported as prime') + +-- With no prime target, the leftmost copyable joker wins; copiers and +-- uncopyable jokers never get suggested. +field('j_blueprint', 'j_hiker', 'j_splash') +G.jokers.cards[2].config.center.blueprint_compat = true +best, prime = JCA.best_copy_target(G.jokers.cards[1]) +eq(best, G.jokers.cards[2], 'falls back to the leftmost copyable joker') +eq(prime, false, 'which is not prime') + +-- A debuffed joker is never the suggestion: it copies as nothing. +field('j_blueprint', 'j_baron') +G.jokers.cards[2].config.center.blueprint_compat = true +G.jokers.cards[2].debuff = true +eq(JCA.best_copy_target(G.jokers.cards[1]), nil, + 'a debuffed joker is not suggested as a copy target') + -------------------------------------------------------------------------------- section('Engine: the sell advisor respects what you can actually sell') --------------------------------------------------------------------------------