Search code examples
swiftenumsprotocols

Iterable Array of Enums


I am trying to initialize a deck of cards. I have card attributes in my card struct. My approach is to try and create an array of "enum states", then iterate through those to initialize each card. I am having trouble doing so.

Game Class

import Foundation

struct Set{
    var cards = [Card]()

    init(){
        let properties : [Any] = 
            [cardShape.self, cardColor.self, cardNumber.self, cardShading.self]
        for prop in properties{
        // Not really sure how to iterate through this array... 
        // Ideally it would be something like this.
        // Iterate through array, for property in array, 
        // card.add(property)
        }
    }
}

Card Class

import UIKit
import Foundation

struct Card{
    var attributes : properties = properties()

    mutating func addProperty(value : Property){
        if value is cardShape{
            attributes.shape = value as! cardShape
        } else if value is cardColor{
            attributes.color = value as! cardColor
        } else if value is cardNumber{
            attributes.number = value as! cardNumber
        }else if value is cardShading{
            attributes.shading = value as! cardShading
        }else{
            print("error")
        }
    }
}

protocol Property{
    static var allValues : [Property] {get}
}

struct properties{
    var shape : cardShape = cardShape.none
    var color : cardColor = cardColor.none
    var number : cardNumber = cardNumber.none
    var shading : cardShading = cardShading.none
}

enum cardShape : String,Property{
    case Square = "■"
    case Triangle = "▲"
    case Circle = "●"
    case none
    static var allValues : [Property]{ return [cardShape.Square,cardShape.Triangle,cardShape.Circle]}
}

enum cardColor:Property  {
    case Red
    case Purple
    case Green
    case none

    static var allValues : [Property] {return [cardColor.Red,cardColor.Purple,cardColor.Green]}
}

enum cardNumber : Int,Property{
    case One = 1
    case Two = 2
    case Three = 3
    case none

    static var allValues : [Property] {return [cardNumber.One,cardNumber.Two,cardNumber.Three]}
}

enum cardShading: Property {
    case Solid
    case Striped
    case Outlined
    case none

    static var allValues : [Property] {return [cardShading.Solid,cardShading.Striped,cardShading.Outlined]}
}

So to summarize, my main issue is trying to create an array of enums, then cycling through the enum states to initialize a card with specific attribute states.


Solution

  • You will want to make sure you cover all combinations of attributes and make sure each card has one of each of the four types of attributes. I would suggest using nested loops:

    for shape in cardShape.allValues {
        for color in cardColor.allValues {
            for number in cardNumber.allValues {
                for shading in cardShading.allValues {
                    var card = Card()
                    card.addProperty(shape)
                    card.addProperty(color)
                    card.addProperty(number)
                    card.addProperty(shading)
                    cards.append(card)
                }
            }
        }
    }
    

    I believe your Card struct is a bit too complex. If you change your representation, it will be easier to create the cards.

    Have your card represent the different attributes as their own property:

    struct Card {
        let shape: CardShape
        let color: CardColor
        let number: CardNumber
        let shading: CardShading
    }
    

    Then use nested loops to create your cards:

    for shape in CardShape.allValues {
        for color in CardColor.allValues {
            for number in CardNumber.allValues {
                for shading in CardShading.allValues {
                    cards.append(Card(shape: shape, color: color, number: number, shading: shading))
                }
            }
        }
    }
    

    Notes:

    • Your enums should start with uppercase characters, and your enum values should start with lowercase characters.
    • Using separate properties for each attribute will make it much easier to check for matching attributes between cards.
    • You get an initializer by default that initializes all properties. By initializing them with nested loops, you will be able to create all possible cards.
    • Change your allValues properties to return arrays of the specific attribute type (for example [CardShape]).

    Alternate Answer:

    Instead of using nested arrays, you could use MartinR's combinations function to create the list of combinations of the properties. Adding an init to Card that takes [Property], you can create the cards in two lines of code:

    struct Card {
        var shape = CardShape.none
        var color = CardColor.none
        var number = CardNumber.none
        var shading = CardShading.none
    
        init(properties: [Property]) {
            for property in properties {
                switch property {
                case let shape as CardShape:
                    self.shape = shape
                case let color as CardColor:
                    self.color = color
                case let number as CardNumber:
                    self.number = number
                case let shading as CardShading:
                    self.shading = shading
                default:
                    break
                }
            }
        }
    }
    
    // https://stackoverflow.com/a/45136672/1630618
    func combinations<T>(options: [[T]]) -> AnySequence<[T]> {
        guard let lastOption = options.last else {
            return AnySequence(CollectionOfOne([]))
        }
        let headCombinations = combinations(options: Array(options.dropLast()))
        return AnySequence(headCombinations.lazy.flatMap { head in
            lastOption.lazy.map { head + [$0] }
        })
    }
    
    
    struct SetGame {
        let cards: [Card]
    
        init(){
            let properties: [Property.Type] = [CardShape.self, CardColor.self, CardNumber.self, CardShading.self]
            cards = combinations(options: properties.map { $0.allValues }).map(Card.init)
        }
    }
    

    How this works:

    1. properties.map { $0.allValues } calls allValues on each item of the properties array creating an [[Property]] with [[.square, .triangle, .circle], [.red, .purple, .green], [.one, .two, .three], [.solid, .striped, .outlined]]
    2. This is passed to combinations which creates a sequence with all 81 combinations of these properties: [[.square, .red, .one, .solid], ..., [.circle, .green, .three, .outlined]].
    3. map is run on this sequence to call Card.init with each combination which results in an [Card] with 81 cards.