/** * ============================================================================= * Texas Hold'em WebSocket Server * Node.js + ws для мультиплеера 2-6 игроков * ============================================================================= */ const express = require('express'); const { WebSocketServer } = require('ws'); const http = require('http'); const path = require('path'); const database = require('./database'); const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server }); // Middleware для JSON app.use(express.json()); // Статические файлы app.use(express.static(path.join(__dirname, 'public'))); // ============================================================================= // REST API - АВТОРИЗАЦИЯ // ============================================================================= /** * Middleware проверки авторизации */ function authMiddleware(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ error: 'Требуется авторизация' }); } const decoded = database.verifyToken(token); if (!decoded) { return res.status(401).json({ error: 'Недействительный токен' }); } req.user = decoded; next(); } /** * Middleware проверки админа */ function adminMiddleware(req, res, next) { if (req.user.role !== 'admin') { return res.status(403).json({ error: 'Требуются права администратора' }); } next(); } // Регистрация app.post('/api/auth/register', (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Введите логин и пароль' }); } if (username.length < 3 || password.length < 6) { return res.status(400).json({ error: 'Логин мин. 3 символа, пароль мин. 6' }); } const result = database.registerUser(username, password); if (result.success) { res.json(result); } else { res.status(400).json({ error: result.error }); } }); // Вход app.post('/api/auth/login', (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Введите логин и пароль' }); } const result = database.loginUser(username, password); if (result.success) { res.json(result); } else { res.status(401).json({ error: result.error }); } }); // Проверка токена app.get('/api/auth/me', authMiddleware, (req, res) => { const user = database.getUserById(req.user.userId); if (user) { res.json({ user }); } else { res.status(404).json({ error: 'Пользователь не найден' }); } }); // ============================================================================= // REST API - НАСТРОЙКИ // ============================================================================= // Получить пользовательские настройки app.get('/api/settings/user', authMiddleware, (req, res) => { const settings = database.getUserSettings(req.user.userId); res.json({ settings }); }); // Сохранить пользовательские настройки app.post('/api/settings/user', authMiddleware, (req, res) => { database.saveUserSettings(req.user.userId, req.body); database.logAction(req.user.userId, req.user.username, 'settings_update', req.body); res.json({ success: true }); }); // Получить админские настройки app.get('/api/settings/admin', authMiddleware, adminMiddleware, (req, res) => { const settings = database.getAdminSettings(); console.log('📤 Отправка админских настроек:', settings); res.json({ settings }); }); // Сохранить админские настройки app.post('/api/settings/admin', authMiddleware, adminMiddleware, (req, res) => { database.saveAdminSettings(req.body, req.user.userId); database.logAction(req.user.userId, req.user.username, 'admin_settings_update', req.body); res.json({ success: true }); }); // Получить публичные настройки (LLM статус для всех) app.get('/api/settings/public', (req, res) => { const adminSettings = database.getAdminSettings(); res.json({ llmEnabled: adminSettings.llmEnabled, llmProvider: adminSettings.llmProvider, llmApiUrl: adminSettings.llmApiUrl, llmModel: adminSettings.llmModel }); }); // ============================================================================= // REST API - ЛОГИ (только для админа) // ============================================================================= // Получить логи app.get('/api/logs', authMiddleware, adminMiddleware, (req, res) => { const { limit, offset, actionType, userId, startDate, endDate } = req.query; const logs = database.getLogs({ limit: parseInt(limit) || 100, offset: parseInt(offset) || 0, actionType, userId, startDate, endDate }); res.json({ logs }); }); // Статистика логов app.get('/api/logs/stats', authMiddleware, adminMiddleware, (req, res) => { const stats = database.getLogStats(); res.json({ stats }); }); // ============================================================================= // REST API - ЧАТ // ============================================================================= // Получить историю чата комнаты app.get('/api/chat/:roomId', authMiddleware, (req, res) => { const { roomId } = req.params; const { limit } = req.query; const messages = database.getChatHistory(roomId, parseInt(limit) || 50); res.json({ messages }); }); // ============================================================================= // ИГРОВЫЕ СТРУКТУРЫ ДАННЫХ // ============================================================================= /** * Комнаты для игры * @type {Map} */ const rooms = new Map(); /** * Связь WebSocket с игроком * @type {Map} */ const connections = new Map(); // Масти и номиналы карт const SUITS = ['hearts', 'diamonds', 'clubs', 'spades']; const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']; // ============================================================================= // КЛАССЫ // ============================================================================= /** * Класс карты */ class Card { constructor(suit, rank) { this.suit = suit; this.rank = rank; this.value = RANKS.indexOf(rank) + 2; // 2-14 (A=14) } toString() { return `${this.rank}${this.suit[0].toUpperCase()}`; } } /** * Класс колоды */ class Deck { constructor() { this.reset(); } reset() { this.cards = []; for (const suit of SUITS) { for (const rank of RANKS) { this.cards.push(new Card(suit, rank)); } } this.shuffle(); } shuffle() { // Fisher-Yates shuffle for (let i = this.cards.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.cards[i], this.cards[j]] = [this.cards[j], this.cards[i]]; } } deal(count = 1) { return this.cards.splice(0, count); } } /** * Класс игрока */ class Player { constructor(id, name, chips = 1000) { this.id = id; this.name = name; this.chips = chips; this.hand = []; this.bet = 0; this.totalBet = 0; // Общая ставка за раунд this.folded = false; this.allIn = false; this.isDealer = false; this.isSmallBlind = false; this.isBigBlind = false; this.isConnected = true; this.lastAction = null; } reset() { this.hand = []; this.bet = 0; this.totalBet = 0; this.folded = false; this.allIn = false; this.isDealer = false; this.isSmallBlind = false; this.isBigBlind = false; this.lastAction = null; } toPublicJSON() { return { id: this.id, name: this.name, chips: this.chips, bet: this.bet, totalBet: this.totalBet, folded: this.folded, allIn: this.allIn, isDealer: this.isDealer, isSmallBlind: this.isSmallBlind, isBigBlind: this.isBigBlind, isConnected: this.isConnected, lastAction: this.lastAction, hasCards: this.hand.length > 0 }; } } /** * Класс комнаты/стола */ class Room { constructor(id, name, smallBlind = 5, bigBlind = 10, maxPlayers = 6) { this.id = id; this.name = name; this.smallBlind = smallBlind; this.bigBlind = bigBlind; this.maxPlayers = maxPlayers; this.players = []; this.deck = new Deck(); this.communityCards = []; this.pot = 0; this.sidePots = []; this.currentBet = 0; this.dealerIndex = 0; this.currentPlayerIndex = 0; this.gamePhase = 'waiting'; // waiting, preflop, flop, turn, river, showdown this.isGameStarted = false; this.minRaise = bigBlind; this.lastRaiseAmount = bigBlind; this.messages = []; this.roundStarted = false; } /** * Добавить игрока в комнату */ addPlayer(player) { if (this.players.length >= this.maxPlayers) { return false; } this.players.push(player); return true; } /** * Удалить игрока из комнаты */ removePlayer(playerId) { const index = this.players.findIndex(p => p.id === playerId); if (index !== -1) { this.players.splice(index, 1); return true; } return false; } /** * Получить активных игроков (не фолднувших) */ getActivePlayers() { return this.players.filter(p => !p.folded && p.isConnected); } /** * Получить игроков с фишками */ getPlayersWithChips() { return this.players.filter(p => p.chips > 0 && p.isConnected); } /** * Начать новую раздачу */ startNewHand() { if (this.getPlayersWithChips().length < 2) { return false; } // Сброс состояния this.deck.reset(); this.communityCards = []; this.pot = 0; this.sidePots = []; this.currentBet = 0; this.minRaise = this.bigBlind; this.lastRaiseAmount = this.bigBlind; this.roundStarted = true; // Сброс игроков for (const player of this.players) { player.reset(); } // Определение позиций this.moveDealer(); this.assignBlinds(); // Раздача карт this.dealHoleCards(); // Начало игры this.gamePhase = 'preflop'; this.isGameStarted = true; // Ставки блайндов this.postBlinds(); // Первый ход - после большого блайнда (UTG) this.setFirstPlayer(); return true; } /** * Передвинуть дилера */ moveDealer() { const playersWithChips = this.getPlayersWithChips(); if (playersWithChips.length < 2) return; // Находим следующего дилера do { this.dealerIndex = (this.dealerIndex + 1) % this.players.length; } while (this.players[this.dealerIndex].chips <= 0 || !this.players[this.dealerIndex].isConnected); this.players[this.dealerIndex].isDealer = true; } /** * Назначить блайнды */ assignBlinds() { const playersWithChips = this.getPlayersWithChips(); if (playersWithChips.length === 2) { // Heads-up: дилер = SB this.players[this.dealerIndex].isSmallBlind = true; let bbIndex = this.getNextActivePlayerIndex(this.dealerIndex); this.players[bbIndex].isBigBlind = true; } else { // 3+ игроков let sbIndex = this.getNextActivePlayerIndex(this.dealerIndex); this.players[sbIndex].isSmallBlind = true; let bbIndex = this.getNextActivePlayerIndex(sbIndex); this.players[bbIndex].isBigBlind = true; } } /** * Поставить блайнды */ postBlinds() { const sbPlayer = this.players.find(p => p.isSmallBlind); const bbPlayer = this.players.find(p => p.isBigBlind); if (sbPlayer) { const sbAmount = Math.min(sbPlayer.chips, this.smallBlind); sbPlayer.chips -= sbAmount; sbPlayer.bet = sbAmount; sbPlayer.totalBet = sbAmount; this.pot += sbAmount; } if (bbPlayer) { const bbAmount = Math.min(bbPlayer.chips, this.bigBlind); bbPlayer.chips -= bbAmount; bbPlayer.bet = bbAmount; bbPlayer.totalBet = bbAmount; this.pot += bbAmount; this.currentBet = bbAmount; } } /** * Раздать карманные карты */ dealHoleCards() { for (const player of this.getPlayersWithChips()) { player.hand = this.deck.deal(2); } } /** * Установить первого игрока для хода */ setFirstPlayer() { if (this.gamePhase === 'preflop') { // UTG - после BB const bbPlayer = this.players.find(p => p.isBigBlind); const bbIndex = this.players.indexOf(bbPlayer); this.currentPlayerIndex = this.getNextActivePlayerIndex(bbIndex); } else { // После флопа - первый активный после дилера this.currentPlayerIndex = this.getNextActivePlayerIndex(this.dealerIndex); } } /** * Получить индекс следующего активного игрока */ getNextActivePlayerIndex(fromIndex) { let index = (fromIndex + 1) % this.players.length; let attempts = 0; while (attempts < this.players.length) { const player = this.players[index]; if (!player.folded && player.chips >= 0 && player.isConnected && !player.allIn) { return index; } index = (index + 1) % this.players.length; attempts++; } return -1; } /** * Обработать действие игрока */ processAction(playerId, action, amount = 0) { const player = this.players.find(p => p.id === playerId); if (!player) return { success: false, error: 'Игрок не найден' }; const currentPlayer = this.players[this.currentPlayerIndex]; if (currentPlayer.id !== playerId) { return { success: false, error: 'Сейчас не ваш ход' }; } const result = { success: true }; switch (action) { case 'fold': player.folded = true; player.lastAction = 'fold'; break; case 'check': if (player.bet < this.currentBet) { return { success: false, error: 'Нельзя чек, нужно уравнять ставку' }; } player.lastAction = 'check'; break; case 'call': const callAmount = Math.min(this.currentBet - player.bet, player.chips); player.chips -= callAmount; player.bet += callAmount; player.totalBet += callAmount; this.pot += callAmount; player.lastAction = `call ${callAmount}`; if (player.chips === 0) { player.allIn = true; player.lastAction = 'all-in'; } break; case 'bet': if (this.currentBet > 0) { return { success: false, error: 'Уже есть ставка, используйте raise' }; } if (amount < this.bigBlind) { return { success: false, error: `Минимальная ставка: ${this.bigBlind}` }; } if (amount > player.chips) { amount = player.chips; } player.chips -= amount; player.bet = amount; player.totalBet += amount; this.pot += amount; this.currentBet = amount; this.lastRaiseAmount = amount; this.minRaise = amount; player.lastAction = `bet ${amount}`; if (player.chips === 0) { player.allIn = true; player.lastAction = 'all-in'; } break; case 'raise': const toCall = this.currentBet - player.bet; const minRaiseTotal = this.currentBet + this.lastRaiseAmount; if (amount < minRaiseTotal && amount < player.chips + player.bet) { return { success: false, error: `Минимальный рейз до: ${minRaiseTotal}` }; } const raiseTotal = Math.min(amount, player.chips + player.bet); const raiseAmount = raiseTotal - player.bet; player.chips -= raiseAmount; this.pot += raiseAmount; this.lastRaiseAmount = raiseTotal - this.currentBet; this.currentBet = raiseTotal; player.bet = raiseTotal; player.totalBet += raiseAmount; player.lastAction = `raise to ${raiseTotal}`; if (player.chips === 0) { player.allIn = true; player.lastAction = 'all-in'; } break; case 'allin': const allInAmount = player.chips; this.pot += allInAmount; player.bet += allInAmount; player.totalBet += allInAmount; if (player.bet > this.currentBet) { this.lastRaiseAmount = player.bet - this.currentBet; this.currentBet = player.bet; } player.chips = 0; player.allIn = true; player.lastAction = `all-in ${player.bet}`; break; default: return { success: false, error: 'Неизвестное действие' }; } // Проверка завершения раунда ставок if (this.isBettingRoundComplete()) { this.nextPhase(); } else { this.moveToNextPlayer(); } return result; } /** * Перейти к следующему игроку */ moveToNextPlayer() { const nextIndex = this.getNextActivePlayerIndex(this.currentPlayerIndex); if (nextIndex !== -1) { this.currentPlayerIndex = nextIndex; } } /** * Проверить завершение раунда ставок */ isBettingRoundComplete() { const activePlayers = this.getActivePlayers().filter(p => !p.allIn); // Все фолднули кроме одного if (this.getActivePlayers().length === 1) { return true; } // Все активные (не all-in) игроки уравняли ставку if (activePlayers.length === 0) { return true; } // Все поставили одинаково и сделали действие const allEqual = activePlayers.every(p => p.bet === this.currentBet); const allActed = activePlayers.every(p => p.lastAction !== null); return allEqual && allActed; } /** * Переход к следующей фазе */ nextPhase() { // Сброс ставок для новой улицы for (const player of this.players) { player.bet = 0; player.lastAction = null; } this.currentBet = 0; const activePlayers = this.getActivePlayers(); // Один игрок остался - он победитель if (activePlayers.length === 1) { this.endHand([activePlayers[0]]); return; } switch (this.gamePhase) { case 'preflop': this.gamePhase = 'flop'; this.communityCards = this.deck.deal(3); break; case 'flop': this.gamePhase = 'turn'; this.communityCards.push(...this.deck.deal(1)); break; case 'turn': this.gamePhase = 'river'; this.communityCards.push(...this.deck.deal(1)); break; case 'river': this.gamePhase = 'showdown'; this.determineWinner(); return; } // Установка первого игрока для новой улицы this.setFirstPlayer(); // Если все в all-in, автоматически переходим к следующей фазе if (this.getActivePlayers().filter(p => !p.allIn).length <= 1) { setTimeout(() => this.nextPhase(), 1000); } } /** * Определить победителя */ determineWinner() { const activePlayers = this.getActivePlayers(); if (activePlayers.length === 1) { this.endHand([activePlayers[0]]); return; } // Оценка рук const playerHands = activePlayers.map(player => ({ player, handResult: evaluateHand([...player.hand, ...this.communityCards]) })); // Сортировка по силе руки playerHands.sort((a, b) => { if (b.handResult.rank !== a.handResult.rank) { return b.handResult.rank - a.handResult.rank; } // Сравнение кикеров for (let i = 0; i < a.handResult.kickers.length; i++) { if (b.handResult.kickers[i] !== a.handResult.kickers[i]) { return b.handResult.kickers[i] - a.handResult.kickers[i]; } } return 0; }); // Находим победителей (может быть сплит) const winners = [playerHands[0]]; for (let i = 1; i < playerHands.length; i++) { const cmp = this.compareHands(playerHands[0].handResult, playerHands[i].handResult); if (cmp === 0) { winners.push(playerHands[i]); } else { break; } } // Добавляем информацию о руках for (const w of winners) { w.player.handName = w.handResult.name; } this.endHand(winners.map(w => w.player), playerHands); } /** * Сравнение двух рук */ compareHands(h1, h2) { if (h1.rank !== h2.rank) return h2.rank - h1.rank; for (let i = 0; i < h1.kickers.length; i++) { if (h1.kickers[i] !== h2.kickers[i]) { return h2.kickers[i] - h1.kickers[i]; } } return 0; } /** * Завершить раздачу */ endHand(winners, allHands = null) { const winAmount = Math.floor(this.pot / winners.length); for (const winner of winners) { winner.chips += winAmount; } // Остаток при нечётном сплите const remainder = this.pot % winners.length; if (remainder > 0) { winners[0].chips += remainder; } this.gamePhase = 'showdown'; this.isGameStarted = false; this.roundStarted = false; return { winners: winners.map(w => ({ id: w.id, name: w.name, amount: winAmount })), pot: this.pot, hands: allHands }; } /** * Получить состояние комнаты для отправки клиентам */ getState(forPlayerId = null) { return { id: this.id, name: this.name, smallBlind: this.smallBlind, bigBlind: this.bigBlind, pot: this.pot, communityCards: this.communityCards.map(c => ({ suit: c.suit, rank: c.rank })), currentBet: this.currentBet, gamePhase: this.gamePhase, isGameStarted: this.isGameStarted, currentPlayerIndex: this.currentPlayerIndex, currentPlayerId: this.players[this.currentPlayerIndex]?.id, minRaise: this.minRaise + this.currentBet, players: this.players.map(p => { const publicData = p.toPublicJSON(); // Показываем карты только владельцу или при шоудауне if (p.id === forPlayerId || this.gamePhase === 'showdown') { publicData.hand = p.hand.map(c => ({ suit: c.suit, rank: c.rank })); } return publicData; }) }; } } // ============================================================================= // ОЦЕНКА РУК ПОКЕРА // ============================================================================= /** * Оценить покерную руку из 7 карт */ function evaluateHand(cards) { const allCombinations = getCombinations(cards, 5); let bestHand = null; for (const combo of allCombinations) { const hand = evaluateFiveCards(combo); if (!bestHand || compareHandResults(hand, bestHand) > 0) { bestHand = hand; } } return bestHand; } /** * Получить все комбинации из n элементов */ function getCombinations(arr, n) { if (n === 0) return [[]]; if (arr.length === 0) return []; const [first, ...rest] = arr; const withFirst = getCombinations(rest, n - 1).map(c => [first, ...c]); const withoutFirst = getCombinations(rest, n); return [...withFirst, ...withoutFirst]; } /** * Оценить 5 карт */ function evaluateFiveCards(cards) { const sortedCards = [...cards].sort((a, b) => b.value - a.value); const values = sortedCards.map(c => c.value); const suits = sortedCards.map(c => c.suit); const isFlush = suits.every(s => s === suits[0]); const isStraight = checkStraight(values); const isWheel = values.join(',') === '14,5,4,3,2'; // A-2-3-4-5 const valueCounts = {}; for (const v of values) { valueCounts[v] = (valueCounts[v] || 0) + 1; } const counts = Object.values(valueCounts).sort((a, b) => b - a); const uniqueValues = Object.keys(valueCounts) .map(Number) .sort((a, b) => { if (valueCounts[b] !== valueCounts[a]) { return valueCounts[b] - valueCounts[a]; } return b - a; }); // Рояль-флеш if (isFlush && isStraight && values[0] === 14) { return { rank: 10, name: 'Роял-флеш', kickers: values }; } // Стрит-флеш if (isFlush && (isStraight || isWheel)) { return { rank: 9, name: 'Стрит-флеш', kickers: isWheel ? [5, 4, 3, 2, 1] : values }; } // Каре if (counts[0] === 4) { return { rank: 8, name: 'Каре', kickers: uniqueValues }; } // Фулл-хаус if (counts[0] === 3 && counts[1] === 2) { return { rank: 7, name: 'Фулл-хаус', kickers: uniqueValues }; } // Флеш if (isFlush) { return { rank: 6, name: 'Флеш', kickers: values }; } // Стрит if (isStraight || isWheel) { return { rank: 5, name: 'Стрит', kickers: isWheel ? [5, 4, 3, 2, 1] : values }; } // Сет if (counts[0] === 3) { return { rank: 4, name: 'Сет', kickers: uniqueValues }; } // Две пары if (counts[0] === 2 && counts[1] === 2) { return { rank: 3, name: 'Две пары', kickers: uniqueValues }; } // Пара if (counts[0] === 2) { return { rank: 2, name: 'Пара', kickers: uniqueValues }; } // Старшая карта return { rank: 1, name: 'Старшая карта', kickers: values }; } /** * Проверка на стрит */ function checkStraight(values) { for (let i = 0; i < values.length - 1; i++) { if (values[i] - values[i + 1] !== 1) { return false; } } return true; } /** * Сравнение результатов рук */ function compareHandResults(h1, h2) { if (h1.rank !== h2.rank) return h1.rank - h2.rank; for (let i = 0; i < h1.kickers.length; i++) { if (h1.kickers[i] !== h2.kickers[i]) { return h1.kickers[i] - h2.kickers[i]; } } return 0; } // ============================================================================= // WEBSOCKET ОБРАБОТЧИКИ // ============================================================================= wss.on('connection', (ws) => { console.log('Новое подключение'); const connection = { ws, playerId: null, roomId: null, playerName: null }; connections.set(ws, connection); // Отправляем список комнат sendRoomList(ws); ws.on('message', (data) => { try { const message = JSON.parse(data); handleMessage(ws, message); } catch (err) { console.error('Ошибка обработки сообщения:', err); } }); ws.on('close', () => { handleDisconnect(ws); }); }); /** * Обработка входящих сообщений */ function handleMessage(ws, message) { const connection = connections.get(ws); switch (message.type) { case 'create_room': handleCreateRoom(ws, message); break; case 'join_room': handleJoinRoom(ws, message); break; case 'leave_room': handleLeaveRoom(ws); break; case 'start_game': handleStartGame(ws); break; case 'action': handlePlayerAction(ws, message); break; case 'chat': handleChat(ws, message); break; case 'get_rooms': sendRoomList(ws); break; case 'new_hand': handleNewHand(ws); break; } } /** * Создание комнаты */ function handleCreateRoom(ws, message) { const { roomName, playerName, smallBlind, bigBlind, maxPlayers } = message; const connection = connections.get(ws); const roomId = 'room_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const playerId = 'player_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const room = new Room(roomId, roomName || 'Новая комната', smallBlind || 5, bigBlind || 10, maxPlayers || 6); const player = new Player(playerId, playerName || 'Игрок', 1000); room.addPlayer(player); rooms.set(roomId, room); connection.playerId = playerId; connection.roomId = roomId; connection.playerName = player.name; // Логируем создание комнаты database.logAction( connection.userId || playerId, player.name, 'room_created', { roomId, roomName, smallBlind, bigBlind, maxPlayers }, roomId ); // Отправляем подтверждение ws.send(JSON.stringify({ type: 'room_joined', roomId, playerId, room: room.getState(playerId) })); // Обновляем список комнат для всех broadcastRoomList(); } /** * Присоединение к комнате */ function handleJoinRoom(ws, message) { const { roomId, playerName } = message; const connection = connections.get(ws); const room = rooms.get(roomId); if (!room) { ws.send(JSON.stringify({ type: 'error', message: 'Комната не найдена' })); return; } if (room.players.length >= room.maxPlayers) { ws.send(JSON.stringify({ type: 'error', message: 'Комната полна' })); return; } if (room.isGameStarted) { ws.send(JSON.stringify({ type: 'error', message: 'Игра уже началась' })); return; } const playerId = 'player_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const player = new Player(playerId, playerName || 'Игрок', 1000); room.addPlayer(player); connection.playerId = playerId; connection.roomId = roomId; connection.playerName = player.name; // Отправляем подтверждение ws.send(JSON.stringify({ type: 'room_joined', roomId, playerId, room: room.getState(playerId) })); // Уведомляем остальных broadcastToRoom(roomId, { type: 'player_joined', player: player.toPublicJSON(), room: room.getState() }, playerId); broadcastRoomList(); } /** * Выход из комнаты */ function handleLeaveRoom(ws) { const connection = connections.get(ws); if (!connection.roomId) return; const room = rooms.get(connection.roomId); if (room) { room.removePlayer(connection.playerId); if (room.players.length === 0) { rooms.delete(connection.roomId); } else { broadcastToRoom(connection.roomId, { type: 'player_left', playerId: connection.playerId, room: room.getState() }); } } connection.roomId = null; connection.playerId = null; broadcastRoomList(); } /** * Начать игру */ function handleStartGame(ws) { const connection = connections.get(ws); const room = rooms.get(connection.roomId); if (!room) return; if (room.players.length < 2) { ws.send(JSON.stringify({ type: 'error', message: 'Нужно минимум 2 игрока' })); return; } const success = room.startNewHand(); if (success) { // Отправляем состояние каждому игроку (с его картами) for (const player of room.players) { const conn = findConnectionByPlayerId(player.id); if (conn) { conn.ws.send(JSON.stringify({ type: 'game_started', room: room.getState(player.id) })); } } } } /** * Новая раздача */ function handleNewHand(ws) { const connection = connections.get(ws); const room = rooms.get(connection.roomId); if (!room || room.isGameStarted) return; const success = room.startNewHand(); if (success) { for (const player of room.players) { const conn = findConnectionByPlayerId(player.id); if (conn) { conn.ws.send(JSON.stringify({ type: 'game_started', room: room.getState(player.id) })); } } } } /** * Обработка действия игрока */ function handlePlayerAction(ws, message) { const connection = connections.get(ws); const room = rooms.get(connection.roomId); if (!room) return; const result = room.processAction(connection.playerId, message.action, message.amount); if (!result.success) { ws.send(JSON.stringify({ type: 'error', message: result.error })); return; } // Отправляем обновлённое состояние всем for (const player of room.players) { const conn = findConnectionByPlayerId(player.id); if (conn) { conn.ws.send(JSON.stringify({ type: 'game_update', room: room.getState(player.id), lastAction: { playerId: connection.playerId, action: message.action, amount: message.amount } })); } } } /** * Обработка чата */ function handleChat(ws, message) { const connection = connections.get(ws); const room = rooms.get(connection.roomId); if (!room) return; const chatMessage = { type: 'chat', playerId: connection.playerId, playerName: connection.playerName, message: message.message, timestamp: Date.now() }; room.messages.push(chatMessage); // Сохраняем сообщение в БД database.saveChatMessage( connection.roomId, 'multiplayer', connection.userId || connection.playerId, connection.playerName, message.message ); // Логируем действие database.logAction( connection.userId || connection.playerId, connection.playerName, 'chat_message', { message: message.message }, connection.roomId ); broadcastToRoom(connection.roomId, chatMessage); } /** * Обработка отключения */ function handleDisconnect(ws) { const connection = connections.get(ws); if (connection && connection.roomId) { const room = rooms.get(connection.roomId); if (room) { const player = room.players.find(p => p.id === connection.playerId); if (player) { player.isConnected = false; // Если игра идёт и это текущий игрок - автофолд if (room.isGameStarted && room.players[room.currentPlayerIndex]?.id === connection.playerId) { room.processAction(connection.playerId, 'fold'); } broadcastToRoom(connection.roomId, { type: 'player_disconnected', playerId: connection.playerId, room: room.getState() }); } // Удаляем игрока если игра не идёт if (!room.isGameStarted) { room.removePlayer(connection.playerId); if (room.players.length === 0) { rooms.delete(connection.roomId); } } } } connections.delete(ws); broadcastRoomList(); } // ============================================================================= // ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ // ============================================================================= /** * Найти соединение по ID игрока */ function findConnectionByPlayerId(playerId) { for (const [ws, conn] of connections) { if (conn.playerId === playerId) { return conn; } } return null; } /** * Отправить сообщение всем в комнате */ function broadcastToRoom(roomId, message, excludePlayerId = null) { for (const [ws, conn] of connections) { if (conn.roomId === roomId && conn.playerId !== excludePlayerId) { ws.send(JSON.stringify(message)); } } } /** * Отправить список комнат */ function sendRoomList(ws) { const roomList = Array.from(rooms.values()).map(room => ({ id: room.id, name: room.name, players: room.players.length, maxPlayers: room.maxPlayers, smallBlind: room.smallBlind, bigBlind: room.bigBlind, isGameStarted: room.isGameStarted })); ws.send(JSON.stringify({ type: 'room_list', rooms: roomList })); } /** * Обновить список комнат для всех */ function broadcastRoomList() { const roomList = Array.from(rooms.values()).map(room => ({ id: room.id, name: room.name, players: room.players.length, maxPlayers: room.maxPlayers, smallBlind: room.smallBlind, bigBlind: room.bigBlind, isGameStarted: room.isGameStarted })); for (const [ws, conn] of connections) { if (!conn.roomId) { ws.send(JSON.stringify({ type: 'room_list', rooms: roomList })); } } } // ============================================================================= // ЗАПУСК СЕРВЕРА // ============================================================================= const PORT = process.env.PORT || 3000; // Асинхронный запуск с инициализацией БД (async () => { try { // Инициализируем базу данных await database.initDatabase(); server.listen(PORT, () => { console.log(` ╔════════════════════════════════════════════════════════════════╗ ║ 🃏 Texas Hold'em Poker Server 🃏 ║ ║ ║ ║ Сервер запущен на http://localhost:${PORT} ║ ║ WebSocket: ws://localhost:${PORT} ║ ║ База данных: SQLite (poker.db) ║ ╚════════════════════════════════════════════════════════════════╝ `); }); } catch (error) { console.error('Ошибка запуска сервера:', error); process.exit(1); } })();