I've taken the challenge of verifying the simulations for Jacks/Better 9/6 as well as Double Jokers poker.
I've looked at the video and source code for an exhaustive search.
I have noticed an issue with the draw function, which starts from the initial deal, and cycles through all possible hold combinations to determine the optimal play.
This function also allows a discarded card to reappear, and increments draw_score_array. In true video poker, a discarded card cannot reappear. The hit rate calculations are hence incorrect.
The irony is that if you search the internet, most poker odds calculators have the same issue. I wouldn't be suprised that they use your data tables.

Here, the plus and minuses tell you whether to add or subtract an array element from the total number of legal draws. So, if you draw to a four card RF, you start with 48 total draws, including the one when the discard comes back into the hand. Then, using the triangle, you subtract off the one hand that contains the RF. This gets you to the correct answer, 47.
If you are looking at a case where you discard all 5 cards, the process is identical, but the number of terms in the correction is much larger. It is a very clever and efficient way of doing the calculations. I learned this from reading explanations from VPGenius and the Wizard 25 years ago. I don't know if a good explanation can be found anywhere on the web today. The VPGenius page seems to be gone.
Now, for a given hand, I start by assuming the best play is "hold all five cards," with value = the hand's value.
For each card, I calculate the "discard that one card" value by taking the value of the 4-card hand of the 4 cards held, subtracting the 5-card hand value of the original hand (since you can't redraw the discard), and dividing the result by 47 (since there are only 47 ways to draw 1 card).
For each pair of cards, I calculate the "discard those two cards" values by starting with the value of the 3-card hand with the 3 held cards, subtracting each of the 4-card values of those three cards plus one of the discarded cards, and adding back the 5-card hand value of the original 5 cards (this works because of the Inclusion-Exclusion Principle), then dividing by 1081 (as there are 1081 ways to draw 2 cards from 47).
Repeat this for each set of 3 cards held, then each set of 4, and finally, for drawing all 5 cards.
Whichever of these 32 plays returns the highest value is the best play.
Quote: ThatDonGuyI do it slightly differently. First, I calculate the hand values of (a) all 2,598,960 5-card hands, (b) all 270,725 4-card hands, (c) all 22,100 3-card hands, (d) all 1326 2-card hands, (e) all 52 1-card "hands", and (f) the one "zero-card hand," which is the sum of all of the 5-card hands.
Now, for a given hand, I start by assuming the best play is "hold all five cards," with value = the hand's value.
For each card, I calculate the "discard that one card" value by taking the value of the 4-card hand of the 4 cards held, subtracting the 5-card hand value of the original hand (since you can't redraw the discard), and dividing the result by 47 (since there are only 47 ways to draw 1 card).
For each pair of cards, I calculate the "discard those two cards" values by starting with the value of the 3-card hand with the 3 held cards, subtracting each of the 4-card values of those three cards plus one of the discarded cards, and adding back the 5-card hand value of the original 5 cards (this works because of the Inclusion-Exclusion Principle), then dividing by 1081 (as there are 1081 ways to draw 2 cards from 47).
Repeat this for each set of 3 cards held, then each set of 4, and finally, for drawing all 5 cards.
Whichever of these 32 plays returns the highest value is the best play.
link to original post
I believe that you, I, VPGenius, and the Wiz are all effectively doing the same thing. I just explicitly use the Sierpinski triangle to keep track of the Inclusion-Exclusion math.
[
void storeCount::tCountUpdate(int n, int t, int dt) {
int row, s;
for (row = 0; row < HOLD_LIM; row++) {
s = sierpinski[dt];
if (s == 2) {
tCountStore[t] += n;
} else if (s == 1) {
tCountStore[t] -= n;
}
}
}
where n is the number of hands of type t, and dt is the index to the 32 ways of drawing to a starting hand.
For the case where we hold all five cards, there is one hand of type t, and a single call to store.tCountUpdate(1, t, HOLD5) does one addition because we are on the tip of the triangle. The rest of the Inclusion-Exclusion math is all done by this simple loop:
for (dt = 0; dt < HOLD5; dt++) {
nh = numHeld[dt];
switch (nh) {
case 4:
k = hand.key4(dt);
for (t = 0; t < maxType; t++) {
n = hand.array1[t][k];
if (n) store.tCountUpdate(n, t, dt);
}
break;
case 3:
k = hand.key3(dt);
for (t = 0; t < maxType; t++) {
n = hand.array2[t][k];
if (n) store.tCountUpdate(n, t, dt);
}
break;
case 2:
k = hand.key2(dt);
for (t = 0; t < maxType; t++) {
n = hand.array3[t][k];
if (n) store.tCountUpdate(n, t, dt);
}
break;
case 1:
k = hand.key1(dt);
for (t = 0; t < maxType; t++) {
n = hand.array4[t][k];
if (n) store.tCountUpdate(n, t, dt);
}
break;
case 0:
for (t = 0; t < maxType; t++) {
n = hand.array5[t];
if (n) store.tCountUpdate(n, t, dt);
}
break;
default:
printf("%2d line %d\n", dt, __LINE__); fflush(stdout);
exit(0);
break;
}
}
where the hand.arrayX[] are the counts of hands containing X cards. The code performs 32 additions/subtractions for the case of discarding all five cards, so a programmer needs something like the triangle to keep track of which operations to use for each case.
How do you perform the Inclusion–Exclusion math and keep everything straight?Gambling is a math contest where the score is tracked in dollars. Try not to get a negative score.November 4th, 2025 at 5:32:53 PM permalinkaren't humans awesome based on this thread?November 5th, 2025 at 7:39:05 AM permalinkI see how the triangle works now, and you are right.Quote: MentalI believe that you, I, VPGenius, and the Wiz are all effectively doing the same thing. I just explicitly use the Sierpinski triangle to keep track of the Inclusion-Exclusion math.
"The long way" - with 32 separate calculations.Quote: MentalHow do you perform the Inclusion–Exclusion math and keep everything straight?
link to original post
For example, to calculate the expected return of discarding the first and fifth cards:PlayValue =
(
Hands2[Hand2(C1, C5)]
- Hands3[Hand3(C1, C2, C5)] - Hands3[Hand3(C1, C3, C5)] - Hands3[Hand3(C1, C4, C5)]
+ Hands4[Hand4(C1, C2, C3, C5)] + Hands4[Hand4(C1, C2, C4, C5)] + Hands4[Hand4(C1, C3, C4, C5)]
- Hands5[Hand5(C1, C2, C3, C4, C5)]
) / 16215.0;
Here, PlayValue is a double containing the ER for this particular play
C1, C2, C3, C4, and C5 are integers representing the five cards, from 0 to 51 (0 to 12 are A-K of spades, 13-25 are hearts, and so on)
Hand2(C1, C5) converts the pair (C1, C5) to a single number - for example, (0, 1) is 0, (0, 2) is 1, ..., (0, 51) is 50, (1, 2) is 51, (1, 3) is 52, ..., (1, 52) is 99, (2, 3) is 100, and so on
Hands2[n] contains the sum of the values of all of the hands that contain the 2 cards represented by n
Hand3, Hand4, Hand5, Hands3[ ], Hands4[ ], and Hands5[ ] work the same way, but with sets of 3, 4, and 5 cards.
16,215 is the number of sets of 3 cards that are in the 47 cards remaining in the deckNovember 5th, 2025 at 9:42:33 AM permalinkI wrote a computer program to calculate VP perfect strategy and it matches WOO.
My program ran through all possible results to calculate the best strategy. It calculates and then selects the maximum of the value of holding all 5 cards, the expected value of holding 4 cards (5 choices, 47 possible results), the expected value of holding 3 cards (10 choices, 1081 possible results), etc.November 5th, 2025 at 2:29:39 PM permalinkQuote: ThatDonGuy
"The long way" - with 32 separate calculations.Quote: MentalHow do you perform the Inclusion–Exclusion math and keep everything straight?
link to original post
For example, to calculate the expected return of discarding the first and fifth cards:PlayValue =
(
Hands2[Hand2(C1, C5)]
- Hands3[Hand3(C1, C2, C5)] - Hands3[Hand3(C1, C3, C5)] - Hands3[Hand3(C1, C4, C5)]
+ Hands4[Hand4(C1, C2, C3, C5)] + Hands4[Hand4(C1, C2, C4, C5)] + Hands4[Hand4(C1, C3, C4, C5)]
- Hands5[Hand5(C1, C2, C3, C4, C5)]
) / 16215.0;
Here, PlayValue is a double containing the ER for this particular play
C1, C2, C3, C4, and C5 are integers representing the five cards, from 0 to 51 (0 to 12 are A-K of spades, 13-25 are hearts, and so on)
Hand2(C1, C5) converts the pair (C1, C5) to a single number - for example, (0, 1) is 0, (0, 2) is 1, ..., (0, 51) is 50, (1, 2) is 51, (1, 3) is 52, ..., (1, 52) is 99, (2, 3) is 100, and so on
Hands2[n] contains the sum of the values of all of the hands that contain the 2 cards represented by n
Hand3, Hand4, Hand5, Hands3[ ], Hands4[ ], and Hands5[ ] work the same way, but with sets of 3, 4, and 5 cards.
16,215 is the number of sets of 3 cards that are in the 47 cards remaining in the deck
link to original post
Thanks for the reply.
For the OP or anyone trying to program a VP analyzer, I cannot emphasize how useful it is to understand what this calculation does and how it speeds up a VP analyzer. It takes just 0.22 milliseconds for me to calculate all this information and cache it. Then, I use the cached information over and over again to analyze each of the 152,646 hands. Then it takes 1.4 seconds for me to analyze jacks or better and create a full strategy.
I recall that TomSki was given credit by Bob Dancer and Dan Paymar for speeding up their commercial VP analyzer. I believe TomSki did the same caching trick.
I don't know where the clearest explanation can be found. It took me a while reading all the abovenamed sources to figure out this trick. Wizard explains some of it here: https://wizardofodds.com/games/video-poker/methodology/Gambling is a math contest where the score is tracked in dollars. Try not to get a negative score.November 5th, 2025 at 2:37:08 PM permalinkQuote: MentalFor the OP or anyone trying to program a VP analyzer, I cannot emphasize how useful it is to understand what this calculation does and how it speeds up a VP analyzer. It takes just 0.22 milliseconds for me to calculate all this information and cache it. Then, I use the cached information over and over again to analyze each of the 152,646 hands. Then it takes 1.4 seconds for me to analyze jacks or better and create a full strategy.
link to original post
152,646 hands? Shouldn't that be 134,459?November 6th, 2025 at 5:08:00 AM permalinkQuote: ThatDonGuyQuote: MentalFor the OP or anyone trying to program a VP analyzer, I cannot emphasize how useful it is to understand what this calculation does and how it speeds up a VP analyzer. It takes just 0.22 milliseconds for me to calculate all this information and cache it. Then, I use the cached information over and over again to analyze each of the 152,646 hands. Then it takes 1.4 seconds for me to analyze jacks or better and create a full strategy.
link to original post
152,646 hands? Shouldn't that be 134,459?
link to original post
Yes, I grabbed the number of unique hands for a double joker game but I used a 52-card deck for the timing test.
Here are the numbers for different numbers of jokers.
No Jokers
Combos: 2,598,960
Unique: 134,459
One Joker
Combos: 2,869,685
Unique: 150,891
Two Jokers
Combos: 3,162,510
Unique: 152,646
I redid the speed test on a faster laptop, but still using only one thread. I can analyze JOB and create an optimal strategy in 0.96 seconds. For double joker games, this takes 1.06 seconds.Gambling is a math contest where the score is tracked in dollars. Try not to get a negative score.

