pets1/public/ai.js

1011 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* =============================================================================
* 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 };
}