Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step2 - 블랙잭 #647

Open
wants to merge 17 commits into
base: sodp5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# kotlin-blackjack
# kotlin-blackjack

[x] 숫자와 문양을 가진 카드 구현

[x] 카드 뭉치인 덱 구현

[x] 점수 합계를 계산할 수 있는 카드목록 구현

[x] 이름과 카드 목록을 가질 수 있는 플레이어 구현

[x] 카드를 두 장 받는 기능 구현

[x] 카드를 한 장 더 받는 기능 구현

[x] 21이 초과했는지 판별하는 기능 구현

Comment on lines +1 to +16

Choose a reason for hiding this comment

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

요구 사항 정리 좋습니다~

[x] 카드를 그만 받는 기능 구현

[x] 입출력 기능 구현
41 changes: 41 additions & 0 deletions src/main/kotlin/blackjack/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package blackjack

import blackjack.domain.BlackJack
import blackjack.domain.Deck
import blackjack.domain.OnGoingPlayer
import blackjack.domain.Player
import blackjack.view.InputView
import blackjack.view.OutputView

fun main() {
val players = InputView.getPlayers()

val blackJack = BlackJack(Deck())

val finishedPlayers: List<Player> = blackJack.play(players)
.onEach { OutputView.printCards(it) }
.map { player ->
var onGoingPlayer = player

while (true) {
if (onGoingPlayer !is OnGoingPlayer) {
break
}

val isHit = InputView.isHit(onGoingPlayer.name)

if (!isHit) {
break
}

onGoingPlayer = blackJack.hit(onGoingPlayer)
OutputView.printCards(onGoingPlayer)
}
Comment on lines +20 to +33

Choose a reason for hiding this comment

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

do while을 사용해보는 것도 좋을 것 같아요 :)

자바와 다르게 kotlin의 do while은 특징이 있어요. 한번 찾아보시겠어요?


onGoingPlayer
}

finishedPlayers.forEach {
OutputView.printResult(it)
}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJack.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package blackjack.domain

