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

[블랙잭 1단계] 타마 미션 제출합니다. #112

Open
wants to merge 30 commits into
base: etama123
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
295a389
docs: 기능 목록 작성
oungsi2000 Mar 4, 2025
8ae0885
feat:딜러와 플레이어 중 카드의 총합이 큰 사람이 이긴다 구현
oungsi2000 Mar 4, 2025
8d8e801
feat: Deck에서 카드를 뽑는 기능 구현
oungsi2000 Mar 4, 2025
51ddf2f
refactor:Result 계산 위치 수정
oungsi2000 Mar 4, 2025
5045eff
refactor: Card 상수 값 설정
oungsi2000 Mar 4, 2025
e96abaf
refactor: Card 상수 값 설정
oungsi2000 Mar 5, 2025
86e625f
feat: 전체 카드 반환 기능 구현
oungsi2000 Mar 5, 2025
fce0b80
refactor: 테스트 코드 수정 및 덱 기능 추가
oungsi2000 Mar 5, 2025
41248d4
feat: Card 생성 로직 구현
oungsi2000 Mar 5, 2025
b04d975
feat: Rank에 점수 프로퍼티 추가
oungsi2000 Mar 5, 2025
d2b8070
refactor: klint 수정
oungsi2000 Mar 5, 2025
9be1954
feat: 카드의 합 계산 로직 구현
oungsi2000 Mar 5, 2025
ea2f1f4
feat: KotlinDsl 구현
oungsi2000 Mar 6, 2025
e1917a8
feat: Dealer 카드 계산 로직 추가
oungsi2000 Mar 6, 2025
12077df
refactor: 승패 상태 상수명 변경
oungsi2000 Mar 6, 2025
930f701
feat: GameResult 클래스 생성
oungsi2000 Mar 6, 2025
6f112b1
feat: Participant 추상 클래스 추가
oungsi2000 Mar 6, 2025
efcfa88
feat: Application 추가
oungsi2000 Mar 6, 2025
4743be0
feat: 출력 양식에 맞게 포맷팅하는 기능 구현
oungsi2000 Mar 6, 2025
6290ee3
feat: Player name 프로퍼티 추가
oungsi2000 Mar 6, 2025
c009117
feat: Dealer 추가 카드 확인 함수 구현
oungsi2000 Mar 6, 2025
785c0da
feat: view 구현
oungsi2000 Mar 6, 2025
38ce545
feat: GameController 구현
oungsi2000 Mar 6, 2025
3419210
fix: 출력 값 포맷팅 수정
oungsi2000 Mar 6, 2025
9229b07
refactor: 추상 클래스 분리
oungsi2000 Mar 6, 2025
866ba67
test:추상 클래스 participant 테스트 작성
oungsi2000 Mar 6, 2025
2b02345
refactor: getPlayerResult 메서드 이관
oungsi2000 Mar 7, 2025
49a50fc
refactor(Participant): calculateTotalSum 로직 변경
oungsi2000 Mar 7, 2025
9c17bdc
refactor: 매직넘버 상수로 변경
oungsi2000 Mar 7, 2025
99cdd2c
refactor: PlayerResult 객체 생성
oungsi2000 Mar 7, 2025
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
45 changes: 44 additions & 1 deletion README.md
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
- [ ] 플레이어는 지급된 카드를 가진다
- [ ] 플레이어는 가지고 있는 카드의 합을 계산할 수 있다
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter", "5.11.4")
testImplementation("org.assertj", "assertj-core", "3.27.3")
testImplementation("io.kotest", "kotest-runner-junit5", "5.9.1")
implementation(kotlin("test"))
}

tasks {
Expand Down
Empty file removed src/main/kotlin/.gitkeep
Empty file.
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/Application.kt
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()
}
80 changes: 80 additions & 0 deletions src/main/kotlin/blackjack/GameController.kt
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
}
}
29 changes: 29 additions & 0 deletions src/main/kotlin/blackjack/domain/Card.kt
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()
}
Comment on lines +8 to +10
Copy link

Choose a reason for hiding this comment

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

toString을 오버라이드 한 이유에 대해 생각해보고 도메인에서 필요로 하는 것인가 고민해보면 좋겠어요 🤔


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
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/blackjack/domain/Dealer.kt
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
Copy link
Author

