diff --git a/tractor-game-simulator/server/src/socket/handlers/roomHandlers.js b/tractor-game-simulator/server/src/socket/handlers/roomHandlers.js index 38757ce..77a405d 100644 --- a/tractor-game-simulator/server/src/socket/handlers/roomHandlers.js +++ b/tractor-game-simulator/server/src/socket/handlers/roomHandlers.js @@ -433,24 +433,29 @@ function removePlayerPermanently(io, roomManager, room, player, socket) { playerName: player.name }); - // 如果房主离开,转移房主权限或删除房间 + // 如果房间内已无真人玩家(只剩 bot 或完全为空),直接清理房间, + // 避免仅剩 bot 时房间残留、房间列表不断堆积的问题。 + const hasHumanPlayer = room.players.some(p => !p.isBot); + if (!hasHumanPlayer) { + const gameEngines = getGameEngines(); + const botServices = getBotServices(); + gameEngines.delete(room.id); + botServices.delete(room.id); + roomManager.deleteRoom(room.id); + logger.info(`玩家 ${player.name} 离开房间: ${room.id},房间内已无真人玩家,房间已删除`); + return; // 房间已删除,不需要再广播 + } + + // 如果房主离开,将房主转移给下一位真人玩家(跳过 bot) if (room.hostId === player.socketId) { - if (room.players.length > 0) { - room.hostId = room.players[0].socketId; + const newHost = room.players.find(p => !p.isBot); + if (newHost) { + room.hostId = newHost.socketId; io.to(room.id).emit('host_changed', { - newHostId: room.players[0].id, - newHostName: room.players[0].name + newHostId: newHost.id, + newHostName: newHost.name }); - logger.info(`房间 ${room.id} 房主转移给: ${room.players[0].name}`); - } else { - // 房间被删除时也清理游戏引擎和bot服务 - const gameEngines = getGameEngines(); - const botServices = getBotServices(); - gameEngines.delete(room.id); - botServices.delete(room.id); - roomManager.deleteRoom(room.id); - logger.info(`玩家 ${player.name} 离开房间: ${room.id},房间已删除`); - return; // 房间已删除,不需要再广播 + logger.info(`房间 ${room.id} 房主转移给: ${newHost.name}`); } } diff --git a/tractor-game-simulator/server/test/roomBotCleanup.test.mjs b/tractor-game-simulator/server/test/roomBotCleanup.test.mjs new file mode 100644 index 0000000..ccbc3ee --- /dev/null +++ b/tractor-game-simulator/server/test/roomBotCleanup.test.mjs @@ -0,0 +1,103 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { RoomManager } from '../src/services/RoomManager.js'; +import { registerRoomHandlers } from '../src/socket/handlers/roomHandlers.js'; + +class FakeSocket { + constructor(id) { + this.id = id; + this.handlers = new Map(); + this.emitted = []; + this.joinedRooms = new Set(); + } + on(event, handler) { this.handlers.set(event, handler); } + emit(event, payload) { this.emitted.push({ event, payload }); } + join(roomId) { this.joinedRooms.add(roomId); } + leave(roomId) { this.joinedRooms.delete(roomId); } + to() { return { emit: () => {} }; } + trigger(event, payload) { return this.handlers.get(event)?.(payload); } + last(event) { return this.emitted.findLast(e => e.event === event)?.payload; } +} + +function createIo() { + return { + broadcasts: [], + to(roomId) { + return { + emit: (event, payload) => { + this.broadcasts.push({ roomId, event, payload }); + } + }; + } + }; +} + +function setupRoomWithBots({ humanCount = 1, botCount = 2 } = {}) { + const io = createIo(); + const roomManager = new RoomManager(); + + // 房主(真人) + const hostSocket = new FakeSocket('socket-host'); + registerRoomHandlers(io, hostSocket, roomManager); + hostSocket.trigger('create_room', { name: 'bot清理测试', playerName: '房主', config: {} }); + const created = hostSocket.last('room_created'); + const room = roomManager.getRoom(created.room.id); + + // 追加真人 + const extraHumans = []; + for (let i = 1; i < humanCount; i++) { + const s = new FakeSocket(`socket-human-${i}`); + registerRoomHandlers(io, s, roomManager); + s.trigger('join_room', { roomId: room.id, playerName: `真人${i}` }); + extraHumans.push(s); + } + + // 追加 bot(通过房主) + for (let i = 0; i < botCount; i++) { + hostSocket.trigger('add_bot', { roomId: room.id, botName: `Bot${i + 1}` }); + } + + return { io, roomManager, room, hostSocket, extraHumans }; +} + +test('唯一真人(房主)离开后,房间内只剩 bot,房间应被删除', () => { + const { roomManager, room, hostSocket } = setupRoomWithBots({ humanCount: 1, botCount: 2 }); + assert.equal(room.players.length, 3, '开始应有 1 真人 + 2 bot'); + + hostSocket.trigger('leave_room', { roomId: room.id }); + + assert.equal(roomManager.getRoom(room.id), undefined, '只剩 bot 时房间应被删除'); +}); + +test('非房主真人最后一个离开时也应删除房间', () => { + // 房主真人 + 1 真人 + 1 bot + const { roomManager, room, hostSocket, extraHumans } = setupRoomWithBots({ humanCount: 2, botCount: 1 }); + assert.equal(room.players.length, 3); + + // 先让房主离开:应转给另一位真人,房间保留 + hostSocket.trigger('leave_room', { roomId: room.id }); + assert.ok(roomManager.getRoom(room.id), '仍有真人在,房间应保留'); + const remaining = roomManager.getRoom(room.id); + assert.equal( + remaining.hostId, + extraHumans[0].id, + '房主应转给真人而不是 bot' + ); + + // 最后一名真人离开 → 房间应被删除 + extraHumans[0].trigger('leave_room', { roomId: room.id }); + assert.equal(roomManager.getRoom(room.id), undefined, '最后真人离开后房间应被删除'); +}); + +test('房主离开但仍有其它真人时,房主转给真人(跳过 bot)', () => { + const { roomManager, room, hostSocket, extraHumans } = setupRoomWithBots({ humanCount: 2, botCount: 2 }); + // 房间人员顺序:房主(0) 真人1(1) Bot1(2) Bot2(3) + // 注意 add_bot 是房主主动加的,因此加入顺序中 bot 在真人后面 + + hostSocket.trigger('leave_room', { roomId: room.id }); + const remaining = roomManager.getRoom(room.id); + assert.ok(remaining, '还有真人,房间应保留'); + assert.equal(remaining.hostId, extraHumans[0].id, '新房主必须是真人'); + // 确认房间里仍然保留 bot + assert.ok(remaining.players.some(p => p.isBot), 'bot 应仍然在房间'); +});