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
80032b83b1
commit
e84717a79d
|
|
@ -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 коротких предложения, как будто говоришь вслух за столом
|
||||
- Реагируй на действия игрока и ситуацию в игре (смотри контекст ниже!)
|
||||
- Комментируй КОНКРЕТНУЮ игровую ситуацию: свои карты, карты на столе, ставки соперников
|
||||
- Комментируй карты НА СТОЛЕ, банк, ставки соперников
|
||||
- Можешь подначивать, блефовать словами, пугать олл-ином
|
||||
- НЕ объясняй правила покера, НЕ давай советы как ассистент
|
||||
- Говори по-русски, неформально
|
||||
- Используй информацию о раздаче для правдоподобных комментариев`
|
||||
|
||||
🚫 СТРОГО ЗАПРЕЩЕНО:
|
||||
- НИКОГДА не называй свои карты на руках (например "У меня туз-король")
|
||||
- НИКОГДА не раскрывай силу своей руки конкретно
|
||||
- НИКОГДА не говори что у тебя "пара", "флеш", "стрит" и т.д.
|
||||
- Можешь ТОЛЬКО намекать общими фразами: "неплохие карты", "посмотрим", "может повезло"`
|
||||
},
|
||||
{
|
||||
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: ['Неплохо сыграно!', 'Уважаю!', 'Сильная рука!', 'Молодец!'],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
802
public/main.js
802
public/main.js
|
|
@ -49,6 +49,7 @@ const sounds = {
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadSettings();
|
||||
loadLeaderboard();
|
||||
loadSessionStats();
|
||||
initSounds();
|
||||
loadCardBackSettings();
|
||||
|
||||
|
|
@ -60,6 +61,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
}
|
||||
|
||||
// Инициализируем кастомные промпты
|
||||
setTimeout(() => {
|
||||
initCustomPrompts();
|
||||
}, 100);
|
||||
|
||||
// Инициализируем авторизацию
|
||||
if (typeof initAuth === 'function') {
|
||||
initAuth();
|
||||
|
|
@ -581,6 +587,9 @@ function updateGameUI() {
|
|||
}
|
||||
|
||||
updateActionPanel(player, game);
|
||||
|
||||
// Обновляем статистику
|
||||
updateGameStats();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -621,6 +630,9 @@ function updateGameUIFromServer(room) {
|
|||
updateActionPanelFromServer(myPlayer, room);
|
||||
}
|
||||
|
||||
// Обновляем статистику (для мультиплеера используем упрощенную версию)
|
||||
updateGameStats();
|
||||
|
||||
// Показываем кнопку новой раздачи если игра завершена
|
||||
if (room.gamePhase === 'showdown' || !room.isGameStarted) {
|
||||
setTimeout(() => {
|
||||
|
|
@ -1105,6 +1117,9 @@ function onHandEnd(result) {
|
|||
// Обновляем лидерборд
|
||||
updateLeaderboard(result);
|
||||
|
||||
// Обновляем статистику сессии
|
||||
updateStatsOnHandEnd(result);
|
||||
|
||||
// НОВОЕ: Генерируем эмоциональные реакции ботов
|
||||
if (!isMultiplayer && game) {
|
||||
generateBotEmotions(result);
|
||||
|
|
@ -2141,3 +2156,790 @@ function checkRoomInUrl() {
|
|||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ПАНЕЛЬ ИНФОРМАЦИИ И СТАТИСТИКИ
|
||||
// =============================================================================
|
||||
|
||||
// Статистика сессии
|
||||
let sessionStats = {
|
||||
handsPlayed: 0,
|
||||
handsWon: 0,
|
||||
totalWon: 0,
|
||||
bestHandRank: 0,
|
||||
bestHandName: '—'
|
||||
};
|
||||
|
||||
/**
|
||||
* Переключить панель информации
|
||||
*/
|
||||
function toggleGameInfo() {
|
||||
document.getElementById('game-info-panel').classList.toggle('expanded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить вкладку информации
|
||||
*/
|
||||
function switchInfoTab(tabName) {
|
||||
// Убираем active со всех вкладок
|
||||
document.querySelectorAll('.info-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.info-tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// Активируем выбранную
|
||||
event.target.classList.add('active');
|
||||
document.getElementById(`info-tab-${tabName}`).classList.add('active');
|
||||
|
||||
// Обновляем статистику при переключении
|
||||
if (tabName === 'stats') {
|
||||
updateGameStats();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить отображение текущей руки в статистике
|
||||
*/
|
||||
function updateCurrentHandDisplay() {
|
||||
const previewContainer = document.getElementById('hand-cards-preview');
|
||||
const strengthDisplay = document.getElementById('hand-strength-display');
|
||||
|
||||
// Получаем текущие карты игрока
|
||||
let playerHand = [];
|
||||
let communityCards = [];
|
||||
|
||||
if (isMultiplayer) {
|
||||
// В мультиплеере данные берем из последнего обновления
|
||||
// Это нужно реализовать через сохранение состояния
|
||||
previewContainer.textContent = 'Обновление...';
|
||||
strengthDisplay.textContent = '—';
|
||||
return;
|
||||
} else if (game && game.isGameStarted) {
|
||||
const player = game.players.find(p => p.id === currentPlayerId);
|
||||
if (player && player.hand.length > 0) {
|
||||
playerHand = player.hand;
|
||||
communityCards = game.communityCards || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (playerHand.length === 0) {
|
||||
previewContainer.textContent = 'Карты не розданы';
|
||||
strengthDisplay.textContent = '—';
|
||||
return;
|
||||
}
|
||||
|
||||
// Отображаем карты
|
||||
previewContainer.innerHTML = '';
|
||||
playerHand.forEach(card => {
|
||||
const miniCard = document.createElement('div');
|
||||
miniCard.className = `mini-card ${card.suit}`;
|
||||
miniCard.innerHTML = `
|
||||
<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);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue