0

So I am creating a card game, Ring of Fire. I have stored images like this:

var picture:[UIImage] = [
        UIImage(named: "Card2")!,
        UIImage(named: "Card3")!,
        UIImage(named: "Card4")!,
        UIImage(named: "Card5")!,
        UIImage(named: "Card6")!,
        UIImage(named: "Card7")!,
        UIImage(named: "Card8")!,
        UIImage(named: "Card9")!,
        UIImage(named: "Card10")!,
        UIImage(named: "CardJack")!,
        UIImage(named: "CardQueen")!,
        UIImage(named: "CardKing")!,
        UIImage(named: "CardAce")!,
        ]

Each card has text displayed under the current card:

var name:String = ""

    var files = ["Velg en som må drikke", // 2
                 "Drikk selv", // 3
                 "Alle jenter må drikke", // 4
                 "Tommelen", // 5
                 "Alle gutter må drikke", // 6
                 "Pek på himmelen", // 7
                 "Drikkepartner", // 8
                 "Rim", // 9
                 "Kategori", // 10
                 "Lag en regel", // Jack
                 "Spørsmålsrunde", // Queen
                 "Hell drikke i koppen", // King
                 "Fossefall"] // Ace

And this is how I pick a random card:

func imageTapped(img: AnyObject){
        if(cardsleftLabel.text != "0") {

            let randomNumber = Int(arc4random_uniform(UInt32(files.count)))
            let image = picture[randomNumber]

            cardImage.image = image
            name = files[randomNumber]
        }
        else{
            print("No more cards")
        }
    }

The problem is that the card may appear many times, and that is wrong. There are 4 of each card, so how can I control that in my game? So the CardJack don't appear 6 times?

Paulw11
  • 108,386
  • 14
  • 159
  • 186
Roduck Nickes
  • 1,021
  • 2
  • 15
  • 41

4 Answers4

5

One way to do it is to generate an array of indices that represent your cards. Shuffle that array, and then remove the indices from that array as you draw a card.

// generate random list of indices from 0...12 four each
var cardIndices = (0...51).map {($0 % 13, arc4random())}.sort{$0.1 < $1.1}.map{$0.0}

// To get a card, remove last card from deck    
let last = cardIndices.removeLast()

// use the index to look up the picture
let randomCard = picture[last]

// It's also easy to check how many cards you have left in your deck
let remaining = cardIndices.count

This works by first creating an array of tuples which contain a number from 0...12 and a some random integer. Then that array is sorted by the random integer element in the tuple, and then map is used to separate out just the array of indices leaving you with a random array of Int with values from 0...12 (four values of each).


Here it is in class form.

import UIKit

struct Card {
    let image: UIImage
    let text:  String
}

class Deck {
    private let cards:[Card] = [
        Card(image: UIImage(named: "Card2")!, text: "Velg en som må drikke"),
        Card(image: UIImage(named: "Card3")!, text: "Drikk selv"),
        Card(image: UIImage(named: "Card4")!, text: "Alle jenter må drikke"),
        Card(image: UIImage(named: "Card5")!, text: "Tommelen"),
        Card(image: UIImage(named: "Card6")!, text: "Alle gutter må drikke"),
        Card(image: UIImage(named: "Card7")!, text: "Pek på himmelen"),
        Card(image: UIImage(named: "Card8")!, text: "Drikkepartner"),
        Card(image: UIImage(named: "Card9")!, text: "Rim"),
        Card(image: UIImage(named: "Card10")!, text: "Kategori"),
        Card(image: UIImage(named: "CardJack")!, text: "Lag en regel"),
        Card(image: UIImage(named: "CardQueen")!, text: "Spørsmålsrunde"),
        Card(image: UIImage(named: "CardKing")!, text: "Hell drikke i koppen"),
        Card(image: UIImage(named: "CardAce")!, text: "Fossefall")
    ]

    private var cardIndices = [Int]()

    var cardsInDeck: Int { return cardIndices.count }

    func shuffleCards() {
        cardIndices = (0...51).map{($0 % 13, arc4random())}.sort{$0.1 < $1.1}.map{$0.0}
    }

    func drawCard() -> Card {
        if cardIndices.count == 0 {
            shuffleCards()
        }

        let last = cardIndices.removeLast()

        return cards[last]
    }
}

Notes:

  • The cards and cardIndices have been made private to hide those details from a user of Deck.
  • Thanks to @Paulw11's suggestion, this solution now uses a struct to represent a card. This keeps the data together and provides a nice value that can be returned from drawCard.
  • The user of a Deck can create a Deck with Deck(), they can call shuffleCards() to randomize the deck, check the cardsInDeck property to find out how many shuffled cards are available, and they can call drawCard() to get the next card from the deck.

