Add game info panel styles and bot personality configuration documentation

- Implemented CSS styles for the game info panel, including rules and statistics sections.
- Added responsive design adjustments for mobile and compact views.
- Created a comprehensive documentation file for bot personality configurations, detailing available personalities, customization instructions, and best practices for prompt creation.
This commit is contained in:
ur002 2026-02-01 19:57:20 +03:00
parent 80032b83b1
commit e84717a79d
6 changed files with 1824 additions and 22 deletions

305
BOT_PERSONALITIES_CONFIG.md Normal file
View File

@ -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

BIN
poker.db

Binary file not shown.

View File

@ -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: ['Неплохо сыграно!', 'Уважаю!', 'Сильная рука!', 'Молодец!'],

View File

@ -365,6 +365,134 @@
</div>
</div>
<!-- Правила игры и статистика -->
<div class="game-info-panel glass-card" id="game-info-panel">
<div class="info-header" onclick="toggleGameInfo()">
📊 Правила & Статистика
<span class="info-toggle"></span>
</div>
<div class="info-body">
<div class="info-tabs">
<button class="info-tab active" onclick="switchInfoTab('rules')">Правила</button>
<button class="info-tab" onclick="switchInfoTab('stats')">Статистика</button>
</div>
<!-- Вкладка правил -->
<div class="info-tab-content active" id="info-tab-rules">
<div class="rules-section">
<h4>Комбинации (от старшей к младшей):</h4>
<div class="hand-rankings">
<div class="hand-rank-item">
<span class="rank-number">10</span>
<span class="rank-name">Роял-флеш</span>
<span class="rank-example">A♠ K♠ Q♠ J♠ 10♠</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">9</span>
<span class="rank-name">Стрит-флеш</span>
<span class="rank-example">9♥ 8♥ 7♥ 6♥ 5♥</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">8</span>
<span class="rank-name">Каре</span>
<span class="rank-example">K♠ K♥ K♦ K♣ 3♠</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">7</span>
<span class="rank-name">Фулл-хаус</span>
<span class="rank-example">A♠ A♥ A♦ 8♣ 8♠</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">6</span>
<span class="rank-name">Флеш</span>
<span class="rank-example">Q♦ 9♦ 7♦ 4♦ 2♦</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">5</span>
<span class="rank-name">Стрит</span>
<span class="rank-example">J♠ 10♥ 9♦ 8♣ 7♠</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">4</span>
<span class="rank-name">Сет (тройка)</span>
<span class="rank-example">7♠ 7♥ 7♦ K♣ 2♠</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">3</span>
<span class="rank-name">Две пары</span>
<span class="rank-example">Q♠ Q♥ 5♦ 5♣ 9♠</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">2</span>
<span class="rank-name">Пара</span>
<span class="rank-example">10♠ 10♥ A♦ 6♣ 3♠</span>
</div>
<div class="hand-rank-item">
<span class="rank-number">1</span>
<span class="rank-name">Старшая карта</span>
<span class="rank-example">A♠ J♥ 8♦ 6♣ 2♠</span>
</div>
</div>
</div>
</div>
<!-- Вкладка статистики -->
<div class="info-tab-content" id="info-tab-stats">
<div class="stats-section">
<h4>Ваша текущая рука:</h4>
<div class="current-hand-display" id="current-hand-display">
<div class="hand-cards-preview" id="hand-cards-preview">
Карты не розданы
</div>
<div class="hand-strength-display" id="hand-strength-display">
</div>
</div>
<h4>Шансы на победу:</h4>
<div class="win-probability" id="win-probability">
<div class="probability-bar-container">
<div class="probability-bar" id="probability-bar" style="width: 0%"></div>
</div>
<div class="probability-text" id="probability-text">0%</div>
</div>
<h4>Вероятности улучшения:</h4>
<div class="improvement-odds" id="improvement-odds">
<div class="odds-item">
<span class="odds-label">Следующая карта:</span>
<span class="odds-value" id="odds-next"></span>
</div>
<div class="odds-item">
<span class="odds-label">До вскрытия:</span>
<span class="odds-value" id="odds-river"></span>
</div>
</div>
<h4>Статистика сессии:</h4>
<div class="session-stats">
<div class="stat-row">
<span class="stat-label">Раздач сыграно:</span>
<span class="stat-value" id="stat-hands-played">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Раздач выиграно:</span>
<span class="stat-value" id="stat-hands-won">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Всего выиграно:</span>
<span class="stat-value stat-highlight" id="stat-total-won">0</span>
</div>
<div class="stat-row">
<span class="stat-label">Лучшая рука:</span>
<span class="stat-value" id="stat-best-hand"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Кнопки управления -->
<div class="game-controls">
<button class="btn btn-icon-only" onclick="toggleSound()">🔊</button>
@ -502,6 +630,7 @@
<div class="admin-tabs">
<button class="btn btn-tab active" onclick="switchAdminTab('settings')">⚙️ Настройки</button>
<button class="btn btn-tab" onclick="switchAdminTab('bot-prompts')">🤖 Промпты ботов</button>
<button class="btn btn-tab" onclick="switchAdminTab('logs')">📋 Логи</button>
<button class="btn btn-tab" onclick="switchAdminTab('users')">👥 Пользователи</button>
</div>
@ -547,6 +676,56 @@
<button class="btn btn-secondary" onclick="testAdminLLM()">🔗 Проверить подключение</button>
</div>
<!-- Вкладка промптов ботов -->
<div id="admin-tab-bot-prompts" class="admin-tab-content" style="display: none;">
<h3>🤖 Персональности ботов</h3>
<p style="color: var(--text-muted); margin-bottom: 16px; font-size: 13px;">
Настройте системные промпты для персональностей ботов.
Каждый бот имеет уникальную личность и стиль общения.
</p>
<!-- Селектор персональности -->
<div class="form-group">
<label>Выберите персональность для настройки</label>
<select id="bot-personality-selector" class="input" onchange="loadBotPersonalityPrompt()">
<!-- Опции будут загружены динамически из ai.js -->
</select>
</div>
<!-- Системный промпт -->
<div class="form-group">
<label>Системный промпт персональности</label>
<textarea
id="bot-system-prompt"
class="input prompt-textarea"
rows="20"
placeholder="Полный системный промпт персональности..."
onchange="saveBotPersonalityPrompt()"
></textarea>
<small style="color: var(--text-muted); display: block; margin-top: 4px;">
Определяет характер, стиль общения и поведение бота во всех ситуациях
</small>
</div>
<div class="prompt-actions">
<button class="btn btn-secondary" onclick="resetBotPersonalityPrompt()">
🔄 Сбросить к оригиналу
</button>
<button class="btn btn-primary" onclick="testBotPersonalityPrompt()">
🧪 Тестировать промпт
</button>
<button class="btn btn-success" onclick="saveBotPersonalityPrompt()">
💾 Сохранить промпт
</button>
</div>
<!-- Результат теста -->
<div id="prompt-test-result" class="prompt-test-result" style="display: none;">
<h4>Результат тестирования:</h4>
<div class="test-result-content" id="test-result-content"></div>
</div>
</div>
<!-- Вкладка логов -->
<div id="admin-tab-logs" class="admin-tab-content" style="display: none;">
<h3>📋 История действий</h3>

