sto-k-odnomu/backend/src/game/game.gateway.ts
2026-01-09 01:56:11 +03:00

585 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { RoomsService } from '../rooms/rooms.service';
import { RoomEventsService } from './room-events.service';
import { PrismaService } from '../prisma/prisma.service';
import { RoomPackService } from '../room-pack/room-pack.service';
import { Prisma } from '@prisma/client';
interface PlayerAction {
action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion';
roomId: string;
roomCode: string;
userId: string;
participantId: string;
// Для revealAnswer:
questionId?: string; // UUID вопроса
answerId?: string; // UUID ответа
}
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[];
}
@WebSocketGateway({
cors: {
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,
},
})
export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
@WebSocketServer()
server: Server;
constructor(
private roomsService: RoomsService,
private roomEventsService: RoomEventsService,
private prisma: PrismaService,
private roomPackService: RoomPackService,
) {}
afterInit(server: Server) {
this.roomEventsService.setServer(server);
}
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
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;
}
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;
}
@SubscribeMessage('joinRoom')
async handleJoinRoom(client: Socket, payload: { roomCode: string; userId: string }) {
client.join(payload.roomCode);
await this.broadcastFullState(payload.roomCode);
}
@SubscribeMessage('startGame')
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;
}
await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING');
// Инициализировать первый вопрос и игрока
const room = (await this.prisma.room.findUnique({
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) {
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
const firstQuestion = questions[0];
// Админ (хост) должен быть первым игроком
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
// Убеждаемся что firstQuestion.id - строка (UUID)
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
? firstQuestion.id
: null;
if (firstQuestionId && firstParticipant) {
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: firstQuestionId,
currentQuestionIndex: 0,
currentPlayerId: firstParticipant.id,
}
});
}
}
await this.broadcastFullState(payload.roomCode);
}
@SubscribeMessage('playerAction')
async handlePlayerAction(client: Socket, payload: PlayerAction) {
// Получаем комнату с данными
const room = (await this.prisma.room.findUnique({
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
} as unknown as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (!room) {
client.emit('error', { message: 'Room not found' });
return;
}
// Проверяем права
const isHost = room.hostId === payload.userId;
const isCurrentPlayer = room.currentPlayerId === payload.participantId;
if (!isHost && !isCurrentPlayer) {
client.emit('error', { message: 'Not your turn!' });
return;
}
// Выполняем действие
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' });
}
}
private async handleRevealAnswerAction(payload: PlayerAction, room: any) {
if (!payload.questionId || !payload.answerId) {
console.error('Missing questionId or answerId in payload');
return;
}
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
const question = questions.find(q => q.id === payload.questionId);
if (!question) {
console.error('Question not found:', payload.questionId);
return;
}
const answer = question.answers?.find((a: any) => a.id === payload.answerId);
if (!answer) {
console.error('Answer not found:', payload.answerId);
return;
}
// Обновляем revealedAnswers
const revealed = (room.revealedAnswers as RevealedAnswers) || {};
const currentRevealed: string[] = revealed[payload.questionId || ''] || [];
if (!currentRevealed.includes(payload.answerId)) {
currentRevealed.push(payload.answerId);
revealed[payload.questionId] = currentRevealed;
// Начисляем очки
await this.prisma.participant.update({
where: { id: payload.participantId },
data: { score: { increment: answer.points } }
});
// Сохраняем revealedAnswers
await this.prisma.room.update({
where: { id: payload.roomId },
data: { revealedAnswers: revealed as Prisma.InputJsonValue }
});
// Определяем следующего игрока
const participants = room.participants;
const currentIdx = participants.findIndex((p) => p.id === payload.participantId);
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 }
});
}
}
}
}
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));
if (currentIdx < questions.length - 1) {
const nextQuestion = questions[currentIdx + 1];
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: nextQuestion.id,
currentQuestionIndex: currentIdx + 1
}
});
}
}
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));
if (currentIdx > 0) {
const prevQuestion = questions[currentIdx - 1];
await this.prisma.room.update({
where: { id: payload.roomId },
data: {
currentQuestionId: prevQuestion.id,
currentQuestionIndex: currentIdx - 1
}
});
}
}
// КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния
private async broadcastFullState(roomCode: string) {
const room = (await this.prisma.room.findUnique({
where: { code: roomCode },
include: {
participants: {
where: { isActive: true },
orderBy: { joinedAt: 'asc' }
},
roomPack: true,
host: { select: { id: true, name: true } }
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (!room) return;
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
// Инициализация 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; // Сбрасываем если вопрос удален
}
}
// Устанавливаем первый вопрос если нет текущего
if (!currentQuestionId && questions.length > 0) {
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
}
});
}
}
// Инициализация 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 }
});
}
}
const fullState = {
roomId: room.id,
roomCode: room.code,
status: room.status,
currentQuestionId: currentQuestionId,
currentPlayerId: currentPlayerId,
revealedAnswers: room.revealedAnswers as RevealedAnswers,
isGameOver: room.isGameOver,
hostId: room.hostId,
participants: room.participants.map((p) => ({
id: p.id,
userId: p.userId,
name: p.name,
role: p.role,
score: p.score
})),
questions: questions.map((q) => ({
id: q.id,
text: q.text || q.question || '',
answers: (q.answers || []).map((a) => ({
id: a.id,
text: a.text,
points: a.points
}))
}))
};
this.server.to(roomCode).emit('gameStateUpdated', fullState);
}
@SubscribeMessage('requestFullState')
async handleRequestFullState(client: Socket, payload: { roomCode: string }) {
await this.broadcastFullState(payload.roomCode);
}
@SubscribeMessage('endGame')
async handleEndGame(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 end the game' });
return;
}
await this.roomsService.updateRoomStatus(payload.roomId, 'FINISHED');
await this.broadcastFullState(payload.roomCode);
}
@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;
}
const room = (await this.prisma.room.findUnique({
where: { id: payload.roomId },
include: {
roomPack: true,
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
} as Prisma.RoomInclude,
})) as unknown as RoomWithPack | null;
if (room) {
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
const firstQuestion = questions[0];
// Админ (хост) должен быть первым игроком
const hostParticipant = room.participants.find(p => p.userId === room.hostId);
const firstParticipant = hostParticipant || room.participants[0];
// Убеждаемся что 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,
}
});
}
await this.prisma.participant.updateMany({
where: { roomId: payload.roomId },
data: { score: 0 },
});
await this.broadcastFullState(payload.roomCode);
}
@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);
}
@SubscribeMessage('updateRoomPack')
async handleUpdateRoomPack(client: Socket, payload: { roomId: string; roomCode: string; userId: string; questions: any[] }) {
const isHost = await this.isHost(payload.roomId, payload.userId);
if (!isHost) {
client.emit('error', { message: 'Only the host can update questions' });
return;
}
// Обновляем вопросы через service (который добавит UUID если нужно)
await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
// После обновления вопросов проверяем и обновляем 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
}
});
}
}
await this.broadcastFullState(payload.roomCode);
}
@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);
await this.broadcastFullState(payload.roomCode);
}
@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 },
});
await this.broadcastFullState(payload.roomCode);
}
}