poker/public/main.js

3337 lines
121 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
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();
loadSessionStats();
initSounds();
loadCardBackSettings();
// Загружаем персональности ботов
if (typeof pokerAI !== 'undefined' && pokerAI.personalities) {
botPersonalities = {};
pokerAI.personalities.forEach(p => {
botPersonalities[p.style] = p;
});
}
// Инициализируем кастомные промпты
setTimeout(() => {
initCustomPrompts();
}, 100);
// Инициализируем авторизацию
if (typeof initAuth === 'function') {
initAuth();
} else {
// Если auth.js не загружен, показываем главное меню
showScreen('main-menu');
}
});
/**
* Инициализация звуков
*/
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();
}
// Загрузка админских настроек
if (screenId === 'admin-screen') {
if (typeof loadAdminSettings === 'function') {
loadAdminSettings();
}
}
}
/**
* Переключение вкладок
*/
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);
// Очищаем чат перед началом новой игры
clearGameChat();
// Создаём игру
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 = '<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}`;
// Генерируем и отображаем ссылку на комнату
updateRoomLink(room.id);
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;
// Обновляем ссылку на комнату (если ID изменился)
if (room.id) {
updateRoomLink(room.id);
}
}
/**
* Выйти из комнаты
*/
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);
updatePlayerBalance(player);
}
updateActionPanel(player, game);
// Обновляем статистику
updateGameStats();
}
/**
* Обновить 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);
updatePlayerBalance(myPlayer);
updateActionPanelFromServer(myPlayer, room);
}
// Обновляем статистику (для мультиплеера используем упрощенную версию)
updateGameStats();
// Показываем кнопку новой раздачи если игра завершена
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');
// Проверяем, изменились ли карты
const currentHandKey = hand && hand.length > 0
? hand.map(c => `${c.suit}-${c.rank}`).join(',')
: 'empty';
// Если карты не изменились, не перерисовываем
if (container.dataset.currentHand === currentHandKey) {
// Только обновляем силу руки, если изменились общие карты
if (settings.showHandStrength !== false && hand && hand.length > 0) {
const strength = getHandStrength(hand, communityCards || []);
document.getElementById('hand-strength').textContent = strength || '';
}
return;
}
// Запоминаем текущие карты
container.dataset.currentHand = currentHandKey;
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 updatePlayerBalance(player) {
if (!player) return;
// Обновляем имя игрока
const nameDisplay = document.getElementById('player-name-display');
if (nameDisplay) {
nameDisplay.textContent = player.name || 'Игрок';
}
// Обновляем баланс
const balanceDisplay = document.getElementById('player-balance');
if (balanceDisplay) {
const chips = player.chips || 0;
balanceDisplay.textContent = chips;
// Добавляем анимацию при изменении баланса
if (balanceDisplay.dataset.lastValue && balanceDisplay.dataset.lastValue !== chips.toString()) {
const oldValue = parseInt(balanceDisplay.dataset.lastValue);
const newValue = chips;
if (newValue > oldValue) {
balanceDisplay.parentElement.classList.add('balance-increase');
setTimeout(() => balanceDisplay.parentElement.classList.remove('balance-increase'), 500);
} else if (newValue < oldValue) {
balanceDisplay.parentElement.classList.add('balance-decrease');
setTimeout(() => balanceDisplay.parentElement.classList.remove('balance-decrease'), 500);
}
}
balanceDisplay.dataset.lastValue = chips.toString();
}
}
/**
* Обновить панель действий
*/
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);
// Обновляем статистику сессии
updateStatsOnHandEnd(result);
// НОВОЕ: Генерируем эмоциональные реакции ботов
if (!isMultiplayer && game) {
generateBotEmotions(result);
// НОВОЕ: Поздравления игрока от ботов при сильной руке
generatePlayerCongratulations(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);
}
/**
* Генерировать поздравления игрока с сильной рукой
*/
async function generatePlayerCongratulations(result) {
if (!game || !result || !result.winners || !result.hands) return;
const winner = result.winners[0];
const potSize = result.pot || game.pot || 0;
// Проверяем, что победитель - игрок (не бот)
const winnerData = game.players.find(p => p.id === winner.id);
if (!winnerData || winnerData.isAI) return;
// Получаем руку победителя
const winnerHand = result.hands?.find(h => h.player.id === winner.id)?.hand;
if (!winnerHand) return;
const handRank = winnerHand.rank;
const handName = winnerHand.name;
// Определяем вероятность поздравления в зависимости от силы руки
let congratsProbability = 0;
if (handRank >= 10) {
// Роял-флеш - 90% вероятность поздравления
congratsProbability = 0.9;
} else if (handRank >= 9) {
// Стрит-флеш - 80% вероятность
congratsProbability = 0.8;
} else if (handRank >= 8) {
// Каре - 60% вероятность
congratsProbability = 0.6;
} else if (handRank >= 7) {
// Фулл-хаус - 40% вероятность
congratsProbability = 0.4;
} else if (handRank >= 6) {
// Флеш - 25% вероятность
congratsProbability = 0.25;
} else if (handRank >= 5) {
// Стрит - 15% вероятность
congratsProbability = 0.15;
} else {
// Слабые руки - не поздравляем
return;
}
// Решаем, будем ли поздравлять
if (Math.random() > congratsProbability) return;
// Выбираем случайного бота для поздравления
const availableBots = game.players.filter(p => p.isAI && !p.folded);
if (availableBots.length === 0) return;
const bot = availableBots[Math.floor(Math.random() * availableBots.length)];
// Получаем личность бота
let botPersonality;
if (typeof botPersonalities !== 'undefined' && bot.personalityId) {
botPersonality = botPersonalities[bot.personalityId];
}
if (!botPersonality) {
botPersonality = { style: 'professional' };
}
// Задержка перед поздравлением (1000-2500ms, чтобы не конфликтовать с эмоциями)
setTimeout(async () => {
try {
let congratulation;
if (typeof llmChat !== 'undefined' && llmChat.generatePlayerCongratulation) {
congratulation = await llmChat.generatePlayerCongratulation(
bot,
botPersonality,
winnerData.name,
handRank,
handName,
potSize,
{ phase: currentGamePhase }
);
} else {
// Запасной вариант
const congrats = [
`${handName}! Респект!`,
'Сильная рука!',
'Впечатляет!',
'Молодец!',
'GG WP!'
];
congratulation = congrats[Math.floor(Math.random() * congrats.length)];
}
if (congratulation) {
addChatMessage('game', bot.name, congratulation);
}
} catch (error) {
console.error('Ошибка генерации поздравления:', error);
}
}, 1000 + Math.random() * 1500);
}
/**
* Закрыть модальное окно результата
*/
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, chatType = 'game') {
const containerId = chatType === 'game' ? 'game-chat-messages' : 'lobby-chat-messages';
const container = document.getElementById(containerId);
if (!container) return;
// Используем querySelectorAll и проверяем dataset, чтобы избежать проблем с экранированием кавычек
const typingIndicators = container.querySelectorAll('.typing-indicator');
typingIndicators.forEach(indicator => {
if (indicator.dataset.sender === sender) {
indicator.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,
phaseRu: {
'preflop': 'префлоп (карты только что розданы)',
'flop': 'флоп (3 карты на столе)',
'turn': 'терн (4 карты на столе)',
'river': 'ривер (5 карт на столе)',
'showdown': 'вскрытие карт'
}[currentGamePhase] || currentGamePhase,
// Банк и текущая ставка
pot: game.pot,
currentBet: game.currentBet,
// Общие карты на столе
communityCards: game.communityCards?.map(c => `${c.rank}${SUIT_SYMBOLS[c.suit]}`) || [],
communityCardsCount: game.communityCards?.length || 0,
// Мои данные
myName: bot.name,
myChips: bot.chips,
myBet: bot.bet,
myTotalBet: bot.totalBet,
myCards: bot.hand?.map(c => `${c.rank}${SUIT_SYMBOLS[c.suit]}`) || [],
myPosition: bot.isDealer ? 'дилер' : (bot.isSmallBlind ? 'малый блайнд' : (bot.isBigBlind ? 'большой блайнд' : 'обычная позиция')),
// Информация о других игроках
players: game.players.map(p => ({
name: p.name,
chips: p.chips,
bet: p.bet,
totalBet: p.totalBet,
folded: p.folded,
allIn: p.allIn,
lastAction: p.lastAction,
isDealer: p.isDealer,
isMe: p.id === bot.id
})),
// Активные игроки (не спасовали)
activePlayers: game.players.filter(p => !p.folded).map(p => p.name),
foldedPlayers: game.players.filter(p => p.folded).map(p => p.name),
// Последние действия
lastPlayerAction: message,
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 clearGameChat() {
const container = document.getElementById('game-chat-messages');
if (container) {
container.innerHTML = '';
}
}
// =============================================================================
// НАСТРОЙКИ
// =============================================================================
/**
* Загрузить настройки
*/
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;
clearGameChat(); // Очищаем чат при выходе из игры
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);
}
// =============================================================================
// ШАРИНГ КОМНАТЫ
// =============================================================================
/**
* Обновить ссылку на комнату
*/
function updateRoomLink(roomId) {
const baseUrl = window.location.origin + window.location.pathname;
const roomUrl = `${baseUrl}?room=${roomId}`;
const input = document.getElementById('room-link-input');
if (input) {
input.value = roomUrl;
}
// Проверяем поддержку Web Share API
const shareBtn = document.getElementById('share-generic-btn');
if (shareBtn) {
if (navigator.share) {
shareBtn.style.display = 'inline-flex';
} else {
shareBtn.style.display = 'none';
}
}
}
/**
* Копировать ссылку на комнату
*/
async function copyRoomLink() {
const input = document.getElementById('room-link-input');
const url = input.value;
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(url);
showNotification('Ссылка скопирована!', 'success');
} else {
// Запасной вариант для старых браузеров
input.select();
document.execCommand('copy');
showNotification('Ссылка скопирована!', 'success');
}
} catch (error) {
console.error('Ошибка копирования:', error);
showNotification('Не удалось скопировать ссылку', 'error');
}
}
/**
* Поделиться в Telegram
*/
function shareToTelegram() {
const input = document.getElementById('room-link-input');
const url = input.value;
const roomName = document.getElementById('lobby-room-name').textContent;
const text = `Присоединяйся к игре в покер "${roomName}"!`;
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
window.open(telegramUrl, '_blank');
}
/**
* Поделиться в WhatsApp
*/
function shareToWhatsApp() {
const input = document.getElementById('room-link-input');
const url = input.value;
const roomName = document.getElementById('lobby-room-name').textContent;
const text = `Присоединяйся к игре в покер "${roomName}"!\n${url}`;
const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text)}`;
window.open(whatsappUrl, '_blank');
}
/**
* Поделиться через Web Share API
*/
async function shareGeneric() {
const input = document.getElementById('room-link-input');
const url = input.value;
const roomName = document.getElementById('lobby-room-name').textContent;
if (navigator.share) {
try {
await navigator.share({
title: 'Texas Hold\'em Poker',
text: `Присоединяйся к игре в покер "${roomName}"!`,
url: url
});
showNotification('Ссылка отправлена!', 'success');
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Ошибка шаринга:', error);
showNotification('Не удалось поделиться', 'error');
}
}
} else {
showNotification('Ваш браузер не поддерживает эту функцию', 'info');
}
}
/**
* Проверить и присоединиться к комнате из URL
*/
function checkRoomInUrl() {
const urlParams = new URLSearchParams(window.location.search);
const roomId = urlParams.get('room');
if (roomId && currentPlayerId) {
// Если есть ID комнаты в URL, пытаемся присоединиться
showNotification('Подключение к комнате...', 'info');
// Переходим в мультиплеер и подключаемся
setTimeout(() => {
showScreen('multiplayer-menu');
// Ждём подключения WebSocket
if (ws && ws.readyState === WebSocket.OPEN) {
joinRoom(roomId);
} else {
connectWebSocket().then(() => {
setTimeout(() => joinRoom(roomId), 500);
});
}
}, 500);
}
}
// =============================================================================
// ПАНЕЛЬ ИНФОРМАЦИИ И СТАТИСТИКИ
// =============================================================================
// Статистика сессии
let sessionStats = {
handsPlayed: 0,
handsWon: 0,
totalWon: 0,
bestHandRank: 0,
bestHandName: '—'
};
/**
* Переключить панель информации
*/
function toggleGameInfo() {
document.getElementById('game-info-panel').classList.toggle('expanded');
}
/**
* Переключить вкладку информации
*/
function switchInfoTab(tabName) {
// Убираем active со всех вкладок
document.querySelectorAll('.info-tab').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.info-tab-content').forEach(content => {
content.classList.remove('active');
});
// Активируем выбранную
event.target.classList.add('active');
document.getElementById(`info-tab-${tabName}`).classList.add('active');
// Обновляем статистику при переключении
if (tabName === 'stats') {
updateGameStats();
}
}
/**
* Обновить отображение текущей руки в статистике
*/
function updateCurrentHandDisplay() {
const previewContainer = document.getElementById('hand-cards-preview');
const strengthDisplay = document.getElementById('hand-strength-display');
// Получаем текущие карты игрока
let playerHand = [];
let communityCards = [];
if (isMultiplayer) {
// В мультиплеере данные берем из последнего обновления
// Это нужно реализовать через сохранение состояния
previewContainer.textContent = 'Обновление...';
strengthDisplay.textContent = '—';
return;
} else if (game && game.isGameStarted) {
const player = game.players.find(p => p.id === currentPlayerId);
if (player && player.hand.length > 0) {
playerHand = player.hand;
communityCards = game.communityCards || [];
}
}
if (playerHand.length === 0) {
previewContainer.textContent = 'Карты не розданы';
strengthDisplay.textContent = '—';
return;
}
// Отображаем карты
previewContainer.innerHTML = '';
playerHand.forEach(card => {
const miniCard = document.createElement('div');
miniCard.className = `mini-card ${card.suit}`;
miniCard.innerHTML = `
<div>${card.rank}</div>
<div style="font-size: 8px;">${SUIT_SYMBOLS[card.suit]}</div>
`;
previewContainer.appendChild(miniCard);
});
// Отображаем силу руки
const strength = getHandStrength(playerHand, communityCards);
strengthDisplay.textContent = strength || '—';
}
/**
* Рассчитать вероятность победы
*/
function calculateWinProbability() {
if (!game || !game.isGameStarted) return 0;
const player = game.players.find(p => p.id === currentPlayerId);
if (!player || player.hand.length === 0) return 0;
const activePlayers = game.getActivePlayers().filter(p => !p.folded);
const opponentsCount = activePlayers.length - 1;
if (opponentsCount === 0) return 100;
// Простая оценка на основе силы руки и фазы игры
const allCards = [...player.hand, ...(game.communityCards || [])];
if (allCards.length < 5) {
// Префлоп - базовая оценка по стартовой руке
return calculatePreflopWinRate(player.hand, opponentsCount);
}
// Постфлоп - оцениваем текущую силу руки
const handResult = evaluateHand(allCards);
return calculatePostflopWinRate(handResult, game.gamePhase, opponentsCount);
}
/**
* Оценка вероятности победы на префлопе
*/
function calculatePreflopWinRate(hand, opponents) {
const [c1, c2] = hand;
const isPair = c1.value === c2.value;
const isSuited = c1.suit === c2.suit;
const highCard = Math.max(c1.value, c2.value);
const lowCard = Math.min(c1.value, c2.value);
const gap = highCard - lowCard;
let baseRate = 0;
if (isPair) {
// Карманные пары
if (highCard >= 12) baseRate = 85; // QQ+
else if (highCard >= 10) baseRate = 75; // TT-JJ
else if (highCard >= 7) baseRate = 65; // 77-99
else baseRate = 55; // 22-66
} else if (highCard === 14 && lowCard >= 10) {
// AK, AQ, AJ, AT
baseRate = isSuited ? 70 : 65;
} else if (highCard >= 13 && lowCard >= 10) {
// KQ, KJ, QJ
baseRate = isSuited ? 65 : 60;
} else if (gap <= 1 && highCard >= 10) {
// Коннекторы высокие
baseRate = isSuited ? 60 : 55;
} else if (isSuited && highCard >= 11) {
// Одномастные с картой
baseRate = 55;
} else if (gap <= 2 && lowCard >= 6) {
// Средние коннекторы
baseRate = isSuited ? 50 : 45;
} else {
// Слабые руки
baseRate = 35;
}
// Корректировка на количество оппонентов
const adjustment = (opponents - 1) * 5;
return Math.max(10, Math.min(95, baseRate - adjustment));
}
/**
* Оценка вероятности победы постфлоп
*/
function calculatePostflopWinRate(handResult, phase, opponents) {
const rankMultipliers = {
10: 95, // Роял-флеш
9: 90, // Стрит-флеш
8: 85, // Каре
7: 80, // Фулл-хаус
6: 75, // Флеш
5: 70, // Стрит
4: 65, // Сет
3: 55, // Две пары
2: 45, // Пара
1: 30 // Старшая карта
};
let baseRate = rankMultipliers[handResult.rank] || 30;
// Корректировка на фазу игры
if (phase === 'flop') {
baseRate *= 0.85; // Еще 2 карты впереди
} else if (phase === 'turn') {
baseRate *= 0.92; // Еще 1 карта
}
// Корректировка на количество оппонентов
const adjustment = (opponents - 1) * 3;
return Math.max(5, Math.min(98, Math.round(baseRate - adjustment)));
}
/**
* Обновить вероятности в панели статистики
*/
function updateWinProbability() {
const probability = calculateWinProbability();
const probabilityBar = document.getElementById('probability-bar');
const probabilityText = document.getElementById('probability-text');
if (probabilityBar && probabilityText) {
probabilityBar.style.width = `${probability}%`;
probabilityText.textContent = `${probability}%`;
// Меняем цвет в зависимости от вероятности
probabilityBar.classList.remove('low', 'medium', 'high');
if (probability < 40) {
probabilityBar.classList.add('low');
} else if (probability < 65) {
probabilityBar.classList.add('medium');
} else {
probabilityBar.classList.add('high');
}
}
}
/**
* Обновить шансы на улучшение
*/
function updateImprovementOdds() {
const oddsNext = document.getElementById('odds-next');
const oddsRiver = document.getElementById('odds-river');
if (!game || !game.isGameStarted) {
if (oddsNext) oddsNext.textContent = '—';
if (oddsRiver) oddsRiver.textContent = '—';
return;
}
const player = game.players.find(p => p.id === currentPlayerId);
if (!player || player.hand.length === 0) {
if (oddsNext) oddsNext.textContent = '—';
if (oddsRiver) oddsRiver.textContent = '—';
return;
}
// Упрощенный расчет аутов
const communityCards = game.communityCards || [];
const phase = game.gamePhase;
// Считаем примерное количество аутов
let outs = estimateOuts(player.hand, communityCards);
if (outs > 0) {
// Правило 2-4: умножаем ауты на 2 для следующей карты, на 4 для двух карт
const nextCardOdds = Math.min(100, outs * 2);
const riverOdds = Math.min(100, outs * 4);
if (phase === 'flop') {
if (oddsNext) oddsNext.textContent = `~${nextCardOdds}% (${outs} аутов)`;
if (oddsRiver) oddsRiver.textContent = `~${riverOdds}%`;
} else if (phase === 'turn') {
if (oddsNext) oddsNext.textContent = `~${nextCardOdds}% (${outs} аутов)`;
if (oddsRiver) oddsRiver.textContent = '—';
} else {
if (oddsNext) oddsNext.textContent = '—';
if (oddsRiver) oddsRiver.textContent = '—';
}
} else {
if (oddsNext) oddsNext.textContent = 'Готовая рука';
if (oddsRiver) oddsRiver.textContent = '—';
}
}
/**
* Приблизительная оценка аутов
*/
function estimateOuts(hand, communityCards) {
if (communityCards.length < 3) return 0;
const allCards = [...hand, ...communityCards];
const currentHand = evaluateHand(allCards);
// Если уже сильная рука (стрит+), аутов нет
if (currentHand.rank >= 5) return 0;
// Подсчет по мастям для флеш-дро
const suitCounts = {};
allCards.forEach(c => {
suitCounts[c.suit] = (suitCounts[c.suit] || 0) + 1;
});
const maxSuitCount = Math.max(...Object.values(suitCounts));
let outs = 0;
// Флеш-дро (4 карты одной масти)
if (maxSuitCount === 4) {
outs += 9;
}
// Стрит-дро (упрощенно)
const values = allCards.map(c => c.value).sort((a, b) => b - a);
const uniqueValues = [...new Set(values)];
// Проверяем на открытый стрит-дро (8 аутов)
for (let i = 0; i < uniqueValues.length - 3; i++) {
const gap = uniqueValues[i] - uniqueValues[i + 3];
if (gap === 4) {
outs += 8;
break;
}
}
// Пара для сета (2 аута)
if (currentHand.rank === 2) {
outs += 2;
}
return Math.min(outs, 21); // Максимум 21 аут
}
/**
* Обновить статистику сессии
*/
function updateSessionStats() {
document.getElementById('stat-hands-played').textContent = sessionStats.handsPlayed;
document.getElementById('stat-hands-won').textContent = sessionStats.handsWon;
document.getElementById('stat-total-won').textContent = sessionStats.totalWon;
document.getElementById('stat-best-hand').textContent = sessionStats.bestHandName;
}
/**
* Обновить всю статистику в панели
*/
function updateGameStats() {
updateCurrentHandDisplay();
updateWinProbability();
updateImprovementOdds();
updateSessionStats();
}
/**
* Обработать завершение раздачи для статистики
*/
function updateStatsOnHandEnd(result) {
sessionStats.handsPlayed++;
// Проверяем, выиграл ли игрок
const playerWon = result.winners.some(w => w.id === currentPlayerId);
if (playerWon) {
sessionStats.handsWon++;
const winAmount = result.winners.find(w => w.id === currentPlayerId)?.amount || 0;
sessionStats.totalWon += winAmount;
// Обновляем лучшую руку
const playerHandResult = result.hands?.find(h => h.player.id === currentPlayerId)?.hand;
if (playerHandResult && playerHandResult.rank > sessionStats.bestHandRank) {
sessionStats.bestHandRank = playerHandResult.rank;
sessionStats.bestHandName = playerHandResult.name;
}
}
// Сохраняем статистику в localStorage
localStorage.setItem('pokerSessionStats', JSON.stringify(sessionStats));
// Обновляем отображение
updateGameStats();
}
/**
* Загрузить статистику сессии
*/
function loadSessionStats() {
const saved = localStorage.getItem('pokerSessionStats');
if (saved) {
sessionStats = JSON.parse(saved);
}
}
/**
* Сбросить статистику сессии
*/
function resetSessionStats() {
sessionStats = {
handsPlayed: 0,
handsWon: 0,
totalWon: 0,
bestHandRank: 0,
bestHandName: '—'
};
localStorage.removeItem('pokerSessionStats');
updateSessionStats();
}
// =============================================================================
// НАСТРОЙКА ПРОМПТОВ БОТОВ (АДМИН-ПАНЕЛЬ)
// =============================================================================
// Дефолтные промпты для разных стилей
const defaultBotPrompts = {
aggressive: {
base: `Ты — агрессивный игрок в покер. Ты любишь рисковать, давить на оппонентов и доминировать за столом.
ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
Характер: уверенный, напористый, любишь психологическое давление.`,
chat: `Отвечай коротко (1-2 предложения). Комментируй игру агрессивно и уверенно.
Используй покерный сленг. Можешь подначивать соперников.`,
emotion: `Реагируй на результат раздачи агрессивно: при победе — ликуй и хвастайся,
при проигрыше — злись и обещай отыграться.`,
congrats: `Поздравляй с сильной рукой, но с оттенком зависти или вызова.
Например: "Ничего, следующая раздача моя!"`,
examples: 'Олл-ин!, Слабаки!, Я вас всех обыграю, Покажу вам класс, Удачи не бывает два раза'
},
conservative: {
base: `Ты — консервативный игрок в покер. Ты осторожен, играешь только с сильными руками.
ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
Характер: сдержанный, расчётливый, терпеливый.`,
chat: `Отвечай коротко (1-2 предложения). Комментируй игру осторожно и вдумчиво.
Подчёркивай важность расчёта и терпения.`,
emotion: `Реагируй сдержанно: при победе — спокойное удовлетворение,
при проигрыше — философское принятие.`,
congrats: `Поздравляй искренне и с уважением к хорошей игре.`,
examples: 'Хороший ход, Надо подумать, Терпение — ключ к успеху, Математика не врёт, Осторожность не помешает'
},
tricky: {
base: `Ты — хитрый и непредсказуемый игрок в покер. Ты любишь обманывать и держать интригу.
ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
Характер: загадочный, изворотливый, любишь блефовать.`,
chat: `Отвечай коротко (1-2 предложения). Говори намёками и загадками.
Держи интригу, не показывай свои намерения.`,
emotion: `Реагируй загадочно и двусмысленно независимо от результата.
Никто не должен понять, расстроен ты или рад.`,
congrats: `Поздравляй загадочно, с намёком что ты что-то знаешь или планируешь.`,
examples: 'Интересно..., Посмотрим, Может быть..., У меня есть план, Ничего не бывает случайным'
},
professional: {
base: `Ты — профессиональный игрок в покер. Ты играешь по правилам GTO и математике.
ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
Характер: серьёзный, аналитичный, уважающий хорошую игру.`,
chat: `Отвечай коротко (1-2 предложения). Комментируй с точки зрения математики и стратегии.
Можешь упомянуть odds, EV, диапазоны.`,
emotion: `Реагируй с точки зрения анализа игры. При победе — отмечай правильность решения,
при проигрыше — анализируй variance.`,
congrats: `Поздравляй профессионально, отмечая техническое мастерство.`,
examples: 'Правильный колл, +EV решение, Variance, GTO подход, По математике всё верно'
},
loose: {
base: `Ты — лузовый игрок в покер. Ты играешь много рук и любишь экшн.
ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
Характер: весёлый, азартный, оптимистичный.`,
chat: `Отвечай коротко (1-2 предложения). Комментируй весело и с энтузиазмом.
Показывай любовь к игре и экшну.`,
emotion: `Реагируй эмоционально и позитивно. При победе — ликуй,
при проигрыше — не расстраивайся, играй дальше.`,
congrats: `Поздравляй искренне и с восторгом, радуйся за хорошую руку.`,
examples: 'Давай!, Играем!, Вот это карта!, Классно!, Ещё раунд!'
},
tight: {
base: `Ты — тайтовый игрок в покер. Ты играешь только премиум-руки и очень избирательно.
ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
Характер: дисциплинированный, терпеливый, методичный.`,
chat: `Отвечай коротко (1-2 предложения). Комментируй сдержанно и по делу.
Подчёркивай важность выбора правильных рук.`,
emotion: `Реагируй сдержанно. При победе — спокойное удовлетворение,
при проигрыше — стоическое принятие.`,
congrats: `Поздравляй сдержанно, с уважением к терпению и дисциплине.`,
examples: 'Ожидал лучшей руки, Фолд — тоже решение, Дисциплина важна, Премиум руки, Терпение окупается'
}
};
/**
* Инициализировать селектор персональностей ботов
*/
function initBotPersonalitySelector() {
if (typeof pokerAI === 'undefined' || !pokerAI.personalities) {
console.error('pokerAI.personalities не загружен');
return;
}
const selector = document.getElementById('bot-personality-selector');
if (!selector) return;
// Очищаем селектор
selector.innerHTML = '';
// Добавляем опции для каждой персональности
pokerAI.personalities.forEach((personality, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${personality.avatar} ${personality.name}`;
selector.appendChild(option);
});
// Загружаем промпт для первой персональности
loadBotPersonalityPrompt();
}
/**
* Загрузить промпт для выбранной персональности бота
*/
function loadBotPersonalityPrompt() {
if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
const index = parseInt(document.getElementById('bot-personality-selector').value);
const personality = pokerAI.personalities[index];
if (!personality) return;
// Проверяем, есть ли кастомный промпт
const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
const customPrompt = customPrompts[personality.name];
// Показываем кастомный промпт если есть, иначе оригинальный
const promptToShow = customPrompt || personality.systemPrompt;
document.getElementById('bot-system-prompt').value = promptToShow;
}
/**
* Сохранить промпт для выбранной персональности
*/
function saveBotPersonalityPrompt() {
if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
const index = parseInt(document.getElementById('bot-personality-selector').value);
const personality = pokerAI.personalities[index];
if (!personality) return;
const newPrompt = document.getElementById('bot-system-prompt').value;
// Сохраняем в localStorage
const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
customPrompts[personality.name] = newPrompt;
localStorage.setItem('customPersonalityPrompts', JSON.stringify(customPrompts));
// Обновляем промпт в самом объекте (для текущей сессии)
personality.systemPrompt = newPrompt;
showNotification(`Промпт для ${personality.name} сохранён!`, 'success');
}
/**
* Сбросить промпт персональности к оригиналу
*/
function resetBotPersonalityPrompt() {
if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
const index = parseInt(document.getElementById('bot-personality-selector').value);
const personality = pokerAI.personalities[index];
if (!personality) return;
if (confirm(`Вы уверены, что хотите сбросить промпт для ${personality.name} к оригиналу?`)) {
// Удаляем кастомный промпт
const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
delete customPrompts[personality.name];
localStorage.setItem('customPersonalityPrompts', JSON.stringify(customPrompts));
// Перезагружаем оригинальный промпт (нужно перезагрузить страницу или хранить оригиналы)
// Для простоты просто перезагружаем
location.reload();
}
}
/**
* Тестировать промпт персональности
*/
async function testBotPersonalityPrompt() {
if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
if (typeof llmChat === 'undefined') {
showNotification('LLM чат не загружен', 'error');
return;
}
const settings = llmChat.getSettings();
if (!settings.llmEnabled) {
showNotification('Включите LLM чат в настройках', 'warning');
return;
}
const index = parseInt(document.getElementById('bot-personality-selector').value);
const personality = pokerAI.personalities[index];
if (!personality) return;
const newPrompt = document.getElementById('bot-system-prompt').value;
// Создаём тестовую персональность с новым промптом
const testPersonality = {
...personality,
systemPrompt: newPrompt
};
const resultDiv = document.getElementById('prompt-test-result');
const contentDiv = document.getElementById('test-result-content');
resultDiv.style.display = 'block';
contentDiv.innerHTML = '<div class="loading">⏳ Тестирование...</div>';
try {
// Создаём тестовый контекст игры
const gameContext = {
phaseRu: 'Флоп',
communityCards: ['K♠', 'Q♥', '7♦'],
pot: 150,
currentBet: 50,
myName: personality.name,
myCards: ['A♠', 'A♥'],
myChips: 1000,
myBet: 50,
players: [
{ name: 'Игрок', chips: 950, bet: 50, lastAction: 'call' }
]
};
const testMessage = 'Как думаешь, какие у меня шансы?';
const response = await llmChat.chat(
'test-bot-' + Date.now(),
testPersonality,
testMessage,
gameContext
);
contentDiv.innerHTML = `
<div class="test-success">
<strong>✅ Тест успешен!</strong>
<div class="test-context">
<strong>Тестовая ситуация:</strong>
<div>🎴 Флоп: K♠ Q♥ 7♦</div>
<div>🃏 Карты бота: A♠ A♥</div>
<div>💰 Банк: 150 | Ставка: 50</div>
<div>💬 Вопрос: "${testMessage}"</div>
</div>
<div class="test-response">
<strong>Ответ бота:</strong>
<div>"${response}"</div>
</div>
</div>
`;
} catch (error) {
contentDiv.innerHTML = `
<div class="test-error">
<strong>❌ Ошибка теста:</strong>
<div>${error.message}</div>
</div>
`;
}
}
/**
* Тестировать промпт бота (старая функция, оставлена для совместимости)
*/
async function testBotPrompt() {
const resultDiv = document.getElementById('prompt-test-result');
const contentDiv = document.getElementById('test-result-content');
resultDiv.style.display = 'block';
contentDiv.innerHTML = '<div class="test-result-loading">Генерация тестового ответа...</div>';
const style = document.getElementById('bot-style-selector').value;
const basePrompt = document.getElementById('bot-base-prompt').value;
const chatPrompt = document.getElementById('bot-chat-prompt').value;
// Создаём тестовый контекст
const testContext = {
phase: 'flop',
pot: 150,
currentBet: 50,
communityCards: ['A♠', 'K♥', '7♦'],
myChips: 800,
myBet: 50,
players: [
{ name: 'Игрок', chips: 950, bet: 50 },
{ name: 'Тестовый бот', chips: 800, bet: 50 }
]
};
const testMessage = 'У меня хорошие карты!';
try {
// Проверяем доступность LLM
if (typeof llmChat === 'undefined' || !llmChat.getSettings().llmEnabled) {
contentDiv.innerHTML = `
<strong>⚠️ LLM чат отключён</strong><br><br>
Для тестирования промпта необходимо включить LLM в настройках.<br><br>
<strong>Симуляция ответа с текущим промптом:</strong><br>
<em>"${getFallbackResponse(style, testMessage, testContext)}"</em>
`;
return;
}
// Генерируем тестовый промпт
const fullPrompt = `${basePrompt}\n\n${chatPrompt}\n\nКонтекст игры:\n${JSON.stringify(testContext, null, 2)}\n\nСообщение игрока: "${testMessage}"\n\nТвой ответ (1-2 предложения):`;
// Вызываем LLM напрямую
const response = await fetch(settings.llmApiUrl + '/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: settings.llmModel,
prompt: fullPrompt,
stream: false,
options: {
temperature: 0.8,
num_predict: 100
}
})
});
if (!response.ok) {
throw new Error('Ошибка LLM API');
}
const data = await response.json();
const botResponse = data.response.trim();
contentDiv.innerHTML = `
<strong>✅ Тестовый ответ сгенерирован:</strong><br><br>
<div style="padding: 8px; background: var(--bg-primary); border-radius: 4px; border-left: 3px solid var(--accent-primary);">
"${botResponse}"
</div>
<br>
<small style="color: var(--text-muted);">
Контекст: Флоп [A♠ K♥ 7♦], банк 150, игрок сказал "У меня хорошие карты!"
</small>
`;
} catch (error) {
console.error('Ошибка тестирования:', error);
contentDiv.innerHTML = `
<strong>❌ Ошибка тестирования</strong><br><br>
${error.message}<br><br>
<strong>Симуляция ответа:</strong><br>
<em>"${getFallbackResponse(style, testMessage, testContext)}"</em>
`;
}
}
/**
* Получить запасной ответ для стиля
*/
function getFallbackResponse(style, message, context) {
const examples = document.getElementById('bot-example-phrases').value.split(',');
if (examples.length > 0 && examples[0].trim()) {
return examples[Math.floor(Math.random() * examples.length)].trim();
}
const fallbacks = {
aggressive: 'Посмотрим, кто тут сильнее!',
conservative: 'Будем играть осторожно.',
tricky: 'Интересный ход...',
professional: 'Правильное решение.',
loose: 'Давай играть!',
tight: 'Хорошая рука нужна.'
};
return fallbacks[style] || 'Ок.';
}
/**
* Обновить промпты в личностях AI
*/
function updateAIPersonalityPrompts() {
if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
const customPrompts = JSON.parse(localStorage.getItem('customBotPrompts') || '{}');
pokerAI.personalities.forEach(personality => {
const custom = customPrompts[personality.style];
if (custom && custom.base) {
personality.systemPrompt = custom.base;
personality.chatPrompt = custom.chat;
personality.emotionPrompt = custom.emotion;
personality.congratsPrompt = custom.congrats;
personality.examplePhrases = custom.examples ? custom.examples.split(',').map(p => p.trim()) : [];
}
});
}
/**
* Инициализация кастомных промптов при загрузке
*/
function initCustomPrompts() {
// Загружаем и применяем кастомные промпты если они есть
updateAIPersonalityPrompts();
// Инициализируем селектор персональностей
const selector = document.getElementById('bot-personality-selector');
if (selector) {
initBotPersonalitySelector();
}
}
// =============================================================================
// УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ (АДМИН-ПАНЕЛЬ)
// =============================================================================
/**
* Переключение вкладок админ-панели
*/
function switchAdminTab(tabName) {
// Скрываем все вкладки
document.querySelectorAll('.admin-tab-content').forEach(tab => {
tab.style.display = 'none';
});
// Убираем активный класс со всех кнопок
document.querySelectorAll('.btn-tab').forEach(btn => {
btn.classList.remove('active');
});
// Показываем выбранную вкладку
const selectedTab = document.getElementById(`admin-tab-${tabName}`);
if (selectedTab) {
selectedTab.style.display = 'block';
}
// Активируем кнопку
event.target.classList.add('active');
// Загружаем данные для вкладки
if (tabName === 'users') {
loadUsers();
} else if (tabName === 'logs') {
loadLogs();
} else if (tabName === 'bot-prompts') {
initBotPersonalitySelector();
}
}
/**
* Загрузить список пользователей
*/
async function loadUsers() {
try {
const token = localStorage.getItem('authToken');
if (!token) {
showNotification('Необходима авторизация', 'error');
// Перезагружаем страницу для возврата на экран входа
setTimeout(() => location.reload(), 1000);
return;
}
const roleFilter = document.getElementById('user-filter-role')?.value || '';
const statusFilter = document.getElementById('user-filter-status')?.value || '';
const search = document.getElementById('user-search')?.value || '';
const response = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
role: roleFilter,
status: statusFilter,
search: search
})
});
if (response.status === 401) {
showNotification('Сессия истекла. Войдите заново', 'error');
localStorage.removeItem('token');
// Перезагружаем страницу для возврата на экран входа
setTimeout(() => location.reload(), 1000);
return;
}
if (response.status === 403) {
showNotification('Доступ запрещён. Требуются права администратора', 'error');
// Показываем ошибку в таблице
const tbody = document.getElementById('users-table-body');
if (tbody) {
tbody.innerHTML = `
<tr>
<td colspan="6" style="padding: 20px; text-align: center; color: var(--danger);">
🚫 Доступ запрещён. Требуются права администратора.
</td>
</tr>
`;
}
return;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Ошибка загрузки пользователей');
}
const data = await response.json();
// Обновляем статистику
updateUserStats(data.stats);
// Обновляем таблицу
renderUsersTable(data.users);
} catch (error) {
console.error('Ошибка загрузки пользователей:', error);
showNotification(error.message || 'Ошибка загрузки пользователей', 'error');
// Показываем заглушку в таблице
const tbody = document.getElementById('users-table-body');
if (tbody) {
tbody.innerHTML = `
<tr>
<td colspan="6" style="padding: 20px; text-align: center; color: var(--danger);">
${error.message}
</td>
</tr>
`;
}
}
}
/**
* Обновить статистику пользователей
*/
function updateUserStats(stats) {
document.getElementById('stat-total-users').textContent = stats.total || 0;
document.getElementById('stat-active-users').textContent = stats.active || 0;
document.getElementById('stat-banned-users').textContent = stats.banned || 0;
document.getElementById('stat-admin-users').textContent = stats.admins || 0;
}
/**
* Отрисовать таблицу пользователей
*/
function renderUsersTable(users) {
const tbody = document.getElementById('users-table-body');
if (!users || users.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" style="padding: 20px; text-align: center; color: var(--text-muted);">
Пользователи не найдены
</td>
</tr>
`;
return;
}
tbody.innerHTML = users.map(user => {
const roleClass = user.role === 'admin' ? 'user-role-admin' : 'user-role-user';
const roleText = user.role === 'admin' ? '👑 Админ' : '👤 Пользователь';
const statusClass = user.banned ? 'user-status-banned' : 'user-status-active';
const statusText = user.banned ? '🚫 Заблокирован' : '✅ Активен';
const lastSeen = user.lastLogin ? formatDate(user.lastLogin) : 'Никогда';
const ipAddress = user.ipAddress || 'Не известен';
return `
<tr>
<td style="font-weight: 600;">
${user.username}
${user.username === currentUser?.username ? '<small style="color: var(--accent-primary);">(Вы)</small>' : ''}
</td>
<td>
<span class="user-role-badge ${roleClass}">${roleText}</span>
</td>
<td style="font-family: monospace; font-size: 12px;">
${ipAddress}
</td>
<td style="color: var(--text-muted);">
${lastSeen}
</td>
<td>
<span class="user-status-badge ${statusClass}">${statusText}</span>
</td>
<td class="user-actions">
<button class="btn btn-icon btn-primary" onclick='editUser(${JSON.stringify(user)})' title="Редактировать">
✏️
</button>
${user.banned ?
`<button class="btn btn-icon btn-success" onclick="toggleBanUser('${user.username}', false)" title="Разблокировать">
</button>` :
`<button class="btn btn-icon btn-danger" onclick="toggleBanUser('${user.username}', true)" title="Заблокировать">
🚫
</button>`
}
${user.username !== currentUser?.username ?
`<button class="btn btn-icon btn-danger" onclick="deleteUser('${user.username}')" title="Удалить">
🗑️
</button>` : ''
}
</td>
</tr>
`;
}).join('');
}
/**
* Форматировать дату
*/
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Только что';
if (minutes < 60) return `${minutes} мин назад`;
if (hours < 24) return `${hours} ч назад`;
if (days < 7) return `${days} дн назад`;
return date.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
/**
* Редактировать пользователя
*/
function editUser(user) {
document.getElementById('edit-user-username').value = user.username;
document.getElementById('edit-user-role').value = user.role;
document.getElementById('edit-user-status').value = user.banned ? 'banned' : 'active';
document.getElementById('edit-user-password').value = '';
document.getElementById('edit-user-modal').style.display = 'flex';
// Сохраняем данные для обновления
window.editingUser = user;
}
/**
* Закрыть модальное окно редактирования
*/
function closeEditUserModal() {
document.getElementById('edit-user-modal').style.display = 'none';
window.editingUser = null;
}
/**
* Сохранить изменения пользователя
*/
async function saveUserEdit() {
if (!window.editingUser) return;
const username = window.editingUser.username;
const role = document.getElementById('edit-user-role').value;
const status = document.getElementById('edit-user-status').value;
const password = document.getElementById('edit-user-password').value;
try {
const token = localStorage.getItem('authToken');
if (!token) {
showNotification('Необходима авторизация', 'error');
setTimeout(() => location.reload(), 1000);
return;
}
const response = await fetch('/api/admin/user/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
username,
role,
banned: status === 'banned',
password: password || undefined
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Ошибка обновления пользователя');
}
showNotification('Пользователь успешно обновлён', 'success');
closeEditUserModal();
loadUsers();
} catch (error) {
console.error('Ошибка обновления пользователя:', error);
showNotification(error.message || 'Ошибка обновления пользователя', 'error');
}
}
/**
* Заблокировать/разблокировать пользователя
*/
async function toggleBanUser(username, ban) {
const action = ban ? 'заблокировать' : 'разблокировать';
if (!confirm(`Вы уверены, что хотите ${action} пользователя ${username}?`)) {
return;
}
try {
const token = localStorage.getItem('authToken');
if (!token) {
showNotification('Необходима авторизация', 'error');
setTimeout(() => location.reload(), 1000);
return;
}
const response = await fetch('/api/admin/user/ban', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ username, banned: ban })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Ошибка изменения статуса пользователя');
}
showNotification(`Пользователь ${username} ${ban ? 'заблокирован' : 'разблокирован'}`, 'success');
loadUsers();
} catch (error) {
console.error('Ошибка изменения статуса:', error);
showNotification(error.message || 'Ошибка изменения статуса пользователя', 'error');
}
}
/**
* Удалить пользователя
*/
async function deleteUser(username) {
if (!confirm(`Вы уверены, что хотите УДАЛИТЬ пользователя ${username}?\n\nЭто действие необратимо!`)) {
return;
}
// Двойное подтверждение для безопасности
if (!confirm(`ВНИМАНИЕ! Вы действительно хотите удалить ${username}?`)) {
return;
}
try {
const token = localStorage.getItem('authToken');
if (!token) {
showNotification('Необходима авторизация', 'error');
setTimeout(() => location.reload(), 1000);
return;
}
const response = await fetch('/api/admin/user/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ username })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Ошибка удаления пользователя');
}
showNotification(`Пользователь ${username} удалён`, 'success');
loadUsers();
} catch (error) {
console.error('Ошибка удаления:', error);
showNotification(error.message || 'Ошибка удаления пользователя', 'error');
}
}
/**
* Заглушка для функции загрузки логов
*/
function loadLogs() {
console.log('Загрузка логов...');
// Будет реализовано позже
}