/**
* =============================================================================
* 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
let botPersonalities = null; // Персональности ботов (загружается из pokerAI)
// Выбранные опции
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();
loadCardBackSettings();
// Загружаем персональности ботов
if (typeof pokerAI !== 'undefined' && pokerAI.personalities) {
botPersonalities = {};
pokerAI.personalities.forEach(p => {
botPersonalities[p.style] = p;
});
}
// Восстанавливаем имя игрока
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 personalities = typeof pokerAI !== 'undefined' && pokerAI.personalities
? pokerAI.personalities
: [];
// Перемешиваем персональности для разнообразия
const shuffledPersonalities = personalities.length > 0
? [...personalities].sort(() => Math.random() - 0.5)
: [];
for (let i = 0; i < botCount; i++) {
let botName, personalityId, personality;
if (shuffledPersonalities.length > 0) {
// Используем персональность из списка
personality = shuffledPersonalities[i % shuffledPersonalities.length];
botName = personality.name;
personalityId = personality.style;
} else {
// Запасной вариант
botName = pokerAI.getRandomName();
personalityId = 'professional';
}
const bot = new Player(`bot_${i}`, botName, startingStack, true, aiDifficulty);
bot.personalityId = personalityId;
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);
});
// Применяем стиль рубашки к новым картам
applyCardBackToNewCards();
}
/**
* Применить стиль рубашки к новым картам на столе
*/
function applyCardBackToNewCards() {
const style = settings.cardBackStyle || 'default';
if (style === 'custom') {
const url = localStorage.getItem('customCardBack');
if (url) {
document.querySelectorAll('.card-back:not(.styled)').forEach(card => {
card.classList.add('style-custom', 'styled');
card.style.backgroundImage = `url(${url})`;
card.style.backgroundSize = 'cover';
card.style.backgroundPosition = 'center';
});
}
} else {
document.querySelectorAll('.card-back:not(.styled)').forEach(card => {
card.classList.add(`style-${style}`, 'styled');
});
}
}
/**
* Отрисовка руки игрока
*/
function renderPlayerHand(hand, communityCards) {
const container = document.getElementById('player-cards');
container.innerHTML = '';
if (!hand || hand.length === 0) {
container.innerHTML = `
`;
applyCardBackToNewCards();
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);
// НОВОЕ: Генерируем эмоциональные реакции ботов
if (!isMultiplayer && game) {
generateBotEmotions(result);
}
// Показываем кнопку новой раздачи
setTimeout(() => {
document.getElementById('new-hand-btn').style.display = 'block';
}, 1000);
}
/**
* Генерировать эмоциональные реакции ботов после раздачи
*/
async function generateBotEmotions(result) {
if (!game || !result) return;
const winnerIds = result.winners.map(w => w.id);
const potSize = result.pot || game.pot || 0;
// Определяем, был ли олл-ин
const wasAllIn = game.players.some(p => p.chips === 0 && !p.folded);
// Случайно решаем, будет ли бот реагировать (30-50% вероятность)
const shouldReact = Math.random() < (wasAllIn ? 0.5 : 0.3);
if (!shouldReact) return;
// Выбираем случайного бота для реакции
const activeBots = game.players.filter(p => p.isAI && !p.folded);
if (activeBots.length === 0) return;
const bot = activeBots[Math.floor(Math.random() * activeBots.length)];
const isWinner = winnerIds.includes(bot.id);
// Получаем личность бота
let botPersonality;
if (typeof botPersonalities !== 'undefined' && bot.personalityId) {
botPersonality = botPersonalities[bot.personalityId];
}
if (!botPersonality) {
botPersonality = { style: 'professional' };
}
// Задержка перед реакцией (500-1500ms)
setTimeout(async () => {
try {
let reaction;
if (typeof llmChat !== 'undefined' && llmChat.generateEmotionalReaction) {
reaction = await llmChat.generateEmotionalReaction(
bot,
botPersonality,
isWinner,
potSize,
wasAllIn,
{ phase: currentGamePhase }
);
} else {
// Запасной вариант
const emotions = isWinner
? ['Отлично!', 'Неплохо!', 'GG', 'Да!']
: ['Эх...', 'Невезение', 'Бывает', 'Хм'];
reaction = emotions[Math.floor(Math.random() * emotions.length)];
}
if (reaction) {
addChatMessage('game', bot.name, reaction);
}
} catch (error) {
console.error('Ошибка генерации эмоции:', error);
}
}, 500 + Math.random() * 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) {
// Проверяем, обращается ли игрок к конкретному боту
let targetBot = null;
if (typeof llmChat !== 'undefined' && llmChat.detectBotMention) {
targetBot = llmChat.detectBotMention(message, bots);
}
// Если обращение не найдено, выбираем случайного бота
const bot = targetBot || 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}`) || [],
mentionedByName: targetBot !== null // Указываем, что игрок обратился по имени
};
// Получаем личность бота
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');
localStorage.removeItem('customCardBack');
loadSettings();
applyCardBackStyle('default');
showNotification('Настройки сброшены', 'success');
}
// =============================================================================
// РУБАШКА КАРТ
// =============================================================================
/**
* Выбрать стиль рубашки карт
*/
function selectCardBack(btn) {
// Убираем active со всех кнопок
const buttons = btn.parentElement.querySelectorAll('.btn-option');
buttons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const style = btn.dataset.style;
// Показываем/скрываем поля для кастомной рубашки
const customGroup = document.getElementById('custom-card-back-group');
const urlGroup = document.getElementById('card-back-url-group');
if (style === 'custom') {
customGroup.style.display = 'block';
urlGroup.style.display = 'block';
} else {
customGroup.style.display = 'none';
urlGroup.style.display = 'none';
}
// Применяем стиль
applyCardBackStyle(style);
// Сохраняем в настройки
settings.cardBackStyle = style;
localStorage.setItem('pokerSettings', JSON.stringify(settings));
}
/**
* Загрузить кастомное изображение рубашки
*/
function loadCustomCardBack(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const imageUrl = e.target.result;
setCustomCardBack(imageUrl);
};
reader.readAsDataURL(file);
}
/**
* Установить URL рубашки
*/
function setCardBackUrl(url) {
if (!url) return;
setCustomCardBack(url);
}
/**
* Установить кастомную рубашку
*/
function setCustomCardBack(imageUrl) {
// Сохраняем в localStorage
localStorage.setItem('customCardBack', imageUrl);
// Применяем стиль
applyCardBackStyle('custom', imageUrl);
showNotification('Рубашка карт обновлена!', 'success');
}
/**
* Применить стиль рубашки ко всем картам
*/
function applyCardBackStyle(style, customUrl = null) {
const root = document.documentElement;
const preview = document.getElementById('card-back-preview');
// Убираем все стили
document.querySelectorAll('.card-back').forEach(card => {
card.classList.remove('style-default', 'style-red', 'style-blue', 'style-custom');
card.style.backgroundImage = '';
});
if (style === 'custom') {
const url = customUrl || localStorage.getItem('customCardBack');
if (url) {
root.style.setProperty('--card-back-custom-url', `url(${url})`);
document.querySelectorAll('.card-back').forEach(card => {
card.classList.add('style-custom');
card.style.backgroundImage = `url(${url})`;
card.style.backgroundSize = 'cover';
card.style.backgroundPosition = 'center';
});
if (preview) {
preview.classList.add('style-custom');
preview.style.backgroundImage = `url(${url})`;
preview.style.backgroundSize = 'cover';
preview.style.backgroundPosition = 'center';
}
}
} else {
document.querySelectorAll('.card-back').forEach(card => {
card.classList.add(`style-${style}`);
});
if (preview) {
preview.className = 'card card-back';
preview.classList.add(`style-${style}`);
preview.style.backgroundImage = '';
}
}
}
/**
* Загрузить настройки рубашки при старте
*/
function loadCardBackSettings() {
const style = settings.cardBackStyle || 'default';
// Активируем нужную кнопку
const buttons = document.querySelectorAll('.card-back-styles .btn-option');
buttons.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.style === style) {
btn.classList.add('active');
}
});
// Показываем поля для кастомной рубашки если нужно
const customGroup = document.getElementById('custom-card-back-group');
const urlGroup = document.getElementById('card-back-url-group');
if (customGroup && urlGroup) {
if (style === 'custom') {
customGroup.style.display = 'block';
urlGroup.style.display = 'block';
} else {
customGroup.style.display = 'none';
urlGroup.style.display = 'none';
}
}
// Применяем стиль
applyCardBackStyle(style);
}
/**
* Переключить звук
*/
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);
}