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단계] 디랙 미션 제출합니다. #126

Open
wants to merge 72 commits into
base: doabletuple
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
2997057
docs(README): 도메인 기능 목록 작성
donghyun81 Mar 4, 2025
a67a547
feat(Card): 카드는 문양과 등급을 가진다.
donghyun81 Mar 4, 2025
384bd1c
feat(Player): 플레이어는 이름과 사용할 카드를 가진다
donghyun81 Mar 4, 2025
3ad6760
feat(Rank): 플레이어는 이름과 사용할 카드를 가진다.
donghyun81 Mar 4, 2025
8bf87d2
feat(Suit): 플레잉 카드의 문양을 만든다.
donghyun81 Mar 4, 2025
704e830
refactor(Card): Card 생성자 매개변수 타입 변경
donghyun81 Mar 4, 2025
ee5b93a
feat(Deck): 덱은 카드를 가진다.
donghyun81 Mar 4, 2025
ed8cb65
feat(Cards):카드들에서 카드를 뽑는다.
donghyun81 Mar 4, 2025
4f03187
feat(Cards): 카드들에서 카드를 제거한다
donghyun81 Mar 4, 2025
84ade66
feat(Player): 플레이어가 카드를 받는다.
donghyun81 Mar 4, 2025
d77d1f6
refactor: 기능 통합
donghyun81 Mar 4, 2025
293589a
feat(Dealer): 딜러는 이름과 사용할 카드를 가진다.
donghyun81 Mar 4, 2025
cf961c5
feat(Dealer): 딜러가 카드를 받는다.
donghyun81 Mar 4, 2025
2bbbab2
feat(Player): 플레이어는 본인의 카드 숫자의 총합을 반환한다.
donghyun81 Mar 4, 2025
3f87634
feat(Dealer): 딜러는 본인의 카드 숫자의 총합을 반환한다.
donghyun81 Mar 4, 2025
7a7987b
feat(Player): 플레이어는 총 점수를 반환한다.
donghyun81 Mar 4, 2025
289972d
feat(Dealer): 딜러는 총 점수를 반환한다.
donghyun81 Mar 4, 2025
f18c1ff
feat(Dealer): 각 플레이어의 승패를 반환한다
donghyun81 Mar 4, 2025
63dfcfa
feat(Dealer): 딜러의 승패들을 반환한다.
donghyun81 Mar 4, 2025
c488b7c
feat(View): 사용자는 게임에 참여할 인원들의 이름을 입력한다.
doabletuple Mar 6, 2025
fc961bd
feat(View): 사용자는 각 참여자가 카드를 받을지 받지 않을지 입력한다.
doabletuple Mar 6, 2025
b8411d6
feat(View): 참여자들의 이름과 나누어진 카드의 개수를 출력한다.
doabletuple Mar 6, 2025
03c03bb
feat(View): 참여자가 보유한 카드의 목록을 출력한다.
doabletuple Mar 6, 2025
5066054
feat(InputView): 참여자의 결과를 출력한다.
doabletuple Mar 6, 2025
e12691e
refactor: Dealer가 Player를 상속하도록 변경
doabletuple Mar 6, 2025
77ad558
refactor: 카드를 뽑을 때 뽑을 개수를 지정할 수 있도록 변경
doabletuple Mar 6, 2025
53b48a5
style: 스타일 가이드 준수
doabletuple Mar 6, 2025
7233d27
feat(Controller): 게임 초기화
doabletuple Mar 6, 2025
0ff8d04
feat(Choice): 참여자가 카드를 받을지 받지 않을지 입력한다.
doabletuple Mar 6, 2025
d3f8451
feat(Controller): 딜러의 점수가 17 이상이 될 때까지 카드를 받는다.
doabletuple Mar 6, 2025
42a7981
feat(View): 참여자의 결과를 출력한다.
doabletuple Mar 6, 2025
0689ab7
fix: 결과가 정상적으로 출력되지 않는 현상 수정
doabletuple Mar 6, 2025
87cca7a
refactor: Dealer 생성자 추가
doabletuple Mar 6, 2025
511229a
refactor: 테스트 수정
doabletuple Mar 6, 2025
c4f80be
refactor: 결과 출력 로직 수정
doabletuple Mar 6, 2025
54159f8
test: 플레이어 승/패/무 테스트
doabletuple Mar 6, 2025
ed7ab1e
test: 딜러 결과 반환 테스트
doabletuple Mar 6, 2025
8fabaf6
test: Choice isHit() 테스트
doabletuple Mar 6, 2025
fef77e8
test: Choice 예외 처리 테스트
doabletuple Mar 6, 2025
61ded93
refactor: Participants 일급 컬렉션 생성
doabletuple Mar 6, 2025
42b4723
test: 참가자들 중 딜러 반환 테스트
doabletuple Mar 6, 2025
8ab7d3f
test: 참가자들 중 플레이어 목록 반환 테스트
doabletuple Mar 6, 2025
6671836
fix: 딜러의 이름이 정상적으로 출력되지 않는 현상 수정
doabletuple Mar 6, 2025
9ff4309
feat: DSL 과제
doabletuple Mar 7, 2025
e0412d6
chore: .gitkeep 삭제
doabletuple Mar 7, 2025
5b8e6d2
refactor: 프로그래밍 요구 사항 준수
donghyun81 Mar 7, 2025
595f338
refactor(Participants): 부 생성자 추가
donghyun81 Mar 7, 2025
7b2a3ed
fix: 재귀함수 조건 변경
donghyun81 Mar 7, 2025
65c065d
feat: 카드를 모두 사용하면 다시 무작위 카드를 받아오도록 수정
donghyun81 Mar 7, 2025
e622be4
refactor: 테스트에서 showCards()를 통해 플레이어의 카드를 받아오도록 수정
doabletuple Mar 7, 2025
215a636
feat: 지정 타입 캐스트 처리 변경
donghyun81 Mar 7, 2025
fb2efec
refactor: 상수 오탈자 수정
doabletuple Mar 7, 2025
8abf380
feat: 오류 발생시에 재입력 기능 구현
donghyun81 Mar 7, 2025
06e0f50
feat(Player): 플레이어의 이름이 공백이 아님을 검증하는 기능 추가
doabletuple Mar 9, 2025
4e19222
refactor: Hand 클래스 생성
doabletuple Mar 9, 2025
103c402
refactor: 사용자 입력 후 빈 줄을 출력하도록 변경
doabletuple Mar 9, 2025
cb79364
refactor: 히트/스탠드를 입력받을 때 Action을 반환하도록 변경
doabletuple Mar 9, 2025
76e597d
refactor: 참가자들 중 딜러를 반환하는 로직 개선
doabletuple Mar 9, 2025
ac39944
refactor: Participants 추상 클래스 생성
doabletuple Mar 9, 2025
dadbe27
refactor: 승패를 가리는 로직 변경
doabletuple Mar 9, 2025
05673d2
refactor: 결과를 문자열로 표현하는 로직을 View로 이동
doabletuple Mar 9, 2025
eadd198
refactor: 입력 관련 메시지를 InputView가 직접 출력하도록 변경
doabletuple Mar 9, 2025
b1394f4
refactor: 변수, 함수 및 클래스 이름 변경
doabletuple Mar 9, 2025
34aaff5
fix: 게임 시작 시 딜러가 카드를 뽑지 않는 현상 수정
doabletuple Mar 9, 2025
9fb9ada
refactor: 클래스 이름 변경
doabletuple Mar 9, 2025
ed36eb7
refactor: 카드를 문자열로 표현하는 로직을 View로 이동
doabletuple Mar 9, 2025
5d92ba9
refactor: 코드 가독성 개선
doabletuple Mar 9, 2025
d93048b
refactor: 히트할 수 있는지 확인하는 로직 추상화
doabletuple Mar 9, 2025
159dbc0
refactor: 출력 형식 개선
doabletuple Mar 9, 2025
f0a6c29
fix: 결과 출력 시 딜러의 카드가 출력되지 않는 현상 수정
doabletuple Mar 9, 2025
c69d71d
refactor: 코드 가독성 개선
doabletuple Mar 9, 2025
2550c2a
test: 딜러 및 플레이어 테스트 추가
doabletuple Mar 9, 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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
# kotlin-blackjack
# kotlin-blackjack

