feat: implement rule validation and joker-based scoring logic (partial delivery 4)

This commit is contained in:
2025-05-27 22:49:07 -04:00
parent 9885cf071b
commit 8c02441a74
6 changed files with 251 additions and 28 deletions

View File

@@ -47,7 +47,36 @@ This delivery improves encapsulation by adding explicit getters and setters to t
- `Score`: `getChips`, `setChips`, `getMultiplier`, `setMultiplier`
- 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;">
<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)
*/
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.

View File

@@ -152,7 +152,6 @@ class Hand {
cards.clear()
throw e
}
cards ++= newCards
}
/**
@@ -174,7 +173,6 @@ class Hand {
throw e
}
jokers ++= newJokers
}
/**
* 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
import munit.FunSuite
import cards._
import jokers._
import cards.*
import exceptions.InvalidCardIndexException
import jokers.*
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")
}
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") {
@@ -155,14 +141,6 @@ class HandTest extends FunSuite {
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") {
val hand = new Hand
@@ -172,6 +150,15 @@ class HandTest extends FunSuite {
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") {
val hand = new Hand
hand.addJoker(joker1)
@@ -181,4 +168,22 @@ class HandTest extends FunSuite {
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)
}
}
}