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

Step2 - 블랙잭 미션 #815

Open
wants to merge 7 commits into
base: goodbyeyo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,66 @@ introduce {
"English" level 3
}
}
```
```

### Step2: 블랙잭

##### `규칙`
- 블랙잭 게임을 변형한 프로그램을 구현한다.
- 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다.
- 카드의 숫자 계산은 카드 숫자를 기본,
- 예외로 Ace는 1 또는 11로 계산할 수 있으며,
- King, Queen, Jack은 각각 10으로 계산한다.
- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며,
- 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다.
- 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다.

##### `실행 결과`
```text
게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)
pobi,jason

pobi, jason에게 2장의 나누었습니다.
pobi카드: 2하트, 8스페이드
jason카드: 7클로버, K스페이드

pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
y
pobi카드: 2하트, 8스페이드, A클로버
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason은 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason카드: 7클로버, K스페이드

pobi카드: 2하트, 8스페이드, A클로버 - 결과: 21
jason카드: 7클로버, K스페이드 - 결과: 17
```

##### `요구사항 정리`
- 카드
- [ ] 카드는 각각 총 13개의 랭크와, 4개의 무늬(mark or suits)가 있다
- [x] rank : 2, 3, 4, 5, 6, 7, 8, 9, 10, A(Ace), K(King), Q(Queen), J(Jack)
- [x] suits : spade(♠️), heart(🖤), diamond(♦️), club(♣️)
- [x] rank 는 포인트를 가진다
- [x] 숫자 : 각 숫자, Ace : 1 또는 11, King, Queen, Jack : 10
- [x] 총 52개의 카드가 무작위 순서로 생성 할 수 있다
- [x] 카드들의 포인트를 더할 수 있다
- 플레이어
- [x] 플레이어는 카드를 추가 할 수 있다
- [x] 플레이어의 카드의 포인트를 합산 할 수 있다
- [x] 카드 드로우를 중지하면 더이상 카드를 받을 수 없다
- [x] 쉼표로 구분된 이름 문자열을 전달하면 플레이어를 생성 할 수 있다
- 컨트롤러
- [x] 게임을 시작하면 게임에 참여할 사람들 입력 요청한다
- [x] 게임을 시작하면 무작위 전체 카드를 생성한다
- [x] 게임 참여자들에게 2장씩 카드를 지급한다
- [x] 모든 참여자가 카드 추가 지급을 거절하면 게임을 종료하고 합산결과 출력 요청한다
- InputView
- [x] 게임에 참여하는 사람을 직접 입력 받는다
- [x] 카드를 더 받을지 입력받는다
- OutputView
- [x] 게임 참여자들에게 처음 지급된 카드 리스트를 출력한다
- [x] 모든 사용자의 카드 리스트와 포인트 합산 결과를 출력한다
- [x] 카드를 더 받으면 지급된 카드와 함께 전체 카드 리스트를 출력한다

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

import blackjack.domain.BlackJackGame
import blackjack.domain.GameCards
import blackjack.view.InputView
import blackjack.view.OutputView

fun main() {
val players = InputView.enterParticipatingPlayers()
val gameCards = GameCards.create()
val blackJackGame = BlackJackGame(players, gameCards)
blackJackGame.startGame()
OutputView.printFirstAllPlayersCards(players)
players.forEach { player ->
blackJackGame.handlePlayerDraw(player, gameCards)
}
OutputView.printFinalResults(players)
}
19 changes: 19 additions & 0 deletions src/main/kotlin/blackjack/UserCards.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package blackjack

import blackjack.domain.Card
import blackjack.domain.Ranks