How to Use

For the viewController that controls the deck, add a property to the viewController:

class MyGame: UIViewController {
    var deck = Deck()

    // the rest of the code
 }

Then when you need a card, for example inside of an @IBAction for a button, just call deck.drawCard:

@IBAction func turnOverNextCard(button: UIButton) {
    let card = deck.drawCard()

    // Use the image and text to update the UI
    topCardImageView.image = card.image
    topCardLabel.text  = card.text

    // I'm not going to wait for the deck to shuffle itself
    if deck.cardsInDeck < 10 {
        deck.shuffleCards()
    }
}

Splitting Hairs: A Better Shuffle

My shuffle routine shuffles the deck by associating a random UInt32 with each card and then sorting the deck by those values. If the same random number is generated for two cards, then it is possible that earlier cards in the deck would be favored over later cards (or vice versa depending on the sort algorithm). This is really splitting hairs, but in the interest of providing the best shuffle possible, I provide the following alternative:

func shuffleCards() {
    cardIndices = (0...51).map {$0 % 13}
    for i in (1...51).reverse() {
        let rand = Int(arc4random_uniform(UInt32(i + 1)))
        (cardIndices[i], cardIndices[rand]) = (cardIndices[rand], cardIndices[i])
    }
}

This algorithm is based upon the Fisher-Yates shuffle.

vacawama
  • 150,663
  • 30
  • 266
  • 294
  • This did not quite work.. I get the same card more then 4 times. – Roduck Nickes Jun 22 '16 at 23:32
  • Did you only generate the cardIndices once? You should do that and then draw cards from cardIndices until it is empty. – vacawama Jun 22 '16 at 23:33
  • Oh, I added the whole code into a button action that set new card. – Roduck Nickes Jun 22 '16 at 23:37
  • Yeah, that would shuffle the deck each card draw. The first line is the shuffle. You only want to do that when your deck is empty. You should check `if cardIndices.isEmpty { cardIndices = (0...51).map ...}` before drawing a card. I'd put the first line in a function called `shuffleDeck`. – vacawama Jun 22 '16 at 23:39
  • I see I forgot to say that each card has text to it. Can you please take a look at my updated question and add it in your code aswell? :) – Roduck Nickes Jun 22 '16 at 23:58
  • Further to both of the answers, you should create a class or struct to represent a Card as well, rather that just using arrays of images – Paulw11 Jun 23 '16 at 00:11
  • @Paulw11 and RoduckNickes, I updated the answer to include a `struct` to represent a card and I added the card text to the card. The internal representation of the deck remains an array of `Int` because that is efficient and just an implementation detail. – vacawama Jun 23 '16 at 00:35
  • Storing a reference to a Card and storing a reference to an Int has no difference in the size of the array – Paulw11 Jun 23 '16 at 01:17
  • @Paulw11, sure they're the same size, but I can eyeball a random array of 52 `Int`s easier than a random array of 52 `Card`s and see that they appear randomized and that there are 4 of each. Again, it's just an implementation detail that is hidden from the user anyway. – vacawama Jun 23 '16 at 01:32
  • Ok, so I added the whole `Card.swift` and added the "A Better Shuffle", and this is my code each time i tap a card: http://pastebin.com/MF9Ce4hK. Everytime it shows up this: https://s31.postimg.org/6v5jc2vjf/Screen_Shot_2016_06_23_at_13_43_39.png - And also, I found the CardKing only 2 times when going through the whole deck of cards.. What did I do wrong? – Roduck Nickes Jun 23 '16 at 11:45
  • I think you took my *Usage Example* a little to literally. The loop of 100 times and the print were just to show what happens when you draw 100 cards. To use this in your game, forget the example usage code. Add a property to your viewController `var deck = Deck()` and then just call `deck.drawCard()` when the button is tapped. – vacawama Jun 23 '16 at 11:54
  • Make sure `deck` is a property of your `viewController`. Don't declare deck inside of your button routine, or you'll get a new deck (with a new shuffle) every time your button is pressed. – vacawama Jun 23 '16 at 11:57
  • Check out my updated answer with the **How to Use** section. – vacawama Jun 23 '16 at 12:14
  • @vacawama I used your **How to Use** now, and i counted how many times I get the King Card within 52 cards. First attempt: 3 times, Second attempt: 3 times, and the Third attempt: 5 times. So it still does not work exactly. :( This is my code now: http://pastebin.com/dQK1cr0R – Roduck Nickes Jun 23 '16 at 12:30
  • Sounds like you are out of sync with the deck. Since the code shuffles the deck if you run out, 52 cards in a row might not be from the same deck. Make sure you call `shuffleCards` and then draw 52 cards. – vacawama Jun 23 '16 at 12:36
  • @vacawama - I added a `println("Shuffle")` into the `if deck.cardsInDeck < 10 {` line, and it got printed once. – Roduck Nickes Jun 23 '16 at 12:38
  • Remove the `if deck.cardsInDeck < 10 { deck.shuffleCards() }`. Again, that is just a demonstration that you don't have to wait to use up all 52 cards before shuffling. – vacawama Jun 23 '16 at 12:39
  • @vacawama You mean I should only use the `deck.shuffleCards()` without the `if` statement? That did the same thing, sometimes i got only 3 kings, and sometimes even 6 kings.. – Roduck Nickes Jun 23 '16 at 12:40
  • No. Take it out altogether. If you do it like you just did, you'll shuffle every card draw. – vacawama Jun 23 '16 at 12:42
  • @vacawama Tried a few time now, works perfect! Thanks :) If you have time, could you also look at this: http://stackoverflow.com/questions/37968762/spin-bottle-with-uigesturerecognizer/ - Accepting your answer :) – Roduck Nickes Jun 23 '16 at 12:45
