Increase the chance of a playing card being drawn from a deck?

146 Views Asked by At

I am trying to meet these requirements:

  • Cards should be drawn in a pseudo-random order, Hearts and the Ace of Spades will be considered special cards with increased chances of being drawn. Each Heart Suit card should be 2x more likely to be drawn than a non-special card. The Ace of Spades should be 3x more likely to be drawn than a non-special card.

  • The drawing odds above refer to the likelihood of a card being the next card drawn. Exactly one card of each suit and numeric value should be seen exactly one time during a run through the deck.

I at first thought to add more cards of the special type to the deck to increase odds. After looking at the requirements, the deck would be more than 52 cards and show more than exactly one card each run. Am I understanding the requirements correctly?

Interpreting it another way, I would shift the special cards towards the beginning of the List so that the chance of drawing the NEXT card was high. But what makes one card 3x more likely and another 2x more likely?

Here is what I have that is relevant so far:

    public void ShuffleDeck()
    {
        for (int i = deck.Count - 1; i > 0; i--)
        {
            int j = Random.Range(0, i + 1);
            SwapCards(i, j);
        }
    }

    private void SwapCards(int i, int j)
    {
        Card temp = deck[i];
        deck[i] = deck[j];
        deck[j] = temp;
    }

    public Card DrawCard()
    {
        if (deck.Count > 0)
        {
            Card drawnCard = deck[0];
            deck.RemoveAt(0);
            drawnCards.Add(drawnCard);
            return drawnCard;
        }
        return null;
    }

    public void ApplyDrawingOdds(int oddsMultiplier, string specialSuit, string specialRank = "")
    {
        // search for special cards in the deck
        List<Card> specialCards = deck.FindAll(card => card.Suit == specialSuit && (specialRank == "" || card.Rank == specialRank));

        // loop through special cards
        foreach (Card specialCard in specialCards)
        {
            // retrieve index in deck
            int index = deck.IndexOf(specialCard);

            // calculate the target index for swapping based on oddsMultiplier
            int targetIndex = System.Math.Max(0, index - oddsMultiplier + 1);

            // swap current special card with other cards in deck
            for (int k = 0; k < oddsMultiplier; k++)
            {
                // move towards beginning of deck
                SwapCards(index, targetIndex + k);
            }
        }
    }

    public void ResetDeck()
    {
        deck.AddRange(drawnCards);
        drawnCards.Clear();
        ShuffleDeck();
    }
3

There are 3 best solutions below

0
derHugo On

There might be more elegant single time processed solutions ... but one simple yet maybe a bit more expensive approach would be to ad-hoc while drawing a card generate a weighted list and pick a random card from that.

Assuming you don't draw a card every frame the performance impact for an only maximum 52 element deck should be quite neglectable.

You wouldn't need to shuffle your deck up front at all ;)

// just assuming a full 54 card deck, adjust the size accordingly 
const int weightedCount = 3 // ace of spades 3x weight
                          + 13 * 2 // heart cards 2x weight
                          + 38; // remaining normal cards 1x weight
// anyway initializing the list with a certain capacity is optional 
private readonly List<Card> weightedDeck = new List(weightedCount);

public Card DrawCard()
{
    switch(deck.Count)
    {
        case 0:
            return null;

        case 1: 
        {
            var drawnCard = deck[0];
            deck.Clear();
            drawnCards.Add(drawnCard);
            return drawnCard;
        }

        default:
        {
            // re-generate the weighted deck according to the cards in deck
            weightedDeck.Clear();
            foreach(var card in deck)
            {
                // adding every card at least once
                weightedDeck.Add(card);

                // pseudo code as we don't have all details about your types
                if(card.color == CardColor.Heart)
                {
                    // adding heart cards again => total weight x2
                    weightedDeck.Add(card);
                } 
                else if (card.color == CardColor.Spade && card.value == Cardalue.Ace)
                {
                    // adding ace of spades 2 times more => total eight x3
                    weightedDeck.Add(card);
                    weightedDeck.Add(card);
                }
            }
            // pick a random card from the weighted deck
            var drawnCard = weightedDeck[Random.Range(0, weightedDeck.Count)];
            deck.Remove(drawnCard);
            drawnCards.Add(drawnCard);
            return drawnCard;
       }
    }
}

