Add bot personality handling and emotional reactions after hands

This commit is contained in:
ur002 2026-02-01 12:20:36 +03:00
parent ab7a8418ed
commit c3ace2834d
2 changed files with 313 additions and 11 deletions

View File

@ -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)];
},
/**
* Очистить всю историю
*/

View File

@ -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 // Указываем, что игрок обратился по имени
};
// Получаем личность бота