Create GameHandler

As I mentioned earlier, GameHandler is where all our game logic is going to live. In this way, we greatly simplify what needs to be done within the GameViewModel and avoid causing too much bloat in that class. Given that GameHandler has no state of its own, we can define it as a Kotlin object declaration rather than a class. Similar to companion objects we saw in Create the AI Class, object declarations are singleton instances. They look very similar to classes but instead use the object keyword, as you’ll see.

In our case, GameHandler will be an object declaration with only functions (as it will not be holding any state) that we can use to run our game. Create that object (under the game package) with some empty functions:

 package​ ​dev.mfazio.pennydrop.game
 
 object​ GameHandler {
 
 fun​ ​roll​(
  players: List<Player>,
  currentPlayer: Player,
  slots: List<Slot>
  ): TurnResult { }
 
 fun​ ​pass​(
  players: List<Player>,
  currentPlayer: Player
  ): TurnResult { }
 
 private​ ​fun​ ​rollDie​(sides: Int = 6) { ... }
 
 private​ ​fun​ ​nextPlayer​(
  players: List<Player>,
  currentPlayer: Player
  ): Player { }
 }

You can see the methods we’ll be implementing in this section, which will then be (mostly) called by the GameViewModel. I say “mostly” because the GameHandler can always call its own methods as well, which we’ll see near the end of the chapter.

Also, most of the functions return a TurnResult? object, which contains game statuses/information. We use this class to send that info back to the GameViewModel.

Let’s create a new data class called TurnResult. This class will have everything the GameViewModel needs to know and no more. We can first get TurnResult out there, then worry about implementing the functions in GameHandler.

Create the TurnResult data class

The TurnResult class contains everything the GameViewModel needs to know after a turn (or for the start of the game). We’ll look at how GameViewModel uses these values later on in this chapter, but let’s look at the TurnResult code first:

 package​ ​dev.mfazio.pennydrop.game
 
 import​ ​dev.mfazio.pennydrop.types.Player
 
 data class​ TurnResult(
 val​ lastRoll: Int? = ​null​,
 val​ coinChangeCount: Int? = ​null​,
 val​ previousPlayer: Player? = ​null​,
 val​ currentPlayer: Player? = ​null​,
 val​ playerChanged: Boolean = ​false​,
 val​ turnEnd: TurnEnd? = ​null​,
 val​ canRoll: Boolean = ​false​,
 val​ canPass: Boolean = ​false​,
 val​ clearSlots: Boolean = ​false​,
 val​ isGameOver: Boolean = ​false
 )
 
 enum​ ​class​ TurnEnd { Pass, Bust, Win }

Not too much to consider here, but let’s talk more about data class and enum class. We already used the data class designation for classes in Add NewPlayer data class, but I want to call out two of the automatically generated functions that are included on every data class.

  • copy, which uses a given object as a base, then changes whichever values you send into the function:

     // filledSlot will have the same values as rolledSlot,
     // but isFilled and lastRolled will be true
     val​ filledSlot = rolledSlot.copy(isFilled = ​true​, lastRolled = ​true​)
  • componentN functions, which allow for destructuring declarations (where properties can be split into multiple variables). For example, we can take the filledSlot object and split it into two separate values. The order is based on the order the properties are declared in the Slot class.

     val​ (slotNum, slotCanBeFilled) = filledSlot

Before we move on, let’s dig into this last piece a bit more. As I mentioned, destructuring objects means we split an object into some number of variables. We already saw one example, but my favorite use case for destructuring declarations is when you’re dealing with a loop and a Map.

If we take a Map<String, Int> where the String is a player’s name and Int is their score, we traditionally would have to do something like this to print out the scores:

 playerScores.forEach {entry ->
  println(​"${entry.key} - ${entry.value} pennies."​)
 }

Instead, we can split our entry into two variables, name and score, making this same block look much better:

 playerScores.forEach {(name, score) ->
  println(​"$name - $score pennies."​)
 }

We’re left with cleaner, more readable code and minimal overhead. We can destructure any objects that implement componentN functions, including (but not limited to) data class, List, and Map objects. With data class covered, let’s quickly check out what’s going on with the TurnEnd enum class.

