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

2단계 - 블랙잭 #780

Open
wants to merge 22 commits into
base: pablo730
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3bfd849
dsl 실습 -> 귀가 후 집에서
Nov 26, 2024
e6317a6
Skills 추가 구현 및 테스트, Person 관련 코드 테스트 코드와 분리
Pablo730 Nov 30, 2024
bc7c509
Languages 관련 기능 추가 및 테스트 구현
Pablo730 Nov 30, 2024
8e5e26e
초기 요구사항 명세 정리
Pablo730 Dec 7, 2024
e70f2a0
모든 카드는 중복이 불가능 하도록 미리 생성하여 관리하는 기능 구현 및 테스트
Pablo730 Dec 7, 2024
139c060
Ace(숫자1)는 1 또는 11로 계산하는 기능 구현 및 테스트
Pablo730 Dec 7, 2024
a92a28b
Suit, Rank enum 활용에 따른 Card 객체 수정
Pablo730 Dec 7, 2024
cccb374
블랙잭 게임 속 카드를 관리할 CardDeck 객체 구현 및 테스트
Pablo730 Dec 7, 2024
d0c2d3f
플레이어가 가질 수 있는 카드 더미를 나타낼 MutableCards 객체 구현 및 테스트
Pablo730 Dec 7, 2024
1e4e656
블랙잭 플레이어 객체 구현 및 테스트
Pablo730 Dec 7, 2024
0d98f91
README 수정
Pablo730 Dec 7, 2024
ebb3859
블랙잭 게임에 참여하는 플레이어들을 관리할 Players 객체 구현 및 테스트
Pablo730 Dec 7, 2024
04af441
블랙잭 게임 초기 로직 구현 및 테스트
Pablo730 Dec 7, 2024
9c8df00
테스트 코드 작성을 위한 util 함수 CreatePlayers 구현
Pablo730 Dec 7, 2024
48995ce
불필요한 gradle 라이브러리 설치 원복
Pablo730 Dec 7, 2024
384ef32
view 로직 구현
Pablo730 Dec 7, 2024
81d98d2
불필요한 테스트 코드 제거
Pablo730 Dec 7, 2024
c591305
formating 수정
Pablo730 Dec 7, 2024
2f63e01
sealed class 활용
Pablo730 Dec 7, 2024
993e49b
Merge remote-tracking branch 'upstream/pablo730' into feature/blackjack
Pablo730 Dec 7, 2024
ce3f865
불필요한 라이브러리 제거
Pablo730 Dec 8, 2024
5130f04
sealed class 활용
Pablo730 Dec 8, 2024
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
32 changes: 31 additions & 1 deletion README.md
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 파일에 정리한 기능 목록 단위로 추가한다.
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/application/Main.kt
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)
}
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJack.kt
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()) } }
Copy link

Choose a reason for hiding this comment

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

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 INIT_CARD_DRAW_REPEAT = 2
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/blackjack/domain/Card.kt
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)
13 changes: 13 additions & 0 deletions src/main/kotlin/blackjack/domain/CardDeck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package blackjack.domain

data class CardDeck(val deck: MutableList<Card>) {
Copy link

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

removeLast()를 사용하도록 변경해볼 수 있을 것 같아요. 😃

Suggested change
return deck.removeAt(deck.lastIndex)
return deck.removeLast()

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

fun defaultDeckGenerator(): MutableList<Card> {
Copy link

Choose a reason for hiding this comment

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

반환 타입이 가변리스트군요? 👀
가급적 가변 리스트보다는 불변 리스트를 사용해보면 어떨까요? 😃

Copy link

Choose a reason for hiding this comment

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

생성한 Card들을 자주 사용되게 될 것 같은데요.
모든 카드에대해 캐싱해서 재사용하면어떨까요? 😃

또한 이런 관점에서 별도의 DeckGenerator를 만들기보다는 Card의 상수로 두면 어떨까요?

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) }
}
29 changes: 29 additions & 0 deletions src/main/kotlin/blackjack/domain/MutableCards.kt
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
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/blackjack/domain/Player.kt
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) {
Copy link

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

현재 21점을 넘어서 Bust가 되는 경우에는 바로 블랙잭 게임이 종료되는대요.
플레이어의 점수가 Bust되더라도 블랙잭 게임이 바로 종료되면 안될 것 같아요. 🙄

image

}

