본문 바로가기
Programming/Java

[우아한테크코스 3기 프리코스 3주차] 블랙잭

by IN.0 2019. 12. 17.
728x90
반응형

2주 차 피드백과 3주 차 미션을 받았다.

피드백 중에 '하드코딩하지 마라'는 내용이 있었는데, 하드코딩이 무슨 말인지 몰라서 찾아봤었다.

2주 차 자동차 경주 게임에서 '랜덤 숫자가 4 이상이 나오면 전진'이라는 룰을 코딩할 때

if (number >= 4) 이런 식이면 하드코딩이라고 할 수 있다.

왜냐면 룰의 숫자가 바뀌면 '4'라는 숫자를 하나하나 바꿔줘야 하기 때문이다.

따라서 static final int referenceVaule = 4;라고 정해두고

if (number >= referenceValue)라고 해야 한다.

 

또 다른 피드백으로 array 대신 java collection 자료구조를 사용하라는 것이 있다.

https://hackersstudy.tistory.com/26

 

Java의 Collections (List, Set, Map) 이해

Java Collection Framework (JCF) : Java에서 데이터를 저장하는 기본적인 자료구조들을 한 곳에 모아 관리하고 편하게 사용하기 위해서 제공하는 것을 의미한다. 다음은 JCF의 상속 구조이며 사용 용도에 따라 Li..

hackersstudy.tistory.com

위 블로그에 잘 정리되어있어 나중에 또 참고하려고 첨부한다.


일단 3주 차 미션을 까 보려고 github fork 후에 eclipse에서 clone 하였다.

java project로 재설정해주고 나니 test 패키지에서 오류가 보였다.

org.junit 부분에 오류가 있어서 검색을 해보니, 라이브러리를 다운받으라고 해서

그 가이드를 따라 JUnit 5 라이브러리를 받았다.

그런데 이번엔 org.assertj가 문제였다.

끝이 j로 끝나는 걸 보니 intellij에서 사용하는 무언가 같았다.

빨간줄!

현 상황에서 내가 test 패키지 부분을 사용할 수 있을 것 같지도 않고, 빨간 줄 없애는 방법도 모르겠고 해서

그냥 주석처리로 밀어버렸다.

주석으로 밀어버린 모습

 

그러고 나서 main 패키지를 열어보니 기본으로 제공된 코드가 꽤 많았는데,

문제는 모르는 기능이 너무 많다는 것이다.

그래서 하나씩 정리해본다.

Card.java에 제공된 기본 코드

제일 먼저 당황스러웠던 부분이다.

저 Object o가 무슨 뜻인가 한참 살펴보다가 구글링을 해보니, 대충 저 @Override를 달고 있는 친구들은

Object (java에서 최상위 클래스라고 한다) 메서드를 문제없이 사용하기 위해 존재하는 것이다.

만일 내가 클로버 5를 카드 클래스로 만들었는데, 이게 랜덤 생성이니 우연히 또 다른 클로버 5가 만들어졌다고 치자.

한 카드 세트 안에 동일한 카드는 존재할 수 없으므로 카드 중복체크를 해야 한다.

이 체크를 equals 메서드를 통해 하는 것 같은데, @Override 메서드 부분이 없다면,

두 개의 클로버 5 카드를 서로 다른 카드로 인식하게 된다. 이 상황을 방지하기 위해 오버라이딩을 사용한다.

정리하자면, 클래스로 객체를 찍어낼 때 같은 값을 가지는 객체들을 인식하기 위한 기능이라고 이해했다.

eclipse에서는 이러한 코드를 자동으로 입력해주는 기능이 있으며, 'Source-Generate 어쩌구'를 클릭하면 된다.

Symbol.java

CardFactory.java를 이해하기 위해서 일단 Symbol.java부터 알아봤다.

enum이라는 클래스는 열거형 상수를 모아놓는 클래스라고 한다. 콤마(,)로 상수를 구분하는데 위의 경우에는

열거형 상수를 다른 값으로 연결하는 방식이다. 즉, Symbol.ACE.getScore()를 하면 1이 리턴된다.

Type.java도 enum으로 구현되어있다.

CardFactory.java (주석은 왜 깨져있는지..)

Symbol과 Type을 호출하여 카드 리스트에 모든 종류의 카드를 담는 클래스로 추정된다.

나는 이런 경우에 아무 제약이 없었다면 for문 안에 또 for문을 돌려서 사용했을 것 같은데,

이렇게 메서드를 분리해서 사용할 수도 있구나... 하고 깨달았다.

