-
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
2단계 - 블랙잭 #780
base: pablo730
Are you sure you want to change the base?
2단계 - 블랙잭 #780
Changes from 20 commits
3bfd849
e6317a6
bc7c509
8e5e26e
e70f2a0
139c060
a92a28b
cccb374
d0c2d3f
1e4e656
0d98f91
ebb3859
04af441
9c8df00
48995ce
384ef32
81d98d2
c591305
2f63e01
993e49b
ce3f865
5130f04
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,31 @@ | ||
# kotlin-blackjack | ||
# kotlin-blackjack | ||
|
||
### 기능 요구사항 | ||
- 블랙잭 게임을 변형한 프로그램을 구현한다. | ||
- 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. | ||
- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. | ||
- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. | ||
- 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. | ||
|
||
### 요구 사항 명세 정리 | ||
- [x] 카드 모양은 Spades, Clubs, Hearts, Diamonds 4가지로 이루어져있다 | ||
- [x] 카드는 1부터 10까지로 이루어져 있다 | ||
- [x] 숫자가 아닌 카드는 Jack, Queen, King으로 이루어져있으며 모두 숫자 10을 나타낸다 | ||
- [x] 카드덱에는 모든 카드가 존재해야한다 | ||
- [x] 카드덱에서 한번 꺼낸 카드는 카드덱에 더이상 존재할 수 없다 | ||
- [x] 플레이어는 본인이 원할 때 까지 카드를 계속 뽑을 수 있다 | ||
- [x] 플레이어는 본인이 가진 카드의 합산을 알 수 있다 | ||
- [x] 플레이어가 21을 넘을 경우 카드를 뽑으면 에러가 발생한다 | ||
- [x] 플레이어가 2명이 아닐 경우 에러가 발생한다 | ||
- [x] 블랙잭 게임 시작 시, 플레이어에게 카드 2장씩 배분한다 | ||
- [x] Ace(숫자1)는 1 또는 11로 계산할 수 있다 | ||
- [x] 플레이어에게 카드는 랜덤하게 제공된다 | ||
|
||
|
||
### 프로그래밍 요구 사항 | ||
- 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외 | ||
- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. | ||
- **모든 엔티티를 작게 유지한다.** | ||
- 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다. | ||
- 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다. | ||
- git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package blackjack.application | ||
|
||
import blackjack.domain.CardDeck | ||
import blackjack.domain.shuffledDeckGenerator | ||
import blackjack.view.BlackJackController | ||
|
||
fun main() { | ||
val blackJackController = BlackJackController() | ||
|
||
val players = blackJackController.initPlayers() | ||
|
||
val blackJack = blackJackController.initBlackJack(players, CardDeck(shuffledDeckGenerator())) | ||
|
||
blackJackController.addCard(blackJack) | ||
|
||
blackJackController.result(blackJack) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package blackjack.domain | ||
|
||
class BlackJack(val players: Players, val cardDeck: CardDeck) { | ||
init { | ||
repeat(INIT_CARD_DRAW_REPEAT) { players.players.forEach { it.addCard(cardDeck.drawCard()) } } | ||
} | ||
|
||
companion object { | ||
const val INIT_CARD_DRAW_REPEAT = 2 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
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,13 @@ | ||||||
package blackjack.domain | ||||||
|
||||||
data class CardDeck(val deck: MutableList<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. CardDeck의 내부 값을 외부에서 변경할 수 없도록해보면 어떨까요? 🤔 |
||||||
init { | ||||||
val defaultDeck = defaultDeckGenerator() | ||||||
require(deck.containsAll(defaultDeck)) { "블랙잭 게임을 하기 위해서는 모든 카드가 준비되어야합니다" } | ||||||
} | ||||||
|
||||||
fun drawCard(): Card { | ||||||
require(deck.isNotEmpty()) { "더이상 카드가 남아있지 않습니다" } | ||||||
return deck.removeAt(deck.lastIndex) | ||||||
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.
Suggested change
|
||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package blackjack.domain | ||
|
||
fun defaultDeckGenerator(): MutableList<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. 반환 타입이 가변리스트군요? 👀 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. 생성한 Card들을 자주 사용되게 될 것 같은데요. 또한 이런 관점에서 별도의 DeckGenerator를 만들기보다는 |
||
return createAllCards().toMutableList() | ||
} | ||
|
||
fun shuffledDeckGenerator(): MutableList<Card> { | ||
return createAllCards().shuffled().toMutableList() | ||
} | ||
|
||
private fun createAllCards(): List<Card> { | ||
return Suit.entries.flatMap { createAllCardsBySuit(it) } | ||
} | ||
|
||
private fun createAllCardsBySuit(suit: Suit): List<Card> { | ||
return Rank.entries.map { Card(suit, it) } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package blackjack.domain | ||
|
||
data class MutableCards(val cards: MutableList<Card>) { | ||
fun add(card: Card) { | ||
cards.add(card) | ||
} | ||
|
||
fun cardsToString(): String { | ||
return cards.joinToString(", ") { it.rank.alias + it.suit.alias } | ||
} | ||
|
||
fun sumValues(): Int { | ||
var sum = cards.sumOf { card -> card.rank.getNumber().max() } | ||
var aceCount = cards.count { card -> card.rank == Rank.ACE } | ||
|
||
while (sum > MAX_SUM_CARD_VALUES && aceCount > ACE_MIN_COUNT) { | ||
sum -= Rank.ACE.getNumber().max() | ||
sum += Rank.ACE.value | ||
aceCount-- | ||
} | ||
|
||
return sum | ||
} | ||
|
||
companion object { | ||
const val MAX_SUM_CARD_VALUES = 21 | ||
const val ACE_MIN_COUNT = 0 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package blackjack.domain | ||
|
||
data class Player(val playerName: PlayerName, val mutableCards: MutableCards) { | ||
fun addCard(card: 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. 현실에서 참가자가 카드를 한장 가져가는 것을 어떻게 표현하면 조금 더 역할이 드러날 수 있을까요? 👀 |
||
mutableCards.add(card) | ||
require(sumCardValues() <= MutableCards.MAX_SUM_CARD_VALUES) { | ||
playerName.name + " Bust! / " + mutableCards.cardsToString() | ||
} | ||
Comment on lines
+6
to
+8
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. |
||
} | ||
|
||
fun cardsToString(): String { | ||
return mutableCards.cardsToString() | ||
} | ||
|
||
fun sumCardValues(): Int { | ||
return mutableCards.sumValues() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package blackjack.domain | ||
|
||
@JvmInline | ||
value class PlayerName(val name: String) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,15 @@ | ||||||
package blackjack.domain | ||||||
|
||||||
data class Players(val players: List<Player>) { | ||||||
init { | ||||||
require(players.size == PLAYER_COUNT) { "블랙잭 게임에서 플레이어는 2명이어야 합니다" } | ||||||
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. 아래와 같이 에러 메시지에도 상수를 사용하면, 추후 값이 변경되더라도 에러 메시지를 추가로 수정하는 공수가 없을 것 같네요. 😃
Suggested change
추가적으로 |
||||||
} | ||||||
|
||||||
fun toPlayerNamesString(): String { | ||||||
return players.joinToString(", ") { it.playerName.name } | ||||||
} | ||||||
Comment on lines
+8
to
+10
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. Players 객체가 출력을 위한 행위를하고있네요? 🤔 |
||||||
|
||||||
companion object { | ||||||
const val PLAYER_COUNT = 2 | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package blackjack.domain | ||
|
||
enum class Rank(val value: Int, val alias: String) { | ||
ACE(1, "A"), | ||
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"), | ||
; | ||
|
||
fun getNumber(): List<Int> { | ||
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. getNumber의 결과는 사실상 |
||
return if (this == ACE) { | ||
listOf(value, 11) | ||
} else { | ||
listOf(value) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package blackjack.domain | ||
|
||
enum class Suit(val alias: String) { | ||
SPADES("스페이드"), | ||
HEARTS("하트"), | ||
DIAMONDS("다이아몬드"), | ||
CLUBS("클로버"), | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package blackjack.view | ||
|
||
import blackjack.domain.BlackJack | ||
import blackjack.domain.CardDeck | ||
import blackjack.domain.Player | ||
|
||
fun addCardView(blackJack: BlackJack) { | ||
val players = blackJack.players.players | ||
println() | ||
players.forEach { addCardViewByPlayer(it, blackJack.cardDeck) } | ||
} | ||
|
||
fun addCardViewByPlayer( | ||
player: Player, | ||
cardDeck: CardDeck, | ||
) { | ||
var inputYesOrNo: String? = "y" | ||
while (inputYesOrNo == "y") { | ||
inputYesOrNo = addCardReturnYesOrNo(player, cardDeck) | ||
} | ||
} | ||
|
||
fun addCardReturnYesOrNo( | ||
player: Player, | ||
cardDeck: CardDeck, | ||
): String { | ||
println(player.playerName.name + "는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)") | ||
|
||
val inputYesOrNo = readlnOrNull() | ||
requireNotNull(inputYesOrNo) { "어떠한 값도 입력하지 않았습니다" } | ||
require(inputYesOrNo == "y" || inputYesOrNo == "n") { "y 또는 n을 올바르게 입력되지 않았습니다" } | ||
|
||
if (inputYesOrNo == "y") { | ||
player.addCard(cardDeck.drawCard()) | ||
println(player.mutableCards.cardsToString()) | ||
} | ||
|
||
return inputYesOrNo | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,38 @@ | ||||||
package blackjack.view | ||||||
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. Controller클래스도 view 패키지에 위치하고있네요. 👀 |
||||||
|
||||||
import blackjack.domain.BlackJack | ||||||
import blackjack.domain.CardDeck | ||||||
import blackjack.domain.MutableCards | ||||||
import blackjack.domain.Player | ||||||
import blackjack.domain.PlayerName | ||||||
import blackjack.domain.Players | ||||||
|
||||||
class BlackJackController { | ||||||
fun initPlayers(): Players { | ||||||
val players = | ||||||
Players( | ||||||
players = | ||||||
inputPlayerNameView().map { | ||||||
Player(PlayerName(it), MutableCards(cards = mutableListOf())) | ||||||
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. Player는 처음에 아무런 카드를 가지고있지 않을텐데요.
Suggested change
|
||||||
}, | ||||||
) | ||||||
return players | ||||||
} | ||||||
|
||||||
fun initBlackJack( | ||||||
players: Players, | ||||||
cardSet: CardDeck, | ||||||
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. 메서드 파라미터의 네이밍도 |
||||||
): BlackJack { | ||||||
val blackJack = BlackJack(players, cardSet) | ||||||
initBlackJackView(blackJack) | ||||||
return blackJack | ||||||
} | ||||||
|
||||||
fun addCard(blackJack: BlackJack) { | ||||||
addCardView(blackJack) | ||||||
} | ||||||
|
||||||
fun result(blackJack: BlackJack) { | ||||||
resultView(blackJack) | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package blackjack.view | ||
|
||
import blackjack.domain.BlackJack | ||
|
||
fun initBlackJackView(blackJack: BlackJack) { | ||
println("\n" + blackJack.players.toPlayerNamesString() + "에게 2장의 나누었습니다.") | ||
blackJack.players.players.forEach { println(it.playerName.name + "카드: " + it.cardsToString()) } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package blackjack.view | ||
|
||
fun inputPlayerNameView(): List<String> { | ||
println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)") | ||
|
||
val inputPlayerNames: String? = readlnOrNull() | ||
requireNotNull(inputPlayerNames) { "플레이어 이름들이 입력되지 않았습니다" } | ||
|
||
val playerNames = inputPlayerNames.split(",") | ||
require(playerNames.size == 2) { "플레이어는 최소 2명 이상 입력되어야합니다" } | ||
|
||
return playerNames | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package blackjack.view | ||
|
||
import blackjack.domain.BlackJack | ||
|
||
fun resultView(blackJack: BlackJack) { | ||
println() | ||
blackJack.players.players.forEach { | ||
println(it.playerName.name + "카드: " + it.mutableCards.cardsToString() + " - 결과: " + it.sumCardValues()) | ||
} | ||
} |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package blackjack.domain | ||
|
||
import blackjack.util.createPlayers | ||
import io.kotest.matchers.equals.shouldBeEqual | ||
import org.junit.jupiter.api.Test | ||
|
||
class BlackJackTest { | ||
@Test | ||
fun `블랙잭 게임을 시작할 때 플레이어에게 카드 2장씩 주고 시작해야한다`() { | ||
val players = createPlayers("pablo", "musk") | ||
val blackJack = BlackJack(players, CardDeck(defaultDeckGenerator())) | ||
|
||
blackJack.players.players.forEach { | ||
it.mutableCards.cards.size shouldBeEqual 2 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package blackjack.domain | ||
|
||
import io.kotest.assertions.throwables.shouldThrowWithMessage | ||
import io.kotest.matchers.equals.shouldBeEqual | ||
import org.junit.jupiter.api.Test | ||
|
||
class CardDeckTest { | ||
@Test | ||
fun `CardDeck에 모든 카드가 준비되어 있지 않으면 에러가 발생한다`() { | ||
shouldThrowWithMessage<IllegalArgumentException>(message = "블랙잭 게임을 하기 위해서는 모든 카드가 준비되어야합니다") { | ||
CardDeck(mutableListOf(Card(Suit.DIAMONDS, Rank.ACE), Card(Suit.HEARTS, Rank.NINE))) | ||
} | ||
} | ||
|
||
@Test | ||
fun `Deck에 더이상 카드가 남아있지 않을 때 카드를 꺼내면 에러가 발생한다`() { | ||
val cardDeck = CardDeck(defaultDeckGenerator()) | ||
shouldThrowWithMessage<IllegalArgumentException>(message = "더이상 카드가 남아있지 않습니다") { | ||
repeat(53) { cardDeck.drawCard() } | ||
} | ||
} | ||
|
||
@Test | ||
fun `Deck에서 한번 꺼낸 카드는 더이상 Deck에 남아있지 않아야한다`() { | ||
val cardDeck = CardDeck(defaultDeckGenerator()) | ||
val card = cardDeck.drawCard() | ||
cardDeck.deck.contains(card) shouldBeEqual false | ||
} | ||
} |
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.
player 객체에게 메시지를 보내보면 어떨까요?
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.
이 부분도 같이 확인 부탁드릴게요. 😃