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 #673

Open
wants to merge 36 commits into
base: yw9594
Choose a base branch
from
Open

Step2 #673

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f2b4dec
doc(step2): 기능 목록 추가
yw9594 Nov 27, 2023
56de002
feat(step2): 카드 숫자 및 문양 문자열 반환 로직 추가
yw9594 Nov 27, 2023
f646c64
feat(step2): 카드 덱 생성 로직 추가
yw9594 Nov 27, 2023
72f3621
doc(step2): 기능 목록 추가
yw9594 Nov 27, 2023
4b96d12
feat(step2): 카드 객체 생성 방법 제한
yw9594 Nov 27, 2023
3a9f31b
feat(step2): 카드 점수 반환 로직 추가
yw9594 Nov 27, 2023
80baf83
feat(step2): 카드 풀에서 랜덤한 카드 한 장 반환 로직 추가
yw9594 Nov 27, 2023
89e84c0
refactor(step2): 테스트 가독성 개선
yw9594 Nov 27, 2023
945a368
feat(step2): 카드의 점수 합 반환 로직 추가
yw9594 Nov 27, 2023
966b657
feat(step2): 점수 래퍼 값 객체 추가
yw9594 Nov 27, 2023
3a40362
refactor(step2): 불필요한 출력문 삭제
yw9594 Nov 27, 2023
754caf0
feat(step2): 플레이어 클래스 및 카드 뽑기 가능 여부 반환 메소드 추가
yw9594 Nov 27, 2023
8f43bdb
feat(step2): 플레이어 이름 검증 로직 추가
yw9594 Nov 27, 2023
30f68ab
feat(step2): 플레이어 이름 값 클래스 추가
yw9594 Nov 27, 2023
55f4706
feat(step2): 플레이어 이름 입력 뷰 추가
yw9594 Nov 27, 2023
bde2e2f
feat(step2): main 함수 추가
yw9594 Nov 27, 2023
f725eef
refactor(step2): 디렉토리 변경
yw9594 Nov 27, 2023
032ec32
feat(step2): 블랙잭 생성 로직 추가
yw9594 Nov 27, 2023
fd2665e
fix(step2): 모든 플레이어가 같은 카드를 분배받는 오류 수정
yw9594 Nov 27, 2023
dd2dba9
feat(step2): 게임 초기화 로직 추가
yw9594 Nov 27, 2023
3cd31d9
feat(step2): 예/아니오 입력 로직 추가
yw9594 Nov 27, 2023
0c52d71
refactor(step2): 메소드 이름 변경
yw9594 Nov 27, 2023
15c1f8e
feat(step2): 참가 플레이어 클래스 일급 컬렉션으로 변경
yw9594 Nov 27, 2023
d4ebf16
feat(step2): 플레이어 힛 가능 여부 반환 로직 추가
yw9594 Nov 27, 2023
9d94bbf
feat(step2): 플레이어가 힛을 거절할 경우 다음 플레이어 턴으로 넘어가도록 변경
yw9594 Nov 27, 2023
9443997
feat(step2): 힛 로직 추가
yw9594 Nov 27, 2023
491eb72
feat(step2): 플레이어 정보 표현 로직 추가
yw9594 Nov 27, 2023
4b711ee
refactor(step2): 불필요한 개행 삭제
yw9594 Nov 27, 2023
0a64aae
feat(step2): 게임 상태 출력 기능 추가
yw9594 Nov 27, 2023
2ab809c
fix(step2): 힛이 가능한 상태에서 힛을 하지 않고 턴이 종료되는 오류 수정
yw9594 Nov 27, 2023
07ce271
refactor(step2): 불필요한 메소드 프리픽스 제거
yw9594 Nov 27, 2023
95e5420
refactor(step2): 메소드 위치 변경
yw9594 Nov 27, 2023
72819b0
refactor(step2): 도메인 코드에서 뷰를 위한 로직 제거
yw9594 Nov 27, 2023
7ad08bc
refactor(step2): 불필요 테스트 제거
yw9594 Nov 27, 2023
c226eb1
feat(step2): 점수 계산 기능 및 출력 로직 추가
yw9594 Nov 27, 2023
0fa1cf3
refactor(step2): 승리 점수 비교 로직 위치 변경
yw9594 Nov 27, 2023
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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
# kotlin-blackjack
# kotlin-blackjack
# Step2
- 기능 목록
- 도메인
- 카드
- 숫자와 문양을 결합한 문자열을 반환
- 숫자에 맞는 점수 리스트를 반환
- 카드 A의 경우 1, 11의 점수를 가짐
- 카드 K, Q, J의 경우 10의 점수를 가짐
- 카드 풀
- 랜덤한 카드 한 장을 반환
- 카드 풀에서 반환한 카드 제거
- 플레이어
- 보유한 카드 점수 합 리스트를 반환
- 추가 카드를 뽑을 수 있는 점수 합인지 검증
- 블랙잭 게임
- 플레이어들을 전달 받고 블랙잭 게임 반환
- 각 개별 플레이어들 블랙젝 게임 실행
- 입력
- 참가하는 플레이어의 이름을 입력받음
- 플레이어들의 이름 문자열이 콤마(,)로 구분되는지 검사
- 플레이어가 카드를 더 뽑을지 말지를 Y/N으로 입력
- 입력받는 값이 Y/N인지 검증
- 출력
- 플레이어의 이름, 보유 카드 및 플레이어의 점수 출력
48 changes: 48 additions & 0 deletions src/main/kotlin/blackjack/BlackjackApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package blackjack

