Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6f92f8f
[1단계 - 블랙잭 게임 실행] 레디(최동근) 미션 제출합니다. (#642)
reddevilmidzy Mar 12, 2024
c7a83f1
docs: 구현 목록 작성
reddevilmidzy Mar 13, 2024
2206919
feat: 배팅 금액이 양의 정수가 아닐 시 예외 발생 기능 구현
reddevilmidzy Mar 13, 2024
dec4970
test: player의 canDeal 테스트 추가
reddevilmidzy Mar 13, 2024
ffbc5e3
refactor: testFixture 상수 변경
reddevilmidzy Mar 13, 2024
ee8b66e
feat: 카드의 갯수가 2장이고 카드의 합이 21인 경우 블랙잭이다.
reddevilmidzy Mar 13, 2024
f37f288
fix: 참여자의 이름의 공백이 포함된 경우 제거하여 중복 판단 기능
reddevilmidzy Mar 13, 2024
285f322
feat: 참가자가 승패에 따라 배팅한 금액 잃거나 받는 기능 구현
reddevilmidzy Mar 13, 2024
197f942
feat: 각 참여자의 배팅 금액을 입력 받는 기능 구현
reddevilmidzy Mar 13, 2024
93e5b86
feat: 도메인 toString 재정의
reddevilmidzy Mar 14, 2024
8aba953
feat: 참가자만 블랙잭인 경우 배팅 금액의 1.5배를 받는 기능 구현
reddevilmidzy Mar 14, 2024
b8b1ef9
feat: 참가자와 딜러 모두 블랙잭인 경우 배팅한 금액 돌려 받는 기능 구현
reddevilmidzy Mar 14, 2024
d51ecdf
feat: 딜러의 최종 수익 계산 기능 구현
reddevilmidzy Mar 14, 2024
4a03db2
feat: 딜러와 참가자의 최종 수익 출력 기능 구현
reddevilmidzy Mar 14, 2024
874cd41
fix: test 코드 오류 수정
reddevilmidzy Mar 14, 2024
52a9b0f
refactor: 딜러가 블랙잭인 경우 출력 메서드 view 로 이동
reddevilmidzy Mar 14, 2024
f5c0b4d
fix: 무승부 조건 변경
reddevilmidzy Mar 14, 2024
87d8f09
refactor: 배팅 금액을 최종 결과 금액을 변경하는 로직 함수형 프로그래밍 이용
reddevilmidzy Mar 14, 2024
2988947
feat: 10으로 나누어 떨어지지 않으면 예외 발생 기능 구현
reddevilmidzy Mar 14, 2024
24e020d
refactor: Participant equals & hashCode 에서 Hands 제거
reddevilmidzy Mar 14, 2024
2fb529f
style: 코드 리포매팅
reddevilmidzy Mar 14, 2024
09ea27a
refactor: Player의 필드에 BetAmount 포함하는 형태로 구현
reddevilmidzy Mar 14, 2024
02f44e0
refactor: repository 삭제 및 테스트 코드 위치 변경
reddevilmidzy Mar 14, 2024
6b5c8e6
refactor: Player equals && hashCode 재정의
reddevilmidzy Mar 14, 2024
e166122
style: 테스트 코드 디스플레이네임 수정
reddevilmidzy Mar 14, 2024
e580c16
feat: 배팅 금액이 1억을 초과할 시 예외 발생 기능 구현
reddevilmidzy Mar 14, 2024
930c385
refactor: controller 변수 명 수정 및 메서드명 수정
reddevilmidzy Mar 14, 2024
c401e21
refactor: Result 이름 GameResult로 변경
reddevilmidzy Mar 14, 2024
e5cc892
refactor: Answer, Amount, BetAmount package 변경
reddevilmidzy Mar 14, 2024
f22ec42
refactor: 테스트 변수에 final 추가
reddevilmidzy Mar 15, 2024
353e923
refactor: Player 의 테스트에서 사용하는 생성자 접근제한자 디폴트로 변경
reddevilmidzy Mar 15, 2024
2fb5001
refactor: betAmount 필드 이름 변경
reddevilmidzy Mar 15, 2024
28f5b8d
refactor: 카드의 합이 21이 된 경우 출력 메시지 수정
reddevilmidzy Mar 15, 2024
b9f19af
refactor: 정적 팩터리 메서드 접근제한자 protected 에서 디폴트로 변경
reddevilmidzy Mar 15, 2024
c48d744
feat: 수익 출력시 세자리 마다 쉼표를 붙여 출력 기능 구현
reddevilmidzy Mar 15, 2024
8b9c0ca
refactor: players 등록 메서드로 분리
reddevilmidzy Mar 15, 2024
6ca77d6
refactor: 컨트롤러의 책임을 Game 객체를 만들어서 분리
reddevilmidzy Mar 15, 2024
4721bfa
style: 주석 제거
reddevilmidzy Mar 15, 2024
7f672e4
refactor: 불필요한 테스트 삭제
reddevilmidzy Mar 15, 2024
5aa83c3
docs: 구현 목록 수정
reddevilmidzy 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
117 changes: 117 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 블랙잭 규칙

## 참여자

* [x] 다시한번 룰 숙지 필요

**승**

* (딜러 카드 합 < 참여자 카드 합) && 참여자가 버스트가 아닌 경우
* 딜러가 버스트인 경우 && 참여자가 버스트가 아닌 경우

**무**

* (딜러 카드 합 == 참여자 카드 합) && 참여자가 버스트가 아닌 경우

**패**

* 참여자가 버스트인 경우
* (딜러 카드 합 > 참여자 카드 합)

<br>

## 블랙잭

* 카드의 갯수가 2장이고 카드의 합이 21인 경우 블랙잭이다.

<br>

# 기능 요구 사항

* [x] 참여자 이름 입력 받는다.
* [x] 양 끝 공백을 제거한다.
* [x] 참여자는 쉼표(,)로 구분한다.
* [x] null 이나 공백인 경우 예외가 발생한다.
* [x] 쉼표로 시작시 예외가 발생한다. ex) ,pobi,jason
* [x] 쉼표로 끝날시 예외가 발생한다. ex) pobi,jason,
* [x] 쉼표가 연속으로 올시 예외가 발생한다. ex) pobi,,jason
* [x] 참여자 이름이 중복시 예외가 발생한다.
* [x] 총 참여자의 수는 2이상 8이하여야 한다.