Create TurnEnd enum class

An enum class holds one or more constant values. In our case, TurnEnd contains the three ways a turn can be over: a user passes, a user busts (and takes the pennies on the board), and a user wins. The reason we’re using TurnEnd instead of an Int or String value is because enum class objects are typed. This means instead of having turnEnd be an unclear Int value like 1 or a mistake-prone String like “pass”, we use TurnEnd.Pass. It makes your code more readable and less prone to errors. The TurnEnd enum class will look like this:

 enum​ ​class​ TurnEnd { Pass, Bust, Win }

With TurnResult ready, we can get back to implementing the GameHandler class.

Handle User Rolls

Quick reminder: rolling in Penny Drop consists of the current player taking a single D6 (a six-sided die) and putting a penny in the slot that they roll. If that slot is taken, they bust and take all the coins currently on the board, then play passes to the next player. If they roll a six, they drop the penny into the open six slot which is kept in the reservoir. With a physical version of the game, the reservoir would be under the main board, but here we just remove the penny and optionally keep track of the number of pennies in the reservoir.

Our roll function will do something similar: we’ll roll the D6, grab the corresponding Slot, then send back a TurnResult based on what happened. We use the let function on rollDie, not as a null check (as we have previously) but rather as a way to have lastRoll be available to us for use in our next few lines of code. It’s completely valid to do the same thing with a standard assignment like val lastRoll = rollDie(), but I like the let { ... } syntax. Again, it’s personal preference for you and your team.

We end this section by grabbing the selected Slot and using that to generate the TurnResult. If the slot is null here, we use the null-coalescing or Elvis operator (?:) to fall back to a default TurnResult object.

The Elvis operator (named for the legendary musician and his legendary hair) is added after an expression, and if that expression is null, the expression on the right side of the Elvis operator is then executed.

The full code (minus the TurnResult generation) looks like this:

 fun​ ​roll​(players: List<Player>,
  currentPlayer: Player,
  slots: List<Slot>): TurnResult =
  rollDie().let { lastRoll ->
  slots.getOrNull(lastRoll - 1)?.let { slot ->
 // TurnResult is created here
  } ?: TurnResult(isGameOver = ​true​)
  }

For reference, our rollDie function takes in a number of sides and gives us a random number from 1 until our entered number of sides (plus one, which I’ll explain in a minute). If we haven’t entered a value for sides, the function uses the default value of 6.

 private​ ​fun​ ​rollDie​(sides: Int = 6) = Random.nextInt(1, sides + 1)

I want to call out something in particular with the function declaration of nextInt:

 Random.nextInt(from: Int, until: Int) { ... }

This function will give us a random number starting with the value of from and up to (but not including) the value for until. The fact that the upper bound is non-inclusive is why rollDie has the second value of sides + 1 instead of just sides. I’m mentioning this not only because it’ll help with Random.nextInt (and the other functions on the Random class) but because when you’re dealing with ranges, until is a keyword there (as opposed to the parameters where until is a variable).

We used an IntRange when generating our Slot objects in the GameViewModel, a technique we first saw in Add NewPlayer Instances to PickPlayersViewModel.

 val​ slots =
  MutableLiveData(
  (1..6).map { slotNum -> Slot(slotNum, slotNum != 6) }
  )

We could have instead started with 0 and counted up to 6, but not actually include 6:

 val​ slots =
  MutableLiveData(
  (0 until 6).map { ind -> Slot(ind + 1, ind != 5) }
  )

As both examples work, I would highly recommend going with the first approach here. Having until is definitely useful in other situations, though, which is why I mentioned it.

Before we move on, I want to also cover the slots logic from earlier. For reference, it’s this block in particular:

 slots.getOrNull(lastRoll - 1)?.let { slot ->
 // TurnResult is created here
 } ?: TurnResult(isGameOver = ​true​)

We’re grabbing the rolled slot using the getOrNull function, which either returns the slot at the given index or null. We then use the Elvis operator to only continue on if we have an actual slot. Given that we control how rollDie works and what’s in the List<Slot>, we’d probably be safe with just using get or indexing (for example, slots[lastRoll - 1]). We’re in control of what’s going on—that means there’s no way that call can fail, right?

