Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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, 아니라면 멈춘다.

## 게임 종료
- [ ] 참가자의 카드 패와 결과 출력
- [ ] 결과를 바탕으로 각 참가자들의 승패 출력
- 점수가 같을 때를 고려
10 changes: 10 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
119 changes: 119 additions & 0 deletions src/main/java/controller/GameController.java
Original file line number Diff line number Diff line change
@@ -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<Participant> 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<Participant> participants, Deck deck){
Participant dealer = participants.getFirst();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

딜러와 플레이어 목록을 하나의 List<Participant>로 묶은 이유는 '동일하게 카드를 받는 참가자'라는 공통점을 활용하기 위한 선택이었을까요?

제안: Participants 또는 BlackjackGame 같은 객체가 Dealer dealer, List<Player> players를 명시적으로 보유하고, 공통 순회가 필요할 때만 allParticipants()를 제공하는 구조는 어떨까요? 그러면 딜러 위치 불변식을 한 곳에서 관리할 수 있습니다.

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())){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

플레이어가 더 이상 카드를 받을 수 없는 상태가 되면 입력을 다시 묻지 않고 턴을 종료하는 흐름을 의도하신 걸까요?

제안: while (player.canReceiveCard() && inputView.askHit(player.getName()))처럼 턴 지속 조건에 점수 상태를 포함하거나, PlayerTurn 같은 도메인/서비스 객체가 한 턴의 종료 조건을 판단하도록 분리해보면 어떨까요? 또한 카드를 받은 직후 Bust 여부를 확인해 즉시 안내하고 반복을 종료하는 테스트를 추가하면 좋겠습니다.

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<Participant> joinParticipant(){
Participant dealer = new Dealer("딜러");
List<String> playerNames;
List<Participant> 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<Participant> 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());
}
}
}
26 changes: 26 additions & 0 deletions src/main/java/domain/card/Card.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package domain.card;

import domain.card.enums.Rank;
import domain.card.enums.Suit;

public class Card {
private Rank rank;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

카드를 불변 객체로 두려는 의도였다면 필드에도 그 의도를 표현해보는 것은 어떨까요?

제안: private final Rank rank;, private final Suit suit;로 선언해 카드가 생성 후 변하지 않음을 명시해보면 어떨까요? 같은 맥락에서 GameResult.labelfinal로 둘 수 있습니다.

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();
}
Comment on lines +23 to +25

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Card가 자신의 표현 방식까지 책임지는 것이 자연스러울까요?

}
34 changes: 34 additions & 0 deletions src/main/java/domain/card/enums/Rank.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
18 changes: 18 additions & 0 deletions src/main/java/domain/card/enums/Suit.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
50 changes: 50 additions & 0 deletions src/main/java/domain/deck/Deck.java
Original file line number Diff line number Diff line change
@@ -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<Card> cards = new ArrayList<>();

public Deck() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

덱의 생성과 셔플을 한 번에 묶은 것은 '게임 시작 전 항상 섞인 덱'이라는 요구사항을 강제하기 위한 의도였을까요?

제안: 운영용으로는 Deck.shuffled() 같은 팩토리를 두고, 테스트용으로는 카드 목록을 주입할 수 있는 생성자를 제공하는 방식은 어떨까요? shuffle()이 외부에서 꼭 필요하지 않다면 접근 범위를 줄이는 것도 고려해볼 수 있습니다.

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()){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

카드가 부족한 상황을 게임 진행 불가 상태로 다루려는 의도였을까요, 아니면 자동으로 새 덱을 준비하는 것을 룰로 보신 걸까요?

제안: draw()에서 카드가 없으면 예외를 던지거나, Deck.hasNext()/remainingCount()를 제공해 게임 진행 계층에서 명시적으로 처리해보면 어떨까요? 테스트도 '덱이 비면 새 덱 생성'보다 '덱이 비면 draw 불가' 또는 '카드 존재 여부를 확인한다'로 요구사항에 맞춰 조정하는 편이 안전해 보입니다.

createdDeck();
this.shuffle();
}
return cards.removeFirst();
}

public void shuffle(){
Collections.shuffle(cards);
}

public List<Card> cards(){
return List.copyOf(cards);
}
}
56 changes: 56 additions & 0 deletions src/main/java/domain/hand/Hand.java
Original file line number Diff line number Diff line change
@@ -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<Card> 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

만약 블랙잭이 21이 아니라 22가 된다면, 몇 군데를 수정해야 할까요?
의미있는 값으로 나타낼 수는 없을까요?

}

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;
}
}
12 changes: 12 additions & 0 deletions src/main/java/domain/participant/Dealer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package domain.participant;

public class Dealer extends Participant{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

추상 클래스는 어떤 단점이 있을까요?

public Dealer(String name){
super(name);
}

@Override
public boolean canReceiveCard(){
return checkScore() <= 16;
}
}
Loading