Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
888b9e4
[1단계 - 블랙잭 게임 실행] 커찬(이충안) 미션 제출합니다. (#622)
leegwichan Mar 12, 2024
a2adac3
refactor (Player) : `equals()` 의미에 오해를 불러일으킬 수 있어, equals()를 제거하고 isE…
leegwichan Mar 13, 2024
8315134
refactor (Players) : 복잡도를 증가시키는 play() 함수 제거
leegwichan Mar 13, 2024
9ff2eb7
docs (README) : 2단계에서 추가된 요구 사항 정리
leegwichan Mar 13, 2024
489c685
refactor : 더 이상 사용하지 않는 인터페이스 제거
leegwichan Mar 13, 2024
150cc56
feat (Money) : 돈은 항상 양수만 가지도록 검증하는 기능 구현
leegwichan Mar 13, 2024
a121d1a
feat (Monet) : 해당 배수만큼 곱하는 기능 구현
leegwichan Mar 13, 2024
106c036
feat (Money) : 금액끼리 서로 더하는 기능 구현
leegwichan Mar 13, 2024
0578c39
feat (Blackjack) : 블랙잭 일 때만 해당 객체를 생성할 수 있도록 검증하는 기능 구현
leegwichan Mar 13, 2024
786608a
refactor (Hand) : Hand를 용이하게 사용하기 위해 불변 객체로 변경
leegwichan Mar 13, 2024
dac6b93
feat (Blackjack) : 블랙잭에서 카드를 추가할 경우, 버스트가 되는 기능 구현
leegwichan Mar 13, 2024
6146caf
refactor (Card) : 카드의 원활한 트래킹을 위해, toString() 구현
leegwichan Mar 13, 2024
5f7b0be
feat (HandRankFactory) : 현재 손패에 따라 다른 HandRank가 생성되는 기능 구현
leegwichan Mar 13, 2024
f2708af
refactor (Money) : 컨벤션에 따라 메서드 순서 변경
leegwichan Mar 13, 2024
979de37
feat (Blackjack) : 해당 랭크가 블랙잭, 버스트인지 확인하는 기능 구현
leegwichan Mar 13, 2024
7ed3ca0
feat (Blackjack) : 딜러가 블랙잭일 때, 승패 판단 기능 구현
leegwichan Mar 13, 2024
124faa7
feat (Bust) : 해당 랭크가 블랙잭, 버스트인지 확인하는 기능 구현
leegwichan Mar 13, 2024
ad9ae1e
feat (Blackjack) : 딜러가 버스트일 때, 승패 판단 기능 구현
leegwichan Mar 13, 2024
35d8986
feat (HandRank) : 일반 케이스에서 두 점수를 비교하기 위해, HandRank에서 Score를 반환하는 기능 구현
leegwichan Mar 13, 2024
a4e86a6
test (NormalRank) : 해당 랭크가 블랙잭, 버스트인지 확인하는 기능 구현
leegwichan Mar 13, 2024
4cbebeb
style (Stand) : NormalRank 네이밍이 추상적이어서, 도메인 용어 중 하나인 Stand로 변경
leegwichan Mar 14, 2024
c74a451
feat (Stand) : 딜러가 Stand일 때, 승패 판단 기능 구현
leegwichan Mar 14, 2024
14c2d9b
style (BetAomunt) : 단순히 애매한 돈 이라는 표현보다, 도메인 용어인 배팅 금액으로 클래스 이름 변경
leegwichan Mar 14, 2024
cf300b7
feat (Profit) : 배팅 금액과 승패에 따라 이익을 계산하는 기능 구현
leegwichan Mar 14, 2024
2ddd8de
feat (Profit) : 두 이익을 더하여 계산할 수 있다.
leegwichan Mar 14, 2024
58e9925
feat (Profit) : 얻은 만큼 잃은 이익을, 잃은 만큼 얻은 이익을 구하는 기능 구현
leegwichan Mar 14, 2024
51e5232
feat (SingleMatchResult) : 매치 결과에 따라 플레이어, 딜러의 이익을 계산하는 기능 구현
leegwichan Mar 14, 2024
c707523
feat (Dealer) : 플레이어들의 이익 결과를 출력하는 기능 구현
leegwichan Mar 14, 2024
1ff93c7
feat (Dealer) : 플레이어들과 대결 후, 딜러의 이익을 계산하는 기능 구현
leegwichan Mar 14, 2024
25408c2
feat (InputView) : 배팅 금액을 입력받는 기능 구현
leegwichan Mar 14, 2024
8e74520
feat (OutputView) : 딜러 및 플레이어들의 최종 결과 출력 기능 구현
leegwichan Mar 14, 2024
8e13c47
feat (BlackjackGame) : 추가된 요구 사항을 반영하여, 전반적인 제어 로직 수정
leegwichan Mar 14, 2024
69045b8
refactor (OutputView) : 사용하지 않는 메서드 제거 및 일부 메서드 분리 실시
leegwichan Mar 14, 2024
9d1e979
feat (Players) : 사용하지 않는 메서드 제거 및 중복 확인 로직 리팩터링 실시
leegwichan Mar 14, 2024
ddad41a
refactor (Participant) : 사용하지 않는 메서드 및 테스트 제거
leegwichan Mar 14, 2024
4a1481b
refactor : SingleMatchResult, BetAmount에서 사용하지 않는 메서드 제거
leegwichan Mar 14, 2024
1d59566
refactor (Profit) : 정적 팩토리 메서드가 편의성이 떨어져, 사용하기 용이한 하나의 메서드롤 통합
leegwichan Mar 14, 2024
329a6e8
style (Test) : 불필요한 필드 제거 및 불필요한 접근 제어자 제거
leegwichan Mar 14, 2024
795a5d3
refactor (HandRankFactory) : static method만 있는 클래스를 객체로 생성하지 못하도록 pri…
leegwichan Mar 14, 2024
4542815
refactor (Players) : 입력받는 List가 null일 경우 예외를 던지도록 변경
leegwichan Mar 14, 2024
243e8f8
refactor(HandRank) : 각 핸드 결과마다 가능한 점수 검증 기능 추가
leegwichan Mar 14, 2024
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,54 @@
## 우아한테크코스 코드리뷰

- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)

