-
Notifications
You must be signed in to change notification settings - Fork 313
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
base: sodp5
Are you sure you want to change the base?
Step2 - 블랙잭 #647
Changes from all commits
e039818
a77fbed
cd11462
4e905ed
17c30fb
1aa3e0e
3134426
f7b3ae6
28b9075
bec11d4
5d53b83
00b8321
a9fd79e
89d7814
0630506
571e425
ce76756
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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이 초과했는지 판별하는 기능 구현 | ||
|
||
[x] 카드를 그만 받는 기능 구현 | ||
|
||
[x] 입출력 기능 구현 |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do while을 사용해보는 것도 좋을 것 같아요 :) 자바와 다르게 kotlin의 do while은 특징이 있어요. 한번 찾아보시겠어요? |
||
|
||
onGoingPlayer | ||
} | ||
|
||
finishedPlayers.forEach { | ||
OutputView.printResult(it) | ||
} | ||
} |
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 | ||
} | ||
} |
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 |
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 |
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, | ||
) |
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 | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 연산자 오버로딩 👍👍 |
||
} |
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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package blackjack.domain | ||
|
||
class FinishedPlayer(player: Player) : Player by player |
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 | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
확실히, 누군가가 작업을 한다면 여러 플레이어에 대한 상태 변화가 OnGoingPlayer에 정적 팩토리 메서드로 되어있다면 파악하기 어려울 것 같아요. 일단, 정적 팩토리 메서드 패턴은 저는 외부의 다른 값으로 특정 클래스의 생성자를 만든다고 생각해요. 하지만, 현재 BlackJackNumber에 따라서 플레이어의 객체가 변경되는 것은 생성자를 만드는 것보다는 도메인 로직이라고 생각해요. 도메인 로직이라고 생각하여 여럿 플레이어가 상태에 따라 변경하도록 한다면, 행위에 대해 추상화하여 상태 머신(State machine)을 도입해보는 것도 나쁘지 않다고 생각이 드네요. 경문님은 어떻게 생각하시나요? |
||
} | ||
} |
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 | ||
} |
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()) | ||
} |
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"), | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package blackjack.domain | ||
|
||
class SoftAcePointStrategy : CardPointStrategy { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} | ||
} |
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("하트"), | ||
} |
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 으로만 입력해주세요") | ||
} | ||
} | ||
} |
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() | ||
}" | ||
) | ||
} | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요구 사항 정리 좋습니다~