Creating an AI for BlackJack

Twister1002 picture Twister1002 · Oct 17, 2012 · Viewed 10.1k times · Source

I am trying to make a Blackjack game! I am successful with player interaction! However, I decided to step it up and do some AI so I can play against AI, and have more of a battle, so to speak.

My main issue though, is I've sat here for about an hour or so thinking of how AI works and how I could use it, and I've not been able to think of anything that could work. So I was wondering if anyone had any ideas, or was able to guide me in a direction.

I do not have any code for a start of an AI, since I can't think of how to start one or work with one. That is why I'm hoping for some kind of direction.

Now I will post my classes that I think is all relevant. I did leave out the Game class. The Game Class is just for validation and checking Cards and all that amazing stuff.

Class Card:

public class Card{
private int rank, suit;

private String[] suitNames = new String[]{ "H", "C", "S", "D" };
private String[] rankNumber = new String[]{ "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" };
private int[] points = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10};

Card(int suitIndex, int rankIndex){
    rank = rankIndex;
    suit = suitIndex;
}

public @Override String toString(){
    return rankNumber[rank]+suitNames[suit]; 
}

public int getRank(){
    return rank;
}

public int getSuit(){
    return suit;
}

public String getSuitName(){
    return suitNames[suit];
}

public String getRankName(){
    return rankNumber[rank];
}

public int getPoints(){
     return points[rank];
}

public ImageIcon ImageOfCard() throws Exception{
    ImageIcon icon = new ImageIcon("/StandardDeck/GameCards/"+getRankName() + getSuitName()+".png");
    return icon;
}
}

Class BlackJack (Game with all the stuff) Yes I did use Java GUI just to work on it.

public class BlackJack extends JFrame {
Game game;
Deck deck;
Card cards;
Player player;
Dealer dealer;
JLabel[] playerCardSlots;
JLabel[] dealerCardSlots;

public BlackJack() {
    String name = JOptionPane.showInputDialog(null, "Enter your name");
    deck = new Deck(4);
    game = new Game();
    player = new Player(name);
    dealer = new Dealer(deck);

    initComponents();
    SetButtons(false);
    playerCardSlots = new JLabel[]{Player1Card1, Player1Card2, Player1Card3, Player1Card4, Player1Card5};
    dealerCardSlots = new JLabel[]{DealerCard1, DealerCard2, DealerCard3, DealerCard4, DealerCard5};
}

@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">                          
private void initComponents() {

    Player1Card1 = new javax.swing.JLabel();
    Player1Card2 = new javax.swing.JLabel();
    Player1Card3 = new javax.swing.JLabel();
    Player1Card4 = new javax.swing.JLabel();
    Player1Card5 = new javax.swing.JLabel();
    Player1Name = new javax.swing.JLabel();
    HitButton = new javax.swing.JButton();
    StandButton = new javax.swing.JButton();
    PointsLabel = new javax.swing.JLabel();
    DealButton = new javax.swing.JButton();
    DealerCard1 = new javax.swing.JLabel();
    DealerCard2 = new javax.swing.JLabel();
    DealerCard3 = new javax.swing.JLabel();
    DealerCard4 = new javax.swing.JLabel();
    DealerCard5 = new javax.swing.JLabel();
    DealerPointsLabel = new javax.swing.JLabel();
    DealerLabel = new javax.swing.JLabel();

    setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    Player1Name.setText("Player 1 Name");

    HitButton.setText("Hit");
    HitButton.addActionListener(new java.awt.event.ActionListener() {
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            HitButtonActionPerformed(evt);
        }
    });

