Escribir un juego de tarjetas de memoria en Swift

Escribir un juego de tarjetas de memoria en Swift

Este artículo describe el proceso de creación de un sencillo juego de entrenamiento de la memoria que realmente me gusta. Además de ser bueno en sí mismo, aprenderá un poco más sobre las clases y protocolos de Swift a medida que avance. Pero antes de comenzar, comprendamos el juego en sí.

Recordamos: para todos los lectores de "Habr": un descuento de 10 rublos al inscribirse en cualquier curso de Skillbox utilizando el código promocional "Habr".

Skillbox recomienda: Curso educativo en línea "Profesión Desarrollador Java".

Cómo jugar a la tarjeta de memoria

El juego comienza con una demostración de un juego de cartas. Se acuestan boca abajo (respectivamente, imagen abajo). Cuando haces clic en cualquiera, la imagen se abre durante unos segundos.

La tarea del jugador es encontrar todas las cartas con las mismas imágenes. Si después de abrir la primera carta, le das la vuelta a la segunda y las imágenes coinciden, ambas cartas permanecen abiertas. Si no coinciden, las tarjetas se vuelven a cerrar. El objetivo es abrirlo todo.

Estructura del proyecto

Para crear una versión simple de este juego necesitas los siguientes componentes:

  • Un controlador: GameController.swift.
  • Una vista: CardCell.swift.
  • Dos modelos: MemoryGame.swift y Card.swift.
  • Main.storyboard para garantizar que todo el conjunto de componentes esté disponible.

Empezamos con el componente más sencillo del juego, las cartas.

Tarjeta.swift

El modelo de tarjeta tendrá tres propiedades: id para identificar cada una, una variable booleana que se muestra para especificar el estado de la tarjeta (oculta o abierta) y ArtworkURL para las imágenes de las tarjetas.

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

También necesitará estos métodos para controlar la interacción del usuario con los mapas:

Método para mostrar una imagen en una tarjeta. Aquí restablecemos todas las propiedades a los valores predeterminados. Para la identificación, generamos una identificación aleatoria llamando a NSUUIS().uuidString.

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

Método de comparación de cédulas de identidad.

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

Método para crear una copia de cada tarjeta. - para obtener un mayor número de idénticos. Este método devolverá una tarjeta con valores similares.

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

Y se necesita un método más para barajar las cartas al principio. Lo convertiremos en una extensión de la clase Array.

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

Y aquí está la implementación del código para el modelo Card con todas las propiedades y métodos.

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

Adelante.

El segundo modelo es MemoryGame, aquí configuramos una cuadrícula de 4*4. El modelo tendrá propiedades como cards (una matriz de cartas en una cuadrícula), una matriz cardsShown con cartas ya abiertas y una variable booleana isPlaying para rastrear el estado del juego.

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

También necesitamos desarrollar métodos para controlar la interacción del usuario con la red.

Un método que baraja cartas en una cuadrícula.

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

Método para crear un nuevo juego. Aquí llamamos al primer método para iniciar el diseño inicial e inicializar la variable isPlaying en verdadero.

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

Si queremos reiniciar el juego, luego configuramos la variable isPlaying en falso y eliminamos el diseño inicial de las cartas.

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

Método para verificar las tarjetas en las que se hizo clic. Más sobre él más adelante.

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

Un método que devuelve la posición de una tarjeta específica.

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

Comprobación de la conformidad de la tarjeta seleccionada con la norma.

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

Este método lee el último elemento de la matriz **cardsShown** y devuelve la tarjeta que no coincide.

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

Main.storyboard se parece a esto:

Escribir un juego de tarjetas de memoria en Swift

Inicialmente, debes configurar el nuevo juego como viewDidLoad en el controlador, incluidas las imágenes de la cuadrícula. En el juego, todo esto estará representado por 4*4 collectionView. Si aún no estás familiarizado con collectionView, aquí lo tienes puedes obtener la información que necesitas.

Configuraremos GameController como controlador raíz de la aplicación. GameController tendrá un collectionView al que nos referiremos como IBOutlet. Otra referencia es al botón IBAction onStartGame(), este es un UIButton, puedes verlo en el guión gráfico llamado PLAY.

Un poco sobre la implementación de controladores:

  • Primero, inicializamos dos objetos principales: la cuadrícula: juego = MemoryGame() y un conjunto de cartas: cards = [Card]().
  • Configuramos las variables iniciales como viewDidLoad, este es el primer método que se llama mientras se ejecuta el juego.
  • collectionView está configurado como oculto porque todas las tarjetas están ocultas hasta que el usuario presiona PLAY.
  • Tan pronto como presionamos PLAY, se inicia la sección onStartGame IBAction y configuramos la propiedad collectionView isHidden en falso para que las tarjetas puedan volverse visibles.
  • Cada vez que el usuario selecciona una tarjeta, se llama al método didSelectItemAt. En el método llamamos didSelectCard para implementar la lógica principal del juego.

Aquí está la implementación final de 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)
    }
}

Ahora hablemos un poco de los protocolos importantes.

Protocolos

Trabajar con protocolos es el núcleo de la programación Swift. Los protocolos brindan la capacidad de definir reglas para una clase, estructura o enumeración. Este principio le permite escribir código modular y extensible. De hecho, este es un patrón que ya estamos implementando para collectionView en GameController. Ahora hagamos nuestra propia versión. La sintaxis se verá así:

protocol MemoryGameProtocol {
    //protocol definition goes here
}

Sabemos que un protocolo nos permite definir reglas o instrucciones para implementar una clase, así que pensemos en cuáles deberían ser. Necesitas cuatro en total.

  • Inicio del juego: MemoryGameDidStart.
  • Debes poner la tarjeta boca abajo: MemoryGameShowCards.
  • Debes poner la tarjeta boca abajo: MemoryGameHideCards.
  • Fin del juego: MemoryGameDidEnd.

Los cuatro métodos se pueden implementar para la clase principal, que es GameController.

memoriaGameDidStart

Cuando se ejecuta este método, el juego debería comenzar (el usuario presiona PLAY). Aquí simplemente recargaremos el contenido llamando a collectionView.reloadData(), que barajará las cartas.

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

memoriaJuegoShowCards

Llamamos a este método desde collectionSDViewSelectItemAt. Primero muestra la tarjeta seleccionada. Luego verifica si hay una tarjeta no coincidente en la matriz cardsShown (si el número de cardsShown es impar). Si la hay, se compara la tarjeta seleccionada con ella. Si las imágenes son iguales, ambas cartas se agregan a las cartas que se muestran y permanecen boca arriba. Si es diferente, la carta deja cardsShown y ambas se ponen boca abajo.

memoriaJuegoOcultarCartas

Si las tarjetas no coinciden, se llama a este método y las imágenes de las tarjetas se ocultan.

mostrado = falso.

memoriaGameDidEnd

Cuando se llama a este método, significa que todas las cartas ya están reveladas y están en la lista cardsShown: cardsShown.count = cards.count, por lo que el juego ha terminado. El método se llama específicamente después de haber llamado a endGame() para establecer la var isPlaying en falso, después de lo cual se muestra el mensaje de finalización del juego. Además, alertController se utiliza como indicador para el controlador. Se llama a viewDidDisappear y el juego se reinicia.

Así es como se ve todo en 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()
    }
}

Escribir un juego de tarjetas de memoria en Swift
En realidad, eso es todo. Puedes utilizar este proyecto para crear tu propia versión del juego.

¡Feliz codificación!

Skillbox recomienda:

Fuente: habr.com

Añadir un comentario