1292 lines
53 KiB
JavaScript
1292 lines
53 KiB
JavaScript
/**
|
||
* =============================================================================
|
||
* 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 и EV в уме
|
||
- Немного занудный, бубнишь про частоты и диапазоны
|
||
- Уважаешь хорошую игру, презираешь "лаки фишей"
|
||
|
||
Правила ответов:
|
||
- ТОЛЬКО 1-2 коротких предложения
|
||
- Можешь упомянуть шансы или +EV
|
||
- Реагируй на игру с точки зрения математики
|
||
- Говори по-русски, можно с англицизмами`
|
||
},
|
||
{
|
||
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();
|
||
},
|
||
|
||
/**
|
||
* Найти бота по имени (поддерживает уменьшительно-ласкательные формы)
|
||
*/
|
||
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)];
|
||
},
|
||
|
||
/**
|
||
* Очистить всю историю
|
||
*/
|
||
clearAllHistory() {
|
||
this.messageHistory.clear();
|
||
}
|
||
};
|
||
|
||
// Для совместимости с серверной и клиентской частью
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = { pokerAI, llmChat };
|
||
}
|