Add bot personality handling and emotional reactions after hands
This commit is contained in:
parent
ab7a8418ed
commit
c3ace2834d
201
public/ai.js
201
public/ai.js
|
|
@ -1076,6 +1076,207 @@ ${gameContextStr}
|
||||||
this.messageHistory.delete(botId);
|
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)];
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Очистить всю историю
|
* Очистить всю историю
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
123
public/main.js
123
public/main.js
|
|
@ -19,6 +19,7 @@ let leaderboard = []; // Таблица лидеров
|
||||||
let soundEnabled = true; // Звук включён
|
let soundEnabled = true; // Звук включён
|
||||||
let currentGamePhase = 'waiting'; // Текущая фаза игры для мультиплеера
|
let currentGamePhase = 'waiting'; // Текущая фаза игры для мультиплеера
|
||||||
let wsConnecting = false; // Флаг подключения WebSocket
|
let wsConnecting = false; // Флаг подключения WebSocket
|
||||||
|
let botPersonalities = null; // Персональности ботов (загружается из pokerAI)
|
||||||
|
|
||||||
// Выбранные опции
|
// Выбранные опции
|
||||||
const selectedOptions = {
|
const selectedOptions = {
|
||||||
|
|
@ -51,6 +52,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
initSounds();
|
initSounds();
|
||||||
loadCardBackSettings();
|
loadCardBackSettings();
|
||||||
|
|
||||||
|
// Загружаем персональности ботов
|
||||||
|
if (typeof pokerAI !== 'undefined' && pokerAI.personalities) {
|
||||||
|
botPersonalities = {};
|
||||||
|
pokerAI.personalities.forEach(p => {
|
||||||
|
botPersonalities[p.style] = p;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Восстанавливаем имя игрока
|
// Восстанавливаем имя игрока
|
||||||
const savedName = localStorage.getItem('playerName');
|
const savedName = localStorage.getItem('playerName');
|
||||||
if (savedName) {
|
if (savedName) {
|
||||||
|
|
@ -191,17 +200,32 @@ function startSinglePlayer() {
|
||||||
game.addPlayer(player);
|
game.addPlayer(player);
|
||||||
currentPlayerId = player.id;
|
currentPlayerId = player.id;
|
||||||
|
|
||||||
// Добавляем ботов
|
// Добавляем ботов с персональностями
|
||||||
const personalityKeys = typeof botPersonalities !== 'undefined'
|
const personalities = typeof pokerAI !== 'undefined' && pokerAI.personalities
|
||||||
? Object.keys(botPersonalities)
|
? pokerAI.personalities
|
||||||
: ['professional', 'aggressive', 'mathematical'];
|
: [];
|
||||||
|
|
||||||
|
// Перемешиваем персональности для разнообразия
|
||||||
|
const shuffledPersonalities = personalities.length > 0
|
||||||
|
? [...personalities].sort(() => Math.random() - 0.5)
|
||||||
|
: [];
|
||||||
|
|
||||||
for (let i = 0; i < botCount; i++) {
|
for (let i = 0; i < botCount; i++) {
|
||||||
const botName = pokerAI.getRandomName();
|
let botName, personalityId, personality;
|
||||||
const bot = new Player(`bot_${i}`, botName, startingStack, true, aiDifficulty);
|
|
||||||
|
|
||||||
// Присваиваем случайную личность боту
|
if (shuffledPersonalities.length > 0) {
|
||||||
bot.personalityId = personalityKeys[i % personalityKeys.length];
|
// Используем персональность из списка
|
||||||
|
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);
|
game.addPlayer(bot);
|
||||||
}
|
}
|
||||||
|
|
@ -1006,12 +1030,82 @@ function onHandEnd(result) {
|
||||||
// Обновляем лидерборд
|
// Обновляем лидерборд
|
||||||
updateLeaderboard(result);
|
updateLeaderboard(result);
|
||||||
|
|
||||||
|
// НОВОЕ: Генерируем эмоциональные реакции ботов
|
||||||
|
if (!isMultiplayer && game) {
|
||||||
|
generateBotEmotions(result);
|
||||||
|
}
|
||||||
|
|
||||||
// Показываем кнопку новой раздачи
|
// Показываем кнопку новой раздачи
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('new-hand-btn').style.display = 'block';
|
document.getElementById('new-hand-btn').style.display = 'block';
|
||||||
}, 1000);
|
}, 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);
|
const bots = game.players.filter(p => p.isAI && !p.folded);
|
||||||
|
|
||||||
if (bots.length > 0) {
|
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
|
// Формируем контекст игры для LLM
|
||||||
const gameContext = {
|
const gameContext = {
|
||||||
|
|
@ -1120,7 +1220,8 @@ function sendGameChat() {
|
||||||
pot: game.pot,
|
pot: game.pot,
|
||||||
myChips: bot.chips,
|
myChips: bot.chips,
|
||||||
lastAction: message,
|
lastAction: message,
|
||||||
communityCards: game.communityCards?.map(c => `${c.rank}${c.suit}`) || []
|
communityCards: game.communityCards?.map(c => `${c.rank}${c.suit}`) || [],
|
||||||
|
mentionedByName: targetBot !== null // Указываем, что игрок обратился по имени
|
||||||
};
|
};
|
||||||
|
|
||||||
// Получаем личность бота
|
// Получаем личность бота
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue