블랙잭 게임 분석 - 2

Jaca·2021년 11월 18일
0

아무래도 모두 분석하려고 하니 너무 길어져서 글을 나눴다 !
블랙잭 게임 분석 - 1

요구 사항 분석

패키지 분석

  • domain
    • card
      • Card.class
      • CardRepository.class
      • Cards.class
      • Deck.class
      • Symbol.enum
      • Type.enum
    • controller
      • blackjackController.class
    • user
      • strategy.draw
        • DealerDrawStrategy.class
        • PlayerDrawStrategy.class
        • DrawStrategy.interface
      • Dealer.class
      • Player.class
      • User.interface
      • Users.class
    • BettingMoney.class
    • PlayerIntentionType.enum
    • PlayerMoneys.class
    • ResultType.enum
    • ScoreType.enum
  • view
    • InputView
    • OutputView

분석

DrawStrategy.inteface

public interface DrawStrategy {
    public boolean canDraw(int score);
}

딜러와 플레이어의 드로우 가능 상태인지 확인 메서드이다.

PlayerDrawStrategy.class

public class PlayerDrawStrategy implements DrawStrategy{
    private static final int BLACKJACK_SCORE = 21;
    private static final int BURST_SCORE = 0;

    @Override
    public boolean canDraw(int score) {
        return score < BLACKJACK_SCORE && score != BURST_SCORE;
    }
}

canDraw() 를 오버라이딩해서 각 역할에 맞는 조건을 달아준다.

DealerDrawStrategy.class

public class DealerDrawStrategy implements DrawStrategy{
    private static final int BURST_SCORE = 0;
    private static final int DEALER_DRAW_BOUND = 17;

    @Override
    public boolean canDraw(int score) {
        return score < DEALER_DRAW_BOUND && score != BURST_SCORE;
    }
}

플레이어와 전과동 이다.
아주 간결한 구문이지만 추후에 보면 알겠지만 이 친구들 덕분에 조건 확인문이 얼마나 간단해지는지 볼 수 있을 것이다.
갑자기 전 내 프로젝트가 얼마나 한심한지 ㅎㅎ 또 상기하게 되었다.

BettingMoney.class

public class BettingMoney {
    private static final int MIN_MONEY = 0;

    private final int bettingMoney;

    private BettingMoney(int bettingMoney) {
        if (bettingMoney <= MIN_MONEY) {
            throw new IllegalArgumentException("베팅 금액은 최소 1원 이상이어야 합니다.");
        }
        this.bettingMoney = bettingMoney;
    }

    public static BettingMoney of(int bettingMoney) {
        return new BettingMoney(bettingMoney);
    }

    public static BettingMoney of(String bettingMoney) {
        return new BettingMoney(Integer.parseInt(bettingMoney));
    }

    public int intValue() {
        return bettingMoney;
    }
}

플레이어들이 금액을 베팅하는 역할을 담당하는 객체이다.
순수하게 플레이어가 얼마는 베팅했는지 정보만 가지고 있는다.

PlayerMoneys.class

public class PlayerMoneys {
    private static final int MULTIPLIER_TO_REVERSE = -1;

    private final Map<Player, Integer> playerMoneys;

    public PlayerMoneys(Map<Player, Integer> playerMoneys) {
        this.playerMoneys = playerMoneys;
    }

    public Map<User, Double> getTotalPrizes(User dealer) {
        Map<Player, Double> playersProfits = getPlayerProfitsBy(dealer);

        Map<User, Double> totalResult = getUserProfitsOf(dealer, playersProfits);

        return totalResult;
    }

    private Map<Player, Double> getPlayerProfitsBy(User dealer) {
        Map<Player, Double> playerProfits = new HashMap<>();

        playerMoneys.keySet().forEach(player ->
                playerProfits.put(player, ResultType.from(player, dealer).getProfit(playerMoneys.get(player))));

        return playerProfits;
    }

    private Map<User, Double> getUserProfitsOf(User dealer, Map<Player, Double> playerProfits) {
        Map<User, Double> userProfit = new LinkedHashMap<>();

        userProfit.put(dealer, playerProfits.values().stream()
                .mapToDouble(playerProfit -> playerProfit * MULTIPLIER_TO_REVERSE)
                .sum());

        playerProfits.forEach(userProfit::put);

        return userProfit;
    }
}

플레이어들의 수익을 계산을 담당한다.
자세한 설명은 추후에..

ResultType.enum

public enum ResultType {
    BLACKJACK_WIN(
            scoreGap -> scoreGap > 0,
            cards -> cards.isInitialSize() && cards.getPoint() == 21,
            money -> money.doubleValue() * 1.5
    ),
    WIN(
            scoreGap -> scoreGap > 0,
            Integer::doubleValue
    ),
    DRAW(
            scoreGap -> scoreGap == 0,
            cards -> cards.getPoint() != 0,
            money -> money.doubleValue() * 0
    ),
    LOSE(
            scoreGap -> scoreGap <= 0,
            money -> money.doubleValue() * -1
    );

    private final Predicate<Integer> resultJudge;
    private final Predicate<Cards> blackjackOrBurstJudge;
    private final Function<Integer, Double> getPrize;

