diff --git a/README.md b/README.md index e1c7c927d..ae584b3c9 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ -# kotlin-blackjack \ No newline at end of file +# kotlin-blackjack + +## Blackjack + +### Key objects +Card + +- [x] Number cards are counted by their face value. +- [x] Aces can count as either 1 or 11. + +Card Number + +- [x] Number or symbol (J, Q, K, A). +- [x] Face cards (King, Queen, Jack) are each worth 10. + +Card Suit + +- [x] Contains four card suits (Spades, Hearts, Diamonds, Clubs). + +Deck + +- [x] Deck must not contain duplicated cards. + +Hands +- [x] Must have two cards to be initialized. +- [x] Should be able to add card to hands. + +Player + +- [x] Receives two cards at start of the game. +- [x] Players can choose to draw additional cards as long as their total does not exceed 21. + +### States + +State +- [x] Interface for all game states + +InitialTurn +- [x] Should remain initial until card size is less than or equal to 2 +- [x] If hands has more than 3 cards after draw, should move state to Hit. + +Hit +- [x] Can add cards until bust diff --git a/src/main/kotlin/blackjack/BlackjackApplication.kt b/src/main/kotlin/blackjack/BlackjackApplication.kt new file mode 100644 index 000000000..a41f48dd4 --- /dev/null +++ b/src/main/kotlin/blackjack/BlackjackApplication.kt @@ -0,0 +1,31 @@ +package blackjack + +import blackjack.domain.deck.Deck +import blackjack.domain.player.Name +import blackjack.domain.player.Player +import blackjack.domain.player.Players +import blackjack.view.InputView +import blackjack.view.OutputView + +fun main() { + val rawNames = InputView.getPlayerNames() + val players = Players(rawNames.map(::Name).map { Player(it) }) + val deck = Deck.create() + + players.initializeState { deck.drawCard() } + OutputView.printInitialCards(players) + + players.values.forEach { player -> player.takeTurn(deck) } + OutputView.printBlackjackResult(players) +} + +private fun Player.takeTurn(deck: Deck) { + while (this.canDraw && InputView.getUserChoice(this)) { + this.draw(deck.drawCard()) + OutputView.printPlayerCards(this) + + if (!this.canDraw) { + return OutputView.announceBust(this) + } + } +} diff --git a/src/main/kotlin/blackjack/domain/card/Card.kt b/src/main/kotlin/blackjack/domain/card/Card.kt new file mode 100644 index 000000000..4d57c2e3d --- /dev/null +++ b/src/main/kotlin/blackjack/domain/card/Card.kt @@ -0,0 +1,27 @@ +package blackjack.domain.card + +class Card private constructor( + val number: CardNumber, + val suit: Suit, +) { + companion object { + private val cache: Map, Card> = + CardNumber.entries.flatMap { number -> + Suit.entries.map { suit -> + Pair(Pair(number, suit), Card(number, suit)) + } + }.toMap() + + val cached: List = cache.values.toList() + + fun of( + number: CardNumber, + suit: Suit, + ) = cache[Pair(number, suit)] ?: throw IllegalStateException("Card does not exist in cache.") + + fun of( + rawNumber: String, + rawSuit: String, + ) = of(CardNumber.fromName(rawNumber), Suit.fromName(rawSuit)) + } +} diff --git a/src/main/kotlin/blackjack/domain/card/CardNumber.kt b/src/main/kotlin/blackjack/domain/card/CardNumber.kt new file mode 100644 index 000000000..9ac2901b2 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/card/CardNumber.kt @@ -0,0 +1,24 @@ +package blackjack.domain.card + +enum class CardNumber(val value: Int) { + ACE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7), + EIGHT(8), + NINE(9), + TEN(10), + JACK(10), + QUEEN(10), + KING(10), + ; + + companion object { + fun fromName(name: String) = + entries.find { it.name == name.uppercase() } + ?: throw IllegalArgumentException("Card number does not exist for $name") + } +} diff --git a/src/main/kotlin/blackjack/domain/card/Suit.kt b/src/main/kotlin/blackjack/domain/card/Suit.kt new file mode 100644 index 000000000..cc300d3dc --- /dev/null +++ b/src/main/kotlin/blackjack/domain/card/Suit.kt @@ -0,0 +1,15 @@ +package blackjack.domain.card + +enum class Suit { + SPADES, + HEARTS, + DIAMONDS, + CLUBS, + ; + + companion object { + fun fromName(name: String) = + entries.find { it.name == name.uppercase() } + ?: throw IllegalArgumentException("Card suit does not exist for $name") + } +} diff --git a/src/main/kotlin/blackjack/domain/deck/Deck.kt b/src/main/kotlin/blackjack/domain/deck/Deck.kt new file mode 100644 index 000000000..e88af802c --- /dev/null +++ b/src/main/kotlin/blackjack/domain/deck/Deck.kt @@ -0,0 +1,22 @@ +package blackjack.domain.deck + +import blackjack.domain.card.Card + +class Deck private constructor( + private val cards: MutableList, +) { + init { + check(cards.size == cards.distinct().size) { + "Deck must not contain duplicated cards." + } + } + + fun drawCard(): Card { + check(cards.isNotEmpty()) { "There are no cards left in deck." } + return cards.removeFirst() + } + + companion object { + fun create(generator: () -> MutableList = RandomDeckGenerator::generate) = Deck(generator()) + } +} diff --git a/src/main/kotlin/blackjack/domain/deck/DeckGenerator.kt b/src/main/kotlin/blackjack/domain/deck/DeckGenerator.kt new file mode 100644 index 000000000..ca4f1397f --- /dev/null +++ b/src/main/kotlin/blackjack/domain/deck/DeckGenerator.kt @@ -0,0 +1,11 @@ +package blackjack.domain.deck + +import blackjack.domain.card.Card + +interface DeckGenerator { + fun generate(): MutableList +} + +object RandomDeckGenerator : DeckGenerator { + override fun generate() = Card.cached.shuffled().toMutableList() +} diff --git a/src/main/kotlin/blackjack/domain/player/Hands.kt b/src/main/kotlin/blackjack/domain/player/Hands.kt new file mode 100644 index 000000000..ebd1f2caf --- /dev/null +++ b/src/main/kotlin/blackjack/domain/player/Hands.kt @@ -0,0 +1,35 @@ +package blackjack.domain.player + +import blackjack.domain.card.Card +import blackjack.domain.card.CardNumber + +class Hands( + val cards: List = emptyList(), +) { + val initialized + get() = cards.size >= 2 + + val size: Int + get() = cards.size + + infix operator fun plus(card: Card) = Hands(cards + card) + + fun isBust(): Boolean = calculateScore() > BLACKJACK_SCORE + + fun calculateScore(): Int { + val sum = cards.sumOf { it.number.value } + + return when { + !hasAce() -> sum + sum + ACE_ADD_SCORE > BLACKJACK_SCORE -> sum + else -> sum + ACE_ADD_SCORE + } + } + + fun hasAce() = cards.map { it.number }.contains(CardNumber.ACE) + + companion object { + private const val BLACKJACK_SCORE = 21 + private const val ACE_ADD_SCORE = 10 + } +} diff --git a/src/main/kotlin/blackjack/domain/player/Name.kt b/src/main/kotlin/blackjack/domain/player/Name.kt new file mode 100644 index 000000000..9bbdf99f0 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/player/Name.kt @@ -0,0 +1,7 @@ +package blackjack.domain.player + +class Name(val value: String) { + init { + require(value.trim().isNotEmpty()) { "Name can't be empty." } + } +} diff --git a/src/main/kotlin/blackjack/domain/player/Player.kt b/src/main/kotlin/blackjack/domain/player/Player.kt new file mode 100644 index 000000000..7de5fb06d --- /dev/null +++ b/src/main/kotlin/blackjack/domain/player/Player.kt @@ -0,0 +1,29 @@ +package blackjack.domain.player + +import blackjack.domain.card.Card +import blackjack.domain.state.InitialState +import blackjack.domain.state.State + +class Player( + val name: Name, + _state: State = InitialState(), +) { + var state = _state + private set + + val cards: List + get() = state.hands.cards + + val canDraw: Boolean + get() = state.canContinue + + val score: Int? + get() = state.score + + constructor(rawName: String) : this(Name(rawName)) + constructor(rawName: String, state: State) : this(Name(rawName), state) + + fun draw(card: Card) { + state = state.addCard(card) + } +} diff --git a/src/main/kotlin/blackjack/domain/player/Players.kt b/src/main/kotlin/blackjack/domain/player/Players.kt new file mode 100644 index 000000000..67bea3d41 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/player/Players.kt @@ -0,0 +1,21 @@ +package blackjack.domain.player + +import blackjack.domain.card.Card + +class Players( + val values: List, +) { + val names = values.map { it.name.value } + + constructor(vararg rawNames: String) : this(rawNames.map { Player(Name(it)) }) + + fun initializeState(block: () -> Card) { + repeat(FIRST_TURN_REPETITIONS) { + values.forEach { it.draw(block()) } + } + } + + companion object { + private const val FIRST_TURN_REPETITIONS = 2 + } +} diff --git a/src/main/kotlin/blackjack/domain/state/Bust.kt b/src/main/kotlin/blackjack/domain/state/Bust.kt new file mode 100644 index 000000000..364f5dae1 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/state/Bust.kt @@ -0,0 +1,15 @@ +package blackjack.domain.state + +import blackjack.domain.card.Card +import blackjack.domain.player.Hands + +class Bust(override val hands: Hands) : State { + override val canContinue = false + + override val score: Int? + get() = null + + override fun addCard(card: Card): State { + throw IllegalStateException("Can't add card after bust.") + } +} diff --git a/src/main/kotlin/blackjack/domain/state/Hit.kt b/src/main/kotlin/blackjack/domain/state/Hit.kt new file mode 100644 index 000000000..2fc8edf8a --- /dev/null +++ b/src/main/kotlin/blackjack/domain/state/Hit.kt @@ -0,0 +1,18 @@ +package blackjack.domain.state + +import blackjack.domain.card.Card +import blackjack.domain.player.Hands + +class Hit(override val hands: Hands) : State { + override val score: Int? + get() = hands.calculateScore() + + override fun addCard(card: Card): State { + val hands = hands + card + if (hands.isBust()) { + return Bust(hands) + } + + return Hit(hands) + } +} diff --git a/src/main/kotlin/blackjack/domain/state/InitialState.kt b/src/main/kotlin/blackjack/domain/state/InitialState.kt new file mode 100644 index 000000000..0a858fe2e --- /dev/null +++ b/src/main/kotlin/blackjack/domain/state/InitialState.kt @@ -0,0 +1,18 @@ +package blackjack.domain.state + +import blackjack.domain.card.Card +import blackjack.domain.player.Hands + +class InitialState(override val hands: Hands = Hands()) : State { + override val score: Int? + get() = hands.calculateScore() + + override fun addCard(card: Card): State { + val hands = hands + card + if (!hands.initialized) { + return InitialState(hands) + } + + return Hit(hands) + } +} diff --git a/src/main/kotlin/blackjack/domain/state/State.kt b/src/main/kotlin/blackjack/domain/state/State.kt new file mode 100644 index 000000000..0eef79839 --- /dev/null +++ b/src/main/kotlin/blackjack/domain/state/State.kt @@ -0,0 +1,15 @@ +package blackjack.domain.state + +import blackjack.domain.card.Card +import blackjack.domain.player.Hands + +interface State { + val hands: Hands + + val canContinue: Boolean + get() = true + + val score: Int? + + fun addCard(card: Card): State +} diff --git a/src/main/kotlin/blackjack/view/InputView.kt b/src/main/kotlin/blackjack/view/InputView.kt new file mode 100644 index 000000000..f2ba279fd --- /dev/null +++ b/src/main/kotlin/blackjack/view/InputView.kt @@ -0,0 +1,35 @@ +package blackjack.view + +import blackjack.domain.player.Player + +object InputView { + fun getPlayerNames(): List { + println("Enter the names of the players (comma-separated):") + + return readln() + .split(",") + .map { it.trim() } + .filter { it.isNotBlank() } + } + + fun getUserChoice(player: Player): Boolean { + println("Would ${player.name.value} like to draw another card? (y for yes, n for no)") + val input = readln() + + return UserChoice.from(input).value + } +} + +enum class UserChoice(val value: Boolean) { + Y(true), + N(false), + ; + + companion object { + fun from(input: String): UserChoice { + return entries.find { + it.name == input.uppercase() + } ?: throw IllegalArgumentException("Invalid user choice: $input") + } + } +} diff --git a/src/main/kotlin/blackjack/view/OutputView.kt b/src/main/kotlin/blackjack/view/OutputView.kt new file mode 100644 index 000000000..f5731ca43 --- /dev/null +++ b/src/main/kotlin/blackjack/view/OutputView.kt @@ -0,0 +1,57 @@ +package blackjack.view + +import blackjack.domain.card.Card +import blackjack.domain.card.CardNumber +import blackjack.domain.card.Suit +import blackjack.domain.player.Player +import blackjack.domain.player.Players + +object OutputView { + fun printInitialCards(players: Players) { + println("Dealing two cards to ${players.names.joinToString()}.") + players.values.forEach { println(it.toView()) } + } + + fun printPlayerCards(player: Player) = println(player.toView()) + + fun printBlackjackResult(players: Players) { + players.values.forEach { + println(it.toView() + " - Total: ${it.score ?: "BUST"}") + } + } + + fun announceBust(player: Player) { + println("${player.name.value} busts!") + } + + private fun Player.toView() = "${this.name.value}'s cards: ${this.cards.toView()}" + + private fun List.toView() = joinToString(" ") { card -> card.toView() } + + private fun Card.toView() = "${this.number.toView()}${this.suit.toView()}" + + private fun CardNumber.toView() = + when (this) { + CardNumber.ACE -> "A" + CardNumber.TWO -> "2" + CardNumber.THREE -> "3" + CardNumber.FOUR -> "4" + CardNumber.FIVE -> "5" + CardNumber.SIX -> "6" + CardNumber.SEVEN -> "7" + CardNumber.EIGHT -> "8" + CardNumber.NINE -> "9" + CardNumber.TEN -> "10" + CardNumber.JACK -> "J" + CardNumber.QUEEN -> "Q" + CardNumber.KING -> "K" + } + + private fun Suit.toView() = + when (this) { + Suit.SPADES -> "♠️" + Suit.DIAMONDS -> "♦️" + Suit.HEARTS -> "❤️" + Suit.CLUBS -> "♣️️" + } +} diff --git a/src/main/kotlin/dsl/Languages.kt b/src/main/kotlin/dsl/Languages.kt index 172bfb185..e357eea8b 100644 --- a/src/main/kotlin/dsl/Languages.kt +++ b/src/main/kotlin/dsl/Languages.kt @@ -1,5 +1,6 @@ package dsl +@PersonDsl data class Languages( val values: MutableList = mutableListOf(), ) { diff --git a/src/main/kotlin/dsl/Person.kt b/src/main/kotlin/dsl/Person.kt index 874bce4e4..cfefcc1f1 100644 --- a/src/main/kotlin/dsl/Person.kt +++ b/src/main/kotlin/dsl/Person.kt @@ -7,6 +7,7 @@ data class Person( val languages: Languages = Languages(), ) +@PersonDsl class PersonBuilder { private lateinit var name: String private var company: String? = null diff --git a/src/main/kotlin/dsl/PersonDsl.kt b/src/main/kotlin/dsl/PersonDsl.kt new file mode 100644 index 000000000..968915c25 --- /dev/null +++ b/src/main/kotlin/dsl/PersonDsl.kt @@ -0,0 +1,4 @@ +package dsl + +@DslMarker +annotation class PersonDsl diff --git a/src/main/kotlin/dsl/Skills.kt b/src/main/kotlin/dsl/Skills.kt index b288a23ec..c2972e9cd 100644 --- a/src/main/kotlin/dsl/Skills.kt +++ b/src/main/kotlin/dsl/Skills.kt @@ -1,14 +1,22 @@ package dsl +@PersonDsl data class Skills( - val softSkills: MutableList = mutableListOf(), - val hardSkills: MutableList = mutableListOf(), + val values: MutableList = mutableListOf(), ) { fun soft(skill: String) { - this.softSkills.add(skill) + this.values.add(Skill.Soft(skill)) } fun hard(skill: String) { - this.hardSkills.add(skill) + this.values.add(Skill.Hard(skill)) } } + +sealed class Skill( + val value: String, +) { + data class Soft(val skill: String) : Skill(skill) + + data class Hard(val skill: String) : Skill(skill) +} diff --git a/src/test/kotlin/blackjack/domain/card/CardFixture.kt b/src/test/kotlin/blackjack/domain/card/CardFixture.kt new file mode 100644 index 000000000..15b4d1617 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/card/CardFixture.kt @@ -0,0 +1,11 @@ +package blackjack.domain.card + +object CardFixture { + val SPADES_ACE = Card.of(CardNumber.ACE, Suit.SPADES) + val SPADES_TWO = Card.of(CardNumber.TWO, Suit.SPADES) + val SPADES_SIX = Card.of(CardNumber.SIX, Suit.SPADES) + val SPADES_SEVEN = Card.of(CardNumber.SEVEN, Suit.SPADES) + val SPADES_JACK = Card.of(CardNumber.JACK, Suit.SPADES) + val SPADES_QUEEN = Card.of(CardNumber.QUEEN, Suit.SPADES) + val SPADES_TEN = Card.of(CardNumber.TEN, Suit.SPADES) +} diff --git a/src/test/kotlin/blackjack/domain/card/CardTest.kt b/src/test/kotlin/blackjack/domain/card/CardTest.kt new file mode 100644 index 000000000..4aaa0e7d2 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/card/CardTest.kt @@ -0,0 +1,69 @@ +package blackjack.domain.card + +import blackjack.domain.card.CardNumber.ACE +import blackjack.domain.card.CardNumber.NINE +import blackjack.domain.card.CardNumber.QUEEN +import blackjack.domain.card.CardNumber.TWO +import blackjack.domain.card.Suit.CLUBS +import blackjack.domain.card.Suit.DIAMONDS +import blackjack.domain.card.Suit.HEARTS +import blackjack.domain.card.Suit.SPADES +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class CardTest : FunSpec({ + context("card creation") { + test("should create with valid enum values") { + this@context.withData( + ACE to SPADES, + QUEEN to HEARTS, + ) { (number, suit) -> + shouldNotThrowAny { + Card.of(number, suit) + } + } + } + + test("should create with raw values") { + this@context.withData( + "ACE" to "CLUBS", + "QUEEN" to "HEARTS", + "KING" to "DIAMONDS", + ) { (number, suit) -> + shouldNotThrowAny { + Card.of(number, suit) + } + } + } + + test("should throw exception with invalid values") { + this@context.withData( + "TRIPLE" to "CLUBS", + "TWO" to "CLOVER", + "ACE" to "DIAMOND", + "ACE" to "SPACEX", + ) { + shouldThrow { + Card.of(it.first, it.second) + } + } + } + + test("same number and suit should have save reference") { + this@context.withData( + ACE to SPADES, + QUEEN to HEARTS, + TWO to DIAMONDS, + NINE to CLUBS, + ) { (number, suit) -> + val card1 = Card.of(number, suit) + val card2 = Card.of(number, suit) + + (card1 === card2) shouldBe true + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/deck/DeckTest.kt b/src/test/kotlin/blackjack/domain/deck/DeckTest.kt new file mode 100644 index 000000000..9f04b7fa9 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/deck/DeckTest.kt @@ -0,0 +1,48 @@ +package blackjack.domain.deck + +import blackjack.domain.card.Card +import blackjack.domain.card.CardFixture.SPADES_ACE +import blackjack.domain.card.CardNumber.ACE +import blackjack.domain.card.Suit.SPADES +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class DeckTest : FunSpec({ + context("create") { + test("should create a Deck") { + shouldNotThrowAny { + Deck.create() + } + } + + test("should throw exception if duplicated cards exist") { + shouldThrow { + Deck.create { + mutableListOf( + Card.of(ACE, SPADES), + Card.of(ACE, SPADES), + ) + } + } + } + } + + context("drawCard") { + test("should draw a card and remove from deck") { + val deck = Deck.create { mutableListOf(SPADES_ACE) } + val card = deck.drawCard() + + card shouldBe SPADES_ACE + } + + test("should throw exception if no card to draw") { + val deck = Deck.create { mutableListOf() } + + shouldThrow { + deck.drawCard() + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/player/HandsTest.kt b/src/test/kotlin/blackjack/domain/player/HandsTest.kt new file mode 100644 index 000000000..543f9f559 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/player/HandsTest.kt @@ -0,0 +1,89 @@ +package blackjack.domain.player + +import blackjack.domain.card.CardFixture.SPADES_ACE +import blackjack.domain.card.CardFixture.SPADES_JACK +import blackjack.domain.card.CardFixture.SPADES_QUEEN +import blackjack.domain.card.CardFixture.SPADES_SEVEN +import blackjack.domain.card.CardFixture.SPADES_SIX +import io.kotest.core.spec.style.FunSpec +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class HandsTest : FunSpec({ + context("initialized") { + test("if there are two cards should return true") { + val hands = + Hands( + listOf( + SPADES_JACK, + SPADES_SIX, + ), + ) + + hands.initialized shouldBe true + } + + test("if there are less than two cards should return false") { + val hands = + Hands( + listOf(SPADES_ACE), + ) + + hands.initialized shouldBe false + } + } + + test("size") { + val hands = Hands() + + hands.size shouldBe 0 + } + + test("addCard") { + val hands = Hands() + val actual = hands + SPADES_JACK + + actual.size shouldBe 1 + } + + test("bust") { + val hands = + Hands( + listOf( + SPADES_JACK, + SPADES_SIX, + SPADES_QUEEN, + ), + ) + + hands.isBust() shouldBe true + } + + context("score") { + test("score is calculated correctly") { + this@context.withData( + listOf(SPADES_JACK, SPADES_SIX) to 16, + listOf(SPADES_SIX, SPADES_SEVEN) to 13, + listOf(SPADES_QUEEN, SPADES_JACK) to 20, + ) { (cards, expected) -> + val hands = Hands(cards) + + hands.calculateScore() shouldBe expected + } + } + + test("ace is counted as 1 if cards + ACE(11) is over than 21") { + val cards = listOf(SPADES_QUEEN, SPADES_JACK, SPADES_ACE) + val hands = Hands(cards) + + hands.calculateScore() shouldBe 21 + } + + test("ace is counted as 11 if cards + ACE(11) is below or equal to 21") { + val cards = listOf(SPADES_QUEEN, SPADES_ACE) + val hands = Hands(cards) + + hands.calculateScore() shouldBe 21 + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/player/NameTest.kt b/src/test/kotlin/blackjack/domain/player/NameTest.kt new file mode 100644 index 000000000..1c93eb991 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/player/NameTest.kt @@ -0,0 +1,15 @@ +package blackjack.domain.player + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.inspectors.forAll + +class NameTest : FunSpec({ + test("should not be empty") { + listOf("", " ", " ").forAll { + shouldThrow { + Name(it) + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/player/PlayerTest.kt b/src/test/kotlin/blackjack/domain/player/PlayerTest.kt new file mode 100644 index 000000000..ad4a15f24 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/player/PlayerTest.kt @@ -0,0 +1,63 @@ +package blackjack.domain.player + +import blackjack.domain.card.CardFixture.SPADES_ACE +import blackjack.domain.card.CardFixture.SPADES_SEVEN +import blackjack.domain.card.CardFixture.SPADES_SIX +import blackjack.domain.card.CardFixture.SPADES_TWO +import blackjack.domain.state.Bust +import blackjack.domain.state.Hit +import blackjack.domain.state.InitialState +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class PlayerTest : FunSpec({ + test("when created, state should be initial state") { + val player = Player("me") + + player.state::class shouldBe InitialState::class + } + + context("canDraw") { + test("return true on initial state") { + val player = Player("sun") + player.canDraw shouldBe true + } + + test("return true on hit state") { + val player = Player("sun", Hit(Hands())) + player.canDraw shouldBe true + } + + test("return false on bust state") { + val player = Player("sun", Bust(Hands())) + player.canDraw shouldBe false + } + } + + test("can draw a card") { + val player = Player("me") + player.draw(SPADES_ACE) + + player.state.hands.size shouldBe 1 + } + + test("state should change") { + val player = Player("me") + player.draw(SPADES_SIX) + player.draw(SPADES_SEVEN) + player.draw(SPADES_TWO) + + assertSoftly { + player.state.hands.size shouldBe 3 + player.state::class shouldBe Hit::class + } + } + + test("score") { + val player = Player("me") + player.draw(SPADES_SIX) + + player.score shouldBe 6 + } +}) diff --git a/src/test/kotlin/blackjack/domain/player/PlayersTest.kt b/src/test/kotlin/blackjack/domain/player/PlayersTest.kt new file mode 100644 index 000000000..30cffe075 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/player/PlayersTest.kt @@ -0,0 +1,42 @@ +package blackjack.domain.player + +import blackjack.domain.card.CardFixture.SPADES_ACE +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldHaveSize + +class PlayersTest : FunSpec({ + context("create") { + test("from raw names") { + shouldNotThrowAny { + Players("sun", "justin", "jason") + } + } + + test("from players") { + shouldNotThrowAny { + Players( + listOf( + Player("sun"), + Player("justin"), + Player("jason"), + ), + ) + } + } + } + + test("initializeState should give exactly two cards to each player") { + val playerNames = listOf("sun", "justin", "jason") + val players = Players(*playerNames.toTypedArray()) + + players.initializeState { SPADES_ACE } + + players.values.zip(playerNames).forEach { (player, name) -> + withClue("Player '$name' should have exactly 2 cards") { + player.cards shouldHaveSize 2 + } + } + } +}) diff --git a/src/test/kotlin/blackjack/domain/state/BustTest.kt b/src/test/kotlin/blackjack/domain/state/BustTest.kt new file mode 100644 index 000000000..6c66e2225 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/state/BustTest.kt @@ -0,0 +1,28 @@ +package blackjack.domain.state + +import blackjack.domain.card.CardFixture.SPADES_ACE +import blackjack.domain.card.CardFixture.SPADES_QUEEN +import blackjack.domain.card.CardFixture.SPADES_TEN +import blackjack.domain.card.CardFixture.SPADES_TWO +import blackjack.domain.player.Hands +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class BustTest : FunSpec({ + test("addCard throws exception if busted") { + shouldThrow { + Bust(Hands()).addCard(SPADES_ACE) + } + } + + test("canContinue returns false on bust") { + Bust(Hands()).canContinue shouldBe false + } + + test("hands score must be null on bust") { + val hands = Hands(listOf(SPADES_TEN, SPADES_QUEEN, SPADES_TWO)) + + Bust(hands).score shouldBe null + } +}) diff --git a/src/test/kotlin/blackjack/domain/state/HitTest.kt b/src/test/kotlin/blackjack/domain/state/HitTest.kt new file mode 100644 index 000000000..bb042b38a --- /dev/null +++ b/src/test/kotlin/blackjack/domain/state/HitTest.kt @@ -0,0 +1,39 @@ +package blackjack.domain.state + +import blackjack.domain.card.CardFixture.SPADES_ACE +import blackjack.domain.card.CardFixture.SPADES_QUEEN +import blackjack.domain.card.CardFixture.SPADES_SEVEN +import blackjack.domain.card.CardFixture.SPADES_SIX +import blackjack.domain.card.CardFixture.SPADES_TWO +import blackjack.domain.player.Hands +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class HitTest : FunSpec({ + context("addCard") { + test("should remain Hit until bust") { + val hit = + Hit( + Hands( + listOf( + SPADES_SIX, + SPADES_SEVEN, + ), + ), + ) + val state = hit.addCard(SPADES_TWO) + + state::class shouldBe Hit::class + } + } + + test("canContinue returns true on hit") { + Hit(Hands()).canContinue shouldBe true + } + + test("should return hands score") { + val hands = Hands(listOf(SPADES_ACE, SPADES_QUEEN)) + + Hit(hands).score shouldBe 21 + } +}) diff --git a/src/test/kotlin/blackjack/domain/state/InitialStateTest.kt b/src/test/kotlin/blackjack/domain/state/InitialStateTest.kt new file mode 100644 index 000000000..72fb0e1c2 --- /dev/null +++ b/src/test/kotlin/blackjack/domain/state/InitialStateTest.kt @@ -0,0 +1,56 @@ +package blackjack.domain.state + +import blackjack.domain.card.Card +import blackjack.domain.card.CardFixture.SPADES_ACE +import blackjack.domain.card.CardFixture.SPADES_QUEEN +import blackjack.domain.card.CardFixture.SPADES_SIX +import blackjack.domain.card.CardFixture.SPADES_TEN +import blackjack.domain.card.CardNumber +import blackjack.domain.card.Suit +import blackjack.domain.player.Hands +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class InitialStateTest : FunSpec({ + context("addCard") { + test("if not initialized, should return initial turn state") { + val initialState = InitialState() + val newCard = Card.of(CardNumber.ACE, Suit.HEARTS) + val state = initialState.addCard(newCard) + + assertSoftly { + state.hands.size shouldBe 1 + state::class shouldBe InitialState::class + } + } + + test("if initialized, should return hit state") { + val initialState = + InitialState( + Hands( + listOf( + SPADES_ACE, + SPADES_SIX, + ), + ), + ) + val state = initialState.addCard(SPADES_ACE) + + assertSoftly { + state.hands.size shouldBe 3 + state::class shouldBe Hit::class + } + } + } + + test("canContinue returns true on initialState") { + InitialState().canContinue shouldBe true + } + + test("should return hands score") { + val hands = Hands(listOf(SPADES_TEN, SPADES_QUEEN)) + + InitialState(hands).score shouldBe 20 + } +}) diff --git a/src/test/kotlin/dsl/DslTest.kt b/src/test/kotlin/dsl/DslTest.kt index 906b5a08b..1440bc1ca 100644 --- a/src/test/kotlin/dsl/DslTest.kt +++ b/src/test/kotlin/dsl/DslTest.kt @@ -40,8 +40,9 @@ class DslTest : FunSpec({ } assertSoftly(person.skills) { - softSkills.size shouldBe 2 - hardSkills.size shouldBe 1 + values.size shouldBe 3 + values.filterIsInstance().size shouldBe 2 + values.filterIsInstance().size shouldBe 1 } } @@ -80,8 +81,9 @@ class DslTest : FunSpec({ assertSoftly(person) { name shouldBe "Sun" company shouldBe "Delivery Hero" - skills.softSkills.size shouldBe 2 - skills.hardSkills.size shouldBe 1 + skills.values.size shouldBe 3 + skills.values.filterIsInstance().size shouldBe 2 + skills.values.filterIsInstance().size shouldBe 1 languages.values.size shouldBe 2 } }