<br>

* [x] 각 참여자의 배팅 금액을 입력받는다.
* [x] 정수가 아닐 시 예외가 발생한다.
* [x] 양의 정수가 아닐 시 예외가 발생한다.
* [x] 10으로 나누어 떨어지지 않으면 예외가 발생한다.
* 블랙잭시 1.5배를 해주기 위함
* [x] 배팅 금액은 최대 100_000_000로 이를 초과시 예외가 발생한다.
* 건전한 플레이를 하기 위함

<br>

* [x] 딜러가 카드 2장을 분배하다.
* [x] 카드는 총 6벌을 둔다. (52 * 6)
* [x] 딜러의 카드를 출력한다. (1장)
* [x] 참여자의 카드를 출력한다. (2장)

<br>

* [x] 참여자는 hit(y) / stay(n)를 선택한다.
* [x] y, n 가 아닐시 예외가 발생한다.
* [x] 참여자가 hit을 하는 경우 현재 가지고 있는 카드를 출력한다.
* [x] 참여자가 hit을 한 적 없이 stay를 하는 경우 현재 가지고 있는 카드를 출력한다.
* [x] 카드 합이 블랙잭인 경우 블랙잭 메시지를 출력한다.
* [x] 카드 합이 21 초과시 버스트 메시지를 출력한다.

<br>

* [x] 딜러의 카드의 합을 계산한다.
* [x] 카드 내의 ACE 가 포함된 경우
* ACE: 11
* 11로 했을 때 카드의 합이 21을 초과한 경우 1로 계산
* [x] 17 이상이 될때까지 카드를 받는다.

<br>

* [x] 모든 참여자의 카드의 합을 계산한다.
* [x] 딜러의 승패무를 확인한다.
* [x] 참여자의 승패무를 확인한다.
* [x] 게임 결과를 출력한다.

<br>

* [x] 딜러의 최종 수익을 계산한다.
* [x] 수익을 출력시 세자리마다 쉼표를 찍는다. ex) 100,000
* [x] 참가자의 최종 수익을 계산한다.
* [x] 참가자만 블랙잭인 경우 배팅 금액의 1.5배를 받는다.
* [x] 참가자와 딜러 모두 블랙잭인 경우 배팅한 금액을 돌려받는다.
* [x] 참가자가 패한다면 배팅한 금액을 잃는다.
* [x] 참가자가 승리한다면 배팅한 금액을 받는다.

---

## 리팩터링 목록

~~추상 클래스 활용~~

* [x] 카드덱이 비었을때 꺼낸 경우 예외 발생
* [x] createEmptyPacket 메서드명 수정
* [x] 참가자의 이름 딜러 불가
* [x] Name 객체 포장하기
* [x] Result 함수형 프로그래밍 사용
* [x] TestFixture 사용
* [x] 패키지 정리
* [x] 메서드 컨벤션 작성 및 확인
* [x] final 컨벤션 통일
* [x] CardDeck 생성자 닫기

