Friday, April 14, 2023

The deceptive game: Inbetweenies

In our teens and early twenties, we are often exposed to card games, and so it was that I discovered inbetweenies (Also known as Acey Deucey, Yablon, and Red Dog). It's a deceptively simple card game that you will almost certainly lose your shirt on! Having watched, won, and lost, I am fascinated by the probabilities, which should be relatively easy to calculate.

The Premise
The game is best played by 5 or 6 players as it becomes difficult to track the cards, and the pot can grow in leaps and bounds. Face cards are valued at 10 and an Ace is usually considered low.
All players contribute a cash amount to a pot. A dealer offers 2 cards face up to a player. Based on the size of the difference, the player proceeds, and bets some amount up to the value of the pot, at which point the dealer deals a third card face-up. If the card's value is between the first two cards, you win the amount that you bet. If the card is outside of those first 2 cards, you lose and have to put that amount into the pot. The real fun starts if you "hit the post". This happens when the third card has the same value as one of your first 2 cards; you have to pay double the amount that you bet. This means that a particularly ballsy player might bet the pot and have to pay in double. I personally saw pots leap to €400 from a modest start of €5 early on. It also means that if you place a bet, you'd better be able to cover the worst outcome!
I've created a simple table below to explain the outcomes 


The Probabilities
As the dealer returns used cards to the bottom of the deck and periodically re-shuffles them, it's possible to calculate some very simple odds in your head at each play. If you were gifted in card counting you could probably do even better, but almost every time I played this game beer was involved so that advantage would probably be lost pretty quickly.

I am going to approach this in two ways: through logical reasoning and then through experimentation using Python.

Logical approach
Knowing that there are 52 cards in a deck we can calculate the basic probability of any hand by looking at how many cards fall between the lowest and highest cards. For example, if the dealer gives a player a 2 and a 10, we know that there are 7 cards in the winning "zone" (3,4,5,6,7,8,9). And we know that there are 4 suits of those cards so 7 * 4 = 28 ways (out of 50 - 2 cards are already dealt) that you can win. The corollary of this is that there are 22 (50 - 28) ways that you can lose. Hitting the post is a different way to lose though, so let's separate that out; 2 cards (low and high) times 3 suits = 6 ways to lose big.
We subtract that from our total ways to lose to get the less painful ways to lose (28-8): 20 out of 50.

Win: 28/50 = 56%
Post: 6/50 = 12%
Lose: 16/50 = 32%


One variation of the game allow the Jack, Queen, and King cards to have their normal values (11,12,13). The same logic applies, and for simplicities sake I've visualized these using a heat map.


Experimentation approach
I used python to simulate the game and visualize the outcomes. The great benefit of the simulation is that I can run a large number of games to test the outcomes. 
After running 1 million games I was able to confirm that the chances of hitting the post is 12% for every hand.

I can also introduce player behaviour. In this case, I can make an assumption that players won't play every hand and will set themselves some threshold, i.e. I'll only play if there is a difference of more than X between the lowest and highest value cards dealt. I was surprised to find that the introduction of this behavior resulted in no change in the outcomes.


What can you take from this?
The major takeaway for players is to favour hands with large differences. This should be obvious but knowing the probabilities allows you to select hands which at least a difference of 7 between them will give you better than 50/50.
Knowing that the odds of hitting the post are 12% might encourage you to think about how much you are willing to bet. One downfall of the game is that the payoffs for different spreads are the same. An innovative variation would be to offer better pay-offs on lower probability spreads. 


Python Code used for research
import pandas as pd
import numpy as np
import random
from copy import deepcopy


print('Rejecting 6 and up')
#Create deck
cardList=[['Ace',1,13,'hearts'],
['2',2,2,'hearts'],
['3',3,3,'hearts'],
['4',4,4,'hearts'],
['5',5,5,'hearts'],
['6',6,6,'hearts'],
['7',7,7,'hearts'],
['8',8,8,'hearts'],
['9',9,9,'hearts'],
['10',10,10,'hearts'],
['Jack',11,11,'hearts'],
['Queen',12,12,'hearts'],
['King',13,13,'hearts'],
['Ace',1,13,'diamonds'],
['2',2,2,'diamonds'],
['3',3,3,'diamonds'],
['4',4,4,'diamonds'],
['5',5,5,'diamonds'],
['6',6,6,'diamonds'],
['7',7,7,'diamonds'],
['8',8,8,'diamonds'],
['9',9,9,'diamonds'],
['10',10,10,'diamonds'],
['Jack',11,11,'diamonds'],
['Queen',12,12,'diamonds'],
['King',13,13,'diamonds'],
['Ace',1,13,'spades'],
['2',2,2,'spades'],
['3',3,3,'spades'],
['4',4,4,'spades'],
['5',5,5,'spades'],
['6',6,6,'spades'],
['7',7,7,'spades'],
['8',8,8,'spades'],
['9',9,9,'spades'],
['10',10,10,'spades'],
['Jack',11,11,'spades'],
['Queen',12,12,'spades'],
['King',13,13,'spades'],
['Ace',1,13,'clubs'],
['2',2,2,'clubs'],
['3',3,3,'clubs'],
['4',4,4,'clubs'],
['5',5,5,'clubs'],
['6',6,6,'clubs'],
['7',7,7,'clubs'],
['8',8,8,'clubs'],
['9',9,9,'clubs'],
['10',10,10,'clubs'],
['Jack',11,11,'clubs'],
['Queen',12,12,'clubs'],
['King',13,13,'clubs']]




