블랙잭 게임 미션

ttomy·2023년 3월 1일
0

위와 같은 블랙잭 게임을 구현하는 미션이다.
카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다.

딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.

처음 미션을 시작할 때의 혼란을 그대로 쓰고싶어 두서없이 쓴 경향이 있겠지만, 오히려 그래서 배울점도 있을 것 같다.

도메인 이해

룰과 특수상황에 대한 이해를 해야했다.

https://namu.wiki/w/%EB%B8%94%EB%9E%99%EC%9E%AD(%EC%B9%B4%EB%93%9C%EA%B2%8C%EC%9E%84)

승패는 딜러와 플레이어간의 비교로 정해진다.
- 딜러가 먼저 21일 경우 -> 플레이어 수만큼 승리 챙김.
(처음 카드를 받았을 때 딜러는  1장만 노출하는데, 이때에는 점수 계산이 안되는 거겠지? 하는 의문)

플레이어 A,B존재
- 플레이어 A가 먼저 21일 경우 ->  플레이어 B는 패배
			 -> 딜러는 A에 대해 패배, B에 대해 승리
			-> 플레이어 A는 승리
            
- 딜러가 21 초과 경우 -> 버스트 되지 않은 플레이어 다 승리, 
		    -> 딜러는 해당 플레이어 수만큼 패배
            
 (딜러가 버스트 되기 전, 플레이어가 카드를 hit하면서 버스트 되었다면 플레이어는 이미 패배한 걸로 계산 되겠지? 하는 의문)
- 플레이어 A가 버스트 된 후 딜러가 버스트 됨 -> B는 승리
					-> A패배
					-> 딜러는 1승 1패


- 플레이어가 A가 버스트 된 경우 -> A는 패배
			    -> 나머지는 경기 지속.
	
 			
- 플레이어 A의 처음 2장이 21인 경우(블랙잭) -> 딜러는 A에게 패배-> 
										A는 승리
					(B와 딜러의 승부는 어떻게 되는 거지? 하는 의문)
                    
- 플레이어 A가 블랙잭, 이후 딜러카드 오픈 시 블랙잭 -> A와 딜러 무승부(push)											-> B는 패배, 딜러 승리

- 딜러가 에이스를 가지고 있어서 1로 쓸지 11로 쓸지에 따라 16이 넘어갈수도 아닐수도 있을경우 -> 딜러는 버스트가 아닌 경우 에이스는 다 11로 판단

도메인에 대한 이해를 어느정도 했으니 설계를 시작했다.

설계

처음 기본적으로 도출해본 설계였다. blackjackGame이란 객체는 필요없을 수도 있으나 큰 개념부터 내려가기 위해 일단 두었다.

크게는 카드를 나눠주고, hit이나 stay를 하고, 결과 계산을 한다.
하는 3부분이 요구사항이다.

여기서 처음의 고민점 몇 가지가 있다.

1. 카드를 나눠주는 책임을 어디서 담당하지?

우리는 처음에 Card가 CardSuit, CardNumber라는 두 Enum 변수로 이뤄질거라 생각했다. 이 때 카드를 뽑는 걸 어떻게 구현하지?생각해보았다.

  1. player나 dealer를 카드를 뽑는 주체로 생각해 이들에게 CardPicker같은 전략 클래스를 주입하고 스스로 뽑도록 하는 방향

-> 그런데 한 게임당 동일한 카드가 또 나오게 할 수는 없으니, 뽑힌 카드들에 대한 정보가 한 군데에 모여 있어야 할 것 같았다. 이 방법에서는 그러면 PickedCard같은 클래스가 더 생길 것 같다. 그래서 일단 기각.

  1. Card에 CardPicker를 주입하고, Card의 정적 메서드에서 pick()같은 함수를 통해 뽑아서 참여자들에게 메서드로 넘기는 방향

-> Card에 Card들의 캐싱을 해놓고, pick할때에도 캐싱한 card들의 인덱스를 랜덤하게 뽑는 식으로 하면 되지 않을까?
이미 뽑힌 카드들에 대한 배열을 저장해 놓던지, Card에 뽑혔는지에 대한 정보를 저장하는 boolean필드를 추가하던지, Card의 캐싱이
Map< Card, Boolean >방식으로 이뤄지게 해놓는다던지의 방법이 생각난다. 그런데 Card는 이미 2개의 Enum필드가 있는데 이게 추가 되면 클래스 변수가 3개 이상이 되어 요구사항을 어기게 된다. Card에 너무 많은 책임이 있는 건가? 일단 넘어가서 다른 방법을 생각해본다.

  • 추가: 캐싱필드는 static이기에 인스턴스 변수 3개 제한에 걸리지 않을거라고 본다. 설령 걸리더라도 Card에 넣는게 적절해보인다.
  1. Card들을 포장하는 Cards클래스에서 pick()하여 참여자들에게 메서드로 넘기는 방향

