Writing a Memory Card Game on Swift

Original author: Xiomara Figueroa
  • Transfer


This article describes the process of creating a simple memory training game that I really like. Besides being good in and of itself, you’ll learn a bit more about Swift classes and protocols during work. But before you start, let's figure out the game itself.

We remind you: for all readers of “Habr” - a discount of 10,000 rubles when registering for any Skillbox course using the “Habr” promo code.

Skillbox recommends: The on-line educational course "Profession Java-developer" .

How to play Memory Card


The game begins with a demonstration of a set of cards. They lie “shirt” up (respectively, face down). When you click on any, an image opens for a few seconds.

The player’s task is to find all cards with the same pictures. If after opening the first card you turn over the second and the pictures match, both cards remain open. If they do not match, the cards are closed again. The task is to open everything.

Project structure


In order to create a simple version of this game you need the following components:

  • One Controller: GameController.swift.
  • One View: CardCell.swift.
  • Two Models: MemoryGame.swift and Card.swift.
  • Main.storyboard so that the entire set of components is available.

We start with the simplest component of the game, the card.

Card.swift

The card model will have three properties: id for identifying each, a logical variable shown to specify the status of the card (hidden or open) and artworkURL for pictures on cards.

class Card {        
    var id: String    
    var shown: Bool = false    
    var artworkURL: UIImage!
}

You will also need these methods to control the user’s interaction with the cards:

Method for displaying the image on the card. Here we reset all properties to default. For id, generate a random id by calling NSUUIS (). UuidString.

init(image: UIImage) {        
    self.id = NSUUID().uuidString        
    self.shown = false        
    self.artworkURL = image    
}

Method for comparing id cards.

func equals(_ card: Card) -> Bool {
    return (card.id == id)    
}

The method for creating a copy of each card is to get a larger number of identical ones. This method will return card with similar values.

func copy() -> Card {        
    return Card(card: self)    
}
init(card: Card) {        
    self.id = card.id        
    self.shown = card.shown        
    self.artworkURL = card.artworkURL    
}

And another method is needed to mix cards at the start. We will make it an extension of the Array class.
extension Array {    
    mutating func shuffle() {        
        for _ in 0...self.count {            
            sort { (_,_) in arc4random() < arc4random() }        
        }   
    }
}

And here is the code implementation for the Card model with all the properties and methods.

class Card {
    var id: String
    var shown: Bool = false
    var artworkURL: UIImage!
    static var allCards = [Card]()
    init(card: Card) {
        self.id = card.id
        self.shown = card.shown
        self.artworkURL = card.artworkURL
    }
    init(image: UIImage) {
        self.id = NSUUID().uuidString
        self.shown = false
        self.artworkURL = image
    }
    func equals(_ card: Card) -> Bool {
        return (card.id == id)
    }
    func copy() -> Card {
        return Card(card: self)
    }
}
extension Array {
    mutating func shuffle() {
        for _ in 0...self.count {
            sort { (_,_) in arc4random() < arc4random() }
        }
    }
}

Move on.

The second model is MemoryGame, here we set the 4 * 4 grid. The model will have such properties as cards (an array of cards on the grid), an cardsShown array with cards already open, and the isPlaying boolean to track the status of the game.

class MemoryGame {        
    var cards:[Card] = [Card]()    
    var cardsShown:[Card] = [Card]()    
    var isPlaying: Bool = false
}

We also need to develop methods to control user interaction with the grid.

A method that shuffles cards in a grid.

func shuffleCards(cards:[Card]) -> [Card] {       
    var randomCards = cards        
    randomCards.shuffle()                
    return randomCards    
}

Method for creating a new game. Here we call the first method to start the initial layout and initialize the variable isPlaying as true.
func newGame(cardsArray:[Card]) -> [Card] {       
    cards = shuffleCards(cards: cardsArray)        
    isPlaying = true            
    return cards    
}

If we want to restart the game, then set the isPlaying variable to false and remove the initial layout of the cards.

func restartGame() {        
    isPlaying = false                
    cards.removeAll()        
    cardsShown.removeAll()    
}

Method for verification of pressed cards. More about it later.

func cardAtIndex(_ index: Int) -> Card? {        
    if cards.count > index {            
        return cards[index]        
    } else {            
        return nil        
    }    
}

A method that returns the position of a specific card.

func indexForCard(_ card: Card) -> Int? {        
    for index in 0...cards.count-1 {            
        if card === cards[index] {                
            return index            
        }      
    }
    return nil    
}

Checking the compliance of the selected card with the standard.

func unmatchedCardShown() -> Bool {
    return cardsShown.count % 2 != 0
}

This method reads the last element in the ** cardsShown ** array and returns an inappropriate card.

func didSelectCard(_ card: Card?) {        
    guard let card = card else { return }                
    if unmatchedCardShown() {            
        let unmatched = unmatchedCard()!                        
        if card.equals(unmatched) {          
            cardsShown.append(card)            
        } else {                
            let secondCard = cardsShown.removeLast()      
        }                    
    } else {            
        cardsShown.append(card)        
    }                
    if cardsShown.count == cards.count {            
        endGame()        
    }    
}

Main.storyboard and GameController.swift


Main.storyboard looks something like this:



Initially, in the controller you need to install a new game as viewDidLoad, including images for the grid. In the game, all this will be represented by 4 * 4 collectionView. If you are not familiar with collectionView, here you can get the necessary information .

