Building a memory card game

while learning about classes and protocols in Swift

We’re going to build a simple memory card game I find very straightforward and fun to play, and hopefully we can learn a little bit about classes and swift’s protocols in the process. But before jumping into the fun part (the coding part 😎) let’s look at how to play this game:

How to play Memory Card

Our memory card game has a grid of 4x4 cells and initially all cells are shown, but their images are hidden. When you tap on a cell, it reveals the image behind it.

The player’s task is to find the matching cell. If the next cell they tap on contains the same image, then it’s a match and the two cells remain visible. If the next cell does not contain the same image, then it’s a miss and both cells go back to the hidden state. The player continues until they find all matching cells.

Straightforward and fun!

Project Structure

In order to build a simple version of this game we’re going to need the following components:

  • One Controller: GameController.swift
  • One View: CardCell.swift
  • Two Models: MemoryGame.swift and Card.swift
  • And the Main.storyboard to implement our game’s grid view.

Let’s start with the simplest component of this game, a card.

Card.swift

Our card model will have the following three properties: an id to identify each card, a bool variable shown to know the status of the card (if it’s being shown or hidden), and an artworkURL for the card images.

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

We will also need to implement the following methods to manage the interactions we will have with a card:

  1. A method to initialize a card with an image. Here’s where we are going to initialize all the properties to their default vals. For the card’s id we’re generating a random id by calling NSUUIS().uuidString
init(image: UIImage) {        
self.id = NSUUID().uuidString
self.shown = false
self.artworkURL = image
}

2. A method to check if two cards are equal by comparing their ids.

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

3. A method to create a copy of each card in order to have an even number of matching cards. This method will return another Card instance with the same values.

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

4. And a method to shuffle the cards randomly every time we start a new game. We’ll make it an extension of the Array class so we can call it from our array of cards in our game’s grid, which we’ll discuss in a bit.

extension Array {    
mutating func shuffle() {
for _ in 0...self.count {
sort { (_,_) in arc4random() < arc4random() }
}
}
}

Here’s the final implementation for our Card model with its properties and methods.

¿Bien hasta aquí? … OK!

Our second model is MemoryGame, and here we’ll define our game’s 4x4 grid. It’ll have the following properties: an array of all the cards on the grid: cards, an array with the cards that are being shown: cardsShown, and a bool variable:isPlaying, to keep track of the status of the game.

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

We will also need to implement some methods to manage the interactions we will have with the grid, which are going to be:

  1. A method to shuffle the cards in our grid.
func shuffleCards(cards:[Card]) -> [Card] {        
var randomCards = cards
randomCards.shuffle()
    return randomCards    
}
  1. A method to create a new game. Here we need to call our shuffle method to initialize our set of cards, and initialize our isPlaying variable to true.
func newGame(cardsArray:[Card]) -> [Card] {        
cards = shuffleCards(cards: cardsArray)
isPlaying = true
    return cards    
}

2. If we want to restart the game, we need to set our isPlaying variable to false, and erase our array of cards so we can start over a new game.

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

3. A method to know which card was pressed. We are going to use this index to identify each cell in the collectionView (the game’s grid). More on this later.

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

4. A method that will return 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
}

5. The unmatchedCardShown method indicates if the card selected is unmatched (the first one selected in the current turn).

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

6. The unmatchedCard method reads the last element in **cardsShown** array, and returns an unmatched card.

func unmatchedCard() -> Card? {
let unmatchedCard = cardsShown.last

return unmatchedCard
}

7. A method that will get called when a player taps on a 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()
}
}

¿bien? … OK! That seems like a lot of code but it really is straightforward. We’re just defining the two main classes that we need for our game: a card and a grid. Now, let’s move to our storyboard and controller.

Main storyboard & GameController.swift

Our Main.storyboard will look like this:

Initially, in the controller, we’ll need to set a new game in the viewDidLoad, including getting the images for our grid. In our game, that’ll consist of a 4X4 collectionView. If you’re not familiar with collection views, here’s a good start.

We’ll set up the GameController as the root view controller of our single view app, and in our GameController we’ll have a collectionView that we’re going to reference as a IBOutlet. Here we are also going to reference a IBAction button onStartGame(), which is the UIButton you can see in our storyboard titled PLAY.

Some hightlights in our controller’s implementation:

  1. We’re first initializing our two main objects, the grid: game = MemoryGame(), and our set of cards: cards = [Card]().
  2. We’re setting the game’s initial vars in the viewDidLoad, which is the first method that gets called once our app is running.
  3. The collectionView is initially set to hidden, so the are no cards visible in the screen until the user press PLAY.
  4. Once the user press PLAY, the onStartGame IBAction gets called, and we set our collectionView’s isHidden property to false so it becomes visible.
  5. Every time the user selects an item, the collectionView’s method didSelectItemAt: gets called. Inside this method we’re calling our own didSelectCard: to pass on the main logic of the game.

Here’s the final implementation for our GameController:

Let’s stop at CollectionView’s Delegate Methods for a moment, or protocols for that matter.

Protocols

Working with protocols is one of swift’s most fundamental programming patterns. With protocols you can define rules for a class, structure or enumerator to adopt and implement. This principle allows us to write decoupled, modular and extensible swift code. In fact, that’s the pattern we’re already implementing for the collectionView in the GameController. And now we’re going to make our own. The syntax will look like this:

protocol MemoryGameProtocol {
//protocol definition goes here
}

Since we already know that in the protocol is where we’ll define the rules or instructions for a class to implement, we can now think of what the instructions for our game should be. We basically need to know 4 things:

  1. When to start the game : memoryGameDidStart
  2. When to show a card downside up: memoryGameShowCards
  3. When to show a card upside down: memoryGameHideCards
  4. When to end the game: memoryGameDidEnd

Once we’ve declared our protocol, we need to implement each four methods in our main class, in this case, our main controller: GameController.

memoryGameDidStart

When this method gets triggered, it means that the game should start (in our game, this happens when the player taps on the play button). Here we will simply reload the content by calling collectionView.reloadData(), being now a set of shuffled cards.

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

memoryGameShowCards

We’re calling this method from the collectionView’s didSelectItemAt:. First, it shows the selected card. Then it checks if we have an unmatched card stored in our cardsShown array (if the cardsShown count is odd). If we do have an unmatched card, we then compare the card selected with the unmatched card. If they’re equal, both cards are appended to cardsShown and they remain visible. If they’re not equal, we remove the unmatched card from the cardsShown list and hide both cards.

memoryGameHideCards

If cards aren’t equal, this method gets called so they return to a hidden state, shown = false.

memoryGameDidEnd

When this method gets called, it means that all the cards are visible, that we have moved all the initial cards to our cardsShown list: cardsShown.count = cards.count, so the game is finished. This method gets called specifically after we called endGame() to set the isPlaying var to false and from here we then display an alert, letting the user know that the game has finished, and also using that alertController as an indicator for our controller. When the alert gets dismissed, viewDidDisappear gets called and it resets the game so the user can play again.

These four methods will look like this in our 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 was indeed a lot of code. Here’s the fully-functional project so you can clone it and play around with it.

Happy coding! ✌🏾