사회자가 게임을 시작하면 가장 먼저 실행되는 state가 '밤'이다
public void start() {
init_game();
while(true) {
this.set_state(new 밤());
this.gameState.execute(매니저);
checkEnd();
this.set_state(new 토론());
this.gameState.execute(매니저);
this.set_state(new 투표());
this.gameState.execute(매니저);
checkEnd();
dayCount++;
}
}
사회자
public void execute(사회자 매니저) {
매니저.getCommandManager().broadcastAll("System:"+"밤이 시작되었습니다. 30초 안에 각자 능력을 사용할 플레이어의 ID를 입력해주세요");
// 시간제한 30초
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
nightResult(매니저);
// 다음 밤 위해 초기화
for (Player p : 매니저.players) {
p.setNightTargetId(0);
}
}
밤
1. 30초 동안 사용자들은 능력을 사용할 대상을 skillField에 입력한다. View는 "Target:" Tag를 붙여서 Client에게 보낸다.
skillField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
JTextField input = (JTextField) e.getSource();
String targetId = input.getText();
clientManager.sendMessage("Target:" + targetId);
input.setText("");
}
});
View
2. ClientManager -> ClientThread -> OutputThread -> ServerThread를 거쳐서 CommandManager에 도착하면 CommandManager는 TargetCommand를 실행한다.
public class TargetCommand implements ICommand {
private CommandManager networkBrain;
private 사회자 logicBrain;
private int count = 0;
public TargetCommand(CommandManager cm, 사회자 logic) {
this.networkBrain = cm;
this.logicBrain=logic;
}
@Override
public void execute(ServerThread sender, String payload, IState currentState) {
int targetId = Integer.parseInt(payload);
Player p = sender.getPlayer();
if(currentState instanceof 밤) {
if (p != null) {
if(p.is_alive) {
p.setNightTargetId(targetId);
System.out.println("[서버] " + p.nickname + " -> 타겟 " + targetId + " 설정완료");
}
}
}
}
}
TargetCommand
3. 밤 state의 nightResult함수가 밤의 결과를 계산하여 출력한다.
private void nightResult(사회자 매니저) {
//초기화
int mafiaTargetId = -1;
int doctorTargetId = -1;
int policeTargetId = -1;
//안정성을 위해 잠시 시간 두기
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 각 역할별로 이번 밤에 누굴 골랐는지 모은다.
for (Player p : 매니저.players) {
//죽은 사람은 능력 사용 불가능
if (!p.is_alive)
continue;
int target = p.getNightTargetId();
System.out.println("밤타겟:"+target); //디버그용
String role = p.getRole();
if ("mafia".equals(role)) {
mafiaTargetId = target;
} else if ("doctor".equals(role)) {
doctorTargetId = target;
} else if ("police".equals(role)) {
policeTargetId = target;
System.out.println("경찰 조사 대상:"+policeTargetId +" target: "+ target);//디버그용
// 0인지 체크하는 걸 추가했음
if (policeTargetId == 0) {
p.getServerThread().sendMessage("System:시간 내에 대상을 지목하지 못했습니다.");
continue;
}
Player 피조사자 = 매니저.getPlayerById(policeTargetId);
if (피조사자 != null) {
if (피조사자.getRole().equals("mafia")) {
p.getServerThread().sendMessage("Inspect:1");
} else {
p.getServerThread().sendMessage("Inspect:0");
}
} else {
System.out.println("경찰: 잘못된 대상을 지목했습니다.");
}
}
}
// 마피아가 지목해서 사망했으면
if (mafiaTargetId != 0) {
Player 사망자 = 매니저.getPlayerById(mafiaTargetId);
if (사망자 != null && 사망자.is_alive) {
// 의사랑 마피아랑 똑같은 애 지목하면
if (mafiaTargetId == doctorTargetId) {
매니저.setKilledID(0);
String msg = "System:" + "[밤 결과] "+사망자.id + "번 플레이어가 공격당했지만 의사의 치료로 생존했습니다!";
System.out.println(msg); // 서버 로그
매니저.getCommandManager().broadcastAll(msg);
return;
}
else {
사망자.is_alive = false;
매니저.ghosts.add(사망자);
매니저.setKilledID(사망자.id);
String msg = "System:" + "[밤 결과] "+ 사망자.id + "번 플레이어가 사망했습니다.";
//Jlist 업데이트
매니저.getCommandManager().broadcastAll("List:"+(사망자.id));
System.out.println(msg); // 서버 로그
매니저.getCommandManager().broadcastAll(msg);
}
}
else {
System.out.println("[결과] 마피아가 지목한 대상이 유효하지 않습니다.");
}
}
}
밤
4. ClientManager는 Inspect tag를 받으면 1인지 0인지 검사하여 결과를 출력하고, System tag를 받으면 강조 표시를 추가하여 출력한다.
else if (message.startsWith("Inspect:")) {
String result = message.substring(8);
if (result.equals("1"))
lobby.getView().allChat("======================조사결과, 마피아입니다======================");
else if (result.equals("0"))
lobby.getView().allChat("======================조사결과, 마피아가 아닙니다======================");
}
else if (message.startsWith("System:")) {
String sysMsg = message.substring(7);
lobby.getView().allChat("======================" + sysMsg+ "======================"); // 채팅창에 출력
}
ClientManager
밤 state를 만드는 중에 또 문제가 발생했다. 분명 setNightTargetId로 TargetID를 각 Player별로 잘 저장해뒀고, 출력도 잘 되었는데 nightResult함수가 꺼내 쓰려고 하니 또 비어있다는 것이다. 처음 결과를 출력하는 부분이 경찰에게만 결과를 보여주는 부분이라 계속 그 line에서 error가 났다. 그래서 처음에는 경찰에게만 결과를 보여주는 logic이 잘못된 줄 알았다. 그건 아니었다. 2시간 정도 팀원 4명이 모두 머리를 싸매고 고민했다.
또다시 싱글톤을 의심했다. 그런데 정말 우리의 의심처럼 사회자 객체가 Server에 하나, Client 측에 하나 만들어졌다면, player 생성부터 문제가 생겼어야 한다. Server가 사회자의 createPlayer를 호출하여 player를 생성했으니 Client 쪽의 player들은 모두 비어있어야 하는 것이다. 근데 그렇지 않았다. 또 현경이가 해쉬코드를 다 찍어봤는데 다 똑같았다. 이로써 우리 프로그램에 사회자는 확실히 1개라는 것이 밝혀졌다.
통신의 문제도 아니다. 우리는 join부터 계속 같은 로직으로 통신을 진행했다. 심지어 지금은 채팅도 매우 잘 된다. 이유를 알 수 없는 문제를 고치려니 정말 막막했다. 모든 코드를 보고 또 봤다. 나중에는 각자 AI들이랑 계속 대화를 나눴다. 코드가 많아서 AI한테 넣어주는 것도 쉽지 않았다.
교수님께 편지도 썼다

