Compare commits

3 Commits

13 changed files with 339 additions and 37 deletions

View File

@@ -47,7 +47,36 @@ This delivery improves encapsulation by adding explicit getters and setters to t
- `Score`: `getChips`, `setChips`, `getMultiplier`, `setMultiplier` - `Score`: `getChips`, `setChips`, `getMultiplier`, `setMultiplier`
- Updated tests to cover the new accessors. - Updated tests to cover the new accessors.
--- ---
## Task 3 - Partial Delivery 4
This delivery focuses on adding **robust rule enforcement** using **custom exceptions**, and introducing the **base scoring logic** influenced by Jokers.
### Changes:
- Implemented constraints and exceptions in `Hand`:
- A hand cannot have more than 8 cards → `TooManyCardsException`
- A hand cannot have more than 2 jokers → `TooManyJokersException`
- Cannot remove a card or joker using an invalid index → `InvalidCardIndexException`, `InvalidJokerIndexException`
- Cannot play more than 3 times → `TooManyPlaysException`
- Cannot discard more than 3 times → `TooManyDiscardsException`
- Playing or discarding must involve 1 to 5 cards → `InvalidPlaySizeException`
- Index validation now rejects unordered or out-of-range lists
- Added `discardCards(indices: List[Int])` method to `Hand` with full validation and tracking
- Created custom exception classes under the `exceptions/` package
- Added unit tests in `ExceptionTest.scala` to cover:
- Each custom exception
- Limits and index validation in `Hand`
- Introduced scoring logic via `ScoreUtils.applyScore(score, joker, cards)`:
- **Greedy Joker**: +3 multiplier per Diamond
- **Devious Joker**: +100 chips if cards form a straight
- **Even Steven**: +4 multiplier per even-valued card
- **Scary Face**: +30 chips per face card (J, Q, K)
---
<div style="text-align:center;"> <div style="text-align:center;">
<img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License"> <img src="https://i.creativecommons.org/l/by/4.0/88x31.png" alt="Creative Commons License">

View File