import blackjack.contoller.BlackjackController
import blackjack.domain.component.BlackjackGameProxy
import blackjack.domain.component.BlackjackInputValidator
import blackjack.domain.model.PlayerName
import blackjack.view.BlackjackInputView
import blackjack.view.BlackjackResultView

fun main() {
val controller = init()

process(controller)
}

private fun init(): BlackjackController {
return BlackjackController(
BlackjackInputView(),
BlackjackInputValidator(),
BlackjackResultView(),
BlackjackGameProxy()
)
}

private fun process(blackjackController: BlackjackController) {
val playerNames: List<PlayerName> = blackjackController.getPlayerNames()

blackjackController.initGame(playerNames)
blackjackController.printInitialGameStatus()

playerNames.forEach {
it.play(blackjackController)
}

blackjackController.printGameResultStatus()
}

private fun PlayerName.play(blackjackController: BlackjackController) {
while (blackjackController.getHitPossible(this)) {
if (blackjackController.getHit(this).isNo()) {
return
}

val player = blackjackController.hit(this)

blackjackController.printPlayerInfo(player)
}
}
Comment on lines +31 to +48
Copy link
Member

Choose a reason for hiding this comment

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

플레이어가 카드를 받는 것은 도메인 객체로 표현해보는 건 어떨까요?

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

import blackjack.domain.component.BlackjackGameProxy
import blackjack.domain.component.BlackjackInputValidator
import blackjack.domain.model.Player
import blackjack.domain.model.PlayerInfo
import blackjack.domain.model.PlayerName
import blackjack.domain.model.YesNo
import blackjack.view.BlackjackInputView
import blackjack.view.BlackjackResultView

