Пишемо гру «Картки пам'яті» на Swift

Пишемо гру «Картки пам'яті» на Swift

Ця стаття описує процес створення простої гри для тренування пам'яті, яка мені дуже подобається. Крім того, що вона сама по собі хороша, під час роботи ви трохи більше дізнаєтеся про класи та протоколи Swift. Але перш ніж почати, давайте розберемося у самій грі.

Нагадуємо: для всіх читачів "Хабра" - знижка 10 000 рублів при записі на будь-який курс Skillbox за промокодом "Хабр".

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

Як грати в Memory Card

Гра починається з демонстрації набору карток. Вони лежать "сорочкою" вгору (відповідно зображенням вниз). Коли ви клацаєте по будь-якій, на кілька секунд відкривається зображення.

Завдання гравця – знайти всі картки з однаковими картинками. Якщо після відкриття першої карти ви перевертаєте другу та картинки збігаються, обидві картки залишаються відкритими. Якщо не збігаються, картки знову закриваються. Завдання – відкрити все.

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

Для того, щоб створити просту версію цієї гри, потрібні наступні компоненти:

  • Один контролер (One Controller): GameController.swift.
  • Один перегляд (One View): CardCell.swift.
  • Дві моделі (Two Models): MemoryGame.swift та 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)    
}

Метод створення копії кожної картки - Для того, щоб отримати більшу кількість однакових. Цей метод буде повертати картку з аналогічними значеннями.

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 виглядає приблизно так:

Пишемо гру «Картки пам'яті» на Swift

Спочатку в контролері потрібно встановити нову гру як viewDidLoad, включаючи зображення для сітки. У грі все це буде представлено 4*4 collectionView. Якщо ви ще не знайомі з collectionView, ось тут можна отримати потрібну інформацію.

Ми налаштуємо GameController як кореневий контролер програми. У GameController буде collectionView, на який ми будемо посилатися як IBOutlet. Ще одне посилання - на кнопку IBAction onStartGame(), це UIButton, її ви можете побачити в розкадруванні під назвою PLAY.

Трохи про реалізацію контролерів:

  • Спочатку ініціалізуємо два основних об'єкта - сітку (the grid): game = MemoryGame (), і на набір карток: cards = [Card] ().
  • Встановлюємо початкові змінні як viewDidLoad, це перший метод, який викликається в процесі роботи гри.
  • collectionView встановлюємо як hidden, оскільки всі карти приховані доти, доки користувач не натисне PLAY.
  • Як тільки натискаємо PLAY, стартує розділ наStartGame 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 непарне). Якщо така є, вибрана карта порівнюється із нею. Якщо картинки однакові, обидві картки додаються до картокзображення і залишаються відкритими. Якщо різні, картка йде з карток, і обидві перевертаються сорочкою вгору.

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

Пишемо гру «Картки пам'яті» на Swift
Власне, ось і все. Цей проект можна використовувати для створення власного варіанта гри.

Вдалого кодингу!

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

Джерело: habr.com

Додати коментар або відгук