Game Development Journal 8: Scoring and Hand-Game Complexity

I am reaching a point in this project where I am trying to figure out how to proceed. I have my base deck and player functionality in place. After that, things get tricky.

Godori is a hand-based card game, similar to poker. In poker, you have multiple hands. If there were not multiple hands, then the betting would be irrelevant. There would be no point in betting on any specific hand because each hand would be all or nothing.

This means that I need to parse out how to separate the hand-based game logic from the overall game logic. This presents a tricky situation where it would be easy to try and place all of this in one massive class that has to deal with a lot of state. There will be a necessary amount of state, but I would prefer the state get handled by the right things.

Initially I was going to place most of the game logic into the Hand class, but I have realized that I don’t want everything in there. That class should simply be running the game. It should have a loop where it keeps track of the current player, ensures the player completes their turn, determines if a win condition has occurred, if the player has differed the win, then start over again with the next player. The scoring is a separate bit of functionality. It should be able to call functions that do the scoring for it rather than putting all of that into the Hand Class.

Yes, I realize this may not be the best way of handling things. I know that you should try to encapsulate functionality within data structures, but I also hate opening up a class to find a thousand lines of code. For me, conceptually, it’s easier for me to think of the scoring in terms of stand alone functions than trying to figure out how they fit within the Hand class.

Scoring Code

So I am creating a utility file of functions that take an array of cards as an input and return an integer that represents the score.

There are four types of cards, so I am going to write four functions to score the cards and one to call all of those functions.

The easiest one to score is the Brights. According to the rules of Godori, there are five Bright cards. You do not get any points until you collect three Brights. If you collect all five, you get a whopping fifteen points! Huzzah!

The stumbling block I came across with this was how to take advantage of a design decision I implemented earlier. I made a Card protocol that all of the card types conform to. Since the type isn’t a property on the struct, I was trying to figure out how to check the type and only count the ones that are of type BrightCard.

Some Googling brought me to a conditional logic statement I have not seen yet: if-is.

If-is allows you to check if something is a certain data type. Instead of having to add another property to each struct that represents its type, I can check it directly using that logic statement.

Here is my function to score my Bright Cards:

func brightScore(cards:[Card]) -> Int {
    
    var numberOfCards:Int = 0
    var score:Int = 0
    
    for card in cards {
        if card is BrightCard {
            numberOfCards += 1
        }
    }
    
    switch numberOfCards {
    case 3:
        score = 3
    case 4:
        score = 4
    case 5:
        score = 15
    default:
        score = 0
    }
    
    return score
}

This feels rather verbose. I am sure there is a better way to write this. I will revisit it later when I have a chance to think it over for a while.

Even though this is verbose, it’s very clear about what this function does. I like that it’s clear if you look this over about how this function works. Future Janie wants to look at code that will be clear to her about what it does.

If I decide to try and do something with Map() or whatever I want to make sure that it’s still perfectly clear to anyone what this does. I don’t care about cutting out lines of code as much as I care about clarity and performance. I understand that the vast amounts of conditional logic in the scoring functions might cause a performance hit, but I don’t want to prematurely optimize.

I am putting a pin in this to check later.

I am also going to share the Animal scoring function. This type is a little more complicated. There are three bird cards in the Animal set. If you capture all three bird cards, it scores you an extra five points.

I had to add a nested if statement to this function. After determining if a card is an Animal card or not, I additionally have to determine if it’s a bird or not. Since bird is not a property on the Card protocol, I have to cast the card as an Animal card when I know it will be an animal to check its bird property.

func animalScore(cards:[Card]) -> Int {
    var numberOfCards:Int = 0
    var numberOfBirds:Int = 0
    var score:Int = 0
    
    for card in cards {
        if card is AnimalCard {
            numberOfCards += 1
            
            let animalCard = card as! AnimalCard
            
            if animalCard.isBird == true {
                numberOfBirds += 1
            }
        }
    }
    
    switch numberOfCards {
    case 5:
        score = 1
    case 6:
        score = 2
    case 7:
        score = 3
    case 8:
        score = 4
    case 9:
        score = 5
    default:
        score = 0
    }
    
    if numberOfBirds == 3 {
        score += 5
    }
    
    return score
}

Refactoring the Switch Statement

After I went through and completed all of the scoring functions, I noticed there was a lot of repetition. This got really annoying with the Junk cards where the points start accumulating at ten cards and went all the way up to a possible 26 junk cards.

The Brights don’t follow a pattern, but all the other cards do. They have a lowest number of cards to be worth any points. Each additional card is worth a point. Instead of doing switch statements, I should be able to do this with less code.

I figured out that if I check if the number of cards is equal to or greater than this barrier to entry, I can subtract a certain amount from the number of cards to get the value.

With the ribbon and animal cards, if you have five cards, it’s worth one point. Each additional card is worth an additional point. This means that I can check to see if the number of cards is above five. If so, I can subtract four from the number of cards to get the number of points. If there are seven cards and you subtract four, you get three. This is scalable in the way the switch statement is not. I don’t have to worry that I miscounted the junk cards and that I will take away the player’s points after they exceed the number I thought there were.

This is the Junk scoring function, after the refactor:

func junkScore(cards:[Card]) -> Int {
    var numberOfCards:Int = 0
    var score:Int = 0
    
    for card in cards {
        if card is JunkCard {
            numberOfCards += 1
            
            let junkCard = card as! JunkCard
            
            if junkCard.isDouble == true {
                numberOfCards += 1
            }
        }
    }
    
    if numberOfCards >= 10 {
        score = numberOfCards - 9
    } else {
        score = 0
    }
    
    return score
}

Returning the Score

Now that all the scoring is encapsulated in each of these functions, I can write a short and sweet function checking the score for the entire hand:

func currentPlayerScore(cards:[Card]) -> Int {
    return brightScore(cards: cards) +
        animalScore(cards: cards) +
        ribbonScore(cards: cards) +
        junkScore(cards: cards)
}

Wrapping Up

One advantage to breaking this out as I have is that it is now a lot easier to unit test. I don’t have to make an instance of a hand just to check the scoring. I can create mock data that checks each of the conditions within all the functions.

One of my goals with this project was to figure out how to break down everything into small, doable chunks. If you look at the entire application as a whole, it’s overwhelming. By pulling out chunks that can be knocked off one by one and building upon those chunks, you can slowly build up a stable application based on a solid foundation without worrying too much about what goes on top.

Next up I am hoping to add more functionality to the Hand class. I need to create the properties and set up the game loop that will walk through how a turn works.

Ta ta for now!