Far too many devs have said something like this, only to have their app crash. To make sure that doesn’t happen to us, let’s use the safe approach by adding the null-safe ?.let block. Now we can move on to the logic inside there.

As I mentioned at the beginning of this section, there are two primary outcomes for a roll: a player rolls the number of an empty slot (including 6) or they roll an already-filled slot. If they roll an empty slot, they put a penny in that slot (unless they roll a six, which goes into the reservoir), and we see if they won the game. As tempting as it is to use Kotlin’s awesome when block here, we only have two sets of two choices, so a standard if...else will suffice. Don’t worry, we’ll discuss when statements later in this chapter when dealing with the game text.

The inside block that goes with our previous roll code will be structured like this:

 if​ (slot.isFilled) {
 // Player busts, play continues to next player
 } ​else​ {
 if​ (!currentPlayer.penniesLeft(​true​)) {
 // Player wins
  } ​else​ {
 // Game continues
  }
 }

While we still have blocks of code to complete here, we should first add penniesLeft to our Player class.

Add a Helper Method to Player Class

The penniesLeft function is straightforward, as it checks if a user’s penny count is greater than zero. But notice the true flag in our call? That’s a flag called subtractPenny, which tells our method to take one away from a user’s penny count when checking if they have any pennies left. This is done because we’re checking the future state for a user, meaning how many pennies they’ll have after taking the current roll into consideration. The Player class will now look like this:

 data class​ Player(
 var​ playerName: String = ​""​,
 var​ isHuman: Boolean = ​true​,
 var​ selectedAI: AI? = ​null
 ) {
 var​ pennies: Int = defaultPennyCount
 
 fun​ ​addPennies​(count: Int = 1) {
  pennies += count
  }
 
 var​ isRolling: Boolean = ​false
 
»fun​ ​penniesLeft​(subtractPenny: Boolean = ​false​) =
» (pennies - (​if​(subtractPenny) 1 ​else​ 0)) > 0
 
 companion​ ​object​ {
 const​ ​val​ defaultPennyCount = 10
  }
 }

Let’s jump back to the GameHandler class. We’ll go through the implementation details of roll in a bit, but first, I want to highlight what’s coming out of the nested let blocks in that method.

Returning an if...else?

Yes, the inside block of roll is an if...else block without a return statement. Yet, the TurnResult objects we create are returned successfully. How can this be? It’s because of two language features in Kotlin. The inside of a lambda function, like inside a let, will return the last value in the block. This includes values/variables and any expressions. Please note that normal functions can’t do this, only expression blocks.

Speaking of which, the reason the if..else block can be returned is that they’re actually expressions in Kotlin. This means they return a value themselves instead of just causing side effects like in other languages. We no longer have to declare an empty variable outside an if...else only to assign it inside, but instead can have immutable values assigned directly from the if...else block. This is also part of the reason the ternary operator ([condition] ? [if true] : [if false]) doesn’t exist in Kotlin; if...else blocks can handle the same syntax without the need for two conventions. Using them also avoids nested ternary statements that can be extremely difficult to understand.

If you look at the penniesLeft function we added, it’s using if...else in a similar way. Instead of having to add an extra value like extraPennies or whatever we’d want to call it to remove the extra penny, we can just inline the if...else statement. You probably get the point by now, so let’s move on to the code inside the if...else blocks.

Let’s start with the block when the rolled Slot is already filled, since there’s only one thing that happens here, the creation of a TurnResult object. To get the number of coins going to the player, we can see how many slots are already filled and assign that to coinChangeCount. We also get the next player in the list via the nextPlayer function (which we’ll look at in a second) and get things ready for a new turn. The code looks like this:

 if​ (slot.isFilled) {
  TurnResult(
  lastRoll,
  coinChangeCount = slots.count { it.isFilled },
  clearSlots = ​true​,
  turnEnd = TurnEnd.Bust,
  previousPlayer = currentPlayer,
  currentPlayer = nextPlayer(players, currentPlayer),
  playerChanged = ​true​,
  canRoll = ​true​,
  canPass = ​false
  )
 }