## 기능 요구 사항

- 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다.
- 각 카드는 점수를 가진다.
- 숫자 카드는 카드 숫자로 계산한다.
- King, Queen, Jack은 각각 10으로 계산한다.
- Ace는 1 또는 11로 계산한다.
- 플레이어는 이름을 가진다.
- 이름은 공백으로만 구성될 수 없다.
- 이름은 앞뒤 공백을 가질 수 없다.
- 중복된 이름은 가질 수 없다.

### 카드 승패에 따른 플레이어의 수익

- 플레이어가 배팅 금액의 1.5배를 얻는 경우
- 플레이어가 '블랙잭'이고, 딜러가 블랙잭이 아닌 경우
- 플레이어가 배팅 금액만큼 얻는 경우
- 플레이어가 '블랙잭'이 아니면서, 딜러보다 점수가 높은 경우
- 플레이어의 수익이 0인 경우
- 플레이어, 딜러 모두 '블랙잭'인 경우
- 플레이어, 딜러 모두 '블랙잭'이 아니면서,
- 플레이러가 배팅 금액만큼 잃는 경우
-

#### 딜러의 수익

- 플레이어가 수익을 얻는 만큼 잃는다.
- 플레이어가 수익을 잃은 만큼 얻는다.
- 모든 플레이어와 비교한 뒤에, 딜러가 얻은 수익들을 모두 더한다.

#### 관련 용어

- 블랙잭 : 카드 2장 만으로 21점을 만든 경우 (ex. A스페이드, K스페이드)
- 버스트 : 카드 점수의 합이 21을 초과한 경우 (ex. K스페이드, K다이아몬드, 2스페이드)

### 프로그램 진행 순서

