poker/public/main.js

1674 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/**
* =============================================================================
* 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();
loadCardBackSettings();
// Восстанавливаем имя игрока
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 = '<div class="room-list-loading">Загрузка комнат...</div>';
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 = '<div class="room-list-loading">Соединение потеряно. <button class="btn btn-small" onclick="connectWebSocket()">Переподключиться</button></div>';
};
ws.onerror = (error) => {
console.error('WebSocket ошибка:', error);
wsConnecting = false;
document.getElementById('room-list').innerHTML = '<div class="room-list-loading">Ошибка подключения к серверу. <button class="btn btn-small" onclick="connectWebSocket()">Переподключиться</button></div>';
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 = '<div class="room-list-loading">Нет доступных комнат</div>';
return;
}
container.innerHTML = rooms.map(room => `
<div class="room-item" onclick="joinRoom('${room.id}')">
<div class="room-info">
<h4>${room.name}</h4>
<span>Блайнды: ${room.smallBlind}/${room.bigBlind}</span>
</div>
<div class="room-players">
<span class="room-status ${room.isGameStarted ? 'playing' : ''}"></span>
${room.players}/${room.maxPlayers}
</div>
</div>
`).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) => `
<div class="lobby-player ${index === 0 ? 'host' : ''}">
<div class="lobby-player-avatar">
${player.name.charAt(0).toUpperCase()}
</div>
<div class="lobby-player-name">${player.name}</div>
</div>
`).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 = `
<span class="card-rank">${cardData.rank}</span>
<span class="card-suit">${symbols[cardData.suit]}</span>
`;
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 = `
<div class="player-name">${player.name}</div>
<div class="player-chips">${player.chips}</div>
${player.lastAction ? `<div class="player-action">${player.lastAction}</div>` : ''}
`;
// Определяем, нужно ли показывать карты
// Показываем карты только на 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 = `
<div class="card card-small card-back"></div>
<div class="card card-small card-back"></div>
`;
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 = `
<div class="card card-back"></div>
<div class="card card-back"></div>
`;
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 = `
<div class="result-amount">+${winner.amount}</div>
`;
if (winner.hand) {
detailsHTML += `<div class="result-hand">${winner.hand.name}</div>`;
}
if (result.hands && result.hands.length > 1) {
detailsHTML += '<div class="all-hands">';
result.hands.forEach(h => {
detailsHTML += `<div>${h.player.name}: ${h.hand.name}</div>`;
});
detailsHTML += '</div>';
}
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 = `
<span class="sender">${sender}</span>: <span class="typing-dots">печатает<span>.</span><span>.</span><span>.</span></span>
<span class="time">${time}</span>
`;
} else {
msgDiv.innerHTML = `
<span class="sender">${sender}</span>: ${message}
<span class="time">${time}</span>
`;
}
}
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');
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 = '<div class="room-list-loading">Нет записей</div>';
return;
}
container.innerHTML = leaderboard.slice(0, 10).map((entry, index) => {
const rankClass = index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? 'bronze' : '';
return `
<div class="leaderboard-item">
<div class="leaderboard-rank ${rankClass}">${index + 1}</div>
<div class="leaderboard-info">
<div class="leaderboard-name">${entry.name}</div>
<div class="leaderboard-stats">
Игр: ${entry.gamesPlayed} | Побед: ${entry.handsWon} | Макс. банк: ${entry.biggestPot}
</div>
</div>
<div class="leaderboard-score">${entry.totalWinnings}</div>
</div>
`;
}).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 =
'<div class="room-list-loading">Глобальный лидерборд недоступен (требуется backend)</div>';
} 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);
}