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 кинут?
ну я впринципе понял