1. 참가자 이름 입력한다.
2. 각 참가자의 배팅 금액을 입력받는다.
3. 카드를 2장씩 나눠준 후에, 현재 카드 상태 출력한다. (딜러는 1장만 출력)
4. 각 참가자들의 턴을 진행한다.
1. 카드가 21을 넘은 경우, 턴이 종료된다.
2. 카드를 더 받을 지 여부를 입력한다.
3. 만약 더 받는다면, 카드를 한 장 추가하고 1으로 돌아간다.
4. 더 받지 않을 경우, 턴을 종료한다.
5. 딜러의 턴을 진행한다.
1. 카드가 16을 넘은 경우, 턴이 종료된다.
2. 카드를 한 장 더 추가하고, 1로 돌아간다.
6. 딜러 및 모든 참가자의 보유 카드들과 점수를 출력한다.
7. 딜러 및 모든 참가자의 최종 수익을 출력한다.
Empty file removed src/main/java/.gitkeep
Empty file.
12 changes: 12 additions & 0 deletions src/main/java/blackjack/BlackJackApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package blackjack;

public class BlackJackApplication {

private BlackJackApplication() {
}

public static void main(String[] args) {
BlackJackGame blackJackGame = new BlackJackGame();
blackJackGame.run();
}
}
70 changes: 70 additions & 0 deletions src/main/java/blackjack/BlackJackGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package blackjack;

import blackjack.domain.card.Deck;
import blackjack.domain.money.Profit;
import blackjack.domain.participant.Dealer;
import blackjack.domain.participant.Player;
import blackjack.domain.participant.Players;
import blackjack.view.InputView;
import blackjack.view.OutputView;
import java.util.List;
import java.util.Map;

public class BlackJackGame {

private final InputView inputView = new InputView();
private final OutputView outputView = new OutputView();

public void run() {
Deck deck = Deck.createShuffledDeck();
Dealer dealer = new Dealer();
Players players = createPlayers();

drawStartCards(dealer, players, deck);
play(players, dealer, deck);
printResult(dealer, players);
}

Comment on lines +17 to +27
Copy link

Choose a reason for hiding this comment

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

실화인가요? 왜 이렇게 깔끔하죠 👍👍👍

Copy link
Author

Choose a reason for hiding this comment

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

페어였던 짱수에게 이 영광을 돌립니다 👍👍👍

private Players createPlayers() {
List<String> names = inputView.inputPlayerNames();
List<Player> players = names.stream()
.map(name -> Player.from(name, inputView.inputBetAmount(name)))
.toList();
return new Players(players);
}

private void drawStartCards(Dealer dealer, Players players, Deck deck) {
dealer.drawStartCards(deck);
players.drawStartCards(deck);
outputView.printStartStatus(dealer, players);
}

private void play(Players players, Dealer dealer, Deck deck) {
for (Player player : players.getPlayers()) {
playTurn(player, deck);
}
playTurn(dealer, deck);
}

private void playTurn(Player player, Deck deck) {
while (player.isDrawable() && inputView.isPlayerWantDraw(player.getName())) {
player.add(deck.draw());
outputView.printPlayerCards(player);
}
}

Comment on lines +49 to +55
Copy link
Member

Choose a reason for hiding this comment

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

저번에 얘기해줬던 리뷰어의 코멘트 반영했나 보네 👍

나는 개인적으로 provider느낌으로 아이디어가 번뜩인다고 생각하긴 했는데
외부에서 주입받는 동작에 의존하는 것이 좋지 않긴 한가봐 🤔

1단계 때 커찬이 리뷰내용 공유해줘서 나도 많은 생각해볼 수 있었으 👍

Copy link
Author

Choose a reason for hiding this comment

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

나와 페어도 번뜩이는 아이디어라고 생각해서 도입했는데,
언제든지 외부에서 Player의 행동을 막 시킬 수 있는 느낌이라, 리뷰어는 복잡도가 올라가는 만큼의 장점은 안느껴졌던 것 같아. 이 말에 어느정도 납득이 가서, 그냥 getPlayers() 사용하도록 바꿨어

private void playTurn(Dealer dealer, Deck deck) {
while (dealer.isDrawable()) {
outputView.printDealerDraw();
dealer.add(deck.draw());
}
}

private void printResult(Dealer dealer, Players players) {
outputView.printEndingStatus(dealer, players);

Profit dealerProfit = dealer.calculateDealerProfit(players);
Map<Player, Profit> playersProfit = dealer.calculatePlayersProfit(players);
outputView.printMatchResult(dealerProfit, playersProfit);
}
}
Comment on lines +63 to +70
Copy link

