sto-k-odnomu/backend/src/game/game.gateway.ts

586 lines
20 KiB
TypeScript
Raw Normal View History

2026-01-03 14:07:04 +00:00
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
2026-01-07 14:32:51 +00:00
OnGatewayInit,
2026-01-03 14:07:04 +00:00
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { RoomsService } from '../rooms/rooms.service';
2026-01-07 14:32:51 +00:00
import { RoomEventsService } from './room-events.service';
2026-01-08 13:18:07 +00:00
import { PrismaService } from '../prisma/prisma.service';
2026-01-08 17:56:00 +00:00
import { RoomPackService } from '../room-pack/room-pack.service';
2026-01-08 22:44:20 +00:00
import { Prisma } from '@prisma/client';
2026-01-03 14:07:04 +00:00
2026-01-08 21:44:38 +00:00
interface PlayerAction {
action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion';
roomId: string;
roomCode: string;
userId: string;
participantId: string;
// Для revealAnswer:
questionId?: string; // UUID вопроса
answerId?: string; // UUID ответа
}
2026-01-08 22:44:20 +00:00
interface Question {
id: string;
text?: string;
question?: string;
answers: Array<{
id: string;
text: string;
points: number;
}>;
}
type RoomWithPack = Prisma.RoomGetPayload<{
include: {
roomPack: true;
participants: true;
}
}> & {
currentQuestionId?: string | null;
};
interface RevealedAnswers {
[questionId: string]: string[];
}
2026-01-03 14:07:04 +00:00
@WebSocketGateway({
cors: {
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
},
})
2026-01-07 14:32:51 +00:00
export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
2026-01-03 14:07:04 +00:00
@WebSocketServer()
server: Server;
2026-01-07 14:32:51 +00:00
constructor(
private roomsService: RoomsService,
private roomEventsService: RoomEventsService,
2026-01-08 13:18:07 +00:00
private prisma: PrismaService,
2026-01-08 17:56:00 +00:00
private roomPackService: RoomPackService,
2026-01-07 14:32:51 +00:00
) {}
afterInit(server: Server) {
this.roomEventsService.setServer(server);
}
2026-01-03 14:07:04 +00:00
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
2026-01-08 13:18:07 +00:00
private async isHost(roomId: string, userId: string): Promise<boolean> {
const room = await this.prisma.room.findUnique({
where: { id: roomId },
select: { hostId: true },
});
return room?.hostId === userId;
}
2026-01-08 21:44:38 +00:00
private async isCurrentPlayer(roomId: string, participantId: string): Promise<boolean> {
const room = await this.prisma.room.findUnique({
where: { id: roomId },
select: { currentPlayerId: true },
});
return room?.currentPlayerId === participantId;
}
2026-01-03 14:07:04 +00:00
@SubscribeMessage('joinRoom')
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
client.join(payload.roomCode);
2026-01-08 21:44:38 +00:00
await this.broadcastFullState(payload.roomCode);
2026-01-03 14:07:04 +00:00
}
@SubscribeMessage('startGame')
2026-01-08 13:18:07 +00:00
async handleStartGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) {
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
client.emit('error', { message: 'Only the host can start the game' });
return;
}
2026-01-03 14:07:04 +00:00
await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING');
2026-01-08 21:44:38 +00:00
// Инициализировать первый вопрос и игрока
2026-01-08 22:44:20 +00:00
const room = (await this.prisma.room.findUnique({
2026-01-08 21:44:38 +00:00
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
2026-01-08 22:44:20 +00:00
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
2026-01-03 14:07:04 +00:00
2026-01-08 21:44:38 +00:00
if (room) {
2026-01-08 22:44:20 +00:00
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
2026-01-08 21:44:38 +00:00
const firstQuestion = questions[0];
2026-01-08 22:56:11 +00:00
// Админ (хост) должен быть первым игроком
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
2026-01-08 21:44:38 +00:00
2026-01-08 22:44:20 +00:00
// Убеждаемся что firstQuestion.id - строка (UUID)
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
? firstQuestion.id
: null;
if (firstQuestionId && firstParticipant) {
2026-01-08 21:44:38 +00:00
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
2026-01-08 22:44:20 +00:00
currentQuestionId: firstQuestionId,
currentQuestionIndex: 0,
2026-01-08 21:44:38 +00:00
currentPlayerId: firstParticipant.id,
}
});
}
2026-01-08 13:18:07 +00:00
}
2026-01-08 21:44:38 +00:00
await this.broadcastFullState(payload.roomCode);
2026-01-03 14:07:04 +00:00
}
2026-01-08 21:44:38 +00:00
@SubscribeMessage('playerAction')
async handlePlayerAction(client: Socket, payload: PlayerAction) {
// Получаем комнату с данными
2026-01-08 22:44:20 +00:00
const room = (await this.prisma.room.findUnique({
2026-01-08 21:44:38 +00:00
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
2026-01-08 22:44:20 +00:00
} as unknown as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
2026-01-08 21:44:38 +00:00
if (!room) {
client.emit('error', { message: 'Room not found' });
2026-01-08 20:14:58 +00:00
return;
}
2026-01-08 21:44:38 +00:00
// Проверяем права
const isHost = room.hostId === payload.userId;
const isCurrentPlayer = room.currentPlayerId === payload.participantId;
2026-01-08 20:14:58 +00:00
2026-01-08 21:44:38 +00:00
if (!isHost && !isCurrentPlayer) {
client.emit('error', { message: 'Not your turn!' });
2026-01-08 20:14:58 +00:00
return;
}
2026-01-08 21:44:38 +00:00
// Выполняем действие
try {
switch (payload.action) {
case 'revealAnswer':
await this.handleRevealAnswerAction(payload, room);
break;
case 'nextQuestion':
await this.handleNextQuestionAction(payload, room);
break;
case 'prevQuestion':
await this.handlePrevQuestionAction(payload, room);
break;
}
// Отправляем полное состояние всем
await this.broadcastFullState(payload.roomCode);
} catch (error) {
console.error('Error handling player action:', error);
client.emit('error', { message: 'Failed to process action' });
}
2026-01-08 20:14:58 +00:00
}
2026-01-08 21:44:38 +00:00
private async handleRevealAnswerAction(payload: PlayerAction, room: any) {
2026-01-08 21:58:00 +00:00
if (!payload.questionId || !payload.answerId) {
console.error('Missing questionId or answerId in payload');
return;
}
2026-01-08 22:44:20 +00:00
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
2026-01-08 21:44:38 +00:00
const question = questions.find(q => q.id === payload.questionId);
if (!question) {
console.error('Question not found:', payload.questionId);
2026-01-08 20:14:58 +00:00
return;
}
2026-01-08 21:44:38 +00:00
const answer = question.answers?.find((a: any) => a.id === payload.answerId);
if (!answer) {
console.error('Answer not found:', payload.answerId);
2026-01-08 13:18:07 +00:00
return;
}
2026-01-08 21:44:38 +00:00
// Обновляем revealedAnswers
2026-01-08 22:44:20 +00:00
const revealed = (room.revealedAnswers as RevealedAnswers) || {};
const currentRevealed: string[] = revealed[payload.questionId || ''] || [];
2026-01-03 14:07:04 +00:00
2026-01-08 21:44:38 +00:00
if (!currentRevealed.includes(payload.answerId)) {
currentRevealed.push(payload.answerId);
revealed[payload.questionId] = currentRevealed;
2026-01-08 13:18:07 +00:00
2026-01-08 21:44:38 +00:00
// Начисляем очки
await this.prisma.participant.update({
where: { id: payload.participantId },
data: { score: { increment: answer.points } }
});
2026-01-08 20:14:58 +00:00
2026-01-08 21:44:38 +00:00
// Сохраняем revealedAnswers
2026-01-08 20:14:58 +00:00
await this.prisma.room.update({
where: { id: payload.roomId },
2026-01-08 22:44:20 +00:00
data: { revealedAnswers: revealed as Prisma.InputJsonValue }
2026-01-08 20:14:58 +00:00
});
2026-01-08 21:44:38 +00:00
// Определяем следующего игрока
const participants = room.participants;
2026-01-08 22:44:20 +00:00
const currentIdx = participants.findIndex((p) => p.id === payload.participantId);
2026-01-08 21:44:38 +00:00
const nextIdx = (currentIdx + 1) % participants.length;
const nextPlayerId = participants[nextIdx]?.id;
// Проверяем, это последний ответ?
const isLastAnswer = currentRevealed.length >= question.answers.length;
if (!isLastAnswer) {
// Меняем игрока
await this.prisma.room.update({
where: { id: payload.roomId },
data: { currentPlayerId: nextPlayerId }
});
} else {
// Последний ответ - проверяем, последний ли вопрос
const currentQuestionIndex = questions.findIndex((q: any) => q.id === payload.questionId);
const isLastQuestion = currentQuestionIndex >= questions.length - 1;
if (isLastQuestion) {
// Конец игры
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
status: 'FINISHED',
isGameOver: true,
finishedAt: new Date()
}
});
} else {
// Меняем игрока для следующего вопроса
await this.prisma.room.update({
where: { id: payload.roomId },
data: { currentPlayerId: nextPlayerId }
});
}
}
2026-01-08 20:14:58 +00:00
}
}
2026-01-08 22:44:20 +00:00
private async handleNextQuestionAction(payload: PlayerAction, room: RoomWithPack) {
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
2026-01-08 20:14:58 +00:00
2026-01-08 21:44:38 +00:00
if (currentIdx < questions.length - 1) {
const nextQuestion = questions[currentIdx + 1];
2026-01-08 20:14:58 +00:00
await this.prisma.room.update({
where: { id: payload.roomId },
2026-01-08 21:44:38 +00:00
data: {
currentQuestionId: nextQuestion.id,
2026-01-08 22:44:20 +00:00
currentQuestionIndex: currentIdx + 1
2026-01-08 21:44:38 +00:00
}
2026-01-08 20:14:58 +00:00
});
2026-01-08 21:44:38 +00:00
}
}
2026-01-08 22:44:20 +00:00
private async handlePrevQuestionAction(payload: PlayerAction, room: RoomWithPack) {
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
2026-01-08 20:14:58 +00:00
2026-01-08 21:44:38 +00:00
if (currentIdx > 0) {
const prevQuestion = questions[currentIdx - 1];
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: prevQuestion.id,
2026-01-08 22:44:20 +00:00
currentQuestionIndex: currentIdx - 1
2026-01-08 21:44:38 +00:00
}
2026-01-08 20:14:58 +00:00
});
}
2026-01-03 14:07:04 +00:00
}
2026-01-08 21:44:38 +00:00
// КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния
private async broadcastFullState(roomCode: string) {
2026-01-08 22:44:20 +00:00
const room = (await this.prisma.room.findUnique({
2026-01-08 21:44:38 +00:00
where: { code: roomCode },
include: {
participants: {
where: { isActive: true },
orderBy: { joinedAt: 'asc' }
},
roomPack: true,
host: { select: { id: true, name: true } }
2026-01-08 22:44:20 +00:00
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
2026-01-08 13:18:07 +00:00
2026-01-08 21:44:38 +00:00
if (!room) return;
2026-01-08 13:18:07 +00:00
2026-01-08 22:44:20 +00:00
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
2026-01-08 21:44:38 +00:00
2026-01-08 22:44:20 +00:00
// Инициализация currentQuestionId если не установлен или невалиден
let currentQuestionId = (room.currentQuestionId as string | null) || null;
// Проверяем, что currentQuestionId валиден (существует в вопросах)
if (currentQuestionId) {
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
if (!questionExists) {
currentQuestionId = null; // Сбрасываем если вопрос удален
}
}
// Устанавливаем первый вопрос если нет текущего
2026-01-08 21:44:38 +00:00
if (!currentQuestionId && questions.length > 0) {
2026-01-08 22:44:20 +00:00
const firstQuestion = questions[0];
// Убеждаемся что id - строка (UUID)
if (firstQuestion.id && typeof firstQuestion.id === 'string') {
currentQuestionId = firstQuestion.id;
await this.prisma.room.update({
where: { id: room.id },
data: {
currentQuestionId: currentQuestionId,
currentQuestionIndex: 0
}
});
}
2026-01-08 13:18:07 +00:00
}
2026-01-08 22:56:11 +00:00
// Инициализация currentPlayerId если не установлен
let currentPlayerId = room.currentPlayerId;
if (!currentPlayerId && room.participants.length > 0) {
// Админ (хост) должен быть первым игроком
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
if (firstParticipant) {
currentPlayerId = firstParticipant.id;
await this.prisma.room.update({
where: { id: room.id },
data: { currentPlayerId: currentPlayerId }
});
}
}
2026-01-08 21:44:38 +00:00
const fullState = {
roomId: room.id,
roomCode: room.code,
status: room.status,
currentQuestionId: currentQuestionId,
2026-01-08 22:56:11 +00:00
currentPlayerId: currentPlayerId,
2026-01-08 22:44:20 +00:00
revealedAnswers: room.revealedAnswers as RevealedAnswers,
2026-01-08 21:44:38 +00:00
isGameOver: room.isGameOver,
hostId: room.hostId,
2026-01-08 22:44:20 +00:00
participants: room.participants.map((p) => ({
2026-01-08 21:44:38 +00:00
id: p.id,
userId: p.userId,
name: p.name,
role: p.role,
score: p.score
})),
2026-01-08 22:44:20 +00:00
questions: questions.map((q) => ({
2026-01-08 21:44:38 +00:00
id: q.id,
2026-01-08 22:44:20 +00:00
text: q.text || q.question || '',
answers: (q.answers || []).map((a) => ({
2026-01-08 21:44:38 +00:00
id: a.id,
text: a.text,
points: a.points
}))
}))
};
this.server.to(roomCode).emit('gameStateUpdated', fullState);
}
2026-01-08 13:18:07 +00:00
2026-01-08 21:44:38 +00:00
@SubscribeMessage('requestFullState')
async handleRequestFullState(client: Socket, payload: { roomCode: string }) {
await this.broadcastFullState(payload.roomCode);
2026-01-08 13:18:07 +00:00
}
2026-01-08 21:44:38 +00:00
@SubscribeMessage('endGame')
async handleEndGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) {
2026-01-08 13:18:07 +00:00
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
2026-01-08 21:44:38 +00:00
client.emit('error', { message: 'Only the host can end the game' });
2026-01-08 13:18:07 +00:00
return;
}
2026-01-08 21:44:38 +00:00
await this.roomsService.updateRoomStatus(payload.roomId, 'FINISHED');
await this.broadcastFullState(payload.roomCode);
2026-01-08 13:18:07 +00:00
}
@SubscribeMessage('restartGame')
async handleRestartGame(client: Socket, payload: { roomId: string; roomCode: string; userId: string }) {
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
client.emit('error', { message: 'Only the host can restart the game' });
return;
}
2026-01-08 22:44:20 +00:00
const room = (await this.prisma.room.findUnique({
2026-01-08 21:44:38 +00:00
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
2026-01-08 22:44:20 +00:00
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
2026-01-08 21:44:38 +00:00
2026-01-08 22:44:20 +00:00
if (room) {
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
const firstQuestion = questions[0];
2026-01-08 22:56:11 +00:00
// Админ (хост) должен быть первым игроком
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
2026-01-08 21:44:38 +00:00
2026-01-08 22:44:20 +00:00
// Убеждаемся что firstQuestion.id - строка (UUID)
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
? firstQuestion.id
: null;
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
status: 'WAITING',
currentQuestionId: firstQuestionId,
currentQuestionIndex: 0,
revealedAnswers: {} as Prisma.InputJsonValue,
currentPlayerId: firstParticipant?.id || null,
isGameOver: false,
answeredQuestions: 0,
}
});
}
2026-01-08 13:18:07 +00:00
await this.prisma.participant.updateMany({
where: { roomId: payload.roomId },
data: { score: 0 },
});
2026-01-08 21:44:38 +00:00
await this.broadcastFullState(payload.roomCode);
2026-01-08 17:56:00 +00:00
}
2026-01-08 22:56:11 +00:00
@SubscribeMessage('setCurrentPlayer')
async handleSetCurrentPlayer(client: Socket, payload: { roomId: string; roomCode: string; userId: string; participantId: string }) {
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
client.emit('error', { message: 'Only the host can set current player' });
return;
}
// Проверяем, что участник существует и активен
const participant = await this.prisma.participant.findFirst({
where: {
id: payload.participantId,
roomId: payload.roomId,
isActive: true
}
});
if (!participant) {
client.emit('error', { message: 'Participant not found' });
return;
}
await this.prisma.room.update({
where: { id: payload.roomId },
data: { currentPlayerId: payload.participantId }
});
await this.broadcastFullState(payload.roomCode);
}
2026-01-08 17:56:00 +00:00
@SubscribeMessage('updateRoomPack')
async handleUpdateRoomPack(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any[] }) {
2026-01-08 13:18:07 +00:00
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
client.emit('error', { message: 'Only the host can update questions' });
return;
}
2026-01-08 22:44:20 +00:00
// Обновляем вопросы через service (который добавит UUID если нужно)
2026-01-08 21:44:38 +00:00
await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
2026-01-08 22:44:20 +00:00
// После обновления вопросов проверяем и обновляем currentQuestionId
const room = (await this.prisma.room.findUnique({
where: { id: payload.roomId },
include: { roomPack: true } as unknown as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) {
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
const currentQuestionId = (room.currentQuestionId as string | null) || null;
// Проверяем, что currentQuestionId все еще валиден
let validQuestionId = currentQuestionId;
if (currentQuestionId) {
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
if (!questionExists) {
// Текущий вопрос был удален, устанавливаем первый
validQuestionId = questions[0]?.id && typeof questions[0].id === 'string'
? questions[0].id
: null;
}
} else if (questions.length > 0) {
// Если нет currentQuestionId, устанавливаем первый
validQuestionId = questions[0].id && typeof questions[0].id === 'string'
? questions[0].id
: null;
}
// Обновляем currentQuestionId если изменился
if (validQuestionId !== currentQuestionId) {
const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId);
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: validQuestionId,
currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0
}
});
}
}
2026-01-08 21:44:38 +00:00
await this.broadcastFullState(payload.roomCode);
2026-01-08 17:56:00 +00:00
}
@SubscribeMessage('importQuestions')
async handleImportQuestions(client: Socket, payload: {
roomId: string;
roomCode: string;
userId: string;
sourcePackId: string;
questionIndices: number[];
}) {
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
client.emit('error', { message: 'Only the host can import questions' });
return;
}
await this.roomPackService.importQuestions(payload.roomId, payload.sourcePackId, payload.questionIndices);
2026-01-08 21:44:38 +00:00
await this.broadcastFullState(payload.roomCode);
2026-01-08 13:18:07 +00:00
}
@SubscribeMessage('kickPlayer')
async handleKickPlayer(client: Socket, payload: { roomId: string; roomCode: string; userId: string; participantId: string }) {
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
client.emit('error', { message: 'Only the host can kick players' });
return;
}
await this.prisma.participant.update({
where: { id: payload.participantId },
data: { isActive: false },
});
2026-01-08 21:44:38 +00:00
await this.broadcastFullState(payload.roomCode);
2026-01-08 13:18:07 +00:00
}
2026-01-03 14:07:04 +00:00
}