From c3ace2834d758223be5e093fa2d45b8e1eb4de4a Mon Sep 17 00:00:00 2001 From: ur002 Date: Sun, 1 Feb 2026 12:20:36 +0300 Subject: [PATCH] Add bot personality handling and emotional reactions after hands --- public/ai.js | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ public/main.js | 123 +++++++++++++++++++++++++++--- 2 files changed, 313 insertions(+), 11 deletions(-) diff --git a/public/ai.js b/public/ai.js index 544cd56..6bc1a14 100644 --- a/public/ai.js +++ b/public/ai.js @@ -1076,6 +1076,207 @@ ${gameContextStr} this.messageHistory.delete(botId); }, + /** + * Очистить всю историю + */ + clearAllHistory() { + this.messageHistory.clear(); + }, + + /** + * Найти бота по имени (поддерживает уменьшительно-ласкательные формы) + */ + findBotByName(name, allBots) { + if (!name || !allBots) return null; + + const searchName = name.toLowerCase().trim(); + + // Словарь уменьшительных форм имён + const nameVariants = { + 'александр': ['саша', 'сань', 'санёк', 'шура', 'алекс'], + 'алексей': ['алёша', 'лёша', 'лёх', 'лёха', 'алекс'], + 'дмитрий': ['дима', 'дим', 'димон', 'митя'], + 'сергей': ['серёжа', 'серёга', 'сергей', 'серый'], + 'иван': ['ваня', 'вань', 'ванёк'], + 'мария': ['маша', 'маш', 'машенька', 'мари'], + 'анна': ['аня', 'ань', 'анечка', 'анют'], + 'елена': ['лена', 'лён', 'леночка', 'алёна'], + 'ольга': ['оля', 'оль', 'олечка'], + 'татьяна': ['таня', 'тань', 'танюша'], + 'николай': ['коля', 'коль', 'колян'], + 'виктор': ['витя', 'вить', 'витёк', 'витюша'], + 'борис': ['боря', 'бор', 'борька'], + 'максим': ['макс', 'максик', 'максюша'], + 'ирина': ['ира', 'ирочка', 'иришка'], + 'екатерина': ['катя', 'кать', 'катюша', 'катерина'], + 'олег': ['олежка', 'лёг', 'лёха'], + 'михаил': ['миша', 'миш', 'мишка', 'михалыч'] + }; + + // Ищем прямое совпадение + for (const bot of allBots) { + const botName = bot.name.toLowerCase(); + + // Проверяем полное имя + if (botName.includes(searchName) || searchName.includes(botName.split(' ')[0])) { + return bot; + } + + // Проверяем прозвище в кавычках (например, "Акула") + const nicknameMatch = bot.name.match(/"([^"]+)"/); + if (nicknameMatch && nicknameMatch[1].toLowerCase().includes(searchName)) { + return bot; + } + + // Проверяем уменьшительные формы + const firstName = botName.split(' ')[0]; + for (const [full, variants] of Object.entries(nameVariants)) { + if (firstName.includes(full) || variants.some(v => firstName.includes(v))) { + if (variants.some(v => searchName.includes(v)) || searchName.includes(full)) { + return bot; + } + } + } + } + + return null; + }, + + /** + * Определить, обращается ли сообщение к конкретному боту + */ + detectBotMention(message, allBots) { + if (!message || !allBots) return null; + + const msg = message.toLowerCase(); + + // Паттерны обращения + const patterns = [ + /^([а-яёА-ЯЁ]+)[,!]?\s+/, // "Имя, ..." + /\s([а-яёА-ЯЁ]+)[,!]?\s+/, // "... Имя, ..." + /@([а-яёА-ЯЁ]+)/, // "@Имя" + ]; + + for (const pattern of patterns) { + const match = msg.match(pattern); + if (match) { + const name = match[1]; + const bot = this.findBotByName(name, allBots); + if (bot) return bot; + } + } + + // Ищем любое имя бота в сообщении + for (const bot of allBots) { + const firstName = bot.name.split(' ')[0].toLowerCase(); + if (msg.includes(firstName)) { + return bot; + } + } + + return null; + }, + + /** + * Сгенерировать эмоциональную реакцию на результат раздачи + */ + async generateEmotionalReaction(bot, botPersonality, isWin, potSize, wasAllIn = false, gameContext = {}) { + const settings = this.getSettings(); + + // Формируем контекст для LLM + const situation = isWin + ? `Ты только что ВЫИГРАЛ раздачу и забрал банк ${potSize} фишек!${wasAllIn ? ' Это был олл-ин!' : ''}` + : `Ты только что ПРОИГРАЛ раздачу.${wasAllIn ? ' Это был олл-ин!' : ''}`; + + const emotionalPrompt = `${botPersonality.systemPrompt} + +СИТУАЦИЯ: ${situation} + +Вырази свою эмоцию ОДНИМ коротким предложением (максимум 5-7 слов). +${isWin ? 'Покажи радость, удовлетворение или самоуверенность.' : 'Покажи разочарование, досаду или фатализм.'} +Говори как настоящий игрок за столом, кратко и эмоционально.`; + + // Если LLM включён, используем его + if (settings.llmEnabled && typeof this.sendToLLM === 'function') { + try { + const messages = [{ role: 'user', content: 'Вырази эмоцию' }]; + const response = await this.sendToLLM(emotionalPrompt, messages, settings); + if (response) return response; + } catch (error) { + console.error('Ошибка генерации эмоции:', error); + } + } + + // Запасные эмоциональные реакции + return this.getFallbackEmotion(botPersonality.style, isWin, wasAllIn); + }, + + /** + * Запасные эмоциональные реакции + */ + getFallbackEmotion(style, isWin, wasAllIn) { + const emotions = { + aggressive: { + win: ['Вот это покер! 🔥', 'Лёгкие деньги!', 'Кто сомневался?', 'Натс!', 'Спасибо за фишки!'], + winAllIn: ['ALL-IN И ПОБЕДА! 💪', 'Вот так мы и играем!', 'Поймал тебя!', 'Читаю как книгу!'], + lose: ['Невезуха...', 'Ну ничего, отыграюсь', 'Бывает', 'Донк-бит'], + loseAllIn: ['СЕРЬЁЗНО?! 😤', 'Этого не может быть!', 'Невероятный сак-аут!', 'Рандом...'] + }, + tricky: { + win: ['Как я и думала... 😏', 'Всё по плану', 'Интересная раздача', 'Хм, неплохо'], + winAllIn: ['А вы не ожидали? 😌', 'Блеф или нет? 😏', 'Загадка разгадана'], + lose: ['Ну что же...', 'Интересно сыграли', 'Посмотрим дальше', 'Занятно'], + loseAllIn: ['Надо же... 😔', 'Не сегодня', 'Карты решили иначе'] + }, + oldschool: { + win: ['Эх, молодёжь! 😊', 'Старость не радость!', 'Опыт решает', 'Как в старые добрые!'], + winAllIn: ['А я ведь говорил! 👴', 'Вот так-то!', 'Старая школа!'], + lose: ['Бывает, бывает...', 'Ну ничего, ещё поиграем', 'Эх...', 'Молодец'], + loseAllIn: ['Ох-хо-хо... 😓', 'Надо же...', 'Эх, невезуха'] + }, + mathematical: { + win: ['Матожидание сработало!', '+EV решение!', 'По GTO всё верно', 'Расчёт точен'], + winAllIn: ['Equity на моей стороне!', 'Математика не врёт! 📊', 'Посчитано верно!'], + lose: ['Стандартная дисперсия', 'Математически верно сыграл', '-EV спот', 'Variance'], + loseAllIn: ['Suck out... 😑', 'Cooler', 'Статистическая погрешность'] + }, + lucky: { + win: ['Ура! 🍀✨', 'Я верила! 😊', 'Удача со мной!', 'Талисман сработал! 💫'], + winAllIn: ['YEEEES! 🎉🍀', 'Я знала! ✨', 'Карта пришла! 💚'], + lose: ['Эх... 😔', 'Не повезло...', 'В следующий раз!', 'Загадаю ещё'], + loseAllIn: ['Нееет! 😭', 'Почему?! 💔', 'Невезение... 😢'] + }, + silent: { + win: ['Хм.', 'Неплохо.', '...', 'Да.'], + winAllIn: ['...!', 'Так.', 'Хм.'], + lose: ['...', 'Ну да.', 'Бывает.'], + loseAllIn: ['...', 'Хм.', 'Да уж.'] + }, + tilted: { + win: ['Наконец-то!', 'Уже пора!', 'О чудо!', 'Ну наконец!'], + winAllIn: ['А ТО! 😤', 'Вот теперь да!', 'Заслуженно!'], + lose: ['Опять?! 😠', 'Как всегда!', 'Рандом!', 'Не верю!'], + loseAllIn: ['СНОВА ЭТОТ РАНДОМ! 😡', 'КАК ТАК?!', 'ЧИТЕР!', 'Я в шоке...'] + }, + professional: { + win: ['GG', 'Хорошая раздача', 'Профит', 'Неплохо'], + winAllIn: ['Стандартный выигрыш', 'Всё верно', 'GG'], + lose: ['Стандарт', 'NH', 'GG', 'Окей'], + loseAllIn: ['Bad beat', 'Cooler', 'Дисперсия', 'GG WP'] + } + }; + + const styleEmotions = emotions[style] || emotions.professional; + + let pool; + if (isWin && wasAllIn) pool = styleEmotions.winAllIn; + else if (isWin) pool = styleEmotions.win; + else if (!isWin && wasAllIn) pool = styleEmotions.loseAllIn; + else pool = styleEmotions.lose; + + return pool[Math.floor(Math.random() * pool.length)]; + }, + /** * Очистить всю историю */ diff --git a/public/main.js b/public/main.js index aae9b91..eefc2a8 100644 --- a/public/main.js +++ b/public/main.js @@ -19,6 +19,7 @@ let leaderboard = []; // Таблица лидеров let soundEnabled = true; // Звук включён let currentGamePhase = 'waiting'; // Текущая фаза игры для мультиплеера let wsConnecting = false; // Флаг подключения WebSocket +let botPersonalities = null; // Персональности ботов (загружается из pokerAI) // Выбранные опции const selectedOptions = { @@ -51,6 +52,14 @@ document.addEventListener('DOMContentLoaded', () => { initSounds(); loadCardBackSettings(); + // Загружаем персональности ботов + if (typeof pokerAI !== 'undefined' && pokerAI.personalities) { + botPersonalities = {}; + pokerAI.personalities.forEach(p => { + botPersonalities[p.style] = p; + }); + } + // Восстанавливаем имя игрока const savedName = localStorage.getItem('playerName'); if (savedName) { @@ -191,17 +200,32 @@ function startSinglePlayer() { game.addPlayer(player); currentPlayerId = player.id; - // Добавляем ботов - const personalityKeys = typeof botPersonalities !== 'undefined' - ? Object.keys(botPersonalities) - : ['professional', 'aggressive', 'mathematical']; + // Добавляем ботов с персональностями + 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++) { - const botName = pokerAI.getRandomName(); - const bot = new Player(`bot_${i}`, botName, startingStack, true, aiDifficulty); + let botName, personalityId, personality; - // Присваиваем случайную личность боту - bot.personalityId = personalityKeys[i % personalityKeys.length]; + 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); } @@ -1006,12 +1030,82 @@ function onHandEnd(result) { // Обновляем лидерборд updateLeaderboard(result); + // НОВОЕ: Генерируем эмоциональные реакции ботов + if (!isMultiplayer && game) { + generateBotEmotions(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); +} + /** * Закрыть модальное окно результата */ @@ -1111,8 +1205,14 @@ function sendGameChat() { const bots = game.players.filter(p => p.isAI && !p.folded); if (bots.length > 0) { - // Выбираем случайного бота для ответа - const bot = bots[Math.floor(Math.random() * bots.length)]; + // Проверяем, обращается ли игрок к конкретному боту + 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 = { @@ -1120,7 +1220,8 @@ function sendGameChat() { pot: game.pot, myChips: bot.chips, lastAction: message, - communityCards: game.communityCards?.map(c => `${c.rank}${c.suit}`) || [] + communityCards: game.communityCards?.map(c => `${c.rank}${c.suit}`) || [], + mentionedByName: targetBot !== null // Указываем, что игрок обратился по имени }; // Получаем личность бота