By registering with us, you'll be able to discuss, share and private message with other members of our community.
SignUp Now!'use strict';
const {
ROLE, ROLE_NAME, ROLE_DESC,
PHASE, TIMER, LIMITS,
MAFIA_ROLES, CIVILIAN_ROLES,
} = require('../config');
const Player = require('./Player');
const { buildTargetKeyboard, votingKeyboard } = require('../utils/keyboards');
class Game {
constructor(chatId, hostId, vk, onEnd) {
this.chatId = chatId;
this.hostId = hostId;
this.vk = vk;
this.onEnd = onEnd;
this.players = new Map();
this.phase = PHASE.REGISTRATION;
this.day = 0;
this.nightActions = this._emptyNightActions();
this.lastDoctorTarget = null;
this.pendingNightDMs = new Map();
this.dayVotes = new Map();
this._timer = null;
}
addPlayer(userId, name) {
if (this.phase !== PHASE.REGISTRATION) {
return { ok: false, reason: 'Регистрация закрыта.' };
}
if (this.players.has(userId)) {
return { ok: false, reason: 'Вы уже в игре.' };
}
if (this.players.size >= LIMITS.MAX_PLAYERS) {
return { ok: false, reason: `Максимум ${LIMITS.MAX_PLAYERS} игроков.` };
}
this.players.set(userId, new Player(userId, name));
return { ok: true };
}
removePlayer(userId) {
if (this.phase !== PHASE.REGISTRATION) {
return { ok: false, reason: 'Игра уже идёт.' };
}
if (!this.players.has(userId)) {
return { ok: false, reason: 'Вы не в игре.' };
}
this.players.delete(userId);
return { ok: true };
}
get playerList() {
return [...this.players.values()].map((p, i) => `${i + 1}. ${p.mention}`).join('\n');
}
async start(initiatorId) {
if (initiatorId !== this.hostId) {
return { ok: false, reason: 'Только создатель может начать игру.' };
}
if (this.players.size < LIMITS.MIN_PLAYERS) {
return { ok: false, reason: `Нужно минимум ${LIMITS.MIN_PLAYERS} игрока.` };
}
this._clearTimer();
this._distributeRoles();
await this._notifyRoles();
await this._sendChat(
`🎭 Игра начинается! Участников: ${this.players.size}\n` +
`Роли розданы — проверьте личные сообщения бота.\n\n` +
`Наступает первая ночь...`
);
await this._startNight();
return { ok: true };
}
_distributeRoles() {
const ids = [...this.players.keys()];
this._shuffle(ids);
const n = ids.length;
const mafiaCount = Math.max(1, Math.floor(n / 3));
const hasCommissioner = n >= 5;
let idx = 0;
if (mafiaCount >= 2) {
this.players.get(ids[idx++]).role = ROLE.DON;
}
while (idx < mafiaCount) {
this.players.get(ids[idx++]).role = ROLE.MAFIA;
}
this.players.get(ids[idx++]).role = ROLE.DOCTOR;
if (hasCommissioner) {
this.players.get(ids[idx++]).role = ROLE.COMMISSIONER;
}
while (idx < n) {
this.players.get(ids[idx++]).role = ROLE.CITIZEN;
}
}
async _notifyRoles() {
const mafia = this._aliveMafia();
const mafiaNames = mafia.map(p => p.mention).join(', ');
const promises = [];
for (const player of this.players.values()) {
let text = `🎭 Ваша роль: ${ROLE_NAME[player.role]}\n${ROLE_DESC[player.role]}`;
if (player.isMafia && mafia.length > 1) {
const allies = mafia.filter(p => p.id !== player.id).map(p => p.name).join(', ');
text += `\n\nВаши сообщники: ${allies}`;
}
promises.push(this._sendDM(player.id, text));
}
await Promise.allSettled(promises);
}
async _startNight() {
this.day++;
this.phase = PHASE.NIGHT;
this.nightActions = this._emptyNightActions();
this.pendingNightDMs.clear();
await this._sendChat(
`🌙 Ночь ${this.day}. Город засыпает...\n` +
`Активные роли — проверьте ЛС бота.`
);
await this._sendNightPrompts();
this._startTimer(TIMER.NIGHT_ACTION, () => this._resolveNight());
}
async _sendNightPrompts() {
const alive = this._alivePlayers();
const nonMafia = alive.filter(p => !p.isMafia);
const promises = [];
for (const m of this._aliveMafia()) {
const targets = nonMafia.map(p => ({ id: p.id, label: p.name }));
if (targets.length === 0) continue;
const kb = buildTargetKeyboard(targets, 'mafia_vote');
promises.push(
this._sendDM(m.id, '🔪 Выберите жертву:', kb)
.then(ok => { if (ok) this.pendingNightDMs.set(m.id, 'mafia'); })
);
}
const don = this._findAliveByRole(ROLE.DON);
if (don) {
const targets = alive.filter(p => p.id !== don.id).map(p => ({ id: p.id, label: p.name }));
if (targets.length > 0) {
const kb = buildTargetKeyboard(targets, 'don_check');
promises.push(
this._sendDM(don.id, '🎩 Кого проверить на Комиссара?', kb)
.then(ok => { if (ok) this.pendingNightDMs.set(`don_${don.id}`, 'don_check'); })
);
}
}
const doc = this._findAliveByRole(ROLE.DOCTOR);
if (doc) {
const targets = alive
.filter(p => p.id !== this.lastDoctorTarget)
.map(p => ({ id: p.id, label: p.name }));
if (targets.length > 0) {
const kb = buildTargetKeyboard(targets, 'doctor_heal');
promises.push(
this._sendDM(doc.id, '💊 Кого вылечить этой ночью?', kb)
.then(ok => { if (ok) this.pendingNightDMs.set(doc.id, 'doctor'); })
);
}
}
const com = this._findAliveByRole(ROLE.COMMISSIONER);
if (com) {
const targets = alive.filter(p => p.id !== com.id).map(p => ({ id: p.id, label: p.name }));
if (targets.length > 0) {
const kb = buildTargetKeyboard(targets, 'commissioner_check');
promises.push(
this._sendDM(com.id, '🔍 Кого проверить этой ночью?', kb)
.then(ok => { if (ok) this.pendingNightDMs.set(com.id, 'commissioner'); })
);
}
}
await Promise.allSettled(promises);
}
handleMafiaVote(mafiaId, targetId) {
if (this.phase !== PHASE.NIGHT) return null;
const mafia = this.players.get(mafiaId);
if (!mafia || !mafia.alive || !mafia.isMafia) return null;
if (this.nightActions.mafiaVotes.has(mafiaId)) return null;
const target = this.players.get(targetId);
if (!target || !target.alive || target.isMafia) return null;
this.nightActions.mafiaVotes.set(mafiaId, targetId);
this._checkNightComplete();
return target.name;
}
handleDonCheck(donId, targetId) {
if (this.phase !== PHASE.NIGHT) return null;
const don = this.players.get(donId);
if (!don || !don.alive || don.role !== ROLE.DON) return null;
if (this.nightActions.donCheckTarget !== null) return null;
const target = this.players.get(targetId);
if (!target || !target.alive) return null;
this.nightActions.donCheckTarget = targetId;
const isCommissioner = target.role === ROLE.COMMISSIONER;
this._checkNightComplete();
return isCommissioner;
}
handleDoctorHeal(docId, targetId) {
if (this.phase !== PHASE.NIGHT) return null;
const doc = this.players.get(docId);
if (!doc || !doc.alive || doc.role !== ROLE.DOCTOR) return null;
if (this.nightActions.doctorTarget !== null) return null;
const target = this.players.get(targetId);
if (!target || !target.alive) return null;
if (targetId === this.lastDoctorTarget) return null;
this.nightActions.doctorTarget = targetId;
this._checkNightComplete();
return target.name;
}
handleCommissionerCheck(comId, targetId) {
if (this.phase !== PHASE.NIGHT) return null;
const com = this.players.get(comId);
if (!com || !com.alive || com.role !== ROLE.COMMISSIONER) return null;
if (this.nightActions.commissionerTarget !== null) return null;
const target = this.players.get(targetId);
if (!target || !target.alive) return null;
this.nightActions.commissionerTarget = targetId;
const isMafia = target.isMafia;
this._checkNightComplete();
return isMafia;
}
_checkNightComplete() {
const expected = this._expectedNightActions();
const received = this._receivedNightActions();
if (received >= expected) {
this._clearTimer();
setTimeout(() => this._resolveNight(), 1500);
}
}
_expectedNightActions() {
let count = this._aliveMafia().length;
if (this._findAliveByRole(ROLE.DON)) count++;
if (this._findAliveByRole(ROLE.DOCTOR)) count++;
if (this._findAliveByRole(ROLE.COMMISSIONER)) count++;
return count;
}
_receivedNightActions() {
let count = this.nightActions.mafiaVotes.size;
if (this.nightActions.donCheckTarget !== null) count++;
if (this.nightActions.doctorTarget !== null) count++;
if (this.nightActions.commissionerTarget !== null) count++;
return count;
}
async _resolveNight() {
if (this.phase !== PHASE.NIGHT) return;
this.phase = PHASE.DAY_ANNOUNCE;
this._clearTimer();
const mafiaTarget = this._resolveMafiaTarget();
const healed = mafiaTarget !== null &&
this.nightActions.doctorTarget === mafiaTarget;
this.lastDoctorTarget = this.nightActions.doctorTarget;
await this._sendCommissionerResult();
await this._sendDonCheckResult();
let killedPlayer = null;
if (mafiaTarget !== null && !healed) {
killedPlayer = this.players.get(mafiaTarget);
killedPlayer.kill();
}
await this._announceNightResults(killedPlayer, healed);
const winner = this._checkWinCondition();
if (winner) {
await this._endGame(winner);
return;
}
await this._startDiscussion();
}
_resolveMafiaTarget() {
const votes = this.nightActions.mafiaVotes;
if (votes.size === 0) {
const nonMafia = this._alivePlayers().filter(p => !p.isMafia);
if (nonMafia.length === 0) return null;
return nonMafia[Math.floor(Math.random() * nonMafia.length)].id;
}
const tally = new Map();
for (const targetId of votes.values()) {
tally.set(targetId, (tally.get(targetId) || 0) + 1);
}
let maxVotes = 0;
for (const v of tally.values()) {
if (v > maxVotes) maxVotes = v;
}
const topTargets = [...tally.entries()]
.filter(([, v]) => v === maxVotes)
.map(([id]) => id);
return topTargets[Math.floor(Math.random() * topTargets.length)];
}
async _sendCommissionerResult() {
const com = this._findAliveByRole(ROLE.COMMISSIONER);
const targetId = this.nightActions.commissionerTarget;
if (!com || targetId === null) return;
const target = this.players.get(targetId);
if (!target) return;
const result = target.isMafia
? `🔴 ${target.name} — МАФИЯ!`
: `🟢 ${target.name} — мирный житель.`;
await this._sendDM(com.id, `🔍 Результат проверки:\n${result}`);
}
async _sendDonCheckResult() {
const don = this._findAliveByRole(ROLE.DON);
const targetId = this.nightActions.donCheckTarget;
if (!don || targetId === null) return;
const target = this.players.get(targetId);
if (!target) return;
const isCom = target.role === ROLE.COMMISSIONER;
const result = isCom
? `🔴 ${target.name} — КОМИССАР!`
: `🟢 ${target.name} — не комиссар.`;
await this._sendDM(don.id, `🎩 Результат проверки:\n${result}`);
}
async _announceNightResults(killedPlayer, healed) {
let text = `☀️ Наступает день ${this.day}. Город просыпается...\n\n`;
if (killedPlayer) {
text += `💀 Этой ночью был убит: ${killedPlayer.mention}\n`;
text += `Роль: ${ROLE_NAME[killedPlayer.role]}`;
} else if (healed) {
text += `💊 Доктор спас жизнь этой ночью! Никто не погиб.`;
} else {
text += `✨ Этой ночью никто не погиб.`;
}
text += `\n\nОсталось в живых: ${this._alivePlayers().length}`;
await this._sendChat(text);
}
async _startDiscussion() {
this.phase = PHASE.DISCUSSION;
const alive = this._alivePlayers();
const list = alive.map((p, i) => `${i + 1}. ${p.mention}`).join('\n');
await this._sendChat(
`💬 Обсуждение (${TIMER.DISCUSSION} сек).\n` +
`Обсудите, кто может быть мафией.\n\n` +
`Живые игроки:\n${list}`
);
this._startTimer(TIMER.DISCUSSION, () => this._startVoting());
}
async _startVoting() {
this.phase = PHASE.VOTING;
this.dayVotes.clear();
const alive = this._alivePlayers();
await this._sendChat(
`🗳 Голосование! (${TIMER.VOTING} сек)\n` +
`Нажмите кнопку в ЛС бота, чтобы проголосовать.`
);
const promises = alive.map(p => {
const kb = votingKeyboard(alive, p.id);
return this._sendDM(p.id, '🗳 Голосуйте! Кого исключить из города?', kb);
});
await Promise.allSettled(promises);
this._startTimer(TIMER.VOTING, () => this._resolveVoting());
}
handleDayVote(voterId, targetId) {
if (this.phase !== PHASE.VOTING) return null;
const voter = this.players.get(voterId);
if (!voter || !voter.alive) return null;
if (this.dayVotes.has(voterId)) return null;
if (targetId !== 'skip') {
const target = this.players.get(targetId);
if (!target || !target.alive) return null;
}
this.dayVotes.set(voterId, targetId);
const aliveCount = this._alivePlayers().length;
if (this.dayVotes.size >= aliveCount) {
this._clearTimer();
setTimeout(() => this._resolveVoting(), 1000);
}
if (targetId === 'skip') return 'Пропустить';
return this.players.get(targetId)?.name ?? null;
}
async _resolveVoting() {
if (this.phase !== PHASE.VOTING) return;
this.phase = PHASE.LAST_WORD;
this._clearTimer();
const tally = new Map();
let skipVotes = 0;
for (const targetId of this.dayVotes.values()) {
if (targetId === 'skip') {
skipVotes++;
} else {
tally.set(targetId, (tally.get(targetId) || 0) + 1);
}
}
let resultText = '📊 Результаты голосования:\n';
const sortedTally = [...tally.entries()].sort((a, b) => b[1] - a[1]);
for (const [id, count] of sortedTally) {
const p = this.players.get(id);
resultText += ` ${p.mention} — ${count} гол.\n`;
}
if (skipVotes > 0) {
resultText += ` Пропустить — ${skipVotes} гол.\n`;
}
if (this.dayVotes.size === 0) {
resultText += ' Никто не голосовал.\n';
}
let maxVotes = skipVotes;
let eliminatedId = null;
for (const [id, count] of tally) {
if (count > maxVotes) {
maxVotes = count;
eliminatedId = id;
} else if (count === maxVotes) {
eliminatedId = null;
}
}
if (eliminatedId) {
const victim = this.players.get(eliminatedId);
victim.kill();
resultText += `\n⚰️ Город решил казнить: ${victim.mention}\n`;
resultText += `Роль: ${ROLE_NAME[victim.role]}`;
await this._sendChat(resultText);
const winner = this._checkWinCondition();
if (winner) {
await this._endGame(winner);
return;
}
await this._sendChat(
`🗣 ${victim.mention}, у вас есть ${TIMER.LAST_WORD} секунд на последнее слово.`
);
this._startTimer(TIMER.LAST_WORD, () => this._startNight());
} else {
resultText += '\n🤝 Ничья — никто не исключён.';
await this._sendChat(resultText);
const winner = this._checkWinCondition();
if (winner) {
await this._endGame(winner);
return;
}
await this._startNight();
}
}
_checkWinCondition() {
const aliveMafia = this._aliveMafia().length;
const aliveCivil = this._alivePlayers().length - aliveMafia;
if (aliveMafia === 0) return 'civilians';
if (aliveMafia >= aliveCivil) return 'mafia';
return null;
}
async _endGame(winner) {
this.phase = PHASE.GAME_OVER;
this._clearTimer();
const isMafiaWin = winner === 'mafia';
let text = isMafiaWin
? '🔴 Мафия победила! Город пал.\n\n'
: '🟢 Мирные жители победили! Мафия уничтожена.\n\n';
text += '📋 Роли игроков:\n';
for (const p of this.players.values()) {
const status = p.alive ? '✅' : '💀';
text += `${status} ${p.mention} — ${ROLE_NAME[p.role]}\n`;
}
text += '\n📊 Статистика:\n';
text += ` Дней прожито: ${this.day}\n`;
text += ` Игроков: ${this.players.size}\n`;
text += ` Выживших: ${this._alivePlayers().length}`;
await this._sendChat(text);
this.onEnd(this.chatId);
}
async handlePlayerLeave(userId) {
const player = this.players.get(userId);
if (!player || !player.alive) return;
player.kill();
await this._sendChat(
`🚪 ${player.mention} покинул игру.\nРоль: ${ROLE_NAME[player.role]}`
);
const winner = this._checkWinCondition();
if (winner) {
await this._endGame(winner);
}
}
async forceStop(userId) {
if (userId !== this.hostId) return false;
this._clearTimer();
this.phase = PHASE.GAME_OVER;
await this._sendChat('🛑 Игра принудительно остановлена создателем.');
this.onEnd(this.chatId);
return true;
}
_alivePlayers() {
const result = [];
for (const p of this.players.values()) {
if (p.alive) result.push(p);
}
return result;
}
_aliveMafia() {
const result = [];
for (const p of this.players.values()) {
if (p.alive && p.isMafia) result.push(p);
}
return result;
}
_findAliveByRole(role) {
for (const p of this.players.values()) {
if (p.alive && p.role === role) return p;
}
return null;
}
_emptyNightActions() {
return {
mafiaVotes: new Map(),
donCheckTarget: null,
doctorTarget: null,
commissionerTarget: null,
};
}
async _sendChat(text, keyboard = undefined) {
try {
await this.vk.api.messages.send({
peer_id: this.chatId,
message: text,
keyboard: keyboard ? keyboard.toString() : undefined,
random_id: Math.floor(Math.random() * 1e9),
});
} catch (err) {
console.error(`[Game] Ошибка отправки в чат ${this.chatId}:`, err.message);
}
}
async _sendDM(userId, text, keyboard = undefined) {
try {
await this.vk.api.messages.send({
user_id: userId,
message: text,
keyboard: keyboard ? keyboard.toString() : undefined,
random_id: Math.floor(Math.random() * 1e9),
});
return true;
} catch (err) {
console.error(`[Game] Не удалось отправить ЛС ${userId}:`, err.message);
const player = this.players.get(userId);
if (player) {
await this._sendChat(
`⚠️ Не удалось отправить сообщение ${player.mention}. ` +
`Убедитесь, что ЛС бота открыты.`
);
}
return false;
}
}
_startTimer(seconds, callback) {
this._clearTimer();
this._timer = setTimeout(async () => {
try {
await callback.call(this);
} catch (err) {
console.error('[Game] Ошибка в таймере:', err);
}
}, seconds * 1000);
}
_clearTimer() {
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
}
_shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
}
module.exports = Game;
'use strict';
const { Keyboard } = require('vk-io');
function buildTargetKeyboard(targets, actionPrefix, columns = 2) {
const builder = Keyboard.builder();
let col = 0;
for (const t of targets) {
builder.callbackButton({
label: t.label.slice(0, 40),
payload: { action: actionPrefix, target: t.id },
color: Keyboard.SECONDARY_COLOR,
});
col++;
if (col >= columns) {
builder.row();
col = 0;
}
}
return builder.inline();
}
function registrationKeyboard() {
return Keyboard.builder()
.callbackButton({
label: 'Присоединиться',
payload: { action: 'join' },
color: Keyboard.POSITIVE_COLOR,
})
.callbackButton({
label: 'Покинуть',
payload: { action: 'leave' },
color: Keyboard.NEGATIVE_COLOR,
})
.row()
.callbackButton({
label: 'Начать игру',
payload: { action: 'start_game' },
color: Keyboard.PRIMARY_COLOR,
})
.inline();
}
function votingKeyboard(alivePlayers, voterId) {
const targets = alivePlayers
.filter(p => p.id !== voterId)
.map(p => ({ id: p.id, label: p.name }));
const builder = Keyboard.builder();
let col = 0;
for (const t of targets) {
builder.callbackButton({
label: t.label.slice(0, 40),
payload: { action: 'day_vote', target: t.id },
color: Keyboard.SECONDARY_COLOR,
});
col++;
if (col >= 2) {
builder.row();
col = 0;
}
}
if (col !== 0) builder.row();
builder.callbackButton({
label: 'Пропустить',
payload: { action: 'day_vote', target: 'skip' },
color: Keyboard.NEGATIVE_COLOR,
});
return builder.inline();
}
module.exports = {
buildTargetKeyboard,
registrationKeyboard,
votingKeyboard,
};
'use strict';
const { PHASE, LIMITS, ROLE_NAME, ROLE_DESC, ROLE } = require('../config');
const { registrationKeyboard } = require('../utils/keyboards');
function registerHandlers(vk, manager) {
vk.updates.on('message_new', async (context, next) => {
if (!context.text) return next();
const text = context.text.toLowerCase().trim();
const isChat = context.peerType === 'chat';
const userId = context.senderId;
const peerId = context.peerId;
if (isChat) {
if (text === '/мафия' || text === '/mafia') {
return handleCreateGame(context, manager, peerId, userId);
}
if (text === '/стоп' || text === '/stop') {
return handleStopGame(context, manager, peerId, userId);
}
if (text === '/правила' || text === '/rules') {
return handleRules(context);
}
if (text === '/роли' || text === '/roles') {
return handleRolesInfo(context);
}
if (text === '/статус' || text === '/status') {
return handleStatus(context, manager, peerId);
}
if (text === '/выйти' || text === '/leave') {
return handleLeaveCommand(context, manager, peerId, userId);
}
}
return next();
});
vk.updates.on('message_event', async (context) => {
const payload = context.eventPayload;
const userId = context.userId;
const peerId = context.peerId;
if (!payload || !payload.action) return;
try {
switch (payload.action) {
case 'join':
await handleJoin(context, vk, manager, peerId, userId);
break;
case 'leave':
await handleLeave(context, vk, manager, peerId, userId);
break;
case 'start_game':
await handleStartGame(context, vk, manager, peerId, userId);
break;
case 'mafia_vote':
await handleNightAction(context, manager, userId, 'mafia_vote', payload.target);
break;
case 'don_check':
await handleNightAction(context, manager, userId, 'don_check', payload.target);
break;
case 'doctor_heal':
await handleNightAction(context, manager, userId, 'doctor_heal', payload.target);
break;
case 'commissioner_check':
await handleNightAction(context, manager, userId, 'commissioner_check', payload.target);
break;
case 'day_vote':
await handleDayVoteAction(context, manager, userId, payload.target);
break;
default:
await context.answer({ type: 'show_snackbar', text: 'Неизвестное действие.' });
}
} catch (err) {
console.error('[Handler] Ошибка обработки callback:', err);
try {
await context.answer({ type: 'show_snackbar', text: 'Произошла ошибка.' });
} catch {}
}
});
}
async function handleCreateGame(context, manager, peerId, userId) {
const result = manager.createGame(peerId, userId);
if (!result.ok) {
return context.send(result.reason);
}
const name = await _getUserName(context.api, userId);
result.game.addPlayer(userId, name);
manager.registerPlayer(userId, peerId);
await context.send({
message:
`🎭 Игра «Мафия» создана!\n` +
`Организатор: [id${userId}|${name}]\n\n` +
`Для участия нажмите «Присоединиться».\n` +
`Минимум ${LIMITS.MIN_PLAYERS} игрока, максимум ${LIMITS.MAX_PLAYERS}.\n\n` +
`Игроки:\n1. [id${userId}|${name}]`,
keyboard: registrationKeyboard(),
});
}
async function handleJoin(context, vk, manager, peerId, userId) {
const game = manager.getGame(peerId);
if (!game) {
return context.answer({ type: 'show_snackbar', text: 'Игра не найдена.' });
}
const name = await _getUserName(vk.api, userId);
const result = game.addPlayer(userId, name);
if (!result.ok) {
return context.answer({ type: 'show_snackbar', text: result.reason });
}
manager.registerPlayer(userId, peerId);
await context.answer({ type: 'show_snackbar', text: '✅ Вы вступили в игру!' });
await vk.api.messages.send({
peer_id: peerId,
message:
`✅ [id${userId}|${name}] вступает в игру!\n\n` +
`Игроки (${game.players.size}):\n${game.playerList}`,
keyboard: registrationKeyboard(),
random_id: Math.floor(Math.random() * 1e9),
});
}
async function handleLeave(context, vk, manager, peerId, userId) {
const game = manager.getGame(peerId);
if (!game) {
return context.answer({ type: 'show_snackbar', text: 'Игра не найдена.' });
}
if (game.phase !== PHASE.REGISTRATION) {
await game.handlePlayerLeave(userId);
manager.unregisterPlayer(userId);
return context.answer({ type: 'show_snackbar', text: 'Вы вышли из игры.' });
}
const result = game.removePlayer(userId);
if (!result.ok) {
return context.answer({ type: 'show_snackbar', text: result.reason });
}
manager.unregisterPlayer(userId);
await context.answer({ type: 'show_snackbar', text: 'Вы покинули игру.' });
const player = `[id${userId}|Игрок]`;
const count = game.players.size;
if (count === 0) {
manager._onGameEnd(peerId);
await vk.api.messages.send({
peer_id: peerId,
message: `${player} покинул. Все вышли — игра отменена.`,
random_id: Math.floor(Math.random() * 1e9),
});
} else {
await vk.api.messages.send({
peer_id: peerId,
message:
`❌ ${player} покидает игру.\n\n` +
`Игроки (${count}):\n${game.playerList}`,
keyboard: registrationKeyboard(),
random_id: Math.floor(Math.random() * 1e9),
});
}
}
async function handleLeaveCommand(context, manager, peerId, userId) {
const game = manager.getGame(peerId);
if (!game) return;
if (game.phase === PHASE.REGISTRATION) {
const result = game.removePlayer(userId);
if (result.ok) {
manager.unregisterPlayer(userId);
await context.send(`❌ Вы покинули игру.\n\nИгроки (${game.players.size}):\n${game.playerList}`);
}
} else {
await game.handlePlayerLeave(userId);
manager.unregisterPlayer(userId);
}
}
async function handleStartGame(context, vk, manager, peerId, userId) {
const game = manager.getGame(peerId);
if (!game) {
return context.answer({ type: 'show_snackbar', text: 'Игра не найдена.' });
}
const result = await game.start(userId);
if (!result.ok) {
return context.answer({ type: 'show_snackbar', text: result.reason });
}
await context.answer({ type: 'show_snackbar', text: '🎮 Игра запущена!' });
}
async function handleNightAction(context, manager, userId, action, targetId) {
const game = manager.getGameByPlayer(userId);
if (!game || game.phase !== PHASE.NIGHT) {
return context.answer({ type: 'show_snackbar', text: 'Сейчас не время для этого.' });
}
let result = null;
let responseText = '';
switch (action) {
case 'mafia_vote': {
const name = game.handleMafiaVote(userId, targetId);
if (name === null) {
return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' });
}
responseText = `🔪 Вы голосуете за: ${name}`;
break;
}
case 'don_check': {
result = game.handleDonCheck(userId, targetId);
if (result === null) {
return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' });
}
const target = game.players.get(targetId);
responseText = result
? `🔴 ${target.name} — КОМИССАР!`
: `🟢 ${target.name} — не комиссар.`;
break;
}
case 'doctor_heal': {
const name = game.handleDoctorHeal(userId, targetId);
if (name === null) {
return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' });
}
responseText = `💊 Вы лечите: ${name}`;
break;
}
case 'commissioner_check': {
result = game.handleCommissionerCheck(userId, targetId);
if (result === null) {
return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' });
}
const target = game.players.get(targetId);
responseText = result
? `🔴 ${target.name} — МАФИЯ!`
: `🟢 ${target.name} — мирный.`;
break;
}
}
await context.answer({ type: 'show_snackbar', text: responseText });
}
async function handleDayVoteAction(context, manager, userId, targetId) {
const game = manager.getGameByPlayer(userId);
if (!game || game.phase !== PHASE.VOTING) {
return context.answer({ type: 'show_snackbar', text: 'Сейчас не время для голосования.' });
}
const result = game.handleDayVote(userId, targetId);
if (result === null) {
return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' });
}
await context.answer({ type: 'show_snackbar', text: `🗳 Ваш голос: ${result}` });
}
async function handleStopGame(context, manager, peerId, userId) {
const game = manager.getGame(peerId);
if (!game) {
return context.send('Нет активной игры.');
}
const ok = await game.forceStop(userId);
if (!ok) {
await context.send('Только создатель игры может её остановить.');
}
}
async function handleRules(context) {
await context.send(
`📖 Правила игры «Мафия»\n\n` +
`🎭 Роли:\n` +
`• Мафия — убивает каждую ночь\n` +
`• Дон мафии — глава мафии + проверяет на комиссара\n` +
`• Комиссар — проверяет игрока ночью (мафия/мирный)\n` +
`• Доктор — лечит одного игрока за ночь\n` +
`• Мирный житель — голосует днём\n\n` +
`🌙 Ночь:\n` +
`Мафия выбирает жертву. Доктор лечит. Комиссар проверяет.\n\n` +
`☀️ День:\n` +
`Обсуждение → Голосование. Игрок с большинством голосов исключён.\n\n` +
`🏆 Победа:\n` +
`• Мирные побеждают, когда вся мафия уничтожена\n` +
`• Мафия побеждает, когда мафий >= мирных\n\n` +
`⚙️ Нюансы:\n` +
`• Доктор не может лечить одного и того же 2 ночи подряд\n` +
`• При ничьей голосов днём — никого не исключают\n` +
`• Если мафия не голосует — жертва случайная\n` +
`• Дон голосует с мафией + проверяет на комиссара\n\n` +
`📝 Команды:\n` +
`/мафия — создать игру\n` +
`/стоп — остановить игру (создатель)\n` +
`/выйти — покинуть игру\n` +
`/роли — распределение ролей\n` +
`/статус — статус текущей игры\n` +
`/правила — эта справка`
);
}
async function handleRolesInfo(context) {
let text = '🎭 Распределение ролей по количеству игроков:\n\n';
for (let n = LIMITS.MIN_PLAYERS; n <= 12; n++) {
const mafia = Math.max(1, Math.floor(n / 3));
const hasDon = mafia >= 2;
const hasCom = n >= 5;
const special = 1 + (hasCom ? 1 : 0);
const citizens = n - mafia - special;
text += `👥 ${n} игроков: `;
if (hasDon) text += `1 Дон + ${mafia - 1} Маф.`;
else text += `${mafia} Маф.`;
text += `, 1 Док.`;
if (hasCom) text += `, 1 Ком.`;
text += `, ${citizens} Мирн.\n`;
}
await context.send(text);
}
async function handleStatus(context, manager, peerId) {
const game = manager.getGame(peerId);
if (!game) {
return context.send('Нет активной игры. Напишите /мафия для создания.');
}
const phaseName = {
[PHASE.REGISTRATION]: '📝 Регистрация',
[PHASE.NIGHT]: '🌙 Ночь',
[PHASE.DAY_ANNOUNCE]: '☀️ Утро',
[PHASE.DISCUSSION]: '💬 Обсуждение',
[PHASE.VOTING]: '🗳 Голосование',
[PHASE.LAST_WORD]: '🗣 Последнее слово',
[PHASE.GAME_OVER]: '🏁 Игра окончена',
};
const alive = game._alivePlayers();
let text = `📊 Статус игры:\n`;
text += `Фаза: ${phaseName[game.phase] || game.phase}\n`;
text += `День: ${game.day}\n`;
text += `Игроков: ${game.players.size} (живых: ${alive.length})\n\n`;
text += `Живые:\n`;
text += alive.map((p, i) => `${i + 1}. ${p.mention}`).join('\n');
await context.send(text);
}
async function _getUserName(api, userId) {
try {
const [user] = await api.users.get({ user_ids: [userId] });
return `${user.first_name} ${user.last_name}`;
} catch {
return `Игрок ${userId}`;
}
}
module.exports = { registerHandlers };
так я готов конечно оплатить
Никто и ничего готового Вам точно не даст.
Хотите что-то? Будьте готовы платить.
в node modules кинут?
нетв node modules кинут?
ну я впринципе понял
можешь помочь пожалуйста ну или сделать этого бота в исходниках руби я тебе деньги дам
можешь помочь пожалуйста ну или сделать этого бота в исходниках руби я тебе деньги дам
Интересная реализацияgame.js:'use strict'; const { ROLE, ROLE_NAME, ROLE_DESC, PHASE, TIMER, LIMITS, MAFIA_ROLES, CIVILIAN_ROLES, } = require('../config'); const Player = require('./Player'); const { buildTargetKeyboard, votingKeyboard } = require('../utils/keyboards'); class Game { constructor(chatId, hostId, vk, onEnd) { this.chatId = chatId; this.hostId = hostId; this.vk = vk; this.onEnd = onEnd; this.players = new Map(); this.phase = PHASE.REGISTRATION; this.day = 0; this.nightActions = this._emptyNightActions(); this.lastDoctorTarget = null; this.pendingNightDMs = new Map(); this.dayVotes = new Map(); this._timer = null; } addPlayer(userId, name) { if (this.phase !== PHASE.REGISTRATION) { return { ok: false, reason: 'Регистрация закрыта.' }; } if (this.players.has(userId)) { return { ok: false, reason: 'Вы уже в игре.' }; } if (this.players.size >= LIMITS.MAX_PLAYERS) { return { ok: false, reason: `Максимум ${LIMITS.MAX_PLAYERS} игроков.` }; } this.players.set(userId, new Player(userId, name)); return { ok: true }; } removePlayer(userId) { if (this.phase !== PHASE.REGISTRATION) { return { ok: false, reason: 'Игра уже идёт.' }; } if (!this.players.has(userId)) { return { ok: false, reason: 'Вы не в игре.' }; } this.players.delete(userId); return { ok: true }; } get playerList() { return [...this.players.values()].map((p, i) => `${i + 1}. ${p.mention}`).join('\n'); } async start(initiatorId) { if (initiatorId !== this.hostId) { return { ok: false, reason: 'Только создатель может начать игру.' }; } if (this.players.size < LIMITS.MIN_PLAYERS) { return { ok: false, reason: `Нужно минимум ${LIMITS.MIN_PLAYERS} игрока.` }; } this._clearTimer(); this._distributeRoles(); await this._notifyRoles(); await this._sendChat( `🎭 Игра начинается! Участников: ${this.players.size}\n` + `Роли розданы — проверьте личные сообщения бота.\n\n` + `Наступает первая ночь...` ); await this._startNight(); return { ok: true }; } _distributeRoles() { const ids = [...this.players.keys()]; this._shuffle(ids); const n = ids.length; const mafiaCount = Math.max(1, Math.floor(n / 3)); const hasCommissioner = n >= 5; let idx = 0; if (mafiaCount >= 2) { this.players.get(ids[idx++]).role = ROLE.DON; } while (idx < mafiaCount) { this.players.get(ids[idx++]).role = ROLE.MAFIA; } this.players.get(ids[idx++]).role = ROLE.DOCTOR; if (hasCommissioner) { this.players.get(ids[idx++]).role = ROLE.COMMISSIONER; } while (idx < n) { this.players.get(ids[idx++]).role = ROLE.CITIZEN; } } async _notifyRoles() { const mafia = this._aliveMafia(); const mafiaNames = mafia.map(p => p.mention).join(', '); const promises = []; for (const player of this.players.values()) { let text = `🎭 Ваша роль: ${ROLE_NAME[player.role]}\n${ROLE_DESC[player.role]}`; if (player.isMafia && mafia.length > 1) { const allies = mafia.filter(p => p.id !== player.id).map(p => p.name).join(', '); text += `\n\nВаши сообщники: ${allies}`; } promises.push(this._sendDM(player.id, text)); } await Promise.allSettled(promises); } async _startNight() { this.day++; this.phase = PHASE.NIGHT; this.nightActions = this._emptyNightActions(); this.pendingNightDMs.clear(); await this._sendChat( `🌙 Ночь ${this.day}. Город засыпает...\n` + `Активные роли — проверьте ЛС бота.` ); await this._sendNightPrompts(); this._startTimer(TIMER.NIGHT_ACTION, () => this._resolveNight()); } async _sendNightPrompts() { const alive = this._alivePlayers(); const nonMafia = alive.filter(p => !p.isMafia); const promises = []; for (const m of this._aliveMafia()) { const targets = nonMafia.map(p => ({ id: p.id, label: p.name })); if (targets.length === 0) continue; const kb = buildTargetKeyboard(targets, 'mafia_vote'); promises.push( this._sendDM(m.id, '🔪 Выберите жертву:', kb) .then(ok => { if (ok) this.pendingNightDMs.set(m.id, 'mafia'); }) ); } const don = this._findAliveByRole(ROLE.DON); if (don) { const targets = alive.filter(p => p.id !== don.id).map(p => ({ id: p.id, label: p.name })); if (targets.length > 0) { const kb = buildTargetKeyboard(targets, 'don_check'); promises.push( this._sendDM(don.id, '🎩 Кого проверить на Комиссара?', kb) .then(ok => { if (ok) this.pendingNightDMs.set(`don_${don.id}`, 'don_check'); }) ); } } const doc = this._findAliveByRole(ROLE.DOCTOR); if (doc) { const targets = alive .filter(p => p.id !== this.lastDoctorTarget) .map(p => ({ id: p.id, label: p.name })); if (targets.length > 0) { const kb = buildTargetKeyboard(targets, 'doctor_heal'); promises.push( this._sendDM(doc.id, '💊 Кого вылечить этой ночью?', kb) .then(ok => { if (ok) this.pendingNightDMs.set(doc.id, 'doctor'); }) ); } } const com = this._findAliveByRole(ROLE.COMMISSIONER); if (com) { const targets = alive.filter(p => p.id !== com.id).map(p => ({ id: p.id, label: p.name })); if (targets.length > 0) { const kb = buildTargetKeyboard(targets, 'commissioner_check'); promises.push( this._sendDM(com.id, '🔍 Кого проверить этой ночью?', kb) .then(ok => { if (ok) this.pendingNightDMs.set(com.id, 'commissioner'); }) ); } } await Promise.allSettled(promises); } handleMafiaVote(mafiaId, targetId) { if (this.phase !== PHASE.NIGHT) return null; const mafia = this.players.get(mafiaId); if (!mafia || !mafia.alive || !mafia.isMafia) return null; if (this.nightActions.mafiaVotes.has(mafiaId)) return null; const target = this.players.get(targetId); if (!target || !target.alive || target.isMafia) return null; this.nightActions.mafiaVotes.set(mafiaId, targetId); this._checkNightComplete(); return target.name; } handleDonCheck(donId, targetId) { if (this.phase !== PHASE.NIGHT) return null; const don = this.players.get(donId); if (!don || !don.alive || don.role !== ROLE.DON) return null; if (this.nightActions.donCheckTarget !== null) return null; const target = this.players.get(targetId); if (!target || !target.alive) return null; this.nightActions.donCheckTarget = targetId; const isCommissioner = target.role === ROLE.COMMISSIONER; this._checkNightComplete(); return isCommissioner; } handleDoctorHeal(docId, targetId) { if (this.phase !== PHASE.NIGHT) return null; const doc = this.players.get(docId); if (!doc || !doc.alive || doc.role !== ROLE.DOCTOR) return null; if (this.nightActions.doctorTarget !== null) return null; const target = this.players.get(targetId); if (!target || !target.alive) return null; if (targetId === this.lastDoctorTarget) return null; this.nightActions.doctorTarget = targetId; this._checkNightComplete(); return target.name; } handleCommissionerCheck(comId, targetId) { if (this.phase !== PHASE.NIGHT) return null; const com = this.players.get(comId); if (!com || !com.alive || com.role !== ROLE.COMMISSIONER) return null; if (this.nightActions.commissionerTarget !== null) return null; const target = this.players.get(targetId); if (!target || !target.alive) return null; this.nightActions.commissionerTarget = targetId; const isMafia = target.isMafia; this._checkNightComplete(); return isMafia; } _checkNightComplete() { const expected = this._expectedNightActions(); const received = this._receivedNightActions(); if (received >= expected) { this._clearTimer(); setTimeout(() => this._resolveNight(), 1500); } } _expectedNightActions() { let count = this._aliveMafia().length; if (this._findAliveByRole(ROLE.DON)) count++; if (this._findAliveByRole(ROLE.DOCTOR)) count++; if (this._findAliveByRole(ROLE.COMMISSIONER)) count++; return count; } _receivedNightActions() { let count = this.nightActions.mafiaVotes.size; if (this.nightActions.donCheckTarget !== null) count++; if (this.nightActions.doctorTarget !== null) count++; if (this.nightActions.commissionerTarget !== null) count++; return count; } async _resolveNight() { if (this.phase !== PHASE.NIGHT) return; this.phase = PHASE.DAY_ANNOUNCE; this._clearTimer(); const mafiaTarget = this._resolveMafiaTarget(); const healed = mafiaTarget !== null && this.nightActions.doctorTarget === mafiaTarget; this.lastDoctorTarget = this.nightActions.doctorTarget; await this._sendCommissionerResult(); await this._sendDonCheckResult(); let killedPlayer = null; if (mafiaTarget !== null && !healed) { killedPlayer = this.players.get(mafiaTarget); killedPlayer.kill(); } await this._announceNightResults(killedPlayer, healed); const winner = this._checkWinCondition(); if (winner) { await this._endGame(winner); return; } await this._startDiscussion(); } _resolveMafiaTarget() { const votes = this.nightActions.mafiaVotes; if (votes.size === 0) { const nonMafia = this._alivePlayers().filter(p => !p.isMafia); if (nonMafia.length === 0) return null; return nonMafia[Math.floor(Math.random() * nonMafia.length)].id; } const tally = new Map(); for (const targetId of votes.values()) { tally.set(targetId, (tally.get(targetId) || 0) + 1); } let maxVotes = 0; for (const v of tally.values()) { if (v > maxVotes) maxVotes = v; } const topTargets = [...tally.entries()] .filter(([, v]) => v === maxVotes) .map(([id]) => id); return topTargets[Math.floor(Math.random() * topTargets.length)]; } async _sendCommissionerResult() { const com = this._findAliveByRole(ROLE.COMMISSIONER); const targetId = this.nightActions.commissionerTarget; if (!com || targetId === null) return; const target = this.players.get(targetId); if (!target) return; const result = target.isMafia ? `🔴 ${target.name} — МАФИЯ!` : `🟢 ${target.name} — мирный житель.`; await this._sendDM(com.id, `🔍 Результат проверки:\n${result}`); } async _sendDonCheckResult() { const don = this._findAliveByRole(ROLE.DON); const targetId = this.nightActions.donCheckTarget; if (!don || targetId === null) return; const target = this.players.get(targetId); if (!target) return; const isCom = target.role === ROLE.COMMISSIONER; const result = isCom ? `🔴 ${target.name} — КОМИССАР!` : `🟢 ${target.name} — не комиссар.`; await this._sendDM(don.id, `🎩 Результат проверки:\n${result}`); } async _announceNightResults(killedPlayer, healed) { let text = `☀️ Наступает день ${this.day}. Город просыпается...\n\n`; if (killedPlayer) { text += `💀 Этой ночью был убит: ${killedPlayer.mention}\n`; text += `Роль: ${ROLE_NAME[killedPlayer.role]}`; } else if (healed) { text += `💊 Доктор спас жизнь этой ночью! Никто не погиб.`; } else { text += `✨ Этой ночью никто не погиб.`; } text += `\n\nОсталось в живых: ${this._alivePlayers().length}`; await this._sendChat(text); } async _startDiscussion() { this.phase = PHASE.DISCUSSION; const alive = this._alivePlayers(); const list = alive.map((p, i) => `${i + 1}. ${p.mention}`).join('\n'); await this._sendChat( `💬 Обсуждение (${TIMER.DISCUSSION} сек).\n` + `Обсудите, кто может быть мафией.\n\n` + `Живые игроки:\n${list}` ); this._startTimer(TIMER.DISCUSSION, () => this._startVoting()); } async _startVoting() { this.phase = PHASE.VOTING; this.dayVotes.clear(); const alive = this._alivePlayers(); await this._sendChat( `🗳 Голосование! (${TIMER.VOTING} сек)\n` + `Нажмите кнопку в ЛС бота, чтобы проголосовать.` ); const promises = alive.map(p => { const kb = votingKeyboard(alive, p.id); return this._sendDM(p.id, '🗳 Голосуйте! Кого исключить из города?', kb); }); await Promise.allSettled(promises); this._startTimer(TIMER.VOTING, () => this._resolveVoting()); } handleDayVote(voterId, targetId) { if (this.phase !== PHASE.VOTING) return null; const voter = this.players.get(voterId); if (!voter || !voter.alive) return null; if (this.dayVotes.has(voterId)) return null; if (targetId !== 'skip') { const target = this.players.get(targetId); if (!target || !target.alive) return null; } this.dayVotes.set(voterId, targetId); const aliveCount = this._alivePlayers().length; if (this.dayVotes.size >= aliveCount) { this._clearTimer(); setTimeout(() => this._resolveVoting(), 1000); } if (targetId === 'skip') return 'Пропустить'; return this.players.get(targetId)?.name ?? null; } async _resolveVoting() { if (this.phase !== PHASE.VOTING) return; this.phase = PHASE.LAST_WORD; this._clearTimer(); const tally = new Map(); let skipVotes = 0; for (const targetId of this.dayVotes.values()) { if (targetId === 'skip') { skipVotes++; } else { tally.set(targetId, (tally.get(targetId) || 0) + 1); } } let resultText = '📊 Результаты голосования:\n'; const sortedTally = [...tally.entries()].sort((a, b) => b[1] - a[1]); for (const [id, count] of sortedTally) { const p = this.players.get(id); resultText += ` ${p.mention} — ${count} гол.\n`; } if (skipVotes > 0) { resultText += ` Пропустить — ${skipVotes} гол.\n`; } if (this.dayVotes.size === 0) { resultText += ' Никто не голосовал.\n'; } let maxVotes = skipVotes; let eliminatedId = null; for (const [id, count] of tally) { if (count > maxVotes) { maxVotes = count; eliminatedId = id; } else if (count === maxVotes) { eliminatedId = null; } } if (eliminatedId) { const victim = this.players.get(eliminatedId); victim.kill(); resultText += `\n⚰️ Город решил казнить: ${victim.mention}\n`; resultText += `Роль: ${ROLE_NAME[victim.role]}`; await this._sendChat(resultText); const winner = this._checkWinCondition(); if (winner) { await this._endGame(winner); return; } await this._sendChat( `🗣 ${victim.mention}, у вас есть ${TIMER.LAST_WORD} секунд на последнее слово.` ); this._startTimer(TIMER.LAST_WORD, () => this._startNight()); } else { resultText += '\n🤝 Ничья — никто не исключён.'; await this._sendChat(resultText); const winner = this._checkWinCondition(); if (winner) { await this._endGame(winner); return; } await this._startNight(); } } _checkWinCondition() { const aliveMafia = this._aliveMafia().length; const aliveCivil = this._alivePlayers().length - aliveMafia; if (aliveMafia === 0) return 'civilians'; if (aliveMafia >= aliveCivil) return 'mafia'; return null; } async _endGame(winner) { this.phase = PHASE.GAME_OVER; this._clearTimer(); const isMafiaWin = winner === 'mafia'; let text = isMafiaWin ? '🔴 Мафия победила! Город пал.\n\n' : '🟢 Мирные жители победили! Мафия уничтожена.\n\n'; text += '📋 Роли игроков:\n'; for (const p of this.players.values()) { const status = p.alive ? '✅' : '💀'; text += `${status} ${p.mention} — ${ROLE_NAME[p.role]}\n`; } text += '\n📊 Статистика:\n'; text += ` Дней прожито: ${this.day}\n`; text += ` Игроков: ${this.players.size}\n`; text += ` Выживших: ${this._alivePlayers().length}`; await this._sendChat(text); this.onEnd(this.chatId); } async handlePlayerLeave(userId) { const player = this.players.get(userId); if (!player || !player.alive) return; player.kill(); await this._sendChat( `🚪 ${player.mention} покинул игру.\nРоль: ${ROLE_NAME[player.role]}` ); const winner = this._checkWinCondition(); if (winner) { await this._endGame(winner); } } async forceStop(userId) { if (userId !== this.hostId) return false; this._clearTimer(); this.phase = PHASE.GAME_OVER; await this._sendChat('🛑 Игра принудительно остановлена создателем.'); this.onEnd(this.chatId); return true; } _alivePlayers() { const result = []; for (const p of this.players.values()) { if (p.alive) result.push(p); } return result; } _aliveMafia() { const result = []; for (const p of this.players.values()) { if (p.alive && p.isMafia) result.push(p); } return result; } _findAliveByRole(role) { for (const p of this.players.values()) { if (p.alive && p.role === role) return p; } return null; } _emptyNightActions() { return { mafiaVotes: new Map(), donCheckTarget: null, doctorTarget: null, commissionerTarget: null, }; } async _sendChat(text, keyboard = undefined) { try { await this.vk.api.messages.send({ peer_id: this.chatId, message: text, keyboard: keyboard ? keyboard.toString() : undefined, random_id: Math.floor(Math.random() * 1e9), }); } catch (err) { console.error(`[Game] Ошибка отправки в чат ${this.chatId}:`, err.message); } } async _sendDM(userId, text, keyboard = undefined) { try { await this.vk.api.messages.send({ user_id: userId, message: text, keyboard: keyboard ? keyboard.toString() : undefined, random_id: Math.floor(Math.random() * 1e9), }); return true; } catch (err) { console.error(`[Game] Не удалось отправить ЛС ${userId}:`, err.message); const player = this.players.get(userId); if (player) { await this._sendChat( `⚠️ Не удалось отправить сообщение ${player.mention}. ` + `Убедитесь, что ЛС бота открыты.` ); } return false; } } _startTimer(seconds, callback) { this._clearTimer(); this._timer = setTimeout(async () => { try { await callback.call(this); } catch (err) { console.error('[Game] Ошибка в таймере:', err); } }, seconds * 1000); } _clearTimer() { if (this._timer) { clearTimeout(this._timer); this._timer = null; } } _shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } } module.exports = Game;
keyboards.js:'use strict'; const { Keyboard } = require('vk-io'); function buildTargetKeyboard(targets, actionPrefix, columns = 2) { const builder = Keyboard.builder(); let col = 0; for (const t of targets) { builder.callbackButton({ label: t.label.slice(0, 40), payload: { action: actionPrefix, target: t.id }, color: Keyboard.SECONDARY_COLOR, }); col++; if (col >= columns) { builder.row(); col = 0; } } return builder.inline(); } function registrationKeyboard() { return Keyboard.builder() .callbackButton({ label: 'Присоединиться', payload: { action: 'join' }, color: Keyboard.POSITIVE_COLOR, }) .callbackButton({ label: 'Покинуть', payload: { action: 'leave' }, color: Keyboard.NEGATIVE_COLOR, }) .row() .callbackButton({ label: 'Начать игру', payload: { action: 'start_game' }, color: Keyboard.PRIMARY_COLOR, }) .inline(); } function votingKeyboard(alivePlayers, voterId) { const targets = alivePlayers .filter(p => p.id !== voterId) .map(p => ({ id: p.id, label: p.name })); const builder = Keyboard.builder(); let col = 0; for (const t of targets) { builder.callbackButton({ label: t.label.slice(0, 40), payload: { action: 'day_vote', target: t.id }, color: Keyboard.SECONDARY_COLOR, }); col++; if (col >= 2) { builder.row(); col = 0; } } if (col !== 0) builder.row(); builder.callbackButton({ label: 'Пропустить', payload: { action: 'day_vote', target: 'skip' }, color: Keyboard.NEGATIVE_COLOR, }); return builder.inline(); } module.exports = { buildTargetKeyboard, registrationKeyboard, votingKeyboard, };
handler/index:'use strict'; const { PHASE, LIMITS, ROLE_NAME, ROLE_DESC, ROLE } = require('../config'); const { registrationKeyboard } = require('../utils/keyboards'); function registerHandlers(vk, manager) { vk.updates.on('message_new', async (context, next) => { if (!context.text) return next(); const text = context.text.toLowerCase().trim(); const isChat = context.peerType === 'chat'; const userId = context.senderId; const peerId = context.peerId; if (isChat) { if (text === '/мафия' || text === '/mafia') { return handleCreateGame(context, manager, peerId, userId); } if (text === '/стоп' || text === '/stop') { return handleStopGame(context, manager, peerId, userId); } if (text === '/правила' || text === '/rules') { return handleRules(context); } if (text === '/роли' || text === '/roles') { return handleRolesInfo(context); } if (text === '/статус' || text === '/status') { return handleStatus(context, manager, peerId); } if (text === '/выйти' || text === '/leave') { return handleLeaveCommand(context, manager, peerId, userId); } } return next(); }); vk.updates.on('message_event', async (context) => { const payload = context.eventPayload; const userId = context.userId; const peerId = context.peerId; if (!payload || !payload.action) return; try { switch (payload.action) { case 'join': await handleJoin(context, vk, manager, peerId, userId); break; case 'leave': await handleLeave(context, vk, manager, peerId, userId); break; case 'start_game': await handleStartGame(context, vk, manager, peerId, userId); break; case 'mafia_vote': await handleNightAction(context, manager, userId, 'mafia_vote', payload.target); break; case 'don_check': await handleNightAction(context, manager, userId, 'don_check', payload.target); break; case 'doctor_heal': await handleNightAction(context, manager, userId, 'doctor_heal', payload.target); break; case 'commissioner_check': await handleNightAction(context, manager, userId, 'commissioner_check', payload.target); break; case 'day_vote': await handleDayVoteAction(context, manager, userId, payload.target); break; default: await context.answer({ type: 'show_snackbar', text: 'Неизвестное действие.' }); } } catch (err) { console.error('[Handler] Ошибка обработки callback:', err); try { await context.answer({ type: 'show_snackbar', text: 'Произошла ошибка.' }); } catch {} } }); } async function handleCreateGame(context, manager, peerId, userId) { const result = manager.createGame(peerId, userId); if (!result.ok) { return context.send(result.reason); } const name = await _getUserName(context.api, userId); result.game.addPlayer(userId, name); manager.registerPlayer(userId, peerId); await context.send({ message: `🎭 Игра «Мафия» создана!\n` + `Организатор: [id${userId}|${name}]\n\n` + `Для участия нажмите «Присоединиться».\n` + `Минимум ${LIMITS.MIN_PLAYERS} игрока, максимум ${LIMITS.MAX_PLAYERS}.\n\n` + `Игроки:\n1. [id${userId}|${name}]`, keyboard: registrationKeyboard(), }); } async function handleJoin(context, vk, manager, peerId, userId) { const game = manager.getGame(peerId); if (!game) { return context.answer({ type: 'show_snackbar', text: 'Игра не найдена.' }); } const name = await _getUserName(vk.api, userId); const result = game.addPlayer(userId, name); if (!result.ok) { return context.answer({ type: 'show_snackbar', text: result.reason }); } manager.registerPlayer(userId, peerId); await context.answer({ type: 'show_snackbar', text: '✅ Вы вступили в игру!' }); await vk.api.messages.send({ peer_id: peerId, message: `✅ [id${userId}|${name}] вступает в игру!\n\n` + `Игроки (${game.players.size}):\n${game.playerList}`, keyboard: registrationKeyboard(), random_id: Math.floor(Math.random() * 1e9), }); } async function handleLeave(context, vk, manager, peerId, userId) { const game = manager.getGame(peerId); if (!game) { return context.answer({ type: 'show_snackbar', text: 'Игра не найдена.' }); } if (game.phase !== PHASE.REGISTRATION) { await game.handlePlayerLeave(userId); manager.unregisterPlayer(userId); return context.answer({ type: 'show_snackbar', text: 'Вы вышли из игры.' }); } const result = game.removePlayer(userId); if (!result.ok) { return context.answer({ type: 'show_snackbar', text: result.reason }); } manager.unregisterPlayer(userId); await context.answer({ type: 'show_snackbar', text: 'Вы покинули игру.' }); const player = `[id${userId}|Игрок]`; const count = game.players.size; if (count === 0) { manager._onGameEnd(peerId); await vk.api.messages.send({ peer_id: peerId, message: `${player} покинул. Все вышли — игра отменена.`, random_id: Math.floor(Math.random() * 1e9), }); } else { await vk.api.messages.send({ peer_id: peerId, message: `❌ ${player} покидает игру.\n\n` + `Игроки (${count}):\n${game.playerList}`, keyboard: registrationKeyboard(), random_id: Math.floor(Math.random() * 1e9), }); } } async function handleLeaveCommand(context, manager, peerId, userId) { const game = manager.getGame(peerId); if (!game) return; if (game.phase === PHASE.REGISTRATION) { const result = game.removePlayer(userId); if (result.ok) { manager.unregisterPlayer(userId); await context.send(`❌ Вы покинули игру.\n\nИгроки (${game.players.size}):\n${game.playerList}`); } } else { await game.handlePlayerLeave(userId); manager.unregisterPlayer(userId); } } async function handleStartGame(context, vk, manager, peerId, userId) { const game = manager.getGame(peerId); if (!game) { return context.answer({ type: 'show_snackbar', text: 'Игра не найдена.' }); } const result = await game.start(userId); if (!result.ok) { return context.answer({ type: 'show_snackbar', text: result.reason }); } await context.answer({ type: 'show_snackbar', text: '🎮 Игра запущена!' }); } async function handleNightAction(context, manager, userId, action, targetId) { const game = manager.getGameByPlayer(userId); if (!game || game.phase !== PHASE.NIGHT) { return context.answer({ type: 'show_snackbar', text: 'Сейчас не время для этого.' }); } let result = null; let responseText = ''; switch (action) { case 'mafia_vote': { const name = game.handleMafiaVote(userId, targetId); if (name === null) { return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' }); } responseText = `🔪 Вы голосуете за: ${name}`; break; } case 'don_check': { result = game.handleDonCheck(userId, targetId); if (result === null) { return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' }); } const target = game.players.get(targetId); responseText = result ? `🔴 ${target.name} — КОМИССАР!` : `🟢 ${target.name} — не комиссар.`; break; } case 'doctor_heal': { const name = game.handleDoctorHeal(userId, targetId); if (name === null) { return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' }); } responseText = `💊 Вы лечите: ${name}`; break; } case 'commissioner_check': { result = game.handleCommissionerCheck(userId, targetId); if (result === null) { return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' }); } const target = game.players.get(targetId); responseText = result ? `🔴 ${target.name} — МАФИЯ!` : `🟢 ${target.name} — мирный.`; break; } } await context.answer({ type: 'show_snackbar', text: responseText }); } async function handleDayVoteAction(context, manager, userId, targetId) { const game = manager.getGameByPlayer(userId); if (!game || game.phase !== PHASE.VOTING) { return context.answer({ type: 'show_snackbar', text: 'Сейчас не время для голосования.' }); } const result = game.handleDayVote(userId, targetId); if (result === null) { return context.answer({ type: 'show_snackbar', text: 'Действие недоступно.' }); } await context.answer({ type: 'show_snackbar', text: `🗳 Ваш голос: ${result}` }); } async function handleStopGame(context, manager, peerId, userId) { const game = manager.getGame(peerId); if (!game) { return context.send('Нет активной игры.'); } const ok = await game.forceStop(userId); if (!ok) { await context.send('Только создатель игры может её остановить.'); } } async function handleRules(context) { await context.send( `📖 Правила игры «Мафия»\n\n` + `🎭 Роли:\n` + `• Мафия — убивает каждую ночь\n` + `• Дон мафии — глава мафии + проверяет на комиссара\n` + `• Комиссар — проверяет игрока ночью (мафия/мирный)\n` + `• Доктор — лечит одного игрока за ночь\n` + `• Мирный житель — голосует днём\n\n` + `🌙 Ночь:\n` + `Мафия выбирает жертву. Доктор лечит. Комиссар проверяет.\n\n` + `☀️ День:\n` + `Обсуждение → Голосование. Игрок с большинством голосов исключён.\n\n` + `🏆 Победа:\n` + `• Мирные побеждают, когда вся мафия уничтожена\n` + `• Мафия побеждает, когда мафий >= мирных\n\n` + `⚙️ Нюансы:\n` + `• Доктор не может лечить одного и того же 2 ночи подряд\n` + `• При ничьей голосов днём — никого не исключают\n` + `• Если мафия не голосует — жертва случайная\n` + `• Дон голосует с мафией + проверяет на комиссара\n\n` + `📝 Команды:\n` + `/мафия — создать игру\n` + `/стоп — остановить игру (создатель)\n` + `/выйти — покинуть игру\n` + `/роли — распределение ролей\n` + `/статус — статус текущей игры\n` + `/правила — эта справка` ); } async function handleRolesInfo(context) { let text = '🎭 Распределение ролей по количеству игроков:\n\n'; for (let n = LIMITS.MIN_PLAYERS; n <= 12; n++) { const mafia = Math.max(1, Math.floor(n / 3)); const hasDon = mafia >= 2; const hasCom = n >= 5; const special = 1 + (hasCom ? 1 : 0); const citizens = n - mafia - special; text += `👥 ${n} игроков: `; if (hasDon) text += `1 Дон + ${mafia - 1} Маф.`; else text += `${mafia} Маф.`; text += `, 1 Док.`; if (hasCom) text += `, 1 Ком.`; text += `, ${citizens} Мирн.\n`; } await context.send(text); } async function handleStatus(context, manager, peerId) { const game = manager.getGame(peerId); if (!game) { return context.send('Нет активной игры. Напишите /мафия для создания.'); } const phaseName = { [PHASE.REGISTRATION]: '📝 Регистрация', [PHASE.NIGHT]: '🌙 Ночь', [PHASE.DAY_ANNOUNCE]: '☀️ Утро', [PHASE.DISCUSSION]: '💬 Обсуждение', [PHASE.VOTING]: '🗳 Голосование', [PHASE.LAST_WORD]: '🗣 Последнее слово', [PHASE.GAME_OVER]: '🏁 Игра окончена', }; const alive = game._alivePlayers(); let text = `📊 Статус игры:\n`; text += `Фаза: ${phaseName[game.phase] || game.phase}\n`; text += `День: ${game.day}\n`; text += `Игроков: ${game.players.size} (живых: ${alive.length})\n\n`; text += `Живые:\n`; text += alive.map((p, i) => `${i + 1}. ${p.mention}`).join('\n'); await context.send(text); } async function _getUserName(api, userId) { try { const [user] = await api.users.get({ user_ids: [userId] }); return `${user.first_name} ${user.last_name}`; } catch { return `Игрок ${userId}`; } } module.exports = { registerHandlers };
Интересная реализация