Add audit.lua: the map that test.lua is not
test.lua is the gate -- it fails on what is broken. This is the map: it reports what is merely suspicious, and (the actual point) it REMEMBERS. 32 jokers score ~0 against all 150: they give nothing but engine roles and want nothing, so the advisor has nothing to say about them. For most that is the correct answer, but I have now worked that question out from scratch twice, and the reasoning evaporated both times. VERDICTS records the conclusion for each one, so an audit only surfaces what is new. It preserves the traps: Steel Joker and Glass Joker look exactly like Blue Joker and Abstract Joker did, but they want Steel/Glass cards specifically, and the only enhance_gen givers make Gold and Stone -- so "fixing" them would invent a synergy that does not exist. That is written down now instead of being rediscovered. It also preserves the leads not yet chased (Ceremonial Dagger's positional destroy, Erosion vs the card-adders, Burglar + Ramen and Green Joker, Chaos + Flash Card), marked candidate because they are unverified. A newly-silent joker lands in "never looked at" and fails the audit until someone decides. AUDIT.md is committed so the map is readable without running anything. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
# Synergy database audit
|
||||||
|
|
||||||
|
Generated by `lua audit.lua --write`. Do not edit by hand.
|
||||||
|
|
||||||
|
`test.lua` is the gate (it fails on what is broken). This is the map: it reports
|
||||||
|
what is merely *suspicious*, and records what we already decided so the analysis
|
||||||
|
does not get redone from scratch every session.
|
||||||
|
|
||||||
|
## Totals
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Jokers in the database | 150 |
|
||||||
|
| Famous pairs | 32 |
|
||||||
|
| Clashes | 22 |
|
||||||
|
| Cautions | 15 |
|
||||||
|
| Themes | 29 |
|
||||||
|
| Jokers in no famous pair | 108 |
|
||||||
|
|
||||||
|
## Jokers the advisor is silent about
|
||||||
|
|
||||||
|
These give nothing but engine roles (Mult/Chips/xMult) and want nothing, so they
|
||||||
|
score at most the generic +1 against any joker in the game. That is a legitimate
|
||||||
|
answer for some of them -- but it should be a decision, not an accident.
|
||||||
|
|
||||||
|
| | count |
|
||||||
|
|---|---|
|
||||||
|
| Silent, reviewed, staying that way | 20 |
|
||||||
|
| Silent, but a lead worth chasing | 12 |
|
||||||
|
| **Silent and never looked at** | **0** |
|
||||||
|
|
||||||
|
### Leads worth chasing
|
||||||
|
|
||||||
|
Unverified. Check each against the Lovely dump before acting on it -- a wrong
|
||||||
|
synergy is worse than a missing one.
|
||||||
|
|
||||||
|
- **`j_baseball`** — xMult per Uncommon joker owned. We have no rarity tag, and adding one for a single payoff may not earn its keep.
|
||||||
|
- **`j_card_sharp`** — xMult when the hand type was already played this round -- it rewards repeating one hand, which is exactly what Obelisk punishes. Clash?
|
||||||
|
- **`j_ceremonial`** — positional like Blueprint: it DESTROYS the joker to its right. JCA.copy_source already reads board order -- the same machinery could warn "will destroy your Baron" before it eats something irreplaceable.
|
||||||
|
- **`j_chaos`** — free reroll each shop; see j_flash
|
||||||
|
- **`j_erosion`** — +Mult per card BELOW 52 in the deck, so it is the inverse of card_gen: DNA/Marble/Certificate permanently shrink it. Likely CLASHES, and possibly a synergy with anything that destroys cards (Trading Card).
|
||||||
|
- **`j_flash`** — +Mult per shop reroll. Chaos the Clown grants a free reroll each shop, which would give both of them a reason to exist (Chaos is inert today).
|
||||||
|
- **`j_green_joker`** — +Mult per hand, -Mult per discard: Burglar removes discards entirely. Same shape as the existing Burglar + Mystic Summit pair.
|
||||||
|
- **`j_obelisk`** — resets on your most-played hand, so it fights every hand-type build. Has a CAUTION already; the pairwise clashes may be worth adding.
|
||||||
|
- **`j_ramen`** — xMult decays per card discarded, so Burglar (zero discards all run) stops the decay dead. Likely a famous pair.
|
||||||
|
- **`j_ring_master`** — Showman allows duplicates in the pool. Enables copy/duplicate strategies, but we have no tag for "shop pool" effects and inventing one for a single joker is probably not worth it.
|
||||||
|
- **`j_square`** — +Chips on exactly-4-card hands; Four Fingers makes 4-card flushes and straights playable. Needs a tag that does not exist yet.
|
||||||
|
- **`j_supernova`** — Mult scales with times the hand was played; see j_card_sharp
|
||||||
|
|
||||||
|
### Reviewed, and deliberately silent
|
||||||
|
|
||||||
|
- **`j_cavendish`** — the reward for Gros Michel dying, not a partner
|
||||||
|
- **`j_chicot`** — disables the boss blind; no joker interaction
|
||||||
|
- **`j_credit_card`** — lets you go negative; a money buffer, not a combo
|
||||||
|
- **`j_diet_cola`** — sell for a Double Tag; tags are outside our model
|
||||||
|
- **`j_glass`** — wants GLASS cards, and no vanilla joker creates them. Same trap as Steel Joker: enhance_gen is too coarse a tag to use here.
|
||||||
|
- **`j_gros_michel`** — self-destructs; the trap is a CAUTION
|
||||||
|
- **`j_half`** — +Mult on small hands; no joker cares
|
||||||
|
- **`j_ice_cream`** — decays; the trap is a CAUTION
|
||||||
|
- **`j_joker`** — plain +Mult; interacts with nothing
|
||||||
|
- **`j_loyalty_card`** — xMult every 6 hands; no joker interaction
|
||||||
|
- **`j_luchador`** — sell to disable a boss; no joker interaction
|
||||||
|
- **`j_misprint`** — random +Mult; interacts with nothing
|
||||||
|
- **`j_mr_bones`** — death insurance; no joker interaction
|
||||||
|
- **`j_mystic_summit`** — already paired with Burglar (zero discards, max Mult)
|
||||||
|
- **`j_popcorn`** — decays; the trap is a CAUTION
|
||||||
|
- **`j_red_card`** — +Mult per skipped booster; no joker interaction
|
||||||
|
- **`j_ride_the_bus`** — scales on face-free hands; the traps are CLASHES
|
||||||
|
- **`j_steel_joker`** — wants STEEL cards. The only enhance_gen givers make Gold (Midas) and Stone (Marble), so tagging it enhance_gen would invent a synergy that does not exist. No vanilla joker creates Steel cards. Leave it alone.
|
||||||
|
- **`j_stuntman`** — big chips, -2 hand size; the trap is a CAUTION
|
||||||
|
- **`j_throwback`** — xMult per skipped blind; no joker interaction
|
||||||
|
|
||||||
|
## Tag health
|
||||||
|
|
||||||
|
A tag with one member can never fire (it takes two jokers to make a synergy).
|
||||||
|
A tag with two is fragile: drop either joker and the theme dies.
|
||||||
|
|
||||||
|
| tag | givers | wanters | jokers | theme? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `card_gen` | 3 | 2 | 5 | yes |
|
||||||
|
| `clubs` | 3 | 4 | 4 | yes |
|
||||||
|
| `copy_target` | 10 | 3 | 13 | yes |
|
||||||
|
| `diamonds` | 2 | 2 | **2** | yes |
|
||||||
|
| `discards_up` | 2 | 8 | 10 | yes |
|
||||||
|
| `enhance_gen` | 2 | 2 | 4 | yes |
|
||||||
|
| `faces` | 1 | 9 | 10 | yes |
|
||||||
|
| `flush` | 5 | 4 | 6 | yes |
|
||||||
|
| `four_kind` | 2 | 1 | **2** | yes |
|
||||||
|
| `gold_gen` | 1 | 1 | **2** | yes |
|
||||||
|
| `hand_size` | 3 | 4 | 7 | yes |
|
||||||
|
| `hands_up` | 1 | 1 | **2** | yes |
|
||||||
|
| `hearts` | 2 | 2 | **2** | yes |
|
||||||
|
| `joker_gen` | 1 | 3 | 4 | yes |
|
||||||
|
| `money` | 15 | 3 | 17 | yes |
|
||||||
|
| `pair` | 4 | 3 | 4 | yes |
|
||||||
|
| `planet` | 3 | 3 | 3 | yes |
|
||||||
|
| `probability` | 1 | 7 | 8 | yes |
|
||||||
|
| `retrig_held` | 1 | 4 | 5 | yes |
|
||||||
|
| `retrig_scoring` | 6 | 16 | 22 | yes |
|
||||||
|
| `sell_value` | 2 | 2 | 4 | yes |
|
||||||
|
| `spades` | 2 | 3 | 3 | yes |
|
||||||
|
| `spectral_gen` | 2 | 2 | **2** | yes |
|
||||||
|
| `stone_gen` | 1 | 1 | **2** | yes |
|
||||||
|
| `straight` | 7 | 6 | 8 | yes |
|
||||||
|
| `suit_flex` | 1 | 11 | 12 | yes |
|
||||||
|
| `tarot_gen` | 6 | 2 | 7 | yes |
|
||||||
|
| `three_kind` | 4 | 3 | 4 | yes |
|
||||||
|
| `two_pair` | 3 | 3 | 3 | yes |
|
||||||
|
|
||||||
|
## Load-bearing jokers
|
||||||
|
|
||||||
|
The jokers that appear in the most scoring relationships. If one of these is
|
||||||
|
mis-tagged, the error is spread across the whole database.
|
||||||
|
|
||||||
|
| joker | partners scoring >= 2 |
|
||||||
|
|---|---|
|
||||||
|
| `j_sock_and_buskin` | 18 |
|
||||||
|
| `j_dusk` | 16 |
|
||||||
|
| `j_hack` | 16 |
|
||||||
|
| `j_hanging_chad` | 16 |
|
||||||
|
| `j_selzer` | 16 |
|
||||||
|
| `j_smeared` | 16 |
|
||||||
|
| `j_splash` | 16 |
|
||||||
|
| `j_to_the_moon` | 16 |
|
||||||
|
| `j_bootstraps` | 15 |
|
||||||
|
| `j_bull` | 15 |
|
||||||
|
| `j_four_fingers` | 12 |
|
||||||
|
| `j_seance` | 12 |
|
||||||
|
|
||||||
|
Jokers with **no** partner scoring >= 2 anywhere in the game: 30
|
||||||
|
|
||||||
@@ -40,6 +40,26 @@ It enforces, among other things:
|
|||||||
|
|
||||||
To poke at the engine by hand, copy the stub block from the top of `test.lua`.
|
To poke at the engine by hand, copy the stub block from the top of `test.lua`.
|
||||||
|
|
||||||
|
Then the audit, after any `synergies.lua` change:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
lua audit.lua --write # refreshes AUDIT.md; exits 1 on an unreviewed joker
|
||||||
|
```
|
||||||
|
|
||||||
|
`test.lua` is the **gate** — it fails on what is broken. `audit.lua` is the **map** —
|
||||||
|
it reports what is merely suspicious, chiefly the ~32 jokers the advisor is silent
|
||||||
|
about (they give nothing but Mult/Chips/xMult and want nothing, so they can only ever
|
||||||
|
earn the generic +1). Silence is the *right* answer for many of them, but it must be a
|
||||||
|
decision rather than an accident, so `audit.lua` carries a `VERDICTS` table recording
|
||||||
|
what we concluded about each one and why — including the traps. **Read `AUDIT.md`
|
||||||
|
before re-deriving any of this.** A newly-silent joker lands in "never looked at" and
|
||||||
|
fails the audit until it gets a verdict.
|
||||||
|
|
||||||
|
The verdicts also hold the leads not yet chased (Ceremonial Dagger's positional
|
||||||
|
destroy, Erosion vs the card-adders, Burglar + Ramen/Green Joker, Chaos + Flash Card).
|
||||||
|
They are marked `candidate` because they are **unverified** — check the dump before
|
||||||
|
acting on one.
|
||||||
|
|
||||||
## In-game verification (this machine)
|
## In-game verification (this machine)
|
||||||
|
|
||||||
- The mod is **symlinked** into the live Mods folder:
|
- The mod is **symlinked** into the live Mods folder:
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
-- Joker Combo Advisor - database audit.
|
||||||
|
--
|
||||||
|
-- lua audit.lua print the report
|
||||||
|
-- lua audit.lua --write print it and refresh AUDIT.md
|
||||||
|
--
|
||||||
|
-- test.lua is the GATE: it fails on things that are broken (dead tags, themes
|
||||||
|
-- nobody can discover, bad keys). This is the MAP: it reports things that are
|
||||||
|
-- merely suspicious, and would be noise as a test failure -- chiefly the jokers
|
||||||
|
-- the advisor is silent about.
|
||||||
|
--
|
||||||
|
-- The point of committing AUDIT.md is that the analysis does not have to be redone
|
||||||
|
-- from scratch every time. A joker scoring 0 against all 150 is not automatically a
|
||||||
|
-- bug; some jokers genuinely have no combo. The VERDICTS table below records which
|
||||||
|
-- ones we have already looked at and what we concluded, so each audit only surfaces
|
||||||
|
-- what is genuinely new or still open.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Verdicts: what we have already decided about jokers the advisor is silent on.
|
||||||
|
--
|
||||||
|
-- settled - looked at it; it should stay silent, and here is why.
|
||||||
|
-- candidate - a real lead, NOT yet verified against the game source. Check
|
||||||
|
-- ~/.local/share/Balatro/Mods/lovely/dump before acting on one.
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local VERDICTS = {
|
||||||
|
-- Genuinely no joker-to-joker combo. These are the right answer, not a gap.
|
||||||
|
j_joker = {'settled', 'plain +Mult; interacts with nothing'},
|
||||||
|
j_misprint = {'settled', 'random +Mult; interacts with nothing'},
|
||||||
|
j_half = {'settled', '+Mult on small hands; no joker cares'},
|
||||||
|
j_credit_card= {'settled', 'lets you go negative; a money buffer, not a combo'},
|
||||||
|
j_mr_bones = {'settled', 'death insurance; no joker interaction'},
|
||||||
|
j_chicot = {'settled', 'disables the boss blind; no joker interaction'},
|
||||||
|
j_luchador = {'settled', 'sell to disable a boss; no joker interaction'},
|
||||||
|
j_diet_cola = {'settled', 'sell for a Double Tag; tags are outside our model'},
|
||||||
|
j_throwback = {'settled', 'xMult per skipped blind; no joker interaction'},
|
||||||
|
j_red_card = {'settled', '+Mult per skipped booster; no joker interaction'},
|
||||||
|
j_loyalty_card = {'settled', 'xMult every 6 hands; no joker interaction'},
|
||||||
|
j_ride_the_bus = {'settled', 'scales on face-free hands; the traps are CLASHES'},
|
||||||
|
j_ice_cream = {'settled', 'decays; the trap is a CAUTION'},
|
||||||
|
j_popcorn = {'settled', 'decays; the trap is a CAUTION'},
|
||||||
|
j_stuntman = {'settled', 'big chips, -2 hand size; the trap is a CAUTION'},
|
||||||
|
j_gros_michel= {'settled', 'self-destructs; the trap is a CAUTION'},
|
||||||
|
j_cavendish = {'settled', 'the reward for Gros Michel dying, not a partner'},
|
||||||
|
|
||||||
|
-- Deliberately silent, and this one is a TRAP for a future editor:
|
||||||
|
j_steel_joker = {'settled',
|
||||||
|
'wants STEEL cards. The only enhance_gen givers make Gold (Midas) and ' ..
|
||||||
|
'Stone (Marble), so tagging it enhance_gen would invent a synergy that ' ..
|
||||||
|
'does not exist. No vanilla joker creates Steel cards. Leave it alone.'},
|
||||||
|
j_glass = {'settled',
|
||||||
|
'wants GLASS cards, and no vanilla joker creates them. Same trap as ' ..
|
||||||
|
'Steel Joker: enhance_gen is too coarse a tag to use here.'},
|
||||||
|
|
||||||
|
-- Leads worth chasing. NOT verified against the game source yet.
|
||||||
|
j_ceremonial = {'candidate',
|
||||||
|
'positional like Blueprint: it DESTROYS the joker to its right. ' ..
|
||||||
|
'JCA.copy_source already reads board order -- the same machinery could ' ..
|
||||||
|
'warn "will destroy your Baron" before it eats something irreplaceable.'},
|
||||||
|
j_erosion = {'candidate',
|
||||||
|
'+Mult per card BELOW 52 in the deck, so it is the inverse of card_gen: ' ..
|
||||||
|
'DNA/Marble/Certificate permanently shrink it. Likely CLASHES, and ' ..
|
||||||
|
'possibly a synergy with anything that destroys cards (Trading Card).'},
|
||||||
|
j_ramen = {'candidate',
|
||||||
|
'xMult decays per card discarded, so Burglar (zero discards all run) ' ..
|
||||||
|
'stops the decay dead. Likely a famous pair.'},
|
||||||
|
j_green_joker = {'candidate',
|
||||||
|
'+Mult per hand, -Mult per discard: Burglar removes discards entirely. ' ..
|
||||||
|
'Same shape as the existing Burglar + Mystic Summit pair.'},
|
||||||
|
j_flash = {'candidate',
|
||||||
|
'+Mult per shop reroll. Chaos the Clown grants a free reroll each shop, ' ..
|
||||||
|
'which would give both of them a reason to exist (Chaos is inert today).'},
|
||||||
|
j_chaos = {'candidate', 'free reroll each shop; see j_flash'},
|
||||||
|
j_square = {'candidate',
|
||||||
|
'+Chips on exactly-4-card hands; Four Fingers makes 4-card flushes and ' ..
|
||||||
|
'straights playable. Needs a tag that does not exist yet.'},
|
||||||
|
j_card_sharp = {'candidate',
|
||||||
|
'xMult when the hand type was already played this round -- it rewards ' ..
|
||||||
|
'repeating one hand, which is exactly what Obelisk punishes. Clash?'},
|
||||||
|
j_supernova = {'candidate', 'Mult scales with times the hand was played; see j_card_sharp'},
|
||||||
|
j_obelisk = {'candidate',
|
||||||
|
'resets on your most-played hand, so it fights every hand-type build. ' ..
|
||||||
|
'Has a CAUTION already; the pairwise clashes may be worth adding.'},
|
||||||
|
j_baseball = {'candidate',
|
||||||
|
'xMult per Uncommon joker owned. We have no rarity tag, and adding one ' ..
|
||||||
|
'for a single payoff may not earn its keep.'},
|
||||||
|
j_mystic_summit = {'settled', 'already paired with Burglar (zero discards, max Mult)'},
|
||||||
|
j_ring_master = {'candidate',
|
||||||
|
'Showman allows duplicates in the pool. Enables copy/duplicate strategies, ' ..
|
||||||
|
'but we have no tag for "shop pool" effects and inventing one for a single ' ..
|
||||||
|
'joker is probably not worth it.'},
|
||||||
|
}
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Stubs: just enough game for the engine to load.
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SMODS = {load_file = function(f) return loadfile(f) end,
|
||||||
|
current_mod = {config = dofile('config.lua')}}
|
||||||
|
Card = {generate_UIBox_ability_table = function() end, update = function() end}
|
||||||
|
G = {UIT = {ROOT = 'ROOT', R = 'R', T = 'T'},
|
||||||
|
C = {UI = {TEXT_DARK = 'd'}, GREEN = 'g', CLEAR = 'c', RED = 'r', GOLD = 'go'},
|
||||||
|
STAGES = {RUN = 1}, FUNCS = {}}
|
||||||
|
|
||||||
|
local data = dofile('synergies.lua')
|
||||||
|
dofile('main.lua')
|
||||||
|
|
||||||
|
local ENGINE = {mult = true, chips = true, xmult = true}
|
||||||
|
|
||||||
|
local out = {}
|
||||||
|
local function w(fmt, ...)
|
||||||
|
out[#out + 1] = select('#', ...) > 0 and fmt:format(...) or fmt
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sorted(t)
|
||||||
|
local keys = {}
|
||||||
|
for k in pairs(t) do keys[#keys + 1] = k end
|
||||||
|
table.sort(keys)
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Gather
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local givers, wanters, members = {}, {}, {}
|
||||||
|
local n_jokers = 0
|
||||||
|
for key, e in pairs(JCA.db) do
|
||||||
|
n_jokers = n_jokers + 1
|
||||||
|
for t in pairs(e.gives) do
|
||||||
|
givers[t] = (givers[t] or 0) + 1
|
||||||
|
members[t] = members[t] or {}; members[t][key] = true
|
||||||
|
end
|
||||||
|
for t in pairs(e.wants) do
|
||||||
|
wanters[t] = (wanters[t] or 0) + 1
|
||||||
|
members[t] = members[t] or {}; members[t][key] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "Silent": nothing but engine roles to give, and nothing wanted. Such a joker
|
||||||
|
-- can only ever earn the generic +1, so the advisor has nothing to say about it.
|
||||||
|
local silent = {}
|
||||||
|
for key, e in pairs(JCA.db) do
|
||||||
|
local has_theme, wants = false, 0
|
||||||
|
for t in pairs(e.gives) do if not ENGINE[t] then has_theme = true end end
|
||||||
|
for _ in pairs(e.wants) do wants = wants + 1 end
|
||||||
|
if not has_theme and wants == 0 then silent[#silent + 1] = key end
|
||||||
|
end
|
||||||
|
table.sort(silent)
|
||||||
|
|
||||||
|
local in_pair = {}
|
||||||
|
for _, p in ipairs(data.pairs) do in_pair[p[1]] = true; in_pair[p[2]] = true end
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Report
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
w('# Synergy database audit')
|
||||||
|
w('')
|
||||||
|
w('Generated by `lua audit.lua --write`. Do not edit by hand.')
|
||||||
|
w('')
|
||||||
|
w('`test.lua` is the gate (it fails on what is broken). This is the map: it reports')
|
||||||
|
w('what is merely *suspicious*, and records what we already decided so the analysis')
|
||||||
|
w('does not get redone from scratch every session.')
|
||||||
|
w('')
|
||||||
|
|
||||||
|
w('## Totals')
|
||||||
|
w('')
|
||||||
|
w('| | |')
|
||||||
|
w('|---|---|')
|
||||||
|
w('| Jokers in the database | %d |', n_jokers)
|
||||||
|
w('| Famous pairs | %d |', #data.pairs)
|
||||||
|
w('| Clashes | %d |', #(data.clashes or {}))
|
||||||
|
w('| Cautions | %d |', (function() local n = 0 for _ in pairs(data.cautions or {}) do n = n + 1 end return n end)())
|
||||||
|
w('| Themes | %d |', (function() local n = 0 for _ in pairs(data.theme_info or {}) do n = n + 1 end return n end)())
|
||||||
|
w('| Jokers in no famous pair | %d |', (function()
|
||||||
|
local n = 0
|
||||||
|
for k in pairs(JCA.db) do if not in_pair[k] then n = n + 1 end end
|
||||||
|
return n
|
||||||
|
end)())
|
||||||
|
w('')
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
w('## Jokers the advisor is silent about')
|
||||||
|
w('')
|
||||||
|
w('These give nothing but engine roles (Mult/Chips/xMult) and want nothing, so they')
|
||||||
|
w('score at most the generic +1 against any joker in the game. That is a legitimate')
|
||||||
|
w('answer for some of them -- but it should be a decision, not an accident.')
|
||||||
|
w('')
|
||||||
|
|
||||||
|
local buckets = {settled = {}, candidate = {}, unreviewed = {}}
|
||||||
|
for _, key in ipairs(silent) do
|
||||||
|
local v = VERDICTS[key]
|
||||||
|
local bucket = v and v[1] or 'unreviewed'
|
||||||
|
table.insert(buckets[bucket], key)
|
||||||
|
end
|
||||||
|
|
||||||
|
w('| | count |')
|
||||||
|
w('|---|---|')
|
||||||
|
w('| Silent, reviewed, staying that way | %d |', #buckets.settled)
|
||||||
|
w('| Silent, but a lead worth chasing | %d |', #buckets.candidate)
|
||||||
|
w('| **Silent and never looked at** | **%d** |', #buckets.unreviewed)
|
||||||
|
w('')
|
||||||
|
|
||||||
|
if #buckets.unreviewed > 0 then
|
||||||
|
w('### Never looked at')
|
||||||
|
w('')
|
||||||
|
w('New arrivals. Each one scores ~0 against all %d jokers; work out whether that', n_jokers)
|
||||||
|
w('is correct and add a verdict to `VERDICTS` in `audit.lua` either way.')
|
||||||
|
w('')
|
||||||
|
for _, key in ipairs(buckets.unreviewed) do w('- `%s`', key) end
|
||||||
|
w('')
|
||||||
|
end
|
||||||
|
|
||||||
|
w('### Leads worth chasing')
|
||||||
|
w('')
|
||||||
|
w('Unverified. Check each against the Lovely dump before acting on it -- a wrong')
|
||||||
|
w('synergy is worse than a missing one.')
|
||||||
|
w('')
|
||||||
|
for _, key in ipairs(buckets.candidate) do
|
||||||
|
w('- **`%s`** — %s', key, VERDICTS[key][2])
|
||||||
|
end
|
||||||
|
w('')
|
||||||
|
|
||||||
|
w('### Reviewed, and deliberately silent')
|
||||||
|
w('')
|
||||||
|
for _, key in ipairs(buckets.settled) do
|
||||||
|
w('- **`%s`** — %s', key, VERDICTS[key][2])
|
||||||
|
end
|
||||||
|
w('')
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
w('## Tag health')
|
||||||
|
w('')
|
||||||
|
w('A tag with one member can never fire (it takes two jokers to make a synergy).')
|
||||||
|
w('A tag with two is fragile: drop either joker and the theme dies.')
|
||||||
|
w('')
|
||||||
|
w('| tag | givers | wanters | jokers | theme? |')
|
||||||
|
w('|---|---|---|---|---|')
|
||||||
|
for _, t in ipairs(sorted(members)) do
|
||||||
|
if not ENGINE[t] then
|
||||||
|
local n = 0
|
||||||
|
for _ in pairs(members[t]) do n = n + 1 end
|
||||||
|
w('| `%s` | %d | %d | %s | %s |', t, givers[t] or 0, wanters[t] or 0,
|
||||||
|
n <= 2 and ('**' .. n .. '**') or tostring(n),
|
||||||
|
data.theme_info[t] and 'yes' or '-')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
w('')
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Which jokers carry the most weight? Losing one of these costs the most edges.
|
||||||
|
w('## Load-bearing jokers')
|
||||||
|
w('')
|
||||||
|
w('The jokers that appear in the most scoring relationships. If one of these is')
|
||||||
|
w('mis-tagged, the error is spread across the whole database.')
|
||||||
|
w('')
|
||||||
|
local edges = {}
|
||||||
|
for a in pairs(JCA.db) do
|
||||||
|
local n = 0
|
||||||
|
for b in pairs(JCA.db) do
|
||||||
|
if a ~= b and JCA.score(a, b) >= 2 then n = n + 1 end
|
||||||
|
end
|
||||||
|
edges[#edges + 1] = {key = a, n = n}
|
||||||
|
end
|
||||||
|
table.sort(edges, function(x, y)
|
||||||
|
if x.n ~= y.n then return x.n > y.n end
|
||||||
|
return x.key < y.key
|
||||||
|
end)
|
||||||
|
w('| joker | partners scoring >= 2 |')
|
||||||
|
w('|---|---|')
|
||||||
|
for i = 1, 12 do w('| `%s` | %d |', edges[i].key, edges[i].n) end
|
||||||
|
w('')
|
||||||
|
local isolated = {}
|
||||||
|
for _, e in ipairs(edges) do
|
||||||
|
if e.n == 0 then isolated[#isolated + 1] = e.key end
|
||||||
|
end
|
||||||
|
w('Jokers with **no** partner scoring >= 2 anywhere in the game: %d', #isolated)
|
||||||
|
w('')
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
local text = table.concat(out, '\n') .. '\n'
|
||||||
|
print(text)
|
||||||
|
|
||||||
|
if arg[1] == '--write' then
|
||||||
|
local f = assert(io.open('AUDIT.md', 'w'))
|
||||||
|
f:write(text)
|
||||||
|
f:close()
|
||||||
|
io.stderr:write('wrote AUDIT.md\n')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A joker that scores 0 against the entire game should be a decision, not an
|
||||||
|
-- accident, so a never-reviewed one is a failure: record a verdict either way.
|
||||||
|
if #buckets.unreviewed > 0 then
|
||||||
|
io.stderr:write(('\n%d silent joker(s) have never been reviewed. Each scores ~0\n'
|
||||||
|
.. 'against all %d jokers. Decide whether that is right and add a verdict to\n'
|
||||||
|
.. 'VERDICTS in audit.lua:\n %s\n')
|
||||||
|
:format(#buckets.unreviewed, n_jokers, table.concat(buckets.unreviewed, '\n ')))
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user