testsAndMisc/kcd2_dice_solver/README.md
Krzysztof kuhy Rudnicki 11ff8e6a26
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
kcd2_dice_solver: Balatro's joker replaces the one, confirmed
The mapping was marked UNVERIFIED because no published source states which pip
the grinning face covers. The player, who owns the game, confirmed it is the one
— the same face the Devil's head die replaces. So the die carries no plain single
pip at all, which is exactly why picking the joker alone counts as a 1.

Comment and README only; wildcardFaces was already [1]. The uniform side weights
stay flagged as the one remaining assumption, since nothing publishes them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 16:33:08 +02:00

90 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# KCD2 Dice Solver
Pick which six dice and which badge to bring to a game of dice in
_Kingdom Come: Deliverance II_. Enter what you own; the solver evaluates every
loadout your inventory allows and reports the best one with the numbers behind
it.
```bash
pnpm install
pnpm dev # http://localhost:5174
pnpm test # full suite
pnpm coverage # 100% statements / branches / functions / lines
pnpm lint # tsc --noEmit + eslint (strictTypeChecked)
pnpm build
```
## Entering an inventory
Three ways, all wired to the same state:
- **Click** a die's row to add one.
- **Scroll** the mouse wheel over its counter to step it up or down.
- **Search** by name — a fuzzy match, so `wei` finds _Weighted die_ and
`paintb` finds _Painter's die B_. The same box filters the badge list.
The inventory is saved to `localStorage`, so it survives a reload.
## How the recommendation is computed
**Scoring** (`src/core/scoring.ts`) is the wiki's table: 100 per one, 50 per
five, `1000` for three ones and `100 × face` otherwise, doubled for each die
beyond three, and 500/750/1500 for the three straights. A roll scores the best
_partition_ of its dice, which is a small memoised search.
**Expected value** (`src/core/evaluate.ts`) is exact, not sampled. Scoring
depends only on the multiset of faces, so the six dice are convolved into a
distribution over count vectors — at most 924 of them, against 6⁶ = 46,656
ordered outcomes. Those vectors stay packed as integers end to end, so the
scorer's memo is keyed on them directly. `evaluate.test.ts` checks the result
against a naive full enumeration that shares none of that machinery.
**The search** (`src/core/search.ts`) evaluates whole sets, because a Farkle
set's value is not the sum of its dice — ranking dice individually and taking
the top six is simply wrong. Dice with identical distributions are pooled
first (nine of the game's dice are plain uniform dice under different names).
Small inventories are enumerated exhaustively and reported as _provably
optimal_; larger ones use a multi-start steepest-ascent local search and are
reported as _not proven_.
**Turn value** (`src/core/simulate.ts`) is a Monte Carlo simulation, and unlike
the above it depends on how you play. The policy is stated explicitly in that
file and its knobs are exposed rather than buried: hold the subset maximising
`points + 60 × dice-left`, bank at 300.
**Badges** (`src/core/badgeValue.ts`) split in two. The five that change the
scoring table are valued on the exact EV delta after re-running the whole dice
search with the rule on — so equipping one can change which dice you should
bring. The rest are simulated one charge at a time, with each turn drawing from
its own seeded stream so the comparison is paired and the measured difference is
the badge rather than a diverged random sequence.
Nothing uses `Math.random`; every random source is a seeded `mulberry32`, so
every number here is reproducible.
## What the numbers are not
- **Balatro's die** publishes no face probabilities: the wiki's dice-effect row
for it is six unfilled `d` placeholders, and Inara does not list the die at
all. Exactly **one** of its faces is the joker (Jimbo's grinning face), so it
is modelled as a fair die whose 1 is replaced by that joker — the same face the
Devil's head die replaces, which is why holding it alone counts as a 1. Only
the uniform weights are an assumption; the joker's face is not.
- **The two joker faces score differently**, and the scorer keeps them apart. A
Devil's head "matches any combination but never scores on its own", so it can
never be the lone 1 or 5 that lets you hold a die; Balatro's joker can. Both
are assumed able to fill in for any face of a combination, including one made
entirely of jokers — the wiki's examples only ever pair a joker with natural
dice, so that part is an assumption.
- **The three "Advantage" formations** (Carpenter's Cut, Executioner's Gallows,
Priest's Eye) and the **Headstart** point leads have no published values. The
constants used are marked `UNVERIFIED` in `src/data/badges.ts`. Correct them
from the game's help screen if you have it open.
- **Badge point-per-game figures** are estimates for ranking badges against each
other, not a prediction of a scoreline.
- The wiki and [Inara](https://inara.cz/kingdom-come-2/items-dice/) **disagree**
on about six dice (Ci/Fer/Lu, Painted, Trinity, Holy Trinity, Devil's head).
The [fandom wiki](https://kingdom-come-deliverance.fandom.com/wiki/Dice/KCD2)
is treated as canonical; the Inara figures sit in a comment beside each
conflicting entry.