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단계] 미플 미션 제출합니다 #119

Open
wants to merge 90 commits into
base: hambeomjoon
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
fc8cbdf
chore: gitkeep 삭제
HamBeomJoon Mar 4, 2025
431eb2e
docs: 기능 목록 정의
HamBeomJoon Mar 4, 2025
b754bb1
feat: 카드 Shape enum class 추가
HamBeomJoon Mar 4, 2025
55fd723
feat: 카드 Number enum class 추가
HamBeomJoon Mar 4, 2025
22d77d5
feat: 카드 data class 추가
HamBeomJoon Mar 4, 2025
bcd629f
feat: 덱 카드 리스트 생성 함수 추가
HamBeomJoon Mar 4, 2025
267d955
feat: 덱 카드 한 장 뽑기 함수 추가
HamBeomJoon Mar 4, 2025
650d0ef
feat: 플레이어 객체 생성
HamBeomJoon Mar 5, 2025
97a422c
feat: 딜러 객체 생성
HamBeomJoon Mar 5, 2025
a705b7d
feat: 카드 52장 소모 시 예외 처리
HamBeomJoon Mar 5, 2025
928c125
feat: 플레이어 카드 받기 함수 추가
HamBeomJoon Mar 5, 2025
eeb753a
feat: 플레이어 카드 받기 함수 수정
HamBeomJoon Mar 5, 2025
b9db320
feat: 플레이어, 딜러 초기 세팅 함수 추가
HamBeomJoon Mar 5, 2025
998000a
feat: Number 클래스 score 값 추가
HamBeomJoon Mar 5, 2025
e917698
feat: 플레이어 카드 합 계산 함수 추가
HamBeomJoon Mar 5, 2025
4170ccf
feat: 사용자 이름 입력 받기, 예외 처리
HamBeomJoon Mar 6, 2025
1115799
feat: 초기 카드 안내 메세지 출력
HamBeomJoon Mar 6, 2025
6b12423
refactor: Player 카드 추가 함수 수정
HamBeomJoon Mar 6, 2025
2cfd6d6
refactor: Deck 카드 드로우 함수 수정
HamBeomJoon Mar 6, 2025
cfeb476
refactor: GameManager 초기 세팅, 카드 드로우 함수 수정
HamBeomJoon Mar 6, 2025
029d16c
feat: Dealer 클래스 추가
HamBeomJoon Mar 6, 2025
f1358cd
refactor: 카드 모양 type 프로퍼티로 변경
HamBeomJoon Mar 6, 2025
9f64b83
feat: 딜러와 플레이어 카드 출력
HamBeomJoon Mar 6, 2025
b014f88
feat: 컨트롤러 play 함수 추가
HamBeomJoon Mar 6, 2025
ec35943
feat: 블랙잭 게임 실행
HamBeomJoon Mar 6, 2025
1f3b5bc
fix: 딜러 카드 한장만 출력하도록 수정
HamBeomJoon Mar 6, 2025
4997796
feat: 플레이어 카드 더 받을지 메세지 출력
HamBeomJoon Mar 6, 2025
6e5f3fe
feat: 플레이어 카드 한 장 받기 함수 추가
HamBeomJoon Mar 6, 2025
cee98c3
feat: 플레이어 카드 출력 함수 추가
HamBeomJoon Mar 6, 2025
6f8f9a5
feat: 플레이어 카드 추가 로직 구현
HamBeomJoon Mar 6, 2025
7c7d831
feat: 사용자 입력에 따른 분기 처리
HamBeomJoon Mar 6, 2025
39b4776
feat: A카드 점수 조정
HamBeomJoon Mar 6, 2025
abf3e76
feat: 딜러 카드 합 계산, 더 받을지 여부 계산 함수 추가
HamBeomJoon Mar 6, 2025
bc8428c
feat: 딜러 카드 hit, stay 판단 로직 구현
HamBeomJoon Mar 6, 2025
3b58f99
feat: 딜러, 플레이어 최종 카드 결과 출력
HamBeomJoon Mar 6, 2025
7b887d1
feat: 딜러 카드 합이 16이하 일 시 카드 추가
HamBeomJoon Mar 6, 2025
04dbc61
refactor: 초기 카드 출력 메세지 함수명 수정
HamBeomJoon Mar 6, 2025
cc38e15
refactor: 덱 클래스 싱글턴으로 변경
HamBeomJoon Mar 6, 2025
6210d0e
refactor: 카드 뽑기 책임에 따라 메서드 이동
HamBeomJoon Mar 6, 2025
9a2f041
refactor: 출력 메서드 정리 및 이름 변경
HamBeomJoon Mar 6, 2025
768234c
feat: 사용자 응답 enum 클래스 추가
HamBeomJoon Mar 6, 2025
735c332
refactor: 사용자 응답 enum 클래스 생성에 따른 로직 수정
HamBeomJoon Mar 6, 2025
8c30b4b
chore: 파일 패키지 이동
HamBeomJoon Mar 6, 2025
ec97c61
refactor: GameManagerTest 메서드 명 변경
HamBeomJoon Mar 6, 2025
ea4638f
refactor: 버스트 이름 수정
HamBeomJoon Mar 6, 2025
ab04116
refactor: 사용자 응답 판단 함수 메서드화
HamBeomJoon Mar 6, 2025
e0d3c23
feat: 플레이어 승패 여부 계산 함수 추가
HamBeomJoon Mar 6, 2025
d81f839
feat: 딜러 최종 승패 계산 함수 추가
HamBeomJoon Mar 6, 2025
0dd2764
feat: 최종 승패 출력 로직 추가
HamBeomJoon Mar 6, 2025
b329a2e
fix: 사용자 카드 더 받기 로직 에러 수정
HamBeomJoon Mar 6, 2025
1cae40b
feat: 결과 타입 저장 ResultType 클래스 생성
HamBeomJoon Mar 6, 2025
8791b2b
chore: 빈 라인 추가
HamBeomJoon Mar 6, 2025
fd6fbd7
refactor: 플레이어, 딜러 결과 계산 함수 수정
HamBeomJoon Mar 6, 2025
c2fa06f
refactor: 플레이어, 딜러 결과 계산결과 출력 함수 수정
HamBeomJoon Mar 6, 2025
c2f1755
refactor: generateCards() 코드 최적화
yrsel Mar 7, 2025
3c29778
refactor: joinToString 연결자 기본 값 활용
yrsel Mar 7, 2025
210ada3
refactor: sumScore 함수명 변경
yrsel Mar 7, 2025
81bd9c0
style: ktlint 적용
yrsel Mar 7, 2025
7db355e
refactor: 컨트롤러 함수 분리
yrsel Mar 7, 2025
51f458c
refactor: 21을 초과할 경우의 로직 처리
yrsel Mar 7, 2025
9ad6e31
refactor: 상수화 진행
HamBeomJoon Mar 7, 2025
997703a
refactor: 컨트롤러 함수 분리
HamBeomJoon Mar 7, 2025
bad91a6
style: public private 함수 위치 이동
HamBeomJoon Mar 7, 2025
dfe6b08
feat: Person 클래스 추가
HamBeomJoon Mar 7, 2025
7411599
refactor: Person 클래스 상속
HamBeomJoon Mar 7, 2025
d735d41
refactor: judgeScore 매개변수 수정
HamBeomJoon Mar 7, 2025
8567600
refactor: Person 추상 클래스로 변경
HamBeomJoon Mar 7, 2025
7e71ac4
refactor: 추상 메서드 구현
HamBeomJoon Mar 7, 2025
d4322bf
style: 띄어쓰기 수정
HamBeomJoon Mar 7, 2025
3b7f68a
docs: README 입출력 기능 추가
HamBeomJoon Mar 7, 2025
fb9202d
test: 플레이어와 딜러 카드 비교 결과 함수 테스트
HamBeomJoon Mar 7, 2025
cfd8383
test: 카드 총 합 계산 함수명 수정
HamBeomJoon Mar 7, 2025
dbe5336
test: Person 클래스 테스트 추가
HamBeomJoon Mar 7, 2025
f27972c
test: Player 카드 점수 조정 테스트
HamBeomJoon Mar 7, 2025
19ee934
test: Player 카드 Bust 테스트
HamBeomJoon Mar 7, 2025
88ef305
test: Dealer 카드 드로우 테스트
HamBeomJoon Mar 7, 2025
55affb2
style: ktlint 적용
HamBeomJoon Mar 7, 2025
d78a3a2
refactor: 출력 문구 수정
HamBeomJoon Mar 7, 2025
0235265
refactor: DrawChoice 함수 최적화
HamBeomJoon Mar 7, 2025
cf606b5
test: DSL Test 코드 추가
HamBeomJoon Mar 7, 2025
ade3e61
refactor: 플레이어와 딜러 카드 나눠주는 함수 역할 이동
HamBeomJoon Mar 7, 2025
fb412a6
style: ktlint 적용
HamBeomJoon Mar 7, 2025
e14e037
refactor: number 클래스 값 표시 toString으로 변경
HamBeomJoon Mar 10, 2025
c06faff
refactor: 플레이어 이름 생성 컨트롤러로 책임 이동
HamBeomJoon Mar 10, 2025
003634a
refactor: 플레이어 카드 더 받을 지 여부 컨트롤러로 책임 이동
HamBeomJoon Mar 10, 2025
2364de1
style: companion object 위치이동
HamBeomJoon Mar 10, 2025
d5c30ca
refactor: 결과 타입에 따른 출력문구 출력 함수
HamBeomJoon Mar 10, 2025
c8f1d51
refactor: 카드 모양에 따른 출력문구 출력 함수
HamBeomJoon Mar 10, 2025
fa526f6
refactor: 플레이어 이름 빈 칸 입력시 예외 처리
HamBeomJoon Mar 10, 2025
2e35bfc
refactor: 잘못된 입력 시 null 반환하고 다시 입력받도록 수정
HamBeomJoon Mar 10, 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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# kotlin-blackjack
# 블랙잭

