-
Notifications
You must be signed in to change notification settings - Fork 6
[블랙잭 게임 1단계] 리뷰 요청 드립니다. #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: chunghyeon-kim
Are you sure you want to change the base?
Changes from 43 commits
cd61a5b
9a8ad41
49385ff
5850dd0
f5e6914
c43c521
55f99ee
0d0b13c
e9ed25c
2b3f5c5
a6c12c5
b2198e8
5ec0118
f0cbf79
28c5a8d
ac18db0
b1e1a25
7b7ed47
8dab5e0
f5829e0
f0ec5f4
2cc96f3
e99f18b
9bfe228
02ba420
02b8932
6765478
d7e50f4
33dc3e5
56880bb
f3f0d12
3260c5d
b71d341
6f547a3
1177903
74e1443
3aa80e6
22f26dd
68db2ce
aa1ab86
e5a2991
4eed0a8
b21caf0
61696cb
b00b293
dc99787
8cd41f3
634acab
3818a06
49d48e7
080bc70
4b3f64b
0aebc8c
bfe22bb
13b9421
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,45 @@ | ||
| # java-blackjack | ||
|
|
||
| # 구현 기능 목록 | ||
|
|
||
| -[x] 게임 | ||
| - [x] 플레이어들의 이름을 입력받는다. | ||
| - [x] 최소 플레이어는 1명으로, 딜러와 게임을 진행한다. | ||
| - [x] ,를 기준으로 분리 | ||
|
|
||
| - [x] 시작시 각 플레이어가 두 장의 카드를 지급받는다. | ||
| - [x] 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 뽑는다. | ||
| - [x] 딜러는 처음에 받은 2장의 합계가 17이상이면 추가로 받을 수 없다. | ||
| - [x] 플레이어는 카드 숫자의 합이 21이 초과하지 않는 동안 원하는 만큼 카드를 뽑을 수 있다. | ||
| - [x] 카드를 추가로 뽑아 21을 초과할 경우 배팅 금액을 모두 잃게 된다. | ||
| - [x] Ace를 가진 플레이어는 에이스의 숫자값으로 1 혹은 11을 선택할 수 있다. | ||
| - [x] King, Queen, Jack의 숫자값은 각각 10으로 취급한다. | ||
|
|
||
| -[x] 출력 | ||
| - [x] 게임 시작 시 각 플레이어가 받은 2장의 카드를 출력한다. | ||
| - [x] 각 플레이어에게 추가로 카드를 받을 것인지 묻는다. | ||
| - [x] 카드를 추가로 받으면 해당 플레이어가 가진 카드 목록을 출력한다. | ||
| - [x] 딜러는 카드값의 합이 16이하면 카드를 추가로 받았다는 메시지를 출력한다. | ||
|
|
||
| - [x] 모든 플레이어가 더이상 카드를 뽑지 않으면 게임이 종료된다. | ||
| - [x] 플레이어별 카드의 목록을 출력한다. | ||
| - [x] 플레이어별 카드 숫자의 합을 계산하여 결과로 출력한다. | ||
|
|
||
| - [x] 게임을 완료한 후 각 플레이어별로 승패를 출력한다. | ||
| - [x] 딜러는 각 플레이어와의 승패를 누적해서 출력한다. | ||
| - [x] 플레이어는 딜러와의 승패를 출력한다. | ||
|
|
||
|
|
||
| - [x] 예외 | ||
| - 이름 | ||
| - [x] 플레이어의 이름은 공백, null 값이 될 수 없다. | ||
| - [x] 플레이어의 이름은 특수문자를 포함할 수 없다. | ||
| - [x] 플레이어의 이름은 "딜러"가 될 수 없다. | ||
| - [x] 플레이어는 서로 중복된 이름을 가질 수 없다. | ||
|
|
||
| - 카드 덱 | ||
| - [x] 하나의 카드덱에서는 카드를 52장 초과하여 뽑을 수 없다. | ||
|
|
||
| - 플레이어에게 카드를 더 받을 것인지 질문 | ||
| - [x] y, n가 아닌 형태로 대답할 수 없다. | ||
| - [x] 대문자 Y, N도 올바른 답으로 인식한다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| domain | ||
|
|
||
| - Card, Deck, Player, Dealer, Game, Statistics | ||
| - Game : 카드 분배 역할 (카드 분배 조건도 이 객체에서 판단) | ||
| - Statistics : 카드분배가 완료된 후 점수계산 및 승패 결과 도출 | ||
|
|
||
| View | ||
|
|
||
| - 플레이어 이름을 입력 받고 결과를 출력 | ||
|
|
||
| Controller | ||
|
|
||
| - 게임 전체 흐름 제어 (입력, 게임실행, 출력) | ||
|
|
||
| ### Enum | ||
|
|
||
| 1. 게임컨트롤러 (카드분배- 질문) | ||
| - 답(YES, NO) | ||
|
|
||
| 2. 게임결과 (결과 승패를 판단) | ||
| - 승(플레이어 점수, 딜러점수, 승(플), 패(딜러)) | ||
| - 패(플레이어 점수, 딜러점수, 패(플), 승(딜러)) | ||
| - 무승부(플레이어 점수, 딜러점수, 무(플,딜러)) | ||
|
|
||
| domain | ||
|
|
||
| - Card, Deck, Player, Dealer, Score(계산), GameResult(승패도출) | ||
| - Answer(질문답), Results(승패무) | ||
|
|
||
| View | ||
|
|
||
| - inputview: 입력받는 것(질문에 대한 답을 받는 부분) | ||
| - outputview: 출력하는 것 | ||
|
|
||
| Controller | ||
|
|
||
| - 카드를 분배(drawCard), 더 받을지 질문, 하나카드를 분배... | ||
| - 흐름제어 | ||
|
|
||
| # Ace 카드 처리 로직 | ||
|
|
||
| Player -> 21 한계치 Dealer -> 17 한계치 | ||
|
|
||
| 모든 게임스코어합은 -> 21이 게임의 종료 지점 | ||
|
|
||
| Score 계산 && 카드를 더 뽑게할지말지 | ||
|
|
||
| - 카드의 Score가 계산로직으로 21(Player) 17(Dealer) 되기전까지 카드를 더 뽑을지 질문 | ||
|
|
||
| ## 계산로직 | ||
|
|
||
| 1. 카드 목록에 포함된 에이스 카드의 숫자를 추출한다. -> n | ||
| 2. n+1의 길이를 가지는 배열을 생성하고, 에이스 카드를 1로 계산한 카드합과 거기에 차례대로 10을 더한 값을 배열에 입력한다. | ||
| 3. 배열의 숫자들 중 21을 초과하지 않으면서 가장 큰 값을 취한다. | ||
|
|
||
| ex) Ace, Ace, 8일 경우 int[3] = {10, 20, 30} -> 20을 숫자합으로 계산 | ||
|
|
||
| ### 게임종료 | ||
|
|
||
| 카드를 한 장 뽑을 때마다 모든 경우의 수를 계산. Ace를 1로, 혹은 11로 계산하는 경우의 수를 다 생각해서 모든 경우의 수 중 카드합의 최소값이 한계치를 넘는 경우에 카드를 그만 뽑도록. | ||
|
|
||
| Ace 7, 9 | ||
|
|
||
| 27을 취한다. 카드를 더 못뽑음. | ||
|
|
||
| 17 | ||
|
|
||
| Ace, 7, 9, 4 | ||
|
|
||
| -> 21 | ||
|
|
||
| ## 카드 뽑기 | ||
|
|
||
| <딜러> | ||
| 처음에 2장의 카드를 받고 카드합이 16 이하이면 한 장을 더 뽑는다. 에이스 1장은 11로 계산하고 나머지 에이스는 1로 계산한다. -> 에이스가 포함되어 있을 경우 카드합에 10을 더해준다 | ||
|
|
||
| <에이스가 포함되어 있지 않을 경우> | ||
|
|
||
| 카드합이 16 이하이면 카드를 뽑음. | ||
|
|
||
| <에이스가 포함되어 있는 경우> | ||
|
|
||
| 카드합에 10을 더한 값이 16 이하이면 카드를 뽑음. | ||
|
|
||
| ## 카드 뽑기 및 에이스가 여러개 포함될 가능성이 있는 카드덱의 합 계산 | ||
|
|
||
| // 카드값의 합(ace가 11) | ||
| int scoreOfAceAsEleven; // 에이스의 개수 int aceCount; // Ace카드값의 차이 int DiFFERENCE_OF_ACE_VALUE = 10; | ||
|
|
||
| calcaluateResult(){ | ||
| while (canCountAceAsOne) { | ||
| 카드값의 합 scoreOfAceAsEleven = scoreOfAceAsOne(scoreOfAsAsEleven); | ||
| aceCount--; | ||
| } | ||
|
|
||
| return scoreOfAceAsEleven; | ||
| } | ||
|
|
||
| boolean canCountAceAsOne(int 카드값의 합, 에이스의 개수){ | ||
| 에이스가 하나라도 있음 > 0 && 카드 합이 21(상수선언)을 넘음(ace를 1로 취급가능)} | ||
| } | ||
|
|
||
| int scoreOfAceAsOne(int 카드값의 합){ | ||
| (ace를 11로 취급한) scoreOfAceAsEleven 카드값의 합 - 10 | ||
| } | ||
|
|
||
| - 17 dealer = threshould | ||
|
|
||
| 컨트롤러에서 딜러에게 boolean Drawble(){ return calculateResult() < 17 } drawCard() | ||
|
|
||
| - 21 player 컨트롤러에서 플레이어에게 더 받을건지 묻는 질문 boolean 플레이어의 답 Yes && Drawable() { return calculateResult() < 21 } drawCard() | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package blackjack; | ||
|
|
||
| import blackjack.controller.BlackJackController; | ||
|
|
||
| public class BlackjackApplication { | ||
|
|
||
| public static void main(String[] args) { | ||
| try { | ||
| new BlackJackController().run(); | ||
| } catch (Exception e) { | ||
| e.printStackTrace(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package blackjack.controller; | ||
|
|
||
| import blackjack.domain.card.Deck; | ||
| import blackjack.domain.participant.Dealer; | ||
| import blackjack.domain.participant.Player; | ||
| import blackjack.domain.participant.PlayersFactory; | ||
| import blackjack.domain.result.GameResult; | ||
| import blackjack.dto.DrawCardResponseDTO; | ||
| import blackjack.dto.PlayersNameInputDTO; | ||
| import blackjack.utils.StringUtil; | ||
| import blackjack.view.InputView; | ||
| import blackjack.view.OutputView; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class BlackJackController { | ||
| private final List<Player> players; | ||
| private final Dealer dealer; | ||
| private final Deck deck; | ||
| private final InputView inputView = new InputView(); | ||
| private final OutputView outputView = new OutputView(); | ||
|
|
||
| public BlackJackController() { | ||
| PlayersNameInputDTO namesInput = inputView.getPlayersName(); | ||
| this.players = PlayersFactory.createPlayers(namesInput.getPlayersName()); | ||
| this.dealer = new Dealer(); | ||
| this.deck = new Deck(); | ||
| } | ||
|
|
||
| public void run() { | ||
| drawTowCards(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메소드 분리가 잘 되어 있어서, 가독성이 좋네요🙂 |
||
| drawCardToPlayers(); | ||
| drawCardToDealer(); | ||
| outputView.printCardsResult(dealer, players); | ||
| outputView.printGameResult(GameResult.of(dealer, players)); | ||
| } | ||
|
|
||
| private void drawTowCards() { | ||
| for (Player player : players) { | ||
| player.receiveCard(deck.drawCard()); | ||
| player.receiveCard(deck.drawCard()); | ||
| } | ||
| dealer.receiveCard(deck.drawCard()); | ||
| dealer.receiveCard(deck.drawCard()); | ||
| outputView.printFirstCardsGiven(players, dealer); | ||
| outputView.printDealerCard(dealer); | ||
| outputView.printPlayersCard(players); | ||
| } | ||
|
|
||
| private void drawCardToPlayers() { | ||
| for (Player player : players) { | ||
| drawCardToPlayer(player); | ||
| } | ||
| } | ||
|
|
||
| private void drawCardToPlayer(Player player) { | ||
| DrawCardResponseDTO drawCardResponse = inputView.getPlayersResponse(player); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DTO도 자신이 가지고 있는 데이터에 대해서, 어떤 행위/판단을 할 수 있다고 생각해요. 입력받은 값이 Y/N인지 판단하는 로직은 DTO로 가도 된다고 생각합니다. 다만 해당 로직이 여러 곳에서 사용되거나, 이를 하나의 도메인 행위로 정의한다면 DTO -> Domain으로 전환한 다음 도메인에서 판단하는 것도 좋을 것 같아요. 현재는 해당 부분에 대한 도메인 로직이 Controller에 나와 있기에 원하시는 형태로 포장해주시면 좋을 것 같습니다🙂 추가로 네이밍은 팀바팀이고 사바사지만, 개인적으로 프로그램에게 넘어온 Input을 담는 DTO는 Request/우리 프로그램에서 반환해주는DTO는 Response로 사용하고 있어요. response로 작성해주신 맥락은 알겠으나, Client-Server가 항상 유동적으로 달라질 수 있다면 DTO가 많아졌을 때 헷갈릴 수 있다고 생각해요🙂 (추가로 DTO는 > Dto로 적고 있긴합니다 - 취향)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dto 이름을 DrawCardRequestDto로 변경하고 판단로직을 Dto로 이동했습니다. Dto 이름을 받아온 것인지, 반환하는 것인지에 따라 통일하면 일관성이 있어 헷갈릴 일이 없어서 좋은 방법인 것 같습니다. |
||
| while (player.drawable() && isYes(drawCardResponse)) { | ||
| player.receiveCard(deck.drawCard()); | ||
|
|
||
| outputView.printCards(player); | ||
| drawCardResponse = inputView.getPlayersResponse(player); | ||
| } | ||
| } | ||
|
|
||
| private boolean isYes(DrawCardResponseDTO drawCardResponse) { | ||
| String response = drawCardResponse.getResponse().trim(); | ||
| StringUtil.validateYesOrNo(response); | ||
| return response.equalsIgnoreCase("y"); | ||
| } | ||
|
|
||
| private void drawCardToDealer() { | ||
| while (dealer.drawable()) { | ||
| dealer.receiveCard(deck.drawCard()); | ||
| outputView.printDealerCardGiven(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package blackjack.domain.card; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| @Getter | ||
| public class Card { | ||
|
|
||
| private final Denomination denomination; | ||
| private final Type type; | ||
|
|
||
| public Card(Denomination denomination, Type type) { | ||
| this.denomination = denomination; | ||
| this.type = type; | ||
| } | ||
|
|
||
| public boolean isAce() { | ||
| return denomination.equals(Denomination.ACE); | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (this == o) { | ||
| return true; | ||
| } | ||
| if (!(o instanceof Card)) { | ||
| return false; | ||
| } | ||
| Card card = (Card) o; | ||
| return denomination == card.denomination && | ||
| type == card.type; | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(denomination, type); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return denomination.getDenominationName() + type.getType(); | ||
|
|
||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package blackjack.domain.card; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
| import java.util.Random; | ||
|
|
||
| public class Deck { | ||
|
|
||
| private static final Random RANDOM = new Random(); | ||
| private static final String ALERT_NO_CARD_LEFT = "사용 가능한 카드를 모두 소진하였습니다."; | ||
|
|
||
| @Getter | ||
| private final List<Card> deck; | ||
|
|
||
| public Deck() { | ||
| this.deck = new LinkedList<>(DeckFactory.createDeck()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LinkedList로 선언하신 이유는 카드를 한장씩 삭제할 때 ArrayList가 비효율적이라서 그렇게 하신걸까요? 👏
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 그렇습니다. |
||
| } | ||
|
|
||
| public Card drawCard() { | ||
| if (deck.isEmpty()) { | ||
| throw new RuntimeException(ALERT_NO_CARD_LEFT); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재는 경기도중에 카드가 모두 소진되면, 프로그램이 종료되는 구조인데요. 일반적으로 경기를 하다, 카드가 모두 소진된다고 그 게임이 무효가 되지 않듯이 조금 더 구체적인 방법/구현을 고민해보면 좋을 것 같아요. (구현은 하지 않으셔도 됩니다) 도메인에 대해서 조금 더 능동적으로 이해하면 어떨까 라는 의미에서 드린 피드백입니다. (참고로 블랙잭에서는 참가 인원을 제한하고 있더라구요🙂) |
||
| } | ||
| return deck.remove(generateRandomNumber()); | ||
| } | ||
|
|
||
| private int generateRandomNumber() { | ||
| return RANDOM.nextInt(deck.size()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Random한 값을 테스트하기 위해서, 레이싱카에서 했던 방식으로 이를 외부에서 주입받는 형태로 하면 어떨까요?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DrawStrategy 인터페이스를 만들고 그것을 구현하는 RandomDrawStrategy 클래스를 만들었습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 외부에서 해당 값을 주입 받는다면 조금 더 좋을 것 같습니다🙂 |
||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package blackjack.domain.card; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public class DeckFactory { | ||
|
|
||
| private static List<Card> cardsDeck = createCardsDeck(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스태틱하게 어플리케이션이 시작됨과 동시에 생긴다는 것을 명시적으로 하기 위해서 static { } 형태로 초기화를 할 수도 있습니다🙂
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스태틱블록으로 변경했습니다. 기능은 등호를 사용한 코드와 정확히 동일한건가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 구조에서 기능적으로는 완전히 동일하구요, 실행순서는 기존에 작성하셨던 등호를 사용한 코드가 먼저 호출되네요🙂 의미를 조금 더 드러낼 수 있는 형태인 것 같아서 말씀드렸습니다. |
||
|
|
||
| private static List<Card> createCardsDeck() { | ||
| List<Card> cards = new ArrayList<>(); | ||
|
|
||
| for (Denomination denomination : Denomination.values()) { | ||
| cards.addAll(createCards(denomination)); | ||
| } | ||
| return cards; | ||
| } | ||
|
|
||
| private static List<Card> createCards(Denomination denomination) { | ||
| List<Card> cards = new ArrayList<>(); | ||
|
|
||
| for (Type type : Type.values()) { | ||
| cards.add(new Card(denomination, type)); | ||
| } | ||
|
|
||
| return cards; | ||
| } | ||
|
|
||
| public static List<Card> createDeck() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메소드의 네이밍으로 봤을 때 혹시 이 메소드를 사용하는 사람 입장에서는 |
||
| return Collections.unmodifiableList(cardsDeck); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Controller를 Entry-point로써 사용하고 있는데, 현재와 같이 해당 값들을 이를 멤버변수로 가지게 되면 해당 게임을 여러명이 동시에 요청하는 경우 데이터 부정합이 발생할 수 있어요. 멤버변수를 지역변수로 가져가는 것이 좋아 보여요🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여러명이 동시에 요청하는 경우를 고려하지 않았네요. 서비스 개발자로서 항상 생각해야 할 부분인 것 같습니다.
view를 제외한 클래스변수들을 로컬변수로 수정했습니다.