- [x] 사용자는 게임에 참여할 인원들의 이름을 입력한다.
- [x] 사용자는 각 참여자가 카드를 받을지 받지 않을지 입력한다.
- [x] 참여자들의 이름과 나누어진 카드의 개수를 출력한다.
- [x] 참여자가 보유한 카드의 목록을 출력한다.
- [x] 참여자의 결과를 출력한다.
- [x] 카드는 문양과 등급을 가진다.
- [x] 플레이어는 이름과 사용할 카드를 가진다.
- [x] 플레잉 카드의 등급을 만든다.
- [x] 플레잉 카드의 문양을 만든다.
- [x] 카드들에서 카드를 뽑는다.
- [x] 플레이어가 카드를 받는다.
- [x] 딜러는 이름과 사용할 카드를 가진다.
- [x] 딜러가 카드를 받는다.
- [x] 플레이어는 총 점수를 반환한다.
- [x] 딜러는 총 점수를 반환한다.
- [x] 각 플레이어의 승패를 반환한다.
- [x] 딜러의 승패들을 반환한다.
- [x] 오류 발생시에 재입력 기능 구현
Empty file removed src/main/kotlin/.gitkeep
Empty file.
6 changes: 6 additions & 0 deletions src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import blackjack.controller.GameController

fun main() {
val gameController = GameController()
gameController.run()
}
87 changes: 87 additions & 0 deletions src/main/kotlin/blackjack/controller/GameController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package blackjack.controller

