Skip to content
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
- 카드를 더 줄 때마다 해당 player가 갖고있는 카드를 출력한다.
- 모든 플레이어가 카드를 더 이상 받지 않으면 결과를 출력한다.
- 결과는 플레이어별로 갖고 있는 카드의 목록과 점수를 출력한다.
- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
- 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리한다.
- 게임을 완료한 후 각 플레이어별로 승패를 출력한다.

## 테스트 구현 목록
- [x] Test 1 : Player 객체에 이름을 부여할 수 있다.
Expand All @@ -32,4 +35,7 @@
- view라서 테스트 미구현
- [x] Test 4 : Player별 점수를 계산할 수 있다.
- card의 리스트를 감싼 Cards 객체에게 점수 계산을 위임
- [x] Test 5 : 딜러의 점수가 17점 이상이면 추가로 카드를 받을 수 없다.
- Dealer가 Player를 상속하도록 하고 addCard 로직을 바꿔서 구현
-

39 changes: 27 additions & 12 deletions src/main/kotlin/blackjack/BlackJackGame.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package blackjack

import blackjack.domain.Dealer
import blackjack.domain.Distributor
import blackjack.domain.Player
import blackjack.domain.Players
import blackjack.domain.card.CardDeck
Expand All @@ -14,30 +15,44 @@ class BlackJackGame {
val players = Players(names)
DisplayView.dealOutCards(players)

val dealer = Dealer(CardDeck())
dealer.dealOutCards(players)
DisplayView.cardsOfPlayers(players)
val distributor = Distributor(CardDeck())
val dealer = Dealer()

dealOutAdditionalCards(dealer, players)
DisplayView.result(players)
distributor.dealOutCards(dealer, players)
DisplayView.cardsOfPlayers(dealer, players)

dealOutAdditionalCards(distributor, players)
dealOutAdditionalCard(distributor, dealer)
DisplayView.finalScore(dealer, players)

GameResultCalculator.getResult(dealer, players)
DisplayView.result(dealer, players)
}

