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:
parent
ca4e1fd087
commit
4c1aa87905
|
|
@ -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
|
||||||
117
public/ai.js
117
public/ai.js
|
|
@ -28,11 +28,16 @@ const pokerAI = {
|
||||||
Правила ответов:
|
Правила ответов:
|
||||||
- ТОЛЬКО 1-2 коротких предложения, как будто говоришь вслух за столом
|
- ТОЛЬКО 1-2 коротких предложения, как будто говоришь вслух за столом
|
||||||
- Реагируй на действия игрока и ситуацию в игре (смотри контекст ниже!)
|
- Реагируй на действия игрока и ситуацию в игре (смотри контекст ниже!)
|
||||||
- Комментируй КОНКРЕТНУЮ игровую ситуацию: свои карты, карты на столе, ставки соперников
|
- Комментируй карты НА СТОЛЕ, банк, ставки соперников
|
||||||
- Можешь подначивать, блефовать словами, пугать олл-ином
|
- Можешь подначивать, блефовать словами, пугать олл-ином
|
||||||
- НЕ объясняй правила покера, НЕ давай советы как ассистент
|
- НЕ объясняй правила покера, НЕ давай советы как ассистент
|
||||||
- Говори по-русски, неформально
|
- Говори по-русски, неформально
|
||||||
- Используй информацию о раздаче для правдоподобных комментариев`
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои карты на руках (например "У меня туз-король")
|
||||||
|
- НИКОГДА не раскрывай силу своей руки конкретно
|
||||||
|
- НИКОГДА не говори что у тебя "пара", "флеш", "стрит" и т.д.
|
||||||
|
- Можешь ТОЛЬКО намекать общими фразами: "неплохие карты", "посмотрим", "может повезло"`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Анна "Блефер"',
|
name: 'Анна "Блефер"',
|
||||||
|
|
@ -52,10 +57,14 @@ const pokerAI = {
|
||||||
- ТОЛЬКО 1-2 коротких предложения
|
- ТОЛЬКО 1-2 коротких предложения
|
||||||
- Держи интригу, отвечай загадочно
|
- Держи интригу, отвечай загадочно
|
||||||
- Смотри на карты на столе, банк, ставки — намекай исходя из ситуации
|
- Смотри на карты на столе, банк, ставки — намекай исходя из ситуации
|
||||||
- Можешь намекать на силу/слабость руки (это тоже блеф)
|
- Можешь намекать на возможную силу руки, но НИКОГДА не раскрывай конкретно
|
||||||
- Улыбайся мысленно, будь обаятельно-опасной
|
- Улыбайся мысленно, будь обаятельно-опасной
|
||||||
- Говори по-русски
|
- Говори по-русски
|
||||||
- Используй игровой контекст для загадочных намёков`
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои карты (например "У меня дама-валет")
|
||||||
|
- НИКОГДА не раскрывай конкретную комбинацию ("У меня флеш", "У меня две пары")
|
||||||
|
- Можешь ТОЛЬКО намекать: "интересные карты", "может повезло", "увидите на вскрытии"`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Дед Михалыч',
|
name: 'Дед Михалыч',
|
||||||
|
|
@ -76,10 +85,14 @@ const pokerAI = {
|
||||||
Правила ответов:
|
Правила ответов:
|
||||||
- 1-2 коротких предложения
|
- 1-2 коротких предложения
|
||||||
- Можешь вспомнить историю из прошлого или вставить "эх, молодёжь"
|
- Можешь вспомнить историю из прошлого или вставить "эх, молодёжь"
|
||||||
- Реагируй на ситуацию за столом, смотри на карты и ставки
|
- Реагируй на ситуацию за столом, смотри на карты на столе и ставки
|
||||||
- Комментируй ход игры по-стариковски мудро
|
- Комментируй ход игры по-стариковски мудро
|
||||||
- Говори тепло и по-человечески
|
- Говори тепло и по-человечески
|
||||||
- Используй контекст игры для житейских комментариев`
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои карты ("У меня две дамы", "Мне пришли тузы")
|
||||||
|
- НИКОГДА не раскрывай свою комбинацию до вскрытия
|
||||||
|
- Говори общими фразами: "карты нормальные", "бывало и лучше", "поглядим"`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Макс "ГТО"',
|
name: 'Макс "ГТО"',
|
||||||
|
|
@ -97,10 +110,15 @@ const pokerAI = {
|
||||||
|
|
||||||
Правила ответов:
|
Правила ответов:
|
||||||
- ТОЛЬКО 1-2 коротких предложения
|
- ТОЛЬКО 1-2 коротких предложения
|
||||||
- Можешь упомянуть шансы или +EV, опираясь на карты на столе и банк
|
- Можешь упомянуть шансы банка или +EV, глядя на карты на столе и размер банка
|
||||||
- Реагируй на игру с точки зрения математики
|
- Реагируй на игру с точки зрения математики
|
||||||
- Анализируй конкретную ситуацию (смотри контекст)
|
- Анализируй ситуацию, но НЕ раскрывай свои карты
|
||||||
- Говори по-русски, можно с англицизмами`
|
- Говори по-русски, можно с англицизмами
|
||||||
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои конкретные карты ("У меня АК", "Карманные короли")
|
||||||
|
- НИКОГДА не говори свою комбинацию ("У меня сет", "Собрал флеш")
|
||||||
|
- Можешь говорить об equity и диапазонах ОБЩО, без раскрытия своих карт`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Катя "Удача"',
|
name: 'Катя "Удача"',
|
||||||
|
|
@ -120,8 +138,13 @@ const pokerAI = {
|
||||||
- ТОЛЬКО 1-2 коротких предложения
|
- ТОЛЬКО 1-2 коротких предложения
|
||||||
- Используй эмодзи уместно: 🍀✨😊🎲
|
- Используй эмодзи уместно: 🍀✨😊🎲
|
||||||
- Реагируй эмоционально на игру и карты на столе
|
- Реагируй эмоционально на игру и карты на столе
|
||||||
- Комментируй удачу/неудачу исходя из ситуации
|
- Комментируй удачу/неудачу в общем
|
||||||
- Говори по-русски, живо`
|
- Говори по-русски, живо
|
||||||
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои карты ("Мне пришли тузы!")
|
||||||
|
- НИКОГДА не раскрывай свою комбинацию ("У меня флеш!", "Собралась пара!")
|
||||||
|
- Говори о чувствах и удаче: "чувствую карта придёт", "удача со мной", "загадала желание"`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Борис "Молчун"',
|
name: 'Борис "Молчун"',
|
||||||
|
|
@ -135,13 +158,18 @@ const pokerAI = {
|
||||||
- Говоришь ОЧЕНЬ мало
|
- Говоришь ОЧЕНЬ мало
|
||||||
- Загадочный, никто не знает что у тебя на уме
|
- Загадочный, никто не знает что у тебя на уме
|
||||||
- Отвечаешь односложно или просто молчишь
|
- Отвечаешь односложно или просто молчишь
|
||||||
- Можешь кивнуть на конкретную карту или ситуацию
|
- Можешь кивнуть на конкретную карту на столе или ситуацию
|
||||||
|
|
||||||
Правила ответов:
|
Правила ответов:
|
||||||
- МАКСИМУМ 1-3 слова или многоточие
|
- МАКСИМУМ 1-3 слова или многоточие
|
||||||
- Примеры: "Хм.", "Нет.", "Посмотрим.", "...", "Да.", "Флоп интересный."
|
- Примеры: "Хм.", "Нет.", "Посмотрим.", "...", "Да.", "Флоп интересный."
|
||||||
- НИКОГДА не говори длинно
|
- НИКОГДА не говори длинно
|
||||||
- Молчание — твоё оружие`
|
- Молчание — твоё оружие
|
||||||
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои карты (даже намёком)
|
||||||
|
- НИКОГДА не раскрывай комбинацию
|
||||||
|
- Максимум что можешь: "Хм.", "...", "Увидим."`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Олег "Тильтер"',
|
name: 'Олег "Тильтер"',
|
||||||
|
|
@ -159,10 +187,15 @@ const pokerAI = {
|
||||||
|
|
||||||
Правила ответов:
|
Правила ответов:
|
||||||
- ТОЛЬКО 1-2 коротких предложения
|
- ТОЛЬКО 1-2 коротких предложения
|
||||||
- Можешь ворчать, возмущаться, жаловаться на конкретные карты
|
- Можешь ворчать, возмущаться, жаловаться на карты НА СТОЛЕ
|
||||||
- Реагируй эмоционально на плохие карты/биты, смотря на ситуацию
|
- Реагируй эмоционально на плохие общие карты/биты
|
||||||
- Комментируй несправедливость раздачи
|
- Комментируй несправедливость раздачи в общем
|
||||||
- Говори по-русски, экспрессивно`
|
- Говори по-русски, экспрессивно
|
||||||
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои карты ("У меня были короли!")
|
||||||
|
- НИКОГДА не раскрывай свою комбинацию ("Мне собрался стрит!")
|
||||||
|
- Жалуйся на удачу соперников и общие карты, но НЕ раскрывай свои`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ирина "Профи"',
|
name: 'Ирина "Профи"',
|
||||||
|
|
@ -181,9 +214,14 @@ const pokerAI = {
|
||||||
Правила ответов:
|
Правила ответов:
|
||||||
- ТОЛЬКО 1-2 коротких предложения
|
- ТОЛЬКО 1-2 коротких предложения
|
||||||
- Говори спокойно, профессионально
|
- Говори спокойно, профессионально
|
||||||
- Можешь прокомментировать интересный розыгрыш, ссылаясь на ситуацию
|
- Можешь прокомментировать интересный розыгрыш в общем
|
||||||
- Анализируй конкретную раздачу если нужно
|
- Анализируй текстуру борда, размеры ставок
|
||||||
- Говори по-русски, корректно`
|
- Говори по-русски, корректно
|
||||||
|
|
||||||
|
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||||
|
- НИКОГДА не называй свои карты ("У меня AQ")
|
||||||
|
- НИКОГДА не раскрывай свою комбинацию ("Собрала топ-пару", "У меня дро")
|
||||||
|
- Можешь анализировать ситуацию профессионально, но без раскрытия своих карт`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -1363,8 +1401,12 @@ ${isWin ? 'Покажи радость, удовлетворение или са
|
||||||
const isStrongHand = handRank >= 7; // Фулл-хаус и выше
|
const isStrongHand = handRank >= 7; // Фулл-хаус и выше
|
||||||
const isVeryStrongHand = handRank >= 9; // Стрит-флеш и роял-флеш
|
const isVeryStrongHand = handRank >= 9; // Стрит-флеш и роял-флеш
|
||||||
|
|
||||||
|
// Получаем кастомный промпт если есть
|
||||||
|
const customPrompt = this.getCustomPrompt(botPersonality.style, 'congrats');
|
||||||
|
const basePrompt = customPrompt || botPersonality.congratsPrompt || botPersonality.systemPrompt;
|
||||||
|
|
||||||
// Формируем промпт для LLM
|
// Формируем промпт для LLM
|
||||||
const congratsPrompt = `${botPersonality.systemPrompt}
|
const congratsPrompt = `${basePrompt}
|
||||||
|
|
||||||
СИТУАЦИЯ: Игрок ${playerName} только что ВЫИГРАЛ раздачу с рукой "${handName}"! Банк: ${potSize} фишек.
|
СИТУАЦИЯ: Игрок ${playerName} только что ВЫИГРАЛ раздачу с рукой "${handName}"! Банк: ${potSize} фишек.
|
||||||
|
|
||||||
|
|
@ -1384,14 +1426,45 @@ ${isVeryStrongHand ? 'Покажи восхищение и уважение!' :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запасные поздравления
|
// Запасные поздравления (используем кастомные примеры если есть)
|
||||||
return this.getFallbackCongratulation(botPersonality.style, handRank, handName);
|
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) {
|
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 = {
|
const congratulations = {
|
||||||
aggressive: {
|
aggressive: {
|
||||||
normal: ['Неплохо сыграно!', 'Уважаю!', 'Сильная рука!', 'Молодец!'],
|
normal: ['Неплохо сыграно!', 'Уважаю!', 'Сильная рука!', 'Молодец!'],
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,134 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="game-controls">
|
||||||
<button class="btn btn-icon-only" onclick="toggleSound()">🔊</button>
|
<button class="btn btn-icon-only" onclick="toggleSound()">🔊</button>
|
||||||
|
|
@ -502,6 +630,7 @@
|
||||||
|
|
||||||
<div class="admin-tabs">
|
<div class="admin-tabs">
|
||||||
<button class="btn btn-tab active" onclick="switchAdminTab('settings')">⚙️ Настройки</button>
|
<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('logs')">📋 Логи</button>
|
||||||
<button class="btn btn-tab" onclick="switchAdminTab('users')">👥 Пользователи</button>
|
<button class="btn btn-tab" onclick="switchAdminTab('users')">👥 Пользователи</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -547,6 +676,56 @@
|
||||||
<button class="btn btn-secondary" onclick="testAdminLLM()">🔗 Проверить подключение</button>
|
<button class="btn btn-secondary" onclick="testAdminLLM()">🔗 Проверить подключение</button>
|
||||||
</div>
|
</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;">
|
<div id="admin-tab-logs" class="admin-tab-content" style="display: none;">
|
||||||
<h3>📋 История действий</h3>
|
<h3>📋 История действий</h3>
|
||||||
|
|
|
||||||
802
public/main.js
802
public/main.js
|
|
@ -49,6 +49,7 @@ const sounds = {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
loadLeaderboard();
|
loadLeaderboard();
|
||||||
|
loadSessionStats();
|
||||||
initSounds();
|
initSounds();
|
||||||
loadCardBackSettings();
|
loadCardBackSettings();
|
||||||
|
|
||||||
|
|
@ -60,6 +61,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Инициализируем кастомные промпты
|
||||||
|
setTimeout(() => {
|
||||||
|
initCustomPrompts();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// Инициализируем авторизацию
|
// Инициализируем авторизацию
|
||||||
if (typeof initAuth === 'function') {
|
if (typeof initAuth === 'function') {
|
||||||
initAuth();
|
initAuth();
|
||||||
|
|
@ -581,6 +587,9 @@ function updateGameUI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActionPanel(player, game);
|
updateActionPanel(player, game);
|
||||||
|
|
||||||
|
// Обновляем статистику
|
||||||
|
updateGameStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -621,6 +630,9 @@ function updateGameUIFromServer(room) {
|
||||||
updateActionPanelFromServer(myPlayer, room);
|
updateActionPanelFromServer(myPlayer, room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем статистику (для мультиплеера используем упрощенную версию)
|
||||||
|
updateGameStats();
|
||||||
|
|
||||||
// Показываем кнопку новой раздачи если игра завершена
|
// Показываем кнопку новой раздачи если игра завершена
|
||||||
if (room.gamePhase === 'showdown' || !room.isGameStarted) {
|
if (room.gamePhase === 'showdown' || !room.isGameStarted) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -1105,6 +1117,9 @@ function onHandEnd(result) {
|
||||||
// Обновляем лидерборд
|
// Обновляем лидерборд
|
||||||
updateLeaderboard(result);
|
updateLeaderboard(result);
|
||||||
|
|
||||||
|
// Обновляем статистику сессии
|
||||||
|
updateStatsOnHandEnd(result);
|
||||||
|
|
||||||
// НОВОЕ: Генерируем эмоциональные реакции ботов
|
// НОВОЕ: Генерируем эмоциональные реакции ботов
|
||||||
if (!isMultiplayer && game) {
|
if (!isMultiplayer && game) {
|
||||||
generateBotEmotions(result);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -709,6 +709,327 @@ body::before {
|
||||||
transform: rotate(180deg);
|
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
|
POKER TABLE
|
||||||
============================================================================= */
|
============================================================================= */
|
||||||
|
|
@ -1613,6 +1934,27 @@ body::before {
|
||||||
bottom: 10px;
|
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 {
|
.table-info {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
@ -1687,6 +2029,37 @@ body::before {
|
||||||
.bet-input {
|
.bet-input {
|
||||||
font-size: 18px;
|
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;
|
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 {
|
@keyframes fadeInUp {
|
||||||
from {
|
from {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue