diff --git a/README.md b/README.md index 1ff5f7b679..7610205748 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ # java-blackjack 블랙잭 미션 저장소 + +## 참가자 입력 +- [ ] 쉼표 기준으로 분리 + +## 카드 배분 +- [ ] 카드는 52장을 사용 + - 숫자 계산은 카드 숫자를 기본 + - Ace는 1 or 11로 계산 가능 + - King, Queen, Jack은 각각 10으로 계산 +- [ ] 카드는 게임이 시작되기 전에 순서를 섞는다. +- [ ] 참가자에게 카드를 2장씩 분배 +- [ ] 각 참가자에게 분배된 카드 출력 + +## 게임 진행 +- [ ] 딜러를 제외한 참가자에게 Hit/Stand 여부 확인 + - 참가자의 카드 합이 21을 넘지 않는지 Bust 검증 + - draw 할 카드가 존재하는지 확인 +- [ ] 딜러 Hit/Stand 여부 확인 + - 16 이하라면 카드를 draw, 아니라면 멈춘다. + +## 게임 종료 +- [ ] 참가자의 카드 패와 결과 출력 +- [ ] 결과를 바탕으로 각 참가자들의 승패 출력 + - 점수가 같을 때를 고려 \ No newline at end of file diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 0000000000..2f87fde1ff --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,10 @@ +import controller.GameController; +import ui.InputView; +import ui.ResultView; + +public class Application { + public static void main(String[] args){ + GameController gameController = new GameController(new ResultView(), new InputView()); + gameController.run(); + } +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 0000000000..4bb682a315 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,119 @@ +package controller; + +import domain.deck.Deck; +import domain.participant.Dealer; +import domain.participant.Participant; +import domain.participant.Player; +import domain.result.ResultJudge; +import ui.InputView; +import ui.ResultView; + +import java.util.ArrayList; +import java.util.List; + +public class GameController { + private final ResultView resultView; + private final InputView inputView; + + public GameController(ResultView resultView, InputView inputView){ + this.resultView = resultView; + this.inputView = inputView; + } + + public void run(){ + Deck deck = new Deck(); + ResultJudge judge = new ResultJudge(); + // 참가자 입력 + List participants = joinParticipant(); + + // 초기 패 설정 + initialAllPlayersHand(participants, deck); + + // 플레이어마다 Hit/Stand 여부 확인 + askHitOrStand(participants, deck); + + // 참가자 패와 점수 공개 + resultView.printScores(participants); + + // 참가자 승/패 공개 + resultView.printGameResult(participants, judge); + + } + + /*** + * 1. 모든 플레이어들에게 Hit/Stand 여부를 묻기 위한 함수 실행 + * 2. 딜러 draw 여부 확인 (16점 이하인지) + */ + private void askHitOrStand(List participants, Deck deck){ + Participant dealer = participants.getFirst(); + for(Participant player : participants.subList(1, participants.size())){ + askPlayerHit(player, deck); + } + + while(dealer.canReceiveCard()){ + dealer.receive(deck.draw()); + resultView.printDealerDraw(); + } + } + + /*** + * 플레이어에게 Hit/Stand 여부 확인 + */ + private void askPlayerHit(Participant player, Deck deck){ + while(inputView.askHit(player.getName())){ + canHit(player, deck); + } + } + + /*** + * Hit 선택시 실제로 플레이어가 Bust가 아닌지 확인 후 카드 지급 + */ + private void canHit(Participant player, Deck deck){ + if(player.canReceiveCard()){ + player.receive(deck.draw()); + resultView.printHand(player); + return; + } + resultView.printIsBust(); + } + + /*** + * 딜러와 입력 받은 플레이어 추가 + */ + private List joinParticipant(){ + Participant dealer = new Dealer("딜러"); + List playerNames; + List participants = new ArrayList<>(); + + participants.add(dealer); + + playerNames = inputView.inputPlayers(); + + for(String playerName : playerNames){ + Participant player = new Player(playerName); + participants.add(player); + } + + return participants; + } + + /*** + * 모든 참가자에게 2장의 카드를 분배하기 위한 함수 호출 및 패 공개 + */ + private void initialAllPlayersHand(List participants, Deck deck){ + for(Participant participant : participants){ + initialHand(participant, deck); + } + resultView.printInitialDeal(participants); + resultView.printAllHands(participants); + } + + /** + * 카드를 2장씩 분배 + */ + private void initialHand(Participant participant, Deck deck){ + for(int i = 0; i < 2; i++){ + participant.receive(deck.draw()); + } + } +} diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java new file mode 100644 index 0000000000..77f85b242a --- /dev/null +++ b/src/main/java/domain/card/Card.java @@ -0,0 +1,26 @@ +package domain.card; + +import domain.card.enums.Rank; +import domain.card.enums.Suit; + +public class Card { + private Rank rank; + private Suit suit; + + public Card(Rank rank, Suit suit){ + this.rank = rank; + this.suit = suit; + } + + public boolean isAce() { + return this.rank == Rank.ACE; + } + + public int score(){ + return this.rank.getScore(); + } + + public String displayName(){ + return rank.getName() + suit.getName(); + } +} diff --git a/src/main/java/domain/card/enums/Rank.java b/src/main/java/domain/card/enums/Rank.java new file mode 100644 index 0000000000..3abbe4b6c0 --- /dev/null +++ b/src/main/java/domain/card/enums/Rank.java @@ -0,0 +1,34 @@ +package domain.card.enums; + +public enum Rank { + ACE("A", 1), + TWO("2", 2), + THREE("3", 3), + FOUR("4", 4), + FIVE("5", 5), + SIX("6", 6), + SEVEN("7", 7), + EIGHT("8", 8), + NINE("9", 9), + TEN("10", 10), + JACK("J", 10), + QUEEN("Q", 10), + KING("K", 10); + + private final String name; + private final int score; + + Rank(String name, int score) { + this.name = name; + this.score = score; + } + + public int getScore() { + return score; + } + + public String getName() { + return name; + } + +} diff --git a/src/main/java/domain/card/enums/Suit.java b/src/main/java/domain/card/enums/Suit.java new file mode 100644 index 0000000000..1f25b7e094 --- /dev/null +++ b/src/main/java/domain/card/enums/Suit.java @@ -0,0 +1,18 @@ +package domain.card.enums; + +public enum Suit { + CLUB("클로버"), + HEART("하트"), + SPADE("스페이드"), + DIAMOND("다이아몬드"); + + private final String name; + + Suit(String name){ + this.name = name; + } + + public String getName(){ + return name; + } +} diff --git a/src/main/java/domain/deck/Deck.java b/src/main/java/domain/deck/Deck.java new file mode 100644 index 0000000000..5370b86769 --- /dev/null +++ b/src/main/java/domain/deck/Deck.java @@ -0,0 +1,50 @@ +package domain.deck; + +import domain.card.Card; +import domain.card.enums.Rank; +import domain.card.enums.Suit; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Deck { + private final List cards = new ArrayList<>(); + + public Deck() { + createdDeck(); + shuffle(); + } + + private void createdDeck(){ + for (Suit suit : Suit.values()){ + addCardsBySuit(suit); + } + } + + private void addCardsBySuit(Suit suit) { + for(Rank rank : Rank.values()){ + this.cards.add(new Card(rank, suit)); + } + } + + public Card firstCard(){ + return cards.get(0); + } + + public Card draw(){ + if(cards.isEmpty()){ + createdDeck(); + this.shuffle(); + } + return cards.removeFirst(); + } + + public void shuffle(){ + Collections.shuffle(cards); + } + + public List cards(){ + return List.copyOf(cards); + } +} diff --git a/src/main/java/domain/hand/Hand.java b/src/main/java/domain/hand/Hand.java new file mode 100644 index 0000000000..0715a8e419 --- /dev/null +++ b/src/main/java/domain/hand/Hand.java @@ -0,0 +1,56 @@ +package domain.hand; + +import domain.card.Card; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Hand { + private final List cards = new ArrayList<>(); + + public void add(Card card) { + cards.add(card); + } + + public int score() { + int score = basicScore(); + int aceCount = aceCount(); + + return bestScore(score, aceCount); + } + + public boolean isBust() { + return score() > 21; + } + + public String displayCards(){ + return cards.stream() + .map(Card::displayName) + .collect(Collectors.joining(", ")); + } + + public String displayFirstCard(){ + return cards.getFirst().displayName(); + } + + private int basicScore() { + int score = 0; + for (Card card : cards) { + score += card.score(); + } + return score; + } + + private int aceCount() { + return (int) cards.stream().filter(Card::isAce).count(); + } + + private int bestScore(int score, int aceCount) { + while (aceCount > 0 && score + 10 <= 21) { + score += 10; + aceCount--; + } + return score; + } +} diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java new file mode 100644 index 0000000000..28efef60d6 --- /dev/null +++ b/src/main/java/domain/participant/Dealer.java @@ -0,0 +1,12 @@ +package domain.participant; + +public class Dealer extends Participant{ + public Dealer(String name){ + super(name); + } + + @Override + public boolean canReceiveCard(){ + return checkScore() <= 16; + } +} diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java new file mode 100644 index 0000000000..de235149a1 --- /dev/null +++ b/src/main/java/domain/participant/Participant.java @@ -0,0 +1,47 @@ +package domain.participant; + +import domain.card.Card; +import domain.hand.Hand; + +public abstract class Participant { + private final String name; + private final Hand hand = new Hand(); + + public Participant(String name){ + this.name = name; + } + + /*** + * 참가자 역할(딜러, 플레이어)에 맞추어 Bust가 아닌 즉, 카드를 더 받을 수 있는지 확인. + */ + public abstract boolean canReceiveCard(); + + public void receive(Card card){ + this.hand.add(card); + } + + public String getName(){ + return this.name; + } + + public String handDisplay(){ + return hand.displayCards(); + } + + public String dealerHandDisplay(){ + return hand.displayFirstCard(); + } + + public int getScore(){ + return hand.score(); + } + + public boolean isBust(){ + return hand.isBust(); + } + + protected int checkScore(){ + return hand.score(); + } + +} diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java new file mode 100644 index 0000000000..ac8b3ea4bf --- /dev/null +++ b/src/main/java/domain/participant/Player.java @@ -0,0 +1,12 @@ +package domain.participant; + +public class Player extends Participant { + public Player (String name){ + super(name); + } + + @Override + public boolean canReceiveCard() { + return checkScore() < 21; + } +} diff --git a/src/main/java/domain/result/GameResult.java b/src/main/java/domain/result/GameResult.java new file mode 100644 index 0000000000..1bf45eddb2 --- /dev/null +++ b/src/main/java/domain/result/GameResult.java @@ -0,0 +1,29 @@ +package domain.result; + +public enum GameResult { + WIN("승"), + LOSE("패"), + PUSH("무승부"); + + private String label; + + GameResult(String label){ + this.label = label; + } + + public String getLabel(){ + return this.label; + } + + public int dealerWinCount(){ + if(this == LOSE) + return 1; + return 0; + } + + public int dealerLoseCount(){ + if(this == WIN) + return 1; + return 0; + } +} diff --git a/src/main/java/domain/result/ResultJudge.java b/src/main/java/domain/result/ResultJudge.java new file mode 100644 index 0000000000..c5b78446d8 --- /dev/null +++ b/src/main/java/domain/result/ResultJudge.java @@ -0,0 +1,23 @@ +package domain.result; + +import domain.participant.Participant; + +public class ResultJudge { + public GameResult checkPlayerGameResult(Participant dealer, Participant player){ + if(player.isBust()) + return GameResult.LOSE; + + if(dealer.isBust()) + return GameResult.WIN; + + if(dealer.getScore() > player.getScore()) + return GameResult.LOSE; + + if(dealer.getScore() < player.getScore()) + return GameResult.WIN; + + return GameResult.PUSH; + } + + +} diff --git a/src/main/java/ui/InputView.java b/src/main/java/ui/InputView.java new file mode 100644 index 0000000000..d78438dc23 --- /dev/null +++ b/src/main/java/ui/InputView.java @@ -0,0 +1,31 @@ +package ui; + +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +public class InputView { + Scanner scanner = new Scanner(System.in); + + public List inputPlayers(){ + System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)"); + String input = scanner.nextLine(); + return Arrays.stream(input.split(",")).map(String::trim).toList(); + } + + public boolean askHit(String playerName){ + String input; + do { + System.out.println(playerName + "는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)"); + input = scanner.nextLine(); + }while(!isValidHitResponse(input)); + return input.trim().equals("y"); + } + + private boolean isValidHitResponse(String response){ + if(response.trim().equals("y")|| response.trim().equals("n")){ + return true; + } + return false; + } +} diff --git a/src/main/java/ui/ResultView.java b/src/main/java/ui/ResultView.java new file mode 100644 index 0000000000..50c9a63924 --- /dev/null +++ b/src/main/java/ui/ResultView.java @@ -0,0 +1,81 @@ +package ui; + +import java.util.List; +import java.util.stream.Collectors; + +import domain.result.GameResult; +import domain.participant.Participant; +import domain.result.ResultJudge; + +public class ResultView { + public void printInitialDeal(List participants){ + String playerNames = name(participants); + System.out.println("딜러와 " + playerNames + "에게 2장을 나누었습니다."); + } + + public void printAllHands(List participants){ + Participant dealer = participants.getFirst(); + printDealerInitialHand(dealer); + List players = participants.subList(1, participants.size()); + for(Participant participant : players){ + printHand(participant); + } + + } + + public void printHand(Participant participant){ + System.out.println(participant.getName() + "카드: " + participant.handDisplay()); + } + + public void printDealerInitialHand(Participant dealer){ + System.out.println(dealer.getName() + "카드: " + dealer.dealerHandDisplay()); + } + + public void printIsBust(){ + System.out.println("해당 플레이어는 Bust로 더이상 카드를 뽑을 수 없습니다."); + } + + public void printDealerDraw(){ + System.out.println("딜러는 16이하라 한장의 카드를 더 받았습니다. \n"); + } + + public void printScores(List participants){ + for(Participant participant : participants){ + System.out.println(participant.getName() + "카드: " + participant.handDisplay() + + " - 결과: " + participant.getScore()); + } + } + + public void printGameResult(List participants, ResultJudge judge){ + System.out.println("\n## 최종 승패"); + Participant dealer = participants.getFirst(); + List players = participants.subList(1, participants.size()); + + printDealerResult(dealer, players, judge); + printPlayerResult(dealer, players, judge); + } + + private String name(List participants){ + return participants.subList(1, participants.size()).stream() + .map(Participant::getName) + .collect(Collectors.joining(", ")); + } + + private void printPlayerResult(Participant dealer, List players, ResultJudge judge){ + for(Participant player : players){ + System.out.println(player.getName() + " : " + judge.checkPlayerGameResult(dealer, player).getLabel()); + } + } + + private void printDealerResult(Participant dealer, List players, ResultJudge judge){ + int win = 0; + int lose = 0; + GameResult result; + for(Participant player : players){ + result = judge.checkPlayerGameResult(dealer, player); + win += result.dealerWinCount(); + lose += result.dealerLoseCount(); + } + System.out.println(dealer.getName() + " : " + win + "승 " + lose + "패"); + } +} diff --git a/src/test/java/card/CardTest.java b/src/test/java/card/CardTest.java new file mode 100644 index 0000000000..7834a5a949 --- /dev/null +++ b/src/test/java/card/CardTest.java @@ -0,0 +1,44 @@ +package card; + +import domain.card.Card; +import domain.card.enums.Rank; +import domain.card.enums.Suit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class CardTest { + @Test + void ACE_카드는_isAce가_true이다(){ + Card card = new Card(Rank.ACE, Suit.SPADE); + + boolean ace = card.isAce(); + + assertThat(ace).isTrue(); + } + + @ParameterizedTest + @EnumSource(value = Rank.class, names = {"JACK", "QUEEN", "KING"}) + void JQK_카드는_10점으로_처리된다(Rank rank){ + Card card = new Card(rank, Suit.SPADE); + + int score = card.score(); + + assertThat(score).isEqualTo(10); + } + + @ParameterizedTest + @CsvSource({ + "TWO, 2", "THREE, 3", "FOUR, 4", "FIVE, 5", "SIX, 6", + "SEVEN, 7", "EIGHT, 8", "NINE, 9", "TEN, 10" + }) + void 숫자_카드는_숫자_그대로_점수가_처리된다(Rank rank, int expectedScore) { + Card card = new Card(rank, Suit.SPADE); + + assertThat(card.score()).isEqualTo(expectedScore); + } +} diff --git a/src/test/java/deck/DeckTest.java b/src/test/java/deck/DeckTest.java new file mode 100644 index 0000000000..d2fe116c84 --- /dev/null +++ b/src/test/java/deck/DeckTest.java @@ -0,0 +1,46 @@ +package deck; + +import domain.card.Card; +import domain.deck.Deck; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DeckTest { + + @Test + void 카드를_뽑으면_첫_번째_카드를_반환한다(){ + Deck deck = new Deck(); + Card firstCard = deck.firstCard(); + + Card card = deck.draw(); + + assertThat(card).isEqualTo(firstCard); + } + + @Test + void 카드를_섞으면_카드의_구성은_같고_순서는_달라진다(){ + Deck deck = new Deck(); + List beforeShuffle = deck.cards(); + + deck.shuffle(); + List afterShuffle = deck.cards(); + + assertThat(afterShuffle).containsExactlyInAnyOrderElementsOf(beforeShuffle); + assertThat(afterShuffle).isNotEqualTo(beforeShuffle); + } + + @Test + void 더이상_뽑을_수_있는_카드가_없다면_새로운_덱을_준비한다(){ + Deck deck = new Deck(); + for(int i = 0; i < 52; i++){ + deck.draw(); + } + + Card card = deck.draw(); + + assertThat(card).isNotNull(); + } +} diff --git a/src/test/java/hand/HandTest.java b/src/test/java/hand/HandTest.java new file mode 100644 index 0000000000..94dd5700df --- /dev/null +++ b/src/test/java/hand/HandTest.java @@ -0,0 +1,86 @@ +package hand; + +import domain.card.Card; +import domain.card.enums.Rank; +import domain.card.enums.Suit; +import domain.hand.Hand; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HandTest { + + @Test + void 카드점수의_합계가_21이하면_버스트가_아니다(){ + Hand hand = new Hand(); + Card card1 = new Card(Rank.QUEEN, Suit.SPADE); + Card card2 = new Card(Rank.QUEEN, Suit.HEART); + hand.add(card1); + hand.add(card2); + + boolean bust = hand.isBust(); + + assertThat(bust).isFalse(); + } + + @Test + void 카드점수의_합계가_22이상이면_버스트다(){ + Hand hand = new Hand(); + Card card1 = new Card(Rank.QUEEN, Suit.SPADE); + Card card2 = new Card(Rank.QUEEN, Suit.HEART); + Card card3 = new Card(Rank.SIX, Suit.HEART); + hand.add(card1); + hand.add(card2); + hand.add(card3); + + boolean bust = hand.isBust(); + + assertThat(bust).isTrue(); + } + + @Test + void ACE를_11점으로_계산해도_21을_넘지않으면_ACE는_11점으로_계산된다(){ + Hand hand = new Hand(); + Card card1 = new Card(Rank.FIVE, Suit.SPADE); + Card card2 = new Card(Rank.FIVE, Suit.HEART); + Card card3 = new Card(Rank.ACE, Suit.HEART); + hand.add(card1); + hand.add(card2); + hand.add(card3); + + int score = hand.score(); + + assertThat(score).isEqualTo(21); + } + + @Test + void ACE를_제외한_합계가_11이상이면_ACE는_1점으로_계산된다(){ + Hand hand = new Hand(); + Card card1 = new Card(Rank.THREE, Suit.SPADE); + Card card2 = new Card(Rank.EIGHT, Suit.HEART); + Card card3 = new Card(Rank.ACE, Suit.HEART); + hand.add(card1); + hand.add(card2); + hand.add(card3); + + int score = hand.score(); + + assertThat(score).isEqualTo(12); + } + + @Test + void ACE가_여러_장이면_21을_넘지_않는_가장_큰_점수로_계산된다(){ + Hand hand = new Hand(); + Card card1 = new Card(Rank.ACE, Suit.SPADE); + Card card2 = new Card(Rank.ACE, Suit.HEART); + Card card3 = new Card(Rank.NINE, Suit.HEART); + hand.add(card1); + hand.add(card2); + hand.add(card3); + + int score = hand.score(); + + assertThat(score).isEqualTo(21); + } + +} diff --git a/src/test/java/participant/ParticipantTest.java b/src/test/java/participant/ParticipantTest.java new file mode 100644 index 0000000000..0885ed0d38 --- /dev/null +++ b/src/test/java/participant/ParticipantTest.java @@ -0,0 +1,42 @@ +package participant; + +import domain.card.Card; +import domain.card.enums.Rank; +import domain.card.enums.Suit; +import domain.participant.Dealer; +import domain.participant.Participant; +import domain.participant.Player; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +public class ParticipantTest { + + @Test + void 딜러는_현재_들고있는_카드의_합계가_16이하이면_canReceiveCard가_true이다(){ + Participant dealer = new Dealer("딜러"); + Card card = new Card(Rank.TWO, Suit.CLUB); + Card card2 = new Card(Rank.QUEEN, Suit.CLUB); + dealer.receive(card); + dealer.receive(card2); + + boolean hit = dealer.canReceiveCard(); + + assertThat(hit).isTrue(); + } + + @Test + void 플레이어는_현재_들고있는_카드의_합계가_21미만이면_canReceiveCard가_true이다(){ + Participant player = new Player("player1"); + Card card = new Card(Rank.TWO, Suit.CLUB); + Card card2 = new Card(Rank.QUEEN, Suit.CLUB); + player.receive(card); + player.receive(card2); + + boolean hit = player.canReceiveCard(); + + assertThat(hit).isTrue(); + } + + + +}