1

You need a class to represent the Deck of cards, like this.

class Deck {

    static let seeds = 4

    var images : [UIImage:Int] = [
        UIImage(named: "Card2")! : seeds,
        UIImage(named: "Card3")! : seeds,
        UIImage(named: "Card4")! : seeds,
        UIImage(named: "Card5")! : seeds,
        UIImage(named: "Card6")! : seeds,
        UIImage(named: "Card7")! : seeds,
        UIImage(named: "Card8")! : seeds,
        UIImage(named: "Card9")! : seeds,
        UIImage(named: "Card10")! : seeds,
        UIImage(named: "CardJack")! : seeds,
        UIImage(named: "CardQueen")! : seeds,
        UIImage(named: "CardKing")! : seeds,
        UIImage(named: "CardAce")! : seeds
    ]

    func extractRandomCard() -> UIImage? {
        let flatten = images.reduce([UIImage]()) { [UIImage](count: $0.1.1, repeatedValue: $0.1.0) }
        guard !flatten.isEmpty else { return nil }
        let random = Int(arc4random_uniform(UInt32(flatten.count)))
        let selectedCard = flatten[random]
        images[selectedCard] = images[selectedCard]! - 1
        return selectedCard
    }
}

Now you can extract cards from the deck writing

let deck = Deck()
let image = deck.extractRandomCard()

Each time you extract a card the Deck keeps track of it and it won't let you extract more than 4 times the same card.

I didn't test it... but it should work

Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
0

An alternative approach to the random-stuff is to use some of Apple's nice GamePlayKit-stuff created exactly for these kind of use-cases. If you have the cards array you could for instance just do:

let shuffled2 = GKRandomSource().arrayByShufflingObjectsInArray(cards) as! [Card]

or you could keep the array unshuffled and instead shuffle the indexes you request:

let indexes = GKShuffledDistribution.init(forDieWithSideCount: 52)
// going through all the cards randomly
for _ in cards {
    let card = cards[indexes.nextInt()]
}
T. Benjamin Larsen
  • 6,373
  • 4
  • 22
  • 32
0
public enum Palo: Int
{
    case Corazones = 1
    case Treboles = 2
    case Picas = 3
    case Diamantes = 4
}

public struct Card
{
    public var text: String
    public var position: Int
    public var palo: Palo

    public func description() -> String
    {
        return "\(self.position) of \(self.palo) -- \(self.text)"
    }
}

public struct Deck
{
    public var cards: [Card]

    public init()
    {
        cards = [Card]()

        for number in 1...13
        {
            for palo in 1...4
            {
                let card: Card = Card(text: "", position: number, palo: Palo(rawValue: palo)!)

                cards.append(card)
            }
        }
    }

    /**
        Return cards one by one
    */
    public mutating func randomCard() -> Card?
    {
        guard !self.cards.isEmpty else
        {
            return nil
        }

        let position: Int = Int(arc4random_uniform(UInt32(self.cards.count)))   

        let card: Card = self.cards.removeAtIndex(position)

        return card
    }
}

//
// TEST
//

var deck: Deck = Deck()

for index in 1...200
{
    if let card = deck.randomCard()
    {
        print("\(index) -- \(card.description())")
    }
}
Adolfo
  • 1,862
  • 13
  • 19