[Analytics] Return zero instead of NaN for impossible draw probabilities

The draw-probability widget computes a hypergeometric probability as a sum of
qLn terms. Two argument combinations reach a non-positive term and return NaN,
which the results table prints as "nan":

- k > K, asking for more copies than the deck holds. A deck with one Forest,
  draw 7, "exactly 3" gives K=1, k=3.
- n - k > N - K, where drawing k targets would need more non-target cards than
  exist. An 8-card deck with 6 targets, draw 5, "exactly 0" gives N-K=2, n-k=5.

Both are reachable: the quantity and drawn spinboxes allow 1-60, and the
"at least" branch caps k at copies while the "exactly" branch passes the spinbox
value straight through.

Extract the function so it can be tested on its own, and guard both cases.
Restoring the old guard fails 3 of the 7 new tests.
This commit is contained in:
Lukas Brübach 2026-10-05 12:01:50 +02:00
parent 90bb3b8807
commit a438be8bda
7 changed files with 134 additions and 21 deletions

View file

@ -201,6 +201,7 @@ set(cockatrice_SOURCES
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config.cpp
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_config_dialog.cpp
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp
src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/hypergeometric_probability.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_config_dialog.cpp
src/interface/widgets/deck_analytics/analyzer_modules/mana_base/mana_base_widget.cpp

View file

@ -6,6 +6,7 @@
#include "../../deck_list_statistics_analyzer.h"
#include "deck_list_model.h"
#include "draw_probability_config_dialog.h"
#include "hypergeometric_probability.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/deck_list/tree/deck_list_card_node.h"
@ -233,23 +234,3 @@ void DrawProbabilityWidget::updateFilterOptions()
resultTable->setItem(row, 2, new QTableWidgetItem(QString::number(probability * 100.0, 'f', 2)));
}
}
double DrawProbabilityWidget::hypergeometricProbability(int N, int K, int n, int k)
{
if (k < 0 || k > n || K > N || n > N) {
return 0.0;
}
double logP = 0.0;
for (int i = 1; i <= k; ++i) {
logP += qLn(double(K - k + i) / i);
}
for (int i = 1; i <= n - k; ++i) {
logP += qLn(double(N - K - (n - k) + i) / i);
}
for (int i = 1; i <= n; ++i) {
logP -= qLn(double(N - n + i) / i);
}
return qExp(logP);
}

View file

@ -3,6 +3,7 @@
#include "../../abstract_analytics_panel_widget.h"
#include "draw_probability_config.h"
#include "hypergeometric_probability.h"
#include <qtmetamacros.h>
@ -51,7 +52,6 @@ private:
QTableWidget *resultTable;
double hypergeometricProbability(int N, int K, int n, int k);
double calculateProbability(int totalCards, int copies, int drawn, bool atLeast);
};

View file

@ -0,0 +1,27 @@
#include "hypergeometric_probability.h"
#include <QtMath>
double hypergeometricProbability(int N, int K, int n, int k)
{
// Every rejected case here would otherwise reach qLn with a non-positive argument and
// produce NaN. Beyond the obvious out-of-range checks, the two that matter are k > K (you
// cannot draw more targets than exist) and n - k > N - K (drawing k targets would need more
// non-target cards than the deck holds).
if (k < 0 || k > n || k > K || K > N || n > N || n - k > N - K) {
return 0.0;
}
double logP = 0.0;
for (int i = 1; i <= k; ++i) {
logP += qLn(double(K - k + i) / i);
}
for (int i = 1; i <= n - k; ++i) {
logP += qLn(double(N - K - (n - k) + i) / i);
}
for (int i = 1; i <= n; ++i) {
logP -= qLn(double(N - n + i) / i);
}
return qExp(logP);
}

View file

@ -0,0 +1,12 @@
#ifndef COCKATRICE_HYPERGEOMETRIC_PROBABILITY_H
#define COCKATRICE_HYPERGEOMETRIC_PROBABILITY_H
/** @brief Probability of drawing exactly k of K target cards in a hand of n from a population of N.
*
* Returns 0.0 for arguments that cannot describe a draw, which includes k > K: you cannot draw
* more target cards than the deck holds, and asking for it yields log of a negative term rather
* than a probability.
*/
double hypergeometricProbability(int N, int K, int n, int k);
#endif // COCKATRICE_HYPERGEOMETRIC_PROBABILITY_H