class UserCards(private val cards: MutableList<Card>) : Collection<Card> by cards {
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

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

아래 코드를 실행시키면 어떤 일이 발생할 수 있을까요?
방어적 복사와 읽기 전용 List 를 활용해보시면 좋겠어요!

val mutableList = mutableListOf()
val cards = UserCards(mutableList)

mutableList.clear()

fun calculatePoints(): Int {
val basePoints = cards.sumOf { it.rank.points[0] }
val hasAce = cards.any { it.rank == Ranks.ACE }
if (hasAce && basePoints + 10 <= 21) {
return basePoints + 10
}
return basePoints
}

fun addCard(card: Card) {
cards.add(card)
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/blackjack/domain/BlackJackGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package blackjack.domain

import blackjack.view.InputView
import blackjack.view.OutputView

class BlackJackGame(
private val players: Players,
private val gameCards: GameCards,
) {
fun startGame() {
repeat(FIRST_ROUND_HAND_SIZE) {
giveOutCards()
}
}

private fun giveOutCards() {
players.forEach { players ->
players.receiveCard(gameCards.drawCard())
}
}

fun handlePlayerDraw(
player: Player,
gameCards: GameCards,
) {
val previousCardCount = player.cardSize()
InputView.enterIsContinueDrawCard(player)
if (player.isDrawContinue) {
player.receiveCard(gameCards.drawCard())
OutputView.printPlayerCard(player)
handlePlayerDraw(player, gameCards) // 재귀 호출
}

if (!player.isDrawContinue && player.cardSize() == previousCardCount) {
OutputView.printPlayerCard(player)
}
}
Comment on lines +22 to +37
Copy link
Member

Choose a reason for hiding this comment

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

기능을 구현하면서 BlackJackGame.handlePlayerDraw () 함수가
View 출력기능도 담당하고 있어서 분리해서 테스트 하기가 �까다로웠는데...
혹시 단위 테스트를 추가할 수 있는 좋은 방법이 있을까요?

View에서 입력을 받는 부분을 람다로 처리해서 외부에서 주입받는 형태로 구성해보는 건 어떨까요?
람다의 활용이 어려우시다면, interface를 만들어서, ConsoleView에 대한 입력 구현체를 주입받는 형태로 구성해볼 수 있겠어요!


companion object {
private const val FIRST_ROUND_HAND_SIZE = 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 rank: Ranks, val suits: Suits)
Comment on lines +2 to +3
Copy link
Member

Choose a reason for hiding this comment

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

카드의 종류는 총 몇가지가 존재할 수 있을까요?
똑같은 카드를 여러번 생성할 수도 있겠지만, 동일한 카드는 인스턴스를 재활용해보는 건 어떨까요?

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

import java.util.LinkedList
import java.util.Queue

class GameCards private constructor(private val deck: Queue<Card>) {
fun drawCard(): Card {
return deck.poll()
}

fun size(): Int {
return deck.size
}

companion object {
fun create(): GameCards {
val allCards =
Suits.entries.flatMap { suit ->
Ranks.entries.map { rank -> Card(rank, suit) }
}
return GameCards(LinkedList(allCards.shuffled()))
Comment on lines +15 to +21
Copy link
Member

Choose a reason for hiding this comment

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

랜덤 요소 때문에 테스트가 어려워보여요.
랜덤의 요소를 외부에서부터 주입받아보는 건 어떨까요?
그렇다면 정확히 어떤 카드들로 순서가 구성되어있는지 개발자가 제어하는 환경을 만들어 테스트 가능할 거라 생각해요 :)

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

import blackjack.UserCards

class Player(val name: String, var isDrawContinue: Boolean = true) {
Comment on lines +4 to +5
Copy link
Member

Choose a reason for hiding this comment

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

isDrawContinue 변수를 외부에서도 변경 가능한 형태예요.
외부에서는 변경하지 못하도록 읽기 전용 프로퍼티로 만들어보는 건 어떨까요?

private var userCards = UserCards(mutableListOf())

fun receiveCard(card: Card) {
require(isDrawContinue) { "카드를 받을 수 없습니다" }
userCards.addCard(card)
}

fun cardSize(): Int {
return userCards.size
}

fun calculateCardPoints(): Int {
return userCards.calculatePoints()
}

fun stopCardDraw() {
isDrawContinue = false
}

fun continueCardDraw() {
isDrawContinue = true
}

fun findAllCardsNames(): List<String> {
return userCards.map { card ->
card.rank.keyword + card.suits.koreanName
}
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/blackjack/domain/Players.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackjack.domain

data class Players(val players: List<Player>) : Collection<Player> by players {
fun getPlayerNames(): List<String> {
return players.map { it.name }
}

companion object {
private const val DELIMITER = ","

fun create(playerNames: String): Players {
val players = playerNames.split(DELIMITER).map { playerName -> Player(playerName) }
Comment on lines +10 to +12
Copy link
Member

Choose a reason for hiding this comment

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

문자열을 파싱하는 행위는 View의 일이라고 생각해요.
뷰와 도메인 로직을 분리해보는 건 어떨까요?

return Players(players)
}
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/Ranks.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain

enum class Ranks(
val keyword: String,
Comment on lines +3 to +4
Copy link
Member

Choose a reason for hiding this comment

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

카드 끝수를 나타내는 이 문자열은 ConsoleView에 종속적인 값이라고 생각해요.
도메인에서 뷰 요소를 분리해보는 건 어떨까요?

val points: List<Int>,
) {
ACE("A", listOf(1, 11)),
Comment on lines +6 to +7
Copy link
Member

Choose a reason for hiding this comment

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

ACE의 기본 점수는 1이고, 특수한 상황에서 10점이 추가되는 것이니, 이것을 로직으로 표현해보는 건 어떨까요?
점수를 계산하는 곳에서 ACE에 10점을 추가할지 말지 결정해보면 좋겠어요!

TWO("2", listOf(2)),
THREE("3", listOf(3)),
FOUR("4", listOf(4)),
FIVE("5", listOf(5)),
SIX("6", listOf(6)),
SEVEN("7", listOf(7)),
EIGHT("8", listOf(8)),
NINE("9", listOf(9)),
TEN("10", listOf(10)),
JACK("J", listOf(10)),
QUEEN("Q", listOf(10)),
KING("K", listOf(10)),
}
10 changes: 10 additions & 0 deletions src/main/kotlin/blackjack/domain/Suits.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package blackjack.domain

enum class Suits(
val koreanName: String,
) {
SPADE("스페이드"),
HEART("하트"),
DIAMOND("다이어"),
CLUB("클로버"),
}
29 changes: 29 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package blackjack.view

import blackjack.domain.Player
import blackjack.domain.Players

object InputView {
private const val CONTINUE_OR_STOP_MESSAGE = "는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"

fun enterParticipatingPlayers(): Players {
println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)")
val playerNames = readln()
return Players.create(playerNames)
}

fun enterIsContinueDrawCard(player: Player) {
val userInput = getIsContinueDraw(player)
when (userInput.lowercase()) {
"n" -> player.stopCardDraw()
"y" -> player.continueCardDraw()
else -> println("잘못된 입력입니다. 다시 입력해주세요.")
}
Comment on lines +15 to +21
Copy link
Member

Choose a reason for hiding this comment

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

플레이어의 게임 진행 자체를 실행하는 로직은 도메인로직이라 생각해요.
View에서는 n 혹은 y 가 입력되었는지 받은 뒤, Boolean등을 반환해서, 도메인에서는 반환 받은 값을 가지고 player에게 메시지를 던지도록 구성해보는 건 어떨까요?

}

fun getIsContinueDraw(player: Player): String {
println()
println(player.name + CONTINUE_OR_STOP_MESSAGE)
return readln()
}
}
51 changes: 51 additions & 0 deletions src/main/kotlin/blackjack/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package blackjack.view

import blackjack.domain.Player
import blackjack.domain.Players

object OutputView {
private const val PLAYER_NAME_DELIMITER = ", "
private const val BLANK_PREFIX_MESSAGE = ""
private const val POST_MESSAGE = "에게 2장의 나누었습니다."
private const val RESULT_EXPRESSION = " - 결과: "
private const val NAME_POSTFIX_EXPRESSION = "카드: "

fun printFirstAllPlayersCards(players: Players) {
println()
val result =
players.getPlayerNames().joinToString(
PLAYER_NAME_DELIMITER,
BLANK_PREFIX_MESSAGE,
POST_MESSAGE,
)
println(result)
players.forEach { player ->
printPlayerAllCards(player)
println()
}
}

fun printFinalResults(players: Players) {
println()
println()
println("게임 종료!")
players.forEach { player ->
printPlayerAllCards(player)
resultExpression(player)
}
}

fun printPlayerCard(player: Player) {
printPlayerAllCards(player)
}

private fun printPlayerAllCards(player: Player) {
print(player.name + NAME_POSTFIX_EXPRESSION)
print(player.findAllCardsNames().joinToString(PLAYER_NAME_DELIMITER))
}

private fun resultExpression(player: Player) {
print(RESULT_EXPRESSION)
println(player.calculateCardPoints())
}
}
Loading