Choose a reason for hiding this comment

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

이 부분에만 개행이 있네요! 당연히 의도한 바라고 생각이 드는데요~ 어떤 의도인지 궁금해요 💭
만약 개행을 사이로 컨텍스트가 다르다는 근거라면, 메서드를 분리하는 것은 어떨까요? 이에 대한 커찬의 의견이 궁금해요 💭

Copy link
Author

Choose a reason for hiding this comment

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

솔직히 말하면, 그냥 생각없이 해버렸다는 게 맞을 것 같아요.

굳이 무의식을 분석해보자면,

  • run() 부분에서 결과를 출력하는 부분을 printResult()로 구분함
  • 이 로직이 printEndingStatus와, printMatchResult를 호출하는 부분으로 나눌 수 있는데, 이걸 나누게 된다면 printEndingStatus 부분이 한 줄 인데도 하나의 메서드로 나눠야 함 (굳이??)
    이런 느낌으로 생각했던 것 같아요~ 그래서 그냥 한 줄 띄어서 컨텍스트를 구분했던 것 같습니다~

60 changes: 60 additions & 0 deletions src/main/java/blackjack/domain/card/Card.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package blackjack.domain.card;

import java.util.Objects;
import java.util.StringJoiner;

public class Card {

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.

record로 사용한다면 코드가 더 깔끔해질 것 같아요.

private final Value value;
private final Shape shape;

public Card(Value value, Shape shape) {
this.value = Objects.requireNonNull(value);
this.shape = Objects.requireNonNull(shape);
}

public int getMinScore() {
return value.getMinScore();
}

public int getMaxScore() {
return value.getMaxScore();
}

public boolean isAce() {
return value.isAce();
}

public Value getValue() {
return value;
}

public Shape getShape() {
return shape;
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
Card card = (Card) object;
return value == card.value && shape == card.shape;
}

@Override
public int hashCode() {
return Objects.hash(value, shape);
}

@Override
public String toString() {
return new StringJoiner(", ", Card.class.getSimpleName() + "[", "]")
.add("value=" + value)
.add("shape=" + shape)
.toString();
}
Comment on lines +53 to +59
Copy link
Member

Choose a reason for hiding this comment

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

디버깅용인가 🤔

toString은 디버깅 용으로 많이 사용한다고 하는데 어떤 부분에서 디버깅이 필요했는지, 테스트 코드만으로 방어할 수 없었는지 커찬의 생각 궁금하네 💭

Copy link
Author

Choose a reason for hiding this comment

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

사실 toString()을 붙였던 시기는 2단계를 구현하다가 한 번 갈아엎기 전에 가시성이 떨어져서 붙였어
최대한 테스트코드로 구분하려고 했는데, @ParameterizedTest가 쓰인 부분이 많아서 어떤 케이스가 실패했는지 구분하기 어려워서 toString을 붙였어.
그리고는 만든 toString()을 지울 이유는 없었어서 그냥 놔두었어

}
40 changes: 40 additions & 0 deletions src/main/java/blackjack/domain/card/Deck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package blackjack.domain.card;

import static java.util.stream.Collectors.toList;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class Deck {

private final Queue<Card> cards;

private Deck(List<Card> cards) {
this.cards = new LinkedList<>(cards);
}

public static Deck createShuffledDeck() {
List<Card> cards = Arrays.stream(Shape.values())
.map(Deck::makeCards)
.flatMap(List::stream)
.collect(toList());
Collections.shuffle(cards);
return new Deck(cards);
}

private static List<Card> makeCards(Shape shape) {
return Arrays.stream(Value.values())
.map(value -> new Card(value, shape))
.toList();
}

public Card draw() {
if (cards.isEmpty()) {
throw new IllegalStateException("카드를 더 이상 뽑을 수 없습니다.");
}
return cards.poll();
}
}
69 changes: 69 additions & 0 deletions src/main/java/blackjack/domain/card/Hand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package blackjack.domain.card;

import java.util.ArrayList;
import java.util.List;