## 기능 목록

- [x] 카드 Shape에는 스페이드, 다이아몬드, 하트, 클로버가 있다.
- [x] 카드 Number에는 1~10, J, Q, K가 있다.
- [x] 카드는 Number와 Shape의 조합을 가진다.

-
- [x] 덱은 카드 리스트를 생성한다.
- [x] 덱에서 카드 한 장을 꺼낼 수 있다.
- [x] 덱에 카드가 존재하지 않은 경우 예외가 발생한다.

- 입력
- [x] 사용자 이름을 입력받는다.
- [x] 사용자 이름이 중복될 경우 재입력 받는다.
-[x] 플레이어에게 한 장 더 받을지 입력받는다.
- [x] 입력값이 y, n이 아니면 재입력 받는다.

- 출력
- [x] 플레이어 이름과 함께 초기 카드 안내 메시지를 출력한다.
- [x] 딜러와 플레이어 초기 카드 상태를 출력한다.
- [x] 각 플레이어마다 현재 카드 상태를 출력한다.
- [x] 딜러의 카드 점수에 따라 안내 문구를 출력한다.
- [x] 딜러와 모든 플레이어의 최종 카드 상태와 점수를 출력한다.
- [x] 최종 승패를 출력한다.
Empty file removed src/main/kotlin/.gitkeep
Empty file.
11 changes: 11 additions & 0 deletions src/main/kotlin/blackjack/BlackjackGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package blackjack