Choose a reason for hiding this comment

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

블랙잭 게임에서 딜러는

  1. 딜러는 첫 번째 카드만 오픈하기 때문에
  2. hit / stay 여부를 외부에서 입력받지 않기 때문에
  3. 로직 자체에서 hit의 여부가 결정되기 때문에
    딜러 내부에서 카드를 뽑는 로직을 설정했습니다.

하지만 이처럼 카드를 미리 뽑아두는게, 현실 세계에서의 게임 진행과 다르기 때문에 괜찮을 까 의문이 들었어요. 해당 코드에 대한 크롱의 생각이 궁금합니다 !

Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/blackjack/domain/DealerResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package blackjack.domain

class DealerResult {
Copy link

Choose a reason for hiding this comment

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

DealerResult, GameResult, PlayerResult 게임의 결과를 위한 길이 많이 험난해보여요 😵‍💫
블랙잭은 딜러와 플레이어간의 게임입니다. 이 말을 잘 곱씹어보면 플레이어의 승패는 딜러의 승패와 관련이 있는 것 같아요.

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++
}
}
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

import java.util.LinkedList

class Deck(shuffledDeck: List<Card>) {
private val deck = LinkedList(shuffledDeck)
Comment on lines +5 to +6
Copy link

Choose a reason for hiding this comment

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

LinkedList...요? 🤔


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
}
}
40 changes: 40 additions & 0 deletions src/main/kotlin/blackjack/domain/GameResult.kt
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
Copy link
Author

Choose a reason for hiding this comment

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

결과를 저장하는 로직에서 Dealer와 Player 모두 같은 Participant이므로, 결과도 비슷한 형태로 저장할 수 있지 않을까 고민했습니다.

현재 PlayerResult가 PlayerGameResultStatus를 가지는 것처럼, ResultParticipantGameResult를 가지는 방식으로 구조를 개선하는 것을 고민 중입니다.

Copy link

Choose a reason for hiding this comment

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

PlayerResult의 설계 의도와 방향을 설명해주실 수 있을까요? 🤠


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()
}
15 changes: 15 additions & 0 deletions src/main/kotlin/blackjack/domain/GameResultStatus.kt
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
Copy link
Author

Choose a reason for hiding this comment

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

OutputView에서 원하는 형식으로 출력하기 위해, Rank와 Suit 같은 클래스에 확장 함수를 추가했습니다.

이 함수들은 View의 역할에 가깝다고도 볼 수 있지만, 동시에 각 Enum 클래스와 밀접한 관련이 있기 때문에 해당 클래스 아래에 위치하는 것이 가독성과 직관성 측면에서 더 적절하다고 생각합니다.

크롱은 어떻게 생각하시나요? 이 방식이 적절한지, 혹은 더 나은 방법이 있을지 의견이 궁금합니다! 😊

Copy link

Choose a reason for hiding this comment

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

언급해주신 것처럼 View의 역할에 가까운 친구들로 보여요.
관련이 있는 enum클래스에 함께 작성하는 것과 OutputView에 작성하기 모두 장단점이 있어서 트레이드 오프의 영역이라고 볼 수 있어요.

현재의 구현 방식은 출력의 형태를 Win, Lose, Draw 로 변경하려 할 때 Domain역시 변경이 필요한데요.
이에 대해서도 고민해보면 좋을 것 같습니다.

그런데 이와는 별개로 꼭 확장 함수를 사용해야할까요? 🤔

}
41 changes: 41 additions & 0 deletions src/main/kotlin/blackjack/domain/Participant.kt
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()
Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

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

ACE가 포함된 카드의 총합을 구하는 방법을 다시 한 번 고민해볼까요? 🤔


return score
}

companion object {
const val BLACKJACK_LIMIT = 21
Copy link

Choose a reason for hiding this comment

The 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
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/blackjack/domain/Player.kt
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
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/blackjack/domain/PlayerResult.kt
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)
35 changes: 35 additions & 0 deletions src/main/kotlin/blackjack/domain/Rank.kt
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
Copy link

Choose a reason for hiding this comment

The 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"
}
}
Loading