@@ -7,6 +7,16 @@ package cards
* @param suit the suit of the card (e.g., Hearts, Spades) * @param suit the suit of the card (e.g., Hearts, Spades)
*/ */
class Card(val rank: Rank, val suit: Suit) { class Card(val rank: Rank, val suit: Suit) {
/**
*
*/
def getRank: Rank = rank
/**
*
* @return
*/
def getSuit: String = suit.name
/** /**
* Returns a string representation of the card. * Returns a string representation of the card.

View File

@@ -0,0 +1,3 @@
package exceptions
class InvalidCardIndexException(message: String = "Invalid card index.") extends Exception(message)

View File

@@ -0,0 +1,3 @@
package exceptions
class InvalidJokerIndexException(message: String = "Invalid joker index.") extends Exception(message)

View File

@@ -0,0 +1,3 @@
package exceptions
class InvalidPlaySizeException(message: String = "Must play between 1 and 5 cards.") extends Exception(message)

View File

@@ -0,0 +1,4 @@
package exceptions
class TooManyCardsException(message: String = "Cannot add more than 8 cards to the hand.")
extends Exception(message)

View File

@@ -0,0 +1,3 @@
package exceptions
class TooManyDiscardsException(message: String = "Cannot discard more than 3 times.") extends Exception(message)

View File

@@ -0,0 +1,4 @@
package exceptions
class TooManyJokersException(message: String = "Cannot add more than 2 jokers to the hand.")
extends Exception(message)

View File

@@ -0,0 +1,3 @@
package exceptions
class TooManyPlaysException(message: String = "Cannot play more than 3 times.") extends Exception(message)

View File

@@ -2,8 +2,9 @@ package hand
import cards.Card import cards.Card
import jokers.Joker import jokers.Joker
import scala.collection.mutable.ListBuffer
import scala.collection.mutable.ListBuffer
import exceptions.{InvalidCardIndexException, InvalidJokerIndexException, InvalidPlaySizeException, TooManyCardsException, TooManyDiscardsException, TooManyJokersException, TooManyPlaysException}
/** /**
* Represents a Hand in Balatro, containing Cards and Jokers. * Represents a Hand in Balatro, containing Cards and Jokers.
* Allows adding/removing cards and jokers by index, and playing cards. * Allows adding/removing cards and jokers by index, and playing cards.
@@ -11,6 +12,8 @@ import scala.collection.mutable.ListBuffer
class Hand { class Hand {
private val cards = ListBuffer[Card]() private val cards = ListBuffer[Card]()
private val jokers = ListBuffer[Joker]() private val jokers = ListBuffer[Joker]()
private var playCount = 0
private var discardCount = 0
/** /**
* Adds a Card to the hand. * Adds a Card to the hand.
@@ -19,6 +22,9 @@ class Hand {
* @param card The Card object to add. * @param card The Card object to add.
*/ */
def addCard(card: Card): Unit = { def addCard(card: Card): Unit = {
if (cards.length >= 8){
throw new TooManyCardsException()
}
cards += card cards += card
} }
@@ -34,8 +40,7 @@ class Hand {
if (index >= 0 && index < cards.length) { if (index >= 0 && index < cards.length) {
cards.remove(index) cards.remove(index)
} else { } else {
// As per requirements, we don't need to strictly handle invalid indices for the final submission. throw new InvalidCardIndexException(s"Index $index out of bounds for cards (size: ${cards.length})")
println(s"Warning: Attempted to remove card at invalid index $index. Hand size: ${cards.length}")
} }
} }
@@ -46,6 +51,9 @@ class Hand {
* @param joker The Joker object to add. * @param joker The Joker object to add.
*/ */
def addJoker(joker: Joker): Unit = { def addJoker(joker: Joker): Unit = {
if (jokers.length >=2){
throw new TooManyJokersException()
}
jokers += joker jokers += joker
} }
@@ -61,8 +69,7 @@ class Hand {
if (index >= 0 && index < jokers.length) { if (index >= 0 && index < jokers.length) {
jokers.remove(index) jokers.remove(index)
} else { } else {
// As per requirements, we don't need to strictly handle invalid indices for the final submission. throw new InvalidJokerIndexException(s"Index $index out of bounds for jokers (size: ${jokers.length})")
println(s"Warning: Attempted to remove joker at invalid index $index. Joker count: ${jokers.length}")
} }
} }
@@ -76,14 +83,44 @@ class Hand {
* @return A list containing the Card objects that were played (and thus removed). * @return A list containing the Card objects that were played (and thus removed).
*/ */
def playCards(indices: List[Int]): List[Card] = { def playCards(indices: List[Int]): List[Card] = {
// Assumes indices are valid and within bounds as per requirements. if (playCount >= 3) throw new TooManyPlaysException()
val cardsToPlay = indices.map(index => cards(index)).toList if (indices.size < 1 || indices.size > 5) throw new InvalidPlaySizeException()
// Removes the selected cards from the hand.
if (!indices.forall(i => i >= 0 && i < cards.length)) {
throw new InvalidCardIndexException("Some indices are out of range.")
}
val cardsToPlay = indices.map(cards)
cards --= cardsToPlay cards --= cardsToPlay
// Returns the list of cards that were played. playCount += 1
cardsToPlay cardsToPlay
} }
/**
* Discards cards from the hand based on a list of indices.
* Validates that the number of discards is between 1 and 5,
* and that the operation has not been performed more than 3 times.
* Also checks that all indices are valid and in range.
*
* @param indices A list of zero-based indices of the cards to discard.
* @return A list of Card objects that were discarded.
* @throws TooManyDiscardsException If more than 3 discards have been attempted.
* @throws InvalidPlaySizeException If fewer than 1 or more than 5 cards are discarded.
* @throws InvalidCardIndexException If any index is out of range.
*/
def discardCards(indices: List[Int]): List[Card] = {
if (discardCount >= 3) throw new TooManyDiscardsException()
if (indices.size < 1 || indices.size > 5) throw new InvalidPlaySizeException("Must discard between 1 and 5 cards.")
if (!indices.forall(i => i >= 0 && i < cards.length)) {
throw new InvalidCardIndexException("Some indices are out of range.")
}
val discarded = indices.map(cards)
cards --= discarded
discardCount += 1
discarded
}
/** /**
* Provides an immutable view of the cards currently in the hand. * Provides an immutable view of the cards currently in the hand.
* Useful for displaying the hand or passing it to other components * Useful for displaying the hand or passing it to other components
@@ -107,7 +144,14 @@ class Hand {
*/ */
def setCards(newCards: List[Card]): Unit = { def setCards(newCards: List[Card]): Unit = {
cards.clear() cards.clear()
cards ++= newCards try{
newCards.foreach(addCard)
} catch {
case e: TooManyCardsException =>
println(s"Failed to set cards: ${e.getMessage}")
cards.clear()
throw e
}
} }
/** /**
@@ -117,8 +161,18 @@ class Hand {
* @param newJokers A list of Joker objects to set as the jokers in hand. * @param newJokers A list of Joker objects to set as the jokers in hand.
*/ */
def setJokers(newJokers: List[Joker]): Unit = { def setJokers(newJokers: List[Joker]): Unit = {
jokers.clear() jokers.clear()
jokers ++= newJokers try{
newJokers.foreach(addJoker)
}catch{
case e: TooManyJokersException=>
println(s"Failed to set jokers: ${e.getMessage}")
jokers.clear()
throw e
}
} }
/** /**
* Returns a string representation of the hand, listing cards and jokers. * Returns a string representation of the hand, listing cards and jokers.

View File

@@ -0,0 +1,95 @@
package score
import jokers.Joker
import cards.Card
object ScoreUtils {
/**
* Applies the effect of a Joker to a given score based on the cards played.
* Each Joker type has a unique effect that modifies either the chip count or multiplier.
*
* - Greedy Joker: +3 multiplier for each Diamond card.
* - Devious Joker: +100 chips if the cards form a straight (consecutive values).
* - Even Steven: +4 multiplier for each card with an even value (2, 4, 6, 8, 10).
* - Scary Face: +30 chips for each face card (Jack, Queen, King).
*
* This method does not use pattern matching or functional programming constructs.
*
* @param score the current score before applying the Joker effect
* @param j the Joker to apply
* @param cards the list of cards played in the current hand
* @return a new Score object with the Joker effect applied
*/
def applyScore(score: Score, j: Joker, cards: List[Card]): Score = {
val jokerType = j.getJokerType
val baseChips = score.getChips
val baseMultiplier = score.getMultiplier
val newScore = new Score(baseChips, baseMultiplier)
if (jokerType == "Greedy Joker") {
var bonus = 0
var i = 0
while (i < cards.size) {
if (cards(i).getSuit == "Diamonds") {
bonus += 3
}
i += 1
}
newScore.setMultiplier(baseMultiplier + bonus)
}
else if (jokerType == "Devious Joker") {
val ranks = new Array[Int](cards.length)
var i = 0
while (i < cards.length) {
ranks(i) = cards(i).getRank.value
i += 1
}
val sortedRanks = ranks.sorted
var isStraight = true
i = 1
while (i < sortedRanks.length && isStraight) {
if (sortedRanks(i) != sortedRanks(i - 1) + 1) {
isStraight = false
}
i += 1
}
if (isStraight && cards.length >= 5) {
newScore.setChips(baseChips + 100)
}
}
else if (jokerType == "Even Steven") {
var bonus = 0
var i = 0
while (i < cards.length) {
val rankValue = cards(i).getRank.value
if (rankValue % 2 == 0) {
bonus += 4
}
i += 1
}
newScore.setMultiplier(baseMultiplier + bonus)
}
else if (jokerType == "Scary Face") {
var bonus = 0
var i = 0
while (i < cards.length) {
val value = cards(i).getRank.value
if (value == 11 || value == 12 || value == 13) { // J, Q, K
bonus += 30
}
i += 1
}
newScore.setChips(baseChips + bonus)
}
newScore
}
}

View File

@@ -0,0 +1,86 @@
package exceptions
import munit.FunSuite
import hand.Hand
import cards._
import jokers.Joker
class ExceptionTest extends FunSuite {
val card = new Card(King, Spades)
val joker = new Joker("Greedy Joker")
test("addCard should throw TooManyCardsException when adding more than 8 cards") {
val hand = new Hand
for (_ <- 1 to 8) {
hand.addCard(card)
}
intercept[TooManyCardsException] {
hand.addCard(card)
}
}
test("addJoker should throw TooManyJokersException when adding more than 2 jokers") {
val hand = new Hand
hand.addJoker(joker)
hand.addJoker(joker)
intercept[TooManyJokersException] {
hand.addJoker(joker)
}
}
test("playCards should throw TooManyPlaysException after 3 plays") {
val hand = new Hand
for (_ <- 1 to 5) hand.addCard(card)
hand.playCards(List(0))
hand.addCard(card)
hand.playCards(List(0))
hand.addCard(card)
hand.playCards(List(0))
hand.addCard(card)
intercept[TooManyPlaysException] {
hand.playCards(List(0))
}
}
test("discardCards should throw TooManyDiscardsException after 3 discards") {
val hand = new Hand
for (_ <- 1 to 5) hand.addCard(card)
hand.discardCards(List(0))
hand.addCard(card)
hand.discardCards(List(0))
hand.addCard(card)
hand.discardCards(List(0))
hand.addCard(card)
intercept[TooManyDiscardsException] {
hand.discardCards(List(0))
}
}
test("playCards should throw InvalidPlaySizeException if playing more than 5 cards") {
val hand = new Hand
for (_ <- 1 to 6) hand.addCard(card)
intercept[InvalidPlaySizeException] {
hand.playCards(List(0, 1, 2, 3, 4, 5))
}
}
test("discardCards should throw InvalidPlaySizeException if discarding less than 1 card") {
val hand = new Hand
hand.addCard(card)
intercept[InvalidPlaySizeException] {
hand.discardCards(List())
}
}
test("removeJokerAtIndex should throw InvalidJokerIndexException for out-of-bounds index") {
val hand = new Hand
intercept[InvalidJokerIndexException] {
hand.removeJokerAtIndex(0)
}
}
}

View File

@@ -1,8 +1,9 @@
package hand package hand
import munit.FunSuite import munit.FunSuite
import cards._ import cards.*
import jokers._ import exceptions.InvalidCardIndexException
import jokers.*
class HandTest extends FunSuite { class HandTest extends FunSuite {
@@ -60,21 +61,6 @@ class HandTest extends FunSuite {
assertEquals(hand.getCards, List(card1, card3), "Remaining cards should be Ace of Hearts and Ten of Diamonds in order") assertEquals(hand.getCards, List(card1, card3), "Remaining cards should be Ace of Hearts and Ten of Diamonds in order")
} }
test("removeCardAtIndex should do nothing for invalid negative index (as per requirement relaxation)") {
val hand = new Hand
hand.addCard(card1)
val initialCards = hand.getCards
hand.removeCardAtIndex(-1)
assertEquals(hand.getCards, initialCards, "Cards should not change for invalid negative index")
}
test("removeCardAtIndex should do nothing for invalid out-of-bounds index (as per requirement relaxation)") {
val hand = new Hand
hand.addCard(card1)
val initialCards = hand.getCards
hand.removeCardAtIndex(1)
assertEquals(hand.getCards, initialCards, "Cards should not change for invalid out-of-bounds index")
}
test("addJoker should add a joker to the hand's joker list") { test("addJoker should add a joker to the hand's joker list") {
@@ -155,14 +141,6 @@ class HandTest extends FunSuite {
assertEquals(cards, List(card1, card2), "getCards should return all added cards in order") assertEquals(cards, List(card1, card2), "getCards should return all added cards in order")
} }
test("setCards should replace the current list of cards") {
val hand = new Hand
hand.addCard(card1)
assertEquals(hand.getCards, List(card1), "Initial card should be card1")
hand.setCards(List(card2, card3))
assertEquals(hand.getCards, List(card2, card3), "Cards should be replaced by card2 and card3")
}
test("getJokers should return the current list of jokers") { test("getJokers should return the current list of jokers") {
val hand = new Hand val hand = new Hand
@@ -172,6 +150,15 @@ class HandTest extends FunSuite {
assertEquals(jokers, List(joker1), "getJokers should return the added joker") assertEquals(jokers, List(joker1), "getJokers should return the added joker")
} }
test("setCards should replace the current list of cards") {
val hand = new Hand
hand.addCard(card1)
assertEquals(hand.getCards, List(card1), "Initial card should be card1")
hand.setCards(List(card2, card3))
assertEquals(hand.getCards, List(card2, card3), "Cards should be replaced by card2 and card3")
}
test("setJokers should replace the current list of jokers") { test("setJokers should replace the current list of jokers") {
val hand = new Hand val hand = new Hand
hand.addJoker(joker1) hand.addJoker(joker1)
@@ -180,5 +167,23 @@ class HandTest extends FunSuite {
hand.setJokers(List(joker2)) hand.setJokers(List(joker2))
assertEquals(hand.getJokers, List(joker2), "Jokers should be replaced by joker2") assertEquals(hand.getJokers, List(joker2), "Jokers should be replaced by joker2")
} }
test("removeCardAtIndex should throw exception for invalid negative index") {
val hand = new Hand
hand.addCard(card1)
intercept[InvalidCardIndexException] {
hand.removeCardAtIndex(-1)
}
}
test("removeCardAtIndex should throw exception for invalid out-of-bounds index") {
val hand = new Hand
hand.addCard(card1)
intercept[InvalidCardIndexException] {
hand.removeCardAtIndex(1)
}
}
} }