private fun dealOutAdditionalCards(dealer: Dealer, players: Players) {
private fun dealOutAdditionalCards(distributor: Distributor, players: Players) {
players.players.forEach {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일급 컬렉션 형태로 묶었을 때 players. players와 같이 사용되는 부분이 조금 어색하실텐데요.

불변 컬렉션을 사용한다면 Kotlin delegation을 적용하거나, 메서드를 추출하고 컬렉션 인터페이스의 메서드(forEach)를 숨기는 형태로 변경해보시면 좋겠습니다.

아마 지금은 dealOutAdditionalCard 같은 부분에 입출력 로직이 포함되어 있으니 delegation을 적용하시는 편이 조금 더 나을 수 있겠네요.

dealOutAdditionalCard(dealer, it)
dealOutAdditionalCard(distributor, it)
}
}

private fun dealOutAdditionalCard(dealer: Dealer, player: Player) {
private fun dealOutAdditionalCard(distributor: Distributor, player: Player) {
DisplayView.dealOutAdditionalCard(player)
if (InputView.inputAdditionalCard() == "y") {
dealer.dealOutCard(player)
takeAnotherCard(dealer, player)
distributor.dealOutCard(player)
takeAnotherCard(distributor, player)
}
}

private fun dealOutAdditionalCard(distributor: Distributor, dealer: Dealer) {
val receiveNewCard = dealer.getScore() < Dealer.LIMIT_SCORE
if (receiveNewCard) {
distributor.dealOutCard(dealer)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

점수를 가져온 뒤 BlackJackGame과 같은 입출력이 포함된 부분에서 비교 후 Boolean 값을 추출하기 보다는 해당 상태를 딜러 클래스에서 바로 가져올 수 있도록 구현하면 어떨까요?

Suggested change
val receiveNewCard = dealer.getScore() < Dealer.LIMIT_SCORE
if (receiveNewCard) {
distributor.dealOutCard(dealer)
}
if (dealer.isReceiveNewCard) {
distributor.dealOutCard(dealer)
}

DisplayView.dealOutAdditionalCard(receiveNewCard)
}

private fun takeAnotherCard(dealer: Dealer, player: Player) {
if (player.getScore() >= MAX_SCORE) {
private fun takeAnotherCard(dealer: Distributor, player: Player) {
if (player.getScore() < MAX_SCORE) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 위에 남긴 코멘트와 동일한 의견입니다. 점수를 꺼내 비교하기보단 해당 행위 자체가 메서드로 추출되면 가독성 측면 등 여러 이점이 있을 것 같아요.

dealOutAdditionalCard(dealer, player)
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/GameResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack

enum class GameResult(val description: String) {
WIN("승"),
LOSE("패")
}
18 changes: 18 additions & 0 deletions src/main/kotlin/blackjack/GameResultCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package blackjack

import blackjack.domain.Dealer
import blackjack.domain.Players

object GameResultCalculator {
// 딜러와 각 플레이어의 점수를 비교해서 승패를 판별하고 기록함
// 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리한다
fun getResult(dealer: Dealer, players: Players) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에 대한 테스트도 작성해볼 수 있지 않을까요?

val scoreOfDealer = dealer.getScore()
val isDealerLose = scoreOfDealer > BlackJackGame.MAX_SCORE
players.players.forEach {
val scoreOfPlayer = it.getScore()
it.getGameResult(isDealerLose || scoreOfPlayer > scoreOfDealer)
dealer.getGameResult(!isDealerLose && scoreOfDealer > scoreOfPlayer)
}
}
}
43 changes: 19 additions & 24 deletions src/main/kotlin/blackjack/domain/Dealer.kt
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
package blackjack.domain

import blackjack.GameResult
import blackjack.domain.card.Card
import blackjack.domain.card.CardDeck
import kotlin.random.Random
import blackjack.domain.card.Cards

class Dealer(
private val cardDeck: CardDeck
) {
class Dealer(name: String, cards: Cards = Cards()) : Player(name, cards) {
var isFinished = false
val gameResults = mutableListOf<GameResult>()

/**
* 플레이어들에게 2장씩 카드를 분배함
*/
fun dealOutCards(players: Players) {
repeat(DEAL_OUT_CARD_AMOUNT){
players.players.forEach{ dealOutCard(it) }
constructor(cards: Cards = Cards()) : this(DEALER_DISPLAY_NAME, cards)

// 17점이 넘으면 호출되어도 더 이상 카드를 추가하지 않는다
override fun addCard(card: Card) {
if (isAddable()) {
super.addCard(card)
} else {
isFinished = true
}
}

/**
* 플레이어들에게 1장씩 카드를 분배함
*/
fun dealOutCard(player: Player) {
player.cards.addCard(peekCard())
override fun getGameResult(win: Boolean) {
gameResults.add(if (win) GameResult.WIN else GameResult.LOSE)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getxxx와 같은 메서드 네이밍은 호출 시 데이터를 반환할 것이라 기대할 것 같은데요. 지금은 getGameResult 메서드는 내부 가변 컬렉션에 데이터를 추가해주는 반환값이 없는 사이드 이펙트를 유발하는 메서드 형태로 보이네요.

이 부분과 관련해서 저는 아래 정도의 의견이 있습니다.

  1. 이 형태를 유지할거라면 메서드 이름을 변경한다.
  2. 가변 컬렉션을 활용하는게 아니라 게임 결과 연산을 매번 수행하고 내부 상태에 맞는 결과를 반환한다.


/**
* 카드덱에서 랜덤한 카드를 1장 꺼냄
*/
private fun peekCard(): Card {
val random = Random.Default
val randomIndex = random.nextInt(cardDeck.cards.size)
return cardDeck.peekCard(randomIndex)
private fun isAddable(): Boolean {
return !isFinished && getScore() < LIMIT_SCORE
}

companion object {
const val DEAL_OUT_CARD_AMOUNT = 2
private const val DEALER_DISPLAY_NAME = "딜러"
const val LIMIT_SCORE = 17
}
}
40 changes: 40 additions & 0 deletions src/main/kotlin/blackjack/domain/Distributor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package blackjack.domain

import blackjack.domain.card.Card
import blackjack.domain.card.CardDeck
import kotlin.random.Random

class Distributor(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CardDeck에 테스트를 작성해주시긴 했지만 이 클래스를 호출한 뒤 dealer나 players에 카드가 정상적으로 분배 되었는지 등을 테스트해볼 수 있을 것 같습니다.

private val cardDeck: CardDeck
) {

/**
* 딜러와 플레이어들에게 2장씩 카드를 분배함
*/
fun dealOutCards(dealer: Dealer, players: Players) {
repeat(DEAL_OUT_CARD_AMOUNT) {
dealOutCard(dealer)
players.players.forEach { dealOutCard(it) }
}
}

/**
* 플레이어들에게 1장씩 카드를 분배함
*/
fun dealOutCard(player: Player) {
player.addCard(peekCard())
}

/**
* 카드덱에서 랜덤한 카드를 1장 꺼냄
*/
private fun peekCard(): Card {
val random = Random.Default
val randomIndex = random.nextInt(cardDeck.cards.size)
return cardDeck.peekCard(randomIndex)
}

companion object {
const val DEAL_OUT_CARD_AMOUNT = 2
}
}
13 changes: 12 additions & 1 deletion src/main/kotlin/blackjack/domain/Player.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package blackjack.domain

import blackjack.GameResult
import blackjack.domain.card.Card
import blackjack.domain.card.Cards

data class Player(
open class Player(
val name: String,
val cards: Cards = Cards()
) {
lateinit var gameResult: GameResult

override fun toString(): String {
return name
}

open fun addCard(card: Card) {
cards.addCard(card)
}

fun getScore(): Int {
return cards.getScore()
}

open fun getGameResult(win: Boolean) {
gameResult = if (win) GameResult.WIN else GameResult.LOSE
}
}
35 changes: 30 additions & 5 deletions src/main/kotlin/blackjack/view/DisplayView.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
package blackjack.view

import blackjack.GameResult
import blackjack.domain.Dealer
import blackjack.domain.Player
import blackjack.domain.Players

object DisplayView {

fun dealOutCards(players: Players) {
val playersName = players.players.joinToString()
println("${playersName}에게 2장의 카드 나누었습니다.")
println("딜러와 ${playersName}에게 2장의 카드 나누었습니다.")
}

fun cardsOfPlayers(players: Players) {
fun cardsOfPlayers(dealer: Dealer, players: Players) {
cardsOfDealer(dealer)
players.players.forEach {
cardsOfPlayer(it)
}
}

fun cardsOfDealer(dealer: Dealer) {
println("${dealer.name}: ${dealer.cards}")
}

fun cardsOfPlayer(player: Player) {
println("${player.name}카드: ${player.cards}")
}
Expand All @@ -24,10 +31,28 @@ object DisplayView {
println("${player}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)")
}

fun result(players: Players) {
players.players.forEach {
println("${it.name}카드: ${it.cards} - 결과: ${it.getScore()}")
fun dealOutAdditionalCard(received: Boolean) {
if (received) {
println("딜러는 16이하라 한장의 카드를 더 받았습니다.")
} else {
println("딜러는 17이상이라 카드를 더 받지 않았습니다.")
}
}

fun finalScore(dealer: Dealer, players: Players) {
printFinalScore(dealer)
players.players.forEach { printFinalScore(it) }
}

fun result(dealer: Dealer, players: Players) {
println("\n## 최종 승패")
val dealerWinCnt = dealer.gameResults.count { it == GameResult.WIN }
val dealerLoseCnt = dealer.gameResults.count { it == GameResult.LOSE }
println("딜러: ${dealerWinCnt}승 ${dealerLoseCnt}패")
players.players.forEach { println("${it.name}: ${it.gameResult.description}") }
}

private fun printFinalScore(player: Player) {
println("${player.name}카드: ${player.cards} - 결과: ${player.getScore()}")
}
}
38 changes: 38 additions & 0 deletions src/test/kotlin/blackjack/domain/DealerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package blackjack.domain

import blackjack.domain.card.Card
import blackjack.domain.card.CardNumber
import blackjack.domain.card.CardSymbol
import blackjack.domain.card.Cards
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.shouldBe

class DealerTest : StringSpec({
"17점이상의 카드를 가진 딜러는 카드를 받지 않는다" {
val cardsUnder17 = Cards(mutableListOf(Card(CardNumber.SIX, CardSymbol.DIAMOND), Card(CardNumber.JACK, CardSymbol.DIAMOND)))
val cardsScore17 = Cards(mutableListOf(Card(CardNumber.SEVEN, CardSymbol.HEART), Card(CardNumber.JACK, CardSymbol.HEART)))
val cardsOver17 = Cards(mutableListOf(Card(CardNumber.EIGHT, CardSymbol.SPADE), Card(CardNumber.JACK, CardSymbol.SPADE)))

val firstDealer = Dealer("cardsUnder17", cardsUnder17)
val scoreOfFirstDealer = firstDealer.getScore()

val secondDealer = Dealer("cardsScore17", cardsScore17)
val scoreOfSecondDealer = secondDealer.getScore()

val thirdDealer = Dealer("cardsOver17", cardsOver17)
val scoreOfThirdDealer = thirdDealer.getScore()

val additionalCard = Card(CardNumber.ACE, CardSymbol.CLOVER)

// when
firstDealer.addCard(additionalCard)
secondDealer.addCard(additionalCard)
thirdDealer.addCard(additionalCard)

// then
firstDealer.getScore() shouldBeGreaterThan scoreOfFirstDealer
secondDealer.getScore() shouldBe scoreOfSecondDealer
thirdDealer.getScore() shouldBe scoreOfThirdDealer
}
})