/** * game.js * * 狼人杀主要游戏逻辑文件 * * 依赖: * - waiting_room.js 中的 room 状态 (status, players, maxPlayers) * - websocket.js 提供的 broadcastMessage, sendMessageToUser 等方法 * - db.js 进行数据库写入 (mysql2 pool) * * 导出: * - startGame(room): 在 waiting_room.js 里被调用,开始游戏流程 * - router: 在 server.js 中挂载,用于提供各种接口 (/sheriff_join, /chat, /vote, 等) */ const express = require('express'); const router = express.Router(); const { broadcastMessage, sendMessageToUser } = require('./websocket'); const pool = require('./db'); const { getRoom } = require('./room'); const room = getRoom(); // 身份映射表 const roleTranslation = { seer: '预言家', wolf: '狼人', villager: '平民', witch: '女巫', hunter: '猎人', }; // ======== 全局或模块级变量 ======== // 每次开始游戏时重新初始化 let gameData = { isGameStarted: false, // 标记游戏是否已开始 createTime: null, // 记录游戏开始时间 (房间锁定时) finishTime: null, // 记录游戏结束时间 // 聊天记录 chats: [], // 这里存储所有聊天记录 [{ userId, message, timestamp }, ...] // 玩家角色分配信息: userId => { role, isAlive, isSheriff, ... } // 例如: rolesMap[123] = { role: 'wolf', isAlive: true, isSheriff: false, ... } rolesMap: {}, // 警长相关 sheriffCandidates: [], // 报名竞选警长的玩家列表 (userId 数组) sheriffVotes: {}, // 记录投票情况 sheriffVotes[voterId] = candidateId isSheriffElected: false, // 是否已选出警长 // 白天、黑夜循环控制 dayCount: 1, isNight: false, // 当前是否是夜晚 // 其他游戏过程所需的临时记录 wolfVotes: {}, // 狼人夜晚刀人投票 seerChoice: null, // 预言家查验目标 witchSaveAvailable: true, // 女巫是否还有解药 witchKillAvailable: true, // 女巫是否还有毒药 witchSaveTarget: null, // 女巫要救的人 witchKillTarget: null, // 女巫要毒的人 hunterKillTarget: null, // 猎人带走的目标(如果被狼人杀死时可带走) dayVotes: {}, // 白天放逐投票 dayVotes[voterId] = targetId // 再次警长平票重投的一些标记 reSheriffVoteNeeded: false, // 是否需要重新警长投票 reSheriffCandidates: [], }; // ======== 工具函数 & 倒计时广播 ======== function getNameById(userId) { const player = room.players.find((p) => p.userId === userId); return player ? player.username : `玩家${userId}`; // 如果未找到,返回默认值 } /** * 广播倒计时消息 * @param {number} seconds 倒计时时长 * @param {string} messagePrefix 提示语前缀,如 "警长竞选倒计时" * @param {Function} callback 倒计时结束后的回调 */ function broadcastCountdown(seconds, messagePrefix, callback) { let remaining = seconds; const interval = setInterval(() => { if (remaining <= 0) { clearInterval(interval); callback && callback(); } else { broadcastMessage('系统', { type: 'countdown', message: `${messagePrefix}:${remaining}秒`, remaining, }); remaining--; } }, 1000); } /** * 向游戏的聊天记录中添加一条消息,并广播给所有玩家 * @param {string|number} userId * @param {string} content 聊天内容 */ function addChatMessage(userId, content) { const timestamp = new Date().toISOString(); gameData.chats.push({ userId, message: content, timestamp }); // 广播最新的 chats broadcastMessage('系统', { type: 'chat_update', chats: gameData.chats, }); } /** * 返回房间中存活玩家 * @param {object} room * @returns {Array<{userId, ...}>} */ function getAlivePlayers(room) { return room.players.filter((p) => { const roleData = gameData.rolesMap[p.userId]; return roleData && roleData.isAlive; }); } /** * 判断游戏是否结束,若结束返回胜利阵营信息 * * 新的胜利条件: * - 狼人胜利: * 1. 所有平民(villager)全死。 * 2. 所有神职(seer、witch、hunter)全死。 * - 好人胜利: * 1. 所有狼人(wolf)全死。 * * @param {object} room * @returns {{isEnd: boolean, winner: number|null}} winner: 0 狼人胜, 1 好人胜, null 尚未决定 */ function checkWinCondition(room) { let wolfCount = 0; let villagerCount = 0; // 仅统计平民(villager) let specialRolesCount = 0; // 统计神职(seer、witch、hunter) for (const p of room.players) { const r = gameData.rolesMap[p.userId]; if (!r || !r.isAlive) continue; if (r.role === 'wolf') { wolfCount++; } else if (r.role === 'villager') { villagerCount++; } else if (['seer', 'witch', 'hunter'].includes(r.role)) { specialRolesCount++; } } // 检查好人是否胜利 if (wolfCount === 0) { return { isEnd: true, winner: 1 }; // 好人胜利 } // 检查狼人是否胜利 if (villagerCount === 0 || specialRolesCount === 0) { return { isEnd: true, winner: 0 }; // 狼人胜利 } return { isEnd: false, winner: null }; } /** * 游戏结束时的数据库写入操作 * @param {object} room * @param {number} winner 0狼胜 / 1好人胜 */ async function endGameAndSaveToDB(room, winner) { gameData.finishTime = new Date(); broadcastMessage('系统', { type: 'game_end', winner, rolesMap: gameData.rolesMap, message: `游戏结束!${winner === 0 ? '狼人阵营获胜' : '好人阵营获胜'}`, }); // 写入数据库 plays 表 let playId; try { const [result] = await pool.execute( `INSERT INTO plays (create_time, finish_time, winner, chats) VALUES (?, ?, ?, ?)`, [ gameData.createTime, gameData.finishTime, winner, JSON.stringify(gameData.chats), ] ); playId = result.insertId; } catch (err) { console.error('写入 plays 表出错:', err); // 即使出错,也继续 } // 写入 play_user 表 if (playId) { const promises = room.players.map((p) => { const r = gameData.rolesMap[p.userId]; if (!r) return Promise.resolve(); // 根据阵营确定获胜关系 const isWolf = r.role === 'wolf'; const isWinner = (winner === 0 && isWolf) || // 狼人阵营获胜,狼人都算赢 (winner === 1 && !isWolf); // 好人阵营获胜,非狼人都算赢 return pool.execute( `INSERT INTO play_user (play_id, user_id, identity, is_sheriff, is_alive, is_winner) VALUES (?, ?, ?, ?, ?, ?)`, [ playId, p.userId, r.role, r.isSheriff ? 1 : 0, r.isAlive ? 1 : 0, isWinner ? 1 : 0, ] ); }); await Promise.all(promises).catch((e) => console.error('写入 play_user 表出错:', e) ); } // 结束后倒计时 10 秒,将所有玩家自动退出房间 broadcastCountdown(10, '游戏结束,10秒后将自动退出房间', () => { // 这里可以直接调用 waiting_room.js 的 removePlayerFromRoom // 或者可以直接清空 room.players broadcastMessage('系统', { type: 'game_end_exit', message: '所有玩家将退出房间', }); room.players.length = 0; // 直接清空 room.status = 'closed'; // 重置房间状态 gameData.isGameStarted = false; }); } // ======== 核心逻辑函数 ======== /** * 分配角色 * @param {object} room */ function assignRoles(room) { // 8人:3狼,1预言家,1女巫,1猎人,2平民 const roles = ['wolf', 'wolf', 'wolf', 'seer', 'witch', 'hunter', 'villager', 'villager']; // 随机洗牌 for (let i = roles.length - 1; i > 0; i--) { const rand = Math.floor(Math.random() * (i + 1)); [roles[i], roles[rand]] = [roles[rand], roles[i]]; } // 为 room.players 逐个分配 room.players.forEach((p, index) => { const role = roles[index]; gameData.rolesMap[p.userId] = { role, isAlive: true, isSheriff: false, }; }); // 通知各玩家身份 // 先把狼人的 id 全部取出来 const wolfIds = Object.entries(gameData.rolesMap) .filter(([, val]) => val.role === 'wolf') .map(([key]) => key); for (let [userId, data] of Object.entries(gameData.rolesMap)) { userId = Number(userId); // 转为数字 if (data.role === 'wolf') { // 发送给狼人的消息:队友有哪些 sendMessageToUser(userId, { type: 'role_info', message: '你的身份是:狼人', wolfFriends: wolfIds.map(Number), // 转成数字数组 }); } else { // 发给好人 sendMessageToUser(userId, { type: 'role_info', message: `你的身份是:${roleTranslation[data.role] || '未知身份'}`, }); } } } /** * 开始游戏(房间锁定后调用) * @param {object} room waiting_room.js 中的 room 对象 */ function startGame(room) { if (room.status !== 'locked') { console.warn('房间未锁定,无法开始游戏'); return; } // 初始化 gameData gameData.isGameStarted = true; gameData.createTime = new Date(); gameData.chats = []; gameData.rolesMap = {}; gameData.sheriffCandidates = []; gameData.sheriffVotes = {}; gameData.isSheriffElected = false; gameData.dayCount = 1; gameData.isNight = false; gameData.wolfVotes = {}; gameData.seerChoice = null; gameData.witchSaveAvailable = true; gameData.witchKillAvailable = true; gameData.witchSaveTarget = null; gameData.witchKillTarget = null; gameData.hunterKillTarget = null; gameData.dayVotes = {}; gameData.reSheriffVoteNeeded = false; gameData.reSheriffCandidates = []; // 1. 分配角色 assignRoles(room); // 2. 广播 5秒 准备时间后进入警长竞选 broadcastMessage('系统', { type: 'game_start', message: '游戏开始!5秒后进入警长竞选...', }); broadcastCountdown(5, '警长竞选准备', () => { // === 新增:让前端有一丝缓冲(3秒)再进下一个环节 === setTimeout(() => { // 进入警长竞选 enterSheriffElection(room); }, 3000); }); } /** * 进入警长竞选 (广播 15 秒倒计时,玩家可点击 /sheriff_join 接口) */ function enterSheriffElection(room) { broadcastMessage('系统', { type: 'sheriff_election', message: '警长竞选开始!想要竞选的玩家请在 15 秒内报名!', }); broadcastCountdown(15, '警长竞选报名', () => { // === 新增:让前端有一丝缓冲 === setTimeout(() => { // 报名结束,判断是否有人竞选 if (gameData.sheriffCandidates.length === 0) { broadcastMessage('系统', { type: 'sheriff_election', message: '无人竞选警长,直接进入黑夜。', }); // 直接进入夜晚 setTimeout(() => { enterNight(room); }, 3000); // 再给一点缓冲 } else { // 存在候选人,开始警长竞选发言 broadcastMessage('系统', { type: 'sheriff_election', candidates: gameData.sheriffCandidates, message: '以下玩家参与了警长竞选:' + gameData.sheriffCandidates.map(getNameById).join(','), }); // 竞选发言 startSheriffSpeech(room, [...gameData.sheriffCandidates]); } }, 3000); }); } /** * 警长竞选发言,一个一个来,每人 15 秒 * @param {object} room * @param {Array} candidates 剩余需要发言的候选人 */ function startSheriffSpeech(room, candidates) { if (candidates.length === 0) { // 发言结束,进入投票 broadcastMessage('系统', { type: 'sheriff_election', message: '所有候选人发言完毕,进入警长投票阶段', }); // === 新增:3秒缓冲后进入投票 === setTimeout(() => { startSheriffVote(room); }, 3000); return; } const current = candidates.shift(); const currentName = getNameById(current); // 广播轮到谁发言 broadcastMessage('系统', { type: 'sheriff_speech', userId: current, message: `轮到玩家 ${currentName} 发言(15秒)`, }); broadcastCountdown(15, `玩家${currentName}发言`, () => { // 结束后马上切下一个 startSheriffSpeech(room, candidates); }); } /** * 开始警长投票,时限15秒 */ function startSheriffVote(room) { // 获取存活玩家 const alivePlayers = getAlivePlayers(room).map(p => p.userId); // 只允许【存活且未参与竞选】的玩家投票 const allowedVoters = alivePlayers.filter( id => !gameData.sheriffCandidates.includes(id) ); // 广播:允许投票的玩家 & 候选人 broadcastMessage('系统', { type: 'sheriff_vote', allowedVoters, candidates: gameData.sheriffCandidates, message: '现在开始警长投票!有 15 秒时间!(只有未参加竞选的人可投票)' }); // 15秒倒计时 broadcastCountdown(15, '警长投票', () => { // === 新增:3秒缓冲再开始结算 === setTimeout(() => { // 收集投票结果 const voteCount = {}; for (let voterId of Object.keys(gameData.sheriffVotes)) { const candidateId = gameData.sheriffVotes[voterId]; if (!voteCount[candidateId]) { voteCount[candidateId] = 0; } voteCount[candidateId]++; } // 找出最高票 let maxVotes = 0; let winners = []; Object.entries(voteCount).forEach(([candidateId, vCount]) => { const numericCandidateId = Number(candidateId); // 转换为数字 if (vCount > maxVotes) { maxVotes = vCount; winners = [numericCandidateId]; // 存储数字类型 } else if (vCount === maxVotes) { winners.push(numericCandidateId); // 添加到 winners } }); // 如果没人投票,则随机选 if (Object.keys(voteCount).length === 0) { winners = [ gameData.sheriffCandidates[ Math.floor(Math.random() * gameData.sheriffCandidates.length) ] ]; } if (winners.length > 1) { // 平票,需要重新发言投票 // 调试 // console.log('平票玩家 ID:', winners); // console.log('玩家列表:', room.players); broadcastMessage('系统', { type: 'sheriff_vote_result', detail: gameData.sheriffVotes, message: `警长投票平票,平票玩家:${winners .map((id) => getNameById(id)) .join('、')},重新开始发言再投票` }); // 重置投票结果 gameData.sheriffVotes = {}; // 重新进入发言 startSheriffSpeech(room, winners.map(x => Number(x))); } else { // 选出警长 const sheriffId = Number(winners[0]); const sheriffName = getNameById(sheriffId); gameData.rolesMap[sheriffId].isSheriff = true; gameData.isSheriffElected = true; broadcastMessage('系统', { type: 'sheriff_elected', sheriffId, detail: gameData.sheriffVotes, message: `警长诞生!玩家 ${sheriffName} 当选` }); // 5秒后进入夜晚 broadcastCountdown(5, '警长选举结束,5秒后进入夜晚', () => { // === 新增:再给 3 秒缓冲 === setTimeout(() => { enterNight(room); }, 3000); }); } }, 3000); }); } /** * 进入夜晚 * @param {object} room */ function enterNight(room) { // 先检查当前是否已经是夜晚 if (gameData.isNight) { // 日志警告可以帮助调试:发现有代码重复调用了 enterNight console.warn('[enterNight] 已经是夜晚,忽略本次调用'); return; } gameData.isNight = true; broadcastMessage('系统', { type: 'night', message: '现在进入夜晚', }); // 这里先给一个“夜晚开始前缓冲” setTimeout(() => { // 1. 狼人行动,15 秒 broadcastCountdown(15, '狼人行动时间', () => { // 这里再加 3 秒缓冲后再结算 setTimeout(() => { resolveWolfAction(room); }, 3000); }); // 给狼人发送可刀人信息 for (let [userId, data] of Object.entries(gameData.rolesMap)) { if (data.role === 'wolf' && data.isAlive) { const alivePlayers = getAlivePlayers(room).map((p) => p.userId); sendMessageToUser(Number(userId), { type: 'wolf_action', message: '请选择要刀的玩家', alivePlayers, }); } } }, 3000); } /** * 结算狼人刀人结果 */ function resolveWolfAction(room) { // 统计wolfVotes let voteMap = {}; for (let [wolfId, targetId] of Object.entries(gameData.wolfVotes)) { if (!voteMap[targetId]) voteMap[targetId] = 0; voteMap[targetId]++; } // 找到最高票 let maxVote = 0; let victims = []; for (let [targetId, count] of Object.entries(voteMap)) { if (count > maxVote) { maxVote = count; victims = [targetId]; } else if (count === maxVote) { victims.push(targetId); } } let killedByWolf = null; if (maxVote === 0) { // 狼人都没行动 } else if (victims.length === 1) { killedByWolf = Number(victims[0]); } else { // 平票随机 killedByWolf = Number(victims[Math.floor(Math.random() * victims.length)]); } // 告知狼人结果 for (let [userId, data] of Object.entries(gameData.rolesMap)) { if (data.role === 'wolf' && data.isAlive) { sendMessageToUser(Number(userId), { type: 'wolf_result', killedPlayer: killedByWolf, message: killedByWolf ? `狼人集体决定刀 ${getNameById(killedByWolf)}` : '无人被刀', }); } } // 清空狼投票数据 gameData.wolfVotes = {}; // 2. 预言家行动,10 秒 // === 在狼人结算完后再等 3 秒再开始预言家行动 === setTimeout(() => { broadcastCountdown(10, '预言家行动时间', () => { // 回调再等 3 秒再真正执行 setTimeout(() => { resolveSeerAction(room, killedByWolf); }, 3000); }); // 给预言家单独发送可查验目标 for (let [userId, data] of Object.entries(gameData.rolesMap)) { if (data.role === 'seer' && data.isAlive) { const alivePlayers = getAlivePlayers(room).map((p) => p.userId); sendMessageToUser(Number(userId), { type: 'seer_action', message: '请选择要查验的玩家', alivePlayers, }); } } }, 3000); } /** * 结算预言家查验结果 */ function resolveSeerAction(room, killedByWolf) { const seerChoice = gameData.seerChoice; if (seerChoice) { // 直接给预言家发回结果 const seerId = Object.entries(gameData.rolesMap).find( ([, r]) => r.role === 'seer' && r.isAlive ); if (seerId) { const [seerUserId] = seerId; // 映射角色为通用的 "wolf" 或 "good" const chosenRole = gameData.rolesMap[seerChoice]?.role; const roleMap = { wolf: 'wolf', villager: 'good', seer: 'good', witch: 'good', hunter: 'good', }; const mappedRole = roleMap[chosenRole] || 'unknown'; // 默认为 "unknown" sendMessageToUser(Number(seerUserId), { type: 'seer_result', userId: seerChoice, role: mappedRole, }); } } // 清空 gameData.seerChoice = null; // 3. 女巫行动,15 秒 // === 同样加一个 3 秒缓冲再给女巫倒计时 === setTimeout(() => { broadcastCountdown(15, '女巫行动时间', () => { setTimeout(() => { resolveWitchAction(room, killedByWolf); }, 3000); }); // 女巫可操作 for (let [userId, data] of Object.entries(gameData.rolesMap)) { if (data.role === 'witch' && data.isAlive) { const alivePlayers = getAlivePlayers(room).map((p) => p.userId); // 通知女巫今晚死亡的是谁 sendMessageToUser(Number(userId), { type: 'witch_info', killedByWolf, hasSave: gameData.witchSaveAvailable, hasKill: gameData.witchKillAvailable, alivePlayers, }); } } }, 3000); } /** * 结算女巫结果 */ function resolveWitchAction(room, killedByWolf) { let finalKilledByWolf = killedByWolf; // 女巫使用了解药 if (gameData.witchSaveTarget !== null && gameData.witchSaveTarget === killedByWolf) { if (gameData.witchSaveAvailable) { finalKilledByWolf = null; // 被救活 gameData.witchSaveAvailable = false; // 消耗了解药 } else { console.warn("女巫试图使用解药,但解药已消耗!"); } } // 女巫使用了毒药 let witchKilled = null; if (gameData.witchKillTarget !== null) { if (gameData.witchKillAvailable) { witchKilled = gameData.witchKillTarget; // 记录女巫毒杀的玩家 gameData.witchKillAvailable = false; // 消耗了毒药 } else { console.warn("女巫试图使用毒药,但毒药已消耗!"); } } // 统计当晚死亡的玩家列表 let diedAtNight = []; if (finalKilledByWolf) { diedAtNight.push(finalKilledByWolf); } if (witchKilled && witchKilled !== finalKilledByWolf) { diedAtNight.push(witchKilled); } // 检查猎人是否被杀 let hunterDieByWolf = false; for (let diedId of diedAtNight) { if (gameData.rolesMap[diedId].role === 'hunter') { if (diedId === finalKilledByWolf) { hunterDieByWolf = true; // 猎人被狼人杀 } } } // 更新死亡状态 diedAtNight.forEach((id) => { if (gameData.rolesMap[id]) { gameData.rolesMap[id].isAlive = false; } }); // 进入白天前,先广播夜晚结果 if (diedAtNight.length === 0) { broadcastMessage('系统', { type: 'night_result', message: '这是个平安夜,无人死亡。', }); } else { broadcastMessage('系统', { type: 'night_result', diedPlayers: diedAtNight.map(Number), // 玩家 ID 转换为数字数组 message: `夜晚结束,玩家 ${diedAtNight .map((id) => getNameById(Number(id))) .join(',')} 死亡。`, }); } // 检查胜负条件 const { isEnd, winner } = checkWinCondition(room); if (isEnd) { endGameAndSaveToDB(room, winner); return; } // 通知猎人死亡 if (hunterDieByWolf) { const hunterId = Object.entries(gameData.rolesMap).find(([id, data]) => { return data.role === 'hunter' && !data.isAlive; }); if (hunterId) { const [deadHunterId] = hunterId; broadcastMessage('系统', { type: 'hunter_dead', userId: Number(deadHunterId), message: `猎人 ${getNameById(Number(deadHunterId))} 被杀,可带走一位玩家!10秒内选择!`, }); // 新增:等待 10 秒后自动进入白天 setTimeout(() => { // 在进入白天前,再检查一下有没有结束 const { isEnd, winner } = checkWinCondition(room); if (isEnd) { endGameAndSaveToDB(room, winner); } else { // 如果猎人还没操作就超时,或者他已经操作但游戏没结束 enterDay(room); } }, 10000); return; } } // 如果猎人不能带人或没有猎人死亡,直接进入白天 // === 夜晚结束后给 3 秒缓冲 === setTimeout(() => { enterDay(room); }, 3000); } /** * 进入白天 * @param {object} room */ function enterDay(room) { // 如果已经不是夜晚,就说明现在是白天或正在白天 if (!gameData.isNight) { console.warn('[enterDay] 已经是白天,忽略本次调用'); return; } gameData.isNight = false; gameData.dayCount++; broadcastMessage('系统', { type: 'day', message: `天亮了,现在是第 ${gameData.dayCount} 天白天`, }); // 白天流程:从警长开始顺序发言 -> 投票放逐 const alive = getAlivePlayers(room).map((p) => p.userId); let startIndex = 0; // 如果有警长且存活,则从警长开始 const sheriffId = Object.entries(gameData.rolesMap).find( ([, val]) => val.isSheriff && val.isAlive ); if (sheriffId) { const [sId] = sheriffId; const idx = alive.indexOf(Number(sId)); if (idx !== -1) { startIndex = idx; } } // 根据 players 顺序做个循环队列 const speakingOrder = [...alive.slice(startIndex), ...alive.slice(0, startIndex)]; broadcastMessage('系统', { type: 'day_speech', speakingOrder, message: '现在开始白天发言,每人 15 秒', }); // === 白天开始前也给一点缓冲 === setTimeout(() => { daySpeech(room, speakingOrder); }, 3000); } function daySpeech(room, list) { if (list.length === 0) { // 投票目标(第一轮为所有存活玩家,后续为平票玩家) const voteTargets = getAlivePlayers(room).map((player) => ({ userId: player.userId, username: player.username, })); // 广播进入投票放逐环节的消息,并附加投票对象 broadcastMessage('系统', { type: 'day_vote', message: '所有玩家发言结束,进入投票放逐环节,15秒后结束', voteTargets, }); // 开始投票 startDayVote(room, voteTargets); return; } const current = list.shift(); const currentName = getNameById(current); // 广播当前玩家发言 broadcastMessage('系统', { type: 'day_speech_turn', userId: current, message: `轮到玩家 ${currentName} 发言(15秒)`, }); // 倒计时15秒,继续下一位玩家 broadcastCountdown(15, `玩家${currentName}发言`, () => { daySpeech(room, list); }); } function startDayVote(room, voteTargets) { // 清空 dayVotes gameData.dayVotes = {}; // 投票目标为传入的目标列表 const voteTargetIds = voteTargets.map((p) => p.userId); broadcastCountdown(15, '放逐投票', () => { // 结算投票前再加 3 秒缓冲 setTimeout(() => { const voteCount = {}; for (let voterId of Object.keys(gameData.dayVotes)) { const targetId = gameData.dayVotes[voterId]; if (!voteCount[targetId]) { voteCount[targetId] = 0; } // 如果投票人是警长,票数+2 if (gameData.rolesMap[voterId].isSheriff) { voteCount[targetId] += 2; } else { voteCount[targetId]++; } } // 找到最高票 let maxVotes = 0; let victims = []; Object.entries(voteCount).forEach(([targetId, count]) => { if (count > maxVotes) { maxVotes = count; victims = [targetId]; } else if (count === maxVotes) { victims.push(targetId); } }); if (Object.keys(voteCount).length === 0) { // 无人投票,作废 victims = []; } if (victims.length === 1) { // 放逐该玩家 const exiled = Number(victims[0]); gameData.rolesMap[exiled].isAlive = false; // 放逐完,先检查一下游戏是不是已经满足结束条件 const { isEnd, winner } = checkWinCondition(room); if (isEnd) { endGameAndSaveToDB(room, winner); return; } // 检查是否是猎人 const isHunter = gameData.rolesMap[exiled]?.role === 'hunter'; // 广播放逐通知 broadcastMessage('系统', { type: 'day_vote_result', detail: gameData.dayVotes, exiled, isHunter, message: `投票结束,玩家 ${getNameById(exiled)} 被放逐`, }); // 15秒遗言 broadcastCountdown(15, `玩家${getNameById(exiled)}的遗言`, () => { // 遗言结束,检查胜利条件 const { isEnd, winner } = checkWinCondition(room); if (isEnd) { endGameAndSaveToDB(room, winner); return; } if (isHunter) { // 广播猎人技能通知 broadcastMessage('系统', { type: 'hunter_dead', userId: exiled, message: `猎人 ${getNameById(exiled)} 被放逐,可带走一位玩家!10秒内选择!`, }); // 这里给猎人 10 秒时间,如果他不调用 /hunter_kill,那 10 秒后自动进夜晚 setTimeout(() => { // 10 秒后先检查游戏是否已经结束(也许猎人秒选了带走人,导致游戏提前结束) const { isEnd, winner } = checkWinCondition(room); if (isEnd) { endGameAndSaveToDB(room, winner); } else { // 如果游戏还没结束,进入夜晚 setTimeout(() => { enterNight(room); }, 3000); } }, 10000); } else { // 如果不是猎人,直接进入夜晚 setTimeout(() => { enterNight(room); }, 3000); } }); } else { // 没人被放逐 broadcastMessage('系统', { type: 'day_vote_result', detail: gameData.dayVotes, message: '无人被放逐。', }); // 直接夜晚 setTimeout(() => { enterNight(room); }, 3000); } }, 3000); }); } // ======== 路由接口 ======== // 1. 玩家参与警长竞选 router.post('/sheriff_join', (req, res) => { const { userId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } if (gameData.isSheriffElected) { return res.status(400).json({ message: '警长已选出,无法再竞选' }); } if (!gameData.rolesMap[userId] || !gameData.rolesMap[userId].isAlive) { return res.status(400).json({ message: '玩家不存在或已死亡' }); } if (!gameData.sheriffCandidates.includes(userId)) { gameData.sheriffCandidates.push(userId); // 获取玩家名称 const userName = getNameById(userId); // 广播通知 broadcastMessage('系统', { type: 'sheriff_join', userId, message: `玩家 ${userName} 参与了警长竞选`, }); } return res.json({ message: '成功参与警长竞选' }); }); // 2. 聊天接口(统一的聊天) router.post('/chat', (req, res) => { const { userId, content } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } // if (!gameData.rolesMap[userId] || !gameData.rolesMap[userId].isAlive) { // return res.status(400).json({ message: '你已死亡,无法发言' }); // } // 添加聊天记录并广播 addChatMessage(userId, content); return res.json({ message: '发送成功' }); }); // 3. 警长投票接口 router.post('/sheriff_vote', (req, res) => { const { voterId, candidateId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } // 判断该玩家是否存活 if (!gameData.rolesMap[voterId] || !gameData.rolesMap[voterId].isAlive) { return res.status(400).json({ message: '你已死亡,无法投票' }); } // 判断该玩家是否在警长候选人中。如果在,就禁止投票 if (gameData.sheriffCandidates.includes(voterId)) { return res.status(403).json({ message: '你正在竞选警长,无法投票' }); } // 判断 candidateId 是否是候选人 if (!gameData.sheriffCandidates.includes(candidateId)) { return res.status(400).json({ message: '该玩家不在候选名单中' }); } // 记录投票 gameData.sheriffVotes[voterId] = candidateId; return res.json({ message: '投票成功' }); }); // 4. 狼人选择刀人 router.post('/wolf_choose', (req, res) => { const { wolfId, targetId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } const roleData = gameData.rolesMap[wolfId]; if (!roleData || roleData.role !== 'wolf' || !roleData.isAlive) { return res.status(403).json({ message: '你不是活着的狼人,无法刀人' }); } // 记录刀人选择 gameData.wolfVotes[wolfId] = targetId; // 同步给其他狼人的消息 for (let [userId, data] of Object.entries(gameData.rolesMap)) { if (data.role === 'wolf' && data.isAlive && Number(userId) !== Number(wolfId)) { sendMessageToUser(Number(userId), { type: 'wolf_notice', message: `队友 ${wolfId} 选择刀玩家 ${targetId}`, }); } } return res.json({ message: '狼人刀人选择成功' }); }); // 5. 预言家查验 router.post('/seer_look', (req, res) => { const { seerId, targetId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } const roleData = gameData.rolesMap[seerId]; if (!roleData || roleData.role !== 'seer' || !roleData.isAlive) { return res.status(403).json({ message: '你不是活着的预言家,无法查验' }); } gameData.seerChoice = targetId; return res.json({ message: '查验目标已记录' }); }); // 6. 女巫救人 router.post('/witch_save', (req, res) => { const { witchId, userId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } const roleData = gameData.rolesMap[witchId]; if (!roleData || roleData.role !== 'witch' || !roleData.isAlive) { return res.status(403).json({ message: '你不是活着的女巫,无法救人' }); } if (!gameData.witchSaveAvailable) { return res.status(400).json({ message: '解药已使用' }); } gameData.witchSaveTarget = userId; return res.json({ message: '已选择救人' }); }); // 7. 女巫毒人 router.post('/witch_kill', (req, res) => { const { witchId, userId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } const roleData = gameData.rolesMap[witchId]; if (!roleData || roleData.role !== 'witch' || !roleData.isAlive) { return res.status(403).json({ message: '你不是活着的女巫,无法毒人' }); } if (!gameData.witchKillAvailable) { return res.status(400).json({ message: '毒药已使用' }); } gameData.witchKillTarget = userId; return res.json({ message: '已选择毒人' }); }); // 8. 猎人带走人 router.post('/hunter_kill', (req, res) => { const { hunterId, userId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } const roleData = gameData.rolesMap[hunterId]; console.log(`roleData: ${JSON.stringify(roleData)}`); if (!roleData || roleData.role !== 'hunter') { return res.status(403).json({ message: '你不是猎人' }); } // 只有被狼刀死的猎人可以带人 if (roleData.isAlive) { return res.status(400).json({ message: '你还没死,不能带走别人' }); } // 这里就直接处理带走逻辑 if (!gameData.rolesMap[userId] || !gameData.rolesMap[userId].isAlive) { return res.status(400).json({ message: '对方已死亡或不存在' }); } // 猎人带走目标玩家 gameData.rolesMap[userId].isAlive = false; // 广播猎人带走玩家的消息 broadcastMessage('系统', { type: 'hunter_kill', hunterId, userId, message: `猎人 ${getNameById(hunterId)} 带走了玩家 ${getNameById(userId)}!`, }); // 检查游戏胜负条件 const { isEnd, winner } = checkWinCondition(room); if (isEnd) { // 游戏结束 endGameAndSaveToDB(room, winner); return res.json({ message: '操作成功,游戏已结束', result: { isEnd, winner }, }); } // 如果游戏没结束,就继续游戏进程 // 判断是白天死(投票死)还是夜晚死(狼刀死) if (gameData.isNight) { // 如果是夜晚死,猎人开枪带人后进入白天 setTimeout(() => { enterDay(room); }, 3000); } else { // 如果是白天死,猎人开枪带人后进入夜晚 setTimeout(() => { enterNight(room); }, 3000); } return res.json({ message: '操作成功,继续游戏' }); }); // 9. 白天投票放逐 router.post('/vote', (req, res) => { const { voterId, targetId } = req.body; if (!gameData.isGameStarted) { return res.status(400).json({ message: '游戏尚未开始' }); } if (!gameData.rolesMap[voterId] || !gameData.rolesMap[voterId].isAlive) { return res.status(400).json({ message: '你已死亡,无法投票' }); } if (!gameData.rolesMap[targetId] || !gameData.rolesMap[targetId].isAlive) { return res.status(400).json({ message: '目标已死亡或不存在' }); } // 记录 gameData.dayVotes[voterId] = targetId; return res.json({ message: '投票成功' }); }); // ======== 导出 ======== module.exports = { router, startGame, };