class BlackjackController(
private val blackJackInputView: BlackjackInputView,
private val blackjackInputValidator: BlackjackInputValidator,
private val blackjackResultView: BlackjackResultView,
private val blackjackGameProxy: BlackjackGameProxy
) {
fun getPlayerNames(): List<PlayerName> {
val playerNamesString: String? = blackJackInputView.getPlayerNames()

return convertPlayerNamesStringToList(playerNamesString)
}

fun getHitPossible(playerName: PlayerName): Boolean {
return blackjackGameProxy.isHitPossible(playerName)
}

fun getHit(playerName: PlayerName): YesNo {
return blackJackInputView
.getHit(playerName.name)
.run { convertYesNo(this) }
}

fun initGame(playerNames: List<PlayerName>) {
blackjackGameProxy.init(playerNames)
}

fun hit(playerName: PlayerName): Player {
return blackjackGameProxy.hit(playerName)
}

fun printInitialGameStatus() {
blackjackGameProxy
.fetchPlayerInfos()
.run { blackjackResultView.printGameInitialStatus(this) }
}

fun printGameResultStatus() {
blackjackGameProxy
.fetchPlayerInfos()
.run { blackjackResultView.printGameResultStatus(this) }
}

fun printPlayerInfos() {
blackjackGameProxy
.fetchPlayerInfos()
.run { blackjackResultView.printPlayerInfos(this) }
}

fun printPlayerInfo(player: Player) {
val playerInfo = PlayerInfo.from(player)

blackjackResultView.printPlayerInfo(playerInfo, false)
}

private fun convertPlayerNamesStringToList(playerNames: String?): List<PlayerName> {
return playerNames
.run { blackjackInputValidator.validatePlayerNamesString(this) }
.split(PLAYER_NAME_SEPARATOR)
.run { blackjackInputValidator.validatePlayerNamesSize(this) }
.map { PlayerName(it) }
}

private fun convertYesNo(yesNo: String?): YesNo {
return yesNo
.run { blackjackInputValidator.validateYesNoString(this) }
.run { YesNo.from(this) ?: YesNo.N }
}

private fun BlackjackGameProxy.fetchPlayerInfos(): List<PlayerInfo> {
return this
.getPlayers()
.players
.map { PlayerInfo.from(it) }
}

companion object {
private const val PLAYER_NAME_SEPARATOR = ","
}
}
49 changes: 49 additions & 0 deletions src/main/kotlin/blackjack/domain/component/BlackjackGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package blackjack.domain.component

import blackjack.domain.model.*

class BlackjackGame private constructor(
val players: Players,
private val cardPool: CardPool
) {
fun isHitPossible(playerName: PlayerName): Boolean {
return players
.findByName(playerName)
.checkNull()
.isPossibleToHit()
}

fun hit(playerName: PlayerName): Player {
val player = players.findByName(playerName)
val card = cardPool.pickAndRemove()

return player
.checkNull()
.append(card)
}

private fun Player?.checkNull(): Player {
check(this != null) { "해당 플레이어는 존재하지 않습니다." }

return this
}

companion object {
const val INITIAL_CARD_COUNT = 2

fun create(playerNames: List<PlayerName>): BlackjackGame {
val cardPool = CardPool.create()
val players = playerNames
.map { Player(it, fetchInitialCards(cardPool)) }
.run { Players(this) }

return BlackjackGame(players, cardPool)
}

private fun fetchInitialCards(cardPool: CardPool): Cards {
return cardPool
.pickAndRemove(INITIAL_CARD_COUNT)
.run { Cards.of(this) }
}
}
}
37 changes: 37 additions & 0 deletions src/main/kotlin/blackjack/domain/component/BlackjackGameProxy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package blackjack.domain.component

import blackjack.domain.model.Player
import blackjack.domain.model.PlayerName
import blackjack.domain.model.Players