We will configure GameController as the root controller of the application. There will be a collectionView in GameController, which we will reference as an IBOutlet. Another link is to the IBAction onStartGame () button, this is UIButton, you can see it in the storyboard called PLAY.

A little bit about the implementation of controllers:

  • First, we initialize the two main objects - the grid (game): game = MemoryGame (), and on the set of cards: cards = [Card] ().
  • Set the initial variables as viewDidLoad, this is the first method that is called during the game.
  • set collectionView as hidden, since all maps are hidden until the user presses PLAY.
  • As soon as we press PLAY, the onStartGame IBAction section starts, and we set the collectionView isHidden property to false so that the cards can become visible.
  • Each time a user selects a card, the didSelectItemAt method is called. In the method, we call didSelectCard to implement the basic logic of the game.

Here is the final implementation of GameController:

class GameController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    let game = MemoryGame()
    var cards = [Card]()
    override func viewDidLoad() {
        super.viewDidLoad()
        game.delegate = self
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.isHidden = true
        APIClient.shared.getCardImages { (cardsArray, error) in
            if let _ = error {
                // show alert
            }
            self.cards = cardsArray!
            self.setupNewGame()
        }
    }
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        if game.isPlaying {
            resetGame()
        }
    }
    func setupNewGame() {
        cards = game.newGame(cardsArray: self.cards)
        collectionView.reloadData()
    }
    func resetGame() {
        game.restartGame()
        setupNewGame()
    }
    @IBAction func onStartGame(_ sender: Any) {
        collectionView.isHidden = false
    }
}
// MARK: - CollectionView Delegate Methods
extension GameController: UICollectionViewDelegate, UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return cards.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CardCell", for: indexPath) as! CardCell
        cell.showCard(false, animted: false)
        guard let card = game.cardAtIndex(indexPath.item) else { return cell }
        cell.card = card
        return cell
    }
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let cell = collectionView.cellForItem(at: indexPath) as! CardCell
        if cell.shown { return }
        game.didSelectCard(cell.card)
        collectionView.deselectItem(at: indexPath, animated:true)
    }
}

Now let's dwell on some important protocols.

Protocols


Working with protocols is the foundation of Swift programming. Protocols provide the ability to set rules for a class, structure, or enumeration. This principle allows you to write modular and extensible code. This is actually a template that we are already implementing for collectionView in GameController. Now let's make our own version. The syntax will look like this:

protocol MemoryGameProtocol {
    //protocol definition goes here
}

We know that a protocol allows you to define rules or instructions for implementing a class, so let's think about what they should be. Only four are needed.

  • Start of the game: memoryGameDidStart.
  • Need to flip the card face down: memoryGameShowCards.
  • Need to flip the card face down: memoryGameHideCards.
  • Game completion: memoryGameDidEnd.

We implement all four methods for the main class, and this is GameController.

memoryGameDidStart


When this method is launched, the game should begin (user presses PLAY). Here, we simply reload the content by calling collectionView.reloadData (), which will cause the maps to shuffle.

func memoryGameDidStart(_ game: MemoryGame) {
    collectionView.reloadData()
}

memoryGameShowCards


Call this method from collectionSDViewSelectItemAt. First, it shows the selected map. Then it checks to see if there is an unmatched card in the cardsShown array (if the number of cardsShown is odd). If there is one, the selected card is compared with it. If the pictures are the same, both cards are added to cardsShown and remain open. If different, the card leaves cardsShown, and both flip upside down.

memoryGameHideCards


If the cards do not match, this method is called and the card images are hidden.

shown = false.

memoryGameDidEnd


When this method is called, it means that all cards are already open and are in the cardsShown list: cardsShown.count = cards.count, so the game is over. The method is called specifically after we called endGame () to set isPlaying var to false, after which a message about the completion of the game is displayed. AlertController is also used as an indicator for the controller. ViewDidDisappear is called and the game is reset.

Here's how it all looks in GameController:

extension GameController: MemoryGameProtocol {
    func memoryGameDidStart(_ game: MemoryGame) {
        collectionView.reloadData()
    }
    func memoryGame(_ game: MemoryGame, showCards cards: [Card]) {
        for card in cards {
            guard let index = game.indexForCard(card)
                else { continue
            }        
            let cell = collectionView.cellForItem(
                at: IndexPath(item: index, section:0)
            ) as! CardCell
            cell.showCard(true, animted: true)
        }
    }
    func memoryGame(_ game: MemoryGame, hideCards cards: [Card]) {
        for card in cards {
            guard let index = game.indexForCard(card)
                else { continue
            }
            let cell = collectionView.cellForItem(
                at: IndexPath(item: index, section:0)
            ) as! CardCell
            cell.showCard(false, animted: true)
        }
    }
    func memoryGameDidEnd(_ game: MemoryGame) {
        let alertController = UIAlertController(
            title: defaultAlertTitle,
            message: defaultAlertMessage,
            preferredStyle: .alert
        )
        let cancelAction = UIAlertAction(
            title: "Nah", style: .cancel) {
            [weak self] (action) in
            self?.collectionView.isHidden = true
        }
        let playAgainAction = UIAlertAction(
            title: "Dale!", style: .default) {
            [weak self] (action) in
            self?.collectionView.isHidden = true
            self?.resetGame()
        }
        alertController.addAction(cancelAction)
        alertController.addAction(playAgainAction)
        self.present(alertController, animated: true) { }
        resetGame()
    }
}


That's all. You can use this project to create your own version of the game.

Good coding!

Skillbox recommends:


Also popular now: