-
Notifications
You must be signed in to change notification settings - Fork 82
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
[블랙잭 1단계] 타마 미션 제출합니다. #112
base: etama123
Are you sure you want to change the base?
Changes from all commits
295a389
8ae0885
8d8e801
51ddf2f
5045eff
e96abaf
86e625f
fce0b80
41248d4
b04d975
d2b8070
9be1954
ea2f1f4
e1917a8
12077df
930f701
6f112b1
efcfa88
4743be0
6290ee3
c009117
785c0da
38ce545
3419210
9229b07
866ba67
2b02345
49a50fc
9c17bdc
99cdd2c
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,44 @@ | ||
# kotlin-blackjack | ||
# kotlin-blackjack | ||
|
||
## 기능 요구 사항 | ||
블랙잭 게임을 변형한 프로그램을 구현한다. | ||
블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. | ||
|
||
카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. | ||
|
||
|
||
게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. | ||
딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. | ||
게임을 완료한 후 각 플레이어별로 승패를 출력한다. | ||
|
||
|
||
## 기능 구현 목록 | ||
1. 플레이어는 2장의 카드를 지급받는다 | ||
- [x] 카드는 A~10,K,Q,J 숫자에 스페이드, 하트, 다이아몬드, 클로버 문양이 각각 있다 (13 x 4 = 52장) | ||
- [x] 게임 중 각 카드는 중복될 수 없다 | ||
2. 딜러는 2장의 카드를 지급받는다 | ||
- [x] 딜러는 첫번째 카드를 오픈한다 | ||
3. 딜러의 카드의 합을 계산할 수 있다 | ||
- [x] 딜러 카드의 합이 16 이하인 경우, 카드의 합이 17 이상이 될 때까지 카드를 받는다 | ||
- [x] 딜러 카드의 합이 17 이상인 경우, 카드를 받을 수 없다 | ||
4. 플레이어의 카드의 합을 계산할 수 있다 | ||
- [x] Ace는 1 또는 11로 계산할 수 있다 | ||
- [x] King, Queen, Jack은 각각 10으로 계산한다 | ||
5. 플레이어는 버스트가 되기 전까지 추가 카드를 받을 지 선택할 수 있다 | ||
- [x] 플레이어는 y/n 로 카드를 받을 지 선택할 수 있다 | ||
- [x] 카드를 받는 도중 숫자가 22가 넘어간 사람은 결과 여부와 상관없이 무조건 진다 | ||
- [x] 받았는데, 21이 넘지 않은 경우, 다시 카드를 받을 지 선택할 수 있다 | ||
6. 최종 승패를 정한다 | ||
- [x] 딜러와 플레이어 중 카드의 총합이 큰 사람이 이긴다 | ||
|
||
### Card | ||
### Deck | ||
- [x] 덱의 사이즈는 52여야 합니다 | ||
- [x] 카드는 중복될 수 없습니다 | ||
- [x] 카드 번호는 0부터 51까지 입니다 | ||
- [x] 카드를 뽑을 수 있다 | ||
- [x] 뽑은 카드는 덱에 존재하지 않는다 | ||
- [x] 덱에서 카드는 최대 52번 뽑을 수 있다 | ||
### Player | ||
- [ ] 플레이어는 지급된 카드를 가진다 | ||
- [ ] 플레이어는 가지고 있는 카드의 합을 계산할 수 있다 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package blackjack | ||
|
||
import blackjack.view.InputView | ||
import blackjack.view.OutputView | ||
|
||
fun main() { | ||
GameController( | ||
InputView, | ||
OutputView, | ||
).run() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package blackjack | ||
|
||
import blackjack.domain.Card | ||
import blackjack.domain.Dealer | ||
import blackjack.domain.Deck | ||
import blackjack.domain.GameResult | ||
import blackjack.domain.Player | ||
import blackjack.view.InputView | ||
import blackjack.view.OutputView | ||
|
||
class GameController( | ||
private val inputView: InputView, | ||
private val outputView: OutputView, | ||
) { | ||
private val shuffledCards: List<Card> = Card.getAllCard().shuffled() | ||
private val deck = Deck(shuffledCards) | ||
|
||
fun run() { | ||
val dealer = Dealer() | ||
val players: List<Player> = getPlayers() | ||
|
||
dealer.getCard(deck) | ||
setInitialPlayerCards(players) | ||
outputView.showInitialCards(dealer, players) | ||
|
||
askPlayerHit(players) | ||
|
||
if (dealer.hasAdditionalCard()) { | ||
outputView.printDealerHaveAdditionalCard() | ||
} | ||
|
||
outputView.printFinalCards(dealer, players) | ||
|
||
showResult(dealer, players) | ||
} | ||
|
||
private fun getPlayers(): List<Player> { | ||
return inputView.getPlayerNames().map { playerName -> | ||
Player(playerName) | ||
} | ||
} | ||
|
||
private fun setInitialPlayerCards(players: List<Player>) { | ||
players.forEach { player -> | ||
repeat(INITIAL_CARD_COUNT) { | ||
player.addCard(deck.draw()) | ||
} | ||
} | ||
} | ||
|
||
private fun askPlayerHit(players: List<Player>) { | ||
players.forEach { player -> | ||
handlePlayerHit(player) | ||
} | ||
} | ||
|
||
private fun handlePlayerHit(player: Player) { | ||
while (player.canHit()) { | ||
val result = inputView.askPlayerHit(player.name) | ||
if (result) { | ||
player.addCard(deck.draw()) | ||
outputView.printPlayerCards(player) | ||
} else { | ||
break | ||
} | ||
} | ||
} | ||
|
||
private fun showResult( | ||
dealer: Dealer, | ||
players: List<Player>, | ||
) { | ||
val result = GameResult(dealer, players).getResult() | ||
outputView.printGameResult(result) | ||
} | ||
|
||
companion object { | ||
private const val INITIAL_CARD_COUNT = 2 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package blackjack.domain | ||
|
||
import java.lang.IllegalArgumentException | ||
|
||
class Card private constructor(val rank: Rank, val suit: Suit) { | ||
fun getScore() = this.rank.score | ||
|
||
override fun toString(): String { | ||
return rank.toDisplayName() + suit.toDisplayName() | ||
} | ||
|
||
companion object { | ||
private val POOL: List<Card> = | ||
Rank.entries.flatMap { rank -> | ||
Suit.entries.map { suit -> | ||
Card(rank, suit) | ||
} | ||
} | ||
|
||
fun of( | ||
rank: Rank, | ||
suit: Suit, | ||
): Card { | ||
return POOL.find { it.rank == rank && it.suit == suit } ?: throw IllegalArgumentException() | ||
} | ||
|
||
fun getAllCard() = POOL | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package blackjack.domain | ||
|
||
class Dealer : Participant() { | ||
override val hitThreshold: Int | ||
get() = DEALER_HIT_THRESHOLD | ||
|
||
fun getCard(deck: Deck) { | ||
while (canHit()) { | ||
addCard(deck.draw()) | ||
} | ||
} | ||
Comment on lines
+7
to
+11
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. 카드를 미리 뽑아둔다는 말을 이해하지 못했는데, 더 자세하게 설명해주시면 이야기를 나눠볼 수 있을 것 같아요. 이와는 별개로 현실 세계의 블랙잭 딜러랑 객체 딜러가 어떤 차이가 있는지 잘 생각해보면 좋겠어요 🤠 |
||
|
||
fun hasAdditionalCard(): Boolean { | ||
return cards.size > INITIAL_CARD_COUNT | ||
} | ||
Comment on lines
+13
to
+15
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. 이 메서드가 필요한 이유는 무엇인가요? 🤔 |
||
|
||
companion object { | ||
const val DEALER_HIT_THRESHOLD = 17 | ||
const val INITIAL_CARD_COUNT = 2 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package blackjack.domain | ||
|
||
class DealerResult { | ||
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.
|
||
var win: Int = 0 | ||
private set | ||
var lose: Int = 0 | ||
private set | ||
var draw: Int = 0 | ||
private set | ||
|
||
fun addWin() { | ||
win++ | ||
} | ||
|
||
fun addLose() { | ||
lose++ | ||
} | ||
|
||
fun addDraw() { | ||
draw++ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package blackjack.domain | ||
|
||
import java.util.LinkedList | ||
|
||
class Deck(shuffledDeck: List<Card>) { | ||
private val deck = LinkedList(shuffledDeck) | ||
Comment on lines
+5
to
+6
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.
|
||
|
||
init { | ||
require(shuffledDeck.size == MAXIMUM_DECK_SIZE) { "덱의 사이즈는 52여야 합니다" } | ||
require(shuffledDeck.distinct().size == MAXIMUM_DECK_SIZE) { "카드는 중복될 수 없습니다" } | ||
} | ||
|
||
fun draw(): Card { | ||
require(deck.isNotEmpty()) { "덱이 비어 있습니다" } | ||
return deck.poll() | ||
} | ||
|
||
fun getSize() = deck.size | ||
|
||
companion object { | ||
const val MAXIMUM_DECK_SIZE = 52 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package blackjack.domain | ||
|
||
class GameResult(private val dealer: Dealer, val players: List<Player>) { | ||
val dealerResult = DealerResult() | ||
private val playerResult: MutableList<PlayerResult> = mutableListOf() | ||
Comment on lines
+4
to
+5
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. 결과를 저장하는 로직에서 Dealer와 Player 모두 같은 Participant이므로, 결과도 비슷한 형태로 저장할 수 있지 않을까 고민했습니다. 현재 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.
|
||
|
||
private fun updateResult( | ||
player: Player, | ||
status: GameResultStatus, | ||
) { | ||
playerResult.add(PlayerResult(player, status)) | ||
|
||
when (status) { | ||
GameResultStatus.PLAYER_WIN -> dealerResult.addLose() | ||
GameResultStatus.PLAYER_LOSE -> dealerResult.addWin() | ||
GameResultStatus.DRAW -> dealerResult.addDraw() | ||
} | ||
} | ||
|
||
fun getPlayerResult(player: Player): GameResultStatus { | ||
if (player.isBust()) return GameResultStatus.PLAYER_LOSE | ||
if (dealer.isBust()) return GameResultStatus.PLAYER_WIN | ||
return when { | ||
dealer.totalSum > player.totalSum -> GameResultStatus.PLAYER_LOSE | ||
player.totalSum > dealer.totalSum -> GameResultStatus.PLAYER_WIN | ||
player.totalSum == dealer.totalSum -> GameResultStatus.DRAW | ||
else -> throw IllegalArgumentException() | ||
} | ||
} | ||
|
||
fun getResult(): GameResult { | ||
players.forEach { player -> | ||
val playerResult = getPlayerResult(player) | ||
updateResult(player, playerResult) | ||
} | ||
return this | ||
} | ||
|
||
fun getAllPlayerResult(): List<PlayerResult> = playerResult.toList() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package blackjack.domain | ||
|
||
enum class GameResultStatus { | ||
PLAYER_WIN, | ||
PLAYER_LOSE, | ||
DRAW, | ||
} | ||
|
||
fun GameResultStatus.toDisplayName(): String { | ||
return when (this) { | ||
GameResultStatus.PLAYER_WIN -> "승" | ||
GameResultStatus.PLAYER_LOSE -> "패" | ||
GameResultStatus.DRAW -> "무" | ||
} | ||
Comment on lines
+9
to
+14
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. OutputView에서 원하는 형식으로 출력하기 위해, Rank와 Suit 같은 클래스에 확장 함수를 추가했습니다. 이 함수들은 View의 역할에 가깝다고도 볼 수 있지만, 동시에 각 Enum 클래스와 밀접한 관련이 있기 때문에 해당 클래스 아래에 위치하는 것이 가독성과 직관성 측면에서 더 적절하다고 생각합니다. 크롱은 어떻게 생각하시나요? 이 방식이 적절한지, 혹은 더 나은 방법이 있을지 의견이 궁금합니다! 😊 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. 언급해주신 것처럼 View의 역할에 가까운 친구들로 보여요. 현재의 구현 방식은 출력의 형태를 Win, Lose, Draw 로 변경하려 할 때 Domain역시 변경이 필요한데요. 그런데 이와는 별개로 꼭 확장 함수를 사용해야할까요? 🤔 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package blackjack.domain | ||
|
||
abstract class Participant { | ||
var totalSum: Int = 0 | ||
private set | ||
val cards: MutableList<Card> = 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. 카드들을 갖는 일급 컬렉션을 만들어볼까요? 어떤 장점을 얻을 수 있을까요? 🤔 |
||
|
||
abstract val hitThreshold: Int | ||
|
||
fun addCard(card: Card) { | ||
cards.add(card) | ||
totalSum = calculateTotalSum() | ||
} | ||
Comment on lines
+10
to
+13
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. addCard라는 메서드명을 보고 카드를 뽑겠구나라고 기대했지만 내부에서는 총합을 함께 계산하고 있어요. |
||
|
||
fun isBust(): Boolean { | ||
return totalSum > BLACKJACK_LIMIT | ||
} | ||
|
||
fun canHit(): Boolean { | ||
return totalSum < hitThreshold | ||
} | ||
|
||
private fun calculateTotalSum(): Int { | ||
var score = cards.sumOf { it.getScore() } | ||
var aceCount = cards.count { it.rank == Rank.ACE } | ||
|
||
while (score > BLACKJACK_LIMIT && aceCount > 0) { | ||
score -= ACE_SCORE_DIFFERENCE | ||
aceCount-- | ||
} | ||
Comment on lines
+23
to
+30
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가 포함된 카드의 총합을 구하는 방법을 다시 한 번 고민해볼까요? 🤔 |
||
|
||
return score | ||
} | ||
|
||
companion object { | ||
const val BLACKJACK_LIMIT = 21 | ||
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은 BUST의 기준! 으로 네이밍하는게 더 적절해 보이네요 🤠 |
||
private const val ACE_HIGH = 11 | ||
private const val ACE_LOW = 1 | ||
private const val ACE_SCORE_DIFFERENCE = ACE_HIGH - ACE_LOW | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package blackjack.domain | ||
|
||
class Player(val name: String) : Participant() { | ||
override val hitThreshold: Int | ||
get() = PLAYER_HIT_THRESHOLD | ||
|
||
companion object { | ||
const val PLAYER_HIT_THRESHOLD = 21 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package blackjack.domain | ||
|
||
data class PlayerResult(val player: Player, val status: GameResultStatus) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package blackjack.domain | ||
|
||
enum class Rank(val score: Int) { | ||
ACE(11), | ||
Comment on lines
+3
to
+4
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는 몇이라고 생각할까요? 🤔 |
||
TWO(2), | ||
THREE(3), | ||
FOUR(4), | ||
FIVE(5), | ||
SIX(6), | ||
SEVEN(7), | ||
EIGHT(8), | ||
NINE(9), | ||
TEN(10), | ||
JACK(10), | ||
QUEEN(10), | ||
KING(10), | ||
} | ||
|
||
fun Rank.toDisplayName(): String { | ||
return when (this) { | ||
Rank.ACE -> "A" | ||
Rank.TWO -> "2" | ||
Rank.THREE -> "3" | ||
Rank.FOUR -> "4" | ||
Rank.FIVE -> "5" | ||
Rank.SIX -> "6" | ||
Rank.SEVEN -> "7" | ||
Rank.EIGHT -> "8" | ||
Rank.NINE -> "9" | ||
Rank.TEN -> "10" | ||
Rank.JACK -> "J" | ||
Rank.QUEEN -> "Q" | ||
Rank.KING -> "K" | ||
} | ||
} |
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.
toString
을 오버라이드 한 이유에 대해 생각해보고 도메인에서 필요로 하는 것인가 고민해보면 좋겠어요 🤔