From f0325e4dbd7e1b0aa67413b35266e8305a5d5888 Mon Sep 17 00:00:00 2001 From: kleinfreund <5774638+kleinfreund@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:41:49 +0100 Subject: [PATCH] fix(core): wrong score when having multiple "Oops! All 6s" jokers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix probabilistic effects being incorrectly simulated with multiple instances of the "Oops! All 6s" joker. Each instance doubles the odds meaning the probability for a lucky card's mult effect to trigger is 1/5 by default, 2/5 with one instance of "Oops! All 6s", 4/5 with 2 instances, and 8/5 (i.e. 1, the guaranteed event) with 3 instances. Previously, the calculator simply added 1 to the numerator (i.e. 1/5 → 2/5 → 3/5, etc.) which isn't correct. --- README.md | 18 ++++++++++++++++++ src/utilities/balanceMultWithLuck.test.ts | 18 +++++++++++------- src/utilities/balanceMultWithLuck.ts | 4 ++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index aa68c90..f53f66b 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,21 @@ ## Contributing [Contribution guidelines for this project](CONTRIBUTING.md) + +## Notes + +### Tell me the odds: how probabilistic effects are handled + +Balatrolator always returns deterministic scores and doesn't roll any “dice” when probabilistic effects (e.g. lucky card) are at play. Instead, it gives you scores for three “luck modes”: + +- **no luck**: the lower bound +- **all luck**: the upper bound +- **average luck**: the score you can expect with perfectly average luck + +“No luck” means probabilistic effects never contribute anything to the score. Lucky cards never apply their +Mult value; Bloodstone never applies its xMult value. + +“All luck” means probablistic effects always contribute to the score. Lucky cards always add their +mult value; Bloodstone always applies its xMult value. + +“Average luck” means probabilistic effects are scored with the scores that you can expect on average. That is, the resulting score is the one you would get as if you would play the same hand an infinite amount of times and averaged the resulting scores. In other words, a lucky card's +Mult value of 20 with standard odds of 1 in 5 would add 4 (20 * 1/5). “Oops! All 6s” jokers do factor into this math: having two instances of that joker would raise the odds of a lucky card's +Mult effect to 4 in 5 and so the value scored would be 16 (20 * 4/5). + +Of special note is the case when there are enough instances of the “Oops! All 6s” joker to guarantee an effect in the game. In that case, the three luck modes become irrelevant and the score is calculated the same way it would in “all luck” mode. diff --git a/src/utilities/balanceMultWithLuck.test.ts b/src/utilities/balanceMultWithLuck.test.ts index 93eb494..61f5cf0 100644 --- a/src/utilities/balanceMultWithLuck.test.ts +++ b/src/utilities/balanceMultWithLuck.test.ts @@ -33,24 +33,28 @@ describe('balance', () => { [3, 3, 3, 'all', 'times', 3], [3, 4, 3, 'all', 'times', 3], + [4, 0, 3, 'none', 'times', 1], [4, 0, 3, 'average', 'times', 2], [4, 1, 3, 'average', 'times', 3], [4, 2, 3, 'average', 'times', 4], - [4, 0, 3, 'none', 'times', 1], [4, 1, 3, 'all', 'times', 4], + [5, 0, 4, 'none', 'plus', 0], + [5, 0, 4, 'average', 'plus', 1.25], + [5, 1, 4, 'average', 'plus', 2.5], + [5, 2, 4, 'average', 'plus', 5], + [5, 1, 4, 'all', 'plus', 5], + [20, 0, 5, 'none', 'plus', 0], [20, 1, 5, 'none', 'plus', 0], [20, 2, 5, 'none', 'plus', 0], - [20, 3, 5, 'none', 'plus', 0], + [20, 3, 5, 'none', 'plus', 20], [20, 4, 5, 'none', 'plus', 20], [20, 5, 5, 'none', 'plus', 20], [20, 0, 5, 'average', 'plus', 4], [20, 1, 5, 'average', 'plus', 8], - [20, 2, 5, 'average', 'plus', 12], - [20, 3, 5, 'average', 'plus', 16], - [20, 4, 5, 'average', 'plus', 20], - [20, 5, 5, 'average', 'plus', 20], + [20, 2, 5, 'average', 'plus', 16], + [20, 3, 5, 'average', 'plus', 20], [20, 0, 5, 'all', 'plus', 20], [20, 1, 5, 'all', 'plus', 20], [20, 2, 5, 'all', 'plus', 20], @@ -58,7 +62,7 @@ describe('balance', () => { [20, 4, 5, 'all', 'plus', 20], [20, 5, 5, 'all', 'plus', 20], [20, 6, 5, 'all', 'plus', 20], - ])('works', (mult, oopses, denominator, luck, mode, expectedResult) => { + ])('works (mult: %s, oopses: %s, p: 1/%s, luck: %s, mode: %s)', (mult, oopses, denominator, luck, mode, expectedResult) => { expect(balanceMultWithLuck(mult, oopses, denominator, luck, mode)).toBe(expectedResult) }) }) diff --git a/src/utilities/balanceMultWithLuck.ts b/src/utilities/balanceMultWithLuck.ts index 522dce5..1f20cfa 100644 --- a/src/utilities/balanceMultWithLuck.ts +++ b/src/utilities/balanceMultWithLuck.ts @@ -5,13 +5,13 @@ import type { Luck } from '#lib/types.js' * * For +Mult (as indicated by `mode === 'plus'`), the mult value is balanced in the range [0, mult] (e.g. [0, 20] for a lucky card). For xMult (as indicated by `mode === 'times'`), the mult value is balanced in the range [1, mult] (e.g. [1, 2] for Bloodstone). * - * The probability of an effect (i.e. “1 in $denominator chance”) is used to determine the _average_ mult value. For example, with lucky card's 1 in 5 chance, the average value used would be 1/5*20 = 4. The numerator is raised by the number of “Oops! All 6s” jokers. Notably, having “Oops! All 6s” four times would guarantee lucky cards to trigger because their probability would be 5/5 = 1. + * The probability of an effect (i.e. “1 in $denominator chance”) is used to determine the _average_ mult value. For example, with lucky card's 1 in 5 chance, the average value used would be 1/5*20 = 4. The numerator is 2 raised to the power of number of “Oops! All 6s” jokers (i.e. without any of them, it would be 1, then 2, then 4, and so on). Notably, having “Oops! All 6s” three times would guarantee lucky cards's mult effects to trigger because their probability would be 8/5 = 1. * * The `luck` parameter modifies the result by either implying “no luck” (in order words: minimum luck or a probability of 0) or “all luck” (in other words: maximum luck or a probability of 1). A number of “Oops! All 6s” jokers resulting in a probability of 1 forces “all luck” even if the `luck` parameter is not “all luck”. */ export function balanceMultWithLuck (mult: number, oopses: number, denominator: number, luck: Luck, mode: 'times' | 'plus'): number { const neutralElement = mode === 'times' ? 1 : 0 - const minimumNumerator = Math.max(0, Math.min(oopses + 1, denominator)) + const minimumNumerator = Math.max(0, Math.min(Math.pow(2, oopses), denominator)) let numerator = minimumNumerator if (luck === 'all') {