fun cardsToString(): String {
return mutableCards.cardsToString()
}

fun sumCardValues(): Int {
return mutableCards.sumValues()
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/blackjack/domain/PlayerName.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package blackjack.domain

@JvmInline
value class PlayerName(val name: String)
15 changes: 15 additions & 0 deletions src/main/kotlin/blackjack/domain/Players.kt
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명이어야 합니다" }
Copy link

Choose a reason for hiding this comment

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

아래와 같이 에러 메시지에도 상수를 사용하면, 추후 값이 변경되더라도 에러 메시지를 추가로 수정하는 공수가 없을 것 같네요. 😃

Suggested change
require(players.size == PLAYER_COUNT) { "블랙잭 게임에서 플레이어는 2명이어야 합니다" }
require(players.size == PLAYER_COUNT) { "블랙잭 게임에서 플레이어는 ${PLAYER_COUNT}명이어야 합니다" }

추가적으로 PLAYER_COUNT는 Players 클래스 내부에서만 사용되는 것 같은데, 접근 제한자를 private로 제한해주시면 좋을 것 같아요. 😁

}

fun toPlayerNamesString(): String {
return players.joinToString(", ") { it.playerName.name }
}
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.

Players 객체가 출력을 위한 행위를하고있네요? 🤔
바로 아래로직에서 한 것처럼 InitBlackJackView에서 출력을 위해 프로퍼티로부터 직접 name을 가져와서 출력을 위한 형태로 변경해보면 어떨까요?


companion object {
const val PLAYER_COUNT = 2
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/blackjack/domain/Rank.kt
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> {
Copy link

Choose a reason for hiding this comment

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

getNumber의 결과는 사실상 value를 가져오는거니 getValue가 조금 더 적절한 네이밍인 것 같아요. 🤔
value는 의미가 조금 애매해지는 것 같아서 점수와 같은 의미를 가지는 네이밍을 고려해보시면 조금 더 네이밍으로 의도를 드러낼 수 있지 않을까 싶어요. 😁

return if (this == ACE) {
listOf(value, 11)
} else {
listOf(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 alias: String) {
SPADES("스페이드"),
HEARTS("하트"),
DIAMONDS("다이아몬드"),
CLUBS("클로버"),
}
39 changes: 39 additions & 0 deletions src/main/kotlin/blackjack/view/AddCardView.kt
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
}
38 changes: 38 additions & 0 deletions src/main/kotlin/blackjack/view/BlackJackController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package blackjack.view
Copy link

Choose a reason for hiding this comment

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

Controller클래스도 view 패키지에 위치하고있네요. 👀
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()))
Copy link

Choose a reason for hiding this comment

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

Player는 처음에 아무런 카드를 가지고있지 않을텐데요.
default arguments를 활용해서 아래와 같이 변경해보면 어떨까요?

Suggested change
Player(PlayerName(it), MutableCards(cards = mutableListOf()))
Player(PlayerName(it))

},
)
return players
}

fun initBlackJack(
players: Players,
cardSet: CardDeck,
Copy link

Choose a reason for hiding this comment

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

메서드 파라미터의 네이밍도 cardDeck이라고 두면 어떨까요? 🤔

): BlackJack {
val blackJack = BlackJack(players, cardSet)
initBlackJackView(blackJack)
return blackJack
}

fun addCard(blackJack: BlackJack) {
addCardView(blackJack)
}

fun result(blackJack: BlackJack) {
resultView(blackJack)
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/view/InitBlackJackView.kt
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()) }
}
13 changes: 13 additions & 0 deletions src/main/kotlin/blackjack/view/InputPlayerNameView.kt
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
}
10 changes: 10 additions & 0 deletions src/main/kotlin/blackjack/view/ResultView.kt
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())
}
}
3 changes: 0 additions & 3 deletions src/main/kotlin/studydsl/HardSkill.kt

This file was deleted.

3 changes: 0 additions & 3 deletions src/main/kotlin/studydsl/SoftSkill.kt

This file was deleted.

17 changes: 17 additions & 0 deletions src/test/kotlin/blackjack/domain/BlackJackTest.kt
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
}
}
}
29 changes: 29 additions & 0 deletions src/test/kotlin/blackjack/domain/CardDeckTest.kt
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
}
}
Loading