class BlackJack(
private val deck: Deck,
) {
fun play(players: List<PreparedPlayer>): List<Player> {
return players.map { player ->
val drawnCards = Cards(
listOf(
deck.draw(),
deck.draw(),
)
)

OnGoingPlayer.of(player.name, drawnCards)
}
}

fun hit(player: OnGoingPlayer): Player {
val drawnCard = deck.draw()

return OnGoingPlayer.of(player.name, player.cards + drawnCard)
}

fun stay(player: Player): FinishedPlayer {
return FinishedPlayer(player)
}

companion object {
const val BlackJackedNumber = 21
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJackedPlayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.domain

class BlackJackedPlayer(
override val name: String,
override val cards: Cards,
) : Player
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/domain/BustedPlayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.domain

class BustedPlayer(
override val name: String,
override val cards: Cards,
) : Player
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/domain/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.domain

data class Card(
val suit: Suit,
val rank: Rank,
)
5 changes: 5 additions & 0 deletions src/main/kotlin/blackjack/domain/CardPointStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package blackjack.domain

fun interface CardPointStrategy {
fun getPoint(rank: Rank): Int
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/Cards.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain

@JvmInline
value class Cards(val value: List<Card>) {
fun getPoints(): Int {
val hardAcePoint = value.sumOf { card ->
HardAcePointStrategy().getPoint(card.rank)
}

val softAcePointStrategy = value.sumOf { card ->
SoftAcePointStrategy().getPoint(card.rank)
}

return softAcePointStrategy.takeIf { it <= BlackJack.BlackJackedNumber } ?: hardAcePoint
}

operator fun plus(other: Card): Cards {
return Cards(value + other)
}
Comment on lines +17 to +19

Choose a reason for hiding this comment

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

연산자 오버로딩 👍👍

}
23 changes: 23 additions & 0 deletions src/main/kotlin/blackjack/domain/Deck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package blackjack.domain

class Deck {
private val cards = ArrayList<Card>(setupDeck())

fun draw(): Card {
check(cards.isNotEmpty()) {
"덱에 남아있는 카드가 없습니다."
}

return cards.removeFirst()
}

private fun setupDeck(): List<Card> {

Choose a reason for hiding this comment

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

👍👍

return Suit.values()
.flatMap { suit ->
Rank.values().map { rank ->
Card(suit, rank)
}
}
.shuffled()
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/blackjack/domain/FinishedPlayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package blackjack.domain

class FinishedPlayer(player: Player) : Player by player
7 changes: 7 additions & 0 deletions src/main/kotlin/blackjack/domain/HardAcePointStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package blackjack.domain

class HardAcePointStrategy : CardPointStrategy {
override fun getPoint(rank: Rank): Int {
return rank.value
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/OnGoingPlayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain

class OnGoingPlayer(
override val name: String,
override val cards: Cards,
) : Player {
companion object {
fun of(name: String, cards: Cards): Player {
val hardAcePoint = cards.getPoints()

return if (hardAcePoint == BlackJack.BlackJackedNumber) {
BlackJackedPlayer(name, cards)
Comment on lines +11 to +12

Choose a reason for hiding this comment

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

참고로 블랙잭은 첫 두장의 카드가 21인 경우를 말해요. 여기서는 21점이면 언제나 블랙잭인 것 같네요.

} else if (hardAcePoint > BlackJack.BlackJackedNumber) {
BustedPlayer(name, cards)
} else {
OnGoingPlayer(name, cards)
}
}
Comment on lines +8 to +18

Choose a reason for hiding this comment

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

과제를 수행하면서 가장 어려웠던 포인트는 Player가 현재 들고 있는 카드셋이 어떤 상태인지 알아보는것이었습니다.
OnGoingPlayer.of로 체크해서 type으로 비교하고 있었는데, 비슷한 모양의 class가 여럿 생기고
누군가가 작업을 한다면 팩토리 메서드가 OnGoingPlayer에 있는것으로 예상하기 어려울것 같은데 적절한 위치를 잘 판단하지 못했습니다

확실히, 누군가가 작업을 한다면 여러 플레이어에 대한 상태 변화가 OnGoingPlayer에 정적 팩토리 메서드로 되어있다면 파악하기 어려울 것 같아요.

일단, 정적 팩토리 메서드 패턴은 저는 외부의 다른 값으로 특정 클래스의 생성자를 만든다고 생각해요. 하지만, 현재 BlackJackNumber에 따라서 플레이어의 객체가 변경되는 것은 생성자를 만드는 것보다는 도메인 로직이라고 생각해요.

도메인 로직이라고 생각하여 여럿 플레이어가 상태에 따라 변경하도록 한다면, 행위에 대해 추상화하여 상태 머신(State machine)을 도입해보는 것도 나쁘지 않다고 생각이 드네요.

경문님은 어떻게 생각하시나요?

}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/domain/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.domain

interface Player {
val name: String
val cards: Cards
}
7 changes: 7 additions & 0 deletions src/main/kotlin/blackjack/domain/PreparedPlayer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package blackjack.domain

class PreparedPlayer(
override val name: String,
) : Player {
override val cards: Cards = Cards(emptyList())
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/domain/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.domain

enum class Rank(val value: Int, val nameValue: String) {
Ace(1, "1"),
Two(2, "2"),
Three(3, "3"),
Four(4, "4"),
Five(5, "5"),
Six(6, "6"),
Seven(7, "7"),
Eight(8, "8"),
Nine(9, "9"),
Ten(10, "10"),
Jack(10, "J"),
Queen(10, "Q"),
King(10, "K"),
}
10 changes: 10 additions & 0 deletions src/main/kotlin/blackjack/domain/SoftAcePointStrategy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package blackjack.domain

class SoftAcePointStrategy : CardPointStrategy {

Choose a reason for hiding this comment

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

ACE에 대한 전략 👍

override fun getPoint(rank: Rank): Int {
return when(rank) {
Rank.Ace -> 11
else -> rank.value
}
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/domain/Suit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackjack.domain

enum class Suit(val value: String) {
Spade("스페이드"),
Club("클로버"),
Diamond("다이아몬드"),
Heart("하트"),
}
22 changes: 22 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package blackjack.view

import blackjack.domain.PreparedPlayer

object InputView {
fun getPlayers(): List<PreparedPlayer> {
println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)")

return readln().split(",")
.map { PreparedPlayer(it) }
}

fun isHit(name: String): Boolean {
println("${name}는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)")

return when (readln()) {
"y" -> true
"n" -> false
else -> error("y/n 으로만 입력해주세요")
}
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.view

import blackjack.domain.Player

object OutputView {
fun printCards(player: Player) {
println("${player.name}카드: ${player.cards.value.joinToString { "${it.rank.nameValue}${it.suit.value}" }}")
}

fun printResult(player: Player) {
println(
"${player.name}카드: ${player.cards.value.joinToString { "${it.rank.nameValue}${it.suit.value}" }} - 결과: ${
player.cards.getPoints()
}"
)
}
}
80 changes: 80 additions & 0 deletions src/test/kotlin/blackjack/domain/BlackJackTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package blackjack.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class BlackJackTest {
@Test
fun `PreparedPlayer는 카드 두 장을 받은 OngoingPlayer가 된다`() {
val deck = Deck()
val blackJack = BlackJack(deck)

val preparedPlayers = listOf(PreparedPlayer("a"))
val onGoingPlayers = blackJack.play(preparedPlayers)

val actualCardsCount = onGoingPlayers.first().cards.value.size
val expectedCardsCount = 2

assertThat(actualCardsCount).isEqualTo(expectedCardsCount)
}

@Test
fun `PreparedPlayer는 OngoingPlayer가 되더라도 이름이 유지된다`() {
val deck = Deck()
val blackJack = BlackJack(deck)

val preparedPlayers = listOf(PreparedPlayer("a"))
val onGoingPlayers = blackJack.play(preparedPlayers)

val actualPlayerName = onGoingPlayers.first().name
val expectedPlayerName = "a"

assertThat(actualPlayerName).isEqualTo(expectedPlayerName)
}

@Test
fun `진행중인 플레이어는 카드를 더 뽑을 수 있다`() {
val deck = Deck()
val blackJack = BlackJack(deck)
val player = OnGoingPlayer("a", Cards(listOf()))

val onGoingPlayer = blackJack.hit(player)

val actualCardsCount = onGoingPlayer.cards.value.size
val expectedCardsCount = 1

assertThat(actualCardsCount).isEqualTo(expectedCardsCount)
}

@Test
fun `카드를 뽑았을 때 21을 초과하면 버스트된 플레이어가 된다`() {
val deck = Deck()
val blackJack = BlackJack(deck)
val cards = Cards(
listOf(
Card(Suit.Spade, Rank.Ten),
Card(Suit.Spade, Rank.Ten),
Card(Suit.Spade, Rank.Ace),
)
)
val player = OnGoingPlayer("a", cards)

val bustedPlayer = blackJack.hit(player)

assertThat(bustedPlayer).isInstanceOf(BustedPlayer::class.java)
}

@Test
fun `스테이 하면 종료한 플레이어가 된다`() {
val deck = Deck()
val blackJack = BlackJack(deck)
val player = object : Player {
override val name: String = ""
override val cards: Cards = Cards(emptyList())
}

val finishedPlayer = blackJack.stay(player)

assertThat(finishedPlayer).isInstanceOf(FinishedPlayer::class.java)
}
}
Loading