View File

@ -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 = `
<div>${card.rank}</div>
<div style="font-size: 8px;">${SUIT_SYMBOLS[card.suit]}</div>
`;
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 = '<div class="loading">⏳ Тестирование...</div>';
try {
// Создаём тестовый контекст игры
const gameContext = {
phaseRu: 'Флоп',
communityCards: ['K♠', 'Q♥', '7♦'],
pot: 150,
currentBet: 50,
myName: personality.name,
myCards: ['A♠', 'A♥'],
myChips: 1000,
myBet: 50,
players: [
{ name: 'Игрок', chips: 950, bet: 50, lastAction: 'call' }
]
};
const testMessage = 'Как думаешь, какие у меня шансы?';
const response = await llmChat.chat(
'test-bot-' + Date.now(),
testPersonality,
testMessage,
gameContext
);
contentDiv.innerHTML = `
<div class="test-success">
<strong> Тест успешен!</strong>
<div class="test-context">
<strong>Тестовая ситуация:</strong>
<div>🎴 Флоп: K Q 7</div>
<div>🃏 Карты бота: A A</div>
<div>💰 Банк: 150 | Ставка: 50</div>
<div>💬 Вопрос: "${testMessage}"</div>
</div>
<div class="test-response">
<strong>Ответ бота:</strong>
<div>"${response}"</div>
</div>
</div>
`;
} catch (error) {
contentDiv.innerHTML = `
<div class="test-error">
<strong> Ошибка теста:</strong>
<div>${error.message}</div>
</div>
`;
}
}
/**
* Тестировать промпт бота (старая функция, оставлена для совместимости)
*/
async function testBotPrompt() {
const resultDiv = document.getElementById('prompt-test-result');
const contentDiv = document.getElementById('test-result-content');
resultDiv.style.display = 'block';
contentDiv.innerHTML = '<div class="test-result-loading">Генерация тестового ответа...</div>';
const style = document.getElementById('bot-style-selector').value;
const basePrompt = document.getElementById('bot-base-prompt').value;
const chatPrompt = document.getElementById('bot-chat-prompt').value;
// Создаём тестовый контекст
const testContext = {
phase: 'flop',
pot: 150,
currentBet: 50,
communityCards: ['A♠', 'K♥', '7♦'],
myChips: 800,
myBet: 50,
players: [
{ name: 'Игрок', chips: 950, bet: 50 },
{ name: 'Тестовый бот', chips: 800, bet: 50 }
]
};
const testMessage = 'У меня хорошие карты!';
try {
// Проверяем доступность LLM
if (typeof llmChat === 'undefined' || !llmChat.getSettings().llmEnabled) {
contentDiv.innerHTML = `
<strong> LLM чат отключён</strong><br><br>
Для тестирования промпта необходимо включить LLM в настройках.<br><br>
<strong>Симуляция ответа с текущим промптом:</strong><br>
<em>"${getFallbackResponse(style, testMessage, testContext)}"</em>
`;
return;
}
// Генерируем тестовый промпт
const fullPrompt = `${basePrompt}\n\n${chatPrompt}\n\nКонтекст игры:\n${JSON.stringify(testContext, null, 2)}\n\nСообщение игрока: "${testMessage}"\n\nТвой ответ (1-2 предложения):`;
// Вызываем LLM напрямую
const response = await fetch(settings.llmApiUrl + '/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: settings.llmModel,
prompt: fullPrompt,
stream: false,
options: {
temperature: 0.8,
num_predict: 100
}
})
});
if (!response.ok) {
throw new Error('Ошибка LLM API');
}
const data = await response.json();
const botResponse = data.response.trim();
contentDiv.innerHTML = `
<strong> Тестовый ответ сгенерирован:</strong><br><br>
<div style="padding: 8px; background: var(--bg-primary); border-radius: 4px; border-left: 3px solid var(--accent-primary);">
"${botResponse}"
</div>
<br>
<small style="color: var(--text-muted);">
Контекст: Флоп [A K 7], банк 150, игрок сказал "У меня хорошие карты!"
</small>
`;
} catch (error) {
console.error('Ошибка тестирования:', error);
contentDiv.innerHTML = `
<strong> Ошибка тестирования</strong><br><br>
${error.message}<br><br>
<strong>Симуляция ответа:</strong><br>
<em>"${getFallbackResponse(style, testMessage, testContext)}"</em>
`;
}
}
/**
* Получить запасной ответ для стиля
*/
function getFallbackResponse(style, message, context) {
const examples = document.getElementById('bot-example-phrases').value.split(',');
if (examples.length > 0 && examples[0].trim()) {
return examples[Math.floor(Math.random() * examples.length)].trim();
}
const fallbacks = {
aggressive: 'Посмотрим, кто тут сильнее!',
conservative: 'Будем играть осторожно.',
tricky: 'Интересный ход...',
professional: 'Правильное решение.',
loose: 'Давай играть!',
tight: 'Хорошая рука нужна.'
};
return fallbacks[style] || 'Ок.';
}
/**
* Обновить промпты в личностях AI
*/
function updateAIPersonalityPrompts() {
if (typeof pokerAI === 'undefined' || !pokerAI.personalities) return;
const customPrompts = JSON.parse(localStorage.getItem('customBotPrompts') || '{}');
pokerAI.personalities.forEach(personality => {
const custom = customPrompts[personality.style];
if (custom && custom.base) {
personality.systemPrompt = custom.base;
personality.chatPrompt = custom.chat;
personality.emotionPrompt = custom.emotion;
personality.congratsPrompt = custom.congrats;
personality.examplePhrases = custom.examples ? custom.examples.split(',').map(p => p.trim()) : [];
}
});
}
/**
* Инициализация кастомных промптов при загрузке
*/
function initCustomPrompts() {
// Загружаем и применяем кастомные промпты если они есть
updateAIPersonalityPrompts();
// Инициализируем селектор персональностей
const selector = document.getElementById('bot-personality-selector');
if (selector) {
initBotPersonalitySelector();
}
}