    StandButton.setText("Stand");
    StandButton.addActionListener(new java.awt.event.ActionListener() {
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            StandButtonActionPerformed(evt);
        }
    });

    PointsLabel.setText("points");

    DealButton.setText("Deal");
    DealButton.addActionListener(new java.awt.event.ActionListener() {
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            DealButtonActionPerformed(evt);
        }
    });

    DealerPointsLabel.setText("points");

    DealerLabel.setText("Dealer");

    org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(getContentPane());
    getContentPane().setLayout(layout);
    layout.setHorizontalGroup(
        layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
        .add(layout.createSequentialGroup()
            .add(54, 54, 54)
            .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                .add(layout.createSequentialGroup()
                    .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                        .add(layout.createSequentialGroup()
                            .add(76, 76, 76)
                            .add(Player1Name)
                            .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                        .add(org.jdesktop.layout.GroupLayout.TRAILING, layout.createSequentialGroup()
                            .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, 104, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                                .add(DealerPointsLabel)
                                .add(PointsLabel))
                            .add(128, 128, 128)))
                    .add(DealButton))
                .add(layout.createSequentialGroup()
                    .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING)
                        .add(org.jdesktop.layout.GroupLayout.LEADING, layout.createSequentialGroup()
                            .add(Player1Card1, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                            .add(Player1Card2, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                            .add(Player1Card3, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                            .add(Player1Card4, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .add(12, 12, 12)
                            .add(Player1Card5, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
                        .add(org.jdesktop.layout.GroupLayout.LEADING, layout.createSequentialGroup()
                            .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING)
                                .add(DealerLabel)
                                .add(layout.createSequentialGroup()
                                    .add(DealerCard1, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                                    .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                                    .add(DealerCard2, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                                    .add(12, 12, 12)
                                    .add(DealerCard3, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)))
                            .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
                            .add(DealerCard4, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                            .add(DealerCard5, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 40, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)))
                    .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING, false)
                        .add(HitButton, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .add(StandButton, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 75, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))))
            .addContainerGap())
    );
    layout.setVerticalGroup(
        layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
        .add(layout.createSequentialGroup()
            .add(13, 13, 13)
            .add(DealerLabel)
            .addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
            .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING)
                .add(HitButton)
                .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                    .add(DealerCard1, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(DealerCard2, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(DealerCard3, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(DealerCard4, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(DealerCard5, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)))
            .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
            .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                .add(StandButton)
                .add(DealerPointsLabel))
            .add(4, 4, 4)
            .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                .add(PointsLabel)
                .add(DealButton))
            .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
            .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                .add(Player1Card1, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                .add(Player1Card2, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                .add(Player1Card3, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                .add(Player1Card4, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                .add(Player1Card5, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, 60, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
            .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
            .add(Player1Name)
            .addContainerGap(29, Short.MAX_VALUE))
    );

    pack();
}// </editor-fold>                        

private void HitButtonActionPerformed(java.awt.event.ActionEvent evt) {                                          
    PlayerHit();
}                                         

private void StandButtonActionPerformed(java.awt.event.ActionEvent evt) {                                            
    PlayerStand();
}                                           

private void DealButtonActionPerformed(java.awt.event.ActionEvent evt) {                                           
    ResetGame();
}                                          

public void SetName(){
    Player1Name.setText(player.getName());
}

public void DealCards(){
    player.AddCard(deck.DrawCard());
    dealer.AddCard(deck.DrawCard());
    player.AddCard(deck.DrawCard());
    dealer.AddCard(deck.DrawCard());
    ShowCards(player, player.CountCards());
    ShowCards(dealer, 1);

    SetButtons(true);
}

public void DealerHit(){
    dealer.AddCard(deck.DrawCard());
}

public void DealerStand(){
    dealer.PlayerStand();
}

public void PlayerHit(){
    player.AddCard(deck.DrawCard());
    ShowCards(player, player.CountCards());
}
public void PlayerStand(){
    player.PlayerStand();
    SetButtons(false);
    dealer.PlayerTurn();
}

public void ShowCards(Player person, int cards){
    if(!person.getName().equals("Dealer")){
        for(int i = 0; i < cards; i++){
            playerCardSlots[i].setText(person.getCard(i).toString());
        }

        int points = game.CardTotal(person);
        if(game.PlayerBust(points)){
            PointsLabel.setText("BUST!");
            SetButtons(false);
        }
        else{
            PointsLabel.setText(points+"");
        }

        if(game.CardDraw(person)){
            PointsLabel.setText("5 Card Draw!");
            SetButtons(false);
        }
    }
    else{
        for(int i = 0; i < cards; i++){
            dealerCardSlots[i].setText(person.getCard(i).toString());
        }

        int points = game.CardTotal(person, cards);

        if(game.PlayerBust(points)){
            DealerPointsLabel.setText("BUST!");
            SetButtons(false);
        }
        else{
            DealerPointsLabel.setText(points+"");
        }

        if(game.CardDraw(person)){
            DealerPointsLabel.setText("5 Card Draw!");
            SetButtons(false);
        }

        if(points >= 17){

        }
    }
}

public void SetButtons(boolean enabled){
        HitButton.setEnabled(enabled);
        StandButton.setEnabled(enabled);
}

public void ResetGame(){
    for(JLabel label : playerCardSlots){
        label.setText("");
    }
    for(JLabel label : dealerCardSlots){
        label.setText("");
    }

    player.ClearCards();
    dealer.ClearCards();

    deck = new Deck(4);
    DealCards();
    SetName();

    player.PlayerTurn();
    dealer.PlayerStand();
}

// <editor-fold defaultstate="collapsed" desc="Variables">
// Variables declaration - do not modify                     
private javax.swing.JButton DealButton;
private javax.swing.JLabel DealerCard1;
private javax.swing.JLabel DealerCard2;
private javax.swing.JLabel DealerCard3;
private javax.swing.JLabel DealerCard4;
private javax.swing.JLabel DealerCard5;
private javax.swing.JLabel DealerLabel;
private javax.swing.JLabel DealerPointsLabel;
private javax.swing.JButton HitButton;
private javax.swing.JLabel Player1Card1;
private javax.swing.JLabel Player1Card2;
private javax.swing.JLabel Player1Card3;
private javax.swing.JLabel Player1Card4;
private javax.swing.JLabel Player1Card5;
private javax.swing.JLabel Player1Name;
private javax.swing.JLabel PointsLabel;
private javax.swing.JButton StandButton;
// End of variables declaration                   
//</editor-fold>
}

Class Player:

 public class Player{
 private String playerName;
 private ArrayList<Card> playerCards = new ArrayList<Card>();
 private boolean turn = false;

Player(String name){
    playerName = name;
}

public Card getCard(int index){
    return playerCards.get(index);
}

public void AddCard(Card card){
    playerCards.add(card);
}

public void ClearCards(){
    playerCards.clear();
}

public int CountCards(){
    return playerCards.size();
}

public String getName(){
    return playerName;
}

public void PlayerStand(){
    turn = false;
}

public void PlayerTurn(){
    turn = true;
}

public boolean getTurn(){
    return turn;
}

}

Answer

First of all: Blackjack is indeed interesting from a geeks viewpoint.

As someone has commented: The bank uses a fixed algorithm for all it's actions. These actions are (usually? or always? Find out your self.) only depending on the banks hand. Implement that first.

For the player you should definitively implement what blackjack players call "Basic Strategy". I guess a google search will give you lots of hits.

Now here is what I would have studied:
Having the bank fixed strategy and the players "basic strategy" implemented, you can start doing Monte Carlo simulations to find the expectation of the game. Try to get the MC simulation code as fast as possible since, you probably want to do a lot of MC simulations.

Now try to slightly modify the deck. Try for example to remove all 5s from the deck. Then do the same MC simulation, and see if this alters the expectation. Would you like to play BJ with a deck without 5s?

Now, modify the deck again. Try to remove half of all the 10s in the deck (That's half of all cards with the value 10). How does that alter the players expectation?

Now modify the deck again. Try to remove half of the non-10 values cards. How does that affect the expectation?

Keep on modifying the deck a tiny bit, and try to get an understanding of how a small bias in the distribution of remaining cards affect the expectation. You should use this understanding to adjust the stake of the game. The strategy is hence not the hit/stand action, but rather the money management based on the distribution bias in the remaining deck in the shoe. Keep hitting/standing decisions based on "basic strategy"!

Now further steps:
Maybe you can train a neural net (or some other estimating algorithm) to estimate the expectation of the game based on the known remaining cards in the deck? (You have to keep track of the cards remaining in the shoe.) Maybe you can even connect the MC simulation tool to the training algorithm to give you a kind of reinforcement learning algorithm. This is actually starting to sound like real geek fun! Or maybe you can get the MC-simulator to be so fast that get a good answer of the expectation on the fly.

Even further step:
When you have found a good way to estimate the expectation of the game, given the remaining cards in the shoe (and "basic "strategy"), which is fast enough, you may be able to implement a reinforcement learning algorithm for the money management.

... or you can stick with the simple plan: Positive expectation => High stakes. Negative expectation => Low stakes.

Good luck with your study,
-Øystein