리턴 값으로 들어가는 Collections.unmodifiableList는 해당 리스트를 변경 불가 리스트로 만들어서

데이터 손상을 방지하는 기능을 담당한다. 


README를 작성하고 코드를 조금 추가하고 push를 하니까 이런 오류가 떴다.

오류

검색해보니까 'git push -u origin +브랜치이름' 하면 된다길래 그대로 따라 했더니 push는 됐는데..

2시간 동안 공들여 쓴 README가 날아갔다 ㅠㅠ

그래서 일단 README를 다시 작성하고 이어서 코딩했다.

package domain.card;

import java.util.Objects;

public class Card {
    private final Symbol symbol;

    private final Type type;

    public Card(Symbol symbol, Type type) {
        this.symbol = symbol;
        this.type = type;
    }
    
    public String showSymbol() {
    	return symbol.name();
    }
    
    public String showType() {
    	return type.name();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Card card = (Card) o;
        return symbol == card.symbol &&
                type == card.type;
    }

    @Override
    public int hashCode() {
        return Objects.hash(symbol, type);
    }

    @Override
    public String toString() {
        return "Card{" +
                "symbol=" + symbol +
                ", type=" + type +
                '}';
    }
}

Card.java

심볼과 타입을 리턴하는 메서드를 만들었다.  

 

package domain.card;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CardFactory {
    public static List<Card> create() {
        List<Card> cards = new ArrayList<>();
        Symbol[] symbols = Symbol.values();
        for (Symbol symbol : symbols) {
            createByType(cards, symbol);
        }
        return cards;
    }
    
    public static List<Card> cardShuffled() {
    	List<Card> cardSet = CardFactory.create();
    	Collections.shuffle(cardSet);
    	return cardSet;
    }
    
    public static Card pickOneCard(List<Card> cards) {
    	Card target = cards.get(cards.size()-1);
    	cards.remove(cards.size()-1);
    	return target;
    }

    private static void createByType(List<Card> cards, Symbol symbol) {
        Type[] types = Type.values();
        for (Type type : types) {
            cards.add(new Card(symbol, type));
        }
    }
}

CardFactory.java

cardShuffled 메서드와 pickOneCard 메서드를 추가했다.

create() 메서드에서 원래 리턴 값은 Collections.unmodifiableList로 나오게 생성되어 있었는데,

리스트에서 값을 하나씩 빼서 사용하고 싶어서 일반 리스트로 변경했다.

 

package domain.user;

import domain.card.Card;
import domain.card.Symbol;

import java.util.ArrayList;
import java.util.List;

public class Dealer {
    private final List<Card> cards = new ArrayList<>();

    public Dealer() {}

    public void addCard(Card card) {
        cards.add(card);
    }
    
    public int howManyCard() {
    	return cards.size();
    }

    public String showOneCard() {
    	Card oneCard = cards.get(0);
    	return oneCard.showSymbol() + " " + oneCard.showType();
    }
    
    public String showCard() {
    	List<String> cardList = new ArrayList<>();
    	for (Card card : cards) {
    		cardList.add(card.showSymbol() + " " + card.showType());
    	}
    	return String.join(", ", cardList);
    }
    
    public int showScore() {
    	int score = 0;
    	int check = 0;
    	for (Card card : cards) {
    		score += Symbol.valueOf(card.showSymbol()).getScore();
    		check += aceCheck(card);
    	}
    	if (score < 12 && check != 0) {
    		score += 10;
    	}
    	return score;
    }
    
    public int aceCheck(Card card) {
    	int check = 0;
    	if (card.showSymbol() == "ACE") {
    		check = 1; 
    	}
    	return check;
    }
}

Dealer.java

package domain.user;

import domain.card.Card;
import domain.card.Symbol;

import java.util.ArrayList;
import java.util.List;

public class Player {
    private final String name;
    private final double bettingMoney;
    private final List<Card> cards = new ArrayList<>();

    public Player(String name, double bettingMoney) {
        this.name = name;
        this.bettingMoney = bettingMoney;
    }

    public void addCard(Card card) {
        cards.add(card);
    }

    public String name() {
    	return name;
    }
    
    public double bettingMoney() {
    	return bettingMoney;
    }
    
    public int howManyCard() {
    	return cards.size();
    }
    
    
    public String showCard() {
    	List<String> cardList = new ArrayList<>();
    	for (Card card : cards) {
    		cardList.add(card.showSymbol() + " " + card.showType());
    	}
    	return String.join(", ", cardList);
    }
    
