/**
* =============================================================================
* 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 = '
Загрузка комнат...
';
resolve();
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleServerMessage(message);
};
ws.onclose = () => {
console.log('Отключено от сервера');
wsConnecting = false;
ws = null;
document.getElementById('room-list').innerHTML = 'Соединение потеряно.
';
};
ws.onerror = (error) => {
console.error('WebSocket ошибка:', error);
wsConnecting = false;
document.getElementById('room-list').innerHTML = 'Ошибка подключения к серверу.
';
reject(error);
};
} catch (error) {
console.error('Ошибка создания WebSocket:', error);
wsConnecting = false;
reject(error);
}
});
}
/**
* Обработка сообщений от сервера
*/
function handleServerMessage(message) {
switch (message.type) {
case 'room_list':
renderRoomList(message.rooms);
break;
case 'room_joined':
currentPlayerId = message.playerId;
currentRoomId = message.roomId;
showRoomLobby(message.room);
break;
case 'player_joined':
updateLobbyPlayers(message.room);
addChatMessage('lobby', null, `${message.player.name} присоединился`, true);
break;
case 'player_left':
case 'player_disconnected':
updateLobbyPlayers(message.room);
break;
case 'game_started':
isMultiplayer = true;
currentGamePhase = message.room.gamePhase;
showScreen('game-screen');
updateGameUIFromServer(message.room);
break;
case 'game_update':
currentGamePhase = message.room.gamePhase;
updateGameUIFromServer(message.room);
if (message.lastAction) {
playSound(message.lastAction.action);
}
break;
case 'chat':
if (document.getElementById('game-screen').classList.contains('active')) {
addChatMessage('game', message.playerName, message.message);
} else {
addChatMessage('lobby', message.playerName, message.message);
}
playSound('message');
break;
case 'error':
showNotification(message.message, 'error');
break;
}
}
/**
* Отрисовка списка комнат
*/
function renderRoomList(rooms) {
const container = document.getElementById('room-list');
if (rooms.length === 0) {
container.innerHTML = 'Нет доступных комнат
';
return;
}
container.innerHTML = rooms.map(room => `
${room.name}
Блайнды: ${room.smallBlind}/${room.bigBlind}
${room.players}/${room.maxPlayers}
`).join('');
}
/**
* Обновить список комнат
*/
function refreshRooms() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'get_rooms' }));
} else {
connectWebSocket();
}
}
/**
* Создать комнату
*/
async function createRoom() {
// Проверяем и устанавливаем соединение
if (!ws || ws.readyState !== WebSocket.OPEN) {
try {
await connectWebSocket();
} catch (e) {
showNotification('Не удалось подключиться к серверу', 'error');
return;
}
}
// Повторная проверка после await
if (!ws || ws.readyState !== WebSocket.OPEN) {
showNotification('Нет соединения с сервером', 'error');
return;
}
const playerName = document.getElementById('mp-player-name').value || 'Игрок';
const roomName = document.getElementById('room-name').value || `Комната ${playerName}`;
const [smallBlind, bigBlind] = selectedOptions['blinds'].split('/').map(Number);
const maxPlayers = parseInt(selectedOptions['max-players']);
localStorage.setItem('playerName', playerName);
ws.send(JSON.stringify({
type: 'create_room',
roomName,
playerName,
smallBlind,
bigBlind,
maxPlayers
}));
}
/**
* Присоединиться к комнате
*/
async function joinRoom(roomId) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
try {
await connectWebSocket();
} catch (e) {
showNotification('Не удалось подключиться к серверу', 'error');
return;
}
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
showNotification('Нет соединения с сервером', 'error');
return;
}
const playerName = document.getElementById('mp-player-name').value || 'Игрок';
localStorage.setItem('playerName', playerName);
ws.send(JSON.stringify({
type: 'join_room',
roomId,
playerName
}));
}
/**
* Показать лобби комнаты
*/
function showRoomLobby(room) {
showScreen('room-lobby');
document.getElementById('lobby-room-name').textContent = room.name;
document.getElementById('lobby-blinds').textContent = `${room.smallBlind}/${room.bigBlind}`;
// Генерируем и отображаем ссылку на комнату
updateRoomLink(room.id);
updateLobbyPlayers(room);
}
/**
* Обновить игроков в лобби
*/
function updateLobbyPlayers(room) {
const container = document.getElementById('lobby-players');
container.innerHTML = room.players.map((player, index) => `
${player.name.charAt(0).toUpperCase()}
${player.name}
`).join('');
document.getElementById('lobby-player-count').textContent =
`${room.players.length}/${room.maxPlayers || 6}`;
const startBtn = document.getElementById('start-game-btn');
startBtn.style.display = room.players[0]?.id === currentPlayerId ? 'block' : 'none';
startBtn.disabled = room.players.length < 2;
// Обновляем ссылку на комнату (если 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 = `
${cardData.rank}
${symbols[cardData.suit]}
`;
return card;
}
/**
* Отрисовка игроков за столом
*/
function renderPlayers(players, currentIndex, myPlayerId) {
const container = document.getElementById('player-positions');
container.innerHTML = '';
// Позиции для ставок (относительно позиции игрока)
const betPositions = [
{ top: '-40px', left: '50%' },
{ top: '-30px', left: '80%' },
{ top: '50%', left: '100%' },
{ top: '100%', left: '50%' },
{ top: '50%', left: '-30%' },
{ top: '-30px', left: '20%' }
];
players.forEach((player, index) => {
// Пропускаем текущего игрока (он отображается снизу)
if (player.id === myPlayerId) return;
const seat = document.createElement('div');
seat.className = 'player-seat';
seat.dataset.position = index;
const isCurrentTurn = index === currentIndex;
const playerBox = document.createElement('div');
playerBox.className = `player-box ${player.folded ? 'folded' : ''} ${isCurrentTurn ? 'current-turn' : ''}`;
playerBox.innerHTML = `
${player.name}
${player.chips}
${player.lastAction ? `${player.lastAction}
` : ''}
`;
// Определяем, нужно ли показывать карты
// Показываем карты только на showdown и только не фолднувшим игрокам
const gamePhase = isMultiplayer ? currentGamePhase : (game ? game.gamePhase : 'waiting');
const showCards = gamePhase === 'showdown' && !player.folded;
// Карты игрока (мини)
if (showCards && player.hand && player.hand.length > 0) {
const cardsDiv = document.createElement('div');
cardsDiv.className = 'player-cards-mini';
player.hand.forEach(card => {
const cardEl = card.toHTML ? card.toHTML(true) : createCardElement(card, true);
cardsDiv.appendChild(cardEl);
});
playerBox.appendChild(cardsDiv);
} else if ((player.hasCards || (player.hand && player.hand.length > 0)) && !player.folded) {
// Карты рубашкой (не показываем карты соперников до showdown)
const cardsDiv = document.createElement('div');
cardsDiv.className = 'player-cards-mini';
cardsDiv.innerHTML = `
`;
playerBox.appendChild(cardsDiv);
}
seat.appendChild(playerBox);
// Позиционные маркеры
if (player.isDealer) {
const dealerBtn = document.createElement('div');
dealerBtn.className = 'dealer-button';
dealerBtn.textContent = 'D';
dealerBtn.style.cssText = 'bottom: -35px; left: 50%; transform: translateX(-50%);';
seat.appendChild(dealerBtn);
}
if (player.isSmallBlind || player.isBigBlind) {
const blindIndicator = document.createElement('div');
blindIndicator.className = `blind-indicator ${player.isSmallBlind ? 'sb' : 'bb'}`;
blindIndicator.textContent = player.isSmallBlind ? 'SB' : 'BB';
blindIndicator.style.cssText = 'bottom: -35px; right: 0;';
seat.appendChild(blindIndicator);
}
// Ставка игрока
if (player.bet > 0) {
const betDisplay = document.createElement('div');
betDisplay.className = 'player-bet-display';
betDisplay.textContent = player.bet;
betDisplay.style.cssText = `${betPositions[index % 6].top}; left: ${betPositions[index % 6].left};`;
seat.appendChild(betDisplay);
}
container.appendChild(seat);
});
// Применяем стиль рубашки к новым картам
applyCardBackToNewCards();
}
/**
* Применить стиль рубашки к новым картам на столе
*/
function applyCardBackToNewCards() {
const style = settings.cardBackStyle || 'default';
if (style === 'custom') {
const url = localStorage.getItem('customCardBack');
if (url) {
document.querySelectorAll('.card-back:not(.styled)').forEach(card => {
card.classList.add('style-custom', 'styled');
card.style.backgroundImage = `url(${url})`;
card.style.backgroundSize = 'cover';
card.style.backgroundPosition = 'center';
});
}
} else {
document.querySelectorAll('.card-back:not(.styled)').forEach(card => {
card.classList.add(`style-${style}`, 'styled');
});
}
}
/**
* Отрисовка руки игрока
*/
function renderPlayerHand(hand, communityCards) {
const container = document.getElementById('player-cards');
// Проверяем, изменились ли карты
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 = `
`;
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 = `
+${winner.amount}
`;
if (winner.hand) {
detailsHTML += `${winner.hand.name}
`;
}
if (result.hands && result.hands.length > 1) {
detailsHTML += '';
result.hands.forEach(h => {
detailsHTML += `
${h.player.name}: ${h.hand.name}
`;
});
detailsHTML += '
';
}
details.innerHTML = detailsHTML;
modal.classList.add('active');
// Обновляем лидерборд
updateLeaderboard(result);
// Обновляем статистику сессии
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 = `
${sender}: печатает...
${time}
`;
} else {
msgDiv.innerHTML = `
${sender}: ${message}
${time}
`;
}
}
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 = 'Нет записей
';
return;
}
container.innerHTML = leaderboard.slice(0, 10).map((entry, index) => {
const rankClass = index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? 'bronze' : '';
return `
${index + 1}
${entry.name}
Игр: ${entry.gamesPlayed} | Побед: ${entry.handsWon} | Макс. банк: ${entry.biggestPot}
${entry.totalWinnings}
`;
}).join('');
}
/**
* Переключить вкладку лидерборда
*/
function switchLeaderboardTab(tab) {
const parent = document.querySelector('.leaderboard-tabs');
parent.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
if (tab === 'global') {
document.getElementById('leaderboard-list').innerHTML =
'Глобальный лидерборд недоступен (требуется backend)
';
} else {
renderLeaderboard();
}
}
/**
* Очистить лидерборд
*/
function clearLeaderboard() {
if (confirm('Вы уверены, что хотите очистить таблицу лидеров?')) {
leaderboard = [];
localStorage.removeItem('pokerLeaderboard');
renderLeaderboard();
showNotification('Таблица лидеров очищена', 'success');
}
}
// =============================================================================
// УТИЛИТЫ
// =============================================================================
/**
* Выйти из игры
*/
function leaveGame() {
if (confirm('Вы уверены, что хотите выйти?')) {
if (isMultiplayer) {
leaveRoom();
}
game = null;
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 = `
${card.rank}
${SUIT_SYMBOLS[card.suit]}
`;
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 = '⏳ Тестирование...
';
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 = `
✅ Тест успешен!
Тестовая ситуация:
🎴 Флоп: K♠ Q♥ 7♦
🃏 Карты бота: A♠ A♥
💰 Банк: 150 | Ставка: 50
💬 Вопрос: "${testMessage}"
Ответ бота:
"${response}"
`;
} catch (error) {
contentDiv.innerHTML = `
❌ Ошибка теста:
${error.message}
`;
}
}
/**
* Тестировать промпт бота (старая функция, оставлена для совместимости)
*/
async function testBotPrompt() {
const resultDiv = document.getElementById('prompt-test-result');
const contentDiv = document.getElementById('test-result-content');
resultDiv.style.display = 'block';
contentDiv.innerHTML = 'Генерация тестового ответа...
';
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 = `
⚠️ LLM чат отключён
Для тестирования промпта необходимо включить LLM в настройках.
Симуляция ответа с текущим промптом:
"${getFallbackResponse(style, testMessage, testContext)}"
`;
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 = `
✅ Тестовый ответ сгенерирован:
"${botResponse}"
Контекст: Флоп [A♠ K♥ 7♦], банк 150, игрок сказал "У меня хорошие карты!"
`;
} catch (error) {
console.error('Ошибка тестирования:', error);
contentDiv.innerHTML = `
❌ Ошибка тестирования
${error.message}
Симуляция ответа:
"${getFallbackResponse(style, testMessage, testContext)}"
`;
}
}
/**
* Получить запасной ответ для стиля
*/
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();
}
}