(** bj.m -- blackjack game probabilities. * @author Eric Laroche * @version @(#)$Id: bj.m,v 1.1 1998/02/25 14:24:21 laroche Exp laroche $ **) (* bj.m -- blackjack game probabilities. * Copyright (C) 1994,1996,1997,1998 Eric Laroche. * * This program is free software; * you can redistribute it and/or modify it. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *) (** The calculated probabilities are based on infinite large decks. * Surrender and side bets are not considered. * Rules used are s17/doa/das. **) (* package bj` *) BeginPackage["bj`"] (* card part *) (** distinct cards of a deck (colors not considered). *) Cards::usage = "Cards: cards of a blackjack deck. Card colors are not considered." Cards := {ace, king, queen, jack, ten, nine, eight, seven, six, five, four, three, two} (** Card predicate. *) CardQ::usage = "CardQ[card]: returns True if 'card' is a card." CardQ[card_] := MemberQ[Cards, card] (** blackjack values of the cards. *) CardValue::usage = "CardValue[card]: list of blackjack values of a card. E.g. CardValue[ace] = {11, 1}, CardValue[king] = {10}." CardValue[ace] := {11, 1} CardValue[king] := {10} CardValue[queen] := {10} CardValue[jack] := {10} CardValue[ten] := {10} CardValue[nine] := {9} CardValue[eight] := {8} CardValue[seven] := {7} CardValue[six] := {6} CardValue[five] := {5} CardValue[four] := {4} CardValue[three] := {3} CardValue[two] := {2} (** CardValue predicate. *) CardValueQ::usage = "CardValueQ[value]: returns True if 'value' is a card value." CardValueQ[value_] := ListQ[value] && Select[value, IntegerQ] == value && Length[value] > 0 (** cards with unique values. *) UniqueValueCards::usage = "UniqueValueCards: cards with unique values." UniqueValueCards[] := Select[Cards, Function[{card}, card === (* find first with that value *) First[Select[Cards, (CardValue[#] == CardValue[card])&]]]] (** minimal single card value. *) MinCardValue::usage = "MinCardValue: minimal single card value." MinCardValue := Min @@ (Max[CardValue[#]]& /@ Cards) (** maximal single card value. *) MaxCardValue::usage = "MaxCardValue: maximal single card value." MaxCardValue := Max @@ (Max[CardValue[#]]& /@ Cards) (** list of possible sums. *) CardSum::usage = "CardSum[x,y]: list of all possible sums of 'x', 'y'. The list will be sorted and duplicates will be removed. 'x' and 'y' must be lists of integers." CardSum[x_?CardValueQ, y_?CardValueQ] := Union @@ Outer[Plus, x, y] (** draw probability of a card. *) CardProbability::usage = "CardProbability[card]: probability that 'card' is drawn. 1/13." CardProbability[card_?CardQ] := 1 / Length[Cards] (* rules part *) (** blackjack value. *) BlackjackValue::usage = "BlackjackValue: 21." BlackjackValue := 21 (** blackjack favor. *) BlackjackFavor::usage = "BlackjackFavor: 3/2 (pays 2:1)." BlackjackFavor := 3/2 (** favor if (late) surrendering. *) SurrenderFavor::usage = "SurrenderFavor: -1/2 (loses half of the bet)." SurrenderFavor := -1/2 (** busted rule. *) BustedQ::usage = "BustedQ[value]: returns True if 'value' is busted. 'value' must be a list of integers." BustedQ[value_?CardValueQ] := (* busted is: all hand values are above 21 *) Min[value] > BlackjackValue (** comparision. *) BjCompare::usage = "BjCompare[value,to]: returns 1 if 'value' is greater than 'to', -1 if it's less and 0 if they're equal. BjCompare considers values that are busted. Note that 0 is returned if 'value' and 'to' are busted." BjCompare[value_?CardValueQ, to_?CardValueQ] := (* busted is handled as -1, 0 is returned if both are busted. *) Sign[ If[BustedQ[value], -1, Max @@ value] - If[BustedQ[to], -1, Max @@ to]] (** sums with busted values removed. *) BjCardSum::usage = "BjCardSum[x,y]: list of all possible sums of 'x', 'y'. The list will be sorted and duplicates will be removed. Busted values will be removed if there are non-busted values present. 'x' and 'y' must be lists of integers." BjCardSum[x_?CardValueQ, y_?CardValueQ] := Module[{csum, bjsum}, csum = CardSum[x, y]; (* strip busted values (leave one value) *) bjsum = Select[csum, (!BustedQ[List[#]])&]; (* leave one value *) If[Length[bjsum] != 0, bjsum, (* minimal busted value *) List[Min[csum]]]] (** blackjack probability. *) BjCardProbability::usage = "BjCardProbability[card]: returns the probability of reaching blackjack from 'card' with one card." BjCardProbability[card_?CardQ] := (* cache it to increase performance *) BjCardProbability[card] = Plus @@ (Function[{cardtwo}, If[Max[BjCardSum[ CardValue[card], CardValue[cardtwo]]] == BlackjackValue, CardProbability[cardtwo], 0]] /@ Cards) (* dealer part *) (** stand value. *) DealerStandValue::usage = "DealerStandValue: value at which dealer must stand." DealerStandValue := 17 (** indicate whether to hit or stand. *) DealerHitStandStrategy::usage = "DealerHitStandStrategy[from]: returns either 'hit or 'stand, depending on the dealer rules." DealerHitStandStrategy[from_?CardValueQ] := (* stand on all 17 or above *) If[Max[from] >= DealerStandValue, stand, hit] (** get all possible results. * recursive *) DealerResults::usage = "DealerResults[from]: returns a list of possible dealer results based on DealerHitStandStrategy[].\n DealerResults[]: returns a list of possible dealer results." DealerResults[from_?CardValueQ] := (* cache it because it's recursive *) DealerResults[from] = If[DealerHitStandStrategy[from] === stand, (* stand if DealerHitStandStrategy[] says so *) List[from], (* once more: unique *) Union[ (* go on with the recursion *) Join @@ (DealerResults /@ (* unique *) Union[ (* all sums from 'from' *) BjCardSum[from, CardValue[#]]& /@ Cards])]] DealerResults[] := DealerResults[List[0]] (** get probabilities for all possible results. * recursive *) DealerHitStandProbability::usage = "DealerHitStandProbability[from,to]: returns the probability of reaching 'to' from 'from'. If 'to' is an integer, the sum of probabilities with the highest value equal to 'to' is returned." DealerHitStandProbability[from_?CardValueQ, to_?CardValueQ] := (* cache it because it's recursive *) DealerHitStandProbability[from, to] = If[DealerHitStandStrategy[from] === stand, (* if dealer doesn't hit, * it's either unity or none *) If[from == to, 1, 0], (* else weighted sum *) Plus @@ (Function[{card}, (* weight *) CardProbability[card] * (* go on with the recursion *) DealerHitStandProbability[ (* new sum *) BjCardSum[from, CardValue[card]], to]] /@ (* for each card *) Cards)] DealerHitStandProbability[from_?CardValueQ, to_Integer] := (* cache it to increase performance *) DealerHitStandProbability[from, to] = Plus @@ (DealerHitStandProbability[from, #]& /@ (* all possible to-values *) Select[DealerResults[], (Max[#] == to)&]) (** get initial probabilities. *) DealerInitialProbability::usage = "DealerInitialProbability[from,to]: returns the probability of reaching 'to' from 'from' where 'from' is the initial card. If 'to' is an integer, the sum of probabilities with the highest value equal to 'to' is returned." DealerInitialProbability[from_?CardQ, to_?CardValueQ] := (* cache it to increase performance *) DealerInitialProbability[from, to] = (* dealer must initially hit *) (* weighted sum *) Plus @@ (Function[{card}, (* new sum *) With[{newfrom = BjCardSum[ CardValue[from], CardValue[card]]}, (* weight *) If[Max[newfrom] == BlackjackValue, (* blackjack not possible * at this stage *) 0, CardProbability[card] / (* correct * the probability *) (1 - BjCardProbability[from])] * DealerHitStandProbability[newfrom, to]]] /@ (* for each card *) Cards) DealerInitialProbability[from_?CardQ, to_Integer] := (* cache it to increase performance *) DealerInitialProbability[from, to] = Plus @@ (DealerInitialProbability[from, #]& /@ (* all possible to-values *) Select[DealerResults[], (Max[#] == to)&]) (* player part *) (** favor if standing. *) PlayerStandFavor::usage = "PlayerStandFavor[from,dealer]: returns the favor against the dealer if standing." PlayerStandFavor[from_?CardValueQ, dealer_?CardQ] := (* cache it to increase performance *) PlayerStandFavor[from, dealer] = (* busted is -1 since player loses on bust *) If[BustedQ[from], -1, Plus @@ (Function[{to}, (* weight *) DealerInitialProbability[dealer, to] * (* favor *) BjCompare[from, to]] /@ DealerResults[CardValue[dealer]])] (** favor if hitting. * indirect recursive *) PlayerHitFavor::usage = "PlayerHitFavor[from,dealer]: returns the favor against the dealer if hitting." PlayerHitFavor[from_?CardValueQ, dealer_?CardQ] := (* cache it because it's indirect recursive *) PlayerHitFavor[from, dealer] = Plus @@ (Function[{card}, (* weight *) CardProbability[card] * (* sub-favor, indirect recursion *) PlayerHitStandFavor[ (* new sum *) BjCardSum[from, CardValue[card]], dealer]] /@ Cards) (** favor against dealer. * maximum favor of hitting or standing is chosen. * indirect recursive *) PlayerHitStandFavor::usage = "PlayerHitStandFavor[from,dealer]: returns the favor against the dealer if hitting or standing." PlayerHitStandFavor[from_?CardValueQ, dealer_?CardQ] := (* cache it because it's indirect recursive *) PlayerHitStandFavor[from, dealer] = (* recursion termination *) If[BustedQ[from], -1, (* favor the maximum *) Max[ PlayerHitFavor[from, dealer], PlayerStandFavor[from, dealer]]] (** favor factor if doubling down. *) DoubleDownFavor::usage = "DoubleDownFavor: 2." DoubleDownFavor := 2 (** favor if doubling down. *) PlayerDoubleDownFavor::usage = "PlayerDoubleDownFavor[from,dealer]: returns the favor against the dealer if doubling down." PlayerDoubleDownFavor[from_?CardValueQ, dealer_?CardQ] := (* cache it to increase performance *) PlayerDoubleDownFavor[from, dealer] = (* we may or may not consider doubling the favor * on doubling the wager, for overall favor *) DoubleDownFavor * (Plus @@ (Function[{card}, (* weight *) CardProbability[card] * (* sub-favor, * not allowed to hit anymore *) PlayerStandFavor[ (* new sum *) BjCardSum[from, CardValue[card]], dealer]] /@ Cards)) (** favor factor if splitting. *) PairFavor::usage = "PairFavor: 2." PairFavor := 2 (** favor if splitting. * indirect recursive. * re-splitting is not considered. *) PlayerPairFavor::usage = "PlayerPairFavor[card,dealer]: returns the favor against the dealer if splitting." PlayerPairFavor[card_?CardQ, dealer_?CardQ] := (* cache it to increase performance *) PlayerPairFavor[card, dealer] = (* we may or may not consider doubling the favor * on doubling the wager, for overall favor *) PairFavor * (Plus @@ (Function[{cardtwo}, (* weight *) CardProbability[cardtwo] * (* don't consider pairing anymore *) PlayerInitialFavor[ BjCardSum[ CardValue[card], CardValue[cardtwo]], dealer]] /@ Cards)) (** favor against dealer from the two initial cards. * indirect recursive (depending on PlayerPairFavor implementation, * due to splitting). * dealer blackjack probability is not considered * (would be needed for overall probability). *) PlayerInitialFavor::usage = "PlayerInitialFavor[from,dealer]: returns the favor against the dealer without considering pairing (splitting).\n PlayerInitialFavor[cardone,cardtwo,dealer]: returns the favor against the dealer, splitting considered." PlayerInitialFavor[from_?CardValueQ, dealer_?CardQ] := (* cache it to increase performance *) PlayerInitialFavor[from, dealer] = (* we could consider doubling down only if favorable *) Max[ PlayerDoubleDownFavor[from, dealer], PlayerHitStandFavor[from, dealer]] PlayerInitialFavor[cardone_?CardQ, cardtwo_?CardQ, dealer_?CardQ] := (* cardone, cardtwo are symmetric *) If[First[First[Position[Cards, cardone]]] > First[First[Position[Cards, cardtwo]]], (* use symmetric result *) PlayerInitialFavor[cardtwo, cardone, dealer], (* cache it because it could be indirect recursive * (depending on PlayerPairFavor implementation) *) PlayerInitialFavor[cardone, cardtwo, dealer] = With[{favor = PlayerInitialFavor[ BjCardSum[CardValue[cardone], CardValue[cardtwo]], dealer]}, (* check for pair that can be split *) If[cardone === cardtwo, (* (not considering * dealer blackjack) *) (* we could consider splitting * only if favorable *) Max[ PlayerPairFavor[cardone, dealer], favor], favor]]] (** initial strategy. * dealer blackjack probability is not considered * (would be needed for overall probability). *) PlayerInitialStrategy::usage = "PlayerInitialStrategy[cardone,cardtwo,dealer]: returns either 'hit, 'stand, 'doubledown or 'pair (split), depending on the favor of the weighted possible results against the dealer.\n PlayerInitialStrategy[from,dealer]: returns either 'hit, 'stand or 'doubledown." PlayerInitialStrategy[from_?CardValueQ, dealer_?CardQ, favor_?NumberQ] := (* (not considering dealer blackjack, as in PlayerInitialFavor) *) Which[ (* check for blackjack *) Max[from] == BlackjackValue, stand, (* favor standing over doubling down over hitting *) favor == PlayerStandFavor[from, dealer], stand, (* consider doubling down (before hitting) * only if favorable *) favor >= 0 && favor == PlayerDoubleDownFavor[from, dealer], doubledown, favor == PlayerHitFavor[from, dealer], hit, (* else must be splitting. * (we don't have the information about what cards here) *) True, pair] PlayerInitialStrategy[from_?CardValueQ, dealer_?CardQ] := PlayerInitialStrategy[ from, dealer, PlayerInitialFavor[from, dealer]] PlayerInitialStrategy[cardone_?CardQ, cardtwo_?CardQ, dealer_?CardQ] := PlayerInitialStrategy[ BjCardSum[CardValue[cardone], CardValue[cardtwo]], dealer, PlayerInitialFavor[cardone, cardtwo, dealer]] (** overall blackjack favor. *) PlayerOverallFavor::usage = "PlayerOverallFavor[]: returns the oveall blackjack favor." PlayerOverallFavor[] := Plus @@ (Function[{cardone}, CardProbability[cardone] * Plus @@ (Function[{cardtwo}, CardProbability[cardtwo] * Plus @@ (Function[{dealer}, CardProbability[dealer] * (* check for blackjack *) If[Max[BjCardSum[ CardValue[cardone], CardValue[cardtwo]]] == BlackjackValue, (* count off pushs *) (1 - BjCardProbability[dealer]) * (* player blackjack pays 2:1, * so weight 3/2 *) BlackjackFavor, (1 - BjCardProbability[dealer]) * PlayerInitialFavor[ cardone, cardtwo, dealer] - (* count off dealer blackjacks *) BjCardProbability[dealer]]] /@ Reverse[Cards])] /@ Cards)] /@ Cards) (** strategy code. *) StrategyCode::usage = "StrategyCode[strategy,favor]: returns upper case strategy initial if favor is positive, else lower case initial." StrategyCode[strategy_, favor_?NumberQ] := (* remain symbolic *) ToExpression[ (* upper or lower case *) Which[ (* player favor *) favor >= 0, ToUpperCase, (* dealer favor *) favor >= SurrenderFavor, ToLowerCase, (* dealer favor, surrender if < -1/2 *) True, ToLowerCase][ (* initial only *) StringTake[SymbolName[strategy], 1]]] (** hard strategy table. *) PlayerHardStrategyTable::usage = "PlayerHardStrategyTable[]: returns a hit or stand table for hard hands. H: hit, player's favor; h: hit, dealer's favor; S: stand, player's favor; s: stand, dealer's favor; D: double down, player's favor (if permitted, else hit). Splitting is not considered." PlayerHardStrategyTable[] := Table[ Function[{dealer}, StrategyCode[ PlayerInitialStrategy[List[from], dealer], PlayerInitialFavor[List[from], dealer]]] /@ (* unique-valued initial dealer cards *) Reverse[UniqueValueCards[]], {from, (* maximal value that's not busted *) BlackjackValue, (* minimal value for two cards *) 2 MinCardValue, (* from higher value to lower value *) -1}] (** soft strategy table. *) PlayerSoftStrategyTable::usage = "PlayerSoftStrategyTable[]: returns a hit or stand table for soft hands. H: hit, player's favor; h: hit, dealer's favor; S: stand, player's favor; s: stand, dealer's favor; D: double down, player's favor (if permitted, else hit). Splitting is not considered." PlayerSoftStrategyTable[] := Table[ Function[{dealer}, With[{value = CardValue[ace] + from}, StrategyCode[ PlayerInitialStrategy[value, dealer], PlayerInitialFavor[value, dealer]]]] /@ (* unique-valued initial dealer cards *) Reverse[UniqueValueCards[]], {from, (* maximal value that's not busted *) BlackjackValue - (* soft hand include an ace *) Max[CardValue[ace]], 1, -1}] (** pair (split) strategy table. *) PlayerPairStrategyTable::usage = "PlayerPairStrategyTable[]: returns a hit or stand table for a pair of cards. H: hit, player's favor; h: hit, dealer's favor; S: stand, player's favor; s: stand, dealer's favor; D: double down, player's favor (if permitted, else hit); P: pair (split), player's favor; p: pair, dealer's favor." PlayerPairStrategyTable[] := Function[{card}, Function[{dealer}, StrategyCode[ PlayerInitialStrategy[card, card, dealer], PlayerInitialFavor[card, card, dealer]]] /@ (* unique-valued initial dealer cards *) Reverse[UniqueValueCards[]]] /@ (* cards with an unique value *) UniqueValueCards[] (** output the results. *) BjResults::usage = "BjResults[]: display blackjack game strategy tables." BjResults[] := Module[{}, Print["hard strategy:"]; Print[TraditionalForm[PlayerHardStrategyTable[]]]; Print["soft strategy:"]; Print[TraditionalForm[PlayerSoftStrategyTable[]]]; Print["pair strategy:"]; Print[TraditionalForm[PlayerPairStrategyTable[]]]; ] (* package end *) EndPackage[]