2025-02-01 13:09:28 +08:00

1253 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<number>} 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,
};