#Create column names for data to track
cols = ['observation','lowCardFace','lowCardSuit','lowCardVal', 'highCardFace','highCardSuit', 'highCardVal', 'testCardFace', 'testCardSuit','testCardVal']



##################################################################################
#Functions

def shuffleDeck():
    #Shuffles the deck in place
    tmpDeck = cardList
    random.shuffle(tmpDeck)
    return tmpDeck
    
#Returns 3 cards for a player
def getCards():
    handEvent=[]
    draw1 = shuffledDeck.pop(0)
    draw2 = shuffledDeck.pop(0)
    draw3 = shuffledDeck.pop(0)
    if draw2[2] > draw1[1]:
        handEvent.append([draw1, draw2, draw3])
    else:
        handEvent.append([draw2, draw1, draw3])
    return handEvent
    
    
##################################################################################
#Evaluate a hand
def textValue(lowVal, highVal, dealtVal):
    if dealtVal > lowVal and dealtVal < highVal:
        tmp='win'
    elif dealtVal < lowVal or dealtVal > highVal:
        tmp='lose'
    elif dealtVal == lowVal or dealtVal == highVal:
        tmp = 'post'
    return tmp
        
def diffVal(lowVal, highVal):
    tmp = highVal - lowVal
    return tmp
    
    
##################################################################################
#Run the script by creating an initial deck
shuffledDeck = deepcopy(cardList)
random.shuffle(shuffledDeck)

#I used deepcopy when i was creating a newly shuffled deck. This was needed when i had offered all of the cards in the current deck to the players and had none left.



testRun=[]

for i in range(0,1000000):
    if len(shuffledDeck) < 3:
        shuffledDeck = deepcopy(cardList)
        random.shuffle(shuffledDeck)
    handEvent=[]
    draw1 = shuffledDeck.pop(0)
    draw2 = shuffledDeck.pop(0)
    if abs(draw2[1] - draw1[1])>1:
        draw3 = shuffledDeck.pop(0)
        if draw2[1] > draw1[1]:
            handEvent.append([draw1, draw2, draw3])
        else:
            handEvent.append([draw2, draw1, draw3])
        cardOut = handEvent
        testRun.append([i, cardOut[0][0][0], cardOut[0][0][3], cardOut[0][0][1],cardOut[0][1][0], cardOut[0][1][3], cardOut[0][1][1], cardOut[0][2][0], cardOut[0][2][3], cardOut[0][2][1] ])

playDF = pd.DataFrame(testRun,columns=cols)

#Calculate the outcome and add a column to the dataframe to record that
playDF['outcome'] = playDF.apply( lambda row: textValue(row['lowCardVal'], row['highCardVal'], row['testCardVal']),axis=1)
playDF['diff'] = playDF.apply( lambda row: diffVal(row['lowCardVal'], row['highCardVal']),axis=1)


table = pd.pivot_table(playDF, values='observation', index=['diff'], columns=['outcome'], aggfunc='count', fill_value=0)
print(table)

table = pd.pivot_table(playDF, values='observation', index=['diff'], columns=['outcome'], aggfunc='count', fill_value=0, margins=True, margins_name='Total').pipe(lambda d: d.div(d['Total'], axis='index')).applymap('{:.000%}'.format)
print(table)


playDF.to_csv('C:\\temp\\inbetweenies_output_reject_01.csv', index=False)
print('Execution complete')
    
#Plot charts
flippedCount = playDF.groupby(['diff','outcome'])['observation'].count().unstack('outcome').fillna(0)
flippedCount[['lose', 'post', 'win']].plot(kind='bar', stacked=True)