diff --git a/BOT_PERSONALITIES_CONFIG.md b/BOT_PERSONALITIES_CONFIG.md
new file mode 100644
index 0000000..f4ceb86
--- /dev/null
+++ b/BOT_PERSONALITIES_CONFIG.md
@@ -0,0 +1,305 @@
+# Настройка персональностей ботов
+
+## Обзор
+
+В игре реализована система настройки персональностей ботов через админ-панель. Каждый бот имеет уникальную личность с собственным системным промптом, который определяет его характер, стиль общения и поведение.
+
+## Доступные персональности
+
+1. **🦈 Виктор "Акула"** (aggressive)
+ - 20 лет опыта, несколько браслетов WSOP
+ - Агрессивный, уверенный, слегка надменный
+ - Любит подкалывать соперников и давить психологически
+
+2. **👩💼 Анна "Блефер"** (tricky)
+ - Загадочная, проницательная
+ - Любит психологические игры и блеф
+ - Говорит намёками и двусмысленностями
+
+3. **👴 Дед Михалыч** (oldschool)
+ - Добродушный, мудрый
+ - Часто вспоминает старые времена
+ - Говорит просто, с народными выражениями
+
+4. **🤓 Макс "ГТО"** (mathematical)
+ - Молодой задрот-математик
+ - Помешан на GTO (Game Theory Optimal)
+ - Постоянно считает odds и EV
+
+5. **🍀 Катя "Удача"** (lucky)
+ - Эмоциональная, позитивная, суеверная
+ - Верит в приметы и талисманы
+ - Радуется победам, расстраивается от проигрышей
+
+6. **🎭 Борис "Молчун"** (silent)
+ - Молчаливый, загадочный
+ - Говорит ОЧЕНЬ мало (максимум 1-3 слова)
+ - Каждое слово на вес золота
+
+7. **😤 Олег "Тильтер"** (tilted)
+ - Эмоциональный, легко тильтует
+ - При проигрыше злится и жалуется
+ - При выигрыше хвастается
+
+8. **💎 Ирина "Профи"** (professional)
+ - Профессиональный онлайн-гриндер
+ - Спокойная, собранная, рациональная
+ - Покер — это работа, эмоции в сторону
+
+## Как настроить персональность
+
+### Через админ-панель
+
+1. **Откройте админ-панель**
+ - Войдите как администратор (логин: `admin`, пароль: `admin`)
+ - Перейдите в раздел "Админ" (иконка ⚙️)
+
+2. **Перейдите во вкладку "🤖 Боты"**
+ - В верхнем меню админ-панели выберите вкладку "Боты"
+
+3. **Выберите персональность**
+ - В выпадающем списке выберите бота, которого хотите настроить
+ - Например: "🦈 Виктор "Акула""
+
+4. **Редактируйте системный промпт**
+ - В большом текстовом поле отобразится текущий системный промпт
+ - Отредактируйте промпт по своему усмотрению
+ - Сохраните изменения кнопкой "💾 Сохранить промпт"
+
+5. **Протестируйте промпт** (опционально)
+ - Нажмите кнопку "🧪 Тестировать промпт"
+ - Система отправит тестовое сообщение к LLM с вашим промптом
+ - Вы увидите ответ бота в тестовой ситуации
+
+6. **Сбросьте к оригиналу** (если нужно)
+ - Кнопка "🔄 Сбросить к оригиналу" вернёт стандартный промпт
+
+## Структура системного промпта
+
+Системный промпт определяет всё поведение бота:
+
+```
+Ты — [имя и прозвище]. Прямо сейчас ты сидишь за покерным столом и играешь в Техасский Холдем.
+
+ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
+
+Твой характер:
+- [черта 1]
+- [черта 2]
+- [черта 3]
+
+Правила ответов:
+- ТОЛЬКО 1-2 коротких предложения
+- Реагируй на игровую ситуацию
+- Комментируй конкретную ситуацию (карты, ставки)
+- [специфичные правила для персональности]
+```
+
+## Хранение кастомных промптов
+
+- Кастомные промпты сохраняются в **localStorage** браузера
+- Ключ: `customPersonalityPrompts`
+- Формат: JSON объект `{ "Имя персональности": "Промпт" }`
+- Промпты применяются при каждой загрузке страницы
+
+### Пример localStorage:
+
+```json
+{
+ "Виктор \"Акула\"": "Ты — Виктор, агрессивный игрок...",
+ "Дед Михалыч": "Ты — Дед Михалыч, мудрый старик..."
+}
+```
+
+## Программное использование
+
+### Загрузка кастомных промптов при инициализации
+
+```javascript
+// В main.js вызывается автоматически:
+function updateAIPersonalityPrompts() {
+ const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
+
+ pokerAI.personalities.forEach(personality => {
+ const custom = customPrompts[personality.name];
+ if (custom) {
+ personality.systemPrompt = custom;
+ }
+ });
+}
+```
+
+### Получение персональности для бота
+
+```javascript
+// В main.js при создании бота:
+const botPersonality = pokerAI.getRandomPersonality();
+
+// botPersonality содержит:
+// {
+// name: 'Виктор "Акула"',
+// avatar: '🦈',
+// style: 'aggressive',
+// systemPrompt: '...' // Может быть кастомным!
+// }
+```
+
+## Интеграция с LLM
+
+Системный промпт используется во всех взаимодействиях бота с LLM:
+
+1. **Обычный чат** (`llmChat.chat()`)
+ - Промпт передаётся как системное сообщение
+
+2. **Эмоциональные реакции** (`generateEmotionalReaction()`)
+ - Промпт + контекст победы/поражения
+
+3. **Поздравления игрока** (`generatePlayerCongratulation()`)
+ - Промпт + информация о сильной руке игрока
+
+## Рекомендации по созданию промптов
+
+### ✅ Хорошие практики
+
+- **Короткие ответы**: Указывайте "ТОЛЬКО 1-2 предложения"
+- **Контекст игры**: Упоминайте, что бот за покерным столом
+- **Избегайте AI-ассистента**: "Ты НЕ ИИ и НЕ ассистент"
+- **Конкретные примеры**: Давайте примеры фраз и реакций
+- **Покерный сленг**: Указывайте, если бот должен использовать термины
+
+### ❌ Частые ошибки
+
+- Слишком длинные промпты (LLM будет давать длинные ответы)
+- Отсутствие ограничений на длину ответа
+- Промпт как для ассистента ("Помоги игроку", "Дай совет")
+- Нет указания на игровой контекст
+- Противоречивые инструкции
+
+### Пример хорошего промпта
+
+```
+Ты — Макс "ГТО", молодой математик-покерист.
+Ты сидишь за покерным столом в живой игре.
+
+ВАЖНО: Ты НЕ ИИ. Ты — реальный игрок.
+
+Характер:
+- Помешан на математике и GTO стратегии
+- Считаешь odds и EV в уме
+- Немного занудный, но уважаешь хорошую игру
+
+Ответы:
+- Максимум 1-2 предложения, как реплика за столом
+- Комментируй с точки зрения математики
+- Можешь упомянуть: +EV, odds, equity, диапазоны, GTO
+- Реагируй на конкретную ситуацию в игре
+
+Примеры: "Матожидание сработало!", "По GTO это фолд", "Equity на моей стороне"
+```
+
+## Отладка
+
+### Проверка промптов в консоли браузера
+
+```javascript
+// Посмотреть все персональности
+console.log(pokerAI.personalities);
+
+// Посмотреть кастомные промпты
+console.log(localStorage.getItem('customPersonalityPrompts'));
+
+// Обновить промпты из localStorage
+updateAIPersonalityPrompts();
+```
+
+### Тестирование через админ-панель
+
+1. Откройте вкладку "🤖 Боты"
+2. Выберите персональность
+3. Нажмите "🧪 Тестировать промпт"
+4. Проверьте ответ бота в тестовой ситуации
+
+Тестовый сценарий:
+- Фаза: Флоп
+- Карты на столе: K♠ Q♥ 7♦
+- Карты бота: A♠ A♥
+- Банк: 150
+- Ставка: 50
+- Вопрос игрока: "Как думаешь, какие у меня шансы?"
+
+## Сброс к оригиналам
+
+### Через админ-панель
+
+- Выберите персональность
+- Нажмите "🔄 Сбросить к оригиналу"
+- Подтвердите действие
+- Страница перезагрузится
+
+### Программно
+
+```javascript
+// Удалить все кастомные промпты
+localStorage.removeItem('customPersonalityPrompts');
+location.reload();
+
+// Удалить промпт конкретной персональности
+const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
+delete customPrompts['Виктор "Акула"'];
+localStorage.setItem('customPersonalityPrompts', JSON.stringify(customPrompts));
+location.reload();
+```
+
+## FAQ
+
+**Q: Промпт сохранился, но бот не изменился?**
+A: Перезагрузите страницу. Промпты применяются при загрузке.
+
+**Q: Можно ли добавить новую персональность?**
+A: Да, но это требует изменения кода в `ai.js` (массив `personalities`).
+
+**Q: Что если LLM выключен?**
+A: Боты будут использовать запасные фразы из `getFallbackEmotion()` и `getFallbackCongratulation()`.
+
+**Q: Промпты синхронизируются между браузерами?**
+A: Нет, localStorage локален для каждого браузера.
+
+**Q: Как экспортировать/импортировать промпты?**
+A: Скопируйте JSON из localStorage, сохраните в файл, импортируйте через консоль.
+
+## Примеры использования
+
+### Создать бота с сильным акцентом на блеф
+
+Выберите "Анна Блефер" и усильте промпт:
+
+```
+Ты — Анна "Блефер", королева психологических игр.
+
+НИКОГДА не показывай свои истинные эмоции.
+ВСЕГДА говори загадками и намёками.
+Блефуй даже в обычном разговоре.
+
+Ответы: 1-2 предложения, загадочно, с ноткой иронии.
+```
+
+### Создать бота-новичка
+
+Выберите любую персональность и измените на:
+
+```
+Ты — начинающий игрок в покер, учишься у профи.
+
+Часто спрашиваешь совета.
+Иногда путаешь термины.
+Радуешься даже мелким победам.
+
+Ответы: 1-2 предложения, неуверенно, с вопросами.
+Примеры: "Это хорошо?", "Надеюсь не ошибся", "Стрит бьёт флеш?"
+```
+
+---
+
+**Версия документа**: 1.0
+**Дата**: 2026-02-01
+**Автор**: GitHub Copilot
diff --git a/poker.db b/poker.db
index abb938f..83bc694 100644
Binary files a/poker.db and b/poker.db differ
diff --git a/public/ai.js b/public/ai.js
index 73d80e5..1f89986 100644
--- a/public/ai.js
+++ b/public/ai.js
@@ -28,11 +28,16 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения, как будто говоришь вслух за столом
- Реагируй на действия игрока и ситуацию в игре (смотри контекст ниже!)
-- Комментируй КОНКРЕТНУЮ игровую ситуацию: свои карты, карты на столе, ставки соперников
+- Комментируй карты НА СТОЛЕ, банк, ставки соперников
- Можешь подначивать, блефовать словами, пугать олл-ином
- НЕ объясняй правила покера, НЕ давай советы как ассистент
- Говори по-русски, неформально
-- Используй информацию о раздаче для правдоподобных комментариев`
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои карты на руках (например "У меня туз-король")
+- НИКОГДА не раскрывай силу своей руки конкретно
+- НИКОГДА не говори что у тебя "пара", "флеш", "стрит" и т.д.
+- Можешь ТОЛЬКО намекать общими фразами: "неплохие карты", "посмотрим", "может повезло"`
},
{
name: 'Анна "Блефер"',
@@ -52,10 +57,14 @@ const pokerAI = {
- ТОЛЬКО 1-2 коротких предложения
- Держи интригу, отвечай загадочно
- Смотри на карты на столе, банк, ставки — намекай исходя из ситуации
-- Можешь намекать на силу/слабость руки (это тоже блеф)
+- Можешь намекать на возможную силу руки, но НИКОГДА не раскрывай конкретно
- Улыбайся мысленно, будь обаятельно-опасной
- Говори по-русски
-- Используй игровой контекст для загадочных намёков`
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои карты (например "У меня дама-валет")
+- НИКОГДА не раскрывай конкретную комбинацию ("У меня флеш", "У меня две пары")
+- Можешь ТОЛЬКО намекать: "интересные карты", "может повезло", "увидите на вскрытии"`
},
{
name: 'Дед Михалыч',
@@ -76,10 +85,14 @@ const pokerAI = {
Правила ответов:
- 1-2 коротких предложения
- Можешь вспомнить историю из прошлого или вставить "эх, молодёжь"
-- Реагируй на ситуацию за столом, смотри на карты и ставки
+- Реагируй на ситуацию за столом, смотри на карты на столе и ставки
- Комментируй ход игры по-стариковски мудро
- Говори тепло и по-человечески
-- Используй контекст игры для житейских комментариев`
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои карты ("У меня две дамы", "Мне пришли тузы")
+- НИКОГДА не раскрывай свою комбинацию до вскрытия
+- Говори общими фразами: "карты нормальные", "бывало и лучше", "поглядим"`
},
{
name: 'Макс "ГТО"',
@@ -97,10 +110,15 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
-- Можешь упомянуть шансы или +EV, опираясь на карты на столе и банк
+- Можешь упомянуть шансы банка или +EV, глядя на карты на столе и размер банка
- Реагируй на игру с точки зрения математики
-- Анализируй конкретную ситуацию (смотри контекст)
-- Говори по-русски, можно с англицизмами`
+- Анализируй ситуацию, но НЕ раскрывай свои карты
+- Говори по-русски, можно с англицизмами
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои конкретные карты ("У меня АК", "Карманные короли")
+- НИКОГДА не говори свою комбинацию ("У меня сет", "Собрал флеш")
+- Можешь говорить об equity и диапазонах ОБЩО, без раскрытия своих карт`
},
{
name: 'Катя "Удача"',
@@ -120,8 +138,13 @@ const pokerAI = {
- ТОЛЬКО 1-2 коротких предложения
- Используй эмодзи уместно: 🍀✨😊🎲
- Реагируй эмоционально на игру и карты на столе
-- Комментируй удачу/неудачу исходя из ситуации
-- Говори по-русски, живо`
+- Комментируй удачу/неудачу в общем
+- Говори по-русски, живо
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои карты ("Мне пришли тузы!")
+- НИКОГДА не раскрывай свою комбинацию ("У меня флеш!", "Собралась пара!")
+- Говори о чувствах и удаче: "чувствую карта придёт", "удача со мной", "загадала желание"`
},
{
name: 'Борис "Молчун"',
@@ -135,13 +158,18 @@ const pokerAI = {
- Говоришь ОЧЕНЬ мало
- Загадочный, никто не знает что у тебя на уме
- Отвечаешь односложно или просто молчишь
-- Можешь кивнуть на конкретную карту или ситуацию
+- Можешь кивнуть на конкретную карту на столе или ситуацию
Правила ответов:
- МАКСИМУМ 1-3 слова или многоточие
- Примеры: "Хм.", "Нет.", "Посмотрим.", "...", "Да.", "Флоп интересный."
- НИКОГДА не говори длинно
-- Молчание — твоё оружие`
+- Молчание — твоё оружие
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои карты (даже намёком)
+- НИКОГДА не раскрывай комбинацию
+- Максимум что можешь: "Хм.", "...", "Увидим."`
},
{
name: 'Олег "Тильтер"',
@@ -159,10 +187,15 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
-- Можешь ворчать, возмущаться, жаловаться на конкретные карты
-- Реагируй эмоционально на плохие карты/биты, смотря на ситуацию
-- Комментируй несправедливость раздачи
-- Говори по-русски, экспрессивно`
+- Можешь ворчать, возмущаться, жаловаться на карты НА СТОЛЕ
+- Реагируй эмоционально на плохие общие карты/биты
+- Комментируй несправедливость раздачи в общем
+- Говори по-русски, экспрессивно
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои карты ("У меня были короли!")
+- НИКОГДА не раскрывай свою комбинацию ("Мне собрался стрит!")
+- Жалуйся на удачу соперников и общие карты, но НЕ раскрывай свои`
},
{
name: 'Ирина "Профи"',
@@ -181,9 +214,14 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
- Говори спокойно, профессионально
-- Можешь прокомментировать интересный розыгрыш, ссылаясь на ситуацию
-- Анализируй конкретную раздачу если нужно
-- Говори по-русски, корректно`
+- Можешь прокомментировать интересный розыгрыш в общем
+- Анализируй текстуру борда, размеры ставок
+- Говори по-русски, корректно
+
+🚫 СТРОГО ЗАПРЕЩЕНО:
+- НИКОГДА не называй свои карты ("У меня AQ")
+- НИКОГДА не раскрывай свою комбинацию ("Собрала топ-пару", "У меня дро")
+- Можешь анализировать ситуацию профессионально, но без раскрытия своих карт`
}
],
@@ -1363,8 +1401,12 @@ ${isWin ? 'Покажи радость, удовлетворение или са
const isStrongHand = handRank >= 7; // Фулл-хаус и выше
const isVeryStrongHand = handRank >= 9; // Стрит-флеш и роял-флеш
+ // Получаем кастомный промпт если есть
+ const customPrompt = this.getCustomPrompt(botPersonality.style, 'congrats');
+ const basePrompt = customPrompt || botPersonality.congratsPrompt || botPersonality.systemPrompt;
+
// Формируем промпт для LLM
- const congratsPrompt = `${botPersonality.systemPrompt}
+ const congratsPrompt = `${basePrompt}
СИТУАЦИЯ: Игрок ${playerName} только что ВЫИГРАЛ раздачу с рукой "${handName}"! Банк: ${potSize} фишек.
@@ -1384,14 +1426,45 @@ ${isVeryStrongHand ? 'Покажи восхищение и уважение!' :
}
}
- // Запасные поздравления
+ // Запасные поздравления (используем кастомные примеры если есть)
return this.getFallbackCongratulation(botPersonality.style, handRank, handName);
},
+ /**
+ * Получить кастомный промпт для стиля
+ */
+ getCustomPrompt(style, type) {
+ try {
+ const customPrompts = JSON.parse(localStorage.getItem('customBotPrompts') || '{}');
+ const stylePrompts = customPrompts[style];
+
+ if (!stylePrompts) return null;
+
+ switch (type) {
+ case 'base': return stylePrompts.base;
+ case 'chat': return stylePrompts.chat;
+ case 'emotion': return stylePrompts.emotion;
+ case 'congrats': return stylePrompts.congrats;
+ case 'examples': return stylePrompts.examples ? stylePrompts.examples.split(',').map(p => p.trim()) : null;
+ default: return null;
+ }
+ } catch (error) {
+ console.error('Ошибка загрузки кастомного промпта:', error);
+ return null;
+ }
+ },
+
/**
* Запасные поздравления
*/
getFallbackCongratulation(style, handRank, handName) {
+ // Сначала пробуем использовать кастомные примеры
+ const customExamples = this.getCustomPrompt(style, 'examples');
+ if (customExamples && customExamples.length > 0) {
+ return customExamples[Math.floor(Math.random() * customExamples.length)];
+ }
+
+ // Иначе используем стандартные поздравления
const congratulations = {
aggressive: {
normal: ['Неплохо сыграно!', 'Уважаю!', 'Сильная рука!', 'Молодец!'],
diff --git a/public/index.html b/public/index.html
index d86f008..cdb1081 100644
--- a/public/index.html
+++ b/public/index.html
@@ -365,6 +365,134 @@
+
+
+
+ 📊 Правила & Статистика
+ ▼
+
+
+
+
+
+
+
+
+
+
+
Комбинации (от старшей к младшей):
+
+
+ 10
+ Роял-флеш
+ A♠ K♠ Q♠ J♠ 10♠
+
+
+ 9
+ Стрит-флеш
+ 9♥ 8♥ 7♥ 6♥ 5♥
+
+
+ 8
+ Каре
+ K♠ K♥ K♦ K♣ 3♠
+
+
+ 7
+ Фулл-хаус
+ A♠ A♥ A♦ 8♣ 8♠
+
+
+ 6
+ Флеш
+ Q♦ 9♦ 7♦ 4♦ 2♦
+
+
+ 5
+ Стрит
+ J♠ 10♥ 9♦ 8♣ 7♠
+
+
+ 4
+ Сет (тройка)
+ 7♠ 7♥ 7♦ K♣ 2♠
+
+
+ 3
+ Две пары
+ Q♠ Q♥ 5♦ 5♣ 9♠
+
+
+ 2
+ Пара
+ 10♠ 10♥ A♦ 6♣ 3♠
+
+
+ 1
+ Старшая карта
+ A♠ J♥ 8♦ 6♣ 2♠
+
+
+
+
+
+
+
+
+
Ваша текущая рука:
+
+
+ Карты не розданы
+
+
+ —
+
+
+
+
Шансы на победу:
+
+
+
+
+
0%
+
+
+
Вероятности улучшения:
+
+
+ Следующая карта:
+ —
+
+
+ До вскрытия:
+ —
+
+
+
+
Статистика сессии:
+
+
+ Раздач сыграно:
+ 0
+
+
+ Раздач выиграно:
+ 0
+
+
+ Всего выиграно:
+ 0
+
+
+ Лучшая рука:
+ —
+
+
+
+
+
+
+
@@ -502,6 +630,7 @@
+
@@ -547,6 +676,56 @@
+
+
+
🤖 Персональности ботов
+
+ Настройте системные промпты для персональностей ботов.
+ Каждый бот имеет уникальную личность и стиль общения.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Определяет характер, стиль общения и поведение бота во всех ситуациях
+
+
+
+
+
+
+
+
+
+
+
+
Результат тестирования:
+
+
+
+
📋 История действий
diff --git a/public/main.js b/public/main.js
index d9c1f65..4be60d2 100644
--- a/public/main.js
+++ b/public/main.js
@@ -49,6 +49,7 @@ const sounds = {
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
loadLeaderboard();
+ loadSessionStats();
initSounds();
loadCardBackSettings();
@@ -60,6 +61,11 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
+ // Инициализируем кастомные промпты
+ setTimeout(() => {
+ initCustomPrompts();
+ }, 100);
+
// Инициализируем авторизацию
if (typeof initAuth === 'function') {
initAuth();
@@ -581,6 +587,9 @@ function updateGameUI() {
}
updateActionPanel(player, game);
+
+ // Обновляем статистику
+ updateGameStats();
}
/**
@@ -621,6 +630,9 @@ function updateGameUIFromServer(room) {
updateActionPanelFromServer(myPlayer, room);
}
+ // Обновляем статистику (для мультиплеера используем упрощенную версию)
+ updateGameStats();
+
// Показываем кнопку новой раздачи если игра завершена
if (room.gamePhase === 'showdown' || !room.isGameStarted) {
setTimeout(() => {
@@ -1105,6 +1117,9 @@ function onHandEnd(result) {
// Обновляем лидерборд
updateLeaderboard(result);
+ // Обновляем статистику сессии
+ updateStatsOnHandEnd(result);
+
// НОВОЕ: Генерируем эмоциональные реакции ботов
if (!isMultiplayer && game) {
generateBotEmotions(result);
@@ -2141,3 +2156,790 @@ function checkRoomInUrl() {
}
}
+// =============================================================================
+// ПАНЕЛЬ ИНФОРМАЦИИ И СТАТИСТИКИ
+// =============================================================================
+
+// Статистика сессии
+let sessionStats = {
+ handsPlayed: 0,
+ handsWon: 0,
+ totalWon: 0,
+ bestHandRank: 0,
+ bestHandName: '—'
+};
+
+/**
+ * Переключить панель информации
+ */
+function toggleGameInfo() {
+ document.getElementById('game-info-panel').classList.toggle('expanded');
+}
+
+/**
+ * Переключить вкладку информации
+ */
+function switchInfoTab(tabName) {
+ // Убираем active со всех вкладок
+ document.querySelectorAll('.info-tab').forEach(tab => {
+ tab.classList.remove('active');
+ });
+
+ document.querySelectorAll('.info-tab-content').forEach(content => {
+ content.classList.remove('active');
+ });
+
+ // Активируем выбранную
+ event.target.classList.add('active');
+ document.getElementById(`info-tab-${tabName}`).classList.add('active');
+
+ // Обновляем статистику при переключении
+ if (tabName === 'stats') {
+ updateGameStats();
+ }
+}
+
+/**
+ * Обновить отображение текущей руки в статистике
+ */
+function updateCurrentHandDisplay() {
+ const previewContainer = document.getElementById('hand-cards-preview');
+ const strengthDisplay = document.getElementById('hand-strength-display');
+
+ // Получаем текущие карты игрока
+ let playerHand = [];
+ let communityCards = [];
+
+ if (isMultiplayer) {
+ // В мультиплеере данные берем из последнего обновления
+ // Это нужно реализовать через сохранение состояния
+ previewContainer.textContent = 'Обновление...';
+ strengthDisplay.textContent = '—';
+ return;
+ } else if (game && game.isGameStarted) {
+ const player = game.players.find(p => p.id === currentPlayerId);
+ if (player && player.hand.length > 0) {
+ playerHand = player.hand;
+ communityCards = game.communityCards || [];
+ }
+ }
+
+ if (playerHand.length === 0) {
+ previewContainer.textContent = 'Карты не розданы';
+ strengthDisplay.textContent = '—';
+ return;
+ }
+
+ // Отображаем карты
+ previewContainer.innerHTML = '';
+ playerHand.forEach(card => {
+ const miniCard = document.createElement('div');
+ miniCard.className = `mini-card ${card.suit}`;
+ miniCard.innerHTML = `
+
${card.rank}
+
${SUIT_SYMBOLS[card.suit]}
+ `;
+ previewContainer.appendChild(miniCard);
+ });
+
+ // Отображаем силу руки
+ const strength = getHandStrength(playerHand, communityCards);
+ strengthDisplay.textContent = strength || '—';
+}
+
+/**
+ * Рассчитать вероятность победы
+ */
+function calculateWinProbability() {
+ if (!game || !game.isGameStarted) return 0;
+
+ const player = game.players.find(p => p.id === currentPlayerId);
+ if (!player || player.hand.length === 0) return 0;
+
+ const activePlayers = game.getActivePlayers().filter(p => !p.folded);
+ const opponentsCount = activePlayers.length - 1;
+
+ if (opponentsCount === 0) return 100;
+
+ // Простая оценка на основе силы руки и фазы игры
+ const allCards = [...player.hand, ...(game.communityCards || [])];
+
+ if (allCards.length < 5) {
+ // Префлоп - базовая оценка по стартовой руке
+ return calculatePreflopWinRate(player.hand, opponentsCount);
+ }
+
+ // Постфлоп - оцениваем текущую силу руки
+ const handResult = evaluateHand(allCards);
+ return calculatePostflopWinRate(handResult, game.gamePhase, opponentsCount);
+}
+
+/**
+ * Оценка вероятности победы на префлопе
+ */
+function calculatePreflopWinRate(hand, opponents) {
+ const [c1, c2] = hand;
+ const isPair = c1.value === c2.value;
+ const isSuited = c1.suit === c2.suit;
+ const highCard = Math.max(c1.value, c2.value);
+ const lowCard = Math.min(c1.value, c2.value);
+ const gap = highCard - lowCard;
+
+ let baseRate = 0;
+
+ if (isPair) {
+ // Карманные пары
+ if (highCard >= 12) baseRate = 85; // QQ+
+ else if (highCard >= 10) baseRate = 75; // TT-JJ
+ else if (highCard >= 7) baseRate = 65; // 77-99
+ else baseRate = 55; // 22-66
+ } else if (highCard === 14 && lowCard >= 10) {
+ // AK, AQ, AJ, AT
+ baseRate = isSuited ? 70 : 65;
+ } else if (highCard >= 13 && lowCard >= 10) {
+ // KQ, KJ, QJ
+ baseRate = isSuited ? 65 : 60;
+ } else if (gap <= 1 && highCard >= 10) {
+ // Коннекторы высокие
+ baseRate = isSuited ? 60 : 55;
+ } else if (isSuited && highCard >= 11) {
+ // Одномастные с картой
+ baseRate = 55;
+ } else if (gap <= 2 && lowCard >= 6) {
+ // Средние коннекторы
+ baseRate = isSuited ? 50 : 45;
+ } else {
+ // Слабые руки
+ baseRate = 35;
+ }
+
+ // Корректировка на количество оппонентов
+ const adjustment = (opponents - 1) * 5;
+ return Math.max(10, Math.min(95, baseRate - adjustment));
+}
+
+/**
+ * Оценка вероятности победы постфлоп
+ */
+function calculatePostflopWinRate(handResult, phase, opponents) {
+ const rankMultipliers = {
+ 10: 95, // Роял-флеш
+ 9: 90, // Стрит-флеш
+ 8: 85, // Каре
+ 7: 80, // Фулл-хаус
+ 6: 75, // Флеш
+ 5: 70, // Стрит
+ 4: 65, // Сет
+ 3: 55, // Две пары
+ 2: 45, // Пара
+ 1: 30 // Старшая карта
+ };
+
+ let baseRate = rankMultipliers[handResult.rank] || 30;
+
+ // Корректировка на фазу игры
+ if (phase === 'flop') {
+ baseRate *= 0.85; // Еще 2 карты впереди
+ } else if (phase === 'turn') {
+ baseRate *= 0.92; // Еще 1 карта
+ }
+
+ // Корректировка на количество оппонентов
+ const adjustment = (opponents - 1) * 3;
+
+ return Math.max(5, Math.min(98, Math.round(baseRate - adjustment)));
+}
+
+/**
+ * Обновить вероятности в панели статистики
+ */
+function updateWinProbability() {
+ const probability = calculateWinProbability();
+ const probabilityBar = document.getElementById('probability-bar');
+ const probabilityText = document.getElementById('probability-text');
+
+ if (probabilityBar && probabilityText) {
+ probabilityBar.style.width = `${probability}%`;
+ probabilityText.textContent = `${probability}%`;
+
+ // Меняем цвет в зависимости от вероятности
+ probabilityBar.classList.remove('low', 'medium', 'high');
+ if (probability < 40) {
+ probabilityBar.classList.add('low');
+ } else if (probability < 65) {
+ probabilityBar.classList.add('medium');
+ } else {
+ probabilityBar.classList.add('high');
+ }
+ }
+}
+
+/**
+ * Обновить шансы на улучшение
+ */
+function updateImprovementOdds() {
+ const oddsNext = document.getElementById('odds-next');
+ const oddsRiver = document.getElementById('odds-river');
+
+ if (!game || !game.isGameStarted) {
+ if (oddsNext) oddsNext.textContent = '—';
+ if (oddsRiver) oddsRiver.textContent = '—';
+ return;
+ }
+
+ const player = game.players.find(p => p.id === currentPlayerId);
+ if (!player || player.hand.length === 0) {
+ if (oddsNext) oddsNext.textContent = '—';
+ if (oddsRiver) oddsRiver.textContent = '—';
+ return;
+ }
+
+ // Упрощенный расчет аутов
+ const communityCards = game.communityCards || [];
+ const phase = game.gamePhase;
+
+ // Считаем примерное количество аутов
+ let outs = estimateOuts(player.hand, communityCards);
+
+ if (outs > 0) {
+ // Правило 2-4: умножаем ауты на 2 для следующей карты, на 4 для двух карт
+ const nextCardOdds = Math.min(100, outs * 2);
+ const riverOdds = Math.min(100, outs * 4);
+
+ if (phase === 'flop') {
+ if (oddsNext) oddsNext.textContent = `~${nextCardOdds}% (${outs} аутов)`;
+ if (oddsRiver) oddsRiver.textContent = `~${riverOdds}%`;
+ } else if (phase === 'turn') {
+ if (oddsNext) oddsNext.textContent = `~${nextCardOdds}% (${outs} аутов)`;
+ if (oddsRiver) oddsRiver.textContent = '—';
+ } else {
+ if (oddsNext) oddsNext.textContent = '—';
+ if (oddsRiver) oddsRiver.textContent = '—';
+ }
+ } else {
+ if (oddsNext) oddsNext.textContent = 'Готовая рука';
+ if (oddsRiver) oddsRiver.textContent = '—';
+ }
+}
+
+/**
+ * Приблизительная оценка аутов
+ */
+function estimateOuts(hand, communityCards) {
+ if (communityCards.length < 3) return 0;
+
+ const allCards = [...hand, ...communityCards];
+ const currentHand = evaluateHand(allCards);
+
+ // Если уже сильная рука (стрит+), аутов нет
+ if (currentHand.rank >= 5) return 0;
+
+ // Подсчет по мастям для флеш-дро
+ const suitCounts = {};
+ allCards.forEach(c => {
+ suitCounts[c.suit] = (suitCounts[c.suit] || 0) + 1;
+ });
+
+ const maxSuitCount = Math.max(...Object.values(suitCounts));
+ let outs = 0;
+
+ // Флеш-дро (4 карты одной масти)
+ if (maxSuitCount === 4) {
+ outs += 9;
+ }
+
+ // Стрит-дро (упрощенно)
+ const values = allCards.map(c => c.value).sort((a, b) => b - a);
+ const uniqueValues = [...new Set(values)];
+
+ // Проверяем на открытый стрит-дро (8 аутов)
+ for (let i = 0; i < uniqueValues.length - 3; i++) {
+ const gap = uniqueValues[i] - uniqueValues[i + 3];
+ if (gap === 4) {
+ outs += 8;
+ break;
+ }
+ }
+
+ // Пара для сета (2 аута)
+ if (currentHand.rank === 2) {
+ outs += 2;
+ }
+
+ return Math.min(outs, 21); // Максимум 21 аут
+}
+
+/**
+ * Обновить статистику сессии
+ */
+function updateSessionStats() {
+ document.getElementById('stat-hands-played').textContent = sessionStats.handsPlayed;
+ document.getElementById('stat-hands-won').textContent = sessionStats.handsWon;
+ document.getElementById('stat-total-won').textContent = sessionStats.totalWon;
+ document.getElementById('stat-best-hand').textContent = sessionStats.bestHandName;
+}
+
+/**
+ * Обновить всю статистику в панели
+ */
+function updateGameStats() {
+ updateCurrentHandDisplay();
+ updateWinProbability();
+ updateImprovementOdds();
+ updateSessionStats();
+}
+
+/**
+ * Обработать завершение раздачи для статистики
+ */
+function updateStatsOnHandEnd(result) {
+ sessionStats.handsPlayed++;
+
+ // Проверяем, выиграл ли игрок
+ const playerWon = result.winners.some(w => w.id === currentPlayerId);
+
+ if (playerWon) {
+ sessionStats.handsWon++;
+ const winAmount = result.winners.find(w => w.id === currentPlayerId)?.amount || 0;
+ sessionStats.totalWon += winAmount;
+
+ // Обновляем лучшую руку
+ const playerHandResult = result.hands?.find(h => h.player.id === currentPlayerId)?.hand;
+ if (playerHandResult && playerHandResult.rank > sessionStats.bestHandRank) {
+ sessionStats.bestHandRank = playerHandResult.rank;
+ sessionStats.bestHandName = playerHandResult.name;
+ }
+ }
+
+ // Сохраняем статистику в localStorage
+ localStorage.setItem('pokerSessionStats', JSON.stringify(sessionStats));
+
+ // Обновляем отображение
+ updateGameStats();
+}
+
+/**
+ * Загрузить статистику сессии
+ */
+function loadSessionStats() {
+ const saved = localStorage.getItem('pokerSessionStats');
+ if (saved) {
+ sessionStats = JSON.parse(saved);
+ }
+}
+
+/**
+ * Сбросить статистику сессии
+ */
+function resetSessionStats() {
+ sessionStats = {
+ handsPlayed: 0,
+ handsWon: 0,
+ totalWon: 0,
+ bestHandRank: 0,
+ bestHandName: '—'
+ };
+ localStorage.removeItem('pokerSessionStats');
+ updateSessionStats();
+}
+
+// =============================================================================
+// НАСТРОЙКА ПРОМПТОВ БОТОВ (АДМИН-ПАНЕЛЬ)
+// =============================================================================
+
+// Дефолтные промпты для разных стилей
+const defaultBotPrompts = {
+ aggressive: {
+ base: `Ты — агрессивный игрок в покер. Ты любишь рисковать, давить на оппонентов и доминировать за столом.
+ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
+Характер: уверенный, напористый, любишь психологическое давление.`,
+ chat: `Отвечай коротко (1-2 предложения). Комментируй игру агрессивно и уверенно.
+Используй покерный сленг. Можешь подначивать соперников.`,
+ emotion: `Реагируй на результат раздачи агрессивно: при победе — ликуй и хвастайся,
+при проигрыше — злись и обещай отыграться.`,
+ congrats: `Поздравляй с сильной рукой, но с оттенком зависти или вызова.
+Например: "Ничего, следующая раздача моя!"`,
+ examples: 'Олл-ин!, Слабаки!, Я вас всех обыграю, Покажу вам класс, Удачи не бывает два раза'
+ },
+ conservative: {
+ base: `Ты — консервативный игрок в покер. Ты осторожен, играешь только с сильными руками.
+ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
+Характер: сдержанный, расчётливый, терпеливый.`,
+ chat: `Отвечай коротко (1-2 предложения). Комментируй игру осторожно и вдумчиво.
+Подчёркивай важность расчёта и терпения.`,
+ emotion: `Реагируй сдержанно: при победе — спокойное удовлетворение,
+при проигрыше — философское принятие.`,
+ congrats: `Поздравляй искренне и с уважением к хорошей игре.`,
+ examples: 'Хороший ход, Надо подумать, Терпение — ключ к успеху, Математика не врёт, Осторожность не помешает'
+ },
+ tricky: {
+ base: `Ты — хитрый и непредсказуемый игрок в покер. Ты любишь обманывать и держать интригу.
+ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
+Характер: загадочный, изворотливый, любишь блефовать.`,
+ chat: `Отвечай коротко (1-2 предложения). Говори намёками и загадками.
+Держи интригу, не показывай свои намерения.`,
+ emotion: `Реагируй загадочно и двусмысленно независимо от результата.
+Никто не должен понять, расстроен ты или рад.`,
+ congrats: `Поздравляй загадочно, с намёком что ты что-то знаешь или планируешь.`,
+ examples: 'Интересно..., Посмотрим, Может быть..., У меня есть план, Ничего не бывает случайным'
+ },
+ professional: {
+ base: `Ты — профессиональный игрок в покер. Ты играешь по правилам GTO и математике.
+ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
+Характер: серьёзный, аналитичный, уважающий хорошую игру.`,
+ chat: `Отвечай коротко (1-2 предложения). Комментируй с точки зрения математики и стратегии.
+Можешь упомянуть odds, EV, диапазоны.`,
+ emotion: `Реагируй с точки зрения анализа игры. При победе — отмечай правильность решения,
+при проигрыше — анализируй variance.`,
+ congrats: `Поздравляй профессионально, отмечая техническое мастерство.`,
+ examples: 'Правильный колл, +EV решение, Variance, GTO подход, По математике всё верно'
+ },
+ loose: {
+ base: `Ты — лузовый игрок в покер. Ты играешь много рук и любишь экшн.
+ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
+Характер: весёлый, азартный, оптимистичный.`,
+ chat: `Отвечай коротко (1-2 предложения). Комментируй весело и с энтузиазмом.
+Показывай любовь к игре и экшну.`,
+ emotion: `Реагируй эмоционально и позитивно. При победе — ликуй,
+при проигрыше — не расстраивайся, играй дальше.`,
+ congrats: `Поздравляй искренне и с восторгом, радуйся за хорошую руку.`,
+ examples: 'Давай!, Играем!, Вот это карта!, Классно!, Ещё раунд!'
+ },
+ tight: {
+ base: `Ты — тайтовый игрок в покер. Ты играешь только премиум-руки и очень избирательно.
+ВАЖНО: Ты НЕ ИИ и НЕ ассистент. Ты — реальный игрок за столом.
+Характер: дисциплинированный, терпеливый, методичный.`,
+ chat: `Отвечай коротко (1-2 предложения). Комментируй сдержанно и по делу.
+Подчёркивай важность выбора правильных рук.`,
+ emotion: `Реагируй сдержанно. При победе — спокойное удовлетворение,
+при проигрыше — стоическое принятие.`,
+ congrats: `Поздравляй сдержанно, с уважением к терпению и дисциплине.`,
+ examples: 'Ожидал лучшей руки, Фолд — тоже решение, Дисциплина важна, Премиум руки, Терпение окупается'
+ }
+};
+
+/**
+ * Инициализировать селектор персональностей ботов
+ */
+function initBotPersonalitySelector() {
+ if (typeof pokerAI === 'undefined' || !pokerAI.personalities) {
+ console.error('pokerAI.personalities не загружен');
+ return;
+ }
+
+ const selector = document.getElementById('bot-personality-selector');
+ if (!selector) return;
+
+ // Очищаем селектор
+ selector.innerHTML = '';
+
+ // Добавляем опции для каждой персональности
+ pokerAI.personalities.forEach((personality, index) => {
+ const option = document.createElement('option');
+ option.value = index;
+ option.textContent = `${personality.avatar} ${personality.name}`;
+ selector.appendChild(option);
+ });
+
+ // Загружаем промпт для первой персональности
+ loadBotPersonalityPrompt();
+}
+
+/**
+ * Загрузить промпт для выбранной персональности бота
+ */
+function loadBotPersonalityPrompt() {
+ if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
+
+ const index = parseInt(document.getElementById('bot-personality-selector').value);
+ const personality = pokerAI.personalities[index];
+
+ if (!personality) return;
+
+ // Проверяем, есть ли кастомный промпт
+ const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
+ const customPrompt = customPrompts[personality.name];
+
+ // Показываем кастомный промпт если есть, иначе оригинальный
+ const promptToShow = customPrompt || personality.systemPrompt;
+
+ document.getElementById('bot-system-prompt').value = promptToShow;
+}
+
+/**
+ * Сохранить промпт для выбранной персональности
+ */
+function saveBotPersonalityPrompt() {
+ if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
+
+ const index = parseInt(document.getElementById('bot-personality-selector').value);
+ const personality = pokerAI.personalities[index];
+
+ if (!personality) return;
+
+ const newPrompt = document.getElementById('bot-system-prompt').value;
+
+ // Сохраняем в localStorage
+ const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
+ customPrompts[personality.name] = newPrompt;
+ localStorage.setItem('customPersonalityPrompts', JSON.stringify(customPrompts));
+
+ // Обновляем промпт в самом объекте (для текущей сессии)
+ personality.systemPrompt = newPrompt;
+
+ showNotification(`Промпт для ${personality.name} сохранён!`, 'success');
+}
+
+/**
+ * Сбросить промпт персональности к оригиналу
+ */
+function resetBotPersonalityPrompt() {
+ if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
+
+ const index = parseInt(document.getElementById('bot-personality-selector').value);
+ const personality = pokerAI.personalities[index];
+
+ if (!personality) return;
+
+ if (confirm(`Вы уверены, что хотите сбросить промпт для ${personality.name} к оригиналу?`)) {
+ // Удаляем кастомный промпт
+ const customPrompts = JSON.parse(localStorage.getItem('customPersonalityPrompts') || '{}');
+ delete customPrompts[personality.name];
+ localStorage.setItem('customPersonalityPrompts', JSON.stringify(customPrompts));
+
+ // Перезагружаем оригинальный промпт (нужно перезагрузить страницу или хранить оригиналы)
+ // Для простоты просто перезагружаем
+ location.reload();
+ }
+}
+
+/**
+ * Тестировать промпт персональности
+ */
+async function testBotPersonalityPrompt() {
+ if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
+ if (typeof llmChat === 'undefined') {
+ showNotification('LLM чат не загружен', 'error');
+ return;
+ }
+
+ const settings = llmChat.getSettings();
+ if (!settings.llmEnabled) {
+ showNotification('Включите LLM чат в настройках', 'warning');
+ return;
+ }
+
+ const index = parseInt(document.getElementById('bot-personality-selector').value);
+ const personality = pokerAI.personalities[index];
+
+ if (!personality) return;
+
+ const newPrompt = document.getElementById('bot-system-prompt').value;
+
+ // Создаём тестовую персональность с новым промптом
+ const testPersonality = {
+ ...personality,
+ systemPrompt: newPrompt
+ };
+
+ const resultDiv = document.getElementById('prompt-test-result');
+ const contentDiv = document.getElementById('test-result-content');
+
+ resultDiv.style.display = 'block';
+ contentDiv.innerHTML = '