See the top rated post in this thread. Click here

Page 1 of 4 123 ... LastLast
Results 1 to 13 of 43

Thread: "Fast" optimal split EVs

  1. #1


    Did you find this post helpful? Yes | No

    "Fast" optimal split EVs

    This is motivated by the recent long thread that was originally about testing CA algorithm correctness-- originally by verifying zero average EOR... but eventually we took a detour to computing optimal split EVs as an alternative "upper bound test": if a CA produces a value greater than that achieved by brute-force perfect play, then it can't possibly be correct under any assumptions about strategy complexity.

    The difficulty is that computing such "perfect play" (i.e., maximum possible) expected values for splitting is generally very expensive. We managed the recent discussion by focusing on very small depleted shoes with just a couple of card ranks; k_c has provided software that can evaluate SPL1 values in just seconds to minutes, while iCountNTrack has software that supports resplits as well.

    To try to further extend our space of feasible analysis, I wrote a program to compute maximum possible split EVs, still in seconds for SPL1, but also in "just" minutes for SPL2 and SPL3 as well. The C++ source code is on GitHub, along with a pre-built Windows executable blackjack_split.exe in the latest release. I've written a short article on my blog describing implementation details.

    Why might we care about this? Recall the analysis from several years ago evaluating SCORE across the spectrum of sophistication of player betting and playing strategy, ranging from the simplest total-dependent fixed basic strategy player at one end, to the "optimal, bring a laptop to the table" player at the other end.

    But that "optimal" player was using CDZ- strategy, since that is what is/was computationally feasible for evaluating the thousands of simulated shoes in the analysis. But that isn't truly "perfect play" in the sense of maximizing the possible expected return. How sub-optimal is it? If we're bring a hypothetical laptop to the table anyway, then how much advantage are we leaving on the table by not computing truly EV-maximizing strategy?

    Following are some example split EVs (in fraction of initial wager) for 1D, S17, DOA, NDAS, SPL3. The first "OPT" table contains the newly computed optimal values, while the second, for comparison, contains CDZ- values representative of that past SCORE analysis.

    Code:
    ====================================================================================================================================================================================================================================
    1D, S17, DOA, NDAS, SPL3, OPT
    
         | Dealer up card
    Pair |   2                     3                     4                      5                     6                      7                     8                     9                    T                     A
    -----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    2-2  | -0.12731327461240569  -0.06731019360527625   0.0087330117071888307  0.13652044928739038   0.13768798528514201   -0.049315002984689875 -0.21190454316571589  -0.38122023704989311 -0.50255734773248062  -0.61464970267534458
    3-3  | -0.19318563629688118  -0.12441600311987655   0.015778629999961557   0.12791103774266827   0.12217131923338384   -0.10694234748364158  -0.26361085030654557  -0.41570182324569077 -0.54391929767942482  -0.64394112951019133
    4-4  | -0.23493247667735509  -0.1206370903640397   -0.013395857124847144   0.094741846106706246  0.083444913574413221  -0.22081240332913163  -0.3418082428186367   -0.48814498547138774 -0.60495120625027488  -0.69510367184729394
    5-5  | -0.22905355382249015  -0.14409121743587841  -0.038239245870428443   0.06811715183228835   0.055851329454062487  -0.29764402338584556  -0.44735853432229311  -0.60049476943714997 -0.69678845102909759  -0.76313200859584351
    6-6  | -0.21157486017258931  -0.12144970378444321  -0.014073802832270575   0.085134021344917987 -0.0038992953078225644 -0.26751870968301378  -0.41251515655243737  -0.56908727506807166 -0.67703785743455092  -0.75591360816357334
    7-7  | -0.14873295085672986  -0.063462123779702381  0.034107274188655778   0.055776863459996863  0.072758015082720881  -0.11030609702311869  -0.42207407649963452  -0.56381157310756069 -0.65354885304539456  -0.73518571315045211
    8-8  |  0.045128998678136037  0.11096084478766208   0.13323104254871435    0.21837336041724606   0.26951283699734219    0.25159280874367501  -0.086925224939870566 -0.42615645609503905 -0.50180563227964692  -0.5453317096717446
    9-9  |  0.17704885667658807   0.17580879829033808   0.2588213565968715     0.34973201289897327   0.36599920786349094    0.34057565610754925   0.19039113052141743  -0.10863147347842586 -0.32491952594157031  -0.37470891415372926
    T-T  |  0.31595096135271911   0.36607900821481326   0.42478479130737634    0.49830546079518145   0.52510690762664047    0.48227301277099821   0.35386413114253584   0.18381704634997303 -0.026022647269274436 -0.21532610008466624
    A-A  |  0.63924713264607214   0.6868416881450462    0.74246911512815439    0.80746553501633411   0.8319646426102727     0.6276495853643872    0.48637078169055392   0.3607810866349318   0.20811519238424928  -0.15070516782918142
    ==================================================================================================================================================================================================================================
    1D, S17, DOA, NDAS, SPL3, CDZ-
    
         | Dealer up card
    Pair |   2                     3                     4                      5                     6                      7                     8                     9                    T                    A
    -----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    2-2  | -0.12931445970716204  -0.068618813839618528  0.0063215711015360519  0.13648927765382843   0.12681199250828923   -0.050855651039314019 -0.21834617353389765  -0.39594773644726194 -0.51775639589807398 -0.62438870325939566
    3-3  | -0.20018179410829598  -0.12712943438577579   0.015778576780075281   0.12791103753057767   0.12217113576498841   -0.10823164361256125  -0.26546195865777805  -0.42462980388086446 -0.55634785696316225 -0.65071599994628226
    4-4  | -0.25576241682421935  -0.13239585860670491  -0.017427876593697697   0.09080401040040674   0.076505621307971811  -0.25343883745279933  -0.36898301985560711  -0.51707499473329499 -0.63178130124554599 -0.71525578854871685
    5-5  | -0.27035899308060563  -0.1772648844187503   -0.065368771196269049   0.05722109169076979   0.030293363670536821  -0.35223826141208464  -0.50869108989381506  -0.67185301199075231 -0.75703952124603435 -0.808325622036281
    6-6  | -0.21236460735070015  -0.12162948975202717  -0.014073802832292792   0.085134021344917807 -0.0038992956154047177 -0.26786412855204106  -0.41861326291163969  -0.58483542757744078 -0.69838888595907922 -0.77007885965472822
    7-7  | -0.15189635030306192  -0.063941323594190511  0.034107273409023989   0.055776850293461411  0.072757499209976415  -0.11031949483251607  -0.42227357145327449  -0.56885738375054895 -0.66634926728142962 -0.73857097928570969
    8-8  |  0.043727194275322241  0.11072077572225238   0.13323049096070749    0.218373360417246     0.26951276688328069    0.25158364097599761  -0.08697051592264482  -0.42630090213098648 -0.50212194073579097 -0.54533343302674253
    9-9  |  0.17293295230325983   0.17268715068416318   0.2587370917510543     0.34971860452960885   0.36594971593751413    0.33496879661903617   0.1902755580632351   -0.10882517875157402 -0.3364467800691287  -0.37479066525298377
    T-T  |  0.048114912608600457  0.12425292768197184   0.22481268990874989    0.32661782031578046   0.36357129401635141    0.25178252863011863   0.011765187564729484 -0.25583004271582221 -0.37222940139267474 -0.53910662600957027
    A-A  |  0.63924713264607236   0.68684168814504631   0.7424691151281545     0.80746553501633433   0.83196464261027259    0.62764958536438742   0.48637078169055409   0.3607810866349318   0.20811519238424928 -0.15070516782918114
    =================================================================================================================================================================================================================================

  2. #2


    Did you find this post helpful? Yes | No
    Hi Eric,

    This is a real tour de force!!! It's super fast and I love the state depth update. It takes 2 seconds to do SPL1 for full deck for 2,2 vs 4. It took 12 hours on my version!!!! . I will need to read more to understand what's going on under the hood but once again this is really impressive!! Congrats!!

    Here are my initial observations:

    a) it would be nice if we can get the distribution of outcomes as well as the standard deviation to the output. I suspect it's not possible to accomplish with your fast implementation?

    b) it appears that it doesnt properly handle those pesky depleted decks where it's possible to run out of cards. For example, our famous 11 sixes and 5 eights deck yields `2.3524475524475523` but my ""Globally Optimum" and your Python version both yield `2.295104895104895`. For a deck with 10 sixes and 4 nines it's even worse (that's because you run out of cards more frequently) `5.3030303030303036` versus `0.721212121` from my "Globally Optimum" and your Python code
    Last edited by iCountNTrack; 12-30-2024 at 07:21 PM.
    Chance favors the prepared mind

  3. #3


    Did you find this post helpful? Yes | No
    Quote Originally Posted by iCountNTrack View Post
    Here are my initial observations:

    a) it would be nice if we can get the distribution of outcomes as well as the standard deviation to the output. I suspect it's not possible to accomplish with your fast implementation?
    Correct. Or at least, not without more memory than my readily available machines have. That is, it would be possible, with comparable speed, to compute entire probability distributions instead of just expected values. But to do so would require an order of magnitude more memory (~17 times more for SPL3). With roughly half a billion states, just storing EVs is already ~16 GB of memory.

    Quote Originally Posted by iCountNTrack View Post
    b) it appears that it doesnt properly handle those pesky depleted decks where it's possible to run out of cards. For example, our famous 11 sixes and 5 eights deck yields `2.3524475524475523` but my ""Globally Optimum" and your Python version both yield `2.295104895104895`. For a deck with 10 sixes and 4 nines it's even worse (that's because you run out of cards more frequently) `5.3030303030303036` versus `0.721212121` from my "Globally Optimum" and your Python code
    Yep, I saw these differences as well. It's not that this algorithm doesn't handle them, but rather that it handles them with slightly different assumptions about what happens when running out of cards. The original Python implementation from the earlier thread assumes that the value of drawing from an empty shoe-- whether the player hitting or doubling down, or the dealer hitting-- is -infinity, so that we are actively avoiding any strategy that has even the remotest positive probability of running out of cards.

    Here, I'm re-using the very fast calculation of dealer probabilities (as ten Steiner trees, one for each up card). Part of that speed optimization is to not explicitly compute the probability of the dealer busting, but rather only explicitly compute probabilities of the dealer reaching totals of 17 through 21, and subtracting these probabilities from 1 to get P(bust). The result of this approach is that, for very depleted shoes where it is possible for the dealer to run out of cards, those possibilities get "rolled up" into P(bust).

    In other words, I'm implicitly assuming that (1) the value of the player drawing from an empty shoe is still -infinity, so that the player strategy will still avoid running out of cards... but that (2) the dealer running out of cards is treated as a dealer bust. (We can reproduce this behavior in the Python implementation by changing the return float('-inf') in line 54 to return dealer(shoe, 22, hands, wagers); doing so, I have verified that the values from the Python and C++ algorithms match exactly in all cases.)

    The difference manifests in about 12% of the 4,840 depleted shoe scenarios we've been looking at, and those all have an average of less than a dozen cards. In reality, I imagine the dealer would never begin a round from such a small shoe anyway, or if he did, neither of these assumptions (-infinity nor bust) would be correct, since I believe technically the procedure would be to reshuffle and continue dealing from a full shoe?

  4. #4


    Did you find this post helpful? Yes | No
    Quote Originally Posted by ericfarmer View Post
    Correct. Or at least, not without more memory than my readily available machines have. That is, it would be possible, with comparable speed, to compute entire probability distributions instead of just expected values. But to do so would require an order of magnitude more memory (~17 times more for SPL3). With roughly half a billion states, just storing EVs is already ~16 GB of memory.
    Understood, the 2,2 SPL3 for 6D ran out of memory on my 128 GB machine and I gave it over 96GB of RAM!!! It got to 4.3 billion states and died!

    I have to better understand how you are storing the states, one thing I did was to make the stored states agnostic to split Hands order of compositions, meaning if SplitHand1: 10,8 and SplitHand2: 10,2 that's the same thing as SplitHand1: 10, 2 and SplitHand2: 10, 8. I also store whether the SplitHand is completed and whether it was doubled. I was looking at the number of stored states and it looks they are for the most part very comparable. I think you benefit from the super fast dealer probabilities calculations you use, while I am using a recursive approach


    Quote Originally Posted by ericfarmer View Post
    Yep, I saw these differences as well. It's not that this algorithm doesn't handle them, but rather that it handles them with slightly different assumptions about what happens when running out of cards. The original Python implementation from the earlier thread assumes that the value of drawing from an empty shoe-- whether the player hitting or doubling down, or the dealer hitting-- is -infinity, so that we are actively avoiding any strategy that has even the remotest positive probability of running out of cards.

    Here, I'm re-using the very fast calculation of dealer probabilities (as ten Steiner trees, one for each up card). Part of that speed optimization is to not explicitly compute the probability of the dealer busting, but rather only explicitly compute probabilities of the dealer reaching totals of 17 through 21, and subtracting these probabilities from 1 to get P(bust). The result of this approach is that, for very depleted shoes where it is possible for the dealer to run out of cards, those possibilities get "rolled up" into P(bust).

    In other words, I'm implicitly assuming that (1) the value of the player drawing from an empty shoe is still -infinity, so that the player strategy will still avoid running out of cards... but that (2) the dealer running out of cards is treated as a dealer bust. (We can reproduce this behavior in the Python implementation by changing the return float('-inf') in line 54 to return dealer(shoe, 22, hands, wagers); doing so, I have verified that the values from the Python and C++ algorithms match exactly in all cases.)
    Understood, in my case I always checked if the dealer has enough cards to have at least a total of hard 17 or soft 17 depending on the game, if not return EV = -infinity this essentially organically will discard a state and the children states as optimal.
    Code:
     //******* Check if there are enough cards to play out the dealer’s hand *********
     vector<short> totalComp(deckCounts.size());
     for (size_t i = 0; i < deckCounts.size(); ++i) {
         // Combine deck composition + dealerOrderedHand composition
         totalComp[i] = deckCounts[i] + dealerOrderedHand[i];
     }
     auto [dealerTotal, dealerIsSoft, dealerHasBlackjack] =
         calculateHandTotalSoftnessAndBlackjack(totalComp, true);
    
     // If the dealer cannot finish their hand, store and return -? for this state
     if ((dealerTotal < 17) || (dealerTotal == 17 && dealerIsSoft && dealerHitsSoft17)) {
         OutcomeStatistics result;
         result.expectationValue = -numeric_limits<double>::infinity();
         result.netOutcomeProbabilities[-numeric_limits<double>::infinity()] = 1;
         memoization[key] = result;
         return result;
     }
    The difference manifests in about 12% of the 4,840 depleted shoe scenarios we've been looking at, and those all have an average of less than a dozen cards. In reality, I imagine the dealer would never begin a round from such a small shoe anyway, or if he did, neither of these assumptions (-infinity nor bust) would be correct, since I believe technically the procedure would be to reshuffle and continue dealing from a full shoe?
    Yes, I am not sure how it's handled in real world that's technically not supposed to happen. I have never played a deeply dealt game (Don?). But the discrepancy can happen for a depleted deck with more cards, for example for `1 1 1 1 1 1 1 4 1 4` which is a quarter deck with 3 extra eights to hand the resplits. Your new code yields `0.65397233168066482` and globally optimum yields `0.653941321024655` which are really close, but there are very few situations where we run out of cards to complete the round.
    Last edited by iCountNTrack; 12-31-2024 at 12:58 AM.
    Chance favors the prepared mind

  5. #5


    Did you find this post helpful? Yes | No
    Quote Originally Posted by iCountNTrack View Post
    Understood, the 2,2 SPL3 for 6D ran out of memory on my 128 GB machine and I gave it over 96GB of RAM!!! It got to 4.3 billion states and died!
    Yeah, I can only run 6D up to SPL2 with exponent=29 on my 32 GB laptop, but for SPL3 I'm confined to 1D, and I can only evaluate all pair cards vs. all up cards for NDAS; for DAS I got stuck trying to split 4s, 3s, and 2s, but I suspect exponent=30 would work there.

    Quote Originally Posted by iCountNTrack View Post
    I have to better understand how you are storing the states, one thing I did was to make the stored states agnostic to split Hands order of compositions, meaning if SplitHand1: 10,8 and SplitHand2: 10,2 that's the same thing as SplitHand1: 10, 2 and SplitHand2: 10, 8. I also store whether the SplitHand is completed and whether it was doubled. I was looking at the number of stored states and it looks they are for the most part very comparable. I think you benefit from the super fast dealer probabilities calculations you use, while I am using a recursive approach.
    Right; I don't even store the individual hand compositions, just their (sorted) totals. This significantly reduces the size of the state space.

    Quote Originally Posted by iCountNTrack View Post
    Understood, in my case I always checked if the dealer has enough cards to have at least a total of hard 17 or soft 17 depending on the game, if not return EV = -infinity this essentially organically will discard a state and the children states as optimal.
    Yeah, I considered doing something like this, but rejected the idea for two reasons:

    1. It slows things down. I don't see how to avoid performing such a check for *all* leaf states (i.e., rounds completed by the player, ready for the dealer to resolve his hand), which is a significant portion of the run time.

    2. It's harder than it might seem to do this right. For example, consider splitting 7s vs. dealer ace starting from a shoe with just that single ace and eight 7s (SPL1, NDAS). Your CDCA indicates an expected value of the split of zero; but we can verify either by hand, or using my blackjack_split.exe or k_c's OptSpl1.exe, that the correct optimal expected value is +2 (hit both pair hands to 21, and let the dealer stand on soft 18), with no danger of running out of cards. More generally, the problem is that there exist subsets of cards-- like this example-- such that including the entire subset in the dealer's hand yields a total *less* than 17, while *every* permutation of those cards contains a prefix yielding a total of 17 or greater.)

    Quote Originally Posted by iCountNTrack View Post
    Yes, I am not sure how it's handled in real world that's technically not supposed to happen. I have never played a deeply dealt game (Don?). But the discrepancy can happen for a depleted deck with more cards, for example for `1 1 1 1 1 1 1 4 1 4` which is a quarter deck with 3 extra eights to hand the resplits. Your new code yields `0.65397233168066482` and globally optimum yields `0.653941321024655` which are really close, but there are very few situations where we run out of cards to complete the round.
    Among the 4,840 test scenarios, the largest shoes that risk running out of cards had 17 cards. But it can in principle be even-- indeed, *much*-- worse than that; it's an interesting thought experiment to consider just how many cards can remain in a depleted shoe and still, with an omniscient-and-malicious player, allow the dealer to run out of cards: I count 96?

  6. #6


    Did you find this post helpful? Yes | No
    Quote Originally Posted by iCountNTrack View Post
    I have never played a deeply dealt game (Don?).
    This should probably never happen in today's modern shoe games, but in the good ol' days, playing the Riviera's spectacular DD game, with S17, DAS, and LS (!), they also used to deal very deeply. I remember several instances of the dealer's running out of cards, and they always simply reshuffled.

    Finally, while I have you guys here, and while I full-well realize that all of your wonderful discussions have been more theoretical in nature than practical, I once again refer yo to BJA3, middle of page 389, starting with "Finally," and spilling over to page 390. I won't rehash what it says, except to comment: Do you really still want to call reckoning ALL the cards in all of your splits and resplits basic strategy, but not the two cards of the player to your right, which you happen to see? Oh no, if I do that, then that's card counting, but if I reckon the 15 cards in my OWN split hands, then that's basic strategy! See?!

    Feel free to defend that indefensible position.

    Happy New Year!

    Don

  7. #7


    Did you find this post helpful? Yes | No
    Quote Originally Posted by DSchles View Post
    This should probably never happen in today's modern shoe games, but in the good ol' days, playing the Riviera's spectacular DD game, with S17, DAS, and LS (!), they also used to deal very deeply. I remember several instances of the dealer's running out of cards, and they always simply reshuffled.

    Finally, while I have you guys here, and while I full-well realize that all of your wonderful discussions have been more theoretical in nature than practical, I once again refer yo to BJA3, middle of page 389, starting with "Finally," and spilling over to page 390. I won't rehash what it says, except to comment: Do you really still want to call reckoning ALL the cards in all of your splits and resplits basic strategy, but not the two cards of the player to your right, which you happen to see? Oh no, if I do that, then that's card counting, but if I reckon the 15 cards in my OWN split hands, then that's basic strategy! See?!

    Feel free to defend that indefensible position.

    Happy New Year!

    Don
    Hi Don! Thanks for chiming in. I was almost certain you had previously encountered a deeply dealt pitch game that ran out of cards!!

    I think we are beyond "basic strategy" at this point, and like Eric pointed out, the purpose of this exercise is to compute the expectation value of a playing decision given the dealer upcard, player's two cards and a given deck composition. Previous CA implementations using "Fixed" strategies for splitting worked well for full decks/shoes and to generate a basic strategy worked well because the difference in EV from an optimal split calculation is too small to impact the playing decision, but for depleted decks the difference between "fixex" sub-optimal split algorithms and optimal algorithms get much larger and will impact the best playing decision for this depleted deck state.

    Happy New Year to you as well!
    Chance favors the prepared mind

  8. #8


    Did you find this post helpful? Yes | No
    Quote Originally Posted by ericfarmer View Post
    Yeah, I can only run 6D up to SPL2 with exponent=29 on my 32 GB laptop, but for SPL3 I'm confined to 1D, and I can only evaluate all pair cards vs. all up cards for NDAS; for DAS I got stuck trying to split 4s, 3s, and 2s, but I suspect exponent=30 would work there.
    yes, I confirm that exponent 30 works for 1D SPL3, unfortunately it doesnt seem that 32 is enough for 4D or more for SPL3.
    Quote Originally Posted by ericfarmer View Post
    Yeah, I considered doing something like this, but rejected the idea for two reasons:

    1. It slows things down. I don't see how to avoid performing such a check for *all* leaf states (i.e., rounds completed by the player, ready for the dealer to resolve his hand), which is a significant portion of the run time.

    2. It's harder than it might seem to do this right. For example, consider splitting 7s vs. dealer ace starting from a shoe with just that single ace and eight 7s (SPL1, NDAS). Your CDCA indicates an expected value of the split of zero; but we can verify either by hand, or using my blackjack_split.exe or k_c's OptSpl1.exe, that the correct optimal expected value is +2 (hit both pair hands to 21, and let the dealer stand on soft 18), with no danger of running out of cards. More generally, the problem is that there exist subsets of cards-- like this example-- such that including the entire subset in the dealer's hand yields a total *less* than 17, while *every* permutation of those cards contains a prefix yielding a total of 17 or greater.)
    This is actually du to a bug in my code where it was incorrectly labelling "soft" deck compositions as not having enough cards to complete the dealer hand. I have created a helper function to do that please see below. Now I get EV +2 for this deck composition

    Code:
    bool canDealerCompleteHand(
        const std::vector<int>& deckCounts,
        const std::vector<short>& dealerHand,
        bool dealerHitsSoft17
    )
    {
    	//check if existing dealer hand is already completed
        auto [dealerTotal, dealerIsSoft, dealerIsBlackjack] =
            calculateHandTotalSoftnessAndBlackjack(dealerHand, false);
    
    
        if (dealerTotal >= 18 || (dealerTotal == 17 && !dealerIsSoft) || (dealerTotal == 17 && dealerIsSoft && !dealerHitsSoft17)) {
            return true;
        }
    
    	//check number of card remaining in the deck
        int totalCardsRemaining = 0;
        for (int c : deckCounts) {
            totalCardsRemaining += c;
        }
        if (totalCardsRemaining == 0) {
            return false;
        }
    
        std::vector<short> tempDealerHand = dealerHand;
        for (int cardRank = 0; cardRank <= 9; ++cardRank) {
            if (deckCounts[cardRank] > 0) {
                tempDealerHand[cardRank]++;
                auto [newTotal, newSoft, ignoreBJ] =
                    calculateHandTotalSoftnessAndBlackjack(tempDealerHand, false);
    
    			// Any total over 18 or hard 17 or a soft 17 with dealerHitsSoft17 == false is a completed hand
                if (newTotal >= 18 || (newTotal == 17 && !newSoft) || (newTotal == 17 && newSoft && !dealerHitsSoft17)) {
                    return true;
                }
            }
        }
    
        return false;
    
    
    //*******this will check for if we have enough card left to play out dealer's hand*********
    //vector<short> totalComp(deckCounts.size()); // add dealerHand composition and deckComposition
    
    //for (size_t i = 0; i < deckCounts.size(); ++i) {
    //    totalComp[i] = deckCounts[i] + dealerOrderedHand[i];
    //}
    //auto [dealerTotal, isSoft, dealerHasBlackjack] = calculateHandTotalSoftnessAndBlackjack(
    //    totalComp,
    //    true
    //);
    
    //if (dealerTotal < 17 || dealerTotal == 17 && isSoft && dealerHitsSoft17)
    //{
    //    // Initialize outcome statistics
    //    OutcomeStatistics result;
    //    result.expectationValue = -numeric_limits<double>::infinity();
    //    result.netOutcomeProbabilities[-numeric_limits<double>::infinity()] = 1;
    //    memoization[key] = result;
    //    return result;
    //}
    // 
    
    if (!canDealerCompleteHand(deckCounts, dealerOrderedHand, dealerHitsSoft17))
    {
    	// Initialize outcome statistics
    	OutcomeStatistics result;
    	result.expectationValue = -numeric_limits<double>::infinity();
    	result.netOutcomeProbabilities[-numeric_limits<double>::infinity()] = 1;
    	memoization[key] = result;
    	return result;
    }
    It would be worth trying something similar, I didnt really see much of a slow down.

    Quote Originally Posted by ericfarmer View Post
    Among the 4,840 test scenarios, the largest shoes that risk running out of cards had 17 cards. But it can in principle be even-- indeed, *much*-- worse than that; it's an interesting thought experiment to consider just how many cards can remain in a depleted shoe and still, with an omniscient-and-malicious player, allow the dealer to run out of cards: I count 96?
    yes, that's the thing there are many more depleted deck compositions where for a SPL3 or SPL2 the dealer would not have enough cards to complete his hand. Low value cards do not add to the total quickly, you can have more than 10 cards in each split hand before you bust. Ideally, we would want to discard the SPL3 scenario that yields by assigning an infinite EV, same to SPL2 even for SPL1 in the extreme case. I think optimal splits are more valuable than "fixed" strategies for depleted deck because this is where large differences in values start to appear.
    Last edited by iCountNTrack; 12-31-2024 at 12:56 PM.
    Chance favors the prepared mind

  9. #9


    Did you find this post helpful? Yes | No
    Quote Originally Posted by DSchles View Post
    Finally, while I have you guys here, and while I full-well realize that all of your wonderful discussions have been more theoretical in nature than practical, I once again refer yo to BJA3, middle of page 389, starting with "Finally," and spilling over to page 390. I won't rehash what it says, except to comment: Do you really still want to call reckoning ALL the cards in all of your splits and resplits basic strategy, but not the two cards of the player to your right, which you happen to see? Oh no, if I do that, then that's card counting, but if I reckon the 15 cards in my OWN split hands, then that's basic strategy! See?!

    Feel free to defend that indefensible position.

    Happy New Year!

    Don
    I searched the text of every page of the previous thread as well as this current thread-- the only person that ever mentions the word "basic" beyond the particular sense you keep quoting from your book in this discussion is you .

    Quote Originally Posted by DSchles View Post
    I full-well realize that all of your wonderful discussions have been more theoretical in nature than practical...
    Nope. Maybe I'm snagging on terminology; we're admittedly talking about implementation and interpretation details here for the moment, as opposed to big-picture analysis application, which is a fair point. We'll get to that once we get these details ironed out. But are you arguing that there is no "practical" interest or application in knowing the feasible performance of truly EV-maximizing strategy? I can think of at least three, the first two of which have been discussed here already.

    1. Recall the SCORE analysis that Gronbog (and to a lesser extent I) worked on several years ago: the whole point of my participation, at least, was to try to provide an upper bound of feasible performance. Since if it turned out that there wasn't much room between, say, HO2 full indices, and that "speed of light" upper bound, then that would be a pretty definitive argument against bothering with a more complex (e.g. multi-parameter) counting system.

    But that analysis didn't actually evaluate the "speed of light" upper bound on performance. It evaluated per-depleted-shoe CDZ- performance, since that's what was-- and still is-- computationally reasonable to execute in my lifetime. Granted, CDZ- re-optimized for every depleted shoe is a pretty good proxy for "perfect EV-maximizing" play. But suppose that HO2 full index SCORE was in fact very close to that CDZ- SCORE? A devil's advocate would be justified in arguing that that didn't really prove that a more complex counting system couldn't do better, since that CDZ- SCORE isn't truly an impenetrable upper bound. That "real" upper bound is what we're talking about trying to evaluate here.

    2. I've tried to argue that we should stop using CDP (as opposed to CDP1) expected returns, because the algorithm is flawed; I still think you are the only person in that past thread that actually understood my argument, which was admittedly somewhat technically detailed. The ability to evaluate truly maximum possible EV as we are doing here allows for a much simpler argument than that initial attempt: CDP can produce values that are simply too large, i.e. greater than the maximum possible EV among all possible (executable) strategies.

    3. Even when you should split, how often should you *not* *re*split, even when you can? For example, look closely at the 10-10 split values in the tables at the top of this post, and compare the "OPT"imal EVs with the CDZ- values. Granted, in that full deck case, we shouldn't split in the first place, but when we should, most of the various CA algorithms implicitly assume that we not only split but resplit at every opportunity. Computing truly EV-maximizing strategy as we are doing here could be really useful, not so much because anyone would actually execute the resulting terribly complex strategy, but just as an analysis tool to help identify when *not* to split, and how much it matters or not, etc.

  10. #10


    Did you find this post helpful? Yes | No
    Quote Originally Posted by iCountNTrack View Post
    This is actually du to a bug in my code where it was incorrectly labelling "soft" deck compositions as not having enough cards to complete the dealer hand. I have created a helper function to do that please see below. Now I get EV +2 for this deck composition

    Code:
    bool canDealerCompleteHand(
        const std::vector<int>& deckCounts,
        const std::vector<short>& dealerHand,
        bool dealerHitsSoft17
    )
    {
    	//check if existing dealer hand is already completed
        auto [dealerTotal, dealerIsSoft, dealerIsBlackjack] =
            calculateHandTotalSoftnessAndBlackjack(dealerHand, false);
    
    
        if (dealerTotal >= 18 || (dealerTotal == 17 && !dealerIsSoft) || (dealerTotal == 17 && dealerIsSoft && !dealerHitsSoft17)) {
            return true;
        }
    
    	//check number of card remaining in the deck
        int totalCardsRemaining = 0;
        for (int c : deckCounts) {
            totalCardsRemaining += c;
        }
        if (totalCardsRemaining == 0) {
            return false;
        }
    
        std::vector<short> tempDealerHand = dealerHand;
        for (int cardRank = 0; cardRank <= 9; ++cardRank) {
            if (deckCounts[cardRank] > 0) {
                tempDealerHand[cardRank]++;
                auto [newTotal, newSoft, ignoreBJ] =
                    calculateHandTotalSoftnessAndBlackjack(tempDealerHand, false);
    
    			// Any total over 18 or hard 17 or a soft 17 with dealerHitsSoft17 == false is a completed hand
                if (newTotal >= 18 || (newTotal == 17 && !newSoft) || (newTotal == 17 && newSoft && !dealerHitsSoft17)) {
                    return true;
                }
            }
        }
    
        return false;
    
    
    //*******this will check for if we have enough card left to play out dealer's hand*********
    //vector<short> totalComp(deckCounts.size()); // add dealerHand composition and deckComposition
    
    //for (size_t i = 0; i < deckCounts.size(); ++i) {
    //    totalComp[i] = deckCounts[i] + dealerOrderedHand[i];
    //}
    //auto [dealerTotal, isSoft, dealerHasBlackjack] = calculateHandTotalSoftnessAndBlackjack(
    //    totalComp,
    //    true
    //);
    
    //if (dealerTotal < 17 || dealerTotal == 17 && isSoft && dealerHitsSoft17)
    //{
    //    // Initialize outcome statistics
    //    OutcomeStatistics result;
    //    result.expectationValue = -numeric_limits<double>::infinity();
    //    result.netOutcomeProbabilities[-numeric_limits<double>::infinity()] = 1;
    //    memoization[key] = result;
    //    return result;
    //}
    // 
    
    if (!canDealerCompleteHand(deckCounts, dealerOrderedHand, dealerHitsSoft17))
    {
    	// Initialize outcome statistics
    	OutcomeStatistics result;
    	result.expectationValue = -numeric_limits<double>::infinity();
    	result.netOutcomeProbabilities[-numeric_limits<double>::infinity()] = 1;
    	memoization[key] = result;
    	return result;
    }
    In your loop (for (int cardRank = 0; ...)), it looks like you're only adding one card of each remaining rank to the dealer's hand, although there might be multiple aces, or multiple twos, etc.? This will work in the specific 7-7 vs. ace scenario discussed here, since there is only one 7 and one ace left in the shoe, but will in general incorrectly flag a lot of shoes as !canDealerCompleteHand(.), right?

    But even if you fix that by adding *all* of the cards remaining in the shoe-- checking for a standing total as each card is drawn, which is the key fix here, instead of just checking the hand total at the end-- this still doesn't address the general problem. You're adding cards to the dealer's hand and checking the hand total one card at a time, but you're still adding the cards in just one specific order (namely, from ace low to ten high). But even if you encounter an intermediate standing hand total 17 or greater-- and so return true-- it's still possible that the dealer might draw those cards in a different order that *never* exceeds 17, and thus run out of cards (so we should have returned false).

    For example, suppose the dealer's up card is a 4, and there are three aces and a six remaining in the shoe (S17). Can the dealer definitely complete his hand? No... but there is just one drawing order that causes a problem (ace-ace-six-ace).

  11. #11


    Did you find this post helpful? Yes | No
    Quote Originally Posted by ericfarmer View Post
    In your loop (for (int cardRank = 0; ...)), it looks like you're only adding one card of each remaining rank to the dealer's hand, although there might be multiple aces, or multiple twos, etc.? This will work in the specific 7-7 vs. ace scenario discussed here, since there is only one 7 and one ace left in the shoe, but will in general incorrectly flag a lot of shoes as !canDealerCompleteHand(.), right?

    But even if you fix that by adding *all* of the cards remaining in the shoe-- checking for a standing total as each card is drawn, which is the key fix here, instead of just checking the hand total at the end-- this still doesn't address the general problem. You're adding cards to the dealer's hand and checking the hand total one card at a time, but you're still adding the cards in just one specific order (namely, from ace low to ten high). But even if you encounter an intermediate standing hand total 17 or greater-- and so return true-- it's still possible that the dealer might draw those cards in a different order that *never* exceeds 17, and thus run out of cards (so we should have returned false).

    For example, suppose the dealer's up card is a 4, and there are three aces and a six remaining in the shoe (S17). Can the dealer definitely complete his hand? No... but there is just one drawing order that causes a problem (ace-ace-six-ace).
    Thanks for catching this, you are right the current code is not right because it's only drawing one rank once. The original implementation worked better , I was trying to avoid recursion. That being said, please note that I am only looking for at least one one sequence that allows the dealer to complete his other uncompletable sequences will be handled by my the dealer function please see below. Of course, the answer was there screaming at my face, all I have to do is check if ` map<DealerOutcome, double>& dealerTotalsProbabilities` is empty after I call the function!!!. Duuuuhhhhhhh

    Code:
    void simulateDealerPlayWithProbabilities(
        bool dealerHitsSoft17,
        vector<int>& deckCounts,
        vector<short>& handComposition,
        double probability,
        map<DealerOutcome, double>& dealerTotalsProbabilities,
        int totalCardsRemaining,
        int unknownCardsInDealerHand,
        bool checkForBlackjack
    )
    {
        if (unknownCardsInDealerHand > 0) {
            // Dealer has unknown cards to be considered
            for (int card = 0; card <= 9; ++card) {
                if (deckCounts[card] > 0) {
                    double cardProbability = probability * deckCounts[card] / totalCardsRemaining;
    
                    // Update the hand and deck counts
                    handComposition[card]++;
                    deckCounts[card]--;
                    totalCardsRemaining--;
    
                    simulateDealerPlayWithProbabilities(
                        dealerHitsSoft17,
                        deckCounts,
                        handComposition,
                        cardProbability,
                        dealerTotalsProbabilities,
                        totalCardsRemaining,
                        unknownCardsInDealerHand - 1,
                        checkForBlackjack
                    );
    
                    // Backtrack
                    handComposition[card]--;
                    deckCounts[card]++;
                    totalCardsRemaining++;
                }
            }
        }
        else {
            // Calculate dealer's total and blackjack status
            auto [total, isSoft, isBlackjack] = calculateHandTotalSoftnessAndBlackjack(handComposition, false);
    
            if (checkForBlackjack) {
                if (isBlackjack) {
                    DealerOutcome outcome = { total, true };
                    dealerTotalsProbabilities[outcome] += probability;
                    return; // Dealer has blackjack, stop further simulation
                }
                else {
                    // Dealer does not have blackjack, continue simulation
                    checkForBlackjack = false; // Only check for blackjack once
                }
            }
    
            // Check if dealer should stand
            if ((total == 17 && !isSoft) ||
                (total == 17 && isSoft && !dealerHitsSoft17) ||
                total >= 18 || total > 21) {
                // Dealer stands or busts
                DealerOutcome outcome = { total, isBlackjack };
                dealerTotalsProbabilities[outcome] += probability;
                return;
            }
    
            // Dealer should hit
            for (int card = 0; card <= 9; ++card) {
                if (deckCounts[card] > 0) {
                    double cardProbability = probability * deckCounts[card] / totalCardsRemaining;
    
                    // Update the hand and deck counts
                    handComposition[card]++;
                    deckCounts[card]--;
                    totalCardsRemaining--;
    
                    simulateDealerPlayWithProbabilities(
                        dealerHitsSoft17,
                        deckCounts,
                        handComposition,
                        cardProbability,
                        dealerTotalsProbabilities,
                        totalCardsRemaining,
                        unknownCardsInDealerHand,
                        checkForBlackjack
                    );
    
                    // Backtrack
                    handComposition[card]--;
                    deckCounts[card]++;
                    totalCardsRemaining++;
                }
            }
        }
    }
    Last edited by iCountNTrack; 12-31-2024 at 02:11 PM.
    Chance favors the prepared mind

  12. #12


    Did you find this post helpful? Yes | No
    Hi Eric,

    Sounds like you have found a workable solution for optimal resplits. It's probably a bit over my head. If I ever decide to try resplits it is nice to have values to consult.

    As far as I've gone is to compute only a single split optimally while only thinking about resplits. My approach for a single split was to list all possible hand1 compositions, including busted hands. For each comp in the hand1 list optimal hand2 EV is computed. I call this h2EVx. Computation of h2EVx doesn't require consideration of busted hands. Hand1 strategy is computed according to the shoe state when strategy decision is made. If hand1 strategy requires a double or hit the pre-computed h2EVx for the resultant h2EVx value is available.

    For a single split there is just one basic drawing sequence; pair or non-pair can be drawn at any time.

    Resplits are more of a problem as there are 3 or more drawing sequences, depending upon number of remaining splits.

    Hi iCountNTrack,

    It seems your approach to resplits has a reasonable chance of success. What worries me is you are predicating your sequence probabilities on "full shoe" comp. In the case of T-T v 7 dealt from shoe comp {0,0,0,0,0,0,14,0,0,14}, full shoe comp is {0,0,0,0,0,0,14,0,0,14}.

    This is a potential source of error. What you are doing will work for SPL1 because there is only one drawing sequence. For resplits this methodology is in jeopardy of misidentifying a drawing sequence as valid when it isn't even though it might seem to otherwise conform to something that is actually possible.

    Not all ordering of undealt cards may be relatable to splitting TT v 7 which is what is being assumed when predicating drawing sequence probabilities on full shoe comp {0,0,0,0,0,0,14,0,0,14}.

    Instead drawing sequence probabilities should be predicated on the condition that 2 pair cards (T) and dealer up card (7) have been removed (shoe comp {0,0,0,0,0,0,13,0,0,12}.) This ensures sequence validity. As an aside removal of up card can be timed in such a way that all up cards can be computed at once, potentially saving computing time.

    At best you are only in jeopardy of misidentifying a valid drawing sequence. At worst you may be doing just that.

    Maybe Eric can give his opinion.

    k_c
    "Perfection is the enemy of success."
    -Elon Musk-

  13. #13


    Did you find this post helpful? Yes | No
    Quote Originally Posted by iCountNTrack View Post
    Thanks for catching this, you are right the current code is not right because it's only drawing one rank once. The original implementation worked better , I was trying to avoid recursion. That being said, please note that I am only looking for at least one one sequence that allows the dealer to complete his other uncompletable sequences will be handled by my the dealer function please see below. Of course, the answer was there screaming at my face, all I have to do is check if ` map<DealerOutcome, double>& dealerTotalsProbabilities` is empty after I call the function!!!. Duuuuhhhhhhh
    Ah, I see, thanks; if you don't happen to catch an uncompletable sequence at a given point in the resolution of the dealer's hand, you recursively deal some more cards, but check again at each point, right?

    Thanks for the continued discussion on this, it has motivated me to look harder at this problem. It's tantalizingly difficult to recognize exactly when a small depleted shoe *might* run out of cards: just dealing in decreasing order of rank (so that if we stand at some point in that particular order, then we are guaranteed to stand when drawing those same cards in *any* order) seems to cover most of the problem shoes, but there are still some outliers with up cards 2 through 4, and aces are a mess as expected.

    But I was able to make a reasonably "surgical" update to the code to address this, that doesn't have a measurable effect on run time for most scenarios with larger shoes, and now reproduces the original behavior of avoiding any possibility of either the player or the dealer running out of cards, with expected values matching the original Python code linked in the previous thread.

Page 1 of 4 123 ... LastLast

Similar Threads

  1. Replies: 0
    Last Post: 03-29-2015, 09:44 AM
  2. "You'll never be going fast enough" - al musous
    By Blackriver in forum General Blackjack Forum
    Replies: 17
    Last Post: 03-23-2013, 03:48 PM
  3. Replies: 4
    Last Post: 08-30-2012, 07:08 PM

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  

About Blackjack: The Forum

BJTF is an advantage player site based on the principles of comity. That is, civil and considerate behavior for the mutual benefit of all involved. The goal of advantage play is the legal extraction of funds from gaming establishments by gaining a mathematic advantage and developing the skills required to use that advantage. To maximize our success, it is important to understand that we are all on the same side. Personal conflicts simply get in the way of our goals.