Alternatively you could as said also generate the full deck only once, draw a random and then simply remove all matches from the weighted deck

// just assuming a full 54 card deck, adjust the size accordingly 
const int weightedCount = 3 // ace of spades 3x weight
                          + 13 * 2 // heart cards 2x weight
                          + 38; // remaining normal cards 1x weight
private readonly List<Card> weightedDeck = new List(weightedCount);

public void ResetDeck()
{
    weightedDeck.Clear();
    drawnCards.Clear();

    foreach(var card in deck)
    {
        // adding every card at least once
        weightedDeck.Add(card);

        // pseudo code as we don't have all details about your types
        if(card.color == CardColor.Heart)
        {
            // adding heart cards again => total weight x2
            weightedDeck.Add(card);
        } 
        else if (card.color == CardColor.Spade && card.value == Cardalue.Ace)
        {
            // adding ace of spades 2 times more => total eight x3
            weightedDeck.Add(card);
            weightedDeck.Add(card);
       }
    }
}

public Card DrawCard()
{
    if(weightedDeck.Count == 0)
    {
        return null;
    }

    // pick a random card from the weighted deck
    var drawnCard = weightedDeck[Random.Range(0, weightedDeck.Count)];
    // then remove all matches from the weighted deck
    weightedDeck.RemoveAll(c => c == drawnCard);
    drawnCards.Add(drawnCard);
    return drawnCard;
}

Note that of course that logically either way this also means that the heart and ace of spades will be drawn earlier ... and then no longer be available for the rest of drawing ;)

0
Salman A On

Since pseudocode is allowed, I will explain the logic:

  • Use 52 cards but assign a numeric weight to each card.
  • Calculate the total weight (13 hearts x 2 + 1 ace of spades x 3 + 38 others x 1 = 67)
  • Pick a random number between [0, 67)
  • Map the number to those weights to find the card e.g. numbers between [0, 2) map to first card and those between [2, 4) map to second card and so on.
  • Remove the card from deck and repeat

Below is JavaScript implementation of the process and tests. Notice the result of 6700 iterations test... each heart was chosen approximately 200 times and ace of spades approximately 300 times.

let weightedDeck = [
  { "card": "HA",  "weight": 2 },
  { "card": "H2",  "weight": 2 },
  { "card": "H3",  "weight": 2 },
  { "card": "H4",  "weight": 2 },
  { "card": "H5",  "weight": 2 },
  { "card": "H6",  "weight": 2 },
  { "card": "H7",  "weight": 2 },
  { "card": "H8",  "weight": 2 },
  { "card": "H9",  "weight": 2 },
  { "card": "H10", "weight": 2 },
  { "card": "HJ",  "weight": 2 },
  { "card": "HQ",  "weight": 2 },
  { "card": "HK",  "weight": 2 },
  { "card": "DA",  "weight": 1 },
  { "card": "D2",  "weight": 1 },
  { "card": "D3",  "weight": 1 },
  { "card": "D4",  "weight": 1 },
  { "card": "D5",  "weight": 1 },
  { "card": "D6",  "weight": 1 },
  { "card": "D7",  "weight": 1 },
  { "card": "D8",  "weight": 1 },
  { "card": "D9",  "weight": 1 },
  { "card": "D10", "weight": 1 },
  { "card": "DJ",  "weight": 1 },
  { "card": "DQ",  "weight": 1 },
  { "card": "DK",  "weight": 1 },
  { "card": "CA",  "weight": 1 },
  { "card": "C2",  "weight": 1 },
  { "card": "C3",  "weight": 1 },
  { "card": "C4",  "weight": 1 },
  { "card": "C5",  "weight": 1 },
  { "card": "C6",  "weight": 1 },
  { "card": "C7",  "weight": 1 },
  { "card": "C8",  "weight": 1 },
  { "card": "C9",  "weight": 1 },
  { "card": "C10", "weight": 1 },
  { "card": "CJ",  "weight": 1 },
  { "card": "CQ",  "weight": 1 },
  { "card": "CK",  "weight": 1 },
  { "card": "SA",  "weight": 3 },
  { "card": "S2",  "weight": 1 },
  { "card": "S3",  "weight": 1 },
  { "card": "S4",  "weight": 1 },
  { "card": "S5",  "weight": 1 },
  { "card": "S6",  "weight": 1 },
  { "card": "S7",  "weight": 1 },
  { "card": "S8",  "weight": 1 },
  { "card": "S9",  "weight": 1 },
  { "card": "S10", "weight": 1 },
  { "card": "SJ",  "weight": 1 },
  { "card": "SQ",  "weight": 1 },
  { "card": "SK",  "weight": 1 }
];

