블랙잭 게임

Jaca·2021년 11월 17일
0

블랙잭 게임

우테코 이전 기수 프리코스를 풀다가 좋은 글을 찾았다.

순수 Java로 이루어진 프로젝트 를 보고 한번 해봐야겠다고 다짐하고 바로 시작했다.

블랙잭 규칙

  • 딜러와 게이머 단 2명만 존재한다.
  • 카드는 조커를 제외한 52장이다. (즉, 카드는 다이아몬드,하트,스페이드,클럽 무늬를 가진 A,2~10,K,Q,J 으로 이루어져있다.)
  • 2~10은 숫자 그대로 점수를, K/Q/J는 10점으로, A는 1로 계산한다. (기존 규칙은 A는 1과 11 둘다 가능하지만 여기선 1만 허용하도록 스펙아웃)
  • 딜러와 게이머는 순차적으로 카드를 하나씩 뽑아 각자 2개의 카드를 소지한다.
  • 게이머는 얼마든지 카드를 추가로 뽑을 수 있다.
  • 딜러는 2카드의 합계 점수가 16점 이하이면 반드시 1장을 추가로 뽑고, 17점 이상이면 추가할 수 없다.
  • 양쪽다 추가 뽑기 없이, 카드를 오픈하면 딜러와 게이머 중 소유한 카드의 합이 21에 가장 가까운 쪽이 승리한다.
  • 단 21을 초과하면 초과한 쪽이 진다.

모델

Card.java

public class Card {
    private Pattern pattern;
    private Denomination denomination;

    public Card(Pattern pattern, Denomination denomination) {
        this.pattern = pattern;
        this.denomination = denomination;
    }

    public enum Pattern {
        ...
    }

    public enum Denomination {
        ...
    }

    public String toString() {
        return "Card = " + pattern + " " + denomination.mark + "\n";
    }

    public int getPoint() {
        return denomination.point;
    }
}

CardDeck.java

public class CardDeck {
    private final Stack<Card> cardDeck;

    public CardDeck() {
        this.cardDeck = generateCard();
        Collections.shuffle(this.cardDeck);
    }

    private Stack<Card> generateCard() {
        Stack<Card> cards = new Stack<>();

        for (Card.Pattern pattern : Card.Pattern.values()) {
            for (Card.Denomination denomination : Card.Denomination.values()) {
                Card card = new Card(pattern, denomination);
                cards.push(card);
            }
        }

        return cards;
    }

    public Card drawCard() {
        return cardDeck.pop();
    }
}

enum을 통해 카드의 속성을 지정해주고,
CardDeck을 통해 52장의 카드를 만들어서 Stack에 넣는다.

Stack인 이유는 카드를 뽑으면 그 카드의 정보를 가져오고 덱에서 그 카드를 삭제해줘야하는데 그 로직을 Stack의 pop으로 간단히 해결 가능하다.

Collections.shuffle() 메서드를 처음 알게되었다.

간단히 보자면,

public static void shuffle(List<?> list) { ... }

public static void shuffle(List<?> list, Random rnd) { ... }

이 두가지 메서드가 있는데, Random 시드값을 지정해줄 수 있다.
지정해주지 않으면 랜덤 시드값을 생성해 섞는다.

enum의 장점은 enum.value() 옵션을 사용하면 내가 설정한 값들만 가져오기 때문에 입력 값의 제한이 자연스럽게 걸리게 된다.

하지만 저 2중 For문을 람다로 바꾸고 싶었지ㅣㅣ만,,, 한번 찾아봐야겠다.
스트림은 좀 되는데 람다는 어렵다 ㅠ

Dealer.java

public class Dealer {
    private final List<Card> dealerHand = new ArrayList<>();
    private int point = 0;

    public void drawingCard(Card card) {
        dealerHand.add(card);
        point += card.getPoint();
    }

    public boolean mustDraw() {
        return point < 17;
    }

    public boolean isBust() {
        return point > 21;
    }

    public String showHand() {
        StringBuilder stringBuilder = new StringBuilder();
        dealerHand.stream().map(Card::toString).forEach(stringBuilder::append);
        return stringBuilder.toString();
    }

    public List<Card> getDealerHand() {
        return dealerHand;
    }

    public int getPoint() {
        return point;
    }
}

Gamer.java

public class Gamer {
    private final List<Card> GamerHand = new ArrayList<>();
    private int point = 0;

    public void drawingCard(Card card) {
        GamerHand.add(card);
        point += card.getPoint();
    }

    public String showHand() {
        StringBuilder stringBuilder = new StringBuilder();
        GamerHand.stream().map(Card::toString).forEach(stringBuilder::append);
        return stringBuilder.toString();
    }

    public boolean isBust() {
        return point > 21;
    }

    public List<Card> getGamerHand() {
        return GamerHand;
    }

    public int getPoint() {
        return point;
    }
}

게이머와 딜러를 보면, 많은 메서드가 중복이 된다.
그래서 인터페이스를 구현하는 쪽으로 만드는 것이 맞았을 것 같다.
그리고 각 플레이어가 자신의 점수를 계산해서 버스트 여부를 체크하는데
이게 너무 과한 역할을 부여한 건가 싶기도 했다.

컨트롤러

GameController.java

public class GameController {
    private final Game game;

    private boolean gameEndContion = false;

    public GameController() {
        CardDeck cardDeck = new CardDeck();
        Gamer gamer = new Gamer();
        Dealer dealer = new Dealer();
        InputView inputView = new InputView();

        game = new Game(gamer, dealer, inputView, cardDeck);
    }