public class Hand {

public static final int BLACKJACK_SCORE = 21;
private static final int BLACKJACK_SIZE = 2;

private final List<Card> cards;

public Hand(List<Card> cards) {
this.cards = List.copyOf(cards);
}

public Hand add(Card card) {
List<Card> newCards = new ArrayList<>(cards);
newCards.add(card);

return new Hand(newCards);
}
Comment on lines +17 to +22
Copy link
Member

Choose a reason for hiding this comment

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

핸드에 카드를 더하는 메서드에 리턴 타입이 필요하다고 생각했던 것 같아 🤔
의도가 있어 보이는데 공유해줄 수 있을깜 😃

Copy link
Author

Choose a reason for hiding this comment

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

처음에는 Hand라는 얘를 가변객체로 사용하고 있었는데, 갈아엎기 전에 이게 가변객체이다보니 불편한 점이 많더라고. 그래서 카드를 한 장 추가할 때 카드를 한장 추가한 불변 객체로 만들었어
이것도 다시 롤백시킬 수 있었지만, 지금 성능에 문제는 안 생길 것 같고 오히려 유지보수하기 편하다는 생각이 들어서 불변객체로 놔두었어.


public int calculateScore() {
int totalMinScore = getMinScore();
int biggerScore = getBiggerScore();

if (biggerScore > BLACKJACK_SCORE) {
return totalMinScore;
}
return biggerScore;
}

private int getMinScore() {
return cards.stream()
.mapToInt(Card::getMinScore)
.sum();
}

private int getBiggerScore() {
int score = getMinScore();
int differenceScore = cards.stream()
.filter(Card::isAce)
.mapToInt(this::calculateDifferenceScore)
.findAny()
.orElse(0);
Comment on lines +42 to +46
Copy link
Member

Choose a reason for hiding this comment

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

에이스에 대한 처리를 가중치로써 진행한다면 파생되는 코드를 줄일 수 있을 것 같다는 개인적인 생각 🧐

Copy link
Author

Choose a reason for hiding this comment

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

나는 오히려 반대.
여기에서 사용하는 Card의 기본 목적은 "ACE가 1 또는 11을 갖는다"는 정보를 최대한 Card 안쪽으로 숨긴다는 목적이 컸어. Ace일 때 10의 가중치를 줄수도 있지만, "10의 가중치"라는 정보다 Card가 아닌 Hand에서 관리하는게 캡슐화에 저해된다고 생각했어. 비록 코드가 더 복잡해지긴 했지만, 추후 유지보수에는 유리할거라고 생각해

return score + differenceScore;
}

private int calculateDifferenceScore(Card card) {
return card.getMaxScore() - card.getMinScore();
}

public boolean isBusted() {
return calculateScore() > BLACKJACK_SCORE;
}

public boolean isBlackjack() {
return cards.size() == BLACKJACK_SIZE && calculateScore() == BLACKJACK_SCORE;
}

public List<Card> getCards() {
return cards;
}

public boolean isEmpty() {
return cards.isEmpty();
}
}
5 changes: 5 additions & 0 deletions src/main/java/blackjack/domain/card/Shape.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package blackjack.domain.card;

public enum Shape {
SPADE, DIAMOND, HEART, CLOVER
}
37 changes: 37 additions & 0 deletions src/main/java/blackjack/domain/card/Value.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package blackjack.domain.card;

public enum Value {
ACE(1, 11),
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(10, 10),
QUEEN(10, 10),
KING(10, 10);

private final int minScore;
private final int maxScore;

Value(int minScore, int maxScore) {
this.minScore = minScore;
this.maxScore = maxScore;
}

public int getMinScore() {
return minScore;
}

public int getMaxScore() {
return maxScore;
}

public boolean isAce() {
return this == ACE;
}
}
29 changes: 29 additions & 0 deletions src/main/java/blackjack/domain/handrank/Blackjack.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package blackjack.domain.handrank;

import blackjack.domain.card.Hand;

public final class Blackjack implements HankRank {

@Override
public SingleMatchResult matchAtDealer(HankRank playerHandRank) {
if (playerHandRank.isBlackjack()) {
return SingleMatchResult.DRAW;
}
return SingleMatchResult.DEALER_WIN;
}

@Override
public int getScore() {
return Hand.BLACKJACK_SCORE;
}

@Override
public boolean isBlackjack() {
return true;
}

@Override
public boolean isBust() {
return false;
}
}
Loading