Roduck Nickes Roduck Nickes - 5 months ago 33
Swift Question

IOS Random Card Game - Ring of Fire

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?

Answer

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.

Comments