[LG CNS AM Inspire Camp] 13. 자바 실습 - 끝말잇기 게임 (with CLI)
0. 들어가며
자바 학습 주차가 끝나고 진행했던 조별 미니 프로젝트에서 만들었던 끝말잇기 게임에 대한 간단한 회고 글을 뒤늦게 작성해본다. Github에는 read me 작성해서 간단하게 올려놓았지만, 블로그에도 흔적을 남기리라..
1. 뭘 만들어야 재밌을까
이번 CLI 기반 자바 실습은 예시로 도서 검색 프로그램이 주어져, 자료 검색을 제시했다.
예전에 자바를 처음 배울 때 CLI 기반으로 계산기, 숫자야구를 만들어본 경험이 있었는데
팀원중에서 한때 모바일 게임 앱으로 대박쳤던 2048 게임(헉..), 다마고치 등을 만든다는걸 듣고 게임을 만들어보자 라고 생각했다.
텍스트 기반의 간단한 RPG 게임같은걸 만들어볼까.. 하다가 뭔가 볼륨도 너무 클 것 같고 완성도를 맞추기에는 시간이 많이 모자랄 것 같아 포기했다.
그러다 생각난게 쿵쿵따, 끄투 같은 끝말잇기 게임이었다!
국어사전 오픈API는 무조건 있을테고, 찾아보니 다행히 무료로 사용이 가능해서 바로 코딩 시작
2. 뜻밖의 외부 API 적용해서 만들어보기
우선, 이 프로젝트는 Java로 작성된 CLI 기반 끝말잇기 게임으로, 사용자가 입력한 단어를 바탕으로 컴퓨터와 번갈아 가며 단어를 이어가는 게임이다. 국립국어원 사전 API를 활용하여 단어의 유효성을 검증하며, 중복 단어 사용 여부도 관리한다.
코드는 크게 다음과 같은 주요 컴포넌트로 구성되어 있다!
- Minyeong (게임 실행 및 루프 관리)
- WordChainGame (게임 진행 로직)
- UsedWordsRepository (사용된 단어 저장 및 관리)
- WordChainService (게임 로직 및 단어 검증, API 연동)
2-1. Minyeong.start
package com.wordChain;
import java.util.Scanner;
public class Minyeong {
public void start(Scanner scanner) {
WordChainGame game = new WordChainGame(scanner);
while (true) {
game.start();
System.out.println("=======================================");
System.out.print("재시작 하시겠습니까? (y/n): ");
String input = scanner.nextLine().trim().toLowerCase();
while (!input.equals("y") && !input.equals("n")) {
System.out.println("=======================================");
System.out.print("y 또는 n을 입력해주세요: ");
input = scanner.nextLine().trim().toLowerCase();
}
if (input.equals("n")) {
System.out.println("=======================================");
System.out.println("게임을 종료합니다");
break;
}
game.resetGame();
}
scanner.close();
}
}
WordChainGame의 인스턴스를 생성하고 게임을 시작한다.
게임이 끝날 때마다 사용자가 재시작 여부를 입력받아 다시 시작할지 종료할지를 결정한다.
지금보니 scanner.close() 는 어차피 팀원들의 코드를 묶을때 Main에서 프로그램을 종료시킬때 scanner.close()를 사용하는게 더 적절했을 것 같다..
2-2. WordChainGame
package com.wordChain;
import java.util.Scanner;
public class WordChainGame {
private final Scanner scanner;
private final WordChainService service;
private final UsedWordsRepository usedWordsRepository;
public WordChainGame(Scanner scanner) {
this.scanner = scanner;
this.service = new WordChainService();
this.usedWordsRepository = new UsedWordsRepository();
}
public void start() {
System.out.println("============= 끝말잇기 게임 =============");
System.out.println("제시어 주제를 선택하면 컴퓨터부터 시작해요");
System.out.println("'자유'를 선택하면 사용자가 원하는 단어를 입력해서 먼저 공격해요");
System.out.println("------------- Info --------------");
System.out.println("일부 단어들은 두음법칙을 적용하고 있어요");
System.out.println("단어는 국립국어원 사전검색을 기준으로 유효성을 판단하고 있어요");
System.out.println("2글자 이상의 단어를 입력해야하며, 사용했던 단어는 재사용 시 패배해요");
System.out.println("=======================================");
System.out.println("주제: [사물, 동물, 과일, 직업, 자유]");
System.out.print("주제를 입력하세요: ");
String category = scanner.nextLine().trim();
if (!service.isValidCategory(category)) {
System.out.println("[사물, 동물, 과일, 직업, 자유] 중에서 선택해주세요. 게임 종료");
return;
}
String computerWord = service.initializeGame(category, usedWordsRepository);
if (computerWord != null) {
System.out.println("=======================================");
System.out.println("컴퓨터: " + computerWord);
System.out.println("=======================================");
} else {
System.out.println("자유 주제입니다. 사용자가 먼저 단어를 입력하세요.");
}
while (true) {
System.out.print("사용자 차례! 단어를 입력하세요: ");
String userWord = scanner.nextLine().trim();
if (usedWordsRepository.isUsed(userWord)) {
System.out.println("=======================================");
System.out.println("저런, 이미 사용된 단어에요. 컴퓨터 승리!");
break;
}
if (!service.isValidWord(userWord, computerWord)) {
System.out.println("=======================================");
System.out.println("국어사전에 등재되어 있지 않은 단어에요. 컴퓨터 승리!");
break;
}
usedWordsRepository.addWord(userWord);
// 컴퓨터 응답 처리
String computerResponse = service.getComputerResponse(userWord, usedWordsRepository);
if (computerResponse == null) {
System.out.println("컴퓨터가 단어를 찾지 못했어요. 사용자 승리!");
break;
}
// 컴퓨터 단어 업데이트
computerWord = computerResponse;
System.out.println("=======================================");
System.out.println("컴퓨터: " + computerResponse);
System.out.println("=======================================");
}
}
public void resetGame() {
usedWordsRepository.clear();
}
}
게임의 전반적인 시퀀스를 담당하는 클래스다.
사용자와 컴퓨터가 번갈아 가며 단어를 입력하는 로직을 처리한다.
WordChainService를 활용해 단어 유효성을 검사하고, UsedWordsRepository를 사용해 중복 여부를 확인한다.
2-3. UsedWordsRepository
package com.wordChain;
import java.util.HashSet;
import java.util.Set;
public class UsedWordsRepository {
private final Set<String> usedWords = new HashSet<>();
public void addWord(String word) {
usedWords.add(word);
}
public boolean isUsed(String word) {
return usedWords.contains(word);
}
public void clear() {
usedWords.clear();
}
}
사용자가 입력한 단어를 HashSet을 사용해 저장하고, 중복 여부를 빠르게 확인할 수 있도록 한다.
2-4. WordChainService
package com.wordChain;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
public class WordChainService {
private static final Map<String, List<String>> predefinedWords = new HashMap<>();
private static final String API_URL = "https://opendict.korean.go.kr/api/search";
private static final String API_KEY = "실제 APP KEY 입력";
private String lastCharacter;
static {
predefinedWords.put("과일", Arrays.asList("사과", "수박", "바나나", "포도", "딸기", "자두"));
predefinedWords.put("동물", Arrays.asList("고양이", "강아지", "토끼", "코끼리", "사자"));
predefinedWords.put("사물", Arrays.asList("책상", "의자", "스피커", "볼펜", "노트"));
predefinedWords.put("직업", Arrays.asList("의사", "선생님", "프로그래머", "디자이너", "경찰관"));
predefinedWords.put("자유", Collections.emptyList());
}
public boolean isValidCategory(String category) {
return predefinedWords.containsKey(category);
}
public String initializeGame(String category, UsedWordsRepository usedWordsRepository) {
if (!category.equals("자유")) {
String word = getRandomWord(predefinedWords.get(category));
usedWordsRepository.addWord(word);
lastCharacter = getLastCharacter(word);
return word;
}
return null;
}
public boolean isValidWord(String userWord, String computerWord) {
if (computerWord == null) return true;
// 컴퓨터 단어의 끝 글자 추출 및 두음법칙 적용
String lastChar = applyInitialSoundChange(getLastCharacter(computerWord));
String firstChar = userWord.substring(0, 1);
// 두 글자 비교
if (!firstChar.equals(lastChar)) {
System.out.printf("[DEBUG] 단어 '%s'의 첫 글자 '%s'가 이전 단어 '%s'의 끝 글자 '%s'와 일치하지 않습니다.%n",
userWord, firstChar, computerWord, lastChar);
return false;
}
// 단어 유효성 확인
return checkWordInDictionary(userWord);
}
public String getComputerResponse(String userWord, UsedWordsRepository usedWordsRepository) {
// 사용자 단어의 끝 글자 추출
lastCharacter = applyInitialSoundChange(getLastCharacter(userWord));
System.out.println("=======================================");
System.out.println("컴퓨터가 단어를 생각하고 있어요...");
List<String> validWords = findWordsFromApi(lastCharacter);
// 유효한 단어 필터링
for (String word : validWords) {
if (!usedWordsRepository.isUsed(word) && word.length() >= 2) {
usedWordsRepository.addWord(word);
return word; // 컴퓨터 단어로 반환
}
}
return null; // 유효한 단어가 없을 경우 null 반환
}
private List<String> findWordsFromApi(String lastChar) {
List<String> validWords = new ArrayList<>();
try {
for (int start = 1; start <= 1000; start += 100) {
// API 요청 URL 구성
String queryUrl = API_URL + "?key=" + API_KEY +
"&req_type=json&q=" + lastChar +
"&start=" + start +
"&sort=dict&advanced=y&method=start&type1=word";
HttpURLConnection connection = (HttpURLConnection) new URL(queryUrl).openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println("[DEBUG] API 응답: " + response);
JSONObject json = new JSONObject(response.toString());
// 응답 데이터에서 단어 추출
if (json.has("channel") && json.getJSONObject("channel").has("item")) {
JSONArray items = json.getJSONObject("channel").getJSONArray("item");
for (int i = 0; i < items.length(); i++) {
JSONObject item = items.getJSONObject(i);
String word = item.getString("word").replaceAll("-", "");
if (word.startsWith(lastChar) &&
word.length() >= 2 &&
word.matches("^[가-힣]+$") &&
item.getJSONArray("sense").getJSONObject(0).getString("pos").equals("명사")) {
validWords.add(word);
}
}
}
// 응답에 단어가 없을 경우 반복 종료
if (json.has("channel") && json.getJSONObject("channel").getInt("num") == 0) {
break;
}
}
} else {
System.out.println("[DEBUG] API 호출 실패: 응답 코드 " + responseCode);
}
}
} catch (Exception e) {
System.out.println("[DEBUG] API 호출 중 오류 발생: " + e.getMessage());
}
return validWords; // 유효한 단어 리스트 반환
}
private boolean checkWordInDictionary(String word) {
try {
URL url = new URL(API_URL + "?key=" + API_KEY + "&req_type=json&q=" + word);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
JSONObject json = new JSONObject(response.toString());
if (json.has("channel") && json.getJSONObject("channel").has("item")) {
JSONArray items = json.getJSONObject("channel").getJSONArray("item");
for (int i = 0; i < items.length(); i++) {
if (items.getJSONObject(i).getString("word").replaceAll("-", "").equals(word)) {
return true;
}
}
}
}
}
} catch (Exception e) {
System.out.println("[DEBUG] API 호출 중 오류 발생: " + e.getMessage());
}
return false;
}
private String getRandomWord(List<String> words) {
Random random = new Random();
return words.get(random.nextInt(words.size()));
}
private String getLastCharacter(String word) {
if (word == null || word.isEmpty()) return "";
return word.substring(word.length() - 1);
}
// 두음법칙 적용
private static String applyInitialSoundChange(String character) {
if (character == null || character.isEmpty()) return character;
switch (character) {
case "락": return "낙";
case "랄": return "날";
case "람": return "남";
case "랍": return "납";
case "랑": return "낭";
case "래": return "내";
case "랭": return "냉";
case "로": return "노";
case "록": return "녹";
case "론": return "논";
case "롤": return "놀";
case "롬": return "놈";
case "롱": return "농";
case "뢰": return "뇌";
case "료": return "요";
case "륙": return "육";
case "률": return "율";
case "륜": return "윤";
case "륭": return "융";
case "니": return "이";
case "님": return "임";
case "륨": return "윰";
case "늄": return "윰";
default: return character; // 변환이 필요 없는 문자는 그대로 반환
}
}
}
- initializeGame(): 주어진 카테고리에 맞는 단어를 랜덤으로 선택한다.
- isValidWord(): 사용자가 입력한 단어가 규칙을 따르는지 확인한다.
- getComputerResponse(): 컴퓨터가 입력할 단어를 결정한다.
- findWordsFromApi(): 국립국어원 API를 사용하여 단어 리스트를 가져온다.
- checkWordInDictionary(): 단어의 유효성을 확인한다.
국립국어원 API에 대한 내용은
https://opendict.korean.go.kr/service/openApiInfo 에서 확인할 수 있다.
API_KEY 같은 경우에는 별도의 파일에 설정해두고 불러오는 방식이 더 안전하다. 하지만 짧은 시간내에 팀원 간 코드 통합 문제가 있어서 어차피 짧게 하는 프로젝트라서 하드코딩하는 방식으로 코드를 짰다.
3. 마무리
만들고 테스트해보면서 나름 재밌게 놀았던 미니 프로젝트였다.
한방단어, 중복단어 사용 체크, 두음법칙 체크 등 예외 상황을 테스트해보면서 수정도 많이하고 재밌었다.
간단하게 CLI 기반으로 만들기 위한 프로젝트였지만 나중에 볼륨 좀 키워서 만들어봐도 괜찮을 것 같다는 생각이 든다.