-> Card의 필드를 2개로 하고 이를 포장한 Cards에 카드 분배의 책임을 넘길 수도 있다. Cards는 Map< Card, Boolean >을 가지고 Boolean을 통해 이미 뽑혔는지의 유무를 저장한다.
그런데 이러면 Card를 어떻게 pick()하지? CardSuit와 CardNumber Enum들의 값들 중 1개씩을 랜덤하게 뽑고, 이것으로 Card를 만들어 Map에 접근할 수 있을 것 같다. 그런데 이러면 Card인스턴스의 생성이 너무 빈번해 질 것 같기는 하다. 이것도 최선일지 확신이 서지 않는다.

  1. Cards에 캐싱을 하는 방법이 있나?

Card의 생성자를 protected로 하고, Cards에서만 Card의 생성자를 호출하고 Cards 가 Card인스턴스를 정적 메서드를 제공하는 식으로 인스턴스를 재사용할 수가 있나 싶다. 이러면 Card에 책임이 몰리지 않으면서 완벽한 보호는 안되도 캐싱이 가능할 것 같다.

  • 추가: Card에 캐싱하는게 나을 것 같다. 변수 개수 때문에 Cards에 굳이 캐싱을 하는 건 아닌 거 같다.

최종제안: Card에 캐싱을 하고, Cards가 참여자들이 소유한 카드들 역할을 한다(카드 점수 계산). 이미 뽑힌 카드들은 정적 변수로 저장해둔다면 뽑힌카드와 남은 카드의 연동이 바로 가능할 것 같다.

최종 구현방식: 실제 카드게임에 카드들이 stack처럼 쌓여있듯이,
Stack< Card >를 가지는 클래스를 만들면 shuffle해서 랜덤하게 뽑을 수도, 원하는 순서대로 쌓인 카드덱을 주입할 수 있을 것 같아 채택.

2. player와 dealer의 구현 - 중복코드 제거

  • 가장 먼저 생각난 방법은 participant라는 상위 객체를 두고 player와 dealer가 이를 상속하는 방식이다.

dealer나 player 모두 카드를 받고, hit를 하던지 stay를 하던지의 동작을 하기 때문에 Participant에 이를 정의하고 상속하면 될 것 같았다.

이 때의 고민점

  • 조합을 사용하는 방법이 있을까?
    상속보다는 조합이라는 말을 들어서인지 조합을 사용한 나은 방법이 있을 것 같았다.
    composite방법을 쓴다면 Participant를 인스턴스 변수로 가지는데, 생각해보니 dealer와 player는 구체적인 행동은 다르게 해야되서
    공통으로 Participant를 가지는 게 의미가 있나 싶다.

이 때 다시 생각하니 Player와 Dealer은 행동의 이름과 OwnedCard를 가진다는 점만 공통적이지 이름만 같지 카드를 뽑는 기준은 다르다. 애초부터 override할 작정으로 상속을 받는 건 재사용이 아닌 듯하다. Player는 이름이 있는데 Dealer는 이름도 없다.
사실 추상클래스, 그냥 클래스, 인터페이스 각각을 상속/구현 했을 때의 차이에 대해 잘 모른다는 걸 알았다.

-> Player와 Dealer는 할 행동에 대해서 추상클래스나 인터페이스를 변수로 가지고 있으면 될 것 같다.

  • Player와 Dealer가 행동의 상위 타입을 인스턴스로 가지고, 행동의 구현체는 객체 생성 시에 다르게 주입하면 되지 않나?
    -> 그런데 같은 Player라도 OwnedCard의 합이 21인지, 초과인지 등에 따라 할수 있는 행동이 다르다.
    -> GamceAct가 ownedCard 의 점수 합에 따라 if문들로 다른 동작들을 하는 게 좋은 방식인가?

플레이어가 카드 받고 -> ownedCard의 합 달라짐. -> 이게 버스트인지, 블랙잭인지, 21미만인지에 따라 플레이어가 상태가 달라짐 -> 상태에 따라 할 수 있는 행동이 달라짐.

  • OwnedCard로부터의 값에 따라 GameAct의 구현체가 바뀌는 식으로 할 수 있나?
    -> ownedCard가 갱신될 때마다 본인의 상태를 갱신시키는 메서드를 만들어야 겠다. , 상태에서 Hit(), Stay()같은 메서드를 동작시켜야겠다.
  • Ownedcard를 가지고, hit(), stand() 등의 공통메서드를 가진 추상클래스를 만드는 게 나을까? -> State라는 추상클래스가 OwnedCard를 가지고, hit, stand등이 state를 상속받아 각기 따라 다르게 draw()를 수행.

-> state는 인터페이스로 하고, 이를 implements한 Running, Finished 추상클래스를 만들면 여기에 상태검사에 대한 책임을 넘기고,
이 running, finished를 상속한 구현 클래스는 draw()동작을 구현해 동작과 상태를 나타내는 책임을 나눌 수 있겠다.

0개의 댓글