    public void run() {
        gameEndContion = game.firstDraw();

        while(!gameEndContion) {
            if(game.askStay()) break;
            gameEndContion = game.turnPlay();
        }

        String s = game.makeResult();
        OutputView.winningMessage(s);
    }
}

이번 프로젝트에서 신경 쓴 것은 게임의 흐름을 제어할 메인 컨트롤러에서 필요한 객체와 의존 관계 설정을 모두 마쳐주고, 컨트롤러는 흐름만 제어할 뿐 게임에 필요한 로직은 다른 객체에게 역할을 위임하고자 했다.

Game.java

public class Game {
    private final Gamer gamer;
    private final Dealer dealer;
    private final InputView inputView;
    private final CardDeck cardDeck;

    public Game(Gamer gamer, Dealer dealer, InputView inputView, CardDeck cardDeck) {
        this.gamer = gamer;
        this.dealer = dealer;
        this.inputView = inputView;
        this.cardDeck = cardDeck;
    }

    public boolean firstDraw() {
        for (int i = 0; i < 2; i++) {
            gamer.drawingCard(drawCard());
            dealer.drawingCard(drawCard());
        }

        return endGameCondition();
    }

    public boolean turnPlay() {
        gamerTurn();
        dealerTurn();

        return endGameCondition();
    }

    public boolean askStay() {
        return inputView.inputDrawCardOpt() == 2;
    }

    private void dealerTurn() {
        if (dealer.mustDraw()) dealer.drawingCard(drawCard());
    }

    private void gamerTurn() {
        gamer.drawingCard(drawCard());
    }

    private Card drawCard() {
        return cardDeck.drawCard();
    }

    private boolean endGameCondition() {
        return gamer.isBust() || dealer.isBust();
    }

    public String makeResult() {
        GameResult gameResult = new GameResult(gamer.getPoint(), dealer.getPoint());
        if (gameResult.gamerBust() && gameResult.dealerBust()) return "DRAW";
        if (gameResult.gamerBust()) return "gamer";
        if (gameResult.dealerBust()) return "dealer";
        if (gameResult.isDrawGame()) return "draw";
        if (gameResult.getWinner()) return "dealer";

        return "gamer";
    }
}

그래서 게임을 진행할 때 사용될 로직들을 가지고 있는 Game 객체를 만들었는데, private 메서드가 많아지면 클래스 객체의 분리를 고려해야한다고 들었던 것이 기억났다.

그래서 메서드를 분리하고자 해서 메서드의 성격을 보자하니,
이 구조를 설계할 때 딜러와 게이머는 오로지 플레이어이고 게임의 진행은 제 3의 진행자가 있는 것 처럼 하고자 했다.

실제 게임 같았으면 카드를 뽑거나 게임의 진행을 모두 딜러가 진행하겠지만 설계적인 측면에서 좋아보이지 않았다.

그래서 Game에 카드를 뽑는 로직을 넣어줬던 것인데,
게임의 진행과 카드를 뽑는 것이 너무 많은 역할이 Game에게 있어서 코드가 이렇게 길어졌나 싶어 카드의 분배만을 책임질 Shuffler 객체를 생성하고자 했다.

그런데 내가 짠 로직은 CardDeck으로부터 카드만 전달 받고 각 플레이어에게 카드를 준다.
하지만 카드를 그냥 뽑는 것이 아니다.
딜러는 16이 넘는지, 플레이어는 카드를 더 받을 것인지를 체크해야한다.
이것을 오로지 카드 분배만 하는 Shuffler 객체에게 넘겨주게 되는 것 같아서 너무 많은 곳에 모델들과 의존 관계가 생기고 결국 또 View를 통해 입출력을 해야하니 Shuffler의 존재가 oop와 너무 멀다고 느껴졌다.

결론은?

그런데 문제는.
게임의 결과 계산을 구현 하지 못했다.

내 코드는 카드를 뽑을 때 마다 버스트한 플레이어가 있는지 체크한다.
버스트한 사람이 있다면 게임이 끝난다.

둘다 버스트하기전,
게이머가 카드를 받지 않겠다고 한다면 즉시 카드를 오픈해 게임의 결과를 계산한다.

그렇다면 나의 로직상 결과는

  • 버스트의 유무
    • 버스트한 사람이 있다면?
      • 무승부 (둘다 버스트)
      • 게이머 승리 (딜러 버스트)
      • 딜러 승리 (게이머 버스트)
    • 버스트한 사람이 없다면?
      • 점수를 계산해 승자를 판별

위 로직을 거쳐야 한다.

그런데 이 로직을 구현하지 못했다 ㅋㅋㅋ...

게임 결과를 만드는 것이 어려워 DTO를 짜려고 했으나,
DTO가 어떤 속성들을 가지고 있어야 할지 명확하게 판단이 안섰다.

그래서 방황하던 중

우테코 2기 프리코스 에서 무려 블랙잭 게임을 진행 했다는 것을 찾았다.

게임 요구사항이 더 복잡하긴하다만..

그래서 이차저차 둘러보던중 굉장한 코드를 발견했다 ㅠ.

전반적인 구조 설계부터 나의 방향과 많이 달라 내가 진행하던 블랙잭을 접고,
다른 고수 분들의 코드를 참고하여 구조를 분석하는게 낫다고 판단했다.

profile
I am me

0개의 댓글

관련 채용 정보