기능 요구 사항
블랙잭 게임을 변형한 프로그램을 구현한다.
블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다.
플레이어는 게임을 시작할 때 배팅 금액을 정해야 한다.
카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며,
King, Queen, Jack은 각각 10으로 계산한다.
게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며,
두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다.
21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다.
단, 카드를 추가로 뽑아 21을 초과할 경우 배팅 금액을 모두 잃게 된다.
처음 두 장의 카드 합이 21일 경우 블랙잭이 되면 베팅 금액의 1.5 배를 딜러에게 받는다.
딜러와 플레이어가 모두 동시에 블랙잭인 경우 플레이어는 베팅한 금액을 돌려받는다.
딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리해 베팅 금액을 받는다.
사진 출처 : 게티이미지뱅크
Trump 라는 상위 추상 클래스를 만들고, 그 클래스는
Trump.createCardDeck() static 매서드 호출 -> 카드덱이 생성됨
trump.drawCard() 매서드로 카드를 한장 뽑음 (Spade 부터 들어가므로, Spade ACE 가 나옴) -> 따라서 테스트가 용이함
trump.shuffle() 매서드는 트럼프가 들어있는 어레이 리스트를 셔플함 (랜덤하게 만듬)
카드를 만들려고 봤는데, 카드 스스로가 객체로써 활용될 것이고, 변경 가능성이 없는 불변/싱글턴 객체이므로 Enum이 적절하다고 생각했음.
그래서 다음과 같은 테스트를 작성함
@Test
void diamondTest() {
List<String> diamondsDeck = Diamonds.getDiamondsDeck();
assertThat(diamondsDeck.get(0)).isEqualTo("ACE DIAMOND");
assertThat(diamondsDeck.get(diamondsDeck.size() - 1)).isEqualTo("K DIAMOND");
}
@Test
void heartTest() {
List<String> diamondsDeck = Diamonds.getDiamondsDeck();
assertThat(diamondsDeck.get(0)).isEqualTo("ACE HEART");
// 과연 이런식의 테스트가 맞는가? 스트링값으로 비교해야 하나?
assertThat(diamondsDeck.get(diamondsDeck.size() - 1)).isEqualTo("K HEART");
}
여기서 Diamonds를 Enum으로 하려고 봤더니, 너무 중복이 많음(Diamond 3, Heart 3... 이런식으로 4번의중복이 발생함)
따라서 아래와 같이 하나의 Enum을 만들고 String값을 더했음
public enum Card {
ACE(1),
TWO(2),
THREE(3),
FOUR(4),
FIVE(5),
SIX(6),
SEVEN(7),
EIGHT(8),
NINE(9),
J(10), Q(10), K(10);
private final Integer number;
private String name;
Card(Integer number) {
this.number = number;
}
public static List<String> createCards(String type){
return Arrays.stream(values()).map(c -> c + type).collect(Collectors.toList());
}
public Integer getNumber() {
return this.number;
}
}
public class Diamonds extends Trump {
private static final String TYPE = " DIAMOND";
private static final List<String> DIAMONDS = Card.createCards(TYPE);
private Diamonds() {}
public static List<String> getDiamondsDeck(){
return DIAMONDS;
}
}
저걸 꼭 저렇게 해야하나? 싶은 생각이 들었음.
ENUM을 사용한 이유는 불변하게 만드는 싱글턴 객체를 활용하기 위함인데, String으로 바꿔야 했는가?
라는 생각이 들었고, 다음과 같은 변경을 생각함
...
private String name;
private void setName(String name) {
this.name = name;
}
public static List<Card> createCardsByName(String name){
return Arrays.stream(values())
.peek(cardValue -> cardValue.setName(name))
.collect(Collectors.toList());
}
public String getName() {
return name;
}
위와 같이, 이미 선언된 TWO(2) .. 등은 그대로 두고, name 이라는 필드를 추가해서 stream으로 set해준 뒤(setter 매서드는 프라이빗) 리턴하는 형태.
그런데, 이것도 한 가지 문제가 있다.
위 코드를 테스트로 돌린 모습이다.
카드 그 자체로는 숫자의 표현은 가능하지만,(KJQ...) name의 표현은 불가능하다
따라서 Enum 의 배를 까서 이름과 숫자를 물어보아야 한다. (상태를 물어봐야함)
상태를 보지 않으면 사용이 불가능한 녀석이라는 게 마음에 안든다
두 가지 방법이 있다.
장점 :
위와 같은 방식으로 Enum 객체는 상태값(숫자, 이름(==DIAMOND)) 을 가짐과 동시에 스스로의 상태를 getter로 까지 않아도 바로 식별이 가능하다
단점 : 중복이 발생한다 (DIAMOND, CLOVER...)
장점 : 중복이 거의 없다
각 녀석들마다 타입만 알려주고, 매서드로 호출하면 (나 DIAMOND야! 카드줘) 알아서 해당 카드를 준다.
단점 : 상태를 까봐야 뭔놈인지 안다
메시지를 주고 받는 입장에서 2번이 더 나은 길 같은데, 확신이 안선다.
객체지향의 사실과 오해 라는 책에서
판사는 증인에게 "증언하라" 는 명령(메시지)를 내린다. 따라서 증인은 다양한 방법을 사용해서 (글을 읽든, 기억을 더듬든, 바디랭귀지를 사용하던) 증언한다
그러나 "기억을 더듬어서, 시간순에 따라서, 말로 설명하라" 고 명령(메시지) 하는 순간 객체의 자율성은 없어지고 따라서 효율이 떨어진다
이 경우에 2번(필드를 두고 상태만 준다. Enum이)이 더 적절하다고 생각 하는데, 정확한 확신이 없다...