    ResultType(Predicate<Integer> resultJudge, Function<Integer, Double> getPrize) {
        this(resultJudge, cards -> true, getPrize);
    }

    ResultType(Predicate<Integer> resultJudge, Predicate<Cards> blackjackOrBurstJudge, Function<Integer, Double> getPrize) {
        this.resultJudge = resultJudge;
        this.blackjackOrBurstJudge = blackjackOrBurstJudge;
        this.getPrize = getPrize;
    }

    public static ResultType from(User result, User compared) {
        return Arrays.stream(ResultType.values())
                .filter(type -> type.resultJudge.test(result.getScoreMinusBy(compared)))
                .filter(type -> type.blackjackOrBurstJudge.test(result.openAllCards()))
                .findFirst()
                .orElseThrow(NullPointerException::new);
    }

    public double getProfit(int bettingMoney) {
        return getPrize.apply(bettingMoney);
    }
}

ㅗㅜㅑㅑㅑㅑㅑㅑ 엄청난 함수형 인터페이스의 향연 !!

일단 Enum을 통해 게임 결과를 관리한다.
게임의 결과는 아래와 같고 각각 특성이 있다.

  • BLACKJACK_WIN
    • Predicate를 사용 scoreGap > 0 인지 판별
    • Predicate를 사용 카드가 2장이고 점수가 21인지 (블랙잭) 판별
    • Function을 사용 배팅 금액 * 1.5를 반환
  • WIN
    • Predicate를 사용 ScoreGap > 0 인지 판별
    • 생성자를 통해 상수 true를 반환하는 Predicate 주입
    • Function을 사용 배팅 금액을 double형으로 반환
  • DRAW
    • Predicate를 사용 ScoreGap == 0 인지 판별
    • Predicate를 사용 핸드의 포인트가 0이 아닌지 (버스트) 판별
    • Function을 통해 배팅 금액 * 0을 반환
  • LOSE
    • Predicate를 사용 ScoreGap <= 0 인지 판별
    • 생성자를 통해 상수 true를 반환하는 Predicate 주입
    • Function을 통해 배팅 금액 * -1를 반환

이 속성들을 이해하고, from() 메서드를 뜯어보자,

User result (player) 와 User compared (dealer) 를 인자로 받는다.
왜 이런 변수명을 사용하셨는지 잘 모르겠다. player와 dealer가 더 직관적인거 같은데
Arrays.stream(ResultType.values())
--> Enum의 속성을 스트림으로 가져온다.

.filter(type -> type .resultJudge.test(result.getScoreMinusBy(compared)))
-- > player와 dealer의 점수 차를 계산해 Enum의 첫번째 속성인 resultJudge의 테스트를 한다.
점수 차에 따라 여러가지 필드가 걸릴 수 있다. (BLACKJACK_WIN 과 WIN)

.filter(type -> type .blackjackOrBurstJudge.test(result.openAllCards()))
--> player의 핸드를 가져와 blackjackOrBurstJudge 의 테스트를 한다.
BLACKJACK_WIN 과 WIN 의 차이는 카드 장 수이다.
최초의 2장 드로우 시점에 21이 완성되어야 블랙잭이다.
WIN의 경우 blackjackOrBurstJudge의 인자로 상수 true를 받기 때문에 블랙잭인 경우에 2개의 Enum이 걸린다.

.findFirst().orElseThrow(NullPointerException::new);
--> 그래서 첫번째 Enum만 가져오게 되고 아무것도 걸린게 없다면 에러이다.

PlayerMoneys.getPlayerProfitsBy() 메서드를 보면,
player별로 from() 메서드를 통해 결과 Enum을 가져오고 해당 Enum의 getPrize 변수를 통해 맞는 상금 결과를 가져온다.

ScoreType.enum

public enum ScoreType {
    BURST(point -> point > 21, point -> 0),
    NORMAL(point -> point <= 21, point -> point);

    private final Predicate<Integer> scoreJudge;
    private final Function<Integer, Integer> getScore;

    ScoreType(Predicate<Integer> scoreJudge, Function<Integer, Integer> getScore) {
        this.scoreJudge = scoreJudge;
        this.getScore = getScore;
    }

    public static ScoreType of(int point) {
        return Arrays.stream(ScoreType.values())
                .filter(scoreType -> scoreType.scoreJudge.test(point))
                .findFirst()
                .orElseThrow(NullPointerException::new);
    }

    public int getScore(int point) {
        return getScore.apply(point);
    }
}

최종 결과의 점수를 계산하기 위한 Enum이다.
ResultType을 이해했다면 특별한 부분은 없다.
결과가 BURST라면 0점을 리턴한다.

BlackjackController.class

public class BlackjackController {
    public static void proceedInitialPhase(Users users, Deck deck) {
        printInitialDistribution(users.getPlayers());

        for (User user : users) {
            user.proceedInitialPhase(deck);
            printInitialStatus(user);
        }
    }

    public static PlayerMoneys getBettingMoney(List<Player> players) {
        Map<Player, Integer> result = new HashMap<>();
        players.forEach(player -> result.put(player, BettingMoney.of(inputBettingMoney(player)).intValue()));
        return new PlayerMoneys(result);
    }

