게임규칙
1. 총 7명의 플레이어가 모이면 게임 시작
2. 마피아 2명, 의사 1명, 경찰 1명, 시민 3명의 역할이 분배가 됩니다. 랜덤으로
3. 낮에는 투표로 1명 추방 최다 득표 수 플레이어만이 추방됩니다
- 중복투표 불가능, 없는 플레이어명 투표 불가능입니다.
4. 밤에는 특수역할(경찰, 마피아, 의사)만 능력 사용가능.
마피아가 2명일 때는 같은 플레이어야 추방이 가능하고 의사랑 같은 플레이어는 추방이 되지 않는다.
5. 경찰은 능력을 사용해도 되고 안 해도 된다.
HashMap을 사용
Map<ID, 주소>
Map<투표자(ID), 타겟>
Map<타겟(ID),받은 표 개수>
Map<ID,역할>
이것들을 사용하여 게임이 진행되는데 사용되는 정보들을 저장해두었다.
게임이 끝나면 gameID라고 해서 게임의 라운드형식이다. 그리고 나머지 역할들을 맡았던 유저들을 저장하고 게임의 승리를 boolean으로 주어서 결과를 가지게 된다.
회고 할때처럼 많은 메서드들 변수를 사용했다. 참고로 데이터베이스는 빠져있다. 실수다.
1번 플레이어
7번 플레이어
각 플레이어들은 각자의 역할을 본인만 전달 받는다 그리고 투표하려면 플레이명들을 알려주어야하니까 한 번 알려주었다.
서버(관리자역할)
누가 어떤 역할인지 서버는 알게 해두었다.
명령어를 이상하게 했을때 처리를 하였다. 그런데 /vote으로 시작하는 조건문으로 만들어 뒤에 더 쓰면 잘 된다고 인식한다.
그리고 유저명을 잘 입력해야한다.
만약 밤에 사용하는 명령어를 썼을때는 잘못된 명령어라고 알려준다.
동점으로 만들었을땐 추방시키지 않는다. 그리고 투표 정보를 공개한다.
그동안 서버는 어떤 플레이어가 어떤 선택을 했는지 알 수 있고, 지금 어떤 상태인지 알 수 있다.
그동안 마피아로 의사를 죽였다. 그리고 4번 클라이언트는 종료되었다고 표시되었다. 4번 플레이어는 어떤 정보를 받았는지 가보면 알 수 있다.
추방된 플레이어는 채팅이 활성화가 되지 않는다.
마피아를 전부 추방한 경우
이렇게 게임이 끝나고 유저들의 역할이 공개되고 플레이어들의 클라이언트를 종료시키면서 게임이 종료되었다.
서버는 컨트롤러의 startServer()메서드를 실행 시킨다. 그리고 컨트롤러는 내부에 클래스 Handler를 무한반복시킨다. 그리고 내가 만든 메서드들을 실행 시키며 게임이 종료될때까지 돈다.
String message;
while ((message = reader.readLine()) != null) {
try {
Thread.sleep(99); // 0.99초 동안 잠들게 만든다 쓰레드 간섭을 최소화 시키려고 만든 방어로직인데 잘은 모르겠다. 찾아볼것
} catch (Exception e) {
System.out.println("Handle>while>Thread.sleep>>>>" + e.getMessage());
}
// 아침은 참, 저녁은 거짓
if ((MORNING) && !(EVENING)) {
dayTime(userID, message);
}
// 아침은 거짓, 저녁은 참
if (!(MORNING) && (EVENING)) {
night(userID, message);
}
if(!message.startsWith("/"))
broadcastMessage(userID,message);
if(!MORNING && !EVENING) {
System.exit(0);
}
}
반복문이 너무 많은 코드들을 만들어 Handler안의 무한 반복문 안에는 간단히 구현되었으면 해서 이렇게 만들었다.
커다란 메서드는 낮() 과 밤() 으로 만들어 가독성을 높였고 그안에 들어가면 중간 메서드인 낮,밤메서드는 작은메서드들을 배치하였다.
// 낮메서드 > 낮에 필요한 메서드를 하위 메서드들로 넣음
private void dayTime(String userID, String message) throws IOException {
MORNING = true;
EVENING = false;
// UserSelection() -> null일수 있다. 조건문으로 검사해야함
String target = UserSelection(userID, message);
// selectedInformation(투표라면 투표한 상황, 능력사용이라면 능력을 사용한 후 상황)
target = selectedInformation(target);
// 추방
ClientOut(target);
if(EVENING)
broadcast("밤이 되었습니다 [/role 플레이어이름]을 사용하여 능력사용을 진행 할 수 있습니다.");
}
밤 메서드도 똑같은 구조를 이루고 있다.
단지 broadcast라는 출력문만 다르고 MORNING
과 ENENING
의 상태값만 다를뿐이다.
이렇게 만든 이유 역시 처음에 server에 모든것을 넣으면서 아쉬운 부분을 참고해서 만들었다.
// 투표할때 사용할수있고, 밤에는 죽일 유저를 선택할수 있어 공통기능으로 사용될 유저를 선택하는 기능
private String UserSelection(String myID, String message) throws IOException {
// 내 아이디에게 어떤 유저를 선택했는지 보여주려면 나의 아이디와 원하는 유저의 아이디가 필요하다
// 플레이어들의 클라이언트의정보가 담긴 map에서 내 소켓정보를 가져온다.
PrintWriter writer = new PrintWriter(playerSockets.get(myID).getOutputStream(), true);
// 아침이 참이고 저녁이 거짓 && message가 /vote로 시작할때
if (MORNING && !(EVENING) && message.startsWith("/vote")) {
// [/vote 유저명]으로 입력받았을때 " "공백을 기준으로 문자배열에 저장 = ["/vote","유저명"]
String[] wantUserID = message.split(" ");
// 닉네임이 정상적으로 저장되지 않았을때
if (wantUserID.length < 2 || 2 > wantUserID.length) {
writer.println("정상적으로 등록된 유저가 아닙니다.");
return null;
}
// 투표한 사람검증 = Map(내아이디,타겟아이디)의 키값이 참인지 거짓인지 있다면 참으로 리턴받는다.
if (playerVotes.containsKey(myID)) {
writer.println("당신은 이미 투표를 마쳤습니다.");
return null;
// 배열의 길이가 2이고 투표MAP에 나의 아이디가 false라면 (투표를 하면 put으로 값을 넣었다)
} else if (wantUserID.length == 2 && !(playerVotes.containsKey(myID))) {
if (!playerMap.containsKey(wantUserID[1])) {
// 생존자 목록에 없는 경우
writer.println("내가 선택한 유저 닉네임은 현재 생존자 목록에 없습니다.");
} else {
// 생존자 목록에 있는 경우
playerVotes.put(myID, wantUserID[1]);
updateVoteCounts(wantUserID[1]);
writer.println("내가 선택한 유저 닉네임은 [ " + wantUserID[1] + " ] 입니다.");
message = wantUserID[1];
}
}
// 추방하기 위해서 유저이름을 리턴해준다. 비정상일 경우 null을 리턴한다. 마지막 메서드에서 null처리
// 저녁일때 = 역할에 따라 플레이어를 선택 아이디를 리턴, 시민은 안리턴, 경찰은 직업리턴을 해준다.
} else if (!(MORNING) && EVENING && message.startsWith("/role")) {
if (message.startsWith("/role")) {
// [/role 유저명]으로 입력받았을때 " "공백을 기준으로 문자배열에 저장 = ["/role","유저명"]
String[] wantUserID = message.split(" ");
if (!playerMap.containsKey(wantUserID[1])) {
// 생존자 목록에 없는 경우
writer.println("내가 선택한 유저 닉네임은 현재 생존자 목록에 없습니다.");
return null;
}
// 닉네임이 정상적으로 저장되지 않았을때
if (wantUserID.length < 2 || 2 > wantUserID.length) {
writer.println("정상적으로 등록된 유저가 아닙니다.");
return null;
}
// 나에게 출력을 해준다.
String myJob = playerMap.get(myID);
if (playerVotes.containsKey(myJob)) {
writer.println("밤 역할 검증 조건문 >>>>>>>>"); // @@@@@@@@@@@@@@@@@@@@@@
writer.println("당신은 이미 역할을 마쳤습니다.");
return null;
} else if ((wantUserID.length == 2 && !(playerVotes.containsKey(myID))
&& (playerMap.get(myID).contains(DOCTOR) || playerMap.get(myID).contains(MAFIA1)
|| playerMap.get(myID).contains(MAFIA2) || playerMap.get(myID).contains(POLICE)))) {
// 플레이어별 투표 정보 ( 투표를 하고 난 후에 초기화 작업이 이루어져야 한다. 게임체크할때 초기화를 해주면 좋을것같다)
writer.println("playerVotes >>>" + playerVotes );
// 마피아,의사 일때
if (playerMap.get(myID).contains(DOCTOR) || playerMap.get(myID).contains(MAFIA1)
|| playerMap.get(myID).contains(MAFIA2)) {
if (playerMap.get(myID).contains(MAFIA1) || playerMap.get(myID).contains(MAFIA2)) {
// 플레이어별 투표 수
updateVoteCounts(wantUserID[1]);
}
writer.println("[ " + wantUserID[1] + " ]를 선택했습니다.");
// 마피아와 의사의 선택 Map<직업,타겟유저명>
playerVotes.put(playerMap.get(myID), wantUserID[1]);
return wantUserID[1];
//// 경찰일때
} else if (playerMap.get(myID).contains(POLICE)) {
// 경찰이니까 카운트에 올릴 필요 없음
playerVotes.put(playerMap.get(myID), wantUserID[1]);
writer.println("선택한 유저직업은 [ " + playerMap.get(wantUserID[1]) + " ] 입니다.");
return playerMap.get(wantUserID[1]);
// 시민일때
}
} else if (playerMap.get(myID).contains(CITIZEN1) || playerMap.get(myID).contains(CITIZEN2)
|| playerMap.get(myID).contains(CITIZEN3)) {
writer.println("시민은 능력이 없답니다");
return null;
}
// 추방하기 위해서 유저이름을 리턴해준다. 비정상일 경우 null을 리턴한다. 그리고 null의 대한 처리는 낮()에서 처리
return null;
// 아침=거짓, 저녁=거짓일때 방어코드
}
} else if(MORNING&&message.startsWith("/role")){
writer.println("지금은 아침입니다");
return null;
} else if(EVENING && message.startsWith("/vote")){
writer.println("지금은 저녁입니다 투표를 할 수 없습니다.");
return null;
} else if(message.startsWith("/")){
writer.println("명령어를 정확히 입력해주세요");
return null;
}
return message;
// 만약 경찰이 밤에 유저를 선택했다면? 경찰의 능력을 사용할때이므로 밤일때의 조건에서 유저가 경찰일때 조건으로 유저의 아이디가 아닌 유저의
// 역할을 리턴해준다.
}
여기에는 낮에 투표하는 기능과 밤에 투표와 비슷한 역할의 기능을 사용하고 있다.
처음엔 밤에 사용하는 투표기능을 만들고 밤에 사용하는 역할능력을 만들다 보니 어라 이거 기능이 비슷하네?라고 생각해서 팀원과 상의하고 하나로 만들어 사용했다.
지금 와서 보니 밤기능을 따로 만들어 그냥 메서드 호출만 하면 좀 더 보기 쉬울 것 같기도 하다
private String selectedInformation(String target) {
String killUser = null;
if (MORNING) {
if (playerVotes.size() != playerCount) {
return null;
}
// 아침이라면 투표 최다 득표자를 출력
int maxVotes = 0;
Iterator<Map.Entry<String, Integer>> iterator = voteCounts.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
if (entry.getValue() > maxVotes) {
maxVotes = entry.getValue();
MostVotesPlayer = entry.getKey();
}
}
if (isTie(maxVotes)) {
broadcast("동점이 발생하여 추방을 하지 않습니다.");
return "reset";
} else {
broadcast(MostVotesPlayer + "님이 추방결정되었습니다. (" + maxVotes + "표)");
killUser = MostVotesPlayer;
return killUser;
}
}
if (EVENING) {
// 저녁이라면 의사가 마피아 선택과 같은지,마피아가 같은인원을 선택해 마피아의 대상을 출력, 마피아가 다른인원을뽑아 추방이안되는출력
boolean doctorsLive = false; //의사 존재하는지 체크
boolean mafiaTwo = false; //두명의 마피아가 살아있는지 체크
boolean mafiaOne = false; //마피아가 한명 살아있는지 체크
String liveOneMafia = null ; // 살아남은 마피아
String Mafiachose = null; // 마피아들이 지목한 사람
killUser = null; //죽일대상 리턴용
//일단 map에 마피아가 두명 있는지 확인
if( playerMap.values().contains(MAFIA1) && playerMap.values().contains(MAFIA2)) {
mafiaTwo = true; // 두명 있다면 두명에 대한 로직 실행
}else if(playerMap.values().contains(MAFIA1) || playerMap.values().contains(MAFIA2)) {
mafiaOne = true; // 마피아가 한명일때
//마피아가 한명일때 역할의 이름을 리턴해준다.
if(playerMap.values().contains(MAFIA1)) {
liveOneMafia = MAFIA1;
}
else if( playerMap.values().contains(MAFIA2)) {
liveOneMafia = MAFIA2;
}
}
if(playerMap.values().contains(DOCTOR)) {
doctorsLive = true; // 의사가 살아있다면 true
}
//마피아둘이랑 의사랑 살아있을때
if(mafiaTwo && doctorsLive) {
//의사랑 마피아 둘이 투표를 했는지?
if(playerVotes.get(DOCTOR) != null && playerVotes.get(MAFIA1) != null &&playerVotes.get(MAFIA2) != null) {
System.out.println("[의사]플레이어가 선택한 플레이어 :[ "+ playerVotes.get(DOCTOR)+" ]"); //서버(관리자)로그용
//두명의 마피아가 같은것을 선택했는지 체크
if(playerVotes.get(MAFIA1).contains(playerVotes.get(MAFIA2))) {
Mafiachose = playerVotes.get(MAFIA1);
System.out.println("[마피아]유저들이 같은 유저를 지목 :[ " + Mafiachose+" ]");
//마피아와 의사가 같은 플레이어를 선택하였는지 비교
if(playerVotes.get(DOCTOR).contains(Mafiachose) ) {
//의사랑 같은 사람을 지목했을떄
System.out.println("[의사]유저와 [마피아]유저들이 같은 유저를 지목. 추방될 유저 기록을 삭제합니다.\nㄴKillUser : [ "+killUser+ " ]");
broadcast("[의사]가 밤에 플레이어를 살렸습니다.");
killUser = "reset";
}else { //의사와 마피아2명(2명의 지목상대는 같다) 선택이 다를 경우 추방
killUser = Mafiachose;
System.out.println("[마피아]유저들이 선택한 유저와 [의사]유저가 선택한 유저가 다릅니다. 추방을 시작합니다.\nㄴkillUser : [ "+killUser+" ]");
broadcast("[마피아]가 밤에 기습으로 플레이어를 죽였습니다.");
}
}else {//마피아가 다른 플레이어들을 지목한 경우
killUser = "reset";
System.out.println("[마피아]유저들이 서로 다른 유저를 지목했습니다. 추방할 수 없습니다.");
}
}
}else if(mafiaOne && doctorsLive) {//마피아 하나랑 의사랑 살아있을때
//의사와 마피아 하나가 투표 했는지?
if(playerVotes.get(liveOneMafia) != null && playerVotes.get(DOCTOR) != null) {
//같은 플레이어를 지목했는지?
if(playerVotes.get(liveOneMafia).contains(playerVotes.get(DOCTOR))) {
System.out.println("[의사]유저와 [마피아]유저가 같은 유저를 지목. 추방될 유저 기록을 삭제합니다.\nㄴKillUser : [ "+killUser+" ]");
broadcast("[의사]가 밤에 기습으로 플레이어를 살렸습니다.");
killUser = "reset";
}else{
System.out.println("[마피아]유저와 [의사]유저가 선택한 유저가 다릅니다. 추방을 시작합니다.\nㄴkillUser : [ "+killUser+" ]");
killUser = playerVotes.get(liveOneMafia);
broadcast("[마피아]가 밤에 기습으로 플레이어를 죽였습니다.");
}
}
}else if(mafiaTwo) { //마피아 둘만 살아있을때
//두명의 마피아가 살아있고 투표를 진행했는지 체크
if(playerVotes.get(MAFIA1) != null && playerVotes.get(MAFIA2) != null) {
//두명의 마피아가 같은것을 선택했는지 체크
if(playerVotes.get(MAFIA1).contains(playerVotes.get(MAFIA2))) {
System.out.println("[마피아]유저들이 같은 유저를 지목. 추방을 시작합니다.\nㄴKillUser : [" + playerVotes.get(MAFIA1)+" ]");
broadcast("[마피아]가 밤에 기습으로 플레이어를 죽였습니다.");
Mafiachose = playerVotes.get(MAFIA1);
killUser = Mafiachose;
}else { // 다른 플레이어들을 선택했을떄
killUser = "reset";
System.out.println("[마피아]유저들이 다른 유저를 지목. 추방할 수 없습니다");
}
}
}else if(mafiaOne) {//마피아 한명만 살아있을때
//한명의 마피아가 투표를 했는지 체크
if( playerVotes.get(liveOneMafia) != null ) {
System.out.println("[마피아]유저가 유저를 지목. 추방을 시작합니다.\nㄴKillUser : [ " + playerVotes.get(liveOneMafia)+" ]");
broadcast("[마피아]가 밤에 기습으로 플레이어를 죽였습니다.");
killUser = playerVotes.get(liveOneMafia);
}
}
return killUser;
}
return killUser;
}
이것도 마찬가지로 한 명이 선출되어 추방될 플레이어를 고르는 게 낮과 밤이 상관없다고 생각하여 메서드를 통합시켰다.
private void ClientOut(String dUser) {
System.out.println("추방될 유저의 닉네임 = [ " + dUser+" ]");
//투표 정보를 7명이 했을 때 표시
if(playerVotes.size() == playerCount && MORNING) {
System.out.println("투표를 모두 마쳤습니다. 투표 정보를 공개합니다.");
System.out.println(playerVotes);
broadcast("투표를 모두 마쳤습니다.\nㅁㅁㅁㅁㅁㅁ투표정보 공개합니다.ㅁㅁㅁㅁㅁㅁ\n"+playerVotes);
}else if(dUser == null) {
System.out.println("추방할 유저의 정보가 없습니다.\nㄴKillUser : [" + dUser+" ]");
return ;
}
if(dUser == "reset") { // 같은 대상을 선택하여 초기화 진행
playerVotes.clear(); // 투표정보 초기화
//낮과 밤 바꾸기
System.out.println("투표 정보를 초기화 합니다.");
MORNING = !MORNING;
EVENING = !EVENING;
return ;
}
// 해당 플레이어의 클라이언트 소켓을 찾아서 종료
try {
Socket playerSocket = playerSockets.get(dUser);
PrintWriter writer = new PrintWriter(playerSockets.get(dUser).getOutputStream(), true);
if (playerSocket != null && !playerSocket.isClosed()) {
//낮과 밤 바꾸기
writer.println("@@@@@@@@@@@@@@@추방 되었습니다@@@@@@@@@@@@@@@@");
playerSocket.close();
broadcast("추방될 유저의 닉네임 = [ " + dUser+" ]");
broadcast(dUser + "님의 클라이언트가 종료되었습니다.");
Initialization(dUser); // 유저 HshMap의 정보 초기화작업
broadcast("남은 플레이어 : " + userName);
MORNING = !MORNING;
EVENING = !EVENING;
if(gameEndCheck()) {
//게임 종료를 체크하여 게임이 끝나는 상태인지 확인
broadcast("게임이 종료되었습니다.");
broadcast("각 플레이어 직업 :\n"+ copyPlayerMap);
broadcast("모든 플레이어의 접속을 종료합니다.");
saveToDatabase(mafiaWin,civilWin);
playerMap.clear();
playerVotes.clear();
voteCounts.clear();
clientWriters.clear();
playerSockets.clear();//모든 소켓 제거
writer.flush();
}
}
} catch (IOException e) {
System.err.println("클라이언트 종료 중 오류가 발생했습니다: " + e.getMessage());
}
}
아침이라면 투표정보를 공개해 주고 추방을 한다. 그리고 UserSelection에서부터 리턴 받은 값에 null인지 reset을 시키는 값이 들어있는지 현재 게임 중인 유저의 정보가 들어있는지 비교하게 된다.
만약 게임이 끝나면 게임종료해야 하는지 체크해 주는 메서드를 호출해서 참이라면 게임이 종료된다.
메서드를 나눈 것까지 좋았으나 계층을 나누어서 관리했어야 했다고 느낌 유저클래스에 공통적인 기능이 들어가는 필드처럼
데이터 베이스와의 연결이 한 번만 있는 것이 아쉬움 유저 입장 시 저장하고 역할 배정이 끝나고 한 번 더 수정하여 저장이 되거나 다른 테이블에 저장되는 방식을 사용하고 마지막에 이긴 팀과 진 팀을 기록하는 방법을 사용하고 싶었었다.
깃허브를 여럿이서 사용하지 못한 게 제일 아쉽다. 물론 사용할 수는 있었지만 2명씩 나뉘었기에 그럴 필요성을 당시에는 못 느꼈다.
명령어에 조건문 처리를 보완해야 한다.
명령어 전체를 검사하는게 아니라 앞의 단어까지만 검사하기 때문에 좋지 않다