/** * ============================================================================= * Texas Hold'em - ИИ Оппонент * 3 уровня сложности: * 1 - Случайный (новичок) * 2 - Тайтово-агрессивный по диапазонам * 3 - Equity-based + блеф-блокеры * ============================================================================= */ const pokerAI = { // Персональности ботов с системными промптами для LLM personalities: [ { name: 'Виктор "Акула"', avatar: '🦈', style: 'aggressive', systemPrompt: `Ты Виктор, опытный профессиональный игрок в покер с прозвищем "Акула". Ты играешь уже 20 лет, у тебя несколько браслетов WSOP. Твой стиль - агрессивный и уверенный. Ты часто подшучиваешь над соперниками, используешь покерный сленг (натс, олл-ин, бэд бит). Отвечай кратко (1-2 предложения), с легкой надменностью победителя. Иногда упоминай свои прошлые победы. Говори на русском языке.` }, { name: 'Анна "Блефер"', avatar: '👩‍💼', style: 'tricky', systemPrompt: `Ты Анна, загадочная женщина-игрок в покер, известная как "Блефер". Ты умна, проницательна и любишь психологические игры. Твой стиль общения - загадочный, с намёками и двусмысленностями. Ты часто говоришь что-то вроде "Посмотрим, кто кого переиграет" или "Интересно...". Отвечай кратко (1-2 предложения), держи интригу. Говори на русском языке.` }, { name: 'Дед Михалыч', avatar: '👴', style: 'oldschool', systemPrompt: `Ты Дед Михалыч, пожилой мудрый игрок в покер, играющий ещё с советских времён. Ты добродушный, любишь рассказывать истории "а вот в наше время...". Используешь старомодные выражения, иногда путаешь покерные термины. Относишься к молодым игрокам снисходительно, но по-доброму. Отвечай кратко (1-2 предложения), с юмором. Говори на русском языке.` }, { name: 'Макс "ГТО"', avatar: '🤓', style: 'mathematical', systemPrompt: `Ты Макс, молодой игрок-математик, помешанный на GTO (Game Theory Optimal). Ты постоянно считаешь odds, equity, EV в уме и любишь об этом говорить. Используешь много технических терминов: "equity realization", "блеф-частота", "диапазон". Немного занудный, но добродушный. Отвечай кратко (1-2 предложения), упоминай математику. Говори на русском языке.` }, { name: 'Катя "Удача"', avatar: '🍀', style: 'lucky', systemPrompt: `Ты Катя, весёлая девушка, которая верит в приметы и удачу. У тебя есть талисман, ты загадываешь желания перед важными раздачами. Очень эмоциональная - радуешься победам, расстраиваешься от проигрышей. Используешь смайлики в речи, говоришь позитивно. Отвечай кратко (1-2 предложения), эмоционально. Говори на русском языке.` }, { name: 'Борис "Молчун"', avatar: '🎭', style: 'silent', systemPrompt: `Ты Борис, молчаливый игрок в покер, известный как "Молчун". Ты говоришь очень мало, каждое слово на вес золота. Твои ответы минималистичны: "Да", "Нет", "Посмотрим", "Хм". Иногда просто многозначительно молчишь (отвечаешь "..."). Отвечай очень кратко (1-3 слова максимум). Говори на русском языке.` }, { name: 'Олег "Тильтер"', avatar: '😤', style: 'tilted', systemPrompt: `Ты Олег, эмоциональный игрок, склонный к тильту. Когда проигрываешь - злишься и обвиняешь удачу, соперников, карты. Когда выигрываешь - становишься чересчур самоуверенным. Часто жалуешься на "рандом", "читеров", "донков". Отвечай кратко (1-2 предложения), эмоционально. Говори на русском языке.` }, { name: 'Ирина "Профи"', avatar: '💎', style: 'professional', systemPrompt: `Ты Ирина, профессиональный онлайн-гриндер, играющая на высоких лимитах. Ты спокойная, рациональная, относишься к покеру как к работе. Даёшь дельные советы, анализируешь розыгрыши. Уважительно относишься к соперникам, но уверена в своих силах. Отвечай кратко (1-2 предложения), профессионально. Говори на русском языке.` } ], // Имена для ботов (для совместимости) botNames: [ 'Алексей', 'Мария', 'Дмитрий', 'Анна', 'Сергей', 'Елена', 'Иван', 'Ольга', 'Николай', 'Татьяна', 'Phil Hellmuth', 'Daniel Negreanu', 'Phil Ivey', 'Vanessa Selbst', 'Fedor Holz' ], /** * Получить случайное имя для бота (используем персональности если LLM включён) */ getRandomName() { return this.botNames[Math.floor(Math.random() * this.botNames.length)]; }, /** * Получить случайную персональность для бота */ getRandomPersonality() { const personality = this.personalities[Math.floor(Math.random() * this.personalities.length)]; return { ...personality }; // Возвращаем копию }, /** * Принять решение на основе уровня ИИ */ makeDecision(player, game) { switch (player.aiLevel) { case 1: return this.level1Decision(player, game); case 2: return this.level2Decision(player, game); case 3: return this.level3Decision(player, game); default: return this.level1Decision(player, game); } }, // ========================================================================= // УРОВЕНЬ 1: Случайный игрок (новичок) // ========================================================================= /** * Случайные решения с небольшой логикой */ level1Decision(player, game) { const actions = game.getAvailableActions(player); const toCall = game.currentBet - player.bet; // Случайный выбор с весами const random = Math.random(); if (toCall === 0) { // Нет ставки if (random < 0.6) { return { action: 'check', amount: 0 }; } else if (random < 0.85) { const betAmount = game.bigBlind * (2 + Math.floor(Math.random() * 3)); return { action: 'bet', amount: Math.min(betAmount, player.chips) }; } else { return { action: 'allin', amount: player.chips }; } } else { // Есть ставка if (random < 0.3) { return { action: 'fold', amount: 0 }; } else if (random < 0.75) { return { action: 'call', amount: toCall }; } else if (random < 0.95 && player.chips > toCall * 2) { const raiseAmount = game.currentBet + game.lastRaiseAmount + Math.floor(Math.random() * game.bigBlind * 3); return { action: 'raise', amount: raiseAmount }; } else { return { action: 'allin', amount: player.chips }; } } }, // ========================================================================= // УРОВЕНЬ 2: Тайтово-агрессивный по диапазонам // ========================================================================= /** * Играет по диапазонам рук */ level2Decision(player, game) { const handStrength = this.evaluatePreflopHand(player.hand); const toCall = game.currentBet - player.bet; const potOdds = toCall / (game.pot + toCall); const position = this.getPosition(player, game); const phase = game.gamePhase; if (phase === 'preflop') { return this.level2Preflop(player, game, handStrength, position); } else { return this.level2Postflop(player, game, position); } }, /** * Префлоп решения для уровня 2 */ level2Preflop(player, game, handStrength, position) { const toCall = game.currentBet - player.bet; // Премиум руки (AA, KK, QQ, AKs) if (handStrength >= 90) { if (toCall === 0 || toCall <= game.bigBlind) { // Рейз 3x return { action: 'raise', amount: game.currentBet + game.bigBlind * 3 }; } else { // 3-бет или 4-бет return { action: 'raise', amount: game.currentBet * 3 }; } } // Сильные руки (JJ, 1010, AQs, AKo) if (handStrength >= 75) { if (toCall === 0) { return { action: 'raise', amount: game.bigBlind * 3 }; } else if (toCall <= game.bigBlind * 4) { return { action: 'call', amount: toCall }; } else if (toCall <= game.bigBlind * 8) { // Иногда коллируем return Math.random() < 0.5 ? { action: 'call', amount: toCall } : { action: 'fold', amount: 0 }; } else { return { action: 'fold', amount: 0 }; } } // Средние руки (99-66, AJs, KQs) if (handStrength >= 55) { if (position === 'late' || position === 'dealer') { if (toCall === 0) { return { action: 'raise', amount: game.bigBlind * 2.5 }; } else if (toCall <= game.bigBlind * 3) { return { action: 'call', amount: toCall }; } } else if (toCall <= game.bigBlind * 2) { return { action: 'call', amount: toCall }; } return { action: 'fold', amount: 0 }; } // Спекулятивные руки (малые пары, suited connectors) if (handStrength >= 35) { if (position === 'late' || position === 'dealer') { if (toCall === 0) { return Math.random() < 0.4 ? { action: 'raise', amount: game.bigBlind * 2 } : { action: 'check', amount: 0 }; } else if (toCall <= game.bigBlind * 2) { return { action: 'call', amount: toCall }; } } else if (toCall <= game.bigBlind) { return { action: 'call', amount: toCall }; } return { action: 'fold', amount: 0 }; } // Слабые руки if (toCall === 0) { return position === 'late' && Math.random() < 0.2 ? { action: 'raise', amount: game.bigBlind * 2 } : { action: 'check', amount: 0 }; } return { action: 'fold', amount: 0 }; }, /** * Постфлоп решения для уровня 2 */ level2Postflop(player, game, position) { const handResult = evaluateHand([...player.hand, ...game.communityCards]); const toCall = game.currentBet - player.bet; const potOdds = toCall / (game.pot + toCall); // Очень сильная рука (сет+) if (handResult.rank >= 4) { if (toCall === 0) { // Вэлью бет const betSize = Math.floor(game.pot * (0.5 + Math.random() * 0.25)); return { action: 'bet', amount: betSize }; } else { // Рейз для вэлью return { action: 'raise', amount: game.currentBet * 2.5 }; } } // Пара топ кикер / две пары if (handResult.rank >= 2) { if (toCall === 0) { return Math.random() < 0.6 ? { action: 'bet', amount: Math.floor(game.pot * 0.5) } : { action: 'check', amount: 0 }; } else if (potOdds < 0.35) { return { action: 'call', amount: toCall }; } else { return Math.random() < 0.3 ? { action: 'call', amount: toCall } : { action: 'fold', amount: 0 }; } } // Дро или оверкарты const hasFlushDraw = this.hasFlushDraw(player.hand, game.communityCards); const hasStraightDraw = this.hasStraightDraw(player.hand, game.communityCards); if (hasFlushDraw || hasStraightDraw) { if (toCall === 0) { // Полублеф return Math.random() < 0.4 ? { action: 'bet', amount: Math.floor(game.pot * 0.5) } : { action: 'check', amount: 0 }; } else if (potOdds < 0.25) { return { action: 'call', amount: toCall }; } } // Слабая рука if (toCall === 0) { return { action: 'check', amount: 0 }; } return { action: 'fold', amount: 0 }; }, // ========================================================================= // УРОВЕНЬ 3: Equity-based + блеф-блокеры // ========================================================================= /** * Продвинутый ИИ с расчётом equity */ level3Decision(player, game) { const phase = game.gamePhase; const position = this.getPosition(player, game); const toCall = game.currentBet - player.bet; if (phase === 'preflop') { return this.level3Preflop(player, game, position); } else { return this.level3Postflop(player, game, position); } }, /** * Префлоп для уровня 3 */ level3Preflop(player, game, position) { const handStrength = this.evaluatePreflopHand(player.hand); const toCall = game.currentBet - player.bet; const stackDepth = player.chips / game.bigBlind; // GTO-подобные диапазоны открытия по позициям const openRanges = { 'early': 85, // Топ 15% 'middle': 70, // Топ 30% 'late': 50, // Топ 50% 'dealer': 40, // Топ 60% 'blind': 55 // Топ 45% }; const openThreshold = openRanges[position] || 60; // Премиум руки - всегда 3-бет if (handStrength >= 92) { if (toCall > game.bigBlind * 10) { return { action: 'allin', amount: player.chips }; } const raiseSize = Math.max(game.currentBet * 3, game.bigBlind * 4); return { action: 'raise', amount: raiseSize }; } // Открытие в позиции if (toCall === 0 || toCall === game.bigBlind) { if (handStrength >= openThreshold) { // Размер открытия зависит от позиции const openSize = position === 'early' ? 3 : 2.5; return { action: toCall === 0 ? 'bet' : 'raise', amount: game.bigBlind * openSize }; } else if (toCall === 0) { return { action: 'check', amount: 0 }; } } // Против рейза if (toCall > game.bigBlind) { const callThreshold = 75 - (position === 'late' ? 10 : 0); const threeBetThreshold = 88; if (handStrength >= threeBetThreshold && Math.random() < 0.7) { return { action: 'raise', amount: game.currentBet * 2.5 }; } else if (handStrength >= callThreshold) { return { action: 'call', amount: toCall }; } else if (handStrength >= 45 && toCall <= game.bigBlind * 3) { // Колл с suited connectors и малыми парами (сетмайнинг) return { action: 'call', amount: toCall }; } } return { action: 'fold', amount: 0 }; }, /** * Постфлоп для уровня 3 */ level3Postflop(player, game, position) { const equity = this.calculateEquity(player.hand, game.communityCards); const toCall = game.currentBet - player.bet; const potOdds = toCall / (game.pot + toCall); const impliedOdds = this.calculateImpliedOdds(player, game); const hasBlockers = this.checkBlockers(player.hand, game.communityCards); // Расчёт EV const callEV = equity * (game.pot + toCall) - (1 - equity) * toCall; // Мы в позиции? const inPosition = position === 'late' || position === 'dealer'; // Сильная рука (> 70% equity) if (equity > 0.7) { if (toCall === 0) { // Вэлью бет с размером зависящим от текстуры борда const betSize = this.calculateOptimalBet(game.pot, equity, game.gamePhase); return { action: 'bet', amount: betSize }; } else { // Рейз для вэлью if (player.chips > game.currentBet * 3) { return { action: 'raise', amount: game.currentBet * 2.5 }; } return { action: 'call', amount: toCall }; } } // Средняя рука (40-70% equity) if (equity > 0.4) { if (toCall === 0) { // Бет для защиты или вэлью if (Math.random() < 0.5) { return { action: 'bet', amount: Math.floor(game.pot * 0.5) }; } return { action: 'check', amount: 0 }; } else if (equity > potOdds * 1.2) { return { action: 'call', amount: toCall }; } } // Дро или блеф с блокерами if (equity > 0.25 || (hasBlockers && Math.random() < 0.3)) { if (toCall === 0 && inPosition && Math.random() < 0.35) { // Полублеф return { action: 'bet', amount: Math.floor(game.pot * 0.6) }; } else if (equity + impliedOdds > potOdds) { return { action: 'call', amount: toCall }; } } // Чистый блеф на ривере с блокерами if (game.gamePhase === 'river' && hasBlockers && toCall === 0 && Math.random() < 0.15) { return { action: 'bet', amount: Math.floor(game.pot * 0.75) }; } // Слабая рука if (toCall === 0) { return { action: 'check', amount: 0 }; } return { action: 'fold', amount: 0 }; }, // ========================================================================= // ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ // ========================================================================= /** * Оценка префлоп руки (0-100) */ evaluatePreflopHand(hand) { if (!hand || hand.length !== 2) return 0; const [c1, c2] = hand; const highCard = Math.max(c1.value, c2.value); const lowCard = Math.min(c1.value, c2.value); const suited = c1.suit === c2.suit; const gap = highCard - lowCard; const isPair = c1.value === c2.value; let score = 0; // Пары if (isPair) { score = 50 + c1.value * 3; if (c1.value >= 10) score += 10; if (c1.value >= 13) score += 10; } else { // Непарные руки score = highCard * 2 + lowCard; // Бонус за одномастные if (suited) score += 8; // Бонус за коннекторы if (gap === 1) score += 6; else if (gap === 2) score += 3; else if (gap >= 4) score -= gap; // Бонус за картинки if (highCard >= 12 && lowCard >= 10) score += 15; if (highCard === 14) score += 10; } return Math.min(100, Math.max(0, score)); }, /** * Получить позицию игрока */ getPosition(player, game) { const playerIndex = game.players.indexOf(player); const dealerIndex = game.dealerIndex; const numPlayers = game.players.length; const relativePosition = (playerIndex - dealerIndex + numPlayers) % numPlayers; if (player.isDealer) return 'dealer'; if (player.isSmallBlind || player.isBigBlind) return 'blind'; if (relativePosition <= numPlayers * 0.33) return 'early'; if (relativePosition <= numPlayers * 0.66) return 'middle'; return 'late'; }, /** * Расчёт equity методом Монте-Карло */ calculateEquity(hand, communityCards, iterations = 500) { if (!hand || hand.length !== 2) return 0; let wins = 0; let ties = 0; // Создаём колоду без известных карт const knownCards = new Set([ ...hand.map(c => `${c.rank}${c.suit}`), ...communityCards.map(c => `${c.rank}${c.suit}`) ]); const deck = []; for (const suit of SUITS) { for (const rank of RANKS) { const key = `${rank}${suit}`; if (!knownCards.has(key)) { deck.push(new Card(suit, rank)); } } } for (let i = 0; i < iterations; i++) { // Перемешиваем колоду const shuffled = [...deck].sort(() => Math.random() - 0.5); // Добавляем карты до 5 const cardsNeeded = 5 - communityCards.length; const simBoard = [...communityCards, ...shuffled.slice(0, cardsNeeded)]; // Рука оппонента const oppHand = shuffled.slice(cardsNeeded, cardsNeeded + 2); // Оценка рук const myHand = evaluateHand([...hand, ...simBoard]); const oppHandResult = evaluateHand([...oppHand, ...simBoard]); const comparison = compareHandResults(myHand, oppHandResult); if (comparison > 0) wins++; else if (comparison === 0) ties++; } return (wins + ties * 0.5) / iterations; }, /** * Проверка на флеш-дро */ hasFlushDraw(hand, community) { const allCards = [...hand, ...community]; const suitCounts = {}; for (const card of allCards) { suitCounts[card.suit] = (suitCounts[card.suit] || 0) + 1; } return Object.values(suitCounts).some(count => count === 4); }, /** * Проверка на стрит-дро */ hasStraightDraw(hand, community) { const allCards = [...hand, ...community]; const values = [...new Set(allCards.map(c => c.value))].sort((a, b) => a - b); // Проверяем гатшот или OESD for (let i = 0; i < values.length - 3; i++) { const window = values.slice(i, i + 4); const gaps = window[3] - window[0]; if (gaps <= 4) return true; } return false; }, /** * Проверка блокеров */ checkBlockers(hand, community) { // Блокеры для флеша const allCards = [...hand, ...community]; const suitCounts = {}; for (const card of allCards) { suitCounts[card.suit] = (suitCounts[card.suit] || 0) + 1; } // Если на борде 3+ одной масти, проверяем блокеры for (const [suit, count] of Object.entries(suitCounts)) { if (count >= 3) { // Проверяем, есть ли у нас туз или король этой масти for (const card of hand) { if (card.suit === suit && card.value >= 13) { return true; } } } } // Блокеры для стрита (тузы, короли на коннекторных бордах) const boardValues = community.map(c => c.value).sort((a, b) => a - b); if (boardValues.length >= 3) { const isConnected = boardValues[boardValues.length - 1] - boardValues[0] <= 4; if (isConnected) { for (const card of hand) { if (card.value >= 13) return true; } } } return false; }, /** * Расчёт implied odds */ calculateImpliedOdds(player, game) { // Упрощённый расчёт implied odds const remainingStreets = { 'flop': 2, 'turn': 1, 'river': 0 }[game.gamePhase] || 0; const avgOppStack = game.players .filter(p => !p.folded && p.id !== player.id) .reduce((sum, p) => sum + p.chips, 0) / game.getActivePlayers().length; // Оцениваем, сколько можем выиграть на следующих улицах const impliedValue = avgOppStack * 0.15 * remainingStreets; return impliedValue / (game.pot + impliedValue); }, /** * Расчёт оптимального размера ставки */ calculateOptimalBet(pot, equity, phase) { // Чем выше equity и чем позже фаза, тем больше ставка const baseMultiplier = { 'flop': 0.5, 'turn': 0.6, 'river': 0.7 }[phase] || 0.5; const equityBonus = (equity - 0.5) * 0.5; const finalMultiplier = Math.min(1, baseMultiplier + equityBonus); return Math.floor(pot * finalMultiplier); } }; // ============================================================================= // LLM КЛИЕНТ ДЛЯ ЧАТА // ============================================================================= const llmChat = { // Кэш истории сообщений для каждого бота messageHistory: new Map(), // Максимум сообщений в истории maxHistory: 10, /** * Отправить сообщение и получить ответ от LLM */ async chat(botId, botPersonality, userMessage, gameContext = {}) { const settings = this.getSettings(); if (!settings.llmEnabled) { return this.getFallbackResponse(botPersonality, userMessage, gameContext); } try { // Получаем или создаём историю для этого бота if (!this.messageHistory.has(botId)) { this.messageHistory.set(botId, []); } const history = this.messageHistory.get(botId); // Формируем контекст игры const gameContextStr = this.buildGameContext(gameContext); // Системный промпт с контекстом const systemPrompt = `${botPersonality.systemPrompt} Текущая ситуация в игре: ${gameContextStr} Отвечай на сообщения игрока в соответствии со своей личностью.`; // Добавляем сообщение пользователя в историю history.push({ role: 'user', content: userMessage }); // Ограничиваем историю while (history.length > this.maxHistory) { history.shift(); } // Отправляем запрос к LLM const response = await this.sendToLLM(systemPrompt, history, settings); // Добавляем ответ в историю if (response) { history.push({ role: 'assistant', content: response }); } return response || this.getFallbackResponse(botPersonality, userMessage, gameContext); } catch (error) { console.error('LLM ошибка:', error); return this.getFallbackResponse(botPersonality, userMessage, gameContext); } }, /** * Отправить запрос к LLM API */ async sendToLLM(systemPrompt, messages, settings) { const { provider, apiUrl, model, apiKey } = settings; let url, body, headers; switch (provider) { case 'ollama': url = `${apiUrl}/api/chat`; body = { model: model, messages: [ { role: 'system', content: systemPrompt }, ...messages ], stream: false }; headers = { 'Content-Type': 'application/json' }; break; case 'lmstudio': url = `${apiUrl}/v1/chat/completions`; body = { model: model, messages: [ { role: 'system', content: systemPrompt }, ...messages ], max_tokens: 150, temperature: 0.8 }; headers = { 'Content-Type': 'application/json' }; break; case 'openai': url = 'https://api.openai.com/v1/chat/completions'; body = { model: model || 'gpt-3.5-turbo', messages: [ { role: 'system', content: systemPrompt }, ...messages ], max_tokens: 150, temperature: 0.8 }; headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }; break; default: throw new Error('Неизвестный провайдер LLM'); } const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); // Извлекаем текст ответа в зависимости от провайдера if (provider === 'ollama') { return data.message?.content || ''; } else { return data.choices?.[0]?.message?.content || ''; } }, /** * Построить контекст игры для LLM */ buildGameContext(ctx) { if (!ctx || Object.keys(ctx).length === 0) { return 'Игра ещё не началась.'; } let context = ''; if (ctx.phase) { const phases = { 'preflop': 'Префлоп', 'flop': 'Флоп', 'turn': 'Тёрн', 'river': 'Ривер', 'showdown': 'Вскрытие' }; context += `Фаза: ${phases[ctx.phase] || ctx.phase}\n`; } if (ctx.pot !== undefined) { context += `Банк: ${ctx.pot} фишек\n`; } if (ctx.myChips !== undefined) { context += `Мои фишки: ${ctx.myChips}\n`; } if (ctx.lastAction) { context += `Последнее действие игрока: ${ctx.lastAction}\n`; } if (ctx.communityCards && ctx.communityCards.length > 0) { context += `Общие карты: ${ctx.communityCards.join(', ')}\n`; } return context || 'Идёт игра.'; }, /** * Запасной ответ если LLM недоступен */ getFallbackResponse(personality, userMessage, gameContext) { const style = personality?.style || 'default'; const responses = { aggressive: [ 'Посмотрим, что ты можешь.', 'Удача любит смелых!', 'Это только начало.', 'Не расслабляйся.', 'Интересно...' ], tricky: [ 'Может быть... 🤔', 'Посмотрим...', 'Интересный ход мыслей.', 'Загадка, не правда ли?', 'Время покажет.' ], oldschool: [ 'А вот в наше время...', 'Эх, молодёжь!', 'Ничего-ничего.', 'Бывает и хуже.', 'Терпение, друг мой.' ], mathematical: [ 'По EV это нормально.', 'Статистика на моей стороне.', 'Интересный спот.', 'Надо посчитать...', '+EV решение.' ], lucky: [ 'Удачи! 🍀', 'Верю в лучшее! ✨', 'Всё будет хорошо! 😊', 'Загадаю желание! 🌟', 'Пусть карта ляжет!' ], silent: [ '...', 'Хм.', 'Да.', 'Нет.', 'Посмотрим.' ], tilted: [ 'Опять этот рандом!', 'Невезение...', 'Ну конечно!', 'Просто невероятно!', 'Как всегда...' ], professional: [ 'Хороший спот.', 'Стандартная игра.', 'Разумно.', 'Интересное решение.', 'Принято.' ], default: [ 'Удачи!', 'Интересно...', 'Посмотрим.', 'Хм...', 'Неплохо!' ] }; const options = responses[style] || responses.default; return options[Math.floor(Math.random() * options.length)]; }, /** * Получить настройки LLM */ getSettings() { const saved = localStorage.getItem('pokerSettings'); if (!saved) { return { llmEnabled: false }; } const settings = JSON.parse(saved); return { llmEnabled: settings.llmEnabled || false, provider: settings.llmProvider || 'ollama', apiUrl: settings.llmApiUrl || 'http://localhost:11434', model: settings.llmModel || 'llama3.2', apiKey: settings.llmApiKey || '' }; }, /** * Проверить подключение к LLM */ async testConnection() { const settings = this.getSettings(); if (!settings.llmEnabled) { return { success: false, error: 'LLM чат отключён в настройках' }; } try { const testMessage = [{ role: 'user', content: 'Привет! Скажи "работает" если ты меня слышишь.' }]; const response = await this.sendToLLM('Ты тестовый бот. Отвечай кратко.', testMessage, settings); if (response) { return { success: true, response }; } else { return { success: false, error: 'Пустой ответ от LLM' }; } } catch (error) { return { success: false, error: error.message }; } }, /** * Очистить историю чата для бота */ clearHistory(botId) { this.messageHistory.delete(botId); }, /** * Очистить всю историю */ clearAllHistory() { this.messageHistory.clear(); } }; // Для совместимости с серверной и клиентской частью if (typeof module !== 'undefined' && module.exports) { module.exports = { pokerAI, llmChat }; }