    public static void proceedGame(List<Player> players, User dealer, Deck deck) {
        players.forEach(player -> proceedPhaseOf(player, deck));
        proceedPhaseOf(dealer, deck);
    }

    private static void proceedPhaseOf(Player player, Deck deck) {
        while (player.canDrawMore() && PlayerIntentionType.of(inputIntentionOf(player)).isWantDraw()) {
            player.receive(deck.pop());
            printCardsStatusOf(player);
        }
    }

    private static void proceedPhaseOf(User dealer, Deck deck) {
        while (dealer.canDrawMore())
            dealer.receive(deck.pop());
        printDealerDrawing();
    }
}

블랙잭 게임의 메인 컨트롤러이다.
나는 컨트롤러는 게임의 전체 흐름을 진행해야한다고 생각했는데,
게임의 메인 컨트롤 로직만 들어있고 게임의 흐름은 밖의 어플리케이션에 있었다.
이 전 프리코스 공부에서 이것에 관련된 리뷰가 있었던거 같아서 한번 더 볼 것이다.
구조를 바꾼다면 그냥 밖에 한겹 더 감싸면 되니까

컨트롤러는 메서드의 흐름을 한번 훑으면 될 것 같다.
흐름이 중요하니까

InputView.class

public class InputView {
    private static final Scanner SCANNER = new Scanner(System.in);

    public static String inputPlayerNames() {
        System.out.println("게임에 참여할 사람의 이름을 입력하시오. (쉼표로 구분)");
        return SCANNER.nextLine();
    }

    public static String inputIntentionOf(User player) {
        System.out.println(player + "는 한장의 카드를 더 받겠습니까? (예는 y, 아니오는 n)");
        return SCANNER.nextLine();
    }

    public static String inputBettingMoney(User player) {
        System.out.println(player + "의 베팅 금액은?");
        return SCANNER.nextLine();
    }
}

OutputView.class

public class OutputView {
    private static final String DELIMITER = ", ";

    public static void printInitialDistribution(List<Player> players) {
        System.out.println("딜러와 " + players.stream()
                .map(Player::toString)
                .collect(Collectors.joining(DELIMITER)) +
                "에게 2장의 카드를 나눠줬습니다.");
    }

    public static void printInitialStatus(User user) {
        System.out.println(user + ": " + user.openInitialCards().toList().stream()
                .map(Card::toString)
                .collect(Collectors.joining(DELIMITER)));
    }

    public static void printCardsStatusOf(Player player) {
        Cards cards = player.openAllCards();
        System.out.println(player + "카드: " +
                cards.toList().stream()
                        .map(Card::toString)
                        .collect(Collectors.joining(DELIMITER)));
    }

    public static void printDealerDrawing() {
        emptyLine();
        System.out.println("딜러는 16이하라 한장의 카드를 더 받았습니다.");
    }

    public static void printResultStatus(Users users) {
        for (User user : users) {
            Cards cards = user.openAllCards();
            System.out.println(user + "카드: " +
                    cards.toList().stream()
                            .map(Card::toString)
                            .collect(Collectors.joining(DELIMITER)) +
                    " - 결과" + cards.getPoint());
        }
    }

    public static void printResultProfit(PlayerMoneys playerMoneys, User dealer) {
        Map<User, Double> totalPrizes = playerMoneys.getTotalPrizes(dealer);
        emptyLine();
        System.out.println("## 최종 수익");
        totalPrizes.forEach(((user, money) -> System.out.println(user + ": " + money)));
    }

    private static void emptyLine() {
        System.out.println();
    }
}

뷰와 관련된 부분은 특별할 것은 없지만, 메서드 네이밍 관련을 눈여겨 봐야한다고 느꼈다.
그런데 저런 출력 메세지는 하드코딩이 아닌가?????..
메인 로직과 직접적인 연관이 있는 것은 아니기 때문에?
전부 Static 메서드라 내부 로직이 안보여서..?

Application.java

public static void main(String[] args) {
    User dealer = new Dealer();
    Deck deck = Deck.of(CardRepository.toList());
    Users users = Users.of(inputPlayerNames(), dealer);
    PlayerMoneys playerMoneys = BlackjackController.getBettingMoney(users.getPlayers());

    BlackjackController.proceedInitialPhase(users, deck);

    if(dealer.isNotBlackJack()) {
        BlackjackController.proceedGame(users.getPlayers(), dealer, deck);
    }

    printResultStatus(users);
    printResultProfit(playerMoneys, dealer);
}

후기

프로젝트를 살펴보며 인상 깊은 점은 static 메서드가 많다는 점 이었다.
view같은 경우는 그렇다 치더라도 부분부분 static 메서드가 굉장히 많이 있었다.
이 부분은 아직 공부가 부족한 것 같다.

아직 부족한 것도 많지만, 굉장히 배운 것도 많았다.
역시 고수의 발자취를 따라가는 것은 유의미 한 것같다.
어느정도 수준까지는 빠르게 올라갈 수 있는 길인 듯하다.

profile
I am me

0개의 댓글