diff --git a/apps/server/src/rooms/bots.room.test.ts b/apps/server/src/rooms/bots.room.test.ts index 023b48c..e13945b 100644 --- a/apps/server/src/rooms/bots.room.test.ts +++ b/apps/server/src/rooms/bots.room.test.ts @@ -312,6 +312,38 @@ describe('practice rooms close once nobody is there', () => { expect(h.manager.get(room.code)).toBeUndefined(); }); + test('a room nobody joined closes as soon as the host leaves it', () => { + const h = harness(); + const room = h.create('A', 'omok', 'public'); + room.join(h.conn('A'), 'player'); + expect(h.manager.publicRooms('omok')).toHaveLength(1); + room.leave('A'); + expect(h.manager.get(room.code)).toBeUndefined(); + expect(h.store.openRooms()).toHaveLength(0); + expect(h.manager.publicRooms('omok')).toHaveLength(0); + // Taking a seat in another room frees the lobby seat: same thing. + const first = h.create('B'); + const b = h.conn('B'); + first.join(b, 'player'); + first.disconnect(b); + h.manager.releaseOtherLobbySeats('B', 'other'); + expect(h.manager.get(first.code)).toBeUndefined(); + }); + + test('the last person leaving a game on purpose closes it; a spectator still watching keeps it', () => { + const h = harness(); + const room = h.create('A'); + room.join(h.conn('A'), 'player'); + room.join(h.conn('B'), 'player'); + room.join(h.conn('S'), 'spectator'); + expect(room.start('A')).toBeNull(); + room.leave('A'); + room.leave('B'); + expect(h.manager.get(room.code)).toBe(room); // S is still connected + room.leave('S'); + expect(h.manager.get(room.code)).toBeUndefined(); + }); + test('rooms without computers keep the old timeouts; someone still connected keeps any room', () => { const h = harness(); const room = h.create('A'); diff --git a/apps/server/src/rooms/manager.ts b/apps/server/src/rooms/manager.ts index f9a6222..7645c3e 100644 --- a/apps/server/src/rooms/manager.ts +++ b/apps/server/src/rooms/manager.ts @@ -50,8 +50,8 @@ export class RoomManager { settings: this.deps.settings, botDelay: this.deps.botDelay, onEmpty: (room, why) => { - // Everyone left a room with computer players on purpose: nobody can come back to it, close it now. - if (why === 'leave' && this.byCode.get(room.code) === room && room.hasBots() && !room.hasPeopleSeated()) this.close(room); + // The last person left on purpose and nobody holds a seat (only computers, or nobody joined): close it now. + if (why === 'leave' && this.byCode.get(room.code) === room && !room.hasPeopleSeated()) this.close(room); }, onSeatChange: (room, userId, seated) => { if (isBotId(userId)) return; diff --git a/docs/06-lobby-rooms.md b/docs/06-lobby-rooms.md index e344d03..19773f6 100644 --- a/docs/06-lobby-rooms.md +++ b/docs/06-lobby-rooms.md @@ -95,7 +95,7 @@ interface Room { ``` - 한 사용자는 동시에 한 방에만 플레이어로 있을 수 있다(다른 방에 들어가면 이전 방 대기실 자리에서 빠짐. 이전 방이 게임 중이면 "진행 중인 게임이 있어요. 그래도 이동할까요?"). - 제한: 서버 전체 방 2,000개, 사용자당 동시에 만든 방 3개. -- 방 정리(1분마다): 접속이 하나도 없는 방 중 대기실은 10분, 끝난 방·멈춘 방은 30분 동안 활동이 없으면 닫는다. 컴퓨터가 있는 방(연습 방)은 마지막 사람이 나간 때부터 재서 대기실·끝난 방은 1분, 게임 중인 방은 30분 뒤 닫는다(사이트를 닫았으면 아래 규칙대로 1분). 사람이 [방 나가기]로 모두 나가 컴퓨터만 남으면 바로 닫는다. +- 방 정리(1분마다): 접속이 하나도 없는 방 중 대기실은 10분, 끝난 방·멈춘 방은 30분 동안 활동이 없으면 닫는다. 컴퓨터가 있는 방(연습 방)은 마지막 사람이 나간 때부터 재서 대기실·끝난 방은 1분, 게임 중인 방은 30분 뒤 닫는다(사이트를 닫았으면 아래 규칙대로 1분). 마지막 사람이 [방 나가기]로 나가서 자리에 앉은 사람이 없으면(컴퓨터만 남았거나 아무도 들어오지 않은 방) 바로 닫는다. - 게임 중이거나 끝난 방(대기실 제외)에서 자리에 앉은 사람이 모두 사이트를 닫으면(서버와 연결된 창이 하나도 없으면) 마지막 사람이 닫은 때부터 1분 뒤 정리 때 닫는다. 진행 중인 게임은 무효 처리되고, 관전자는 셈하지 않고, 남아 있던 관전자도 방에서 내보낸다. 대기실은 초대받은 사람이 들어올 수 있으므로 위 규칙을 따른다. 자리가 있는 사람은 다른 화면에 있어도 서버 연결을 유지한다(새로 연 창에서도 `/api/me`의 `activeRoom`이 있으면 연결). - 방 화면을 벗어나면(홈, 규칙 등) 클라이언트가 `detach`를 보낸다. 자리는 그대로이고 '접속 중'으로 남지만, 방 메시지를 받지 않고 방의 접속 수에서도 빠진다. 그 상태로 사이트를 닫으면 연결 끊김으로 처리한다.