    public int showScore() {
    	int score = 0;
    	int check = 0;
    	for (Card card : cards) {
    		score += Symbol.valueOf(card.showSymbol()).getScore();
    		check += aceCheck(card);
    	}
    	if (score < 12 && check != 0) {
    		score += 10;
    	}
    	return score;
    }
    
    public int aceCheck(Card card) {
    	int check = 0;
    	if (card.showSymbol() == "ACE") {
    		check = 1; 
    	}
    	return check;
    }

}

Player.java

딜러와 플레이어 클래스는 겹치는 부분이 많아서 중복을 없애기 위해 상속을 사용하고 싶었다.

java 상속에 대해 검색하고 공부를 해서 상속의 개념과 기본적인 쓰임에 대해서는 알겠는데,

실제로 딜러와 플레이어 클래스에서 응용을 하기가 힘들었다.

겹치는 메서드로는 addCard, howManyCard, showCard, showScore, aceCheck가 있다.

(>> 마지막 날에 다시 찾아보고 Dealer를 부모 클래스로 두고 Player를 자식 클래스로 설정했다.)

 

package domain.main;

import java.util.List;
import java.util.Scanner;
import domain.user.Player;
import domain.card.Symbol;
import domain.user.Dealer;

public class Template {
	
	public static String nameRequest() {
		Scanner scanner = new Scanner(System.in);
		System.out.println("게임에 참여할 사람의 이름을 입력하세요. (쉼표 기준으로 분리하며 8명까지 참여 가능합니다.)");
		String inputName = scanner.nextLine();
		return inputName;
	}
	
	public static String bettingMoneyRequest(String name) {
		Scanner scanner = new Scanner(System.in);
		System.out.println(name + "의 배팅 금액은?");
		String bettingMoney = scanner.nextLine();
		return bettingMoney;
	}
	
	public static String oneMoreCardRequest(Player player) {
		Scanner scanner = new Scanner(System.in);
		System.out.println(player.name() + "는(은) 한장의 카드를 더 받겠습니까? (예는 y, 아니오는 n)");
		String answer = scanner.nextLine();
		return answer;
	}
	
	public static void interimResult(List<Player> players, Dealer dealer) {
		System.out.println();
		for (Player player : players) {
			System.out.println(player.name()+ " : " + player.showCard() + " - " + player.showScore());
		}
		System.out.println();
		System.out.println("딜러 : " + dealer.showOneCard());
		System.out.println();
	}
	
	public static void finalResult(List<Player> players, Dealer dealer) {
		System.out.println();
		for (Player player : players) {
			System.out.println(player.name()+ " : " + player.showCard() + " - 결과 : " + Integer.toString(player.showScore()));
		}
		System.out.println();
		System.out.println("딜러 : " + dealer.showCard() + " - 결과 : " + Integer.toString(dealer.showScore()));
		System.out.println("\n");
	}
	
	public static void dealerOneMoreCard() {
		System.out.println("딜러는 16 이하라 한장의 카드를 더 받았습니다.\n");
	}
}

Template.java

게임 진행 시 시각적 요소를 담당하는 클래스를 따로 만들었다.

interimResult 메서드와 finalResult 메서드는 중복되는 코드가 있는데, 합치기가 애매해서 그냥 분리해두었다.

 

package domain.main;

import java.util.ArrayList;
import java.util.List;

import domain.user.Player;
import domain.main.Main;

public class InputHandler {
	
	static List<String> nameStringList = new ArrayList<>();
	static List<Double> bettingMoneyDoubleList = new ArrayList<>();
	
	
	public static void nameHandler() {
		String names = Template.nameRequest();
		List<String> nameList = new ArrayList<>();
		String[] nameArray = names.split(",");
		for (String name : nameArray) {
			nameList.add(name.trim());
		}
		int errorCode = nameHandlerException(nameList);
		nameHandlerResult(errorCode, nameList);
	}
	
	public static int nameHandlerException(List<String> nameList) {
		int error = 0;
		if (nameList.size() > 8) {
			System.out.println("게임 인원이 8명을 초과했습니다."); error = 1;
		}
		if (nameList.contains("")) {
			System.out.println("빈칸은 입력할 수 없습니다."); error = 2;
		}
		return error;
	}
	
	public static void nameHandlerResult(int error, List<String> nameList) {
		if (error != 0) {
			nameHandler();
		}
		if (error == 0) {
			nameStringList = nameList;
		}
	}
	
	
	
	
	public static void bettingMoneyHandler(List<String> nameList) {
		for (String name : nameList) {
			bettingMoneyHandlerException(name);
		}
	}
	
