2946 lines
106 KiB
JavaScript
2946 lines
106 KiB
JavaScript
/**
|
||
* =============================================================================
|
||
* 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();
|
||
}
|
||
}
|
||
|