UNPKG

blackjack-strategy

Version:

provides a suggested action for a player based on Basic Strategy

711 lines (658 loc) 26.8 kB
/* * MIT License * Copyright (c) 2016 Garrett Vargas * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const ec = require('../src/ExactComposition'); const easy = require('../src/EasyStrategy'); const BlackjackCalculation = require('./BlackjackCalculation'); module.exports = { // Recommended actions follow Basic Strategy, based on the rules currently in play GetRecommendedPlayerAction: function(playerCards, dealerCard, handCount, dealerCheckedBlackjack, options) { const playerOptions = ExtractOptions(options); var exactCompositionOverride = null; var countResult = null; var handValue = HandTotal(playerCards); // If early surrender is allowed, check that now (that's what early surrender means - before dealer checks for blackjack if ((playerOptions.surrender == "early") && (ShouldPlayerSurrender(playerCards, dealerCard, handValue, handCount, playerOptions))) { return "surrender"; } // OK, let's see if the count will change anything if (playerOptions.count.system == "HiLo") { countResult = AdjustPlayForHiLoCount(playerCards, dealerCard, handValue, handCount, dealerCheckedBlackjack, playerOptions); if (countResult) { return countResult; } } // OK, if an ace is showing it's easy - never take insurance if (((dealerCard == 1) && !dealerCheckedBlackjack && playerOptions.offerInsurance)) { return "noinsurance"; } if ((playerOptions.strategyComplexity == "easy")) { return easy.EasyBasicStrategy(playerCards, dealerCard, handValue, handCount, dealerCheckedBlackjack, playerOptions); } // blackjackcalculation.com basic strategies if ((playerOptions.strategyComplexity == "bjc-supereasy")) { return BlackjackCalculation.SuperEasyStrategy(playerCards, dealerCard, handValue, handCount, dealerCheckedBlackjack, playerOptions); } if ((playerOptions.strategyComplexity == "bjc-simple")) { return BlackjackCalculation.SimpleStrategy(playerCards, dealerCard, handValue, handCount, dealerCheckedBlackjack, playerOptions); } if ((playerOptions.strategyComplexity == "bjc-great")) { return BlackjackCalculation.GreatStrategy(playerCards, dealerCard, handValue, handCount, dealerCheckedBlackjack, playerOptions); } // OK, first, if there is an exact composition override use that exactCompositionOverride = ec.GetExactCompositionOverride(playerCards, dealerCard, handValue, handCount, dealerCheckedBlackjack, playerOptions); if (exactCompositionOverride) { return exactCompositionOverride; } // Check each situation if (ShouldPlayerSplit(playerCards, dealerCard, handValue, handCount, playerOptions)) { return "split"; } else if (ShouldPlayerDouble(playerCards, dealerCard, handValue, handCount, playerOptions)) { return "double"; } // Note if early surrender is allowed we already checked, so no need to check again else if ((playerOptions.surrender != "early") && ShouldPlayerSurrender(playerCards, dealerCard, handValue, handCount, playerOptions)) { return "surrender"; } else if (ShouldPlayerStand(playerCards, dealerCard, handValue, handCount, playerOptions)) { return "stand"; } else if (ShouldPlayerHit(playerCards, dealerCard, handValue, handCount, playerOptions)) { return "hit"; } // I got nothing return "none"; } }; /* * Internal functions */ function HandTotal(cards) { var retval = { total: 0, soft: false }; var hasAces = false; for (var i = 0; i < cards.length; i++) { retval.total += cards[i]; // Note if there's an ace if (cards[i] == 1) { hasAces = true; } } // If there are aces, add 10 to the total (unless it would go over 21) // Note that in this case the hand is soft if ((retval.total <= 11) && hasAces) { retval.total += 10; retval.soft = true; } return retval; } function ExtractOptions(options) { const playerOptions = { hitSoft17: true, surrender: "late", doubleRange:[0,21], doubleAfterSplit: true, resplitAces: false, offerInsurance: true, numberOfDecks: 6, maxSplitHands: 4, count: {system: null, trueCount: null}, strategyComplexity: "simple"}; // Override defaults where set if (options) { if (options.hasOwnProperty("hitSoft17")) { playerOptions.hitSoft17 = options.hitSoft17; } if (options.hasOwnProperty("surrender")) { playerOptions.surrender = options.surrender; } if (options.hasOwnProperty("doubleAfterSplit")) { playerOptions.doubleAfterSplit = options.doubleAfterSplit; } if (options.hasOwnProperty("resplitAces")) { playerOptions.resplitAces = options.resplitAces; } if (options.hasOwnProperty("offerInsurance")) { playerOptions.offerInsurance = options.offerInsurance; } if (options.hasOwnProperty("numberOfDecks")) { playerOptions.numberOfDecks = options.numberOfDecks; } if (options.hasOwnProperty("maxSplitHands")) { playerOptions.maxSplitHands = options.maxSplitHands; } if (options.hasOwnProperty("count")) { playerOptions.count = options.count; } if (options.hasOwnProperty("strategyComplexity")) { // Note that prior to version 1.1.2, "basic" was used instead of "simple" playerOptions.strategyComplexity = options.strategyComplexity; if (playerOptions.strategyComplexity == "basic") { playerOptions.strategyComplexity = "simple"; } } // Double rules - make sure doubleRange is set as that is all we use here if (options.hasOwnProperty("doubleRange")) { playerOptions.doubleRange = options.doubleRange; } else if (options.hasOwnProperty("double")) { // Translate to doubleRange if (options.double == "none") { playerOptions.doubleRange[0] = 0; playerOptions.doubleRange[1] = 0; } else if (options.double === "10or11") { playerOptions.doubleRange[0] = 10; playerOptions.doubleRange[1] = 11; } else if (options.double === "9or10or11") { playerOptions.doubleRange[0] = 9; playerOptions.doubleRange[1] = 11; } } } return playerOptions; } // // Split strategy // function ShouldPlayerSplit(playerCards, dealerCard, handValue, handCount, options) { var shouldSplit = false; // It needs to be a possible action if ((playerCards.length != 2) || (handCount == options.maxSplitHands) || (playerCards[0] != playerCards[1])) { return false; } // OK, it's a possibility switch (playerCards[0]) { case 1: // Always split aces shouldSplit = true; break; case 2: // Against 4-7, or 2 and 3 if you can double after split; also against a dealer 3 in single deck shouldSplit = ((dealerCard > 3) && (dealerCard < 8)) || (((dealerCard == 2) || (dealerCard == 3)) && (options.doubleAfterSplit)); if ((dealerCard == 3) && (options.numberOfDecks == 1)) { shouldSplit = true; } break; case 3: // Against 4-7, or 2 and 3 if you can double after split // Also in single deck against an 8 if you can double after split shouldSplit = ((dealerCard > 3) && (dealerCard < 8)) || (((dealerCard == 2) || (dealerCard == 3)) && (options.doubleAfterSplit)); if ((dealerCard == 8) && (options.numberOfDecks == 1) && (options.doubleAfterSplit)) { shouldSplit = true; } break; case 4: // Against 5 or 6, and only if you can double after split // Or against a 4 in single deck, if you can double after split shouldSplit = ((dealerCard == 5) || (dealerCard == 6) || ((dealerCard == 4) && (options.numberOfDecks == 1))) && (options.doubleAfterSplit); break; case 6: // Split 3-6, or against a 2 if double after split is allowed (for four or more decks, always for one or two decks) // Or in single or double deck, against a 7 if double after split is allowed shouldSplit = ((dealerCard > 2) && (dealerCard < 7)) || ((dealerCard == 2) && (((options.numberOfDecks >= 4) && options.doubleAfterSplit) || (options.numberOfDecks <= 2))); if ((options.numberOfDecks <= 2) && (dealerCard == 7) && options.doubleAfterSplit) { shouldSplit = true; } break; case 7: // Split on 2-7, and on single or double deck split against an 8 only if you can double after split shouldSplit = ((dealerCard > 1) && (dealerCard < 8)); if ((dealerCard == 8) && (options.numberOfDecks <= 2)) { shouldSplit = options.doubleAfterSplit; } break; case 8: // The simple rule is always split 8s - there are exceptions where we will surrender instead, which are covered in // the case where the player should surrender. Check if they should surrender, if not they should split shouldSplit = !ShouldPlayerSurrender(playerCards, dealerCard, handValue, handCount, options); break; case 9: // Split against 2-9 except 7 // An advanced exception - 9s should split against an ace in single deck if dealer has an ace and you can double after split // and the dealer hits soft 17 (that's a mouthful!) shouldSplit = ((dealerCard > 1) && (dealerCard < 10) && (dealerCard != 7)); if ((options.strategyComplexity != "simple") && (dealerCard == 1) && (options.numberOfDecks == 1) && (options.doubleAfterSplit) && options.hitSoft17) { shouldSplit = true; } break; case 5: case 10: default: // Don't split 5s or 10s ... or cards I don't know break; } return shouldSplit; } // // Double strategy // function ShouldPlayerDouble(playerCards, dealerCard, handValue, handCount, options) { var shouldDouble = false; // It needs to be a possible action if ((playerCards.length != 2) || ((handCount > 1) && !options.doubleAfterSplit)) { return false; } if ((handValue.total < options.doubleRange[0]) || (handValue.total > options.doubleRange[1])) { return false; } // OK, looks like you can double if (handValue.soft) { // Let's look at the non-ace card to determine what to do (get this by the total) switch (handValue.total) { case 13: case 14: // Double against dealer 5 or 6; on single deck you should also double against 4 // On double deck a soft 14 should double against 4 if the dealer hits soft 17 shouldDouble = (dealerCard == 5) || (dealerCard == 6); if ((dealerCard == 4) && ((options.numberOfDecks == 1) || ((options.numberOfDecks == 2) && (handValue.total == 14) && options.hitSoft17))) { shouldDouble = true; } break; case 15: case 16: // Double against dealer 4-6 shouldDouble = (dealerCard >= 4) && (dealerCard <= 6); break; case 17: // Double against 3-6, and against 2 in single deck shouldDouble = ((dealerCard >= 3) && (dealerCard <= 6)) || ((dealerCard == 2) && (options.numberOfDecks == 1)); break; case 18: // Double against 3-6 - also 2 if the dealer hits soft 17 and there is more than one deck shouldDouble = ((dealerCard >= 3) && (dealerCard <= 6)) || ((dealerCard == 2) && options.hitSoft17 && (options.numberOfDecks > 1)); break; case 19: // Double against 6 if the dealer hits soft 17, or always in single deck shouldDouble = (dealerCard == 6) && (options.hitSoft17 || (options.numberOfDecks == 1)); break; default: // Don't double break; } } else { // Double on 9, 10, or 11 only (8 in single deck) switch (handValue.total) { case 8: // Double 5-6 in single deck shouldDouble = ((dealerCard >= 5) && (dealerCard <= 6) && (options.numberOfDecks == 1)); break; case 9: // Double 3-6, and 2 in single or double deck shouldDouble = ((dealerCard >= 3) && (dealerCard <= 6)) || ((dealerCard == 2) && (options.numberOfDecks <= 2)); break; case 10: // Double 2-9 shouldDouble = (dealerCard >= 2) && (dealerCard <= 9); break; case 11: // Double anything except an ace (and then only if the dealer doesn't hit soft 17 or there are more than two decks) shouldDouble = !((dealerCard == 1) && !options.hitSoft17 && (options.numberOfDecks > 2)); break; default: break; } } return shouldDouble; } // // Surrender strategy // // This is fairly complex and handles different strategies for early or late surrender // It also looks at various combinations of player hands for more advanced strategy complexities // function ShouldPlayerSurrender(playerCards, dealerCard, handValue, handCount, options) { var shouldSurrender = false; // You can only surrender on your first two cards, and it has to be an option if (((options.surrender != "early") && (options.surrender != "late")) || (playerCards.length != 2) || (handCount != 1)) { return false; } // See if there is a special suggestion based on the exact composition of the player's hand var exactCompositionSurrender = ec.GetSurrenderOverride(playerCards, dealerCard, handValue, handCount, options); if (exactCompositionSurrender != null) { return exactCompositionSurrender; } // Don't surrender a soft hand if (handValue.soft) { return false; } if (options.surrender == "early") { if (dealerCard == 1) { // Surrender Dealer Ace vs. hard 5-7, hard 12-17, including pair of 3's, 6's, 7's or 8's // Also surender against pair of 2's if the dealer hits soft 17 if (((handValue.total >= 5) && (handValue.total <= 7)) || ((handValue.total >= 12) && (handValue.total <= 17))) { shouldSurrender = true; } if ((playerCards[0] == 2) && (playerCards[1] == 2) && options.hitSoft17) { shouldSurrender = true; } } else if (dealerCard == 10) { // Surrender dealer 10 against a hard 14-16, including pair of 7's or 8's if ((handValue.total >= 14) && (handValue.total <= 16)) { // UNLESS it's a pair of 8's in single deck and double after split is allowed // This is an advanced option (for simple, we will "always split 8s") if ((playerCards[0] == 8) && (playerCards[1] == 8)) { shouldSurrender = (options.strategyComplexity != "simple") && (options.numberOfDecks == 1) && (options.doubleAfterSplit); } else { shouldSurrender = true; } } } else if (dealerCard == 9) { // Surrender if we have 16, but no including a pair of 8's if ((handValue.total == 16) && (playerCards[0] != 8)) { shouldSurrender = true; } } } // Late surrender against an Ace when dealer hits on soft 17 else if ((options.hitSoft17) && (dealerCard == 1)) { switch (handValue.total) { case 14: // If looking at advanced, then surrender a pair of 7s in a single deck game only shouldSurrender = ((options.strategyComplexity != "simple") && (options.numberOfDecks == 1) && (playerCards[0] == 7)); break; case 15: // Surrender shouldSurrender = true; break; case 16: // Surrender unless it's a pair of 8s in a single deck game or a double deck game with no double after split // This is an advanced option (simple is "always split 8s") shouldSurrender = (playerCards[0] != 8); if ((options.strategyComplexity != "simple") && (playerCards[0] == 8)) { // Surrender pair of 8s if four or more decks or in a double-deck game where double after split isn't allowed if ((options.numberOfDecks >= 4) || ((options.numberOfDecks == 2) && !options.doubleAfterSplit)) { shouldSurrender = true; } } break; case 17: // Surrender (that's new by me) shouldSurrender = true; break; default: // Don't surender break; } } // Late surrender against an Ace, dealer doesn't hit soft 17 else if (dealerCard == 1) { // We only surrender a 16 in this case (not a pair of 8s). shouldSurrender = (handValue.total == 16) && (playerCards[0] != 8); } // Late surrender against a non-Ace else { // The simple rule is 15 against 10 if more than one deck, and 16 (non-8s) against 10 always or against 9 if 4 or more decks // If looking at advanced, 7s surrender against 10 in single deck if (handValue.total == 14) { shouldSurrender = ((options.strategyComplexity != "simple") && (playerCards[0] == 7) && (dealerCard == 10) && (options.numberOfDecks == 1)); } else if (handValue.total == 15) { // Surrender against 10 unless it's a single deck game shouldSurrender = ((dealerCard == 10) && (options.numberOfDecks > 1)); } else if (handValue.total == 16) { // Surrender against 10 or Ace, and against 9 if there are more than 4 decks shouldSurrender = (playerCards[0] != 8) && ((dealerCard == 10) || ((dealerCard == 9) && (options.numberOfDecks >= 4))); } } return shouldSurrender; } // // Stand Strategy // // Note that we've already checkd other actions such as double, split, and surrender // So at this point we're only assessing whether you should stand as opposed to hitting // function ShouldPlayerStand(playerCards, dealerCard, handValue, handCount, options) { var shouldStand = false; if (handValue.soft) { // Don't stand until you hit 18 if (handValue.total > 18) { shouldStand = true; } else if (handValue.total == 18) { // Stand against dealer 2-8, and against a dealer Ace in single deck if they dealer will stand on soft 17 shouldStand = ((dealerCard >= 2) && (dealerCard <= 8)) || ((dealerCard == 1) && (options.numberOfDecks == 1) && !options.hitSoft17); } } else { // Stand on 17 or above if (handValue.total > 16) { shouldStand = true; } else if (handValue.total > 12) { // 13-16 you should stand against dealer 2-6 shouldStand = (dealerCard >= 2) && (dealerCard <= 6); } else if (handValue.total == 12) { // Stand on dealer 4-6 shouldStand = (dealerCard >= 4) && (dealerCard <= 6); } // Advanced option - in single deck a pair of 7s should stand against a dealer 10 if ((options.strategyComplexity != "simple") && (handValue.total == 14) && (playerCards[0] == 7) && (dealerCard == 10) && (options.numberOfDecks == 1)) { shouldStand = true; } } return shouldStand; } // // Hit strategy // // Note this is the last action we check (we told them not to do anything else), so by default you should hit // Since we don't have the full game state, it's assumed that the caller made sure not to call if the player // took an action where the player has no choice of play (e.g. doubled or split aces) // function ShouldPlayerHit(playerCards, dealerCard, handValue, handCount, options) { // The only sanity check we'll do is that you haven't already busted return (handValue.total < 21); } // // Hi-Lo strategy // // Adjussts the recommendation based on the Hi-Lo count as defined at // http://wizardofodds.com/games/blackjack/card-counting/high-low/ // We re // function AdjustPlayForHiLoCount(playerCards, dealerCard, handValue, handCount, dealerCheckedBlackjack, options) { var canSplit = (playerCards[0] == playerCards[1]) && (playerCards.length == 2) && (handCount < options.maxSplitHands); var canDouble = ((playerCards.length == 2) && ((handCount == 1) || options.doubleAfterSplit)) && ((handValue.total < options.doubleRange[0]) || (handValue.total > options.doubleRange[1])); var canSurrender = ((options.surrender == "early") || (options.surrender == "late")) && (playerCards.length == 2) && (handCount == 1); var surrenderMatrix = [ {player: 14, dealer: 10, count: 3}, {player: 15, dealer: 10, count: 0}, {player: 15, dealer: 9, count: 2}, {player: 15, dealer: 1, count: 1} ]; // We may take insurance! if ((dealerCard == 1) && !dealerCheckedBlackjack && options.offerInsurance && (options.count.trueCount >= 3)) { return "insurance"; } else if ((handValue.total == 16) && (dealerCard == 10) && (options.count.trueCount > 0)) { return "stand"; } else if ((handValue.total == 15) && (dealerCard == 10) && (options.count.trueCount > 4)) { return "stand"; } else if (canSplit && (handValue.total == 20)) { if ((dealerCard == 5) && (options.count.trueCount >= 5)) { return "split"; } else if ((dealerCard == 6) && (options.count.trueCount >= 4)) { return "split"; } } else if (canDouble && (handValue.total == 10) && (dealerCard == 10) && (options.count.trueCount >= 4)) { return "double"; } else if ((handValue.total == 12) && (dealerCard == 3) && (options.count.trueCount >= 2)) { return "stand"; } else if ((handValue.total == 12) && (dealerCard == 2) && (options.count.trueCount >= 3)) { return "stand"; } else if (canDouble && (handValue.total == 11) && (dealerCard == 1) && (options.count.trueCount >= 1)) { return "double"; } else if (canDouble && (handValue.total == 9) && (dealerCard == 2) && (options.count.trueCount >= 1)) { return "double"; } else if (canDouble && (handValue.total == 10) && (dealerCard == 1) && (options.count.trueCount >= 4)) { return "double"; } else if (canDouble && (handValue.total == 9) && (dealerCard == 7) && (options.count.trueCount >= 3)) { return "double"; } else if ((handValue.total == 16) && (dealerCard == 9) && (options.count.trueCount >= 5)) { return "stand"; } else if ((handValue.total == 13) && (dealerCard == 2) && (options.count.trueCount < -1)) { return "hit"; } else if ((handValue.total == 12) && (dealerCard == 4) && (options.count.trueCount < 0)) { return "hit"; } else if ((handValue.total == 12) && (dealerCard == 5) && (options.count.trueCount < -2)) { return "hit"; } else if ((handValue.total == 12) && (dealerCard == 6) && (options.count.trueCount < -1)) { return "hit"; } else if ((handValue.total == 13) && (dealerCard == 3) && (options.count.trueCount < -2)) { return "hit"; } else if (canSurrender) { for (var i = 0; i < surrenderMatrix.length; i++) { if ((handValue.total == surrenderMatrix[i].player) && (dealerCard == surrenderMatrix[i].dealer) && (options.count.trueCount >= surrenderMatrix[i].count)) { return "surrender"; } } } // Nope, no adjustment return null; }