While we don’t need to have the parameters explicitly listed out here, it makes this call much clearer. Otherwise, we’d be looking at a whole bunch of Boolean values trying to remember which is which. This is another personal-preference situation, but I think the labels make the code more readable.

Grab the Next Player

We saw the nextPlayer function call in the TurnResult class, so let’s dig into that now.

We’re doing three things in this method:

  • Get the index of the current player in the list of players.
  • Figure out the next player’s index (which may wrap around).
  • Return the next player.
 private​ ​fun​ ​nextPlayer​(
  players: List<Player>,
  currentPlayer: Player): Player? {
 val​ currentIndex = players.indexOf(currentPlayer)
 val​ nextIndex = (currentIndex + 1) % players.size
 
 return​ players[nextIndex]
 }

indexOf will give us the position of our current player (from zero to the number of players in the game minus one), which we can then use to find the next player in turn order. In most cases, we take the current index, add one (to move to the next player), and we’re ready. However, if we reach the end of the player list, we want to swing back around to the beginning of the list. That’s why I included the second half the expression: % players.size.

The % operator is the modulo or remainder operator, which divides the left-hand value by the right-hand value but returns the left-over amount (the remainder) rather than the actual result. This means our index can never be higher than the number of players in the game and will wrap back around to zero if the index is higher than the limit.

For example, if we have five players in our game, player four is playing and their turn ends, player five would be up. As a result, currentIndex is 3 (since indexes start at 0), players.size is 5, and nextIndex is 4:

 val​ nextIndex = (3 + 1) % 5 ​// 4 % 5 == 4

The % operator doesn’t do much here, but if player five’s turn ends and we need to swing back to player one, things could be trickier. currentIndex is 4 and adding one to that would put the index outside of the bounds of players (since indexes are going from 0 to 4). Adding % makes nextIndex 0 since there’s no remainder when dividing five by five.

 val​ nextIndex = (4 + 1) % 5 ​// 5 % 5 == 0

One last item of note is that the % operator is equivalent to the .rem function, meaning the following code is equivalent to what we did before:

 val​ nextIndex = (currentIndex + 1).rem(players.size)

You can use either the function or operator interchangeably, and even overload an operator by overloading its associated function. This is outside the scope of this book, but more info can be found at https://link.mfazio.dev/operator-overloading.

We’re now set if the player rolls a slot number that already has a penny, so we can move to open slots.

Handle an Open Slot

If the current Player rolls an open slot number (including the 6), the logic will differ a bit. We use the penniesLeft function we saw earlier to see if the current player has won, but otherwise both of these TurnResult instances are straightforward.

 } ​else​ {
 if​ (!currentPlayer.penniesLeft(​true​)) {
  TurnResult(
  lastRoll,
  currentPlayer = currentPlayer,
  coinChangeCount = -1,
  isGameOver = ​true​,
  turnEnd = TurnEnd.Win,
  canRoll = ​false​,
  canPass = ​false
  )
  } ​else​ {
  TurnResult(
  lastRoll,
  currentPlayer = currentPlayer,
  canRoll = ​true​,
  canPass = ​true​,
  coinChangeCount = -1
  )
  }
 }

With those changes in place, we’re set on logic for when players roll on their turn. Now, we can move on to when players pass their turn.

Handle Users Passing

We’ve addressed the possibilities when a user rolls in Penny Drop. Now we can address the scenario where the current player has rolled at least once on their turn, decides “Nah, I’m good,” and passes to the next player.

Since we don’t have to handle a roll result, less will be going on when a user passes. Here’s the entire function:

 fun​ ​pass​(players: List<Player>, currentPlayer: Player) =
  TurnResult(
  previousPlayer = currentPlayer,
  currentPlayer = nextPlayer(players, currentPlayer),
  playerChanged = ​true​,
  turnEnd = TurnEnd.Pass,
  canRoll = ​true​,
  canPass = ​false
  )

Looks similar enough to roll, right? We’re still returning a TurnResult, plus marking the next player as able to roll but not pass (since it’s the first roll of their turn), and we’re grabbing the next player in the game. We also include the previousPlayer so the UI can update the text view properly with a message like “Michael passed. They currently have 7 pennies.”

With that, the GameHandler is mostly ready and we can start updating the UI.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.13.255