Skip to content

Commit 01e2d7f

Browse files
authored
[2단계 - 블랙잭 베팅] 밍트(김명지) 미션 제출합니다. (#917)
* refactor: 패키지 이동 * refactor(Dealer): 우승 결과 판단 로직을 Dealer로 이동 * feat(generator): 인터페이스에 존재하는 상수 제거 - 인터페이스는 행위를 정의하기 위해 사용되어야한다. 상수가 존재하면 인터페이스가 하는 역할에 벗어난다. 관리와 변경도 어렵다. - 덱을 생성하도록 변경하고, 덱에서는 한장씩 뽑을 수 있도록 한다. * refactor(Deck): 카드 집합을 저장하는 Deck의 자료구조를 덱(Deque)으로 변경 - 기존에는 ArrayList로 저장하여 removeFirst를 하였는데, 카드 하나를 뽑을 때마다 모든 원소의 인덱스가 한칸씩 움직여 성능상 좋지 않았다. - 따라서 Deque을 이용하여 pollFrist()를 하면 O(1) 시간복잡도가 걸린다. * refactor: controller 이름 대신 BlackjackGame 사용 - controller가 흐름 처리 및 비즈니스 로직도 처리하고 있으므로, BlackjackGame이라는 클래스명으로 변경한다. * fix: 자료구조 변경에 따라 적절하게 예외 발생하도록 수정 * refactor(BlackjackGame): 메서드 호출 순서에 맞게 변경 * refactor: 응답 객체 view로 이동 - 응답에 대한 출력은 view의 역할이므로 view 패키지로 이동한다. * refactor: Players 내부 필드를 Gamer가 아닌 Player로 변경 - Player는 Gamer의 하위 클래스로 더 많은 역할을 할 수 있으므로 Player 참조하도록 변경한다. * refactor: Participants 객체 도입 - 참가자들을 나타내는 Participants 객체를 도입하여 참가자 공통 로직 수행 * refactor: 상속을 하지 않은 클래스에 final 붙임 - 명시적으로 상속을 불가능하게 만든다. * feat: 2장으로 만들어진 블랙잭를 우선순위가 높은 승리로 고려하도록 수정 * docs(README): 기능 목록에 기능 추가 - 추가된 요구사항에 따라 기능 목록을 수정한다. * feat: 베팅 금액 입력 기능 구현 * feat: 최종 수익 계산 기능 구현 * refactor(CardScore): CardScore 내부 자료구조를 List가 아닌 Integer로 변경 - ACE의 경우 특별하게 처리하고, 그 외는 점수값을 반환한다. - ACE 하나로 인해 모든 카드 점수가 List가 되는 것은 오버엔지니어링이 될 수 있다. Q,K,J가 추가 점수를 가질 확률이 낮기 때문이다. * refactor(InputView): Scanner를 생성자에서 주입하도록 수정 - 좀 더 유연하고 테스트하기 쉬운 구조로 변경한다. * feat(Players): 플레이어 생성 책임을 BlackjackGame에서 Players로 이동 * feat(Deck): 셔플된 덱을 생성하는 정적 팩토리 메서드 생성 - BlackjackGame은 셔플된 덱을 생성하기 위해 Deck의 생성자에 shuffleDeckGenerator를 생성하여 주입한다는 사실을 몰라도 생성 가능하다. * refactor(BlackjackGame): game 패키지로 이동 * refactor(Participants): 초기 카드 개수는 Deck이 아닌 Participants가 가지도록 수정 * feat(Hand): 하나의 카드로 Hand를 생성하는 정적 팩토리 메서드 생성 * refactor(BlackjackGame): 이중 반복문 제거 * refactor(Gamer, Dealer, Player,): gamer 패키지로 묶어 가독성 향상 * feat(Hand): 인덱스 검증 예외 추가 * refactor(CardScore): Set 자료구조를 이용하여 성능 향상 - Set을 이용하면 조회시 O(1)의 시간이 걸리므로 리스트보다 성능이 좋다. * refactor(Card): 테스트 메서드명 명확하게 변경 * refactor: 메서드명 수정 및 순서 변경 * test: 사용하지 않은 코드 제거 * refactor: 특수한 케이스를 나타내는 경우에만 정적 팩토리 메서드를 사용하고 그 외는 삭제하도록 구현 - 생성자로 대체 가능하다면 대체한다. * test(CardScore): 테스트 작성 * refactor: 변수 선언시 final 붙임 - 코드 일관성을 유지한다. * refactor(Hand): 메서드명 변경 * refactor(ProfitResult): 우승 결과가 아닌 수익을 저장하도록 수정 * refactor: Dealer가 초기에 카드 나눠주도록 수정 * refactor: ResultStatus가 수익률 저장하도록 수정 * refactor: 사용하지 않은 메서드 제거 * refactor: 메서드명 수정 * feat(GameRule): 게임 규칙을 집행하는 GameRule 인터페이스 추가 - 딜러는 게임의 참가자 뿐 아니라 규칙 집행자이다. 규칙 집행하는 로직을 인터페이스로 분리하여, 명세한다. * refactor: Gamer -> Participant 클래스명 변경 - 게임 참가자로서의 성격이 드러나도록 클래스명을 수정한다. * refactor: 메서드명 수정 * refactor: 사용하지 않는 상수 제거 * refactor: 패키지 변경 * refactor: 클래스명 수정 * refactor: 플레이어 관점으로 승패 로직 조정 - 딜러가 승패로직을 계산할 때 플레이어 기준으로 승패를 체크함을 반영한다. * feat: 승패 로직 계산을 담당하는 PlayerScore 객체 생성 * feat: controller가 최대한 흐름 처리만 하도록 구현 - BlackjackGame에서 흐름 처리 부분을 controller로 분리한다. - view와 결합도가 큰 카드 추가 여부는 BlackjackGame에서 수행할 수 없으므로 controller에 두었다. * refactor: 변수 추출 및 메서드명 수정 * refactor(ResultStatus): 객체에게 메세지 보내도록 수정 * feat: 예외 메세지 관리 클래스 추가 - 예외 메세지의 공통 포맷을 정의하여 생성한다. * feat(ShuffleDeckGenerator): 캐싱 적용 - 캐싱을 적용하여 카드들을 재사용한다. * refactor: 재정의한 equals & hashCode 제거 - 단순 getter를 이용하여 테스트한다. * refactor: 클래스명 수정 * refactor: 주석 제거 * refactor: static 블록 사용하여 초기화하도록 수정 - 클래스 로딩시에 한번만 수행되어 성능상 이점이 있다. * refactor: 수익을 다룰 때 BigDecimal 타입 사용 * refactor: BigDecimal에서 double 타입의 값 사용시 문자열 사용하도록 변경 - 오차가 발생하지 않도록 문자열로 값을 생성한다. * feat: 테스트를 위한 equals & hashCode 삭제 * refactor: 사용하지 않은 코드 삭제 * refactor: BlackjackGame이 게임 진행 흐름을 가지도록 수정 - BlackjackGame이 현재 턴의 플레이어 상태를 가지도록 한다. * refactor: 상태 패턴으로 리팩토링 - if문 분기 제거 - 각 상태별 로직 구현 * refactor: 패키지 분리 * refactor: 초기 상태를 고려한 상태 분기 처리 - 초기에 카드 두장을 받을 때는 버스트가 발생할 수 없으므로 분기 상황에서 제외한다. * test: 테스트 추가 * refactor: 메서드명 수정 * feat: 종료 상태에서 카드를 받으려고 하면 예외 발생하도록 수정 * fix: 딜러는 블랙잭이 될 수 없으므로 조건문 삭제 * test: stay 상태 테스트 추가 * refactor: 사용하지 않은 메서드 제거 * refactor: 사용하지 않은 ResultStatus 제거 * test: 테스트 추가 * refactor: 상수로 변경 * test: 테스트 추가 * test: 테스트 추가 * refactor: 불필요한 인터페이스 구현 제거 - Running은 State 인터페이스를 구현한 Started을 상속받기 때문에, 또 다시 State를 구현할 필요가 없다. * refactor: 클래스명과 필드명를 다르게 수정 - 클래스명과 필드명을 동일하게 하면 내부 필드를 가리키는지, 객체를 가리키는지 혼란스럽기 때문에 수정한다. * refactor: 객체명을 클래스명과 같도록 수정 * feat: 딜러 관련 카드 로직은 DealerRunning으로 이동 - Hand는 플레이어와 딜러 모두 가지므로, 딜러 관련 카드 로직은 DealerRunning으로 이동한다. * refactor: 파라미터명 변경 * fix: 초기 카드 2장일때만 블랙잭이도록 수정 * refactor: 메서드명 직관적으로 수정 * refactor: 메서드 위치 조정 * feat: 카드를 나눠주는 책임을 Participants가 갖도록 수정 - Dealer와 Players를 갖는 Participants가 카드를 나눠주도록 하여 의미상 자연스럽도록 수정한다. - 카드를 나눠주는 크기인 SPREAD_CARD_SIZE 공통 상수도 Participants로 이동한다. * feat: 종료 상태일 때 stay() 호출하면 아무 작업도 수행하지 않도록 수정 - stay()는 Running 상태일때 Stay() 상태로 바꾸는 메서드이다. - 이미 종료한 경우 stay()를 호출하면 아무 작업도 하지 않도록 한다. - 이를 통해 루프문에서 Running 상태인지, Finished 상태인지 매번 확인하지 않고, Running일때만 Stay()로 변하도록 할 수 있다. * refactor: 추상 클래스를 인터페이스로 수정 - 공통 필드나 구현 메서드가 존재하지 않으므로 인터페이스로 둔다. - 이를 통해 다중 구현을 가능하게 하여 확장성을 높일 수 있다. * refactor: 매개변수 DealerState -> Dealer로 변경 - DealerState는 State 타입이므로 Dealer의 타입임을 특정할 수 없다. - Dealer를 매개변수로 받아 상태 처리를 하면 Dealer의 상태임을 보장할 수 있다. * feat: 수익 계산 로직 State -> Player로 이동 - State는 Player와 Dealer 모두 쓰이므로 수익 계산 기준이 모호하다. 따라서 Player로 이동한다. * fix: 초기화되지 않는 문제 해결 - 초기에 BlackjackGame을 만들때 빈 상태에서 만들기 때문에, 딜러와 플레이어의 initialState()가 제대로 호출되지 않았다. - 블랙잭은 플레이어의 초기 카드 2장이 21인 경우에만 해당되므로, 기존에는 블랙잭이어도 초기 상태가 아니므로 블랙잭으로 판단되지 않았다. - 초기에 빈 카드로 생성하지 않고 deck에서 꺼낸 카드로 초기화되도록 수정한다. * refactor: 메서드명 수정 및 공통으로 사용하지 않은 인터페이스 메서드 제거 * refactor: 메서드명 수정 * refactor: 사용하지 않은 메서드 제거 * refactor: 클래스명 변경 및 오버라이드하지 않은 메서드 final 붙이도록 수정 * fix: 딜러와 플레이어 모두 블랙잭이면 수익은 0원이 되도록 수정 * refactor: 메서드명 수정
1 parent 23dfbf0 commit 01e2d7f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+2188
-1688
lines changed

README.md

+17-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
- 예외
99
- [x] 중복된 이름인 경우
1010

11+
### 베팅 금액 입력
12+
- [x] 베팅 금액을 입력받는다.
13+
- 예외
14+
- [x] 숫자가 아닌 경우
15+
- [x] 음수인 경우
16+
- [x] 0인 경우
17+
1118
### 카드 2장씩 분배 후 공개
1219

1320
- [X] 참가자별로 모두 2장씩 분배한다.
@@ -29,16 +36,22 @@
2936
- [x] 참가자 모두의 카드를 공개한다.
3037
- [x] 참가자가 지닌 카드의 합을 계산하여 공개한다.
3138

32-
### 승패 결과 출력
39+
### 승패 결과 계산
3340

3441
- [x] 딜러와 각 플레이어 사이의 승패를 계산한다.
35-
- 딜러와 플레이어가 둘 다 버스트이면, 딜러 승
42+
- 플레이어가 버스트이면 딜러가 버스트 여부와 상관없이, 딜러 승
3643
- 딜러나 플레이어 둘 중 한명이 버스트이면, 버스트가 아닌 사람이 승
3744
- 딜러와 플레이어가 둘 다 버스트가 아닌 상황
3845
- 카드 합이 같다면 무승부
3946
- 카드 합이 다르다면 합이 큰 사람이 승
40-
- [x] 딜러는 `0승 0무 0패` 형식으로 결과를 출력한다.
41-
- [x] 플레이어는 `` or `` or `` 형식으로 결과를 출력한다.
47+
48+
### 최종 수익 계산
49+
- [x] 딜러와 플레이어들의 최종 수익을 계산한다.
50+
- [x] 버스트의 경우 베팅 금액을 모두 잃음
51+
- [x] 버스트가 아니면서 카드 총 합이 더 크면 베팅 금액을 받음
52+
- [x] 플레이어의 처음 두 장의 합이 21일 경우 블랙잭이 되어 베팅 금액의 1.5배를 딜러에게 받음
53+
- [x] 딜러와 플레이어가 동시에 블랙잭인 경우(비긴 경우) 플레이어는 베팅 금액을 돌려받음
54+
- [x] 딜러와 플레이어들의 최종 수익 출력
4255

4356
## 블랙잭 도메인 용어
4457
- 딜러(Dealer) : 카드를 나눠주고 게임을 진행하는 역할
+9-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package blackjack;
22

3-
import blackjack.controller.BlackjackController;
3+
import blackjack.controller.Controller;
4+
import blackjack.blackjack.card.Deck;
45
import blackjack.view.InputView;
56
import blackjack.view.ResultView;
7+
import java.util.Scanner;
68

7-
public class Application {
9+
public final class Application {
810

911
public static void main(String[] args) {
10-
final BlackjackController blackjackController = makeController();
11-
blackjackController.run();
12+
Controller controller = makeController();
13+
controller.startGame(Deck.shuffled());
1214
}
1315

14-
private static BlackjackController makeController() {
15-
final InputView inputView = new InputView();
16+
private static Controller makeController() {
17+
final InputView inputView = new InputView(new Scanner(System.in));
1618
final ResultView resultView = new ResultView();
17-
return new BlackjackController(inputView, resultView);
19+
return new Controller(inputView, resultView);
1820
}
1921
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package blackjack.blackjack;
2+
3+
import blackjack.blackjack.card.Deck;
4+
import blackjack.blackjack.participant.Participants;
5+
import blackjack.blackjack.participant.Player;
6+
import blackjack.blackjack.result.ProfitResult;
7+
import java.util.ArrayDeque;
8+
import java.util.Queue;
9+
10+
public final class BlackjackGame {
11+
12+
private final Participants participants;
13+
14+
private Queue<Player> players;
15+
16+
public BlackjackGame(final Participants participants) {
17+
this.participants = participants;
18+
this.players = new ArrayDeque<>(participants.findHitEligiblePlayers().getPlayers());
19+
}
20+
21+
public boolean isPlaying() {
22+
return !players.isEmpty();
23+
}
24+
25+
public Player findCurrentTurnPlayer() {
26+
return players.poll();
27+
}
28+
29+
public ProfitResult makeProfitResult() {
30+
return participants.makeDealerWinningResult();
31+
}
32+
33+
public Participants getParticipants() {
34+
return participants;
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package blackjack.blackjack.card;
2+
3+
import java.util.Objects;
4+
5+
public final class Card {
6+
7+
private final Suit suit;
8+
private final Denomination denomination;
9+
10+
public Card(final Suit suit, final Denomination denomination) {
11+
this.suit = suit;
12+
this.denomination = denomination;
13+
}
14+
15+
public boolean isAce() {
16+
return this.denomination == Denomination.A;
17+
}
18+
19+
@Override
20+
public boolean equals(final Object o) {
21+
if (!(o instanceof final Card card)) {
22+
return false;
23+
}
24+
return suit == card.suit && denomination == card.denomination;
25+
}
26+
27+
@Override
28+
public int hashCode() {
29+
return Objects.hash(suit, denomination);
30+
}
31+
32+
public Suit getSuit() {
33+
return suit;
34+
}
35+
36+
public String getDenominationName() {
37+
return denomination.getName();
38+
}
39+
40+
public int getCardMinNumber() {
41+
return denomination.getMinNumber();
42+
}
43+
44+
public int getCardMaxNumber() {
45+
return denomination.getMaxNumber();
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package blackjack.blackjack.card;
2+
3+
import blackjack.blackjack.card.generator.DeckGenerator;
4+
import blackjack.blackjack.card.generator.ShuffleDeckGenerator;
5+
import blackjack.util.ExceptionMessage;
6+
import java.util.Deque;
7+
import java.util.stream.IntStream;
8+
9+
public final class Deck {
10+
11+
private final Deque<Card> cards;
12+
13+
public Deck(final DeckGenerator deckGenerator) {
14+
this.cards = deckGenerator.makeDeck();
15+
}
16+
17+
public static Deck shuffled() {
18+
return new Deck(new ShuffleDeckGenerator());
19+
}
20+
21+
public Hand drawCardsByCount(final int count) {
22+
return new Hand(IntStream.range(0, count)
23+
.mapToObj(o -> drawCard())
24+
.toList());
25+
}
26+
27+
public Card drawCard() {
28+
Card card = cards.pollFirst();
29+
if (card == null) {
30+
throw new IllegalStateException(ExceptionMessage.makeMessage("[ERROR] 카드가 더이상 없습니다."));
31+
}
32+
return card;
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package blackjack.blackjack.card;
2+
3+
import java.util.Set;
4+
5+
public enum Denomination {
6+
7+
A(1),
8+
TWO(2),
9+
THREE(3),
10+
FOUR(4),
11+
FIVE(5),
12+
SIX(6),
13+
SEVEN(7),
14+
EIGHT(8),
15+
NINE(9),
16+
TEN(10),
17+
K(10),
18+
Q(10),
19+
J(10);
20+
21+
private static final Set<Denomination> SPECIAL_CARD_SCORE = Set.of(Denomination.A, Denomination.K,
22+
Denomination.Q, Denomination.J);
23+
private static final int ACE_MAX_NUMBER = 11;
24+
25+
private final int score;
26+
27+
Denomination(final int score) {
28+
this.score = score;
29+
}
30+
31+
public String getName() {
32+
if (SPECIAL_CARD_SCORE.contains(this)) {
33+
return this.name();
34+
}
35+
return String.valueOf(score);
36+
}
37+
38+
public int getMinNumber() {
39+
return score;
40+
}
41+
42+
public int getMaxNumber() {
43+
if (this == A) {
44+
return ACE_MAX_NUMBER;
45+
}
46+
return score;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package blackjack.blackjack.card;
2+
3+
import blackjack.util.ExceptionMessage;
4+
import java.util.ArrayList;
5+
import java.util.Collections;
6+
import java.util.List;
7+
import java.util.Objects;
8+
9+
public final class Hand {
10+
11+
private static final int BLACKJACK_SIZE = 2;
12+
public static final int BURST_THRESHOLD = 21;
13+
private static final int ACE_SUBTRACT = 10;
14+
15+
private final List<Card> cards;
16+
17+
public Hand(final List<Card> cards) {
18+
this.cards = new ArrayList<>(cards);
19+
}
20+
21+
public Hand(final Card card) {
22+
this(List.of(card));
23+
}
24+
25+
public int calculateScore() {
26+
int maxScore = calculateScore(cards);
27+
if (isNotBurst(maxScore)) {
28+
return maxScore;
29+
}
30+
return subtractAce(maxScore);
31+
}
32+
33+
public int calculateWithHardHand() {
34+
return cards.stream()
35+
.mapToInt(Card::getCardMinNumber)
36+
.sum();
37+
}
38+
39+
public void add(final Card card) {
40+
cards.add(card);
41+
}
42+
43+
public void addAll(final Hand givenHand) {
44+
cards.addAll(givenHand.getCards());
45+
}
46+
47+
public boolean isBlackjack() {
48+
return cards.size() == BLACKJACK_SIZE && calculateScore() == BURST_THRESHOLD;
49+
}
50+
51+
public boolean isBust() {
52+
return calculateScore() > BURST_THRESHOLD;
53+
}
54+
55+
private int calculateScore(final List<Card> cards) {
56+
return cards.stream()
57+
.mapToInt(Card::getCardMaxNumber)
58+
.sum();
59+
}
60+
61+
private boolean isNotBurst(final int score) {
62+
return score <= BURST_THRESHOLD;
63+
}
64+
65+
private int subtractAce(int score) {
66+
int aceCount = countAce(cards);
67+
while (!isNotBurst(score) && aceCount-- > 0) {
68+
score -= ACE_SUBTRACT;
69+
}
70+
return score;
71+
}
72+
73+
private int countAce(final List<Card> cards) {
74+
return (int) cards.stream()
75+
.filter(Card::isAce)
76+
.count();
77+
}
78+
79+
private void validateIndex(final int start, final int end) {
80+
final int size = cards.size();
81+
if (start < 0 || end < 0 || start >= size || end > size) {
82+
throw new IllegalArgumentException(ExceptionMessage.makeMessage("인덱스는 0 이상 hand 크기 이하여야 합니다"));
83+
}
84+
if (start > end) {
85+
throw new IllegalArgumentException(ExceptionMessage.makeMessage("끝 인덱스는 시작 인덱스보다 커야합니다"));
86+
}
87+
}
88+
89+
@Override
90+
public boolean equals(final Object o) {
91+
if (!(o instanceof final Hand hand1)) {
92+
return false;
93+
}
94+
return Objects.equals(cards, hand1.cards);
95+
}
96+
97+
@Override
98+
public int hashCode() {
99+
return Objects.hashCode(cards);
100+
}
101+
102+
public int getSize() {
103+
return cards.size();
104+
}
105+
106+
public Card getFirstCard() {
107+
return cards.getFirst();
108+
}
109+
110+
public Hand getPartialHand(final int startInclusive, final int endExclusive) {
111+
validateIndex(startInclusive, endExclusive);
112+
return new Hand(cards.subList(startInclusive, endExclusive));
113+
}
114+
115+
public List<Card> getCards() {
116+
return Collections.unmodifiableList(cards);
117+
}
118+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package blackjack.blackjack.card;
2+
3+
public enum Suit {
4+
5+
SPADE,
6+
DIAMOND,
7+
HEART,
8+
CLOB;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package blackjack.blackjack.card.generator;
2+
3+
import blackjack.blackjack.card.Card;
4+
import java.util.Deque;
5+
6+
public interface DeckGenerator {
7+
8+
Deque<Card> makeDeck();
9+
}

0 commit comments

Comments
 (0)