/** * ============================================================================= * Texas Hold'em - Главный клиентский модуль * UI, WebSocket, звуки, управление игрой * ============================================================================= */ // ============================================================================= // ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ // ============================================================================= let game = null; // Локальная игра (одиночный режим) let ws = null; // WebSocket соединение let currentPlayerId = null; // ID текущего игрока let currentRoomId = null; // ID текущей комнаты let isMultiplayer = false; // Режим игры let settings = {}; // Настройки let leaderboard = []; // Таблица лидеров let soundEnabled = true; // Звук включён let currentGamePhase = 'waiting'; // Текущая фаза игры для мультиплеера let wsConnecting = false; // Флаг подключения WebSocket // Выбранные опции const selectedOptions = { 'bot-count': '1', 'ai-difficulty': '1', 'starting-stack': '1000', 'blinds': '5/10', 'max-players': '6' }; // Звуки const sounds = { deal: null, check: null, call: null, bet: null, fold: null, win: null, chip: null, message: null }; // ============================================================================= // ИНИЦИАЛИЗАЦИЯ // ============================================================================= document.addEventListener('DOMContentLoaded', () => { loadSettings(); loadLeaderboard(); initSounds(); // Восстанавливаем имя игрока const savedName = localStorage.getItem('playerName'); if (savedName) { document.getElementById('sp-player-name').value = savedName; document.getElementById('mp-player-name').value = savedName; } }); /** * Инициализация звуков */ function initSounds() { // Простые звуки через Web Audio API const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const createBeep = (frequency, duration) => { return () => { if (!soundEnabled) return; const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = frequency; oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + duration); }; }; sounds.deal = createBeep(400, 0.1); sounds.check = createBeep(600, 0.1); sounds.call = createBeep(500, 0.15); sounds.bet = createBeep(700, 0.2); sounds.fold = createBeep(300, 0.2); sounds.win = createBeep(800, 0.4); sounds.chip = createBeep(1000, 0.05); sounds.message = createBeep(900, 0.1); } /** * Воспроизвести звук */ function playSound(name) { if (sounds[name] && soundEnabled) { sounds[name](); } } // ============================================================================= // НАВИГАЦИЯ // ============================================================================= /** * Показать экран */ function showScreen(screenId) { document.querySelectorAll('.screen').forEach(screen => { screen.classList.remove('active'); }); document.getElementById(screenId).classList.add('active'); // Особая логика для мультиплеера if (screenId === 'multiplayer-menu') { connectWebSocket(); } // Загрузка лидерборда if (screenId === 'leaderboard-screen') { renderLeaderboard(); } } /** * Переключение вкладок */ function switchTab(tabId) { const parent = event.target.closest('.glass-container'); parent.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); parent.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); event.target.classList.add('active'); document.getElementById(tabId).classList.add('active'); } /** * Выбор опции */ function selectOption(button, optionGroup) { const group = button.parentElement; group.querySelectorAll('.btn-option').forEach(btn => { btn.classList.remove('active'); }); button.classList.add('active'); selectedOptions[optionGroup] = button.dataset.value; } // ============================================================================= // ОДИНОЧНАЯ ИГРА // ============================================================================= /** * Начать одиночную игру */ function startSinglePlayer() { isMultiplayer = false; const playerName = document.getElementById('sp-player-name').value || 'Игрок'; const botCount = parseInt(selectedOptions['bot-count']); const aiDifficulty = parseInt(selectedOptions['ai-difficulty']); const startingStack = parseInt(selectedOptions['starting-stack']); // Сохраняем имя localStorage.setItem('playerName', playerName); // Создаём игру game = new PokerGame({ smallBlind: 5, bigBlind: 10, onUpdate: updateGameUI, onAction: onPlayerAction, onHandEnd: onHandEnd }); // Добавляем игрока const player = new Player('player_0', playerName, startingStack, false); game.addPlayer(player); currentPlayerId = player.id; // Добавляем ботов const personalityKeys = typeof botPersonalities !== 'undefined' ? Object.keys(botPersonalities) : ['professional', 'aggressive', 'mathematical']; for (let i = 0; i < botCount; i++) { const botName = pokerAI.getRandomName(); const bot = new Player(`bot_${i}`, botName, startingStack, true, aiDifficulty); // Присваиваем случайную личность боту bot.personalityId = personalityKeys[i % personalityKeys.length]; game.addPlayer(bot); } // Показываем игровой экран showScreen('game-screen'); // Начинаем раздачу setTimeout(() => { game.startNewHand(); }, 500); } /** * Новая раздача */ function startNewHand() { document.getElementById('new-hand-btn').style.display = 'none'; if (isMultiplayer) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'new_hand' })); } } else { if (game && game.getPlayersWithChips().length >= 2) { game.startNewHand(); } else { showNotification('Недостаточно игроков с фишками', 'error'); } } } // ============================================================================= // МУЛЬТИПЛЕЕР // ============================================================================= /** * Подключение к WebSocket серверу */ function connectWebSocket() { const serverUrl = settings.serverUrl || 'ws://localhost:3000'; // Если уже подключены или подключаемся if ((ws && ws.readyState === WebSocket.OPEN) || wsConnecting) { return Promise.resolve(); } // Если соединение в процессе закрытия, ждём if (ws && ws.readyState === WebSocket.CONNECTING) { return new Promise((resolve) => { ws.addEventListener('open', resolve, { once: true }); }); } wsConnecting = true; return new Promise((resolve, reject) => { try { ws = new WebSocket(serverUrl); ws.onopen = () => { console.log('Подключено к серверу'); wsConnecting = false; document.getElementById('room-list').innerHTML = '
Загрузка комнат...
'; resolve(); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); handleServerMessage(message); }; ws.onclose = () => { console.log('Отключено от сервера'); wsConnecting = false; ws = null; document.getElementById('room-list').innerHTML = '
Соединение потеряно.
'; }; ws.onerror = (error) => { console.error('WebSocket ошибка:', error); wsConnecting = false; document.getElementById('room-list').innerHTML = '
Ошибка подключения к серверу.
'; reject(error); }; } catch (error) { console.error('Ошибка создания WebSocket:', error); wsConnecting = false; reject(error); } }); } /** * Обработка сообщений от сервера */ function handleServerMessage(message) { switch (message.type) { case 'room_list': renderRoomList(message.rooms); break; case 'room_joined': currentPlayerId = message.playerId; currentRoomId = message.roomId; showRoomLobby(message.room); break; case 'player_joined': updateLobbyPlayers(message.room); addChatMessage('lobby', null, `${message.player.name} присоединился`, true); break; case 'player_left': case 'player_disconnected': updateLobbyPlayers(message.room); break; case 'game_started': isMultiplayer = true; currentGamePhase = message.room.gamePhase; showScreen('game-screen'); updateGameUIFromServer(message.room); break; case 'game_update': currentGamePhase = message.room.gamePhase; updateGameUIFromServer(message.room); if (message.lastAction) { playSound(message.lastAction.action); } break; case 'chat': if (document.getElementById('game-screen').classList.contains('active')) { addChatMessage('game', message.playerName, message.message); } else { addChatMessage('lobby', message.playerName, message.message); } playSound('message'); break; case 'error': showNotification(message.message, 'error'); break; } } /** * Отрисовка списка комнат */ function renderRoomList(rooms) { const container = document.getElementById('room-list'); if (rooms.length === 0) { container.innerHTML = '
Нет доступных комнат
'; return; } container.innerHTML = rooms.map(room => `

