c++ code to adapt to removal of generic non-pair cards in pair splitting
A pair hand consists of 2 pair cards to be split given a specific dealer up card. With a finite number of decks a simple way to view the shoe composition after the initial pair and up card have been dealt is to separate it into 2 groups of cards; undealt pair cards and undealt non-pair cards. This algorithm more fully addresses adapting to non-pair card removal than this approximate solution c++ code to approximate computation of any number of splits in blackjack.
In blackjack expected values for non-pair hands are relatively simple to compute. Basic computed values are stand EV, double EV, and hit EV for the 2 initially dealt player cards. This relative simplicity can be extended to computing splits by employing 2 basic values. First is EVx, which is defined as the EV of drawing to a single pair card without resplitting if another pair card is drawn. Second is EVPair, which is the EV of playing an unsplit pair.
EVx is EV of drawing ranks 1,2,3,4,5,6,7,8,9,10, one of which is the pair value, to a single pair card
let EVn = EV of drawing a non-pair card to a single pair card
if pP = probability of drawing another pair card, the basic relationship of EVx, EVPair and EVn is:
EVn = (EVx - pP*EVPair) / (1 - pP)
This relationship can be extended to any shoe composition so the value of drawing a non-pair card can always be computed in terms of EVx, EVPair, and probability of drawing a pair card. The algorithm relies on this relationship to compute its values.
Finite Shoe Algorithm Parameters
pCards = number of undrawn to pair cards (initially = 2)
remSp = number of remaining splits (= allowed splits - 1)
p = number of pair cards present after 2 pair cards and dealer up card have been dealt
np = number of non-pair cards present after 2 pair cards and dealer up card have been dealt
pRem = number of pair cards removed from initial shoe state (initially = 0)
xh[] = array to store multipliers of EVx values (each element initialized to 0)
ph[] = array to store multipliers of EVPair values (each element initialized to 0)
prob = probability (initially = 1)
Use of Algorithm
The algorithm by itself does not compute split EV. It requires computation of EVx and EVPair values given number of pair cards removed. The range of EVx and EVPair values needed depends upon number of remSp = allowed splits - 1. For example computation of 3 allowed splits (remSp = 2) requires values for 0,1,2,3,4 pair cards removed.
Code
double getSplitHands(const int &pCards, const int &remSp, const int &p, const int &np, const int &pRem, double xh[], double ph[], const double &prob) { if (p == 0 || remSp == 0) { xh[pRem] += prob * pCards; return (double)pCards; } double pP = double(p) / (p + np); double hp = 0, hn = 0; // hp = expected hands when pair card is drawn hp = pP * getSplitHands(pCards + 1, remSp - 1, p - 1, np, pRem + 1, xh, ph, pP * prob); // hn = expected hands when non-pair card is drawn // computed on condition of not drawing a pair card so only pair cards are removed if (pCards > 1) { double hfull = 0, hfull_p = 0; hfull = getSplitHands(pCards - 1, remSp, p, np, pRem, xh, ph, prob); hfull_p = getSplitHands(pCards - 1, remSp, p - 1, np, pRem + 1, xh, ph, (-pP * prob)); hn = (hfull - pP * hfull_p) + (1 - pP); // = (1 - pP) * ((hfull - pP * hfull_p) / (1 - pP) + 1) } else // pCards == 1 hn = (1 - pP); xh[pRem] += prob; ph[pRem] -= (pP * prob); return hp + hn; }
Example Finite Shoe: split EV 2-2 v 3, NDAS 6 decks s17, 3 allowed splits
double EVx[5] = {-.047220209964047750,-.047045547796896001,-.046879352186933704, -.046721724929954231,-.046572767612829226}; double EVPair[5] = {-.081884586831497574,-.081716096948035125,-.081558453563186556, -.081411936217665126, 0}; // EVPair[4] doesn't need to be computed double xh[5] = {0}, ph[5] = {0}; // initialize to 0 int remSp = 2; double expectedHands = getSplitHands(2, remSp, 22, 287, 0, xh, ph, 1); // run code after code runs xh[0] = 2, xh[1] = 0.28478964401294499, xh[2] = 0.038834951456310690 xh[3] = -0.0037949463963821515, xh[4] = 7.8544642190915976e-005 ph[0] = -0.14239482200647249, ph[1] = -0.019417475728155338 ph[2] = 0.0018974731981910753, ph[3] = -3.9272321095457988e-005 expectedHands = 2.1599540968575321 // use algorithm data to compute split EV double testEV = 0; double handCheck = 0; for (int pRem = 0; pRem <= 4; ++pRem) { testEV += xh[pRem] * EVx[pRem] + ph[pRem] * EVPair[pRem]; handCheck += xh[pRem] + ph[pRem]; } testEV computes to -0.096390309951751879 = split EV 2-2 v 3, NDAS 6 decks s17 handCheck computes to 2.1599540968575317 = EVx hands + EVPair hands // agrees with expectedHands
Infinite Shoe Version
The infinite shoe version is essentially the same as the finite version with one exception. In an infinite shoe the probability of drawing any rank is constant whereas in a finite shoe rank probabilities change as cards are removed. Rather than passing p = pair cards present and np = non-pair cards present as in the finite shoe code, a single parameter p = constant probability of drawing a pair card is passed in the infinite shoe version.
Infinite Shoe Code
double infGetSpHands(const int &pCards, const int &remSp, const double &p, const int &pRem, double xh[], double ph[], const double &prob) { if (remSp == 0) { xh[pRem] += prob * pCards; return (double)pCards; } double hp = 0, hn = 0; hp = p * infGetSpHands(pCards + 1, remSp - 1, p, pRem + 1, xh, ph, p * prob); if (pCards > 1) { double hfull = 0, hfull_p = 0; hfull = infGetSpHands(pCards - 1, remSp, p, pRem, xh, ph, prob); hfull_p = infGetSpHands(pCards - 1, remSp, p, pRem + 1, xh, ph, (-p * prob)); hn = (hfull - p * hfull_p) + (1 - p); // = (1 - p) * ((hfull - p * hfull_p) / (1 - p) + 1) } else // pCards == 1 hn = (1 - p); xh[pRem] += prob; ph[pRem] -= (p * prob); return hp + hn; }
Example Infinite Shoe: split EV T-T v 6, standard infinite deck, H17, NDAS, 3 allowed splits
// 10,10 vs 6; standard infinite deck; H17, NDAS | p = 4/13 = prob(10) const double EVPair = 0.678245261281511; // EV for playing (10,10) = 20 vs 6 (not split) using best strategy const double EVx = 0.282480847764203; // EV for a single 10, no more splits allowed // EVn not necessary to algorithm: EVn = (EVx - p*EVPair) / (1 - p) const double EVn = 0.106585552867622; // EV for a single hand when non-pair card is drawn first const double p = double(4) / 13; // constant probability of drawing a pair card double xh[5] = {0}, ph[5] = {0}; // initialize to 0 int remSp = 2; double expectedHands = infGetSpHands(2, remSp, p, 0, xh, ph, 1); // run code after code runs xh[0] = 2, xh[1] = 1.230769230769231, xh[2] = 0.757396449704142 xh[3] = -0.349567592171143, xh[4] = 0.035853086376527 ph[0] = -0.615384615384615, ph[1] = -0.378698224852071 ph[2] = 0.174783796085571, ph[3] = -0.017926543188264 expectedHands = 2.837225587339379 // use algorithm data to compute split EV double testEV = 0; double handCheck = 0; for (int pRem = 0; pRem <= 4; ++pRem) { testEV += xh[pRem] * EVx[pRem] + ph[pRem] * EVPair[pRem]; handCheck += xh[pRem] + ph[pRem]; } testEV computes to 0.47011779565486361 = split EV T-T v 6, NDAS standard infinite shoe H17 handCheck computes to 2.8372255873393790 = EVx hands + EVPair hands // agrees with expectedHands