	public static void bettingMoneyHandlerException(String name) {
		int error = 0;
		Double bettingMoney = 1.0;
		String bettingMoneyString = Template.bettingMoneyRequest(name);
		try {
			bettingMoney = Double.parseDouble(bettingMoneyString);
		} catch(Exception e) {
			System.out.println("숫자를 입력해주세요."); error = 2;
		}
		bettingMoneyHandlerResult(error, bettingMoney, name);
	}
	
	public static void bettingMoneyHandlerResult(int error, Double bettingMoney, String name) {
		if (bettingMoney <= 0) {
			System.out.println("0보다 큰 금액을 입력해주세요."); error = 1;
		}
		if (error != 0) {
			bettingMoneyHandlerException(name);
		}
		if (error == 0) {
			bettingMoneyDoubleList.add(bettingMoney);
		}
	}
	
	
	
	
	public static List<Player> makePlayer() {
		List<Player> playerList = new ArrayList<>();
		int count = 0;
		for (String name : nameStringList) {
			Double bettingMoney = bettingMoneyDoubleList.get(count);
			playerList.add(new Player(name, bettingMoney));
			count += 1;
		}
		return playerList;
	}
	
	
	
	public static void oneMoreCardOrNot(List<Player> playerList) {
		for (Player player : playerList) {
			oneMoreCardOrNotException(player);
		}
	}
	
	public static void oneMoreCardOrNotException(Player player) {
		String answer = Template.oneMoreCardRequest(player);
		if (answer.charAt(0) == 'y' && answer.length() == 1) {
			Main.giveOneCardToPlayer(player);
			System.out.printf(player.name()+ " : " + player.showCard() + " - " + player.showScore() + "\n\n");
			oneMoreCardOrNotControl(player);
		}
		if (answer.length() != 1 || (answer.charAt(0) != 'y' && answer.charAt(0) != 'n') ) {
			System.out.println("y 또는 n만 입력이 가능합니다.");
			oneMoreCardOrNotControl(player);
		}
	}
	
	public static void oneMoreCardOrNotControl(Player player) {
		if (player.showScore() <= 21) {
			oneMoreCardOrNotException(player);
		}
	}
}

InputHandler.java

이 클래스가 만드는데 가장 오래 걸렸고, 어려웠다.

메서드를 10라인 이하로 구성하고 인덴트 1을 유지하며 예외처리를 하려고 하다 보니까

어떤 부분에서 분리를 해야 하는지, 리턴 값을 줘야 하는지 아니면 void로 처리해야 하는지 등

생각해야 할 요소가 많았다.

이전 미션에서도 그렇고, 예외처리 후에 값을 다시 받아야 할 때 재귀 호출을 통해서 해결했는데

재귀 호출 외에 인덴트 1을 유지하면서 다시 값을 받는 방법이 또 있는지 궁금하다.

 

package domain.main;

import java.util.List;

import domain.user.Dealer;
import domain.user.Player;

public class MoneyCalculator {
	private static double dealerIncome = 0;
	
	public static void calculate(List<Player> playerList, Dealer dealer) {
		System.out.println("## Income ##");
		for (Player player : playerList) {
			double playerIncome = checkScore(player, dealer, player.bettingMoney());
			System.out.println(player.name() + " : " + playerIncome);
			dealerIncome -= playerIncome;
		}
		System.out.println("딜러 : " + dealerIncome);
	}
	
	public static double checkScore(Player player, Dealer dealer, double playerBettingMoney) {
		double money = blackjackCheck(player, dealer, playerBettingMoney);
		if (money == 0) {
			money = dealerOutCheck(dealer, playerBettingMoney);
		}
		if (money == 0) {
			money = playerOutCheck(player, playerBettingMoney);
		}
		if (money == 0) {
			money = winOrLoseCheck(player, dealer, playerBettingMoney);
		}
		return money;
	}
	
	public static double blackjackCheck(Player player, Dealer dealer, double playerBettingMoney) {
		double money = 0;
		if (player.showScore() == 21 && player.howManyCard() == 2 && (dealer.showScore() != 21 || dealer.howManyCard() != 2)) {
			money = playerBettingMoney*1.5;
		}
		if (player.showScore() == 21 && player.howManyCard() == 2 && dealer.showScore() == 21 && dealer.howManyCard() == 2) {
			money = playerBettingMoney;
		}
		return money;
	}
	