import blackjack.domain.model.Action
import blackjack.domain.model.Dealer
import blackjack.domain.model.Deck
import blackjack.domain.model.Participant
import blackjack.domain.model.Player
import blackjack.view.InputView
import blackjack.view.OutputView

class GameController(
private val inputView: InputView = InputView(),
private val outputView: OutputView = OutputView(),
) {
fun run() {
val deck = Deck()
val dealer = Dealer()
val players: List<Player> = inputView.readPlayerNames().map(::Player)
processInitialDeals(deck, listOf(dealer) + players)
announceInitialDeals(dealer, players)

players.forEach { player -> processPlayerHits(deck, player) }
processDealerHits(deck, dealer)

announceResults(dealer, players)
}

private fun processInitialDeals(
deck: Deck,
participants: List<Participant>,
) {
participants.forEach { player ->
player.accept(deck.draw(Participant.INITIAL_DRAW_COUNT))
}
}

private fun announceInitialDeals(
dealer: Dealer,
players: List<Player>,
) {
outputView.printInitialDeals(dealer, players)
outputView.printParticipantStatus(dealer, players)
}

private fun processPlayerHits(
deck: Deck,
player: Player,
) {
if (!player.canHit()) return
val action = inputView.readPlayerAction(player)
if (action == Action.STAND) {
printStatusOnNoHit(player)
return
}
player.accept(deck.draw())
outputView.printPlayerStatus(player)
processPlayerHits(deck, player)
}

private fun printStatusOnNoHit(player: Player) {
if (player.showHand().count() == Participant.INITIAL_DRAW_COUNT) outputView.printPlayerStatus(player)
}

private fun processDealerHits(
deck: Deck,
dealer: Dealer,
) {
while (dealer.canHit()) {
outputView.printDealerHit()
dealer.accept(deck.draw())
}
}

private fun announceResults(
dealer: Dealer,
players: List<Player>,
) {
outputView.printPlayerResult(dealer)
players.forEach { player -> outputView.printPlayerResult(player) }

outputView.printResultsHeader()
val playerResults = dealer.getPlayerResult(players)
val dealerResults = dealer.getDealerResults(playerResults)
outputView.printDealerResults(dealer, dealerResults)
playerResults.forEach { (player, result) -> outputView.printPlayerResult(player, result) }
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Action.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package blackjack.domain.model

enum class Action {
HIT,
STAND,
}
3 changes: 3 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Card.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package blackjack.domain.model

data class Card(val suit: Suit, val rank: Rank)
26 changes: 26 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Dealer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package blackjack.domain.model

class Dealer(name: String = DEALER_NAME) : Participant(name) {
constructor(name: String = DEALER_NAME, cards: List<Card>) : this(name) {
accept(cards)
}

override fun canHit(): Boolean {
return (computePoint() <= DEALER_HIT_THRESHOLD)
}

fun getPlayerResult(players: List<Player>): Map<Player, Result> {
return players.associateWith { player -> player.compareAgainst(this) }
}

fun getDealerResults(playerResults: Map<Player, Result>): Map<Result, Int> {
return Result.entries.associateWith { result ->
playerResults.values.count { playerResult -> result == playerResult.reverse() }
}
}

companion object {
const val DEALER_NAME = "딜러"
const val DEALER_HIT_THRESHOLD = 16
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Deck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package blackjack.domain.model

class Deck(cards: List<Card> = pack.shuffled()) {
private val cards: MutableList<Card> = cards.toMutableList()

fun draw(count: Int = Participant.DEFAULT_DRAW_COUNT): List<Card> {
if (cards.count() < count) cards.addAll(cards.shuffled())
return List(count.coerceAtMost(count)) { cards.removeFirst() }
}

companion object {
private val pack = Suit.entries.flatMap { suit -> makeCardsFrom(suit) }

private fun makeCardsFrom(suit: Suit): List<Card> {
return Rank.entries.map { rank ->
Card(suit, rank)
}
}
}
}
35 changes: 35 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Hand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package blackjack.domain.model

class Hand() {
private val _cards: MutableList<Card> = mutableListOf()
val cards: List<Card> get() = _cards.toList()

constructor(cards: List<Card>) : this() {
add(cards)
}

fun add(cards: List<Card>) {
_cards.addAll(cards)
}

fun computePoint(): Int {
val point = _cards.sumOf { it.rank.point }
return point + computeBonusPoint(point)
}

private fun computeBonusPoint(point: Int): Int {
if (point + BONUS_POINT <= BUST_THRESHOLD && hasAce()) return BONUS_POINT
return 0
}

private fun hasAce(): Boolean = _cards.any { it.rank == Rank.ACE }

fun isBusted(): Boolean {
return computePoint() > BUST_THRESHOLD
}

companion object {
private const val BUST_THRESHOLD = 21
private const val BONUS_POINT = 10
}
}
34 changes: 34 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Participant.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package blackjack.domain.model

abstract class Participant(val name: String) {
private val hand = Hand()

init {
require(name.isNotBlank()) { ERROR_MESSAGE_BLANK_PARTICIPANT_NAME }
}

fun showHand(count: Int = hand.cards.size): List<Card> {
return hand.cards.take(count).map { card -> card.copy() }
}

fun accept(cards: List<Card>) {
hand.add(cards)
}

fun computePoint(): Int {
return hand.computePoint()
}

fun isBusted(): Boolean {
return hand.isBusted()
}

abstract fun canHit(): Boolean

companion object {
const val INITIAL_DRAW_COUNT = 2
const val DEFAULT_DRAW_COUNT = 1

private const val ERROR_MESSAGE_BLANK_PARTICIPANT_NAME = "참가자의 이름은 공백일 수 없습니다."
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package blackjack.domain.model

class Player(name: String) : Participant(name) {
constructor(name: String, cards: List<Card>) : this(name) {
accept(cards)
}

override fun canHit(): Boolean {
return !isBusted()
}

fun compareAgainst(dealer: Dealer): Result {
if (isBusted()) return Result.LOSE
if (dealer.isBusted()) return Result.WIN

val point: Int = computePoint()
val dealerPoint: Int = dealer.computePoint()
return when {
point > dealerPoint -> Result.WIN
point < dealerPoint -> Result.LOSE
else -> Result.DRAW
}
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Rank.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package blackjack.domain.model

enum class Rank(val point: Int) {
ACE(1),
TWO(2),
THREE(3),
FOUR(4),
FIVE(5),
SIX(6),
SEVEN(7),
EIGHT(8),
NINE(9),
TEN(10),
JACK(10),
QUEEN(10),
KING(10),
}
16 changes: 16 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Result.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package blackjack.domain.model

enum class Result {
WIN,
LOSE,
DRAW,
;

fun reverse(): Result {
return when (this) {
WIN -> LOSE
LOSE -> WIN
else -> DRAW
}
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/blackjack/domain/model/Suit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package blackjack.domain.model

enum class Suit {
HEART,
SPADE,
DIAMOND,
CLUB,
}
51 changes: 51 additions & 0 deletions src/main/kotlin/blackjack/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package blackjack.view

import blackjack.domain.model.Action
import blackjack.domain.model.Player

class InputView {
fun readPlayerNames(): List<String> {
println(MESSAGE_REQUEST_PLAYER_NAMES)
val input: String = readInput()
return input.split(PLAYER_NAMES_DELIMITER).map { name: String -> name.trim() }
}

fun readPlayerAction(player: Player): Action {
return readUntilValid {
println(MESSAGE_REQUEST_PLAYER_YES_OR_NO.format(player.name))
val input: String = readInput().lowercase()
convertToAction(input)
}
}

private fun convertToAction(input: String): Action {
return when (input) {
PLAYER_ACTION_YES -> Action.HIT
PLAYER_ACTION_NO -> Action.STAND
else -> throw IllegalArgumentException(MESSAGE_ERROR_INVALID_CHOICE)
}
}

private fun readInput(): String {
val input: String = readln()
println()
return input
}

private fun <T> readUntilValid(event: () -> T): T {
while (true) {
kotlin.runCatching { event() }.onSuccess { return it }
.onFailure { println(it.message ?: it.stackTraceToString()) }
}
}

companion object {
private const val MESSAGE_REQUEST_PLAYER_NAMES = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"
private const val MESSAGE_REQUEST_PLAYER_YES_OR_NO = "%s은(는) 한 장의 카드를 더 받겠습니까? (예는 y, 아니오는 n)"
private const val MESSAGE_ERROR_INVALID_CHOICE = "y 또는 n을 입력해주세요."

private const val PLAYER_ACTION_YES = "y"
private const val PLAYER_ACTION_NO = "n"
private const val PLAYER_NAMES_DELIMITER = ","
}
}
Loading