Game Development Journal 12: Checking for a Match

Generally speaking, a turn in Godori is quite simple. You have a hand of cards. On your turn, you play a card. The game board has a collection of cards on it. If the suit of the card you play matches the suit of one of the cards on the board, you capture it. Then a card is played from the draw pile. If that suit matches any on the board, you capture those as well.

From this simple explanation, you can either capture two, four, or zero cards. But there is an exception to this rule. What happens if you made a match on the card you played and then the same suit comes up from the draw pile?

In that situation, you do not capture any cards. All three cards remain on the board until another player or the deck plays the final card in the suit. At that point you capture all four cards.

I have to write programming logic to account for all of these scenarios. Because it’s possible for your cards to get stuck on the board, the way I am approaching this is that I am creating a temporary array of all the cards concerned in a turn.

These are all of the possible numbers of cards in play and how they would be distributed:

  • Two Cards: In this case, neither the card you played nor the card from the draw pile matched any suits currently in play. Both cards would be distributed to the game board.
  • Three Cards: There are two cases where you get three cards. You either made a match with either the card you played or the card that was drawn and the other card was not a match or all three cards are the same suit and they stay on the board.
  • Four Cards: The card you played and the drawn card each found another card on the board to pair with and you get all four cards.
  • Five Cards: You captured a set of three with one of your cards or one that was drawn, but the other card did not match.
  • Six Cards: Lucky you! You captured a complete suit and picked up another match from the board with the other card.
    • I can represent this logic in a method with a switch statement:

          func checkForMatch(cards:[Card]) {
              
              let numberOfCards = cards.count
              
              switch numberOfCards {
              case 2:
                  cardsInPlay += cards
              case 3:
                  let countCards = countSuits(cards: cards)
                  players[currentPlayer].capturedCards += countCards.playerCards
                  cardsInPlay += countCards.gameCards
              case 4:
                  players[currentPlayer].capturedCards += cards
              case 5:
                  let countCards = countSuits(cards: cards)
                  players[currentPlayer].capturedCards += countCards.playerCards
                  cardsInPlay += countCards.gameCards
              case 6:
                  players[currentPlayer].capturedCards += cards
              default:
                  break
              }
          }
      

      The two, four, and six options are easy. If there are two they go to the board. If there are four or six the player captures all of them.

      The hard cases to deal with are the sets of three or five. If there are three, then there is a case where the player gets no cards and a case where the player gets two cards and one goes back to the board. If there are five cards, then the player gets four cards and the board gets one, but which cards go where?

      As a person, this is easy to determine. I look at the cards all at once and I pick out the ones that don’t match. Trying to explain this to the computer is difficult.

      I have to check the suit property on each card and compare them to find the ones that match.

      This is made more difficult by the fact that I don’t know what suit I am dealing with. It’s easy to check to see if all the cards are from the .December suit, but to check a random blob of five cards where four will be one of twelve suits and the last one will be another means that I can’t simply loop through the array of cards checking to see if they’re a specific suit. I have to compare them to one another.

      The only think I have available to me to limit the amount of conditional logic is to know the order that the cards are entered into the array. I know that if I have three cards then either the card I played matched or the card from the draw stack matched. There would be no scenario where the first and third card would match without also matching the second card.

      A similar pattern exists with the five card option. If you capture a complete suit of cards, then there will be four cards in a row in the array with the same suit. Instead of having to check each card in the array for the mismatching card, I theoretically only have to check the first two cards and the last two cards. If the first two match, the last card will not. If the last two match, then the first one won’t.

      This is the current implementation I have:

          func countSuits(cards:[Card]) -> (playerCards:[Card], gameCards:[Card]) {
              
              if cards.count == 3 {
                  // I don't think there is a situation where the first and third card would match
                  // Either the first and second or second and third cards will match
                  if cards[0].cardSuit == cards[1].cardSuit && cards[0].cardSuit == cards[2].cardSuit {
                      return ([], cards)
                  } else if cards[0].cardSuit == cards[1].cardSuit {
                      return([cards[0], cards[1]], [cards[2]])
                  } else {
                      return([cards[1], cards[2]], [cards[0]])
                  }
              } else {
                  // Four cards will match and one will not. If arrays work the way I think they're supposed to, 
                  // in this case either the first four cards will match or the last four will.
                  if cards[0].cardSuit == cards[1].cardSuit {
                      return([cards[0], cards[1], cards[2], cards[3]], [cards[4]])
                  } else {
                      return([cards[1], cards[2], cards[3], cards[4]], [cards[0]])
                  }
              }
              
          }
      

      Again, I understand that this implementation is really ugly. Everything is hard coded. There are a lot of if/else statements and array positions. There are a lot of ways this can go wrong.

      Conclusions

      I know there is probably a better way to deal with this. It’s possible that there is a class in the Gameplay Kit framework that would handle this better.

      I am planning to explore better ways to deal with this. I also am planning to write unit tests for this when I move this stuff over to a project where I can include them.

      There is simply a lot of state and conditional logic in this application because it’s a game. I saw a bunch of people on Twitter recently talking about how embarrassed we’re going to be in ten years because we use if/else statements. I have not heard any suggestions about how to get around this.

      I know writing conditional logic is ugly and there are people reading this who want to completely rewrite my code. This is a question I deal with every day.

      There is a certain level of necessary complexity. There are a lot of conditions present in the rules of the game that need to be accommodated for. Simon Allardice talked about how when you’re teaching something there is a certain level of necessary complexity. I know myself and lot of people get frustrated when we do a tutorial for something like OpenGL and we have a spinning cube of death. It’s like, this is cool, but I can’t apply this knowledge to anything.

      There are certain circumstances that get chosen for teaching purposes because they’re easy and not messy. There is the world as it is and the world as we wish it would be.

      I see situations in here where it’s like “Oh this is simple! I can just set this up like this. Except it doesn’t work in this case, or this one, or this one…”

      I am trying to avoid setting up a bunch of spaghetti code or putting in a bunch of unnecessary objects to embed other object with even more objects.

      Programming is an iterative process. This code will change down the road. I would rather get something done now and go back and change it later when I get a better idea of what I need than spend all my time waxing poetic about how I could do this with a map() if I didn’t have to account for these edge cases. Bad code that actually gets written trumps perfect code that can only live in your head.