<br>

* [x] 예외 시 재입력 기능
* [x] 예외 메시지 상수화
* 커스텀 예외 구현
* [x] CardDeck 웨어하우스로 바라보기
* [ ] ~~컨트롤러 메서드 수정하기~~
* [x] ~~Participant 추상 클래스 대신 클래스로 변경하기~~
* 추상 클래스를 유지하고 추상 메서드 추가
18 changes: 18 additions & 0 deletions src/main/java/application/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package application;

import controller.BlackJackController;
import controller.InputController;
import java.util.Scanner;
import view.InputView;
import view.OutputView;

public class Application {
public static void main(String[] args) {
final Scanner scanner = new Scanner(System.in);
final InputView inputView = new InputView(scanner);
final OutputView outputView = new OutputView();
final InputController inputController = new InputController(inputView, outputView);
final BlackJackController blackJackController = new BlackJackController(inputController, outputView);
blackJackController.run();
}
}
15 changes: 15 additions & 0 deletions src/main/java/constants/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package constants;

public enum ErrorCode {

NOT_EXIST_MESSAGE,
INVALID_SEPARATOR,
INVALID_INPUT,
INVALID_SIZE,
INVALID_BET_AMOUNT,
DUPLICATE_NAME,
RESERVED_NAME,
BLANK_VALUE,
EMPTY_CARD,
INVALID_COMMAND,
}
76 changes: 76 additions & 0 deletions src/main/java/controller/BlackJackController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package controller;

import domain.Game;
import domain.amount.Amount;
import domain.amount.BetAmount;
import domain.participant.Dealer;
import domain.participant.Name;
import domain.participant.Names;
import domain.participant.Player;
import domain.participant.Players;
import dto.ParticipantsDto;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import view.OutputView;

public class BlackJackController {

private final InputController inputController;
private final OutputView outputView;

public BlackJackController(final InputController inputController, final OutputView outputView) {
this.inputController = inputController;
this.outputView = outputView;
}

public void run() {
final Names names = inputController.getNames();
final Dealer dealer = new Dealer();
final Players players = registerPlayers(names);

final Game game = new Game(dealer, players);

initHands(game);
dealWithPlayers(game);
dealerTurn(game);

printFinalResult(players, dealer);
}
Comment on lines +27 to +39
Copy link
Member

Choose a reason for hiding this comment

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

Controller란, 어떤 요청이 들어왔을 때 그 요청을 처리할 수 있는 model 에게 그 요청의 처리를 위임하고, 처리 결과를 View에 전달하는 것을 말합니다. 하지만, 우리 미션에서는 요청을 표현하는 것이 없습니다. 그저 프로그램을 실행하면 알아서 동작을 하기 때문에 Controller라는 이름은 부적절하다고 생각합니다!


private void dealerTurn(final Game game) {
game.dealerTurn(outputView::printDealerTurnMessage);
}

private Players registerPlayers(final Names names) {
final List<Player> playerList = new ArrayList<>();
for (final Name name : names.getPlayerNames()) {
final BetAmount betAmount = inputController.getBetAmount(name.getValue());
playerList.add(new Player(name, betAmount));
}
return new Players(playerList);
}

private void dealWithPlayers(final Game game) {
if (game.isAlreadyEnd()) {
outputView.printDealerBlackJack();
return;
}
game.checkingPlayersBlackJack(outputView::printBlackJack);
game.dealWithPlayers(
inputController::getAnswer,
outputView::printHands,
outputView::printPlayerEndMessage);
Comment on lines +60 to +63
Copy link
Member

Choose a reason for hiding this comment

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

game에 메서드를 넘겨주는군요
궁금한게 이렇게 해도 모델과 뷰가 분리되었다고 할 수 있는건가요??
mvc에 대해 잘 몰라서 물어봅니다!

}

private void initHands(final Game game) {
game.initHands(outputView::printStartDeal);
}

private void printFinalResult(final Players players, final Dealer dealer) {
outputView.printHandsResult(ParticipantsDto.of(dealer, players));
final Map<Player, Amount> finalResult = players.calculateResult(dealer);
final Amount dealerAmount = dealer.calculateRevenue(finalResult);
outputView.printGameResult(finalResult, dealerAmount);
}
}
74 changes: 74 additions & 0 deletions src/main/java/controller/InputController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package controller;

import domain.participant.Answer;
import domain.amount.BetAmount;
import domain.participant.Names;
import exception.CustomException;
import java.util.List;
import view.InputView;
import view.OutputView;

