[Перевод] Пишем игру «Карточки памяти» на Swift

bd8736e74ae382f71c8c0c8220a5b4be.jpg

В этой статье описывается процесс создания простой игры для тренировки памяти, которая мне очень нравится. Кроме того, что она сама по себе хороша, во время работы вы немного больше узнаете о классах и протоколах Swift. Но прежде чем начать, давайте разберемся в самой игре.

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Skillbox рекомендует: Образовательный онлайн-курс «Профессия Java-разработчик».


Как играть в Memory Card


Игра начинается с демонстрации набора карточек. Они лежат «рубашкой» вверх (соответственно, изображением вниз). Когда вы кликаете по любой, на несколько секунд открывается изображение.

Задача игрока — найти все карточки с одинаковыми картинками. Если после открытия первой карты вы переворачиваете вторую и картинки совпадают, обе карточки остаются открытыми. Если не совпадают, карточки снова закрываются. Задача — открыть все.

Структура проекта


Для того, чтобы создать простую версию этой игры нужны следующие компоненты:

  • Один контроллер (One Controller): GameController.swift.
  • Один просмотр (One View): CardCell.swift.
  • Две модели (Two Models): MemoryGame.swift and Card.swift.
  • Main.storyboard для того, чтобы весь набор компонентов был в наличии.


Начинаем с самого простого компонента игры, карточки.

Card.swift

У модели карточки будет три свойства: id для идентификации каждой, логическая переменная shown для уточнения статуса карты (скрыта или открыта) и artworkURL для картинок на карточках.

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


Также будут нужны эти методы для управления взаимодействием пользователя с картами:

Метод для вывода изображения на карту. Здесь мы сбрасываем все свойства на дефолтные. Для id генерируем случайный id путем вызова NSUUIS ().uuidString.

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


Метод для сравнения id карт.

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


Метод для создания копии каждой карточки — для того, чтобы получить большее число одинаковых. Этот метод будет возвращать card с аналогичными значениями.

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


И еще один метод нужен для перемешивания карточек на старте. Мы сделаем его расширением класса Array.

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


А вот реализация кода для модели Card со всеми свойствами и методами.

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() }
        }
    }
}


Идем дальше.

Вторая модель — MemoryGame, здесь задаем сетку 4×4. У модели будут такие свойства, как cards (массив карточек на сетке), массив cardsShown с уже открытыми карточками и логическая переменная isPlaying для отслеживания статуса игры.

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


Нам также нужно разработать методы для управления взаимодействия пользователя с сеткой.

Метод, который перемешивает карточки в сетке.

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


Метод для создания новой игры. Здесь мы вызываем первый метод для старта начальной раскладки и инициализируем переменную isPlaying как true.

func newGame(cardsArray:[Card]) -> [Card] {       
    cards = shuffleCards(cards: cardsArray)        
    isPlaying = true            
 
    return cards    
}


Если мы хотим перезапустить игру, то устанавливаем переменную isPlaying как false и убираем первоначальную раскладку карточек.

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


Метод для верификации нажатых карточек. Подробнее о нем позже.

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


Метод, возвращающий позицию определенной карточки.

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


Проверка соответствия выбранной карточке эталону.

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


Этот метод читает последний элемент в массиве **cardsShown** и возвращает несоответствующую карточку.

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 и GameController.swift


Main.storyboard выглядит примерно так:

560fdba5bf68f57bd0b8741b0b91cd0f.png

Изначально в контроллере нужно установить новую игру как viewDidLoad, включая изображения для сетки. В игре все это будет представлено 4×4 collectionView. Если вы еще не знакомы с collectionView, вот здесь можно получить нужную информацию.

Мы настроим GameController в качестве корневого контроллера приложения. В GameController будет collectionView, на который мы будем ссылаться в качестве IBOutlet. Еще одна ссылка — на кнопку IBAction onStartGame (), это UIButton, ее вы можете увидеть в раскадровке под названием PLAY.

Немного о реализации контроллеров:

  • Сначала инициализируем два главных объекта — сетку (the grid): game = MemoryGame (), и на набор карточек: cards = [Card]().
  • Устанавливаем начальные переменные как viewDidLoad, это первый метод, который вызывается в процессе работы игры.
  • collectionView устанавливаем как hidden, поскольку все карты скрыты до того момента, пока пользователь не нажмет PLAY.
  • Как только нажимаем PLAY, стартует раздел onStartGame IBAction, и мы выставляем свойство collectionView isHidden как false, чтобы карточки могли стать видимыми.
  • Каждый раз, когда пользователь выбирает карточку, вызывается метод didSelectItemAt. В методе мы вызываем didSelectCard для реализации основной логики игры.


Вот финальная реализация 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)
    }
}


Теперь давайте немного остановимся на важных протоколах.

Протоколы


Работа с протоколами — основа основ программирования на Swift. Протоколы дают возможность задать правила для класса, структуры или перечисления. Этот принцип позволяет писать модульный и расширяемый код. Фактически это шаблон, который мы уже реализуем для collectionView в GameController. Теперь сделаем собственный вариант. Синтаксис будет выглядеть так:

protocol MemoryGameProtocol {
    //protocol definition goes here
}


Мы знаем, что протокол позволяет определить правила или инструкции для реализации класса, поэтому давайте подумаем, какими они должны быть. Всего нужно четыре.

  • Начало игры: memoryGameDidStart.
  • Нужно перевернуть карточку рубашкой вниз: memoryGameShowCards.
  • Нужно перевернуть карточку рубашкой вверх: memoryGameHideCards.
  • Завершение игры: memoryGameDidEnd.


Все четыре метода реализуем для основного класса, а это GameController.

memoryGameDidStart


Когда этот метод запущен, игра должна начаться (пользователь нажимает PLAY). Здесь просто перезагрузим контент, вызвав collectionView.reloadData (), что приведет к перемешиванию карт.

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


memoryGameShowCards


Вызываем этот метод из collectionSDViewSelectItemAt. Сначала он показывает выбранную карту. Затем проверяет, есть ли в массиве cardsShown несопоставленная карта (если число cardsShown нечетное). Если такая есть, выбранная карта сравнивается с ней. Если картинки одинаковые, обе карты добавляются к cardsShown и остаются открытыми. Если разные, карта уходит из cardsShown, и обе переворачиваются рубашкой вверх.

memoryGameHideCards


Если карты не соответствуют друг другу, вызывается этот метод, и картинки карточек скрываются.

shown = false.

memoryGameDidEnd


Когда вызывается этот метод, означает, что все карты уже открыты и находятся в списке cardsShown: cardsShown.count = cards.count, так что игра окончена. Метод вызывается специально после того, как мы вызвали endGame (), чтобы установить isPlaying var в false, после чего показывается сообщение о завершении игры. Также alertController используется в качестве индикатора для контроллера. Вызывается viewDidDisappear и игра сбрасывается.

Вот как все это выглядит в 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()
    }
}


1fdb6b1e7b62a4d49397b1e2d590d57b.gif
Собственно, вот и все. Вы можете использовать этот проект для создания собственного варианта игры.

Удачного кодинга!

Skillbox рекомендует:

© Habrahabr.ru