diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1c4e3cef883..71bbfc612fb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,24 +12,6 @@ - [ ] 미션의 필수 요구사항을 모두 구현했나요? - [ ] Gradle `test`를 실행했을 때, 모든 테스트가 정상적으로 통과했나요? - [ ] 애플리케이션이 정상적으로 실행되나요? -- [ ] [프롤로그](https://prolog.techcourse.co.kr)에 셀프 체크를 작성했나요? - - - - -## 객체지향 생활체조 요구사항을 얼마나 잘 충족했다고 생각하시나요? - -### 1~5점 중에서 선택해주세요. - -- [ ] 1 (전혀 충족하지 못함) -- [ ] 2 -- [ ] 3 (보통) -- [ ] 4 -- [ ] 5 (완벽하게 충족) - -### 선택한 점수의 이유를 적어주세요. - - - ## 어떤 부분에 집중하여 리뷰해야 할까요? diff --git a/README.md b/README.md index 102a377493b..2bb212491fb 100644 --- a/README.md +++ b/README.md @@ -2,66 +2,302 @@ 블랙잭 미션 저장소 -## 구현할 기능 목록 +--- + +# 사이클1 구현 기능 + +## 1. 카드 덱 생성 및 셔플 -### 카드 덱 생성 + 셔플 - [x] 52장의 카드를 생성한다. -- [x] 카드는 무늬별 숫자순으로 생성된다. -- [x] 카드를 셔플한다. +- [x] 카드는 무늬별 숫자 순서로 생성한다. +- [x] 생성된 카드를 셔플한다. -### 플레이어/딜러 관리 -- [x] 플레이어를 플레이어 목록에 등록한다. -- [x] 딜러를 만든다. -- [x] 카드를 돌린다. +--- -### 카드 점수 계산 -- [x] J,Q,K는 10으로 처리한다. -- [x] Ace는 1점으로 처리한다. +## 2. 플레이어 및 딜러 관리 + +- [x] 플레이어를 생성한다. +- [x] 플레이어 목록에 등록한다. +- [x] 딜러를 생성한다. +- [x] 플레이어와 딜러에게 카드를 분배한다. + +--- + +## 3. 카드 점수 계산 + +- [x] 카드 숫자를 기본 점수로 사용한다. +- [x] Jack, Queen, King은 10점으로 처리한다. +- [x] Ace는 기본적으로 1점으로 처리한다. - [x] 카드 점수를 합산한다. -### Ace 1/11 처리 -- [x] 현재 합산 점수에 10점의 여유가 있으면 10점을 더한다. +--- + +## 4. Ace 점수 처리 (1 / 11) + +- [x] 현재 점수에서 10점을 추가해도 21을 넘지 않는 경우 Ace를 11로 계산한다. + +--- + +## 5. 버스트 판정 -### 버스트 판정 -- [x] 21점이 넘으면 버스트 +- [x] 카드 합이 21을 초과하면 버스트로 판정한다. + +--- -### 히트/스탠드 입력 -- [x] 버스트가 아니면 히트/스탠드를 입력받는다. -- [x] 히트면 카드를 한 장 더 받는다. -- [x] 스탠드면 턴을 종료한다. +## 6. 히트 / 스탠드 진행 + +- [x] 버스트가 아닌 경우 히트/스탠드 입력을 받는다. +- [x] 히트를 선택하면 카드를 한 장 더 받는다. +- [x] 스탠드를 선택하면 해당 플레이어의 턴을 종료한다. + +--- -### 승패 판정 -- [x] 딜러와 점수를 비교한다. -- [x] 결과를 생성한다. +## 7. 승패 판정 -### 결과 출력 +- [x] 딜러와 플레이어의 점수를 비교한다. +- [x] 게임 결과(승/무/패)를 생성한다. --- -### TO-DO -- [x] Enum 출력값 변경 - - [x] 승패 출력값 변경 - - [x] domain.card.Suit 출력값 변경 +## 8. 결과 출력 + +- [x] 플레이어 및 딜러의 카드와 점수를 출력한다. +- [x] 게임 결과를 출력한다. + +--- + +# 사이클1 리팩터링 및 개선 작업 + +## 출력 및 표현 개선 + +- [x] Enum 출력값 수정 + - [x] 승패 결과 출력값 변경 + - [x] `domain.card.Suit` 출력값 변경 +- [x] Ace, Jack, Queen, King이 숫자 값으로 출력되는 문제 수정 + +--- + +## 게임 로직 수정 + - [x] 딜러 승패 판정 오류 수정 -- [x] 버스트인데 hit/stand 질문 -- [x] Ace, Jack, Queen, King 이름이 값(1,10)으로 출력되는 문제 -- [x] domain.game.GameManager 리팩터링 -- [x] 패키지 정리 -- [x] drawDealerCard() 메서드의 응답값 미사용 -- [x] GameManager의 책임 재분배 - - [x] Deck 초기화, 셔플 책임 제거 - - [x] DTO 생성 책임 분리 - - [x] 승패 판정 책임 분리 -- [x] GameController 구조 수정 -- [x] 디미터의 법칙 - - [x] 스트림 체이닝은 줄바꿈해서 작성 - - [x] 체이닝 메서드 안생기도록 구조 수정 -- [x] rank 1 보다는 의미있는 값 (`ACE`)가 이해하기 쉽다 -- [x] 주석이 정말 필요한지, 이름이나 구조로 충분하지 않은지 -- [x] primitive 타입 wrapper로 감싸기 -- [x] 일급 컬렉션 -> Hand로 수정 - - [x] 핸드 기능 추가(스코어 계산, 상태값 판단, ACE 계산 로직) 및 기존 Calculator 제거 -- [x] mutable이어야 할 이유가 없다면, 반드시 final을 붙일 것 -- [x] 플레이어가 입력되지 않는 경우에 대한 예외처리 -- [x] 규칙 적용 +- [x] 버스트 상태에서도 hit/stand 질문이 나오는 문제 수정 + +--- + +## 구조 리팩터링 + +- [x] `GameManager` 리팩터링 +- [x] `GameController` 구조 수정 +- [x] 패키지 구조 정리 + +### GameManager 책임 재분배 + +- [x] Deck 초기화 및 셔플 책임 제거 +- [x] DTO 생성 책임 분리 +- [x] 승패 판정 책임 분리 + +--- + +## 코드 스타일 및 설계 개선 + +- [x] 디미터의 법칙 적용 + - [x] 스트림 체이닝 시 줄바꿈 적용 + - [x] 과도한 체이닝 메서드 제거 +- [x] 의미 있는 상수 사용 (`1` → `ACE`) +- [x] 불필요한 주석 제거 (이름/구조로 표현) +- [x] primitive 타입을 wrapper 객체로 감싸기 +- [x] mutable일 필요가 없는 경우 `final` 사용 + +--- + +## 도메인 구조 개선 + +- [x] 일급 컬렉션 적용 → `Hand` +- [x] `Hand`에 기능 통합 + - [x] 스코어 계산 + - [x] 상태 판단 + - [x] Ace 계산 로직 +- [x] 기존 `Calculator` 제거 + +--- + +## 예외 처리 및 테스트 + +- [x] 플레이어 입력이 없는 경우 예외 처리 +- [x] 게임 규칙 검증 로직 추가 - [x] 테스트 케이스 추가 + + +--- + +# 사이클2 기능 구현 + +## 1. 배팅 금액 관리 +- [X] 배팅 금액을 관리하는 도메인을 구현한다. + - [X] 처음 받은 배팅 금액을 저장 + - [X] 입력 받은 정산 시 결과 상태(승/무/패/블랙잭/버스트)에 따라 계산한 총 수익을 리턴 + - [X] 딜러를 플레이어의 상속을 받지 않고, 딜러와 플레이어 모두 추상화 시키는 Participants 생성 +## 2. 판정 추가 +- [X] 기존 `Result`에 블랙잭과 버스트를 추가 +- [X] 처음 BlackJack이 뜬 경우 블랙잭 상태로 결과에 저장 + - 기존에는 상태값을 저장해두지 않고 마지막에 핸드 결과로 최종 Dto 리스트를 만들어 승패판정을 하였으나, 수정된 상황에서 블랙잭을 미리 체크하고 히트/스탠드 및 승패 판정에서 제외해야 하므로 새로운 기능 추가 + - [X] 베팅금액과 결과를 함께 저장하도록 수정 + - [X] 초기 카드 드로우가 끝난 직후 블랙잭인 유저를 찾아서 결과 필드 갱신 +- [X] 결과에 따른 배당 수익을 상태 값으로 가지도록 + - 무승부는 원금 리턴 -> *1 + - 승은 원금 + 수익 -> * 2 + - 패, 버스트는 원금 손실 -> * -1 + - 처음부터 블랙잭인 경우 배팅의 1.5배 수익 -> * 2.5 +- [X] 플레이어 Hit/Stand 단계에서 BlackJack이 아닌 유저만 필터링하여 문답 진행 +- [X] 딜러 추가 드로우 단계에서 Bust가 되면 즉시 최종 결과 생성 단계로 이동 + - [X] 각 플레이어에 대해 최종 Result를 계산 + - [X] 플레이어가 bust면 BUST + - [X] bust가 아니면 딜러와 점수 비교 후 WIN, LOSE, DRAW 판정 + - [X] natural blackjack인 경우 블랙잭 배당 규칙을 반영할 수 있도록 별도 처리 + - [X] 계산된 Result 바탕으로 정산 금액 계산 + - [X] Result와 정산 금액을 포함한 DTO 생성 + +## 3. 배팅 금액 입력하여 플레이어 생성 +- [X] 플레이어 이름 입력 후 배팅 금액도 입력 +- [X] 플레이어와 금액을 매핑해서 플레이어 생성 +- [X] 플레이어의 배팅 금액을 다루는 메서드 구현 + +## 4. 베팅 결과 판정 및 출력 +- [X] 기존 딜러의 승,무,패 출력 기능 제거 +- [X] 플레이어들의 승, 무, 패(버스트), 블랙잭 기준으로 값을 계산하여 수익을 출력하도록 수정 +- [X] 딜러 기준 총 수익 계산 받아서 출력하는 기능 구현 + + +## 고민 +- Players를 copyOf로 해도 얕은 복사라 Player 자체에는 접근이 가능해진다. Player 자체의 변동 가능성은? + + +--- + +## PR 리뷰 후 추가 수정 사항 +- [X] 사용하지 않는 Result enum 필드 제거 +- [X] DTO의 intialHandSize 제거 +- [X] 동일한 카드가 플레이어에게 드로우되는 문제 해결 +- [X] Players 인스턴스 생성 및 추가 방식 수정 +- [X] splitPlayerNameInput 스트림 불변 리스트로 수정(toList) +- [X] 테스트에서만 쓰이는 handSize 메서드 제거 +- [X] GameResultJudge의 결과 분기 책임을 enum에게 넘기기 +~~- [ ] 컨트롤러가 매니저를 상태로 들고있는 구조 개선~~ +- [X] Result와 배당 수익금 비율 분리 +- [X] PlayerStatus에서 수익 계산 하는 부분 분리하기 +- [X] 기록 내용 README.md 추가 +- [X] updateNaturalBlackJack 상태 업데이트가 밖에서 호출되는 문제 수정 + + + + +## 📝 미션 기록 + +### 기능 추가로 인해 수정한 위치 + +이번 미션에서는 **베팅 기능을 추가한다고 가정하고 변경 범위를 분석**하였다. + +베팅 기능은 단순한 내부 로직이 아니라 +**입력 → 상태 저장 → 게임 진행 → 결과 계산 → 출력** 전체 흐름에 영향을 주는 기능이었다. + +그 결과 다음 위치에서 수정이 필요하다고 판단하였다. + +| 수정 위치 | 이유 | +|---|---| +| Player | 플레이어가 베팅 금액을 상태로 가져야 한다 | +| Players | 플레이어 집합에서 베팅 정보와 결과 집계가 필요 | +| GameManager | 게임 결과 계산 시 베팅 정산 로직 추가 | +| GameController | 게임 시작 전에 베팅 금액 입력 필요 | +| InputView | 플레이어별 베팅 금액 입력 | +| OutputView | 베팅 결과 및 수익 출력 | +| GameFinalResultDto | 결과 DTO에 수익 정보 추가 | +| Result | 승패에 따른 정산 기준 추가 | + +**총 수정 위치: 8개** + +이를 통해 **하나의 기능이 여러 계층을 따라 이동하면 수정 범위가 넓어진다**는 것을 확인했다. + +--- + +### 사이클 1 대비 수정 범위 + +사이클1에서는 게임 로직 대부분이 `GameManager`에 집중되어 있었다. + +``` +GameManager + ├ 카드 분배 + ├ 점수 계산 + ├ 게임 진행 + └ 승패 계산 +``` + +이 구조에서는 기능이 추가될 때 **GameManager에 수정이 집중되는 문제**가 있었다. + +이번 사이클에서는 다음과 같이 책임을 분리하였다. + +- `Hand` → 카드 점수 계산 +- `Players` → 플레이어 집합 관리 +- `GameResultJudge` → 승패 판정 +- `PlayerStatus` → 플레이어 상태 관리 + +그 결과 **기능 추가 시 수정 범위가 특정 객체에 집중되지 않고 분산되도록 개선되었다.** + +--- + +### 규칙 적용으로 변경한 코드 + +팀에서 다음과 같은 규칙을 정하였다. + +- 책임 분리 기준 + - **(If-Then)** 만약 특정 요구사항이 변경되었을 때, 그와 무관한 코드들까지 한 클래스 안에서 함께 수정하거나 테스트해야 한다면 → 변경의 원인이 하나가 되도록 책임을 분리하여 응집도를 높인다. + - **(기준)** 변경의 이유가 하나 이상일 때 분리한다. +- 변경 범위 제한 규칙 + - **(If-Then)** 만약 기능 추가 시 3곳 이상의 파일 수정이 필요하면 → 공통 로직을 추출하거나 책임을 재분배한다 + - **(기준)** 책임을 적절히 캡슐화하지 못해, 단일 변경의 파급 효과가 여러 객체(3곳 이상)로 흩어질 때 변경 범위를 제한한다. +- 테스트와 설계의 관계 규칙 + - **(If-Then)** 만약 비교와 같은 연산을 위해 해당 데이터를 가진 객체의 데이터를 직접 꺼내와서 사용하고 있다면 → 데이터를 가진 객체에게 직접 물어보도록(Tell, Don't Ask) 수정하여 변경 여파를 내부에 가둔다. + - **(기준)** 외부에서 객체의 데이터를 직접 꺼내어 연산이나 판단을 수행하는 경우, 데이터를 가진 객체에게 직접 물어보도록 변경한다. + +모든 규칙을 항상 의식하면서 작업하진 못하였지만, 대표적으로 아래의 규칙을 가장 의식하면서 작업했다. + +> **Tell, Don't Ask** +> +> 객체의 데이터를 꺼내와 외부에서 판단하지 말고 +> 해당 객체에게 직접 물어보도록 한다. + +이 규칙을 적용하면서, 기존에 도메인 객체의 필드 값을 외부에 호출하여 사용하던 방식에서 +도메인 객체 스스로 행동하여 상태 값을 활용한 책임을 지도록 하였고 외부에서는 내부 구현을 몰라도 +요청만으로 원하는 값을 얻을 수 있도록 하였다. + +이를 통해 응집도가 감소하고, 객체 간의 협력 관계를 기존보다 개선된 형태로 구축할 수 있었다고 생각한다. + +--- + +### 테스트가 설계를 도운 순간 + +딜러의 최종 수익을 계산하는 테스트를 작성하면서 +딜러 수익을 별도의 규칙으로 다시 계산하기보다 +**플레이어 수익의 합에 음수를 취한 값으로 정의하는 구조**가 더 자연스럽다는 점을 확인했다. + +```java +assertThat(firstPlayerResult.getProceeds()).isEqualTo(1000); +assertThat(secondPlayerResult.getProceeds()).isEqualTo(-2000); +assertThat(dealerResult.getProceeds()).isEqualTo(1000); +``` +이 테스트를 통해 +딜러와 플레이어의 정산 로직을 각각 따로 두기보다 +플레이어 결과를 먼저 계산한 뒤 딜러 결과를 합산으로 구하는 것이 +규칙 중복을 줄이고 설계를 더 단순하게 만든다는 점을 알 수 있었다. + +--- + +### 이번 미션에서 얻은 인사이트 + +기능 추가 자체보다 더 크게 느낀 점은 다음이다. + +- 역할이 명확한 객체 (`Card`, `Hand`, `Deck`) 는 변화에 강하다. +- 여러 책임이 섞인 객체 (`GameManager`) 는 변화에 약하다. +- 테스트를 작성하면서 설계에서 불안정한 지점이 어디인지 파악할 수 있다. + +이번 미션을 통해 구조에 대한 설계가 잘못 되었는지 알아챌 수 있는 신호에는 무엇이 있고, +어떤 설계가 유연하면서도 안정적인 코드를 작성할 수 있는지 알 수 있었다. diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java index cfaa67630ea..290449bc838 100644 --- a/src/main/java/controller/GameController.java +++ b/src/main/java/controller/GameController.java @@ -1,8 +1,9 @@ package controller; +import dto.GameInitialInfoDto; import domain.game.GameManager; import domain.participant.Player; -import domain.dto.GameInitialInfoDto; +import java.util.ArrayList; import view.InputView; import view.OutputView; @@ -22,7 +23,6 @@ public GameController(GameManager manager, InputView inputView, OutputView outpu public void run() { registerPlayer(); - // TODO: 베팅 기능 추가 시 플레이어 등록 후 베팅 입력 단계가 추가 필요 initGame(); playPlayerTurn(); playDealerTurn(); @@ -31,23 +31,27 @@ public void run() { } private void registerPlayer() { - // TODO: 베팅 기능 추가 시 이름 입력 후 각 플레이어의 베팅 금액도 함께 등록 while (true) { try { List playerNames = inputView.readPlayerName(); validatePlayerNames(playerNames); - - for (String playerName : playerNames) { - manager.addPlayer(playerName); - } + registerPlayersWithBettingMoney(playerNames); return; - } catch (IllegalArgumentException e) { outputView.printError(e.getMessage()); } } } + private void registerPlayersWithBettingMoney(List playerNames) { + List bettingMoneyList = new ArrayList<>(); + for (String playerName : playerNames) { + int bettingMoney = inputView.readBettingMoney(playerName); + bettingMoneyList.add(bettingMoney); + } + manager.registerPlayers(playerNames, bettingMoneyList); + } + private void initGame() { manager.startGame(); @@ -56,15 +60,24 @@ private void initGame() { } private void playPlayerTurn() { - for (Player player : manager.getPlayerSequence()) { + for (Player player : manager.getPlayersToPlay()) { playSinglePlayerTurn(player); } } private void playSinglePlayerTurn(Player player) { - while (player.canDraw() && wantsToDraw(player)) { - List playerHand = manager.drawPlayerCard(player); + while (player.canDraw()) { + boolean wantsToDraw = wantsToDraw(player); + + List playerHand = player.getHandToString(); + if(wantsToDraw){ + playerHand = manager.drawPlayerCard(player); + } outputView.printHand(playerHand, player.getName()); + + if(!wantsToDraw) { + break; + } } } @@ -87,4 +100,4 @@ private void validatePlayerNames(List playerNames) { throw new IllegalArgumentException("플레이어 이름은 비어 있을 수 없습니다."); } } -} +} \ No newline at end of file diff --git a/src/main/java/domain/DtoFactory.java b/src/main/java/domain/DtoFactory.java index 78fefee90af..9094f3ec9d7 100644 --- a/src/main/java/domain/DtoFactory.java +++ b/src/main/java/domain/DtoFactory.java @@ -1,7 +1,7 @@ package domain; -import domain.dto.GameInitialInfoDto; -import domain.dto.GameScoreResultDto; +import dto.GameInitialInfoDto; +import dto.GameScoreResultDto; import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; @@ -37,7 +37,7 @@ private static void addDealerScoreResult(List results, Deale } private static void addPlayerScoreResults(List results, Players players) { - for (Player player : players.getPlayers()) { + for (Player player : players.getNonNaturalBlackJackPlayers()) { results.add(new GameScoreResultDto( player.getName(), player.getHandToString(), @@ -49,16 +49,14 @@ private static void addPlayerScoreResults(List results, Play private static void addDealerInitialInfo(List results, Dealer dealer) { results.add(new GameInitialInfoDto( dealer.getName(), - INITIAL_HAND_SIZE, List.of(dealer.getOpenCard()) )); } private static void addPlayerInitialInfos(List results, Players players) { - for (Player player : players.getPlayers()) { + for (Player player : players.getNonNaturalBlackJackPlayers()) { results.add(new GameInitialInfoDto( player.getName(), - INITIAL_HAND_SIZE, player.getHandToString() )); } diff --git a/src/main/java/domain/Hand.java b/src/main/java/domain/Hand.java index 046aad2727e..5c2849bfa0d 100644 --- a/src/main/java/domain/Hand.java +++ b/src/main/java/domain/Hand.java @@ -18,10 +18,6 @@ public void add(Card card) { hand.add(card); } - public List getHand() { - return List.copyOf(hand); - } - public List toStringList() { return hand.stream() .map(Card::toString) diff --git a/src/main/java/domain/PlayerStatus.java b/src/main/java/domain/PlayerStatus.java new file mode 100644 index 00000000000..0b950a74535 --- /dev/null +++ b/src/main/java/domain/PlayerStatus.java @@ -0,0 +1,25 @@ +package domain; + +import domain.constant.Result; +import domain.game.ProceedsCalculator; + +public class PlayerStatus { + private final int bettingMoney; + private boolean naturalBlackJack; + + public PlayerStatus(int bettingMoney) { + this.bettingMoney = bettingMoney; + } + + public void markNaturalBlackJack() { + naturalBlackJack = true; + } + + public boolean isNaturalBlackJack() { + return naturalBlackJack; + } + + public double calculateProceeds(Result result) { + return ProceedsCalculator.calculate(bettingMoney, result); + } +} \ No newline at end of file diff --git a/src/main/java/domain/card/Deck.java b/src/main/java/domain/card/Deck.java index eace46ab946..0dde97bdff6 100644 --- a/src/main/java/domain/card/Deck.java +++ b/src/main/java/domain/card/Deck.java @@ -5,7 +5,7 @@ import java.util.List; public class Deck { - List cards = new ArrayList<>(); + final List cards = new ArrayList<>(); public Deck () { init(); diff --git a/src/main/java/domain/constant/Result.java b/src/main/java/domain/constant/Result.java index 7f4455cfb9e..e71c79319f1 100644 --- a/src/main/java/domain/constant/Result.java +++ b/src/main/java/domain/constant/Result.java @@ -1,15 +1,24 @@ package domain.constant; +import domain.participant.Dealer; +import domain.participant.Player; + public enum Result { - WIN("승"), DRAW("무"), LOSE("패"); + BUST, + BLACKJACK, + WIN, + LOSE, + DRAW; - private String name; + public static Result from(Player player, Dealer dealer) { + int playerScore = player.getScore(); + int dealerScore = dealer.getScore(); - Result(String name) { - this.name = name; - } + if (player.isBust()) return BUST; + if (player.isNaturalBlackJack()) return BLACKJACK; + if (playerScore > dealerScore) return WIN; + if (playerScore < dealerScore) return LOSE; - public String getName() { - return name + " "; + return DRAW; } -} +} \ No newline at end of file diff --git a/src/main/java/domain/dto/GameFinalResultDto.java b/src/main/java/domain/dto/GameFinalResultDto.java deleted file mode 100644 index 1ea7ff21350..00000000000 --- a/src/main/java/domain/dto/GameFinalResultDto.java +++ /dev/null @@ -1,35 +0,0 @@ -package domain.dto; - -import domain.constant.Result; - -public class GameFinalResultDto { - String playerName; - Result result; - // TODO: 베팅 기능 추가 시 베팅 금액, 정산 금액(수익/손실) 필드 추가 필요 - - - public GameFinalResultDto(String playerName) { - this(playerName, null); - } - - public GameFinalResultDto(String playerName, Result result) { - this.playerName = playerName; - this.result = result; - } - - public String getPlayerName() { - return playerName; - } - - public Result getResult() { - return result; - } - - @Override - public String toString() { - return "domain.dto.GameFinalResultDto{" + - "name='" + playerName + '\'' + - ", result=" + result + - '}'; - } -} diff --git a/src/main/java/domain/game/GameManager.java b/src/main/java/domain/game/GameManager.java index 47cd52c71ec..ec890f18518 100644 --- a/src/main/java/domain/game/GameManager.java +++ b/src/main/java/domain/game/GameManager.java @@ -2,31 +2,30 @@ import domain.DtoFactory; import domain.card.Deck; -import domain.dto.GameFinalResultDto; -import domain.dto.GameInitialInfoDto; -import domain.dto.GameScoreResultDto; - +import dto.GameResultDto; +import dto.GameInitialInfoDto; +import dto.GameScoreResultDto; import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; +import java.util.ArrayList; import java.util.List; public class GameManager { - private final int FIRST_DRAW_CARDS = 2; + private static final int FIRST_DRAW_CARDS = 2; private final Deck deck; - private final Players players; + private Players players; private final Dealer dealer; public GameManager(Deck deck) { this.deck = deck; - this.players = new Players(); this.dealer = new Dealer(); } public void startGame() { for (int i = 0; i < FIRST_DRAW_CARDS; i++) { - players.receiveCard(deck.draw()); + players.receiveOneCardFrom(deck); dealer.receiveCard(deck.draw()); } } @@ -36,13 +35,18 @@ public List drawPlayerCard(Player player) { return player.getHandToString(); } - public void addPlayer(String name) { - // TODO: 베팅 기능 추가 시 이름만이 아니라 베팅 금액도 함께 받도록 수정 필요 - players.add(new Player(name)); + public void registerPlayers(List playerNames, List bettingMoneyList) { + List players = new ArrayList<>(); + + for (int i = 0; i < playerNames.size(); i++) { + players.add(new Player(playerNames.get(i), bettingMoneyList.get(i))); + } + + this.players = Players.of(players); } - public List getPlayerSequence() { - return players.getPlayers(); + public List getPlayersToPlay() { + return players.getNonNaturalBlackJackPlayers(); } public List getScoreResults() { @@ -53,7 +57,6 @@ public List getInitialInfo() { return DtoFactory.toInitialInfo(dealer, players); } - public boolean proceedDealerTurn() { if (!dealer.canDraw()) { return false; @@ -63,8 +66,7 @@ public boolean proceedDealerTurn() { return true; } - public List getFinalResult() { + public List getFinalResult() { return GameResultJudge.judge(dealer, players); } - -} +} \ No newline at end of file diff --git a/src/main/java/domain/game/GameResultJudge.java b/src/main/java/domain/game/GameResultJudge.java index 0cba57ba2fc..986d07cff36 100644 --- a/src/main/java/domain/game/GameResultJudge.java +++ b/src/main/java/domain/game/GameResultJudge.java @@ -1,7 +1,7 @@ package domain.game; import domain.constant.Result; -import domain.dto.GameFinalResultDto; +import dto.GameResultDto; import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; @@ -12,45 +12,32 @@ public class GameResultJudge { private GameResultJudge() { } - public static List judge(Dealer dealer, Players players) { - // TODO: 베팅 기능 추가 시 승/패/무 뿐 아니라 정산 금액까지 포함한 결과 생성 필요 - List results = new ArrayList<>(); - results.add(new GameFinalResultDto(dealer.getName())); + public static List judge(Dealer dealer, Players players) { + List results = new ArrayList<>(); + addPlayerResults(results, dealer, players); + + double dealerProceeds = calculateDealerProceeds(results); + results.add(0, new GameResultDto(dealer.getName(), dealerProceeds)); + return results; } - private static void addPlayerResults(List results, Dealer dealer, Players players) { + private static void addPlayerResults(List results, Dealer dealer, Players players) { for (Player player : players.getPlayers()) { results.add(judgePlayer(player, dealer)); } } - private static GameFinalResultDto judgePlayer(Player player, Dealer dealer) { - Result result = calculateResult(player, dealer); - return new GameFinalResultDto(player.getName(), result); + private static GameResultDto judgePlayer(Player player, Dealer dealer) { + Result result = Result.from(player, dealer); + double proceeds = player.calculateProceeds(result); + return new GameResultDto(player.getName(), result, proceeds); } - private static Result calculateResult(Player player, Dealer dealer) { - int playerScore = player.getScore(); - int dealerScore = dealer.getScore(); - - if (player.isBust()) { - return Result.LOSE; - } - - if (dealer.isBust()) { - return Result.WIN; - } - - if (playerScore > dealerScore) { - return Result.WIN; - } - - if (playerScore < dealerScore) { - return Result.LOSE; - } - - return Result.DRAW; + private static double calculateDealerProceeds(List results) { + return -results.stream() + .mapToDouble(GameResultDto::getProceeds) + .sum(); } -} +} \ No newline at end of file diff --git a/src/main/java/domain/game/ProceedsCalculator.java b/src/main/java/domain/game/ProceedsCalculator.java new file mode 100644 index 00000000000..5a50d9ed479 --- /dev/null +++ b/src/main/java/domain/game/ProceedsCalculator.java @@ -0,0 +1,27 @@ +package domain.game; + +import domain.constant.Result; + +public class ProceedsCalculator { + + private static final double BLACKJACK_PROCEEDS_RATE = 1.5; + private static final double WIN_PROCEEDS_RATE = 1.0; + private static final double DRAW_PROCEEDS_RATE = 0.0; + private static final double LOSE_PROCEEDS_RATE = -1.0; + + private ProceedsCalculator() { + } + + public static double calculate(int bettingMoney, Result result) { + if (result == Result.BLACKJACK) { + return bettingMoney * BLACKJACK_PROCEEDS_RATE; + } + if (result == Result.WIN) { + return bettingMoney * WIN_PROCEEDS_RATE; + } + if (result == Result.DRAW) { + return bettingMoney * DRAW_PROCEEDS_RATE; + } + return bettingMoney * LOSE_PROCEEDS_RATE; + } +} \ No newline at end of file diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java index 27936afc5ce..fdfbce5c3c6 100644 --- a/src/main/java/domain/participant/Dealer.java +++ b/src/main/java/domain/participant/Dealer.java @@ -1,18 +1,17 @@ package domain.participant; -public class Dealer extends Player { - private final int DEALER_DRAW_CONDITION = 16; +public class Dealer extends Participant { + private static final int DEALER_DRAW_CONDITION = 16; public Dealer() { super("딜러"); } - @Override - public boolean canDraw(){ - return this.getScore() <= DEALER_DRAW_CONDITION; + public boolean canDraw() { + return getScore() <= DEALER_DRAW_CONDITION; } public String getOpenCard() { return getHandToString().getFirst(); } -} +} \ No newline at end of file diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java new file mode 100644 index 00000000000..a0a538d39a6 --- /dev/null +++ b/src/main/java/domain/participant/Participant.java @@ -0,0 +1,44 @@ +package domain.participant; + +import domain.Hand; +import domain.card.Card; +import java.util.List; + +public abstract class Participant { + private final String name; + private final Hand hand = new Hand(); + + protected Participant(String name) { + this.name = name; + } + + protected boolean hasTwoCards() { + return hand.size() == 2; + } + + public boolean isBust() { + return hand.isBust(); + } + + public boolean isBlackJack() { + return hand.isBlackjack(); + } + + public void receiveCard(Card card) { + hand.add(card); + } + + public List getHandToString() { + return hand.toStringList(); + } + + public int getScore() { + return hand.calculateScore(); + } + + public String getName() { + return name; + } + + public abstract boolean canDraw(); +} \ No newline at end of file diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java index b3cbe731eb6..6f94691adf4 100644 --- a/src/main/java/domain/participant/Player.java +++ b/src/main/java/domain/participant/Player.java @@ -1,49 +1,46 @@ package domain.participant; +import domain.PlayerStatus; import domain.card.Card; -import domain.Hand; -import java.util.List; +import domain.constant.Result; -public class Player { - private final String name; - private final Hand hand = new Hand(); - // TODO: 베팅 기능 추가 시 베팅 금액 필드 또는 일급 객체 필요 +public class Player extends Participant { + private final PlayerStatus status; - - public Player(String name) { - this.name = name; - // TODO: 베팅 기능 추가 시 생성자에서 베팅 금액도 함께 받아야 할 수 있음 - } - - public boolean isBust() { - return hand.isBust(); + public Player(String name, int bettingMoney) { + super(name); + this.status = new PlayerStatus(bettingMoney); } + @Override public void receiveCard(Card card) { - hand.add(card); + super.receiveCard(card); + updateNaturalBlackJackStatus(); } - public boolean canDraw() { - return !(isBust()|| hand.isBlackjack()); + private void updateNaturalBlackJackStatus() { + if (isInitialBlackJack()) { + status.markNaturalBlackJack(); + } } - public int handSize() { - return hand.size(); + private boolean isInitialBlackJack() { + return hasTwoCards() && isBlackJack(); } - public List getHandToString() { - return hand.toStringList(); + public boolean isNaturalBlackJack() { + return status.isNaturalBlackJack(); } - public int getScore(){ - return hand.calculateScore(); + public void markNaturalBlackJack() { + status.markNaturalBlackJack(); } - public String getName() { - return name; + public double calculateProceeds(Result result) { + return status.calculateProceeds(result); } - public Hand getHand() { - return hand; + public boolean canDraw() { + return !(isBust() || isNaturalBlackJack()); } -} +} \ No newline at end of file diff --git a/src/main/java/domain/participant/Players.java b/src/main/java/domain/participant/Players.java index b26264b8ac8..7dd9a8d0008 100644 --- a/src/main/java/domain/participant/Players.java +++ b/src/main/java/domain/participant/Players.java @@ -1,21 +1,31 @@ package domain.participant; -import domain.card.Card; +import domain.card.Deck; import java.util.ArrayList; import java.util.List; public class Players { - private final List players = new ArrayList<>(); + private final List players; - public void add(Player player) { - players.add(player); + private Players(final List players) { + this.players = new ArrayList<>(players); } - public void receiveCard(Card card) { - players.forEach(player -> player.receiveCard(card)); + public static Players of(List playersList) { + return new Players(playersList); + } + + public void receiveOneCardFrom(Deck deck) { + players.forEach(player -> player.receiveCard(deck.draw())); } public List getPlayers() { return List.copyOf(players); } + + public List getNonNaturalBlackJackPlayers() { + return players.stream() + .filter(player -> !player.isNaturalBlackJack()) + .toList(); + } } diff --git a/src/main/java/domain/dto/GameInitialInfoDto.java b/src/main/java/dto/GameInitialInfoDto.java similarity index 55% rename from src/main/java/domain/dto/GameInitialInfoDto.java rename to src/main/java/dto/GameInitialInfoDto.java index ecef57c2caa..5877b90425a 100644 --- a/src/main/java/domain/dto/GameInitialInfoDto.java +++ b/src/main/java/dto/GameInitialInfoDto.java @@ -1,16 +1,14 @@ -package domain.dto; +package dto; import java.util.List; public class GameInitialInfoDto { private String playerName; - private int initialHandSize; private List hand; - public GameInitialInfoDto(String playerName, int initialHandSize, List hand) { + public GameInitialInfoDto(String playerName, List hand) { this.playerName = playerName; - this.initialHandSize = initialHandSize; this.hand = hand; } @@ -18,10 +16,6 @@ public String getPlayerName() { return playerName; } - public int getInitialHandSize() { - return initialHandSize; - } - public List getHand() { return hand; } diff --git a/src/main/java/dto/GameResultDto.java b/src/main/java/dto/GameResultDto.java new file mode 100644 index 00000000000..ac5bc03e30d --- /dev/null +++ b/src/main/java/dto/GameResultDto.java @@ -0,0 +1,40 @@ +package dto; + +import domain.constant.Result; + +public class GameResultDto { + private final String playerName; + private final Result result; + private final double proceeds; + + public GameResultDto(String playerName, double proceeds) { + this(playerName, null, proceeds); + } + + public GameResultDto(String playerName, Result result, double proceeds) { + this.playerName = playerName; + this.result = result; + this.proceeds = proceeds; + } + + public String getPlayerName() { + return playerName; + } + + public Result getResult() { + return result; + } + + public double getProceeds() { + return proceeds; + } + + @Override + public String toString() { + return "GameResultDto{" + + "playerName='" + playerName + '\'' + + ", result=" + result + + ", proceeds=" + proceeds + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/domain/dto/GameScoreResultDto.java b/src/main/java/dto/GameScoreResultDto.java similarity index 67% rename from src/main/java/domain/dto/GameScoreResultDto.java rename to src/main/java/dto/GameScoreResultDto.java index 3cc84efa21b..460f742396e 100644 --- a/src/main/java/domain/dto/GameScoreResultDto.java +++ b/src/main/java/dto/GameScoreResultDto.java @@ -1,11 +1,11 @@ -package domain.dto; +package dto; import java.util.List; public class GameScoreResultDto { - String playerName; - List hand; - int result; + final String playerName; + final List hand; + final int result; public GameScoreResultDto(String playerName, List hand, int result) { this.playerName = playerName; @@ -17,26 +17,14 @@ public String getPlayerName() { return playerName; } - public void setPlayerName(String playerName) { - this.playerName = playerName; - } - public List getHand() { return hand; } - public void setHand(List hand) { - this.hand = hand; - } - public int getResult() { return result; } - public void setResult(int result) { - this.result = result; - } - @Override public String toString() { return "domain.dto.GameScoreResultDto{" + diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java index 043fe6eb3a9..01894d37b07 100644 --- a/src/main/java/view/InputView.java +++ b/src/main/java/view/InputView.java @@ -10,10 +10,13 @@ public class InputView { private static final String COMMAND_REINPUT_MESSAGE = "y 혹은 n만 입력할 수 있습니다."; private static final String COMMAND_INPUT_MESSAGE = "는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"; private static final String PLAYER_NAMES_INPUT_MESSAGE = "게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"; + private static final String BETTING_MONEY_INPUT_MESSAGE = "의 배팅 금액은?"; + private static final String BETTING_MONEY_REINPUT_MESSAGE = "배팅 금액은 0보다 큰 숫자만 입력할 수 있습니다."; private static final List CORRECT_COMMAND_INPUT = List.of("y", "n"); + private final Scanner scanner = new Scanner(System.in); + public List readPlayerName() { - Scanner scanner = new Scanner(System.in); System.out.println(PLAYER_NAMES_INPUT_MESSAGE); String input = scanner.nextLine(); System.out.println(); @@ -21,27 +24,42 @@ public List readPlayerName() { return splitPlayerNameInput(input); } - private static List splitPlayerNameInput(String s) { - if (s == null || s.trim().isEmpty()) { - return new ArrayList<>(); - } + public int readBettingMoney(String playerName) { + while (true) { + try { + System.out.println(playerName + BETTING_MONEY_INPUT_MESSAGE); + int bettingMoney = Integer.parseInt(scanner.nextLine()); - return Arrays.stream(s.split(",")) - .map(String::trim) // split된 항목 앞 뒤 공백 제거 - .filter(c -> !c.isEmpty()) // 콤마가 두번 겹치는 경우 필터링 - .collect(Collectors.toList()); + if (bettingMoney <= 0) { + throw new IllegalArgumentException(); + } + System.out.println(); + return bettingMoney; + } catch (IllegalArgumentException e) { + System.out.println(BETTING_MONEY_REINPUT_MESSAGE); + } + } } public String readCommand(String playerName) { - Scanner sc = new Scanner(System.in); while (true) { System.out.println(playerName + COMMAND_INPUT_MESSAGE); - String input = sc.nextLine(); + String input = scanner.nextLine(); if (CORRECT_COMMAND_INPUT.contains(input)) { return input; } System.out.println(COMMAND_REINPUT_MESSAGE); } } - // TODO: 베팅 기능 추가 시 플레이어별 베팅 금액 입력 메서드 필요 -} + + private static List splitPlayerNameInput(String s) { + if (s == null || s.trim().isEmpty()) { + return new ArrayList<>(); + } + + return Arrays.stream(s.split(",")) + .map(String::trim) + .filter(c -> !c.isEmpty()) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java index c5d584511ce..dd7cb0dadbb 100644 --- a/src/main/java/view/OutputView.java +++ b/src/main/java/view/OutputView.java @@ -1,23 +1,21 @@ package view; -import domain.constant.Result; -import domain.dto.GameFinalResultDto; -import domain.dto.GameInitialInfoDto; -import domain.dto.GameScoreResultDto; +import dto.GameResultDto; +import dto.GameInitialInfoDto; +import dto.GameScoreResultDto; import java.text.MessageFormat; -import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; public class OutputView { private static final String DEAL_MESSAGE = "딜러와 {0}에게 {1}장을 나누었습니다."; private static final String SHOW_HAND_MESSAGE = "{0}카드: {1}"; - private static final String FINAL_RESULT_MESSAGE = "{0}: {1}"; + private static final String SHOW_DEALER_HAND_MESSAGE = "{0} 카드: {1}"; + private static final String FINAL_PROCEEDS_MESSAGE = "{0}: {1}"; private static final String DEALER_DRAW_MESSAGE = "딜러는 16이하라 한장의 카드를 더 받았습니다."; private static final String ERROR_PREFIX = "[ERROR] "; - + private static final int INITIAL_HAND_SIZE = 2; public void printInitialInfo(List initialInfo) { printHandOutNotice(initialInfo); @@ -26,7 +24,7 @@ public void printInitialInfo(List initialInfo) { public void printHand(List hand, String name) { System.out.println(MessageFormat.format( - SHOW_HAND_MESSAGE, + getHandMessageFormat(name), name, String.join(", ", hand) )); @@ -41,85 +39,65 @@ public void printScoreResults(List scoreResults) { System.out.println(); for (GameScoreResultDto scoreResult : scoreResults) { System.out.println(MessageFormat.format( - SHOW_HAND_MESSAGE, + getHandMessageFormat(scoreResult.getPlayerName()), scoreResult.getPlayerName(), - String.join(", ", scoreResult.getHand()) - + " - 결과: " + scoreResult.getResult() + String.join(", ", scoreResult.getHand()) + " - 결과: " + scoreResult.getResult() )); } System.out.println(); } - public void printFinalResult(List finalResult) { - // TODO: 베팅 기능 추가 시 정산 금액도 출력하도록 형식 수정 필요 - System.out.println("## 최종 승패"); - GameFinalResultDto firstPlayer = finalResult.removeFirst(); - Map resultCounts = new EnumMap<>(Result.class); - - countDealerResult(finalResult, resultCounts); - printDealerResult(firstPlayer, resultCounts); - printPlayerResult(finalResult); - } - - private void countDealerResult(List finalResult, Map resultCounts) { - for (GameFinalResultDto result : finalResult) { - if (result.getResult() == Result.WIN) { - resultCounts.put(Result.LOSE, resultCounts.getOrDefault(Result.LOSE, 0) + 1); - continue; - } - - if (result.getResult() == Result.LOSE) { - resultCounts.put(Result.WIN, resultCounts.getOrDefault(Result.WIN, 0) + 1); - continue; - } + public void printFinalResult(List finalResult) { + System.out.println("## 최종 수익"); - resultCounts.put(result.getResult(), resultCounts.getOrDefault(result.getResult(), 0) + 1); - } - } - - private void printDealerResult(GameFinalResultDto firstPlayer, Map resultCounts) { - StringBuilder sb = new StringBuilder(); - sb.append(firstPlayer.getPlayerName()).append(": "); - for (Result result : resultCounts.keySet()) { - sb.append(resultCounts.get(result)).append(result.getName()); - } - System.out.println(sb); - } - - private void printPlayerResult(List finalResult) { - // TODO: 베팅 기능 추가 시 플레이어별 베팅 금액,수익 출력 필요 - for (GameFinalResultDto result : finalResult) { + for (GameResultDto result : finalResult) { System.out.println(MessageFormat.format( - FINAL_RESULT_MESSAGE, + FINAL_PROCEEDS_MESSAGE, result.getPlayerName(), - result.getResult().getName() + formatProceeds(result.getProceeds()) )); } } private void printHandOutNotice(List initialInfo) { String playerNames = initialInfo.stream() - .skip(1) // 0번은 딜러 + .skip(1) .map(GameInitialInfoDto::getPlayerName) .collect(Collectors.joining(", ")); System.out.println(MessageFormat.format( DEAL_MESSAGE, playerNames, - initialInfo.getFirst().getInitialHandSize() + 2 )); } + private void printInitialHands(List initialInfo) { for (GameInitialInfoDto info : initialInfo) { System.out.println(MessageFormat.format( - SHOW_HAND_MESSAGE, + getHandMessageFormat(info.getPlayerName()), info.getPlayerName(), String.join(", ", info.getHand()) )); } System.out.println(); } + public void printError(String message) { System.out.println(ERROR_PREFIX + message); } -} + + private String getHandMessageFormat(String name) { + if ("딜러".equals(name)) { + return SHOW_DEALER_HAND_MESSAGE; + } + return SHOW_HAND_MESSAGE; + } + + private String formatProceeds(double proceeds) { + if (proceeds == (long) proceeds) { + return String.valueOf((long) proceeds); + } + return String.valueOf(proceeds); + } +} \ No newline at end of file diff --git a/src/test/java/domain/DtoFactoryTest.java b/src/test/java/domain/DtoFactoryTest.java index 6ee8cfb6375..019e91fe0f6 100644 --- a/src/test/java/domain/DtoFactoryTest.java +++ b/src/test/java/domain/DtoFactoryTest.java @@ -5,8 +5,8 @@ import domain.card.Card; import domain.card.Rank; import domain.card.Suit; -import domain.dto.GameInitialInfoDto; -import domain.dto.GameScoreResultDto; +import dto.GameInitialInfoDto; +import dto.GameScoreResultDto; import domain.participant.Dealer; import domain.participant.Player; import domain.participant.Players; @@ -21,11 +21,11 @@ public class DtoFactoryTest { dealer.receiveCard(new Card(Rank.ACE, Suit.SPADE)); dealer.receiveCard(new Card(Rank.KING, Suit.HEART)); - Players players = new Players(); - Player player = new Player("pobi"); + Player player = new Player("pobi", 1000); player.receiveCard(new Card(Rank.TWO, Suit.CLUB)); player.receiveCard(new Card(Rank.THREE, Suit.DIAMOND)); - players.add(player); + + Players players = Players.of(List.of(player)); List result = DtoFactory.toInitialInfo(dealer, players); @@ -40,11 +40,11 @@ public class DtoFactoryTest { dealer.receiveCard(new Card(Rank.ACE, Suit.SPADE)); dealer.receiveCard(new Card(Rank.KING, Suit.HEART)); - Players players = new Players(); - Player player = new Player("pobi"); + Player player = new Player("pobi", 1000); player.receiveCard(new Card(Rank.TWO, Suit.CLUB)); player.receiveCard(new Card(Rank.THREE, Suit.DIAMOND)); - players.add(player); + + Players players = Players.of(List.of(player)); List result = DtoFactory.toInitialInfo(dealer, players); @@ -53,16 +53,16 @@ public class DtoFactoryTest { } @Test - void 점수결과를_생성하면_딜러와_플레이어의_이름_승패_정보를_포함한다() { + void 점수결과를_생성하면_딜러와_플레이어의_이름과_점수_정보를_포함한다() { Dealer dealer = new Dealer(); dealer.receiveCard(new Card(Rank.TEN, Suit.SPADE)); dealer.receiveCard(new Card(Rank.SEVEN, Suit.HEART)); - Players players = new Players(); - Player player = new Player("pobi"); + Player player = new Player("pobi", 1000); player.receiveCard(new Card(Rank.NINE, Suit.CLUB)); player.receiveCard(new Card(Rank.EIGHT, Suit.DIAMOND)); - players.add(player); + + Players players = Players.of(List.of(player)); List result = DtoFactory.toScoreResults(dealer, players); @@ -75,4 +75,4 @@ public class DtoFactoryTest { assertThat(result.get(1).getHand()).hasSize(2); assertThat(result.get(1).getResult()).isEqualTo(17); } -} +} \ No newline at end of file diff --git a/src/test/java/domain/GameManagerTest.java b/src/test/java/domain/GameManagerTest.java index 776118a109b..ef5c2fd6cac 100644 --- a/src/test/java/domain/GameManagerTest.java +++ b/src/test/java/domain/GameManagerTest.java @@ -1,9 +1,9 @@ package domain; import domain.card.Deck; -import domain.dto.GameFinalResultDto; -import domain.dto.GameInitialInfoDto; -import domain.dto.GameScoreResultDto; +import dto.GameInitialInfoDto; +import dto.GameResultDto; +import dto.GameScoreResultDto; import domain.game.GameManager; import domain.participant.Player; import org.junit.jupiter.api.Test; @@ -18,8 +18,10 @@ class GameManagerTest { void 등록된_플레이어와_딜러_순서대로_카드를_돌린다() { GameManager manager = new GameManager(new Deck()); - manager.addPlayer("pobi"); - manager.addPlayer("cary"); + manager.registerPlayers( + List.of("pobi", "cary"), + List.of(1000, 1000) + ); manager.startGame(); List scoreResults = manager.getScoreResults(); @@ -36,7 +38,10 @@ class GameManagerTest { void 딜러의_카드는_한_장만_공개한다() { GameManager manager = new GameManager(new Deck()); - manager.addPlayer("pobi"); + manager.registerPlayers( + List.of("pobi"), + List.of(1000) + ); manager.startGame(); List initialInfo = manager.getInitialInfo(); @@ -48,7 +53,10 @@ class GameManagerTest { void 플레이어의_카드는_두_장_공개한다() { GameManager manager = new GameManager(new Deck()); - manager.addPlayer("pobi"); + manager.registerPlayers( + List.of("pobi"), + List.of(1000) + ); manager.startGame(); List initialInfo = manager.getInitialInfo(); @@ -59,42 +67,49 @@ class GameManagerTest { @Test void 플레이어를_한명_등록한다() { GameManager manager = new GameManager(new Deck()); - manager.addPlayer("pobi"); - List result = manager.getPlayerSequence(); + manager.registerPlayers( + List.of("pobi"), + List.of(1000) + ); - assertThat(result.size()).isEqualTo(1); + List result = manager.getPlayersToPlay(); + + assertThat(result).hasSize(1); assertThat(result.getFirst().getName()).isEqualTo("pobi"); } @Test void 플레이어를_세명_등록한다() { GameManager manager = new GameManager(new Deck()); - List playerNames = List.of("pobi", "cary", "rudy"); - for (String playerName : playerNames) { - manager.addPlayer(playerName); - } + manager.registerPlayers( + List.of("pobi", "cary", "rudy"), + List.of(1000, 1000, 1000) + ); - List result = manager.getPlayerSequence(); + List result = manager.getPlayersToPlay(); - assertThat(result.size()).isEqualTo(3); + assertThat(result).hasSize(3); } - @Test void 플레이어가_카드를_한장_더_받는다() { GameManager manager = new GameManager(new Deck()); - manager.addPlayer("pobi"); + + manager.registerPlayers( + List.of("pobi"), + List.of(1000) + ); manager.startGame(); - Player player = manager.getPlayerSequence().getFirst(); + Player player = manager.getPlayersToPlay().getFirst(); - int before = player.handSize(); + int before = player.getHandToString().size(); manager.drawPlayerCard(player); - int after = player.handSize(); + int after = player.getHandToString().size(); assertThat(after).isEqualTo(before + 1); } @@ -111,10 +126,13 @@ class GameManagerTest { @Test void 게임_최종_결과에는_딜러와_모든_플레이어_결과가_포함된다() { GameManager manager = new GameManager(new Deck()); - manager.addPlayer("pobi"); - manager.addPlayer("cary"); - List result = manager.getFinalResult(); + manager.registerPlayers( + List.of("pobi", "cary"), + List.of(1000, 1000) + ); + + List result = manager.getFinalResult(); assertThat(result).hasSize(3); assertThat(result.get(0).getPlayerName()).isEqualTo("딜러"); @@ -126,6 +144,7 @@ class GameManagerTest { void 플레이어가_없는_경우_초기정보에는_딜러만_포함된다() { GameManager manager = new GameManager(new Deck()); + manager.registerPlayers(List.of(), List.of()); manager.startGame(); List result = manager.getInitialInfo(); @@ -138,6 +157,7 @@ class GameManagerTest { void 플레이어가_없는_경우_점수결과에는_딜러만_포함된다() { GameManager manager = new GameManager(new Deck()); + manager.registerPlayers(List.of(), List.of()); manager.startGame(); List result = manager.getScoreResults(); @@ -150,12 +170,12 @@ class GameManagerTest { void 플레이어가_없는_경우_최종결과에는_딜러만_포함된다() { GameManager manager = new GameManager(new Deck()); + manager.registerPlayers(List.of(), List.of()); manager.startGame(); - List result = manager.getFinalResult(); + List result = manager.getFinalResult(); assertThat(result).hasSize(1); assertThat(result.getFirst().getPlayerName()).isEqualTo("딜러"); } - } \ No newline at end of file diff --git a/src/test/java/domain/GameResultJudgeTest.java b/src/test/java/domain/GameResultJudgeTest.java index f7dd192e57b..066365afecc 100644 --- a/src/test/java/domain/GameResultJudgeTest.java +++ b/src/test/java/domain/GameResultJudgeTest.java @@ -2,9 +2,9 @@ import domain.card.Card; import domain.card.Rank; -import domain.constant.Result; import domain.card.Suit; -import domain.dto.GameFinalResultDto; +import domain.constant.Result; +import dto.GameResultDto; import domain.game.GameResultJudge; import domain.participant.Dealer; import domain.participant.Player; @@ -15,110 +15,142 @@ import static org.assertj.core.api.Assertions.assertThat; -class GameResultJudgeTest { +public class GameResultJudgeTest { @Test - void 플레이어가_딜러보다_점수가_높으면_WIN이다() { + void 플레이어가_bust면_BUST와_음수_수익을_가진다() { Dealer dealer = new Dealer(); + Player player = new Player("pobi", 1000); + dealer.receiveCard(new Card(Rank.TEN, Suit.SPADE)); - dealer.receiveCard(new Card(Rank.SEVEN, Suit.HEART)); + dealer.receiveCard(new Card(Rank.SEVEN, Suit.HEART)); // 17 - Player player = new Player("pobi"); - player.receiveCard(new Card(Rank.TEN, Suit.CLUB)); - player.receiveCard(new Card(Rank.NINE, Suit.DIAMOND)); + player.receiveCard(new Card(Rank.KING, Suit.CLUB)); + player.receiveCard(new Card(Rank.QUEEN, Suit.DIAMOND)); + player.receiveCard(new Card(Rank.TWO, Suit.HEART)); // 22 bust + + Players players = Players.of(List.of(player)); - Players players = new Players(); - players.add(player); + List results = GameResultJudge.judge(dealer, players); - List result = GameResultJudge.judge(dealer, players); + GameResultDto dealerResult = results.get(0); + GameResultDto playerResult = results.get(1); - assertThat(result).hasSize(2); - assertThat(result.get(1).getPlayerName()).isEqualTo("pobi"); - assertThat(result.get(1).getResult()).isEqualTo(Result.WIN); + assertThat(playerResult.getPlayerName()).isEqualTo("pobi"); + assertThat(playerResult.getResult()).isEqualTo(Result.BUST); + assertThat(playerResult.getProceeds()).isEqualTo(-1000); + + assertThat(dealerResult.getPlayerName()).isEqualTo("딜러"); + assertThat(dealerResult.getProceeds()).isEqualTo(1000); } @Test - void 플레이어가_딜러보다_점수가_낮으면_LOSE이다() { + void 플레이어_점수가_더_높으면_WIN이다() { Dealer dealer = new Dealer(); + Player player = new Player("pobi", 1000); + dealer.receiveCard(new Card(Rank.TEN, Suit.SPADE)); - dealer.receiveCard(new Card(Rank.NINE, Suit.HEART)); + dealer.receiveCard(new Card(Rank.SEVEN, Suit.HEART)); // 17 - Player player = new Player("pobi"); player.receiveCard(new Card(Rank.TEN, Suit.CLUB)); - player.receiveCard(new Card(Rank.SEVEN, Suit.DIAMOND)); + player.receiveCard(new Card(Rank.EIGHT, Suit.DIAMOND)); // 18 - Players players = new Players(); - players.add(player); + Players players = Players.of(List.of(player)); - List result = GameResultJudge.judge(dealer, players); + List results = GameResultJudge.judge(dealer, players); + GameResultDto playerResult = results.get(1); - assertThat(result.get(1).getResult()).isEqualTo(Result.LOSE); + assertThat(playerResult.getResult()).isEqualTo(Result.WIN); + assertThat(playerResult.getProceeds()).isEqualTo(1000); } @Test - void 플레이어와_딜러의_점수가_같으면_DRAW이다() { + void 플레이어_점수가_더_낮으면_LOSE다() { Dealer dealer = new Dealer(); + Player player = new Player("pobi", 1000); + dealer.receiveCard(new Card(Rank.TEN, Suit.SPADE)); - dealer.receiveCard(new Card(Rank.EIGHT, Suit.HEART)); + dealer.receiveCard(new Card(Rank.NINE, Suit.HEART)); // 19 - Player player = new Player("pobi"); - player.receiveCard(new Card(Rank.NINE, Suit.CLUB)); - player.receiveCard(new Card(Rank.NINE, Suit.DIAMOND)); + player.receiveCard(new Card(Rank.TEN, Suit.CLUB)); + player.receiveCard(new Card(Rank.EIGHT, Suit.DIAMOND)); // 18 - Players players = new Players(); - players.add(player); + Players players = Players.of(List.of(player)); - List result = GameResultJudge.judge(dealer, players); + List results = GameResultJudge.judge(dealer, players); + GameResultDto playerResult = results.get(1); - assertThat(result.get(1).getResult()).isEqualTo(Result.DRAW); + assertThat(playerResult.getResult()).isEqualTo(Result.LOSE); + assertThat(playerResult.getProceeds()).isEqualTo(-1000); } @Test - void 플레이어가_버스트면_딜러와_상관없이_LOSE이다() { + void 플레이어와_딜러의_점수가_같으면_DRAW다() { Dealer dealer = new Dealer(); + Player player = new Player("pobi", 1000); + dealer.receiveCard(new Card(Rank.TEN, Suit.SPADE)); - dealer.receiveCard(new Card(Rank.SEVEN, Suit.HEART)); + dealer.receiveCard(new Card(Rank.EIGHT, Suit.HEART)); // 18 - Player player = new Player("pobi"); - player.receiveCard(new Card(Rank.KING, Suit.CLUB)); - player.receiveCard(new Card(Rank.QUEEN, Suit.DIAMOND)); - player.receiveCard(new Card(Rank.TWO, Suit.HEART)); + player.receiveCard(new Card(Rank.NINE, Suit.CLUB)); + player.receiveCard(new Card(Rank.NINE, Suit.DIAMOND)); // 18 - Players players = new Players(); - players.add(player); + Players players = Players.of(List.of(player)); - List result = GameResultJudge.judge(dealer, players); + List results = GameResultJudge.judge(dealer, players); + GameResultDto playerResult = results.get(1); - assertThat(result.get(1).getResult()).isEqualTo(Result.LOSE); + assertThat(playerResult.getResult()).isEqualTo(Result.DRAW); + assertThat(playerResult.getProceeds()).isEqualTo(0); } @Test - void 딜러가_버스트면_플레이어가_버스트가_아닌_한_WIN이다() { + void naturalBlackJack인_플레이어는_BLACKJACK과_블랙잭_배당을_가진다() { Dealer dealer = new Dealer(); - dealer.receiveCard(new Card(Rank.KING, Suit.SPADE)); - dealer.receiveCard(new Card(Rank.QUEEN, Suit.HEART)); - dealer.receiveCard(new Card(Rank.TWO, Suit.CLUB)); + Player player = new Player("pobi", 10000); - Player player = new Player("pobi"); - player.receiveCard(new Card(Rank.TEN, Suit.DIAMOND)); - player.receiveCard(new Card(Rank.SEVEN, Suit.HEART)); + dealer.receiveCard(new Card(Rank.TEN, Suit.SPADE)); + dealer.receiveCard(new Card(Rank.NINE, Suit.HEART)); // 19 + + player.receiveCard(new Card(Rank.ACE, Suit.CLUB)); + player.receiveCard(new Card(Rank.KING, Suit.DIAMOND)); // natural blackjack + player.markNaturalBlackJack(); - Players players = new Players(); - players.add(player); + Players players = Players.of(List.of(player)); - List result = GameResultJudge.judge(dealer, players); + List results = GameResultJudge.judge(dealer, players); + GameResultDto playerResult = results.get(1); - assertThat(result.get(1).getResult()).isEqualTo(Result.WIN); + assertThat(playerResult.getResult()).isEqualTo(Result.BLACKJACK); + assertThat(playerResult.getProceeds()).isEqualTo(15000); } @Test - void 최종결과_첫번째에는_딜러가_포함된다() { + void 딜러의_총수익은_플레이어_수익의_합에_음수를_취한_값이다() { Dealer dealer = new Dealer(); - Players players = new Players(); - players.add(new Player("pobi")); - List result = GameResultJudge.judge(dealer, players); + Player winPlayer = new Player("pobi", 1000); + Player losePlayer = new Player("jason", 2000); + + dealer.receiveCard(new Card(Rank.TEN, Suit.SPADE)); + dealer.receiveCard(new Card(Rank.EIGHT, Suit.HEART)); // 18 + + winPlayer.receiveCard(new Card(Rank.TEN, Suit.CLUB)); + winPlayer.receiveCard(new Card(Rank.NINE, Suit.DIAMOND)); // 19 -> WIN + + losePlayer.receiveCard(new Card(Rank.TEN, Suit.HEART)); + losePlayer.receiveCard(new Card(Rank.SEVEN, Suit.CLUB)); // 17 -> LOSE + + Players players = Players.of(List.of(winPlayer, losePlayer)); + + List results = GameResultJudge.judge(dealer, players); + + GameResultDto dealerResult = results.get(0); + GameResultDto firstPlayerResult = results.get(1); + GameResultDto secondPlayerResult = results.get(2); - assertThat(result.getFirst().getPlayerName()).isEqualTo("딜러"); + assertThat(firstPlayerResult.getProceeds()).isEqualTo(1000); + assertThat(secondPlayerResult.getProceeds()).isEqualTo(-2000); + assertThat(dealerResult.getProceeds()).isEqualTo(1000); } } \ No newline at end of file diff --git a/src/test/java/domain/PlayerStatusTest.java b/src/test/java/domain/PlayerStatusTest.java new file mode 100644 index 00000000000..c1fb9a41487 --- /dev/null +++ b/src/test/java/domain/PlayerStatusTest.java @@ -0,0 +1,36 @@ +package domain; + +import domain.constant.Result; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PlayerStatusTest { + + @Test + void naturalBlackJack_상태를_true로_변경한다() { + PlayerStatus status = new PlayerStatus(1000); + + status.markNaturalBlackJack(); + + assertThat(status.isNaturalBlackJack()).isTrue(); + } + + @ParameterizedTest + @CsvSource({ + "10000, WIN, 10000", + "10000, DRAW, 0", + "10000, LOSE, -10000", + "10000, BUST, -10000", + "10000, BLACKJACK, 15000" + }) + void 결과값에_따라_정산_금액을_계산한다(int bettingMoney, Result result, double expected) { + PlayerStatus status = new PlayerStatus(bettingMoney); + + double proceeds = status.calculateProceeds(result); + + assertThat(proceeds).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/domain/PlayerTest.java b/src/test/java/domain/PlayerTest.java new file mode 100644 index 00000000000..b4908deab93 --- /dev/null +++ b/src/test/java/domain/PlayerTest.java @@ -0,0 +1,34 @@ +package domain; + +import domain.card.Card; +import domain.card.Rank; +import domain.card.Suit; +import domain.participant.Player; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PlayerTest { + + @Test + void naturalBlackJack_상태인_플레이어는_카드를_더_뽑을_수_없다() { + Player player = new Player("pobi", 1000); + + player.receiveCard(new Card(Rank.ACE, Suit.SPADE)); + player.receiveCard(new Card(Rank.KING, Suit.HEART)); + player.markNaturalBlackJack(); + + assertThat(player.canDraw()).isFalse(); + } + + @Test + void bust인_플레이어는_카드를_더_뽑을_수_없다() { + Player player = new Player("pobi", 1000); + + player.receiveCard(new Card(Rank.KING, Suit.SPADE)); + player.receiveCard(new Card(Rank.QUEEN, Suit.HEART)); + player.receiveCard(new Card(Rank.TWO, Suit.CLUB)); + + assertThat(player.canDraw()).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/domain/PlayersTest.java b/src/test/java/domain/PlayersTest.java index 9b5a673dbc0..77c7de3fdf9 100644 --- a/src/test/java/domain/PlayersTest.java +++ b/src/test/java/domain/PlayersTest.java @@ -1,5 +1,8 @@ package domain; +import domain.card.Card; +import domain.card.Rank; +import domain.card.Suit; import domain.participant.Player; import domain.participant.Players; import org.junit.jupiter.api.Test; @@ -12,12 +15,49 @@ public class PlayersTest { @Test void 플레이어를_등록한다() { - Players players = new Players(); - players.add(new Player("pobi")); - players.add(new Player("abc")); + Players players = Players.of(List.of( + new Player("pobi", 1000), + new Player("abc", 1000) + )); List records = players.getPlayers(); - assertThat(records).anyMatch(player -> player.getName().equals("abc")); + assertThat(records).hasSize(2); + assertThat(records).extracting(Player::getName) + .containsExactly("pobi", "abc"); } -} + + @Test + void 초기_블랙잭인_플레이어는_naturalBlackJack_상태가_true가_된다() { + Player blackJackPlayer = new Player("pobi", 1000); + Player normalPlayer = new Player("jason", 1000); + + blackJackPlayer.receiveCard(new Card(Rank.ACE, Suit.SPADE)); + blackJackPlayer.receiveCard(new Card(Rank.KING, Suit.HEART)); + + normalPlayer.receiveCard(new Card(Rank.TWO, Suit.CLUB)); + normalPlayer.receiveCard(new Card(Rank.THREE, Suit.DIAMOND)); + + assertThat(blackJackPlayer.isNaturalBlackJack()).isTrue(); + assertThat(normalPlayer.isNaturalBlackJack()).isFalse(); + } + + @Test + void 초기_블랙잭이_아닌_플레이어들만_반환한다() { + Player blackJackPlayer = new Player("pobi", 1000); + Player normalPlayer = new Player("jason", 1000); + + blackJackPlayer.receiveCard(new Card(Rank.ACE, Suit.SPADE)); + blackJackPlayer.receiveCard(new Card(Rank.KING, Suit.HEART)); + + normalPlayer.receiveCard(new Card(Rank.TWO, Suit.CLUB)); + normalPlayer.receiveCard(new Card(Rank.THREE, Suit.DIAMOND)); + + Players players = Players.of(List.of(blackJackPlayer, normalPlayer)); + + List result = players.getNonNaturalBlackJackPlayers(); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getName()).isEqualTo("jason"); + } +} \ No newline at end of file