	public static double dealerOutCheck(Dealer dealer, double playerBettingMoney) {
		double money = 0;
		if (dealer.showScore() > 21) {
			money = playerBettingMoney;
		}
		return money;
	}
	
	public static double playerOutCheck(Player player, double playerBettingMoney) {
		double money = 0;
		if (player.showScore() > 21) {
			money = -playerBettingMoney;
		}
		return money;
	}
	
	public static double winOrLoseCheck(Player player, Dealer dealer, double playerBettingMoney) {
		double money = 0;
		if (player.showScore() > dealer.showScore()) {
			money = playerBettingMoney;
		}
		if (player.showScore() < dealer.showScore()) {
			money = -playerBettingMoney;
		}
		return money;
	}

}

MoneyCalculator.java

처음에 배팅한 금액을 승패에 따라 처리하고 마지막에 최종 수익을 보여주는 클래스이다.

calculate 메서드는 Template.java에 들어가면 더 좋았을 것 같긴 한데.. 더 이상 머리가 안 돌아가서 저기 내버려두었다.

경우의 수가 꽤 많아서 라인 10줄 넘지 않게 작성하기가 힘들었다.

 

package domain.main;

import java.util.ArrayList;
import java.util.List;

import domain.card.Card;
import domain.card.CardFactory;
import domain.user.Player;
import domain.user.Dealer;

public class Main {
	
	private static List<Card> cardSet = new ArrayList<>();

	public static void main(String[] args) {
		InputHandler.nameHandler();
		InputHandler.bettingMoneyHandler(InputHandler.nameStringList);
		
		List<Player> playerList = InputHandler.makePlayer();
		Dealer dealer = new Dealer();
		cardSet = CardFactory.cardShuffled();
		
		mainHelper(playerList, dealer);
	}
	
	public static void mainHelper(List<Player> playerList, Dealer dealer) {
		basicRation(playerList, dealer);
		Template.interimResult(playerList, dealer);
		InputHandler.oneMoreCardOrNot(playerList);
		dealerMoreCard(dealer);
		Template.finalResult(playerList, dealer);
		MoneyCalculator.calculate(playerList, dealer);
	}
	
	public static void basicRation(List<Player> playerList, Dealer dealer) {
		giveOneCardToDealer(dealer);
		giveOneCardToDealer(dealer);
		for (Player player : playerList) {
			giveOneCardToPlayer(player);
			giveOneCardToPlayer(player);
		}
	}
	
	public static void dealerMoreCard(Dealer dealer) {
		if (dealer.showScore() <= 16) {
			Template.dealerOneMoreCard();
			giveOneCardToDealer(dealer);
			dealerMoreCard(dealer);
		}
	}
	
	public static void giveOneCardToDealer(Dealer dealer) {
		Card oneCard = CardFactory.pickOneCard(cardSet);
		dealer.addCard(oneCard);
	}
	
	public static void giveOneCardToPlayer(Player player) {
		Card oneCard = CardFactory.pickOneCard(cardSet);
		player.addCard(oneCard);
	}

}

Main.java

마지막! 메인 클래스이다.

main과 mainHelper 메서드를 제외한 나머지 친구들은 메인 클래스 말고 다른 곳에 넣어도 될듯하지만,

포지션이 어중간한.. 그런 메서드들이라 Main.java임에도 불구하고 창고 같은 느낌이 난다.

 


상속 처리

제출 전에 상속에 대해서 다시 찾아보고, Player 클래스를 Dealer의 자식 클래스로 만들었다.

 

하드코딩 처리 1
하드코딩 처리 2
하드코딩 처리 3

 

그리고 이전 피드백으로 받았던 하드코딩에 대해 살펴보고 다듬었다.

인원과 기준 숫자 등은 게임 룰에 따라 바뀔 수 있으니 메서드로 따로 빼주었다.


중간중간에 git push를 하면 에러가 떴는데, README를 작성하고 pull을 하지 않은 상태라 그랬다.

그리고 enum을 새로 접했는데 아직까지 왜 쓰는지, 쓰면 뭐가 좋은지 잘 모르겠다.

하루에 3~4시간씩 4일? 5일? 정도 한 것 같은데, 오프라인 시험에서 시간에 맞춰 제출할 수 있을지 걱정이 된다.

도중에 List나 String, Double 등등의 부속 기능 사용법을 검색하느라 더 오래 걸린 것 같다.

시험 치러 가기 전에 자주 쓰는 기능들은 한번 더 체크하고, 상속에 대해서 좀 더 알아보고 가야겠다.

728x90
반응형

댓글