${room.name}

Блайнды: ${room.smallBlind}/${room.bigBlind}
${room.players}/${room.maxPlayers}
`).join(''); } /** * Обновить список комнат */ function refreshRooms() { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'get_rooms' })); } else { connectWebSocket(); } } /** * Создать комнату */ async function createRoom() { // Проверяем и устанавливаем соединение if (!ws || ws.readyState !== WebSocket.OPEN) { try { await connectWebSocket(); } catch (e) { showNotification('Не удалось подключиться к серверу', 'error'); return; } } // Повторная проверка после await if (!ws || ws.readyState !== WebSocket.OPEN) { showNotification('Нет соединения с сервером', 'error'); return; } const playerName = document.getElementById('mp-player-name').value || 'Игрок'; const roomName = document.getElementById('room-name').value || `Комната ${playerName}`; const [smallBlind, bigBlind] = selectedOptions['blinds'].split('/').map(Number); const maxPlayers = parseInt(selectedOptions['max-players']); localStorage.setItem('playerName', playerName); ws.send(JSON.stringify({ type: 'create_room', roomName, playerName, smallBlind, bigBlind, maxPlayers })); } /** * Присоединиться к комнате */ async function joinRoom(roomId) { if (!ws || ws.readyState !== WebSocket.OPEN) { try { await connectWebSocket(); } catch (e) { showNotification('Не удалось подключиться к серверу', 'error'); return; } } if (!ws || ws.readyState !== WebSocket.OPEN) { showNotification('Нет соединения с сервером', 'error'); return; } const playerName = document.getElementById('mp-player-name').value || 'Игрок'; localStorage.setItem('playerName', playerName); ws.send(JSON.stringify({ type: 'join_room', roomId, playerName })); } /** * Показать лобби комнаты */ function showRoomLobby(room) { showScreen('room-lobby'); document.getElementById('lobby-room-name').textContent = room.name; document.getElementById('lobby-blinds').textContent = `${room.smallBlind}/${room.bigBlind}`; updateLobbyPlayers(room); } /** * Обновить игроков в лобби */ function updateLobbyPlayers(room) { const container = document.getElementById('lobby-players'); container.innerHTML = room.players.map((player, index) => `
${player.name.charAt(0).toUpperCase()}
${player.name}
`).join(''); document.getElementById('lobby-player-count').textContent = `${room.players.length}/${room.maxPlayers || 6}`; const startBtn = document.getElementById('start-game-btn'); startBtn.style.display = room.players[0]?.id === currentPlayerId ? 'block' : 'none'; startBtn.disabled = room.players.length < 2; } /** * Выйти из комнаты */ function leaveRoom() { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'leave_room' })); } currentRoomId = null; showScreen('multiplayer-menu'); } /** * Начать мультиплеерную игру */ function startMultiplayerGame() { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'start_game' })); } } // ============================================================================= // ИГРОВОЙ UI // ============================================================================= /** * Обновить UI игры (локальный режим) */ function updateGameUI() { if (!game) return; document.getElementById('pot-amount').textContent = game.pot; const phaseNames = { 'waiting': 'Ожидание', 'preflop': 'Префлоп', 'flop': 'Флоп', 'turn': 'Тёрн', 'river': 'Ривер', 'showdown': 'Вскрытие' }; document.getElementById('game-phase').textContent = phaseNames[game.gamePhase] || game.gamePhase; renderCommunityCards(game.communityCards); renderPlayers(game.players, game.currentPlayerIndex, currentPlayerId); const player = game.players.find(p => p.id === currentPlayerId); if (player) { renderPlayerHand(player.hand, game.communityCards); } updateActionPanel(player, game); } /** * Обновить UI из данных сервера */ function updateGameUIFromServer(room) { document.getElementById('pot-amount').textContent = room.pot; const phaseNames = { 'waiting': 'Ожидание', 'preflop': 'Префлоп', 'flop': 'Флоп', 'turn': 'Тёрн', 'river': 'Ривер', 'showdown': 'Вскрытие' }; document.getElementById('game-phase').textContent = phaseNames[room.gamePhase] || room.gamePhase; // Конвертируем данные сервера в объекты Card const communityCards = room.communityCards.map(c => new Card(c.suit, c.rank)); renderCommunityCards(communityCards); // Конвертируем игроков const players = room.players.map((p, i) => { const player = { ...p, hand: p.hand ? p.hand.map(c => new Card(c.suit, c.rank)) : [] }; return player; }); renderPlayers(players, room.currentPlayerIndex, currentPlayerId); const myPlayer = players.find(p => p.id === currentPlayerId); if (myPlayer) { renderPlayerHand(myPlayer.hand, communityCards); updateActionPanelFromServer(myPlayer, room); } // Показываем кнопку новой раздачи если игра завершена if (room.gamePhase === 'showdown' || !room.isGameStarted) { setTimeout(() => { document.getElementById('new-hand-btn').style.display = 'block'; }, 2000); } } /** * Отрисовка общих карт */ function renderCommunityCards(cards) { const container = document.getElementById('community-cards'); container.innerHTML = ''; for (let i = 0; i < 5; i++) { if (cards[i]) { const cardEl = cards[i].toHTML ? cards[i].toHTML() : createCardElement(cards[i]); cardEl.classList.add('dealing'); cardEl.style.animationDelay = `${i * 0.1}s`; container.appendChild(cardEl); } else { const placeholder = document.createElement('div'); placeholder.className = 'card card-placeholder'; placeholder.style.opacity = '0.2'; container.appendChild(placeholder); } } playSound('deal'); } /** * Создать элемент карты из данных */ function createCardElement(cardData, isSmall = false) { const card = document.createElement('div'); card.className = `card ${cardData.suit}${isSmall ? ' card-small' : ''}`; const symbols = { hearts: '♥', diamonds: '♦', clubs: '♣', spades: '♠' }; card.innerHTML = ` ${cardData.rank} ${symbols[cardData.suit]} `; return card; } /** * Отрисовка игроков за столом */ function renderPlayers(players, currentIndex, myPlayerId) { const container = document.getElementById('player-positions'); container.innerHTML = ''; // Позиции для ставок (относительно позиции игрока) const betPositions = [ { top: '-40px', left: '50%' }, { top: '-30px', left: '80%' }, { top: '50%', left: '100%' }, { top: '100%', left: '50%' }, { top: '50%', left: '-30%' }, { top: '-30px', left: '20%' } ]; players.forEach((player, index) => { // Пропускаем текущего игрока (он отображается снизу) if (player.id === myPlayerId) return; const seat = document.createElement('div'); seat.className = 'player-seat'; seat.dataset.position = index; const isCurrentTurn = index === currentIndex; const playerBox = document.createElement('div'); playerBox.className = `player-box ${player.folded ? 'folded' : ''} ${isCurrentTurn ? 'current-turn' : ''}`; playerBox.innerHTML = `
${player.name}
${player.chips}
${player.lastAction ? `
${player.lastAction}
` : ''} `; // Определяем, нужно ли показывать карты // Показываем карты только на showdown и только не фолднувшим игрокам const gamePhase = isMultiplayer ? currentGamePhase : (game ? game.gamePhase : 'waiting'); const showCards = gamePhase === 'showdown' && !player.folded; // Карты игрока (мини) if (showCards && player.hand && player.hand.length > 0) { const cardsDiv = document.createElement('div'); cardsDiv.className = 'player-cards-mini'; player.hand.forEach(card => { const cardEl = card.toHTML ? card.toHTML(true) : createCardElement(card, true); cardsDiv.appendChild(cardEl); }); playerBox.appendChild(cardsDiv); } else if ((player.hasCards || (player.hand && player.hand.length > 0)) && !player.folded) { // Карты рубашкой (не показываем карты соперников до showdown) const cardsDiv = document.createElement('div'); cardsDiv.className = 'player-cards-mini'; cardsDiv.innerHTML = `
`; playerBox.appendChild(cardsDiv); } seat.appendChild(playerBox); // Позиционные маркеры if (player.isDealer) { const dealerBtn = document.createElement('div'); dealerBtn.className = 'dealer-button'; dealerBtn.textContent = 'D'; dealerBtn.style.cssText = 'bottom: -35px; left: 50%; transform: translateX(-50%);'; seat.appendChild(dealerBtn); } if (player.isSmallBlind || player.isBigBlind) { const blindIndicator = document.createElement('div'); blindIndicator.className = `blind-indicator ${player.isSmallBlind ? 'sb' : 'bb'}`; blindIndicator.textContent = player.isSmallBlind ? 'SB' : 'BB'; blindIndicator.style.cssText = 'bottom: -35px; right: 0;'; seat.appendChild(blindIndicator); } // Ставка игрока if (player.bet > 0) { const betDisplay = document.createElement('div'); betDisplay.className = 'player-bet-display'; betDisplay.textContent = player.bet; betDisplay.style.cssText = `${betPositions[index % 6].top}; left: ${betPositions[index % 6].left};`; seat.appendChild(betDisplay); } container.appendChild(seat); }); } /** * Отрисовка руки игрока */ function renderPlayerHand(hand, communityCards) { const container = document.getElementById('player-cards'); container.innerHTML = ''; if (!hand || hand.length === 0) { container.innerHTML = `
`; return; } hand.forEach((card, i) => { const cardEl = card.toHTML ? card.toHTML() : createCardElement(card); cardEl.classList.add('dealing'); cardEl.style.animationDelay = `${i * 0.15}s`; container.appendChild(cardEl); }); // Показываем силу руки if (settings.showHandStrength !== false) { const strength = getHandStrength(hand, communityCards || []); document.getElementById('hand-strength').textContent = strength || ''; } } /** * Обновить панель действий */ function updateActionPanel(player, gameState) { if (!player || !gameState || !gameState.isGameStarted) { document.getElementById('action-panel').style.display = 'none'; return; } const isMyTurn = gameState.getCurrentPlayer()?.id === player.id; document.getElementById('action-panel').style.display = isMyTurn ? 'block' : 'none'; if (!isMyTurn) return; const toCall = gameState.currentBet - player.bet; const canCheck = toCall === 0; const canBet = gameState.currentBet === 0; document.getElementById('btn-check').style.display = canCheck ? 'inline-flex' : 'none'; document.getElementById('btn-call').style.display = !canCheck ? 'inline-flex' : 'none'; document.getElementById('btn-bet').style.display = canBet ? 'inline-flex' : 'none'; document.getElementById('btn-raise').style.display = !canBet ? 'inline-flex' : 'none'; document.getElementById('call-amount').textContent = toCall; // Настройка слайдера const slider = document.getElementById('bet-slider'); const minBet = canBet ? gameState.bigBlind : gameState.currentBet + gameState.lastRaiseAmount; slider.min = minBet; slider.max = player.chips + player.bet; slider.value = minBet; document.getElementById('bet-value').value = minBet; } /** * Обновить панель действий из данных сервера */ function updateActionPanelFromServer(player, room) { if (!player || room.gamePhase === 'showdown' || !room.isGameStarted) { document.getElementById('action-panel').style.display = 'none'; return; } const isMyTurn = room.currentPlayerId === player.id; document.getElementById('action-panel').style.display = isMyTurn ? 'block' : 'none'; if (!isMyTurn) return; const toCall = room.currentBet - player.bet; const canCheck = toCall === 0; const canBet = room.currentBet === 0; document.getElementById('btn-check').style.display = canCheck ? 'inline-flex' : 'none'; document.getElementById('btn-call').style.display = !canCheck ? 'inline-flex' : 'none'; document.getElementById('btn-bet').style.display = canBet ? 'inline-flex' : 'none'; document.getElementById('btn-raise').style.display = !canBet ? 'inline-flex' : 'none'; document.getElementById('call-amount').textContent = toCall; const slider = document.getElementById('bet-slider'); const bigBlind = room.bigBlind || 10; const minBet = canBet ? bigBlind : room.minRaise || room.currentBet * 2; slider.min = minBet; slider.max = player.chips + player.bet; slider.value = minBet; document.getElementById('bet-value').value = minBet; } // ============================================================================= // ДЕЙСТВИЯ ИГРОКА // ============================================================================= /** * Выполнить действие */ function playerAction(action) { playSound(action); if (isMultiplayer) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'action', action: action, amount: 0 })); } } else { if (game) { game.processAction(currentPlayerId, action, 0); } } hideBetSlider(); } /** * Показать слайдер ставки */ function showBetSlider() { document.getElementById('bet-slider-container').style.display = 'block'; } /** * Скрыть слайдер ставки */ function hideBetSlider() { document.getElementById('bet-slider-container').style.display = 'none'; } /** * Обновить значение ставки из слайдера */ function updateBetValue() { const slider = document.getElementById('bet-slider'); document.getElementById('bet-value').value = slider.value; } /** * Обновить слайдер из инпута */ function updateSliderFromInput() { const input = document.getElementById('bet-value'); const slider = document.getElementById('bet-slider'); let value = parseInt(input.value); value = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), value)); slider.value = value; input.value = value; } /** * Установить пресет ставки */ function setBetPreset(multiplier) { let pot; if (isMultiplayer) { pot = parseInt(document.getElementById('pot-amount').textContent) || 0; } else { pot = game ? game.pot : 0; } const betAmount = Math.floor(pot * multiplier); const slider = document.getElementById('bet-slider'); const value = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), betAmount)); slider.value = value; document.getElementById('bet-value').value = value; } /** * Подтвердить ставку */ function confirmBet() { const amount = parseInt(document.getElementById('bet-value').value); const action = document.getElementById('btn-bet').style.display !== 'none' ? 'bet' : 'raise'; playSound('bet'); if (isMultiplayer) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'action', action: action, amount: amount })); } } else { if (game) { game.processAction(currentPlayerId, action, amount); } } hideBetSlider(); } // ============================================================================= // СОБЫТИЯ ИГРЫ // ============================================================================= /** * Событие действия игрока */ function onPlayerAction(player, action, amount) { // Можно добавить анимации или уведомления console.log(`${player.name}: ${action} ${amount || ''}`); } /** * Событие завершения раздачи */ function onHandEnd(result) { playSound('win'); // Показываем результат const modal = document.getElementById('hand-result-modal'); const title = document.getElementById('result-title'); const details = document.getElementById('result-details'); const winner = result.winners[0]; title.textContent = result.winners.length > 1 ? 'Сплит!' : `${winner.name} победил!`; let detailsHTML = `
+${winner.amount}
`; if (winner.hand) { detailsHTML += `
${winner.hand.name}
`; } if (result.hands && result.hands.length > 1) { detailsHTML += '
'; result.hands.forEach(h => { detailsHTML += `
${h.player.name}: ${h.hand.name}
`; }); detailsHTML += '
'; } details.innerHTML = detailsHTML; modal.classList.add('active'); // Обновляем лидерборд updateLeaderboard(result); // Показываем кнопку новой раздачи setTimeout(() => { document.getElementById('new-hand-btn').style.display = 'block'; }, 1000); } /** * Закрыть модальное окно результата */ function closeResultModal() { document.getElementById('hand-result-modal').classList.remove('active'); } // ============================================================================= // ЧАТ // ============================================================================= /** * Добавить сообщение в чат */ function addChatMessage(chatType, sender, message, isSystem = false, isTyping = false) { const containerId = chatType === 'game' ? 'game-chat-messages' : 'lobby-chat-messages'; const container = document.getElementById(containerId); const msgDiv = document.createElement('div'); msgDiv.className = `chat-message ${isSystem ? 'system' : ''} ${isTyping ? 'typing-indicator' : ''}`; if (isTyping) { msgDiv.dataset.sender = sender; } if (isSystem) { msgDiv.textContent = message; } else { const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); if (isTyping) { msgDiv.innerHTML = ` ${sender}: печатает... ${time} `; } else { msgDiv.innerHTML = ` ${sender}: ${message} ${time} `; } } container.appendChild(msgDiv); container.scrollTop = container.scrollHeight; } /** * Удалить индикатор "печатает..." для конкретного отправителя */ function removeTypingIndicator(sender) { const container = document.getElementById('game-chat-messages'); const typingMsg = container.querySelector(`.typing-indicator[data-sender="${sender}"]`); if (typingMsg) { typingMsg.remove(); } } /** * Отправить сообщение в чат лобби */ function sendLobbyChat() { const input = document.getElementById('lobby-chat-input'); const message = input.value.trim(); if (!message) return; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'chat', message: message })); } input.value = ''; } /** * Отправить сообщение в игровой чат */ function sendGameChat() { const input = document.getElementById('game-chat-input'); const message = input.value.trim(); if (!message) return; if (isMultiplayer && ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'chat', message: message })); } else { // Одиночная игра - ИИ отвечает const playerName = localStorage.getItem('playerName') || 'Игрок'; addChatMessage('game', playerName, message); // Получаем всех ботов для ответа const bots = game.players.filter(p => p.isAI && !p.folded); if (bots.length > 0) { // Выбираем случайного бота для ответа const bot = bots[Math.floor(Math.random() * bots.length)]; // Формируем контекст игры для LLM const gameContext = { phase: currentGamePhase, pot: game.pot, myChips: bot.chips, lastAction: message, communityCards: game.communityCards?.map(c => `${c.rank}${c.suit}`) || [] }; // Получаем личность бота const botPersonality = typeof botPersonalities !== 'undefined' ? botPersonalities[bot.personalityId] || botPersonalities.professional : { style: 'default' }; // Показываем индикатор "печатает..." addChatMessage('game', bot.name, '...', false, true); // Вызываем LLM чат (async () => { try { let response; if (typeof llmChat !== 'undefined' && llmChat.getSettings().llmEnabled) { response = await llmChat.chat(bot.id, botPersonality, message, gameContext); } else { // Запасные ответы если LLM отключён await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 1500)); response = llmChat?.getFallbackResponse(botPersonality, message, gameContext) || 'Удачи!'; } // Удаляем индикатор "печатает..." removeTypingIndicator(bot.name); // Добавляем ответ addChatMessage('game', bot.name, response); } catch (error) { console.error('Ошибка LLM чата:', error); removeTypingIndicator(bot.name); addChatMessage('game', bot.name, 'Удачи!'); } })(); } } input.value = ''; } /** * Обработка Enter в чате лобби */ function handleLobbyChatKey(event) { if (event.key === 'Enter') { sendLobbyChat(); } } /** * Обработка Enter в игровом чате */ function handleGameChatKey(event) { if (event.key === 'Enter') { sendGameChat(); } } /** * Переключить чат */ function toggleChat() { document.getElementById('game-chat').classList.toggle('expanded'); } // ============================================================================= // НАСТРОЙКИ // ============================================================================= /** * Загрузить настройки */ function loadSettings() { const saved = localStorage.getItem('pokerSettings'); if (saved) { settings = JSON.parse(saved); } else { settings = { sound: true, animations: true, showHandStrength: true, autofold: true, serverUrl: 'ws://localhost:3000', llmEnabled: false, llmProvider: 'ollama', llmApiUrl: 'http://localhost:11434', llmModel: 'llama3.2', llmApiKey: '' }; } // Применяем настройки к UI document.getElementById('setting-sound').checked = settings.sound !== false; document.getElementById('setting-animations').checked = settings.animations !== false; document.getElementById('setting-hand-strength').checked = settings.showHandStrength !== false; document.getElementById('setting-autofold').checked = settings.autofold !== false; document.getElementById('server-url').value = settings.serverUrl || 'ws://localhost:3000'; // LLM настройки const llmEnabled = document.getElementById('setting-llm-enabled'); const llmProvider = document.getElementById('llm-provider'); const llmApiUrl = document.getElementById('llm-api-url'); const llmModel = document.getElementById('llm-model'); const llmApiKey = document.getElementById('llm-api-key'); if (llmEnabled) llmEnabled.checked = settings.llmEnabled || false; if (llmProvider) llmProvider.value = settings.llmProvider || 'ollama'; if (llmApiUrl) llmApiUrl.value = settings.llmApiUrl || 'http://localhost:11434'; if (llmModel) llmModel.value = settings.llmModel || 'llama3.2'; if (llmApiKey) llmApiKey.value = settings.llmApiKey || ''; // Обновляем видимость API ключа updateLLMProviderUI(); soundEnabled = settings.sound !== false; } /** * Обновить UI для LLM провайдера */ function updateLLMProviderUI() { const provider = document.getElementById('llm-provider')?.value || 'ollama'; const apiKeyGroup = document.getElementById('llm-api-key-group'); const apiUrlLabel = document.getElementById('llm-api-url-label'); const apiUrl = document.getElementById('llm-api-url'); if (apiKeyGroup) { apiKeyGroup.style.display = (provider === 'openai') ? 'block' : 'none'; } if (apiUrlLabel && apiUrl) { switch (provider) { case 'ollama': apiUrl.placeholder = 'http://localhost:11434'; if (!apiUrl.value || apiUrl.value.includes('localhost:1234')) { apiUrl.value = 'http://localhost:11434'; } break; case 'lmstudio': apiUrl.placeholder = 'http://localhost:1234'; if (!apiUrl.value || apiUrl.value.includes('localhost:11434')) { apiUrl.value = 'http://localhost:1234'; } break; case 'openai': apiUrl.placeholder = 'https://api.openai.com'; apiUrl.value = 'https://api.openai.com'; break; } } } /** * Обновить настройки */ function updateSettings() { settings = { sound: document.getElementById('setting-sound').checked, animations: document.getElementById('setting-animations').checked, showHandStrength: document.getElementById('setting-hand-strength').checked, autofold: document.getElementById('setting-autofold').checked, serverUrl: document.getElementById('server-url').value, llmEnabled: document.getElementById('setting-llm-enabled')?.checked || false, llmProvider: document.getElementById('llm-provider')?.value || 'ollama', llmApiUrl: document.getElementById('llm-api-url')?.value || 'http://localhost:11434', llmModel: document.getElementById('llm-model')?.value || 'llama3.2', llmApiKey: document.getElementById('llm-api-key')?.value || '' }; localStorage.setItem('pokerSettings', JSON.stringify(settings)); soundEnabled = settings.sound; } /** * Тестировать подключение к LLM */ async function testLLMConnection() { const testBtn = document.getElementById('test-llm-btn'); const originalText = testBtn?.textContent; if (testBtn) { testBtn.textContent = 'Тестирую...'; testBtn.disabled = true; } // Сохраняем настройки перед тестом updateSettings(); try { if (typeof llmChat !== 'undefined') { const result = await llmChat.testConnection(); if (result.success) { showNotification('LLM подключён успешно! ' + (result.response?.substring(0, 50) || ''), 'success'); } else { showNotification('Ошибка: ' + result.error, 'error'); } } else { showNotification('LLM модуль не загружен', 'error'); } } catch (error) { showNotification('Ошибка: ' + error.message, 'error'); } if (testBtn) { testBtn.textContent = originalText; testBtn.disabled = false; } } /** * Выбрать LLM провайдера */ function selectLLMProvider(btn) { // Убираем active со всех кнопок const buttons = btn.parentElement.querySelectorAll('.btn-option'); buttons.forEach(b => b.classList.remove('active')); // Добавляем active на выбранную btn.classList.add('active'); // Устанавливаем значение в скрытый input const value = btn.dataset.value; document.getElementById('llm-provider').value = value; // Обновляем UI updateLLMProviderUI(); // Сохраняем настройки updateSettings(); } /** * Сбросить настройки */ function resetSettings() { localStorage.removeItem('pokerSettings'); loadSettings(); showNotification('Настройки сброшены', 'success'); } /** * Переключить звук */ function toggleSound() { soundEnabled = !soundEnabled; settings.sound = soundEnabled; localStorage.setItem('pokerSettings', JSON.stringify(settings)); const btn = document.getElementById('sound-toggle'); btn.textContent = soundEnabled ? '🔊' : '🔇'; showNotification(soundEnabled ? 'Звук включён' : 'Звук выключён', 'info'); } // ============================================================================= // ЛИДЕРБОРД // ============================================================================= /** * Загрузить лидерборд */ function loadLeaderboard() { const saved = localStorage.getItem('pokerLeaderboard'); if (saved) { leaderboard = JSON.parse(saved); } else { leaderboard = []; } } /** * Обновить лидерборд */ function updateLeaderboard(result) { const playerName = localStorage.getItem('playerName') || 'Игрок'; // Находим или создаём запись let entry = leaderboard.find(e => e.name === playerName); if (!entry) { entry = { name: playerName, gamesPlayed: 0, handsWon: 0, totalWinnings: 0, biggestPot: 0 }; leaderboard.push(entry); } entry.gamesPlayed++; // Проверяем, выиграл ли текущий игрок const won = result.winners.some(w => w.id === currentPlayerId); if (won) { entry.handsWon++; const winAmount = result.winners.find(w => w.id === currentPlayerId)?.amount || 0; entry.totalWinnings += winAmount; if (result.pot > entry.biggestPot) { entry.biggestPot = result.pot; } } // Сортируем и сохраняем leaderboard.sort((a, b) => b.totalWinnings - a.totalWinnings); localStorage.setItem('pokerLeaderboard', JSON.stringify(leaderboard)); } /** * Отрисовка лидерборда */ function renderLeaderboard() { const container = document.getElementById('leaderboard-list'); if (leaderboard.length === 0) { container.innerHTML = '
Нет записей
'; return; } container.innerHTML = leaderboard.slice(0, 10).map((entry, index) => { const rankClass = index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? 'bronze' : ''; return `
${index + 1}
${entry.name}
Игр: ${entry.gamesPlayed} | Побед: ${entry.handsWon} | Макс. банк: ${entry.biggestPot}
${entry.totalWinnings}
`; }).join(''); } /** * Переключить вкладку лидерборда */ function switchLeaderboardTab(tab) { const parent = document.querySelector('.leaderboard-tabs'); parent.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); event.target.classList.add('active'); if (tab === 'global') { document.getElementById('leaderboard-list').innerHTML = '
Глобальный лидерборд недоступен (требуется backend)
'; } else { renderLeaderboard(); } } /** * Очистить лидерборд */ function clearLeaderboard() { if (confirm('Вы уверены, что хотите очистить таблицу лидеров?')) { leaderboard = []; localStorage.removeItem('pokerLeaderboard'); renderLeaderboard(); showNotification('Таблица лидеров очищена', 'success'); } } // ============================================================================= // УТИЛИТЫ // ============================================================================= /** * Выйти из игры */ function leaveGame() { if (confirm('Вы уверены, что хотите выйти?')) { if (isMultiplayer) { leaveRoom(); } game = null; showScreen('main-menu'); } } /** * Показать уведомление */ function showNotification(message, type = 'info') { const container = document.getElementById('notifications'); const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; container.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); }