View File

@ -709,6 +709,327 @@ body::before {
transform: rotate(180deg);
}
/* =============================================================================
GAME INFO PANEL (Правила и статистика)
============================================================================= */
.game-info-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 350px;
max-height: 500px;
z-index: 100;
overflow: hidden;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding-bottom: 12px;
border-bottom: 1px solid var(--glass-border);
margin-bottom: 12px;
font-weight: 600;
}
.info-toggle {
transition: transform var(--transition-fast);
}
.info-body {
display: none;
max-height: 430px;
overflow-y: auto;
}
.game-info-panel.expanded .info-body {
display: block;
}
.game-info-panel.expanded .info-toggle {
transform: rotate(180deg);
}
/* Вкладки информации */
.info-tabs {
display: flex;
gap: 4px;
background: var(--bg-tertiary);
padding: 4px;
border-radius: var(--border-radius-sm);
margin-bottom: 16px;
}
.info-tab {
flex: 1;
padding: 8px 12px;
font-size: 12px;
font-weight: 500;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: var(--border-radius-sm);
transition: all var(--transition-fast);
}
.info-tab:hover {
color: var(--text-primary);
}
.info-tab.active {
background: var(--accent-gradient);
color: white;
}
.info-tab-content {
display: none;
}
.info-tab-content.active {
display: block;
}
/* Правила игры */
.rules-section h4 {
font-size: 13px;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.hand-rankings {
display: flex;
flex-direction: column;
gap: 6px;
}
.hand-rank-item {
display: grid;
grid-template-columns: 30px 1fr auto;
gap: 8px;
align-items: center;
padding: 8px 10px;
background: var(--bg-tertiary);
border-radius: 6px;
font-size: 11px;
transition: all var(--transition-fast);
}
.hand-rank-item:hover {
background: rgba(99, 102, 241, 0.1);
transform: translateX(3px);
}
.rank-number {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: var(--accent-gradient);
color: white;
border-radius: 50%;
font-weight: 700;
font-size: 10px;
}
.rank-name {
font-weight: 600;
color: var(--text-primary);
}
.rank-example {
font-family: monospace;
color: var(--text-muted);
font-size: 10px;
white-space: nowrap;
}
/* Статистика */
.stats-section h4 {
font-size: 13px;
font-weight: 600;
margin-bottom: 12px;
margin-top: 16px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stats-section h4:first-child {
margin-top: 0;
}
/* Текущая рука */
.current-hand-display {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
}
.hand-cards-preview {
display: flex;
gap: 6px;
justify-content: center;
margin-bottom: 8px;
min-height: 30px;
align-items: center;
font-size: 12px;
color: var(--text-muted);
}
.hand-cards-preview .mini-card {
width: 28px;
height: 40px;
background: white;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.hand-cards-preview .mini-card.hearts,
.hand-cards-preview .mini-card.diamonds {
color: var(--suit-hearts);
}
.hand-cards-preview .mini-card.clubs,
.hand-cards-preview .mini-card.spades {
color: var(--suit-clubs);
}
.hand-strength-display {
text-align: center;
font-weight: 600;
color: var(--accent-primary);
font-size: 13px;
}
/* Вероятность победы */
.win-probability {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
}
.probability-bar-container {
width: 100%;
height: 24px;
background: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
margin-bottom: 8px;
position: relative;
}
.probability-bar {
height: 100%;
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
border-radius: 12px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
color: white;
font-size: 11px;
font-weight: 700;
}
.probability-bar.low {
background: linear-gradient(90deg, #ef4444 0%, #dc2626 100%);
}
.probability-bar.medium {
background: linear-gradient(90deg, #f59e0b 0%, #d97706 100%);
}
.probability-bar.high {
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
}
.probability-text {
text-align: center;
font-weight: 700;
font-size: 18px;
color: var(--text-primary);
}
/* Шансы улучшения */
.improvement-odds {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
}
.odds-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--glass-border);
font-size: 12px;
}
.odds-item:last-child {
border-bottom: none;
}
.odds-label {
color: var(--text-secondary);
}
.odds-value {
font-weight: 600;
color: var(--text-primary);
}
/* Статистика сессии */
.session-stats {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 12px;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--glass-border);
font-size: 12px;
}
.stat-row:last-child {
border-bottom: none;
}
.stat-label {
color: var(--text-secondary);
}
.stat-value {
font-weight: 600;
color: var(--text-primary);
}
.stat-highlight {
color: var(--warning);
font-size: 14px;
}
/* =============================================================================
POKER TABLE
============================================================================= */
@ -1613,6 +1934,27 @@ body::before {
bottom: 10px;
}
/* Панель информации на мобильных */
.game-info-panel {
width: 280px;
right: 10px;
bottom: 10px;
max-height: 400px;
}
.game-info-panel .info-body {
max-height: 330px;
}
.hand-rank-item {
grid-template-columns: 25px 1fr;
font-size: 10px;
}
.rank-example {
display: none; /* Скрываем примеры на маленьких экранах */
}
.table-info {
flex-direction: column;
gap: 8px;
@ -1687,6 +2029,37 @@ body::before {
.bet-input {
font-size: 18px;
}
/* Компактный режим для панелей на очень маленьких экранах */
.game-chat,
.game-info-panel {
width: calc(50% - 15px);
font-size: 11px;
}
.game-info-panel {
max-height: 300px;
}
.game-info-panel .info-body {
max-height: 230px;
}
/* Скрываем некоторые элементы для экономии места */
.hand-rankings {
gap: 4px;
}
.hand-rank-item {
padding: 6px 8px;
font-size: 9px;
}
.rank-number {
width: 20px;
height: 20px;
font-size: 9px;
}
}
/* =============================================================================
@ -1964,6 +2337,76 @@ select.input {
padding-right: 36px;
}
/* Textarea для промптов */
.prompt-textarea {
min-height: 100px;
resize: vertical;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
}
.prompt-actions {
display: flex;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap;
}
.btn-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
}
.btn-success:hover {
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.5);
transform: translateY(-2px);
}
/* Результат теста промпта */
.prompt-test-result {
margin-top: 24px;
padding: 16px;
background: var(--bg-tertiary);
border-radius: 8px;
border: 1px solid var(--glass-border);
}
.prompt-test-result h4 {
font-size: 14px;
margin-bottom: 12px;
color: var(--text-secondary);
}
.test-result-content {
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
font-size: 13px;
line-height: 1.6;
color: var(--text-primary);
white-space: pre-wrap;
word-wrap: break-word;
}
.test-result-loading {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-muted);
}
.test-result-loading::before {
content: '⏳';
animation: spin 2s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Анимация появления */
@keyframes fadeInUp {
from {