import blackjack.controller.BlackjackController
import blackjack.model.Dealer
import blackjack.view.InputView
import blackjack.view.OutputView

fun main() {
val blackjackController = BlackjackController(InputView(), OutputView())
blackjackController.play(dealer = Dealer())
}
75 changes: 75 additions & 0 deletions src/main/kotlin/blackjack/controller/BlackjackController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package blackjack.controller

import blackjack.model.Dealer
import blackjack.model.Deck.INITIAL_HAND_OUT_CARD_COUNT
import blackjack.model.DrawChoice
import blackjack.model.GameManager
import blackjack.model.Player
import blackjack.view.InputView
import blackjack.view.OutputView

class BlackjackController(
private val inputView: InputView,
private val outputView: OutputView,
) {
private lateinit var gameManager: GameManager

fun play(dealer: Dealer) {
val players = playerSetting()

outputView.printInitialHandOutCardMessage(players)
gameManager = GameManager(dealer, players)
gameManager.dealInitialCardWithCount(INITIAL_HAND_OUT_CARD_COUNT)
outputView.printAllPlayerHands(dealer, players)

playersDrawCards(players)

dealerDrawCards(dealer)

outputView.printFinalHandStatus(dealer, players)

resultSummary(gameManager)
}

private fun playerSetting(): List<Player> {
var playerNames: List<String>? = null
while (playerNames == null) {
playerNames = inputView.readPlayerNames()
}
return playerNames.map { Player(it) }
}

private fun playersDrawCards(players: List<Player>) {
players.forEach { player -> playerDrawOrStay(player) }
}

private fun playerDrawOrStay(player: Player) {
var condition: String? = null
while (condition == null) {
condition = inputView.readMoreCardCondition(player)
}
val playerCondition = DrawChoice.from(condition)
if (playerCondition!!.isStay()) {
outputView.printPlayerHands(player)
return
}
gameManager.drawCard(player)
outputView.printPlayerHands(player)
if (player.isBust()) return
playerDrawOrStay(player)
}

private fun dealerDrawCards(dealer: Dealer) {
val moreCard = dealer.isMoreCard()
if (moreCard) {
gameManager.drawCard(dealer)
}
outputView.printDealerHandStatus(moreCard)
}

private fun resultSummary(gameManager: GameManager) {
val result = gameManager.calculateResultMap()
val dealerResult = gameManager.calculateDealerResult(result)
outputView.printFinalResult(result, dealerResult)
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/blackjack/model/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package blackjack.model

data class Card(val shape: Shape, val number: Number)
16 changes: 16 additions & 0 deletions src/main/kotlin/blackjack/model/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackjack.model

import blackjack.model.ResultType.Companion.BUST_NUMBER

class Dealer : Person() {
val name = DEALER_NAME

fun isMoreCard() = calculateTotalScore() < DEALER_MORE_CARD_MINIMUM

override fun isBust(): Boolean = super.calculateTotalScore() > BUST_NUMBER

companion object {

Choose a reason for hiding this comment

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

companion object 는 어디에 위치를 해야 하는가. 를 확인해보시면 좋을 것 같네요.

https://kotlinlang.org/docs/coding-conventions.html#class-layout

private const val DEALER_NAME = "딜러"
private const val DEALER_MORE_CARD_MINIMUM = 17
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/blackjack/model/Deck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package blackjack.model

object Deck {
const val INITIAL_HAND_OUT_CARD_COUNT = 2
private const val ERROR_NO_MORE_CARD_MESSAGE = "카드가 더 없습니다."
private val CARDS = generateCards()

fun draw(): Card {
return CARDS.removeFirstOrNull() ?: throw IllegalStateException(ERROR_NO_MORE_CARD_MESSAGE)
}

fun drawWithCount(count: Int): List<Card> {
return List(count) { draw() }
}

private fun generateCards(): MutableList<Card> =
Shape.entries.flatMap { shape ->
Number.entries.map { number ->
Card(shape, number)
}
}.shuffled().toMutableList()
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/model/DrawChoice.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.model

enum class DrawChoice(val answer: String) {
YES("y"),
NO("n"),
;

fun isStay(): Boolean {
return this == NO
}

companion object {
fun from(answer: String): DrawChoice? {
return entries.find { choice -> choice.answer == answer }
}
}
}
38 changes: 38 additions & 0 deletions src/main/kotlin/blackjack/model/GameManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package blackjack.model

class GameManager(
private val dealer: Dealer,
private val players: List<Player>,
) {
fun dealInitialCardWithCount(count: Int) {
dealer.addCards(Deck.drawWithCount(count))
players.forEach { player -> player.addCards(Deck.drawWithCount(count)) }
}

fun calculateResultMap(): Map<Player, ResultType> {
val playersStatus =
players.associateBy(
{ player -> player },
{ player -> ResultType.judgeScore(dealer, player) },
)
return playersStatus
}

fun calculateDealerResult(resultMap: Map<Player, ResultType>): Map<ResultType, Int> {
val result = mutableMapOf<ResultType, Int>()

resultMap.forEach {
when (it.value) {
ResultType.WIN -> result[ResultType.LOSS] = result.getOrDefault(ResultType.LOSS, 0) + 1
ResultType.TIE -> result[ResultType.TIE] = result.getOrDefault(ResultType.TIE, 0) + 1
ResultType.LOSS -> result[ResultType.WIN] = result.getOrDefault(ResultType.WIN, 0) + 1
}
}

return result
}

fun drawCard(person: Person) {
person.addCard(Deck.draw())
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/blackjack/model/Number.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package blackjack.model

enum class Number(val score: Int) {
ACE(11),
TWO(2),
THREE(3),
FOUR(4),
FIVE(5),
SIX(6),
SEVEN(7),
EIGHT(8),
NINE(9),
TEN(10),
JACK(10),
QUEEN(10),
KING(10),
;

override fun toString(): String {
return when (this) {
ACE -> "A"
JACK -> "J"
QUEEN -> "Q"
KING -> "K"
else -> (ordinal + 1).toString()
}
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/blackjack/model/Person.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package blackjack.model

abstract class Person {

Choose a reason for hiding this comment

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

사람으로 따지면 "이름" 도 공통이 될 수 있지 않을까요?

private val _cards: MutableList<Card> = mutableListOf()
open val cards get() = _cards.toList()

Choose a reason for hiding this comment

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

이 값이 open 되어야 할 필요가 있을까요?

그리고 외부로 공개 되어 있는 이유는 지금 이 객체에게 메세지를 던지지 않고 있는 상황이 아닌지 생각해 봅시다.


fun addCard(card: Card) = _cards.add(card)

fun addCards(cards: List<Card>) = _cards.addAll(cards)

Comment on lines +7 to +10

Choose a reason for hiding this comment

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

abstract 으로 만들었는데 addCard addCards 이 2개 정말 공존해야 할까요? 사람 입장에서는 한 장씩 받는 것이지 않을까요?

fun calculateTotalScore() = cards.sumOf { card -> card.number.score }

abstract fun isBust(): Boolean
}
26 changes: 26 additions & 0 deletions src/main/kotlin/blackjack/model/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package blackjack.model

import blackjack.model.ResultType.Companion.BUST_NUMBER

class Player(
val name: String,
) : Person() {
fun adjustScore(): Int {
var sumScore = calculateTotalScore()
var countAce = countAce()
while (countAce-- > 0) {
if (sumScore > BUST_NUMBER) {
sumScore -= ADJUST_ACE_NUMBER
}
}
return sumScore
}

private fun countAce() = super.cards.count { it.number == Number.ACE }

override fun isBust() = adjustScore() > BUST_NUMBER

companion object {
private const val ADJUST_ACE_NUMBER = 10
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/blackjack/model/ResultType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package blackjack.model

enum class ResultType(val value: Char) {
WIN('승'),
TIE('무'),
LOSS('패'), ;
Comment on lines +3 to +6

Choose a reason for hiding this comment

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

해당 value도 view에만 표시하기 위한 선언부인데 만약 이 라는 것 말고 디자인에서 승리 라고 띄어주고 싶다고 하면 domain model 까지 변경이 되어야 하는 상황이 와버립니다.


companion object {
fun judgeScore(
dealer: Dealer,
player: Player,
): ResultType {
val dealerFinalScore = if (dealer.isBust()) 0 else dealer.calculateTotalScore()
val playerFinalScore = if (player.isBust()) 0 else player.calculateTotalScore()
if (dealerFinalScore < playerFinalScore) return WIN
if (dealerFinalScore == playerFinalScore) return TIE
return LOSS
}

const val BUST_NUMBER = 21
}
Comment on lines +8 to +21

Choose a reason for hiding this comment

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

ResultType 이라는 객체를 봤을 때 결과 에 집중을 하고 있는데 여기서 BUST_NUMBER 도 갖고 있고 승부 결과까지 만들고 있네요.

블랙잭이라는 도메인으로 생각했을 때, 블랙잭은 플레이어와 딜러간의 대결일텐데요. 이 부분에 대해서 천천히 고민해봅시다.

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

enum class Shape(val type: String) {

Choose a reason for hiding this comment

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

여기도 ResultType 과 동일합니다.

SPADE("스페이드"),
DIAMOND("다이아몬드"),
HEART("하트"),
CLOVER("클로버"),
}
43 changes: 43 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package blackjack.view

import blackjack.model.DrawChoice
import blackjack.model.Player

class InputView {
fun readPlayerNames(): List<String>? {
println(PLAYER_NAME_MESSAGE_GUIDE)
val playerNames: List<String> = readln().split(PLAYER_NAME_DELIMITER).map { name -> name.trim() }

if (playerNames.any { it.isEmpty() }) {
println(ERROR_PLAYER_NAME_EMPTY)
return null
}

if (playerNames.size != playerNames.toSet().size) {
println(ERROR_INVALID_PLAYER_NAMES)
return null
}

return playerNames
}

fun readMoreCardCondition(player: Player): String? {
println(PLAYER_MORE_CARD_MESSAGE_GUIDE.format(player.name))
val condition: String = readln().trim()
if (DrawChoice.from(condition) == null) {
println(ERROR_INVALID_CARD_CONDITION)
return null
}

return condition
}

companion object {
private const val PLAYER_NAME_MESSAGE_GUIDE = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"
private const val ERROR_PLAYER_NAME_EMPTY = "플레이어 이름은 비어있으면 안 됩니다. 다시 입력해주세요."
private const val ERROR_INVALID_PLAYER_NAMES = "중복된 이름이 있습니다. 다시 입력해주세요."
private const val PLAYER_MORE_CARD_MESSAGE_GUIDE = "%s은(는) 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"
private const val ERROR_INVALID_CARD_CONDITION = "y나 n을 입력해야 합니다. 다시 입력해주세요."
private const val PLAYER_NAME_DELIMITER = ","
}
}
Loading