function drawCard(weightedDeck) {
  let randomMax = weightedDeck.reduce((carry, item) => carry + item.weight, 0);
  let randomNumber = Math.random() * randomMax;
  let runningSum = 0;
  let index = 0;
  while (index < weightedDeck.length) {
    if (randomNumber >= runningSum && randomNumber < runningSum + weightedDeck[index].weight) {
      break;
    }
    runningSum += weightedDeck[index].weight;
    index += 1;
  }
  let items = weightedDeck.splice(index, 1);
  return items[0].card;
}

// demo 

(function() {
  let deckCopy = weightedDeck.slice();
  let draw1 = drawCard(deckCopy);
  let draw2 = drawCard(deckCopy);
  console.log(`draw #1 = ${draw1}, draw #2 = ${draw2}`);
})();

// tests

(function() {
  let results1 = {};
  let results2 = {};
  weightedDeck.forEach((item) => {
    results1[item.card] = 0;
    results2[item.card] = 0;
  });
  for (let i = 0; i < 6700; i++) {
    let deckCopy = weightedDeck.slice();
    let draw1 = drawCard(deckCopy);
    let draw2 = drawCard(deckCopy);
    results1[draw1] += 1;
    results2[draw2] += 1;
  }
  console.log(results1);
  console.log(results2);
})();

0
derpirscher On

If you only want to draw two cards from a complete deck (this is what I understood from your comment) a simple approach would be the following

  • Init your deck as follows

     var deck = new List<string>{         
       "H2", "H2", "H3", "H3",  ..., "HA", "HA", //2 for each heart
       "D2", "D3", ..., "DA",        //1 for each diamond
       "C2", "C3", ..., "CA",        //1 for each club
       "S2", "S3", ..., "SK",        //1 for every spade except the ace
       "SA", "SA", "SA",             //3 Ace of spades
      }
    

    That will give you a total deck of 67 slots.

  • For the first card select a random index with 0 <= index < 67. Depending on what card you drew, remove all equal cards. Because of the way the deck is initialized, it follows from the selected index, how many cards to remove.

     var index = rand.Next(67);
     var firstcard = deck[index];
     if (index < 26) {  
       //it's heart, remove both equal cards
       index = index / 2 * 2;  //this will give you the even index (ie the first of two equal hearts)
       deck.RemoveRange(index,2); //remove both hearts
     } else if (index < 64) {
       //it's club, diamond or spade (except ace), remove that card
       deck.RemoveAt(index);
     } else {
       //it's ace of spades, remove the last three cards of the deck
       deck.RemoveRange(64, 3);
     }
    
  • For the second card select a random index with 0 <= index < deck.Count

     index = rand.Next(deck.Count);
     var secondcard = deck[index];
    

    As in the first step, we removed all cards equal to the drawn card, the second card will always be different from the first card.

Of course if you want to draw more than two cards from the deck, this simple approach won't work anymore, because after the first draw, the index alone cannot be used anymore to decide what cards to remove. You then would have to compare the drawn card with its neighbours and remove them too if necessary.