diff --git a/README.md b/README.md index c8ad0d5eca2..9ffdf274951 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,59 @@ # java-blackjack 블랙잭 미션 저장소 -## 기능 요구사항 +## 구현할 기능 목록 ---- -블랙잭 게임을 변형한 프로그램을 구현한다. 블랙잭 게임은 **딜러**와 **플레이어** 중 카드의 합이 **21** 또는 **21에 가장 가까운 숫자**를 가지는 쪽이 이기는 게임이다. - -카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. -게임을 시작하면 플레이어는 **두 장의 카드**를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. -21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. -딜러는 처음에 받은 2장의 합계가 **16이하**이면 반드시 1장의 카드를 추가로 받아야 하고, **17점 이상**이면 추가로 받을 수 없다. -게임을 완료한 후 각 플레이어별로 승패를 출력한다. - -### Card -#### field -- CardNumber(Enum class) -- CardPattern(Enum class) -#### method -- 숫자 검증 -- 카드 패턴 검증 ---- -### Hand(1급 컬렉션) -#### field -- List Card -- 카드 주인 -#### method -- Append Card (카드 추가) -- 전체 값(필드로 두지는 않고, 계산으로 반환) 계산 calculate -- 문양과 숫자가 중복되는지 검사 -- Ace 값을 결정? ---- -### Member -#### field -- Hand -- Role(player | dealer) - -#### method -- receive -- roleIsDealer 딜러인지 확인 후 카드 뽑을지 -- decideWinner(Member(역할이 dealer)) ---- -### GameTable -#### field -- Members(일급 컬렉션) +### 1. 카드 도메인 고도화 +- [x] **카드 캐싱(Flyweight Pattern)**: 52장의 카드를 미리 생성하고 재사용한다. +- [x] **셔플 전략 주입**: 테스트 가능하도록 셔플 로직을 인터페이스로 분리한다. -#### method -- join : player 추가 -- CardResult playRound() -- List draw(memberName, Card) +### 2. 베팅 도메인 추가 +- [x] **금액(Money) 객체**: 베팅 금액 원시 값을 포장하고, 수익률에 따른 계산 로직을 가진다. +- [x] **플레이어별 베팅**: 게임 시작 시 플레이어별로 베팅 금액을 입력받는다. -### Service -- holdingPlayerCards() -``` -딜러카드: 3다이아몬드, 9클로버, 8다이아몬드 - 결과: 20 -pobi카드: 2하트, 8스페이드, A클로버 - 결과: 21 -jason카드: 7클로버, K스페이드 - 결과: 17 -``` -- finalResult() -``` - ## 최종 승패 - 딜러: 1승 1패 - pobi: 승 - jason: 패 - ``` +### 3. 상태(State) 패턴 기반 로직 리팩토링 +- [x] **상태 추상화**: `Started`, `Running`, `Finished` 등 게임의 상태를 객체로 정의한다. +- [x] **상태 전이 구현**: + - `Hit` 상태에서 `draw` 호출 시 점수에 따라 `Hit` 혹은 `Bust`로 전이한다. + - `Hit` 상태에서 `stay` 호출 시 `Stay` 상태로 전이한다. +- [x] **수익률(Earnings Rate) 캡슐화**: + - `Blackjack`: 1.5배 수익률 반환 + - `Bust`: -1.0배 수익률 반환 + - `Stay`: 딜러와 비교 결과에 따라 수익률 반환 -### Controller -- inputView로 입력받는 멤버 추가 (join 호출) -- GameTable에서 멤버 리스트를 받아와서 멤버별 draw의사 판별 반복 +### 4. 게임 진행 및 정산 +- [x] **수익 계산**: 게임 종료 후 플레이어의 상태에 따른 최종 수익을 계산한다. +- [x] **딜러 최종 수익**: 모든 플레이어 수익의 합에 -1을 곱한 값을 도출한다. ---- -### Deck -#### method -- draw -- Card Generator 52 queue에 초기화 +### 5. UI 및 출력 +- [x] 각 플레이어의 베팅 금액 입력 기능 +- [x] 최종 결과 시 플레이어별/딜러 수익 합계 출력 + + +## 기능 요구사항 +블랙잭 게임을 변형한 프로그램을 구현한다. 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다. -## 실행 결과 +- 플레이어는 게임을 시작할 때 배팅 금액을 정해야 한다. +- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다. +- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. 단, 카드를 추가로 뽑아 21을 초과할 경우 배팅 금액을 모두 잃게 된다. +- 처음 두 장의 카드 합이 21일 경우 블랙잭이 되면 베팅 금액의 1.5 배를 딜러에게 받는다. 딜러와 플레이어가 모두 동시에 블랙잭인 경우 플레이어는 베팅한 금액을 돌려받는다. +- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리해 베팅 금액을 받는다. + +### 실행 결과 --- ``` 게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리) pobi,jason +pobi의 배팅 금액은? +10000 + +jason의 배팅 금액은? +20000 + 딜러와 pobi, jason에게 2장을 나누었습니다. -딜러카드: 3다이아몬드 +딜러: 3다이아몬드 pobi카드: 2하트, 8스페이드 jason카드: 7클로버, K스페이드 @@ -91,66 +62,67 @@ y pobi카드: 2하트, 8스페이드, A클로버 pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) n -jason는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) +pobi카드: 2하트, 8스페이드, A클로버 +jason은 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n) n jason카드: 7클로버, K스페이드 딜러는 16이하라 한장의 카드를 더 받았습니다. -딜러카드: 3다이아몬드, 9클로버, 8다이아몬드 - 결과: 20 +딜러 카드: 3다이아몬드, 9클로버, 8다이아몬드 - 결과: 20 pobi카드: 2하트, 8스페이드, A클로버 - 결과: 21 jason카드: 7클로버, K스페이드 - 결과: 17 -## 최종 승패 -딜러: 1승 1패 -pobi: 승 -jason: 패 +## 최종 수익 +딜러: 10000 +pobi: 10000 +jason: -20000 ``` ## 프로그래밍 요구 사항 --- - 자바 코드 컨벤션을 지키면서 프로그래밍한다. -- 기본적으로 Java Style Guide을 원칙으로 한다. -- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. -힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. + - 기본적으로 Java Style Guide을 원칙으로 한다. +- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. + - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. + - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. - 3항 연산자를 쓰지 않는다. -- else 예약어를 쓰지 않는다. else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. -힌트: if문에서 값을 반환하는 방식으로 구현하면 else 예약어를 사용하지 않아도 된다. +- else 예약어를 쓰지 않는다. + - else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. + - 힌트: if문에서 값을 반환하는 방식으로 구현하면 else 예약어를 사용하지 않아도 된다. - 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외 -- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. -- UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. + - 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. + - UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. - 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다. -- 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라. + - 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라. - 배열 대신 컬렉션을 사용한다. - 모든 원시 값과 문자열을 포장한다. - 줄여 쓰지 않는다(축약 금지). - 일급 컬렉션을 쓴다. - -## 추가된 요구 사항 - ---- - 모든 엔티티를 작게 유지한다. - 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다. - 딜러와 플레이어에서 발생하는 중복 코드를 제거해야 한다. -## 미션 중 할 일 - ---- -토론 활동에서 정한 규칙을 의식하며 코드 작성 -규칙 때문에 코드를 변경한 곳 기록 -막히는 순간 기록 ## 미션 중 기록 --- 필수 기록: -[ ] 규칙을 적용해서 변경한 코드 1곳 이상 -[ ] 테스트 작성이 어려웠던 코드 1곳 이상 -[ ] 막힌 순간 1회 이상 +- [x] 기능 추가로 인해 수정한 위치 개수 +```markdown +- 약 5곳 (State 클래스군, Members, Player, GameTable, BlackjackService) +- 상태 패턴 도입 후, 새로운 규칙(수익률) 추가 시 기존 클래스를 수정하지 않고 + 새로운 State 구현체만 만지면 되어 수정 범위가 제한됨. +``` +- [ ] 사이클1 때보다 수정 범위가 줄었는가/늘었는가 +```markdown -## 미션 완료 조건 +``` +- [ ] 규칙 적용으로 변경한 코드 1곳 +```markdown ---- -[ ] 요구사항 구현 -[ ] 규칙에 의한 코드 변경 1회 이상 -[ ] 미션 중 기록 작성 +``` +- [ ] 테스트가 설계를 도운 순간 1회 +```markdown + +``` \ No newline at end of file diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 159a4f427c0..694458d3bea 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,5 +1,5 @@ -import application.BlackjackService; import presentation.BlackjackController; +import presentation.ui.BlackjackView; import presentation.ui.InputView; import presentation.ui.OutputView; @@ -8,8 +8,7 @@ public static void main(String[] args) { InputView inputView = new InputView(); OutputView outputView = new OutputView(); - BlackjackService blackjackService = new BlackjackService(); - BlackjackController blackjackController = new BlackjackController(blackjackService, inputView, outputView); + BlackjackController blackjackController = new BlackjackController(new BlackjackView(inputView, outputView)); blackjackController.executeGame(); } diff --git a/src/main/java/application/BlackjackService.java b/src/main/java/application/BlackjackService.java index 154b0955154..3367ba86786 100644 --- a/src/main/java/application/BlackjackService.java +++ b/src/main/java/application/BlackjackService.java @@ -1,42 +1,59 @@ package application; -import domain.Card; +import domain.card.Card; import domain.GameTable; -import application.dto.RoundResult; -import domain.StandardDeck; -import domain.dto.GameResult; -import domain.dto.MemberStatus; +import domain.member.Money; +import dto.RoundResult; +import domain.card.StandardDeck; +import dto.GameResult; +import dto.MemberStatus; + +import java.util.HashMap; import java.util.List; +import java.util.Map; public class BlackjackService { - private GameTable gameTable; + private final GameTable gameTable; - public BlackjackService() { + public BlackjackService(Map playerBetAmounts) { + Map playerBets = new HashMap<>(); + for (String name : playerBetAmounts.keySet()) { + Money betMoney = new Money(playerBetAmounts.get(name)); + playerBets.put(name, betMoney); + } + this.gameTable = new GameTable(playerBets, new StandardDeck()); + gameTable.distributeInitCard(); } - public void initializeGame(List playerNames) { - this.gameTable = new GameTable(playerNames, new StandardDeck()); - gameTable.distributeInitCard(); + public boolean isFinishedByName(String playerName) { + return gameTable.isPlayerFinished(playerName); } public RoundResult startOneRound(String memberName) { List playerCards = gameTable.drawForMember(memberName); - boolean isBust = gameTable.checkBust(memberName); + boolean isBust = gameTable.isPlayerBust(memberName); return new RoundResult(playerCards, isBust); } + public void endPlayerRound(String playerName) { + gameTable.changePlayerState(playerName); + } + public boolean checkDealerDrawable() { return gameTable.drawForDealer(); } public List getMemberStatuses() { - return gameTable.checkMemberStatuses(); + return gameTable.getMemberStatuses(); } public List getGameResults() { - return gameTable.checkGameResult(); + Map profits = gameTable.getFinalProfits(); + return profits.entrySet().stream() + .map(entry -> new GameResult(entry.getKey(), entry.getValue())) + .toList(); } } diff --git a/src/main/java/domain/exception/DuplicatedException.java b/src/main/java/constant/exception/DuplicatedException.java similarity index 84% rename from src/main/java/domain/exception/DuplicatedException.java rename to src/main/java/constant/exception/DuplicatedException.java index c863ff2a2ae..dc705910e83 100644 --- a/src/main/java/domain/exception/DuplicatedException.java +++ b/src/main/java/constant/exception/DuplicatedException.java @@ -1,4 +1,4 @@ -package domain.exception; +package constant.exception; public class DuplicatedException extends RuntimeException { public DuplicatedException() { diff --git a/src/main/java/domain/Card.java b/src/main/java/domain/Card.java deleted file mode 100644 index 74f6c0f3563..00000000000 --- a/src/main/java/domain/Card.java +++ /dev/null @@ -1,35 +0,0 @@ -package domain; - -import java.util.Objects; - -public class Card { - - private final CardPattern pattern; - private final CardNumber number; - - public Card(String number, String pattern) { - this.number = CardNumber.matchCardNumber(number); - this.pattern = CardPattern.matchCardPattern(pattern); - } - - public int number() { - return number.getValue(); - } - - public String cardName() { - return number.getCourt() + pattern.getName(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Card card)) { - return false; - } - return pattern == card.pattern && number == card.number; - } - - @Override - public int hashCode() { - return Objects.hash(pattern, number); - } -} diff --git a/src/main/java/domain/Dealer.java b/src/main/java/domain/Dealer.java deleted file mode 100644 index 0a0396321ac..00000000000 --- a/src/main/java/domain/Dealer.java +++ /dev/null @@ -1,23 +0,0 @@ -package domain; - -import constant.Word; - -public class Dealer extends Member { - - public Dealer() { - super(Word.DEALER.getWord()); - } - - @Override - public MatchResult compareScoreWith(Member player) { - int dealerScore = currentValue(); - int playerScore = player.currentValue(); - if (playerScore > BUST_CONDITION) { - return MatchResult.WIN; - } - if (dealerScore > BUST_CONDITION) { - return MatchResult.LOSE; - } - return calculateResultFromNormalCase(dealerScore, playerScore); - } -} diff --git a/src/main/java/domain/GameTable.java b/src/main/java/domain/GameTable.java index f81f665d22c..af00014123a 100644 --- a/src/main/java/domain/GameTable.java +++ b/src/main/java/domain/GameTable.java @@ -1,70 +1,71 @@ package domain; -import constant.Word; -import domain.dto.GameResult; -import domain.dto.MemberStatus; +import domain.card.Card; +import domain.card.Deck; +import domain.member.Money; +import dto.MemberStatus; +import domain.member.Members; + import java.util.ArrayList; import java.util.List; import java.util.Map; public class GameTable { - private static final int BLACKJACK = 21; - private static final int DEALER_DRAW_CONDITION = 16; - private final Members members; private final Deck deck; - public GameTable(List playerNames, Deck deck) { - this.members = new Members(playerNames); + public GameTable(Map playerBets, Deck deck) { + this.members = new Members(playerBets); this.deck = deck; } public void distributeInitCard() { - for (String memberName : members.getAllPlayerName()) { - members.provideCard(memberName, deck.draw()); - members.provideCard(memberName, deck.draw()); + members.provideCardToDealer(deck.draw()); + members.provideCardToDealer(deck.draw()); + for (String playerName : members.getAllPlayerName()) { + members.provideCardToPlayer(playerName, deck.draw()); + members.provideCardToPlayer(playerName, deck.draw()); } } - public boolean checkBust(String memberName) { - return members.checkValue(memberName) > BLACKJACK; + public boolean isPlayerBust(String playerName) { + return members.isPlayerBust(playerName); + } + + public boolean isPlayerFinished(String playerName) { + return members.isPlayerFinishedByName(playerName); + } + + public void changePlayerState(String playerName) { + members.changePlayerStateToStay(playerName); } - public List drawForMember(String memberName) { - members.provideCard(memberName, deck.draw()); - return members.findCardByName(memberName); + public List drawForMember(String playerName) { + members.provideCardToPlayer(playerName, deck.draw()); + return members.findCardByName(playerName); } public boolean drawForDealer() { - if (members.checkValue(Word.DEALER.getWord()) <= DEALER_DRAW_CONDITION) { - members.provideCard(Word.DEALER.getWord(), deck.draw()); + if (members.canTheDealerDraw()) { + members.provideCardToDealer(deck.draw()); return true; } return false; } - public List checkMemberStatuses() { - return members.getAllPlayerName() - .stream() - .map(name -> { - List cards = members.findCardByName(name); - int totalValue = members.checkValue(name); - return new MemberStatus(name, cards, totalValue); - }).toList(); - } - - public List checkGameResult() { - List gameResults = new ArrayList<>(); - gameResults.add(new GameResult(Word.DEALER.getWord(), - members.judgeDealerGameResult())); + public List getMemberStatuses() { + List memberStatuses = new ArrayList<>(); + memberStatuses.add( + new MemberStatus(members.getDealerName(), members.findDealerCards(), members.getDealerScore())); - Map playerResults = members.judgePlayerGameResult(); + members.getAllPlayerName().stream() + .map(name -> new MemberStatus(name, members.findCardByName(name), members.getPlayerScore(name))) + .forEach(memberStatuses::add); + return List.copyOf(memberStatuses); + } - for (String playerName : playerResults.keySet()) { - gameResults.add(new GameResult(playerName, - List.of(playerResults.get(playerName)))); - } - return gameResults; + public Map getFinalProfits() { + return members.calculateFinalProfits(); } } diff --git a/src/main/java/domain/MatchResult.java b/src/main/java/domain/MatchResult.java index 44699cba894..4e30b2557ff 100644 --- a/src/main/java/domain/MatchResult.java +++ b/src/main/java/domain/MatchResult.java @@ -1,7 +1,18 @@ package domain; public enum MatchResult { - WIN, - DRAW, - LOSE + BLACKJACK_WIN(1.5), + WIN(1.0), + DRAW(0.0), + LOSE(-1.0); + + private final double profitRate; + + MatchResult(double profitRate) { + this.profitRate = profitRate; + } + + public double profitRate() { + return profitRate; + } } diff --git a/src/main/java/domain/Member.java b/src/main/java/domain/Member.java deleted file mode 100644 index 38a67c18a54..00000000000 --- a/src/main/java/domain/Member.java +++ /dev/null @@ -1,43 +0,0 @@ -package domain; - -import java.util.List; - -public abstract class Member { - protected static final int BUST_CONDITION = 21; - - private final String name; - private final Hand hand; - - public Member(String name) { - this.name = name; - this.hand = new Hand(); - } - - public String name() { - return name; - } - - public int currentValue() { - return hand.calculateTotalValue(); - } - - public List currentCards() { - return hand.cards(); - } - - public void receiveCard(Card card) { - hand.appendCard(card); - } - - public abstract MatchResult compareScoreWith(Member other); - - protected MatchResult calculateResultFromNormalCase(int myScore, int targetScore) { - if (myScore > targetScore) { - return MatchResult.WIN; - } - if (myScore < targetScore) { - return MatchResult.LOSE; - } - return MatchResult.DRAW; - } -} diff --git a/src/main/java/domain/Members.java b/src/main/java/domain/Members.java deleted file mode 100644 index c33584c6d91..00000000000 --- a/src/main/java/domain/Members.java +++ /dev/null @@ -1,82 +0,0 @@ -package domain; - -import constant.Word; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; - -public class Members { - - private final List members; - - public Members(List playerNames) { - this.members = new ArrayList<>(); - join(new Dealer()); - for (String name : playerNames) { - join(new Player(name)); - } - } - - private void join(Member member) { - members.add(member); - } - - public void provideCard(String memberName, Card card) { - Member member = findByName(memberName); - member.receiveCard(card); - } - - public List findCardByName(String memberName) { - Member member = findByName(memberName); - return member.currentCards(); - } - - public int checkValue(String memberName) { - Member member = findByName(memberName); - return member.currentValue(); - } - - private Member findByName(String memberName) { - return members.stream() - .filter(member -> member.name().equals(memberName)) - .findAny() - .orElseThrow(NoSuchElementException::new); - } - - public List getAllPlayerName() { - return members.stream() - .map(Member::name) - .toList(); - } - - public List judgeDealerGameResult() { - Member dealer = findByName(Word.DEALER.getWord()); - List players = members.stream() - .filter(member -> !member.name().equals(Word.DEALER.getWord())) - .toList(); - - List gameResult = new ArrayList<>(); - for (Member player : players) { - gameResult.add(dealer.compareScoreWith(player)); - } - return gameResult; - } - - public Map judgePlayerGameResult() { - Member dealer = findByName(Word.DEALER.getWord()); - List players = members.stream() - .filter(member -> !member.name().equals(Word.DEALER.getWord())) - .toList(); - - Map gameResult = new HashMap<>(); - for (Member player : players) { - String playerName = player.name(); - gameResult.put(playerName, - player.compareScoreWith(dealer)); - } - - return gameResult; - } -} diff --git a/src/main/java/domain/Player.java b/src/main/java/domain/Player.java deleted file mode 100644 index 93757e526bc..00000000000 --- a/src/main/java/domain/Player.java +++ /dev/null @@ -1,22 +0,0 @@ -package domain; - -public class Player extends Member { - - public Player(String name) { - super(name); - } - - public MatchResult compareScoreWith(Member dealer) { - int playerScore = currentValue(); - int dealerScore = dealer.currentValue(); - - if (playerScore > BUST_CONDITION) { - return MatchResult.LOSE; - } - if (dealerScore > BUST_CONDITION) { - return MatchResult.WIN; - } - - return calculateResultFromNormalCase(playerScore, dealerScore); - } -} diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java new file mode 100644 index 00000000000..07a8c67185b --- /dev/null +++ b/src/main/java/domain/card/Card.java @@ -0,0 +1,61 @@ +package domain.card; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Card { + + private static final Map CACHE = new HashMap<>(); + + static { + Arrays.stream(CardPattern.values()) + .forEach(pattern -> Arrays.stream(CardNumber.values()) + .forEach(number -> { + String key = generateKey(number.getCourt(), pattern.getName()); + CACHE.put(key, new Card(number.getCourt(), pattern.getName())); + })); + } + + private final CardPattern pattern; + private final CardNumber number; + + public Card(String number, String pattern) { + this.number = CardNumber.matchCardNumber(number); + this.pattern = CardPattern.matchCardPattern(pattern); + } + + public static Card from(String number, String pattern) { + String key = generateKey(number, pattern); + if (!CACHE.containsKey(key)) { + throw new IllegalArgumentException("존재하지 않는 카드 조합입니다: " + key); + } + return CACHE.get(key); + } + + private static String generateKey(String number, String pattern) { + return number + pattern; + } + + public int number() { + return number.getValue(); + } + + public String cardName() { + return number.getCourt() + pattern.getName(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Card card)) { + return false; + } + return pattern == card.pattern && number == card.number; + } + + @Override + public int hashCode() { + return Objects.hash(pattern, number); + } +} diff --git a/src/main/java/domain/CardNumber.java b/src/main/java/domain/card/CardNumber.java similarity index 91% rename from src/main/java/domain/CardNumber.java rename to src/main/java/domain/card/CardNumber.java index 5b71704531f..22cb3606bc4 100644 --- a/src/main/java/domain/CardNumber.java +++ b/src/main/java/domain/card/CardNumber.java @@ -1,4 +1,4 @@ -package domain; +package domain.card; import java.util.Arrays; @@ -17,8 +17,8 @@ public enum CardNumber { QUEEN(10, "Q"), KING(10, "K"); - private int number; - private String court; + private final int number; + private final String court; CardNumber(int number, String court) { this.number = number; diff --git a/src/main/java/domain/CardPattern.java b/src/main/java/domain/card/CardPattern.java similarity index 91% rename from src/main/java/domain/CardPattern.java rename to src/main/java/domain/card/CardPattern.java index b955b3ac5b4..abb8024182a 100644 --- a/src/main/java/domain/CardPattern.java +++ b/src/main/java/domain/card/CardPattern.java @@ -1,4 +1,4 @@ -package domain; +package domain.card; import java.util.Arrays; import java.util.NoSuchElementException; @@ -9,7 +9,7 @@ public enum CardPattern { DIAMOND("다이아몬드"), CLUB("클로버"); - private String name; + private final String name; CardPattern(String name) { this.name = name; diff --git a/src/main/java/domain/Deck.java b/src/main/java/domain/card/Deck.java similarity index 67% rename from src/main/java/domain/Deck.java rename to src/main/java/domain/card/Deck.java index 77a74092d15..620811d98d6 100644 --- a/src/main/java/domain/Deck.java +++ b/src/main/java/domain/card/Deck.java @@ -1,4 +1,4 @@ -package domain; +package domain.card; public interface Deck { Card draw(); diff --git a/src/main/java/domain/StandardDeck.java b/src/main/java/domain/card/StandardDeck.java similarity index 55% rename from src/main/java/domain/StandardDeck.java rename to src/main/java/domain/card/StandardDeck.java index c3ecc262b82..fc38fb78f65 100644 --- a/src/main/java/domain/StandardDeck.java +++ b/src/main/java/domain/card/StandardDeck.java @@ -1,10 +1,12 @@ -package domain; +package domain.card; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import java.util.Queue; +import java.util.stream.Collectors; public class StandardDeck implements Deck { @@ -16,16 +18,12 @@ public StandardDeck() { } private void init() { - for (CardPattern cardPattern : CardPattern.values()) { - makeCard(cardPattern.getName()); - } - Collections.shuffle((List) queue); - } - - private void makeCard(String cardPattern) { - for (CardNumber cardNumber : CardNumber.values()) { - queue.add(new Card(cardNumber.getCourt(), cardPattern)); - } + List cards = Arrays.stream(CardPattern.values()) + .flatMap(pattern -> Arrays.stream(CardNumber.values()) + .map(number -> Card.from(number.getCourt(), pattern.getName()))) + .collect(Collectors.toList()); + Collections.shuffle(cards); + queue.addAll(cards); } @Override diff --git a/src/main/java/domain/dto/GameResult.java b/src/main/java/domain/dto/GameResult.java deleted file mode 100644 index 5563f99294e..00000000000 --- a/src/main/java/domain/dto/GameResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package domain.dto; - -import domain.MatchResult; -import java.util.List; - -public record GameResult( - String name, - List result -) { -} diff --git a/src/main/java/domain/member/Dealer.java b/src/main/java/domain/member/Dealer.java new file mode 100644 index 00000000000..e4ecdf711b0 --- /dev/null +++ b/src/main/java/domain/member/Dealer.java @@ -0,0 +1,15 @@ +package domain.member; + +public class Dealer implements Participant { + + private final MemberInfo dealerInfo; + + public Dealer(MemberInfo dealerInfo) { + this.dealerInfo = dealerInfo; + } + + @Override + public MemberInfo info() { + return dealerInfo; + } +} diff --git a/src/main/java/domain/Hand.java b/src/main/java/domain/member/Hand.java similarity index 70% rename from src/main/java/domain/Hand.java rename to src/main/java/domain/member/Hand.java index e9fad1ec802..93f417ba437 100644 --- a/src/main/java/domain/Hand.java +++ b/src/main/java/domain/member/Hand.java @@ -1,6 +1,8 @@ -package domain; +package domain.member; -import domain.exception.DuplicatedException; +import domain.card.Card; +import domain.card.CardNumber; +import constant.exception.DuplicatedException; import java.util.ArrayList; import java.util.List; @@ -9,16 +11,26 @@ public class Hand { private final List cards; public Hand() { - cards = new ArrayList<>(); + cards = List.of(); } - public List cards() { - return cards; + private Hand(List cards) { + this.cards = cards; } - public void appendCard(Card card) { + public int size() { + return cards.size(); + } + + public List getAllCard() { + return List.copyOf(cards); + } + + public Hand appendCard(Card card) { validateDuplicate(card); - cards.add(card); + List newCards = new ArrayList<>(cards); + newCards.add(card); + return new Hand(newCards); } public int calculateTotalValue() { diff --git a/src/main/java/domain/member/MemberInfo.java b/src/main/java/domain/member/MemberInfo.java new file mode 100644 index 00000000000..ceb28e646b3 --- /dev/null +++ b/src/main/java/domain/member/MemberInfo.java @@ -0,0 +1,56 @@ +package domain.member; + +import domain.card.Card; +import domain.state.Bust; +import domain.state.State; + +import java.util.List; + +public class MemberInfo { + public static final String DEALER_NAME = "딜러"; + + private final String name; + private State state; + + public MemberInfo(State state) { + this.name = DEALER_NAME; + this.state = state; + } + + public MemberInfo(String name, State state) { + this.name = name; + this.state = state; + } + + public String name() { + return name; + } + + public State state() { + return state; + } + + public int currentScore() { + return state.hand().calculateTotalValue(); + } + + public List currentCards() { + return state.hand().getAllCard(); + } + + public boolean isFinished() { + return state.isFinished(); + } + + public void receiveCard(Card card) { + state = state.draw(card); + } + + public boolean isBust() { + return state.isBust(); + } + + public void changeToStay() { + this.state = state.stay(); + } +} diff --git a/src/main/java/domain/member/Members.java b/src/main/java/domain/member/Members.java new file mode 100644 index 00000000000..3656a03cc49 --- /dev/null +++ b/src/main/java/domain/member/Members.java @@ -0,0 +1,103 @@ +package domain.member; + +import domain.card.Card; +import domain.state.DealerHit; +import domain.state.Hit; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +public class Members { + + private final Dealer dealer; + private final List players; + + public Members(Map playerBets) { + this.dealer = new Dealer(new MemberInfo(new DealerHit(new Hand()))); + this.players = playerBets.entrySet().stream() + .map(entry -> new Player(new MemberInfo(entry.getKey(), new Hit(new Hand())), entry.getValue())) + .toList(); + } + + public void provideCardToPlayer(String playerName, Card card) { + Player player = findByPlayerName(playerName); + player.receiveCard(card); + } + + public void provideCardToDealer(Card card) { + dealer.receiveCard(card); + } + + public List findCardByName(String playerName) { + Player player = findByPlayerName(playerName); + return player.currentCards(); + } + + public List findDealerCards() { + return dealer.currentCards(); + } + + public int getPlayerScore(String playerName) { + Player player = findByPlayerName(playerName); + return player.currentScore(); + } + + public int getDealerScore() { + return dealer.currentScore(); + } + + public boolean isPlayerFinishedByName(String playerName) { + return findByPlayerName(playerName).isFinished(); + } + + public boolean canTheDealerDraw() { + return !dealer.isFinished(); + } + + public List getAllPlayerName() { + return players.stream() + .map(Player::name) + .toList(); + } + + public Map calculateFinalProfits() { + validateFinished(); + Map totalResults = new LinkedHashMap<>(); + players.forEach(player -> + totalResults.put(player.name(), player.calculateProfit(dealer.info()))); + int totalPlayerProfit = totalResults.values() + .stream() + .mapToInt(Integer::intValue) + .sum(); + totalResults.put(dealer.name(), -1 * totalPlayerProfit); + return totalResults; + } + + private void validateFinished() { + if (!dealer.isFinished() || players.stream() + .anyMatch(player -> !player.isFinished())) { + throw new IllegalArgumentException("게임이 끝나지 않은 사람이 있습니다."); + } + } + + public boolean isPlayerBust(String name) { + return findByPlayerName(name).isBust(); + } + + public void changePlayerStateToStay(String playerName) { + findByPlayerName(playerName).changeToStay(); + } + + public String getDealerName() { + return dealer.name(); + } + + private Player findByPlayerName(String playerName) { + return players.stream() + .filter(player -> player.name().equals(playerName)) + .findAny() + .orElseThrow(NoSuchElementException::new); + } +} diff --git a/src/main/java/domain/member/Money.java b/src/main/java/domain/member/Money.java new file mode 100644 index 00000000000..07f9b7e79f7 --- /dev/null +++ b/src/main/java/domain/member/Money.java @@ -0,0 +1,34 @@ +package domain.member; + +import java.util.Objects; + +public class Money { + + private final int amount; + + public Money(int amount) { + validateMoreThanZero(amount); + this.amount = amount; + } + + private void validateMoreThanZero(int amount) { + if (amount <= 0) { + throw new IllegalArgumentException("베팅 금액은 0보다 커야 합니다."); + } + } + + public int calculateProfit(double earningRate) { + return (int) (amount * earningRate); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Money money)) return false; + return amount == money.amount; + } + + @Override + public int hashCode() { + return Objects.hashCode(amount); + } +} diff --git a/src/main/java/domain/member/Participant.java b/src/main/java/domain/member/Participant.java new file mode 100644 index 00000000000..be3570965e7 --- /dev/null +++ b/src/main/java/domain/member/Participant.java @@ -0,0 +1,37 @@ +package domain.member; + +import domain.card.Card; +import java.util.List; + +public interface Participant { + + MemberInfo info(); + + default String name() { + return info().name(); + } + + default void receiveCard(Card card) { + info().receiveCard(card); + } + + default int currentScore() { + return info().currentScore(); + } + + default List currentCards() { + return info().currentCards(); + } + + default boolean isFinished() { + return info().isFinished(); + } + + default boolean isBust() { + return info().isBust(); + } + + default void changeToStay() { + info().changeToStay(); + } +} diff --git a/src/main/java/domain/member/Player.java b/src/main/java/domain/member/Player.java new file mode 100644 index 00000000000..26201f4dbb9 --- /dev/null +++ b/src/main/java/domain/member/Player.java @@ -0,0 +1,21 @@ +package domain.member; + +public class Player implements Participant { + + private final MemberInfo playerInfo; + private final Money betMoney; + + public Player(MemberInfo playerInfo, Money betMoney) { + this.playerInfo = playerInfo; + this.betMoney = betMoney; + } + + @Override + public MemberInfo info() { + return playerInfo; + } + + public int calculateProfit(MemberInfo memberInfo) { + return betMoney.calculateProfit(playerInfo.state().earningRate(memberInfo.state())); + } +} diff --git a/src/main/java/domain/state/Blackjack.java b/src/main/java/domain/state/Blackjack.java new file mode 100644 index 00000000000..f03ef6d44f1 --- /dev/null +++ b/src/main/java/domain/state/Blackjack.java @@ -0,0 +1,29 @@ +package domain.state; + +import domain.MatchResult; +import domain.member.Hand; + +public class Blackjack extends Finished { + + public Blackjack(Hand hand) { + super(hand); + } + + @Override + public boolean isBlackjack() { + return true; + } + + @Override + public double earningRate(State dealerState) { + if (dealerState.isBlackjack()) { + return MatchResult.DRAW.profitRate(); + } + return MatchResult.BLACKJACK_WIN.profitRate(); + } + + @Override + public State stay() { + return this; + } +} diff --git a/src/main/java/domain/state/Bust.java b/src/main/java/domain/state/Bust.java new file mode 100644 index 00000000000..2ae2d093d0f --- /dev/null +++ b/src/main/java/domain/state/Bust.java @@ -0,0 +1,25 @@ +package domain.state; + +import domain.member.Hand; + +public class Bust extends Finished { + + public Bust(Hand hand) { + super(hand); + } + + @Override + public boolean isBust() { + return true; + } + + @Override + public double earningRate(State dealerState) { + return -1.0; + } + + @Override + public State stay() { + return this; + } +} diff --git a/src/main/java/domain/state/DealerHit.java b/src/main/java/domain/state/DealerHit.java new file mode 100644 index 00000000000..70db4c4ece9 --- /dev/null +++ b/src/main/java/domain/state/DealerHit.java @@ -0,0 +1,45 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; + +public class DealerHit extends Running { + private static final int DEALER_DRAW_CONDITION = 16; + + public DealerHit(Hand hand) { + super(hand); + } + + @Override + public State draw(Card card) { + Hand newHand = hand.appendCard(card); + int score = newHand.calculateTotalValue(); + int newHandSize = newHand.size(); + + if (score >= BUST_CONDITION) { + return new Bust(newHand); + } + if (newHandSize < INITIAL_CARDS_COUNT) { + return new DealerHit(newHand); + } + if (newHandSize == INITIAL_CARDS_COUNT) { + return judgeInitialState(newHand, score); + } + return new Stay(newHand); + } + + private State judgeInitialState(Hand newHand, int score) { + if (score == BLACKJACK_CONDITION) { + return new Blackjack(newHand); + } + if (score > DEALER_DRAW_CONDITION) { + return new Stay(newHand); + } + return new DealerHit(newHand); + } + + @Override + public State stay() { + throw new IllegalArgumentException("딜러는 스스로 멈출 수 없습니다."); + } +} diff --git a/src/main/java/domain/state/Finished.java b/src/main/java/domain/state/Finished.java new file mode 100644 index 00000000000..dbb165987b6 --- /dev/null +++ b/src/main/java/domain/state/Finished.java @@ -0,0 +1,26 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; + +public abstract class Finished extends Started { + + public Finished(Hand hand) { + super(hand); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public State draw(Card card) { + throw new IllegalArgumentException("이미 종료된 상태입니다."); + } + + @Override + public State stay() { + return this; + } +} diff --git a/src/main/java/domain/state/Hit.java b/src/main/java/domain/state/Hit.java new file mode 100644 index 00000000000..c3fbf8862dd --- /dev/null +++ b/src/main/java/domain/state/Hit.java @@ -0,0 +1,37 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; + +public class Hit extends Running { + + public Hit(Hand hand) { + super(hand); + } + + @Override + public State draw(Card card) { + Hand newHand = hand.appendCard(card); + int score = newHand.calculateTotalValue(); + + if (score >= BUST_CONDITION) { + return new Bust(newHand); + } + if (score == BLACKJACK_CONDITION) { + return judgeInitialState(newHand); + } + return new Hit(newHand); + } + + private State judgeInitialState(Hand newHand) { + if (newHand.size() == INITIAL_CARDS_COUNT) { + return new Blackjack(newHand); + } + return new Stay(newHand); + } + + @Override + public State stay() { + return new Stay(hand); + } +} diff --git a/src/main/java/domain/state/Running.java b/src/main/java/domain/state/Running.java new file mode 100644 index 00000000000..aec71a8e434 --- /dev/null +++ b/src/main/java/domain/state/Running.java @@ -0,0 +1,23 @@ +package domain.state; + +import domain.member.Hand; + +public abstract class Running extends Started { + protected static final int BUST_CONDITION = 22; + protected static final int BLACKJACK_CONDITION = 21; + protected static final int INITIAL_CARDS_COUNT = 2; + + public Running(Hand hand) { + super(hand); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public double earningRate(State dealerState) { + throw new IllegalStateException("게임이 끝나지 않은 상태에서는 수익률을 계산할 수 없습니다."); + } +} diff --git a/src/main/java/domain/state/Started.java b/src/main/java/domain/state/Started.java new file mode 100644 index 00000000000..9a547634831 --- /dev/null +++ b/src/main/java/domain/state/Started.java @@ -0,0 +1,26 @@ +package domain.state; + +import domain.member.Hand; + +public abstract class Started implements State { + protected final Hand hand; + + protected Started(Hand hand) { + this.hand = hand; + } + + @Override + public boolean isBust() { + return false; + } + + @Override + public boolean isBlackjack() { + return false; + } + + @Override + public Hand hand() { + return hand; + } +} diff --git a/src/main/java/domain/state/State.java b/src/main/java/domain/state/State.java new file mode 100644 index 00000000000..d7fd1d9e861 --- /dev/null +++ b/src/main/java/domain/state/State.java @@ -0,0 +1,14 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; + +public interface State { + boolean isFinished(); + boolean isBust(); + boolean isBlackjack(); + double earningRate(State dealerState); + State draw(Card card); + State stay(); + Hand hand(); +} diff --git a/src/main/java/domain/state/Stay.java b/src/main/java/domain/state/Stay.java new file mode 100644 index 00000000000..61a8ff1abf2 --- /dev/null +++ b/src/main/java/domain/state/Stay.java @@ -0,0 +1,33 @@ +package domain.state; + +import domain.MatchResult; +import domain.member.Hand; + +public class Stay extends Finished { + + public Stay(Hand hand) { + super(hand); + } + + @Override + public double earningRate(State dealerState) { + if (dealerState.isBust()) { + return MatchResult.WIN.profitRate(); + } + if (dealerState.isBlackjack()) { + return MatchResult.LOSE.profitRate(); + } + return judgeScore(dealerState.hand().calculateTotalValue()); + } + + private double judgeScore(int dealerScore) { + int myScore = hand.calculateTotalValue(); + if (myScore > dealerScore) { + return MatchResult.WIN.profitRate(); + } + if (myScore < dealerScore) { + return MatchResult.LOSE.profitRate(); + } + return MatchResult.DRAW.profitRate(); + } +} diff --git a/src/main/java/dto/GameResult.java b/src/main/java/dto/GameResult.java new file mode 100644 index 00000000000..8244995c03c --- /dev/null +++ b/src/main/java/dto/GameResult.java @@ -0,0 +1,7 @@ +package dto; + +public record GameResult( + String name, + int result +) { +} diff --git a/src/main/java/domain/dto/MemberStatus.java b/src/main/java/dto/MemberStatus.java similarity index 61% rename from src/main/java/domain/dto/MemberStatus.java rename to src/main/java/dto/MemberStatus.java index fab0d42e7ab..eb4e0e4bdfd 100644 --- a/src/main/java/domain/dto/MemberStatus.java +++ b/src/main/java/dto/MemberStatus.java @@ -1,10 +1,11 @@ -package domain.dto; +package dto; + +import domain.card.Card; -import domain.Card; import java.util.List; public record MemberStatus( - String playerName, + String memberName, List cards, int totalValue ) { diff --git a/src/main/java/application/dto/RoundResult.java b/src/main/java/dto/RoundResult.java similarity index 70% rename from src/main/java/application/dto/RoundResult.java rename to src/main/java/dto/RoundResult.java index 4eb18c2220b..c195fa4bb81 100644 --- a/src/main/java/application/dto/RoundResult.java +++ b/src/main/java/dto/RoundResult.java @@ -1,6 +1,7 @@ -package application.dto; +package dto; + +import domain.card.Card; -import domain.Card; import java.util.List; public record RoundResult( diff --git a/src/main/java/presentation/BlackjackController.java b/src/main/java/presentation/BlackjackController.java index 3bc93421658..44ca1f822f4 100644 --- a/src/main/java/presentation/BlackjackController.java +++ b/src/main/java/presentation/BlackjackController.java @@ -1,74 +1,76 @@ package presentation; import application.BlackjackService; -import application.dto.RoundResult; -import domain.dto.GameResult; -import domain.dto.MemberStatus; +import dto.RoundResult; +import dto.GameResult; +import dto.MemberStatus; + +import java.util.HashMap; import java.util.List; -import presentation.ui.InputView; -import presentation.ui.OutputView; +import java.util.Map; + +import presentation.ui.BlackjackView; public class BlackjackController { - private final BlackjackService blackjackService; - private final InputView inputView; - private final OutputView outputView; + private final BlackjackView blackjackView; - public BlackjackController(BlackjackService blackjackService, InputView inputView, OutputView outputView) { - this.blackjackService = blackjackService; - this.inputView = inputView; - this.outputView = outputView; + public BlackjackController(BlackjackView blackjackView) { + this.blackjackView = blackjackView; } public void executeGame() { - List playerNames = getPlayerNames(); - InitialCards(); - playGame(playerNames); - checkDrawableOfDealer(); - finalGameStatus(); - finalGameResult(); + List playerNames = blackjackView.inputView().readPlayerNames(); + BlackjackService blackjackService = new BlackjackService(readPlayerInitStatus(playerNames)); + setUpGame(blackjackService); + playGame(blackjackService, playerNames); + checkDrawableOfDealer(blackjackService); + finalGameStatus(blackjackService); + finalGameResult(blackjackService); } - private void finalGameResult() { + private void finalGameResult(BlackjackService blackjackService) { List gameResults = blackjackService.getGameResults(); - outputView.printGameResult(gameResults); + blackjackView.outputView().printGameResult(gameResults); } - private void finalGameStatus() { + private void finalGameStatus(BlackjackService blackjackService) { List statuses = blackjackService.getMemberStatuses(); - outputView.printFinalMemberStatus(statuses); + blackjackView.outputView().printFinalMemberStatus(statuses); } - private void checkDrawableOfDealer() { + private void checkDrawableOfDealer(BlackjackService blackjackService) { boolean dealerDrawable = blackjackService.checkDealerDrawable(); if (dealerDrawable) { - outputView.printDealerDrawOut(); + blackjackView.outputView().printDealerDrawOut(); } } - private void playGame(List playerNames) { + private void playGame(BlackjackService blackjackService, List playerNames) { for (String playerName : playerNames) { - playAllRoundOfPlayer(playerName); + playAllRoundOfPlayer(blackjackService, playerName); } } - private void InitialCards() { - List memberStatuses = blackjackService.getMemberStatuses(); - outputView.printInitialStatus(memberStatuses); + private Map readPlayerInitStatus(List playerNames) { + Map playerBets = new HashMap<>(); + for (String playerName : playerNames) { + int betAmount = blackjackView.inputView().readPlayerBetAmount(playerName); + playerBets.put(playerName, betAmount); + } + return playerBets; } - private List getPlayerNames() { - List playerNames = inputView.readPlayerNames(); - blackjackService.initializeGame(playerNames); - return playerNames; + private void setUpGame(BlackjackService blackjackService) { + List memberStatuses = blackjackService.getMemberStatuses(); + blackjackView.outputView().printInitialStatus(memberStatuses); } - private void playAllRoundOfPlayer(String playerName) { - boolean isBust = false; - while (!isBust && inputView.playContinue(playerName)) { + private void playAllRoundOfPlayer(BlackjackService blackjackService, String playerName) { + while (!blackjackService.isFinishedByName(playerName) && blackjackView.inputView().playContinue(playerName)) { RoundResult roundResult = blackjackService.startOneRound(playerName); - outputView.printCurrentCard(playerName, roundResult); - isBust = roundResult.isBust(); + blackjackView.outputView().printCurrentCard(playerName, roundResult); } + blackjackService.endPlayerRound(playerName); } } diff --git a/src/main/java/presentation/ui/BlackjackView.java b/src/main/java/presentation/ui/BlackjackView.java new file mode 100644 index 00000000000..2a6d452228f --- /dev/null +++ b/src/main/java/presentation/ui/BlackjackView.java @@ -0,0 +1,7 @@ +package presentation.ui; + +public record BlackjackView( + InputView inputView, + OutputView outputView +) { +} diff --git a/src/main/java/presentation/ui/InputView.java b/src/main/java/presentation/ui/InputView.java index a9fa838d584..1112b14ee78 100644 --- a/src/main/java/presentation/ui/InputView.java +++ b/src/main/java/presentation/ui/InputView.java @@ -1,7 +1,8 @@ package presentation.ui; -import static constant.Word.CARD_MORD_MESSAGE; -import static constant.Word.PLAYER_NAME_MESSAGE; +import static presentation.ui.ViewMessage.CARD_MORD_MESSAGE; +import static presentation.ui.ViewMessage.PLAYER_BET_AMOUNT_MESSAGE; +import static presentation.ui.ViewMessage.PLAYER_NAME_MESSAGE; import java.io.BufferedReader; import java.io.IOException; @@ -12,17 +13,32 @@ public class InputView { private final BufferedReader bufferedReader; + private final ValidatedInput validatedInput; public InputView() { this.bufferedReader = new BufferedReader(new InputStreamReader(System.in)); + this.validatedInput = new ValidatedInput(); } public List readPlayerNames() { try { System.out.println(PLAYER_NAME_MESSAGE.format()); - return Stream.of(bufferedReader.readLine().split(",")) - .map(String::trim) + List playerNames = Stream.of(bufferedReader.readLine().split(",")) + .map(String::strip) .toList(); + validatedInput.validatePlayerName(playerNames); + return playerNames; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public int readPlayerBetAmount(String name) { + try { + System.out.println(); + System.out.println(PLAYER_BET_AMOUNT_MESSAGE.format(name)); + String inputBetAmount = bufferedReader.readLine(); + return Integer.parseInt(inputBetAmount); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/presentation/ui/OutputView.java b/src/main/java/presentation/ui/OutputView.java index a17505d695a..a7a53e0f20a 100644 --- a/src/main/java/presentation/ui/OutputView.java +++ b/src/main/java/presentation/ui/OutputView.java @@ -1,16 +1,17 @@ package presentation.ui; -import static constant.Word.*; +import static presentation.ui.ViewMessage.*; + +import dto.RoundResult; +import domain.card.Card; +import dto.GameResult; +import dto.MemberStatus; -import application.dto.RoundResult; -import domain.Card; -import domain.MatchResult; -import domain.dto.GameResult; -import domain.dto.MemberStatus; import java.util.List; import java.util.stream.Collectors; public class OutputView { + private static final String DEALER_NAME = "딜러"; public void printInitialStatus(List playerStatuses) { System.out.println(); @@ -37,24 +38,29 @@ public void printFinalMemberStatus(List statuses) { public void printGameResult(List gameResults) { System.out.println(FINAL_GAME_RESULT_MESSAGE.format()); - gameResults.forEach(this::printMemberResult); + gameResults.stream() + .filter(result -> result.name().equals(DEALER_NAME)) + .forEach(this::printMemberResult); + gameResults.stream() + .filter(result -> !result.name().equals(DEALER_NAME)) + .forEach(this::printMemberResult); } private void printDistributeMessage(List playerStatuses) { String playerNames = playerStatuses.stream() - .map(MemberStatus::playerName) - .filter(s -> !s.equals(DEALER.getWord())) + .map(MemberStatus::memberName) + .filter(s -> !s.equals(DEALER_NAME)) .collect(Collectors.joining(", ")); - System.out.println(DISTRIBUTE_MESSAGE.format(DEALER.getWord(), playerNames)); + System.out.println(DISTRIBUTE_MESSAGE.format(DEALER_NAME, playerNames)); } private void printMemberCurrentCard(MemberStatus playerStatus) { - if (playerStatus.playerName().equals(DEALER.getWord())) { + if (playerStatus.memberName().equals(DEALER_NAME)) { printDealerCurrentCard(playerStatus); return; } System.out.println( - playerStatus.playerName() + playerStatus.memberName() + ": " + playerStatus.cards().stream() .map(Card::cardName) @@ -64,7 +70,7 @@ private void printMemberCurrentCard(MemberStatus playerStatus) { private void printDealerCurrentCard(MemberStatus dealerStatus) { System.out.println( - dealerStatus.playerName() + dealerStatus.memberName() + ": " + dealerStatus.cards().getFirst().cardName() ); @@ -72,46 +78,12 @@ private void printDealerCurrentCard(MemberStatus dealerStatus) { private void printFinalMemberCardAndResult(MemberStatus status) { String cards = status.cards().stream().map(Card::cardName).collect(Collectors.joining(", ")); - System.out.println(CARD_STATUS.format(status.playerName(), cards) + RESULT_MESSAGE.format(status.totalValue())); + System.out.println(CARD_STATUS.format(status.memberName(), cards) + RESULT_MESSAGE.format(status.totalValue())); } private void printMemberResult(GameResult gameResult) { String name = gameResult.name(); - List results = gameResult.result(); - if (name.equals(DEALER.getWord())) { - printDealerResult(results, name); - return; - } - printPlayerResult(results.getFirst(), name); - } - - private void printPlayerResult(MatchResult playerResult, String name) { - if (playerResult == MatchResult.WIN) { - System.out.println(PLAYER_GAME_WIN.format(name)); - return; - } - if (playerResult == MatchResult.DRAW) { - System.out.println(PLAYER_GAME_DRAW.format(name)); - } - System.out.println(PLAYER_GAME_LOSE.format(name)); - } - - private void printDealerResult(List results, String name) { - int win = countResult(results, MatchResult.WIN); - int draw = countResult(results, MatchResult.DRAW); - int lose = countResult(results, MatchResult.LOSE); - - StringBuilder dealerResult = new StringBuilder(); - dealerResult.append(name).append(": "); - if (win > 0) dealerResult.append(win).append("승 "); - if (draw > 0) dealerResult.append(draw).append("무 "); - if (lose > 0) dealerResult.append(lose).append("패 "); - System.out.println(dealerResult); - } - - private int countResult(List results, MatchResult target) { - return (int) results.stream() - .filter(result -> result == target) - .count(); + int result = gameResult.result(); + System.out.println(MEMBER_GAME_RESULT_MESSAGE.format(name, result)); } } diff --git a/src/main/java/presentation/ui/ValidatedInput.java b/src/main/java/presentation/ui/ValidatedInput.java new file mode 100644 index 00000000000..bd350b2543d --- /dev/null +++ b/src/main/java/presentation/ui/ValidatedInput.java @@ -0,0 +1,30 @@ +package presentation.ui; + +import java.util.List; + +public class ValidatedInput { + private static final String DEALER_NAME = "딜러"; + + public void validatePlayerName(List playerNames) { + playerNames.forEach(name -> { + validateIsNotNumber(name); + validateIsNotDealerName(name); + }); + } + + private void validateIsNotNumber(String playerName) { + if (isNumeric(playerName)) { + throw new IllegalArgumentException("플레이어 이름은 숫자로 설정할 수 없습니다."); + } + } + + private boolean isNumeric(String str) { + return str != null && str.matches("\\d+"); + } + + private void validateIsNotDealerName(String playerName) { + if (DEALER_NAME.equals(playerName)) { + throw new IllegalArgumentException("플레이어의 이름을 '딜러'로 설정할 수 없습니다."); + } + } +} diff --git a/src/main/java/constant/Word.java b/src/main/java/presentation/ui/ViewMessage.java similarity index 73% rename from src/main/java/constant/Word.java rename to src/main/java/presentation/ui/ViewMessage.java index 857381e51c2..43016fda817 100644 --- a/src/main/java/constant/Word.java +++ b/src/main/java/presentation/ui/ViewMessage.java @@ -1,29 +1,23 @@ -package constant; +package presentation.ui; -public enum Word { - DEALER("딜러"), +public enum ViewMessage { CARD_STATUS("%s카드: %s"), CARD_MORD_MESSAGE("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"), DEALER_DRAW_MESSAGE("딜러는 16이하라 한장의 카드를 더 받았습니다."), RESULT_MESSAGE(" - 결과: %d"), DISTRIBUTE_MESSAGE("%s와 %s에게 2장을 나누었습니다."), PLAYER_NAME_MESSAGE("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"), + PLAYER_BET_AMOUNT_MESSAGE("%s의 배팅 금액은?"), FINAL_GAME_RESULT_MESSAGE("## 최종 승패"), - PLAYER_GAME_WIN("%s: 승"), - PLAYER_GAME_DRAW("%s: 무"), - PLAYER_GAME_LOSE("%s: 패"); + MEMBER_GAME_RESULT_MESSAGE("%s: %d"); private final String word; - Word(String word) { + ViewMessage(String word) { this.word = word; } public String format(Object... args) { return String.format(word, args); } - - public String getWord() { - return word; - } } diff --git a/src/test/java/domain/GameTableTest.java b/src/test/java/domain/GameTableTest.java index 3de9eb6f2ba..5172f4215d5 100644 --- a/src/test/java/domain/GameTableTest.java +++ b/src/test/java/domain/GameTableTest.java @@ -1,6 +1,12 @@ package domain; +import domain.card.Card; +import domain.card.FixedDeck; + import java.util.List; +import java.util.Map; + +import domain.member.Money; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -15,30 +21,31 @@ class GameTableTest { @BeforeEach void setUpTest() { List cards = List.of( - new Card("10", "클로버"), - new Card("9", "클로버"), - new Card("8", "클로버"), - new Card("7", "클로버") + Card.from("10", "클로버"), + Card.from("9", "클로버"), + Card.from("8", "클로버"), + Card.from("7", "클로버") ); - this.gameTable = new GameTable(List.of(pobiName), new FixedDeck(cards)); + Map playerBets = Map.of(pobiName, new Money(10_000)); + this.gameTable = new GameTable(playerBets, new FixedDeck(cards)); } - @DisplayName("카드의 총합이 21보다 크면 CurrentResult의 isBust는 true이다.") + @DisplayName("카드의 총합이 21보다 크면 isPlayerBust는 true이다.") @Test - void checkCurrentTest_playerHasCardSumOf22_returnTrue() { + void isPlayerBust_ScoreOver21_ReturnTrue() { gameTable.drawForMember(pobiName); gameTable.drawForMember(pobiName); gameTable.drawForMember(pobiName); - Assertions.assertTrue(gameTable.checkBust(pobiName)); + Assertions.assertTrue(gameTable.isPlayerBust(pobiName)); } - @DisplayName("카드의 총합이 21보다 작으면 CurrentResult의 isBust는 false이다.") + @DisplayName("카드의 총합이 21보다 작으면 isPlayerBust는 false이다.") @Test - void checkCurrentTest_playerHasCardSumOf20_returnFalse() { + void isPlayerBust_ScoreUnder21_ReturnFalse() { gameTable.drawForMember(pobiName); gameTable.drawForMember(pobiName); - Assertions.assertFalse(gameTable.checkBust(pobiName)); + Assertions.assertFalse(gameTable.isPlayerBust(pobiName)); } } diff --git a/src/test/java/domain/MemberTest.java b/src/test/java/domain/MemberTest.java deleted file mode 100644 index ecbf018273c..00000000000 --- a/src/test/java/domain/MemberTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package domain; - -import java.util.Arrays; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -public class MemberTest { - - @DisplayName("딜러와 플레이어의 승패를 가르고, 결과를 출력하는 테스트") - @ParameterizedTest - @CsvSource({ - "5:4, 3:2, WIN", - "5:4, 6:3, DRAW", - "3:2, 5:4, LOSE", - "10:9:3, 8:7:6:5, WIN" - }) - void winnerTest_CompareDealerAndPlayerScore_returnMatchResult(String dealerCardValue, String playerCardValue, - MatchResult expected) { - Member dealer = new Dealer(); - Member player = new Player("브리"); - - Arrays.stream(dealerCardValue.split(":")) - .forEach(number -> dealer.receiveCard(new Card(number, "하트"))); - Arrays.stream(playerCardValue.split(":")) - .forEach(number -> player.receiveCard(new Card(number, "하트"))); - - Assertions.assertEquals(expected, dealer.compareScoreWith(player)); - } -} diff --git a/src/test/java/domain/CardNumberTest.java b/src/test/java/domain/card/CardNumberTest.java similarity index 98% rename from src/test/java/domain/CardNumberTest.java rename to src/test/java/domain/card/CardNumberTest.java index 77cc5517b7c..9ac12c7b431 100644 --- a/src/test/java/domain/CardNumberTest.java +++ b/src/test/java/domain/card/CardNumberTest.java @@ -1,4 +1,4 @@ -package domain; +package domain.card; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/domain/CardPatternTest.java b/src/test/java/domain/card/CardPatternTest.java similarity index 98% rename from src/test/java/domain/CardPatternTest.java rename to src/test/java/domain/card/CardPatternTest.java index 4b472c78861..208d5947b09 100644 --- a/src/test/java/domain/CardPatternTest.java +++ b/src/test/java/domain/card/CardPatternTest.java @@ -1,6 +1,7 @@ -package domain; +package domain.card; import java.util.NoSuchElementException; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; diff --git a/src/test/java/domain/card/CardTest.java b/src/test/java/domain/card/CardTest.java new file mode 100644 index 00000000000..d7d94225d19 --- /dev/null +++ b/src/test/java/domain/card/CardTest.java @@ -0,0 +1,18 @@ +package domain.card; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CardTest { + + @DisplayName("동일한 숫자와 문양의 카드를 요청하면 같은 인스턴스를 반환한다") + @Test + void staticFactoryMethod_SameInstance_ReturnTrue() { + Card card1 = Card.from("A", "스페이드"); + Card card2 = Card.from("A", "스페이드"); + + assertThat(card1).isSameAs(card2); + } +} diff --git a/src/test/java/domain/DeckTest.java b/src/test/java/domain/card/DeckTest.java similarity index 97% rename from src/test/java/domain/DeckTest.java rename to src/test/java/domain/card/DeckTest.java index 02eaf3c27e7..ad672bfdb64 100644 --- a/src/test/java/domain/DeckTest.java +++ b/src/test/java/domain/card/DeckTest.java @@ -1,7 +1,8 @@ -package domain; +package domain.card; import java.util.List; import java.util.NoSuchElementException; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/domain/FixedDeck.java b/src/test/java/domain/card/FixedDeck.java similarity index 95% rename from src/test/java/domain/FixedDeck.java rename to src/test/java/domain/card/FixedDeck.java index cf1478054db..524c68066e8 100644 --- a/src/test/java/domain/FixedDeck.java +++ b/src/test/java/domain/card/FixedDeck.java @@ -1,4 +1,4 @@ -package domain; +package domain.card; import java.util.LinkedList; import java.util.List; diff --git a/src/test/java/domain/HandTest.java b/src/test/java/domain/member/HandTest.java similarity index 84% rename from src/test/java/domain/HandTest.java rename to src/test/java/domain/member/HandTest.java index d9f938c130f..5b394cc6c55 100644 --- a/src/test/java/domain/HandTest.java +++ b/src/test/java/domain/member/HandTest.java @@ -1,9 +1,12 @@ -package domain; +package domain.member; + +import domain.card.Card; +import constant.exception.DuplicatedException; -import domain.exception.DuplicatedException; import java.util.LinkedList; import java.util.List; import java.util.Queue; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -23,7 +26,7 @@ void setUp() { @DisplayName("카드 추가 시 중복 검사 예외 테스트") @Test void appendAndDuplicateTest_holdingTwoAndAppendTwo_ThrowDuplicatedException() { - hand.appendCard(new Card("2", "하트")); + hand = hand.appendCard(new Card("2", "하트")); Assertions.assertThatThrownBy( () -> hand.appendCard(new Card("2", "하트"))) @@ -33,8 +36,8 @@ void appendAndDuplicateTest_holdingTwoAndAppendTwo_ThrowDuplicatedException() { @DisplayName("카드 총합 계산 기능 테스트") @Test void calculateTest_holdTwoThree_ReturnTotalValue() { - hand.appendCard(new Card("2", "하트")); - hand.appendCard(new Card("3", "스페이드")); + hand = hand.appendCard(new Card("2", "하트")); + hand = hand.appendCard(new Card("3", "스페이드")); Assertions.assertThat(hand.calculateTotalValue()) .isEqualTo(5); @@ -56,7 +59,7 @@ void aceTest_AceAmountOneAndSum11_return12(String numbers, String names, int res Queue numberQueue = new LinkedList<>(List.of(numbers.split(":"))); Queue nameQueue = new LinkedList<>(List.of(names.split(":"))); while (!numberQueue.isEmpty()) { - hand.appendCard(new Card(numberQueue.poll(), nameQueue.poll())); + hand = hand.appendCard(new Card(numberQueue.poll(), nameQueue.poll())); } Assertions.assertThat(hand.calculateTotalValue()).isEqualTo(result); } diff --git a/src/test/java/domain/member/MembersTest.java b/src/test/java/domain/member/MembersTest.java new file mode 100644 index 00000000000..4a63d72909c --- /dev/null +++ b/src/test/java/domain/member/MembersTest.java @@ -0,0 +1,89 @@ +package domain.member; + +import domain.card.Card; + +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MembersTest { + + private final Money defaultMoney = new Money(10_000); + + @DisplayName("멤버 명단을 정상적으로 불러오는지 테스트") + @Test + void getAllPlayerName_MembersCreated_ContainsNames() { + String player1 = "pobi"; + String player2 = "jason"; + Map playerBets = Map.of(player1, defaultMoney, player2, defaultMoney); + + Members members = new Members(playerBets); + + assertThat(members.getAllPlayerName()).contains(player1, player2); + } + + @DisplayName("해당 멤버에게 카드를 주면 정상적으로 카드를 보유하고 있는지 테스트") + @Test + void provideCardToPlayer_CardGiven_MemberContainsCard() { + String playerName = "pobi"; + Members members = new Members(Map.of(playerName, defaultMoney)); + Card card = Card.from("2", "하트"); + + members.provideCardToPlayer(playerName, card); + + assertThat(members.findCardByName(playerName)).contains(card); + } + + @DisplayName("모든 멤버의 수익 정산이 정상적으로 작동하는지 테스트") + @Test + void calculateFinalProfits_GameOver_ReturnsCorrectResults() { + String playerName = "pobi"; + Members members = new Members(Map.of(playerName, new Money(10_000))); + members.provideCardToPlayer(playerName, Card.from("Q", "하트")); + members.provideCardToPlayer(playerName, Card.from("K", "하트")); + members.changePlayerStateToStay(playerName); + members.provideCardToDealer(Card.from("6", "하트")); + members.provideCardToDealer(Card.from("4", "하트")); + members.provideCardToDealer(Card.from("7", "하트")); + Map results = members.calculateFinalProfits(); + + assertThat(results.get(playerName)).isEqualTo(10_000); + assertThat(results.get(members.getDealerName())).isEqualTo(-10_000); + } + + @DisplayName("딜러와 플레이어가 모두 블랙잭이면 수익은 0원이다") + @Test + void calculateFinalProfits_BothBlackjack_ReturnsZero() { + String playerName = "pobi"; + Members members = new Members(Map.of(playerName, new Money(10_000))); + members.provideCardToPlayer(playerName, Card.from("A", "하트")); + members.provideCardToPlayer(playerName, Card.from("K", "하트")); + members.provideCardToDealer(Card.from("A", "스페이드")); + members.provideCardToDealer(Card.from("Q", "스페이드")); + + Map results = members.calculateFinalProfits(); + + assertThat(results.get(playerName)).isEqualTo(0); + assertThat(results.get(members.getDealerName())).isEqualTo(0); + } + + @DisplayName("딜러가 버스트되면 Bust되지 않은 플레이어는 무조건 승리한다") + @Test + void calculateFinalProfits_DealerBust_PlayerWins() { + String playerName = "pobi"; + Members members = new Members(Map.of(playerName, new Money(10_000))); + members.provideCardToPlayer(playerName, Card.from("2", "하트")); + members.provideCardToPlayer(playerName, Card.from("Q", "하트")); + members.changePlayerStateToStay(playerName); + members.provideCardToDealer(Card.from("10", "하트")); + members.provideCardToDealer(Card.from("6", "하트")); + members.provideCardToDealer(Card.from("7", "하트")); + Map results = members.calculateFinalProfits(); + + assertThat(results.get(playerName)).isEqualTo(10_000); + assertThat(results.get(members.getDealerName())).isEqualTo(-10_000); + } +} diff --git a/src/test/java/domain/member/MoneyTest.java b/src/test/java/domain/member/MoneyTest.java new file mode 100644 index 00000000000..2d2e68848e1 --- /dev/null +++ b/src/test/java/domain/member/MoneyTest.java @@ -0,0 +1,34 @@ +package domain.member; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class MoneyTest { + + @DisplayName("금액이 같으면 같은 객체로 취급한다 (VO)") + @Test + void equals_SameAmount_ReturnTrue() { + assertThat(new Money(10_000)).isEqualTo(new Money(10_000)); + } + + @DisplayName("베팅 금액이 0원 이하이면 예외가 발생한다") + @Test + void constructor_UnderZero_ThrowException() { + assertThatThrownBy(() -> new Money(0)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("수익률을 곱해 최종 수익금을 계산한다") + @Test + void multiply_EarningRate_ReturnProfit() { + Money bet = new Money(10_000); + double earningRate = 1.5; + + int profit = bet.calculateProfit(earningRate); + + assertThat(profit).isEqualTo(15_000); + } +} diff --git a/src/test/java/domain/member/PlayerTest.java b/src/test/java/domain/member/PlayerTest.java new file mode 100644 index 00000000000..cb51b234bac --- /dev/null +++ b/src/test/java/domain/member/PlayerTest.java @@ -0,0 +1,40 @@ +package domain.member; + +import domain.card.Card; +import domain.state.Blackjack; +import domain.state.Bust; +import domain.state.Stay; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PlayerTest { + + @DisplayName("플레이어가 블랙잭이고 딜러가 블랙잭이 아니면 베팅 금액의 1.5배 수익을 반환한다") + @Test + void calculateProfit_StateIsBlackjack_ReturnAccurateEarning() { + Money betMoney = new Money(10_000); + Player player = new Player(new MemberInfo("pobi", new Blackjack(new Hand())), betMoney); + Dealer dealer = new Dealer(new MemberInfo(new Stay(new Hand().appendCard(Card.from("10", "하트"))))); + int expected = 15_000; + + int profit = player.calculateProfit(dealer.info()); + + assertThat(profit).isEqualTo(expected); + } + + @DisplayName("플레이어가 버스트 상태이면 딜러의 상태와 상관없이 베팅 금액을 모두 잃는다") + @Test + void calculateProfit_StateIsBust_ReturnLostEarning() { + Money betMoney = new Money(10_000); + Player player = new Player(new MemberInfo("pobi", + new Bust(new Hand().appendCard(Card.from("10", "하트")))), betMoney); + Dealer dealer = new Dealer(new MemberInfo(new Stay(new Hand().appendCard(Card.from("2", "클로버"))))); + int expected = -10_000; + + int profit = player.calculateProfit(dealer.info()); + + assertThat(profit).isEqualTo(expected); + } +} diff --git a/src/test/java/domain/state/DealerHitTest.java b/src/test/java/domain/state/DealerHitTest.java new file mode 100644 index 00000000000..a9b570aaa2a --- /dev/null +++ b/src/test/java/domain/state/DealerHitTest.java @@ -0,0 +1,45 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DealerHitTest { + + @DisplayName("딜러가 1장의 카드를 가진 상태에서 추가 카드를 뽑으면 점수에 따라 상태가 반환된다") + @ParameterizedTest + @CsvSource({ + "10, 6, domain.state.DealerHit", + "10, 7, domain.state.Stay", + "A, 10, domain.state.Blackjack" + }) + void dealerDraw_SecondCardDrawn_ReturnStatus(String initialNumber, String drawNumber, String expectedState) { + Hand hand = new Hand().appendCard(Card.from(initialNumber, "하트")); + State state = new DealerHit(hand); + + State nextState = state.draw(Card.from(drawNumber, "클로버")); + + assertThat(nextState.getClass().getName()).isEqualTo(expectedState); + } + + @DisplayName("딜러가 2장의 카드를 가진 상태(16점 이하)에서 추가 카드를 뽑으면 결과와 상관없이 종료 상태가 반환된다") + @ParameterizedTest + @CsvSource({ + "10, 4, 2, domain.state.Stay", + "10, 5, 2, domain.state.Stay", + "10, 6, 10, domain.state.Bust" + }) + void draw_ThirdCardDrawn_ReturnsFinishedState(String card1, String card2, String drawNumber, String expectedState) { + Hand hand = new Hand().appendCard(Card.from(card1, "하트")) + .appendCard(Card.from(card2, "다이아몬드")); + State state = new DealerHit(hand); + + State nextState = state.draw(Card.from(drawNumber, "클로버")); + + assertThat(nextState.getClass().getName()).isEqualTo(expectedState); + } +} diff --git a/src/test/java/domain/state/FinishedTest.java b/src/test/java/domain/state/FinishedTest.java new file mode 100644 index 00000000000..19268be566c --- /dev/null +++ b/src/test/java/domain/state/FinishedTest.java @@ -0,0 +1,40 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FinishedTest { + + @DisplayName("Bust 상태는 딜러의 상태와 상관없이 항상 수익률 -1.0을 반환하고 종료 상태이다") + @Test + void bustEarningRate_Always_IsMinusOne() { + State playerState = new Bust(new Hand()); + State dealerState = new Stay(new Hand().appendCard(Card.from("10", "하트"))); + + assertThat(playerState.isFinished()).isTrue(); + assertThat(playerState.earningRate(dealerState)).isEqualTo(-1.0); + } + + @DisplayName("Blackjack 상태는 딜러가 Blackjack이 아니면 수익률 1.5을 반환하고 종료 상태이다") + @Test + void blackjackEarningRate_DealerIsNotBlackjack_IsOnePointFive() { + State playerState = new Blackjack(new Hand()); + State dealerState = new Stay(new Hand()); + + assertThat(playerState.isFinished()).isTrue(); + assertThat(playerState.earningRate(dealerState)).isEqualTo(1.5); + } + + @DisplayName("Blackjack 상태는 딜러도 Blackjack이면 수익률 0.0(무승부)을 반환한다") + @Test + void blackjackEarningRate_BothBlackjack_ReturnsZero() { + State playerState = new Blackjack(new Hand()); + State dealerState = new Blackjack(new Hand()); + + assertThat(playerState.earningRate(dealerState)).isEqualTo(0.0); + } +} diff --git a/src/test/java/domain/state/HitTest.java b/src/test/java/domain/state/HitTest.java new file mode 100644 index 00000000000..6cefa770e9c --- /dev/null +++ b/src/test/java/domain/state/HitTest.java @@ -0,0 +1,65 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HitTest { + + @DisplayName("Hit 상태에서 카드를 뽑아 21 이하이면 다시 Hit 상태를 반환한다") + @Test + void draw_TotalScoreLessThanOrEqualTo21_ReturnHit() { + State state = new Hit(new Hand()); + + State nextState = state.draw(Card.from("2", "스페이드")); + + assertThat(nextState).isInstanceOf(Hit.class); + } + + @DisplayName("Hit 상태에서 카드를 뽑아 21을 초과하면 Bust 상태를 반환한다") + @Test + void draw_TotalScoreGreaterThan21_ReturnBust() { + Hand hand = new Hand().appendCard(Card.from("10", "스페이드")) + .appendCard(Card.from("10", "하트")); + State state = new Hit(hand); + + State nextState = state.draw(Card.from("2", "클로버")); + + assertThat(nextState).isInstanceOf(Bust.class); + } + + @DisplayName("플레이어가 카드를 뽑아 21점이 되었을 때 카드 개수가 2개이면 Blackjack 상태를 반환한다") + @Test + void draw_TotalScoreIs21AndCardSizeIs2_ReturnBlackjack() { + Hand hand = new Hand().appendCard(Card.from("10", "스페이드")); + State state = new Hit(hand); + + State nextState = state.draw(Card.from("A", "클로버")); + + assertThat(nextState).isInstanceOf(Blackjack.class); + } + + @DisplayName("플레이어가 카드를 뽑아 21점이 되었을 때 카드 개수가 2개가 아니면 Stay 상태를 반환한다") + @Test + void draw_TotalScoreIs21AndCardSizeIsNot2_ReturnStay() { + Hand hand = new Hand().appendCard(Card.from("10", "스페이드")) + .appendCard(Card.from("9", "하트")); + State state = new Hit(hand); + + State nextState = state.draw(Card.from("2", "클로버")); + + assertThat(nextState).isInstanceOf(Stay.class); + } + + @DisplayName("Hit 상태에서 stay를 호출하면 Stay 상태를 반환한다") + @Test + void stay_Always_ReturnStay() { + State state = new Hit(new Hand()); + State nextState = state.stay(); + + assertThat(nextState).isInstanceOf(Stay.class); + } +} diff --git a/src/test/java/domain/state/StayTest.java b/src/test/java/domain/state/StayTest.java new file mode 100644 index 00000000000..5dd6b578552 --- /dev/null +++ b/src/test/java/domain/state/StayTest.java @@ -0,0 +1,60 @@ +package domain.state; + +import domain.card.Card; +import domain.member.Hand; +import org.junit.jupiter.api.DisplayName; +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; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class StayTest { + + @DisplayName("Stay 상태는 종료된 상태이다") + @Test + void isFinished_Always_ReturnTrue() { + State state = new Stay(new Hand()); + assertThat(state.isFinished()).isTrue(); + } + + @DisplayName("Stay 상태는 딜러가 Bust되면 점수와 상관없이 수익률 1.0을 반환한다") + @Test + void earningRate_DealerBust_ReturnsOnePointZero() { + Hand playerHand = new Hand().appendCard(Card.from("10", "하트")) + .appendCard(Card.from("5", "하트")); + State playerState = new Stay(playerHand); + + State dealerState = new Bust(new Hand()); + + assertThat(playerState.earningRate(dealerState)).isEqualTo(1.0); + } + + @DisplayName("Stay 상태는 딜러가 Stay일 때 점수 비교를 통해 수익률을 반환한다") + @ParameterizedTest + @CsvSource({ + "10, 10, 10, 9, 1.0", + "10, 8, 10, 9, -1.0", + "10, 9, 10, 9, 0.0" + }) + void earningRate_CompareScoreWithDealer_ReturnsExpectedRate( + String p1, String p2, String d1, String d2, double expectedRate) { + + Hand playerHand = new Hand().appendCard(Card.from(p1, "하트")).appendCard(Card.from(p2, "다이아몬드")); + State playerState = new Stay(playerHand); + + Hand dealerHand = new Hand().appendCard(Card.from(d1, "클로버")).appendCard(Card.from(d2, "스페이드")); + State dealerState = new Stay(dealerHand); + + assertThat(playerState.earningRate(dealerState)).isEqualTo(expectedRate); + } + + @DisplayName("Stay 상태에서 카드를 더 뽑으려 하면 예외가 발생한다") + @Test + void draw_WhenCalled_ThrowsException() { + State state = new Stay(new Hand()); + assertThatThrownBy(() -> state.draw(null)) + .isInstanceOf(IllegalArgumentException.class); + } +}