그때 남규 언니 gpt가 Multi Thread 문제 같다면서 딱 한 줄을 바꿔보라고 헀다.
new Thread(() -> logicBrain.start()).start();
CommandManager
사회자의 start함수를 호출하는 부분을 람다식으로 바꿨다. 그랬더니 경찰결과가 화면에 나왔다. 진짜 모든 팀원이 소리 지르고 난리도 아니었다. 아마 프로젝트 진행 중 가장 기쁜 순간이었던 것 같다.
그렇지만 람다식이라서 찝찝한 기분이 들었다. 인간 4명이 2시간을 쏟았는데 AI한테 진 기분이었다...
일단 시간이 너무 늦어서 이해는 내일 하고 집에 가기로 했다.
그리고 집에 가는 길에 밤이 끝난 후의 결과를 알려주는 System 메세지도 안 나온 줄 알고 울었다

이번 프로젝트 기간 중 가장 절망적인 순간이었다. 더 이상 의심할 이유도 없는 상황에서 새로운 문제 발생이라니, 기한 내에 절대 완성하지 못할 것 같았다.
다음날에 보니 우리가 출력문을 아직 안 넣은 상태였다. 괜히 울었다.
람다식은 새로운 thread를 만들어서 그 thread가 사회자의 start 함수를 호출하도록 하는 식이었다. 우리의 원래 Server Thread가 CommandManager.processMessage()를 호출했고 Start 명령을 처리하면서 사회자의 start()를 실행, 밤 state의 excute 함수 안에 시간 제한을 위해 넣어둔 Thread.sleep()을 보고 잠에 들어버렸다. 그래서 아무리 targetID를 저장해도 받질 못했다. 그래서 start()호출하는 thread를 따로 만들어주자 문제가 해결되었던 것이다. Multi Thread 문제까지 겪다니 우리가 겪을 수 있는 문제는 다 겪는 것 같다.
'JAVA' 카테고리의 다른 글
| [마피아] 완성 (0) | 2025.12.02 |
|---|---|
| [마피아] 투표 (0) | 2025.12.02 |
| [마피아] Message (0) | 2025.12.02 |
| [마피아] Role (0) | 2025.12.01 |
| [JAVA] Spring Boot - 2 (0) | 2025.11.25 |