Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d37008f
[1단계 - 블랙잭 게임 실행] 재즈(함석명) 미션 제출합니다. (#636)
seokmyungham Mar 12, 2024
75656d1
refactor: 출력에 의존하는 Dealer 내 상수를 제거를 위해 리팩토링
seokmyungham Mar 12, 2024
3dce8a7
refactor(Deck): 카드 52장을 재사용할 수 있도록 캐싱
seokmyungham Mar 12, 2024
4c41d0f
fix: dto 클래스를 도메인 패키지 안으로 이동
seokmyungham Mar 12, 2024
cdf6f80
docs(README): 구현 기능 목록 재정의
seokmyungham Mar 12, 2024
7b7d33f
refactor(Hand): 기존 Hand에서 패 점수를 계산하던 로직을 HandValue 객체로 분리
seokmyungham Mar 13, 2024
e99482b
feat(Player): 딜러의 HandValue를 파라미터로 받아 승,무,패,블랙잭 승리를 계산하는 로직 추가
seokmyungham Mar 13, 2024
872cb10
move: 의존 관계를 고려하여 dto 패키지 이동
seokmyungham Mar 13, 2024
cfcf2af
refactor(Player): 승무패를 계산하는 compete 메서드 리팩토링
seokmyungham Mar 13, 2024
23a17b0
feat(InputView): 플레이어로부터 배팅 금액을 입력받는 메서드 추가
seokmyungham Mar 13, 2024
157d53a
feat(GameAccount): 배팅 금액만큼 금액을 저장하는 저장소 클래스 생성
seokmyungham Mar 13, 2024
feaa005
feat(GameAccount): 배팅한 금액에 게임 결과를 적용하는 기능 추가
seokmyungham Mar 13, 2024
6753755
feat(GameAccount): 플레이어의 배팅 금액 결과로 딜러의 수익을 계산하는 기능 추가
seokmyungham Mar 14, 2024
920b6cf
feat: 딜러와 플레이어의 최종 수익 결과를 출력하는 기능 추가
seokmyungham Mar 14, 2024
af934a5
feat(GameAccount): map 저장소를 초기화하는 메서드 추가
seokmyungham Mar 14, 2024
661c788
refactor(BlackjackController): 컨트롤러에서 블랙잭 게임 흐름을 담당하는 로직을 분리
seokmyungham Mar 14, 2024
0343057
docs(README): 구현 기능 목록 동기화
seokmyungham Mar 14, 2024
19e55ee
fix: Deck, HandValue 생성자 접근 제한자 default로 수정
seokmyungham Mar 15, 2024
c565111
fix(HandValue): 블랙잭 요구 카드 개수(2) 조건 상수화
seokmyungham Mar 15, 2024
6134b06
refactor(HandValue): 가독성 향상을 위해 HandValue 생성 메서드 네이밍 수정
seokmyungham Mar 15, 2024
4384505
refactor(Player): 테스트를 위한 생성자 파라미터 수정
seokmyungham Mar 15, 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
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,50 @@

블랙잭 미션 저장소

## 우아한테크코스 코드리뷰
## 기능 요구 사항

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

- [x] 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 쪽이 승리한다.
- [x] 게임을 시작하면 플레이어는 두 장의 카드를 지급 받는다.
- [x] 이후 플레이어와 딜러의 카드를 출력한다.
- [x] 단, 딜러의 카드는 하나만 출력한다.
- [x] 플레이어는 게임을 시작할 때 원하는 금액을 배팅한다.
- [x] 플레이어는 카드의 숫자 합이 21을 초과하지 않는다면 카드를 원하는 만큼 다시 뽑을 수 있다.
- [x] 새로 받을 때 마다 해당 플레이어의 카드를 출력한다.
- [x] 플레이어가 카드를 다 받으면 딜러의 카드를 확인한다.
- [x] 딜러의 카드 합이 17 이상이 될 때 까지 카드를 받는다.
- [x] 플레이어의 게임 결과로부터 수익을 계산한다.
- [x] 블랙잭으로 승리 할 경우 배팅 금액의 1.5배를 딜러로부터 받는다.
- [x] 무승부인 경우 플레이어는 베팅한 금액을 돌려받는다.
- [x] 승리할 경우 베팅 금액만큼 받는다.
- [x] 패배할 경우 배팅 금액을 모두 잃는다.
- [x] 딜러와 플레이어의 카드, 결과와 최종 수익 결과를 출력한다.

### 카드

- [x] 클로버, 스페이드, 하트, 다이아몬드 모양을 가진다.
- [x] 카드는 2부터 10까지의 숫자와 Ace, King, Queen, Jack으로 이루어져 있다.
- [x] King, Queen, Jack은 10으로 계산한다.
- [x] ACE는 1 또는 11로 계산할 수 있다.

### 딜러, 플레이어 공통

- [x] 최소 1글자, 최대 5글자의 이름을 가진다.
- [x] ACE 카드를 가지는 경우, 일단 11로 계산한 뒤, 합이 21을 초과하면 1로 계산한다.
- [x] 현재 가진 카드 숫자의 합을 기준으로 카드를 더 받을 수 있는지 결정한다.
- [x] 딜러는 숫자의 합이 16 이하이면 카드를 더 받을 수 있다.
- [x] 플레이어는 숫자의 합이 21 이하이면 카드를 더 받을 수 있다.

### 플레이어

- [x] 플레이어의 이름은 중복될 수 없다.
- [x] 플레이어는 최소 2명부터 최대 8명까지 가능하다.
- [x] 딜러의 점수를 입력받아 승,무,패를 결정한다.
- [x] (블랙잭) 처음 두 장의 카드 합이 21일 경우 블랙잭.
- [x] (무승부) 딜러와 플레이어가 동시에 블랙잭인 경우.
- [x] (무승부) 플레이어의 점수가 21 이하이고, 딜러와 동점인 경우.
- [x] (승리) 플레이어의 점수가 21 이하이고, 딜러의 점수가 21을 초과하는 경우.
- [x] (승리) 플레이어와 딜러의 점수가 모두 21 이하이고, 딜러의 점수보다 큰 경우.
- [x] (패배) 플레이어의 점수가 21을 초과하면 딜러의 점수와 무관.
- [x] (패배) 플레이어와 딜러의 점수가 모두 21 이하이고, 딜러의 점수보다 작은 경우.
11 changes: 11 additions & 0 deletions src/main/java/blackjack/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package blackjack;

import blackjack.controller.BlackjackController;

public class Application {

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

import blackjack.domain.game.BlackjackGame;
import blackjack.domain.game.Money;
import blackjack.domain.gamer.Dealer;
import blackjack.domain.gamer.Player;
import blackjack.domain.gamer.Players;
import blackjack.dto.DealerInitialHandDto;
import blackjack.dto.HandDto;
import blackjack.dto.PlayerGameResultsDto;
import blackjack.dto.PlayerHandDto;
import blackjack.dto.PlayersHandDto;
import blackjack.view.InputView;
import blackjack.view.OutputView;
import blackjack.view.object.Command;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BlackjackController {

private final InputView inputView;
private final OutputView outputView;
private final BlackjackGame blackjackGame;

public BlackjackController() {
this.inputView = new InputView();
this.outputView = new OutputView();
this.blackjackGame = new BlackjackGame();
}

public void run() {
Players players = getPlayers();
Dealer dealer = new Dealer();
blackjackGame.distributeInitialHand(players, dealer);
blackjackGame.betPlayerMoney(receivePlayersBetMoney(players));
printInitialHands(players, dealer);

distributeCardToPlayers(players);
blackjackGame.distributeCardToDealer(dealer);

printDealerCanReceiveCardMessage(dealer);
printAllGamerScores(dealer, players);
printResult(players, dealer);
}

private Players getPlayers() {
List<String> playerNames = inputView.receivePlayerNames();
return new Players(playerNames);
}

private Map<Player, Money> receivePlayersBetMoney(Players players) {
Map<Player, Money> playerBetMoney = new HashMap<>();
for (Player player : players.getPlayers()) {
int betMoney = inputView.receivePlayerMoney(player.getName().value());
playerBetMoney.put(player, new Money(betMoney));
}
return playerBetMoney;
}

private void printInitialHands(Players players, Dealer dealer) {
outputView.printInitialHands(DealerInitialHandDto.fromDealer(dealer), PlayersHandDto.fromPlayers(players));
}

private void distributeCardToPlayers(Players players) {
for (Player player : players.getPlayers()) {
distributeCardToPlayer(player);
}
}

private void distributeCardToPlayer(Player player) {
while (player.canReceiveCard() && Command.isHit(getCommand(player))) {
blackjackGame.addCardToPlayer(player);
outputView.printPlayerHand(PlayerHandDto.fromPlayer(player));
}
}

private Command getCommand(Player player) {
return inputView.receiveCommand(player.getName().value());
}

private void printDealerCanReceiveCardMessage(Dealer dealer) {
if (dealer.canReceiveCard()) {
outputView.printDealerMessage();
}
}

private void printAllGamerScores(Dealer dealer, Players players) {
outputView.printDealerHandScore(HandDto.fromHand(dealer.getHand()));
outputView.printPlayersHandScore(PlayersHandDto.fromPlayers(players));
}

private void printResult(Players players, Dealer dealer) {
Money dealerIncome = blackjackGame.calculateDealerIncome(players, dealer);
PlayerGameResultsDto playerGameResultsDto = PlayerGameResultsDto.fromPlayerBetResults(blackjackGame.getStore());
outputView.printResult(dealerIncome.value(), playerGameResultsDto);
}
}
47 changes: 47 additions & 0 deletions src/main/java/blackjack/domain/card/Card.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package blackjack.domain.card;

import java.util.Objects;

public class Card {

private final CardShape cardShape;
private final CardNumber cardNumber;

public Card(CardShape cardShape, CardNumber cardNumber) {
this.cardShape = cardShape;
this.cardNumber = cardNumber;
}

public boolean isAce() {
return cardNumber == CardNumber.ACE;
}

public int getNumberValue() {
return cardNumber.getValue();
}

public CardShape getCardShape() {
return cardShape;
}

public CardNumber getCardNumber() {
return cardNumber;
}

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

@Override
public int hashCode() {
return Objects.hash(cardShape, cardNumber);
}
}
28 changes: 28 additions & 0 deletions src/main/java/blackjack/domain/card/CardNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package blackjack.domain.card;

public enum CardNumber {

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);

private final int value;

CardNumber(int value) {
this.value = value;
}

public int getValue() {
return value;
}
}
9 changes: 9 additions & 0 deletions src/main/java/blackjack/domain/card/CardShape.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package blackjack.domain.card;

public enum CardShape {

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

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

public class Deck {

private static final List<Card> CACHE = Arrays.stream(CardShape.values())
.flatMap(cardShape -> Arrays.stream(CardNumber.values())
.map(number -> new Card(cardShape, number))).toList();

Comment on lines +10 to +14
Copy link
Member

Choose a reason for hiding this comment

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

덱 캐싱 👍🏻

private final LinkedList<Card> cards;

public Deck() {
this.cards = new LinkedList<>(CACHE);
}

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

public void shuffle() {
Collections.shuffle(cards);
}
Comment on lines +17 to +27

Choose a reason for hiding this comment

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

미리 섞여있는 덱을 주지않고, 순서대로 정렬된 덱을 만든 후에, 해당 덱을 받는 쪽에서 섞어 주는 이유가 있을까?
생성자가 사용된 메서드들을 봤는데 지금 구조를 유지하면서도 public으로는 섞여있는 덱을 줄 수 있을 것 같아서.

public Deck() {
    List<Card> cards = new LinkedList<>(CACHE);
    Collections.shuffle(cards);
    this.cards = cards;
}

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

Copy link
Member Author

Choose a reason for hiding this comment

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

섞여 있는 덱, 덱을 생성하고 섞는 것 둘 다 나는 괜찮다고 생각해

깊게 생각 안해봤었는데,
블랙잭 게임에서 덱을 섞는다는 행위가 덱 입장에서는 당연하다는 걸 생각해보면
커찬 말대로 섞여 있는 덱을 사용하지 않을 이유가 없긴한 것 같네..


public Card draw() {
return cards.poll();
}

public List<Card> getCards() {
return new ArrayList<>(cards);
}
}
60 changes: 60 additions & 0 deletions src/main/java/blackjack/domain/game/BlackjackGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package blackjack.domain.game;

import blackjack.domain.card.Deck;
import blackjack.domain.gamer.Dealer;
import blackjack.domain.gamer.GameResult;
import blackjack.domain.gamer.Player;
import blackjack.domain.gamer.Players;
import java.util.Map;

public class BlackjackGame {

Choose a reason for hiding this comment

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

컨트롤러에서의 진행 로직을 최대한 BlackjackGame으로 분리한 것이 좋다. 굿굿!!


private final GameAccount gameAccount;
private final Deck deck;

public BlackjackGame() {
this.gameAccount = new GameAccount();
this.deck = new Deck();
}

public void distributeInitialHand(Players players, Dealer dealer) {
deck.shuffle();
setUpInitialHands(players, dealer);
}


private void setUpInitialHands(Players players, Dealer dealer) {
players.initAllPlayersCard(deck);
dealer.initCard(deck);
}

public void betPlayerMoney(Map<Player, Money> playersBetMoney) {
for (Player player : playersBetMoney.keySet()) {
gameAccount.betMoney(player, playersBetMoney.get(player));
}
}

public void addCardToPlayer(Player player) {
player.addCard(deck.draw());
}

public void distributeCardToDealer(Dealer dealer) {
while (dealer.canReceiveCard()) {
dealer.addCard(deck.draw());
}
}

public Money calculateDealerIncome(Players players, Dealer dealer) {
applyResultToBetMoney(players, dealer);
return gameAccount.calculateDealerIncome();
}

private void applyResultToBetMoney(Players players, Dealer dealer) {
Map<Player, GameResult> playerGameResults = players.collectPlayerGameResults(dealer.getHandValue());
gameAccount.applyGameResults(playerGameResults);
}

public Map<Player, Money> getStore() {
return gameAccount.getStore();
}
}
44 changes: 44 additions & 0 deletions src/main/java/blackjack/domain/game/GameAccount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package blackjack.domain.game;

import blackjack.domain.gamer.GameResult;
import blackjack.domain.gamer.Player;
import java.util.LinkedHashMap;
import java.util.Map;

public class GameAccount {

private static final Map<Player, Money> store = new LinkedHashMap<>();

public void betMoney(Player player, Money money) {
store.put(player, money);
}

public Money findMoney(Player player) {
return store.get(player);
}
Comment on lines +8 to +18

Choose a reason for hiding this comment

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

  1. Player가 배팅한 금액을 들고있으면서, 해당 로직들을 최대한 Player 안으로 넣을 수 있었을 것 같은데, Map으로 관리한 이유가 있을까?
    (Map으로 사용한다면, 돈을 외부에서 관리하고 Player는 승패만을 관리한다는 점에서 좋을 것 같은데, 조금 큰 단위의 변경이 필요하다면 Player, GameAccount를 둘 다 손봐야 해서 더 힘들어질수도 있겠다는 생각이 드네)
  2. Map 형식을 차용했다면, storeprivate static final 말고 private final로 각 객체마다 store를 하나씩 들고 있는 것이 좋을 것 같아. 이걸 static으로 이용한다면, GameAccount를 여러개 사용할 때 각 고객 데이터가 공유될 수 있을 것 같아.

Copy link
Member Author

Choose a reason for hiding this comment

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

커찬 말도 맞는 말이라고 생각해.
딜러 입장에서 승패를 결정하는 사람이 있고, 플레이어 입장에서 승패를 결정하는 사람이 있다면
나는 후자의 입장이 좀 더 자연스럽다고 생각해서 일단 플레이어가 승패 로직을 들고 있도록 구현했었어.

그런데 배팅 금액을 플레이어가 가지고 있게 되면, 플레이어의 역할이 너무 많아질 거라 생각해
(커찬이 제시한 방향대로 설계를 했다면, 아마 나는 딜러나 다른 객체에게 승패 결정을 맡겼을 것 같아)

조금 큰 단위의 변경이 필요하다면 Player, GameAccount를 둘 다 손봐야 해서 더 힘들어질수도 있겠다는 생각이 드네

그리고 Repository 개념의 Map을 선택한 이유를 커찬의 생각과 반대로 적용시켜보면
정말 큰 단위의 변경이 일어났을 때 한 곳만을 수정할 수 있을지는 조금 의문이 들기도 하는데
상태 데이터를 한 곳에서 관리한다는 측면에서 서비스 확장성을 생각했을 때 이 방법이 좋은 선택이 될 수 있을 것 같아

Copy link
Member Author

Choose a reason for hiding this comment

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

앗 2번은 내가 일부러 static final 키워드를 사용한 이유이기도 해
해당 Map을 모든 GameAccount가 공동으로 사용하길 원했고, 프로젝트 실행 동안 "단 하나"만 존재하길 원했어
(블랙잭 게임의 공용 배팅 계좌? 같은 느낌을 원했던 건데 의미가 잘 전달이 안된건 내가 네이밍을 잘 못했던 것 같군..)

Choose a reason for hiding this comment

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

그런데 배팅 금액을 플레이어가 가지고 있게 되면, 플레이어의 역할이 너무 많아질 거라 생각해

나도 이 의견에 대해서 매우 공감해! 확실히 많이 무거워 질 것 같네...

해당 Map을 모든 GameAccount가 공동으로 사용하길 원했고, 프로젝트 실행 동안 "단 하나"만 존재하길 원했어

그러면 Map이 단 하나만 존재하는게 아니라, GameAccount가 단 하나만 존재해야 되지 않을까? 지금처럼 new GameAccount()로 생성하는 것을 허용한다면, 모든 GameAccount가 공용 데이터를 쓴다고 생각하지 못할 것 같아. (아마 싱글톤 패턴의 도입을 고려해봐야 하지 않을까?)


public void applyGameResults(Map<Player, GameResult> gameResults) {
for (Player player : gameResults.keySet()) {
Money money = store.get(player);
GameResult gameResult = gameResults.get(player);
Money gameResultMoney = money.multipleRatio(gameResult.getRatio());
store.put(player, gameResultMoney);
}
}

public Money calculateDealerIncome() {
int dealerIncome = 0;
for (Money money : store.values()) {
dealerIncome += money.value();
}
return new Money(-dealerIncome);
}
Comment on lines +29 to +35
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public Money calculateDealerIncome() {
int dealerIncome = 0;
for (Money money : store.values()) {
dealerIncome += money.value();
}
return new Money(-dealerIncome);
}
public Money calculateDealerIncome() {
int total = calculatePlayersTotalProfit()
return new Money(-total);
}

플레이어 수익의 총합을 계산할 수 있다라는 메서드로 구분할 수 있을 것 같네
책임 분리의 관점에서 고치는게 좋아보이는 것 같아 😀

그리고 이렇게 고치면서 가장 좋은 효과를 볼 수 있는 부분은 따로 있다고 생각하는데 바로 dealerIncome이라는 불변하지 않은 변수를 사용하지 않아도 된다는 것!

상태가 변하게 되는 변수는 (맥락에 따라 다르겠지만) 시스템의 취약성과 직결된다고 생각해
불변하게 사용할 수 있으면 사용하는게 개인적으로 좋다는 생각이야! 👊

Copy link
Member

Choose a reason for hiding this comment

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

추가적으로 만약 calculatePlayersTotalProfit메서드가 Money를 반환하게 하고

Money도메인에 inverse혹은 negate메서드 기능을 추가하면 금액을 마이너스로 바꾸는 책임을 책임에 맞는 도메인에 위임할 수 있을 듯 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

오오 리비 정말 좋은 의견인 것 같아
예리하군 👍 👍


public Map<Player, Money> getStore() {
return new LinkedHashMap<>(store);
}

public void clearStore() {
store.clear();
}
}
Loading