class BlackjackGameProxy(
private var blackjackGame: BlackjackGame? = null
Comment on lines +6 to +8
Copy link
Member

Choose a reason for hiding this comment

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

이 객체가 필요한 이유가 궁금하네요 ?_? 어떤 역할을 가지는 객체일까요?

) {
fun init(playerNames: List<PlayerName>) {
blackjackGame = BlackjackGame.create(playerNames)
}

fun isHitPossible(playerName: PlayerName): Boolean {
return blackjackGame
.checkGameExist()
.isHitPossible(playerName)
}

fun hit(playerName: PlayerName): Player {
return blackjackGame
.checkGameExist()
.hit(playerName)
}

fun getPlayers(): Players {
return blackjackGame
.checkGameExist()
.players
}

private fun BlackjackGame?.checkGameExist(): BlackjackGame {
check(this != null) { "게임이 존재하지 않습니다." }

return this
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package blackjack.domain.component

class BlackjackInputValidator {
fun validatePlayerNamesString(playerNames: String?): String {
require(!playerNames.isNullOrBlank()) { "플레이어 이름은 null 또는 공백일 수 없습니다." }

return playerNames
}

fun validatePlayerNamesSize(playerNames: List<String>): List<String> {
require(playerNames.isNotEmpty()) { "게임에는 1명 이상의 플레이어가 참가해야합니다." }

return playerNames
}

fun validateYesNoString(yesNo: String?): String {
require(!yesNo.isNullOrBlank()) { "힛은 null 또는 공백일 수 없습니다." }

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

class Card private constructor(
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.

카드 인스턴스를 재활용해보는 건 어떨까요?
똑같은 카드를 동일한 인스턴스를 활용할 수 있게끔 52장의 카드를 캐싱해볼 수 있겠어요 :)

val number: CardNumber,
val shape: CardShape,
val scores: List<Score>
) {
companion object {
private val CARD_ACE_SCORES = listOf(Score(1), Score(11))
private val CARD_KQJ_SCORES = listOf(Score(10))
Comment on lines +9 to +10
Copy link
Member

Choose a reason for hiding this comment

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

CardNumber enum class가 카드의 점수를 갖게 해보는 건 어떨까요?

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점이 되는 경우는 로직으로 녹여내보는 건 어떨까요?


fun of(number: CardNumber, shape: CardShape): Card {
if (number.isAce()) {
return Card(number, shape, CARD_ACE_SCORES)
}

if (number.isOneOfKQJ()) {
return Card(number, shape, CARD_KQJ_SCORES)
}

return Card(number, shape, listOf(Score(number.number)))
}
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/blackjack/domain/model/CardNumber.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package blackjack.domain.model

enum class CardNumber(val number: Int, val displayName: 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(11, "J"),
QUEEN(12, "Q"),
KING(13, "K"),
;

fun isAce(): Boolean {
return number == 1
}

fun isOneOfKQJ(): Boolean {
return number >= 11
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/blackjack/domain/model/CardPool.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package blackjack.domain.model

class CardPool private constructor(private var cards: MutableList<Card>) {
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.

생성자에 Mutable Collection을 넣는 것은 지양해보는 건 어떨까요?

생성자에 넘겨준 가변 컬렉션의 참조를 외부에서 실수로 다루게 되면 이 객체에도 문제가 발생할 수 있어요.

val mutableList = mutableListOf()
val pool = CardPool(mutableList)

mutableList.clear()

방어적 복사를 활용해보는 건 어떨까요?

fun pickAndRemove(): Card {
check(cards.size > 0) { "카드 풀의 카드가 존재하지 않습니다." }

Comment on lines +4 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.

52장의 카드를 모두 사용하면 Exception이 발생하고 게임이 비정상 종료될 것으로 보여요.
아직 플레이어들이 카드게임을 진행하고 있을 수도 있는데 말이죠!

카드를 다 사용하더라도 게임을 계속 진행할 수 있게 구성해보는 건 어떨까요?
또, 블랙잭 게임의 다른 룰에서는 한 번에 여러 벌의 카드를 가지고 시작하는 경우도 있어요. 이런 요구사항 등에 유연하게 대응할 수 있게 만들어보셔도 좋겠어요!

val target = (0 until cards.size).random()

return cards.removeAt(target)
}

fun pickAndRemove(count: Int): List<Card> {
if (count < 0) {
return listOf()
}
Comment on lines +14 to +15
Copy link
Member

Choose a reason for hiding this comment

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

빈 list는 emptyList() 함수를 활용해볼 수 있어요 :)


return (0 until count).map { pickAndRemove() }
}

companion object {
fun create(): CardPool {
return CardPool(Cards.create().cards.toMutableList())
}
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/domain/model/CardShape.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackjack.domain.model

enum class CardShape(val shape: String) {
Spade("스페이드"),
Clover("클로버"),
Heart("하트"),
Diamond("다이아몬드"),
}
Loading