public class InputController {

private final InputView inputView;
private final OutputView outputView;
Comment on lines +11 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.

사실 MVC의 Controller의 역할이 무엇인지 생각해보면
해당 컨트롤러는 어떤 Model과 view 사이의 연결다리 역할을 해주는 것인지 의문이 드는데
클래스를 따로 두신 의도가 궁금합니다.


public InputController(final InputView inputView, final OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
}

public Names getNames() {
Names names;
do {
names = readNames();
} while (names == null);
return names;
}

public Answer getAnswer(String name) {
Answer answer;
do {
answer = readAnswer(name);
} while (answer == null);
return answer;
}

public BetAmount getBetAmount(String name) {
BetAmount betAmount;
do {
betAmount = readBetAmount(name);
} while (betAmount == null);
return betAmount;
}

private Names readNames() {
try {
List<String> rawNames = inputView.readNames();
return Names.from(rawNames);
} catch (CustomException exception) {
outputView.printException(exception.getErrorCode());
return null;
}
}

private Answer readAnswer(String name) {
try {
String value = inputView.readAnswer(name);
return Answer.from(value);
} catch (CustomException exception) {
outputView.printException(exception.getErrorCode());
return null;
}
}

private BetAmount readBetAmount(final String name) {
try {
Long value = inputView.readBetAmount(name);
return new BetAmount(value);
} catch (CustomException exception) {
outputView.printException(exception.getErrorCode());
return null;
}
}
}
94 changes: 94 additions & 0 deletions src/main/java/domain/Game.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package domain;

import domain.participant.Answer;
import domain.participant.Dealer;
import domain.participant.Participant;
import domain.participant.Player;
import domain.participant.Players;
import dto.DealerHandsDto;
import dto.ParticipantDto;
import dto.ParticipantsDto;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;

public class Game {

private final Dealer dealer;
private final Players players;

public Game(final Dealer dealer, final Players players) {
this.dealer = dealer;
this.players = players;
}

public void initHands(BiConsumer<DealerHandsDto, ParticipantsDto> handsPrinter) {
dealer.initHands(players);
handsPrinter.accept(DealerHandsDto.from(dealer), ParticipantsDto.of(players));
}
Comment on lines +25 to +28
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
Member

Choose a reason for hiding this comment

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

추가적으로, 자바에 기본으로 정의되어있는 함수형 인터페이스는 그 의도를 명확히 전달할 수 없습니다. 꼭 이렇게 하고 싶다면, 별도의 인터페이스를 정의하여 타입으로부터 의도를 설명하는 것이 좋을 것 같습니다.


public boolean isAlreadyEnd() {
return dealer.isBlackJack();
}

public void checkingPlayersBlackJack(Consumer<String> blackJackPrinter) {
players.filter(Participant::isBlackJack)
.forEach(player -> blackJackPrinter.accept(player.getName()));
}

public void dealWithPlayers(Function<String, Answer> answerReader,
Consumer<ParticipantDto> handsPrinter,
Consumer<Boolean> endMessagePrinter) {
players.forEach(player -> deal(player, answerReader,
handsPrinter,
endMessagePrinter
));
}

public void deal(final Player player, Function<String, Answer> answerReader,
Consumer<ParticipantDto> handsPrinter,
Consumer<Boolean> endMessagePrinter) {
if (player.isBlackJack()) {
return;
}
boolean handsChanged = false;
boolean turnEnded = false;

while (!turnEnded) {
final Answer answer = answerReader.apply(player.getName());
dealer.deal(player, answer);
printHandsIfRequired(player, handsChanged, answer, handsPrinter);
handsChanged = true;
turnEnded = isTurnEnded(player, answer, endMessagePrinter);
}
}

public void dealerTurn(Runnable dealerTurnMessagePrinter) {
if (players.isAllBust()) {
return;
}
dealer.deal();
for (int i = 0; i < dealer.countAddedHands(); i++) {
dealerTurnMessagePrinter.run();
}
}

private void printHandsIfRequired(final Player player, final boolean handsChanged, final Answer answer,
Consumer<ParticipantDto> handsPrinter) {
if (shouldShowHands(handsChanged, answer)) {
handsPrinter.accept(ParticipantDto.from(player));
}
}

private boolean isTurnEnded(final Player player, final Answer answer, Consumer<Boolean> endMessagePrinter) {
if (player.canDeal()) {
return !answer.isHit();
}
endMessagePrinter.accept(player.isBust());
return true;
}

private boolean shouldShowHands(final boolean handsChanged, final Answer answer) {
return answer.isHit() || !handsChanged;
}
}
Loading