feat: add player balance display and congratulatory system for strong hands

- Implemented a player info row displaying the player's name and balance in the game interface.
- Added animations for balance changes (increase/decrease) to enhance user experience.
- Introduced a congratulatory system where bots congratulate players for strong hands based on hand strength probabilities.
- Updated styles for player balance display and added relevant CSS animations.
- Enhanced game logic to trigger congratulatory messages from bots after a hand concludes.
- Documented the new congratulatory system and player balance display features.
This commit is contained in:
ur002 2026-02-01 19:12:06 +03:00
parent eec780d8af
commit 24e457885d
10 changed files with 1006 additions and 40 deletions

197
CONGRATULATIONS_SYSTEM.md Normal file
View File

@ -0,0 +1,197 @@
# Система поздравлений игрока от ботов
## 🎯 Описание
Боты теперь **поздравляют игрока**, когда он выигрывает с сильной покерной рукой! Вероятность поздравления зависит от редкости и силы руки.
---
## 📊 Вероятности поздравлений по силе руки
| Рука | Ранг | Вероятность | Пример |
|------|------|-------------|--------|
| **Роял-флеш** | 10 | 90% | "A♠ K♠ Q♠ J♠ 10♠ - Легенда!" |
| **Стрит-флеш** | 9 | 80% | "9♥ 8♥ 7♥ 6♥ 5♥ - Невероятно!" |
| **Каре** | 8 | 60% | "K♠ K♥ K♦ K♣ - Впечатляет!" |
| **Фулл-хаус** | 7 | 40% | "Q♠ Q♥ Q♦ 5♠ 5♥ - Красиво!" |
| **Флеш** | 6 | 25% | "A♠ J♠ 9♠ 7♠ 3♠ - Неплохо!" |
| **Стрит** | 5 | 15% | "10♦ 9♠ 8♥ 7♣ 6♦ - Хорошо!" |
| Сет и ниже | ≤4 | 0% | Не поздравляют |
---
## 🤖 Стили поздравлений от разных ботов
### 1. **Виктор "Акула"** (агрессивный)
- **Обычная рука:** "Неплохо сыграно!", "Уважаю!"
- **Сильная:** "Вау, фулл-хаус! 💪", "Монстр-рука!"
- **Очень сильная:** "Роял-флеш?! Респект! 🔥", "НЕВЕРОЯТНО!"
### 2. **Анна "Блефер"** (загадочная)
- **Обычная:** "Интересно сыграно... 😏"
- **Сильная:** "Фулл-хаус? Впечатляет! 👏"
- **Очень сильная:** "Стрит-флеш... Я в восторге! 😮"
### 3. **Дед Михалыч** (старая школа)
- **Обычная:** "Молодец, голубчик!"
- **Сильная:** "Фулл-хаус! Красота! 👴"
- **Очень сильная:** "Роял-флеш! За 50 лет такое редко видел! 😲"
### 4. **Макс "ГТО"** (математик)
- **Обычная:** "Хороший EV!"
- **Сильная:** "Фулл-хаус! Rare! 📊", "Топ 0.1% рук!"
- **Очень сильная:** "Роял-флеш... 0.00154%! 🤯"
### 5. **Катя "Удача"** (суеверная)
- **Обычная:** "Ура! Молодец! 😊"
- **Сильная:** "Фулл-хаус! Удача! 🍀✨"
- **Очень сильная:** "Роял-флеш! ЭТО МАГИЯ! 🎉🍀"
### 6. **Борис "Молчун"** (молчаливый)
- **Обычная:** "Хм. Неплохо."
- **Сильная:** "Фулл-хаус... Ого."
- **Очень сильная:** "Роял-флеш?! ..."
### 7. **Олег "Тильтер"** (эмоциональный)
- **Обычная:** "Ну везёт же... 😒"
- **Сильная:** "Фулл-хаус?! Ладно, красиво 😤"
- **Очень сильная:** "Роял-флеш... Я не верю! 😱"
### 8. **Ирина "Профи"** (профессионал)
- **Обычная:** "GG WP!"
- **Сильная:** "Фулл-хаус. Респект! 💎"
- **Очень сильная:** "Роял-флеш! Incredible! 🎯"
---
## 🎬 Примеры игровых ситуаций
### Ситуация 1: Флеш (25% вероятность)
```
Игрок выигрывает с: A♠ K♠ 9♠ 7♠ 3♠ (Флеш)
Банк: 250 фишек
🎲 Случайный бот (25% шанс):
Макс "ГТО": "Флеш! Статистически сильно! 📊"
```
### Ситуация 2: Фулл-хаус (40% вероятность)
```
Игрок выигрывает с: K♠ K♥ K♦ 8♠ 8♥ (Фулл-хаус)
Банк: 500 фишек
🎲 Случайный бот (40% шанс):
Дед Михалыч: "Фулл-хаус! Красота! Как в старые времена! 👴"
```
### Ситуация 3: Каре (60% вероятность)
```
Игрок выигрывает с: A♠ A♥ A♦ A♣ Q♠ (Каре тузов)
Банк: 800 фишек
🎲 Случайный бот (60% шанс):
Виктор "Акула": "Каре тузов! МОНСТР-РУКА! 💪🔥"
```
### Ситуация 4: Стрит-флеш (80% вероятность)
```
Игрок выигрывает с: 9♥ 8♥ 7♥ 6♥ 5♥ (Стрит-флеш)
Банк: 1200 фишек
🎲 Случайный бот (80% шанс):
Анна "Блефер": "Стрит-флеш до девятки... Я в восторге! 😮✨"
```
### Ситуация 5: Роял-флеш (90% вероятность)
```
Игрок выигрывает с: A♠ K♠ Q♠ J♠ 10♠ (Роял-флеш!)
Банк: 2000 фишек
🎲 Случайный бот (90% шанс):
Макс "ГТО": "РОЯЛ-ФЛЕШ! Математическое чудо! Вероятность 0.00154%! 🤯🎯"
```
---
## ⚙️ Технические детали
### Алгоритм работы:
1. **Проверка победителя**
- Игрок победил? (не бот)
- Есть информация о руке?
2. **Определение вероятности**
- Ранг руки ≥ 5 (стрит или выше)
- Вероятность от 15% до 90%
3. **Выбор бота**
- Случайный бот из активных (не спасовавших)
- Учитывается его личность
4. **Генерация поздравления**
- С помощью LLM (если включён)
- Или запасные фразы по стилю бота
5. **Отправка в чат**
- Задержка 1-2.5 секунды
- Не конфликтует с эмоциональными реакциями
### Интеграция с LLM:
При включенном LLM бот получает контекст:
```
Игрок [Имя] только что ВЫИГРАЛ с рукой "Фулл-хаус"!
Банк: 500 фишек
Это сильная рука!
Поздравь игрока КРАТКО (максимум 5-7 слов), в своём стиле.
```
---
## 🎮 Примеры с LLM (если включён)
**Виктор "Акула":**
- Флеш: "С флешем на терне? Красиво прочитал борд! 🔥"
- Каре: "Каре на ривере?! Вот это хладнокровие! 💪"
**Дед Михалыч:**
- Фулл-хаус: "Эх, молодость! С таким фулл-хаусом я бы в 82-м весь турнир взял!"
- Стрит-флеш: "Святые угодники! Такое в жизни раз видел!"
**Макс "ГТО":**
- Флеш: "Флеш на этой текстуре борда - +EV колл!"
- Каре: "Четыре аута превратились в каре. Probability magic! 📊"
---
## 📝 Файлы изменены
1. **`ai.js`**
- Добавлена `generatePlayerCongratulation()` - генерация поздравления через LLM
- Добавлена `getFallbackCongratulation()` - запасные фразы для каждого стиля
2. **`main.js`**
- Добавлена `generatePlayerCongratulations()` - логика выбора бота и отправки
- Вызывается в `onHandEnd()` после завершения раздачи
---
## 🎯 Результат
Теперь игра стала **более живой и реалистичной**:
- Боты замечают и ценят сильные руки игрока
- Редкие комбинации вызывают восхищение
- Каждый бот поздравляет в своём уникальном стиле
- LLM делает поздравления ещё более контекстуальными
**Пример полного взаимодействия:**
```
[Игрок собрал каре королей и выиграл банк 800 фишек]
Макс "ГТО": "Каре королей! Топ 0.024% рук! 📊" (через 1.2 сек)
```
🎰🃏 **Играйте и получайте заслуженные поздравления!**

128
LLM_CONTEXT_EXAMPLE.md Normal file
View File

@ -0,0 +1,128 @@
# Пример контекста для LLM
## Что теперь видит бот при ответе на сообщение игрока
### Пример 1: Флоп в игре
**Игрок пишет:** "Виктор, что думаешь?"
**LLM получает следующий контекст:**
```
📍 Фаза: флоп (3 карты на столе)
🃏 Карты на столе: K♥ 9♦ 2♣
💰 Банк: 150 фишек
💵 Текущая ставка: 30 фишек
🤖 Мои данные:
- Имя: Виктор "Акула"
- Карты: A♠ K♠
- Фишки: 470
- Моя ставка: 30 (всего в раздаче: 45)
- Позиция: дилер
👥 Другие игроки:
- Игрок: 480 фишек, ставка 30 🎲 [последнее: call]
- Анна "Блефер": 350 фишек [спасовал ❌]
- Дед Михалыч: 200 фишек [последнее: fold]
✅ Активные: Виктор "Акула", Игрок
❌ Спасовали: Анна "Блефер", Дед Михалыч
💬 Игрок написал: "Виктор, что думаешь?"
```
**Возможный ответ бота:**
"С топ-парой на таком сухом флопе? Думаю, что банк мой. 😎"
---
### Пример 2: Терн, напряжённая ситуация
**Игрок пишет:** "Макс, всё плохо?"
**LLM получает:**
```
📍 Фаза: терн (4 карты на столе)
🃏 Карты на столе: 7♥ 8♦ 9♣ J♠
💰 Банк: 450 фишек
💵 Текущая ставка: 100 фишек
🤖 Мои данные:
- Имя: Макс "ГТО"
- Карты: A♣ K♣
- Фишки: 280
- Моя ставка: 50 (всего в раздаче: 165)
- Позиция: обычная позиция
👥 Другие игроки:
- Игрок: 520 фишек, ставка 100 [последнее: raise]
- Борис "Молчун": 150 фишек [спасовал ❌]
✅ Активные: Макс "ГТО", Игрок
❌ Спасовали: Борис "Молчун"
💬 Игрок написал: "Макс, всё плохо?"
```
**Возможный ответ бота:**
"На этом борде с прямым дро? Просчитываю pot odds... 🤔"
---
### Пример 3: All-in ситуация
**Игрок пишет:** "Катя, рискнёшь?"
**LLM получает:**
```
📍 Фаза: ривер (5 карт на столе)
🃏 Карты на столе: 3♥ 3♦ 8♣ Q♠ A♥
💰 Банк: 800 фишек
💵 Текущая ставка: 200 фишек
🤖 Мои данные:
- Имя: Катя "Удача"
- Карты: 3♠ 8♠
- Фишки: 195
- Моя ставка: 0 (всего в раздаче: 105)
- Позиция: малый блайнд
👥 Другие игроки:
- Игрок: 95 фишек, ставка 200 [ва-банк 💥]
✅ Активные: Катя "Удача", Игрок
❌ Спасовали:
💬 Игрок написал: "Катя, рискнёшь?"
```
**Возможный ответ бота:**
"С фулл-хаусом? Загадала желание! 🍀✨"
---
## Преимущества нового контекста
1. **Видит свои карты** - может блефовать или говорить честно
2. **Видит карты на столе** - может комментировать текстуру борда
3. **Видит действия других игроков** - может реагировать на чужие ставки
4. **Знает размер банка и ставок** - может говорить про pot odds
5. **Знает фазу игры** - может адаптировать ответ под префлоп/флоп/терн/ривер
6. **Видит кто спасовал** - может подшучивать или сочувствовать
## Примеры улучшенных ответов
### До изменений:
- Игрок: "Что думаешь?"
- Бот: "Удачи! Интересная игра."
### После изменений:
- Игрок: "Что думаешь?"
- Бот: "С тремя червами на столе и у меня флеш дро? Думаю поплыть дальше. 😏"
---
Теперь боты отвечают КОНТЕКСТУАЛЬНО, используя реальную информацию о раздаче!

155
PLAYER_BALANCE_DISPLAY.md Normal file
View File

@ -0,0 +1,155 @@
# Отображение баланса игрока
## ✅ Исправлено
Теперь игрок **видит свой баланс фишек** прямо под картами!
---
## 🎯 Что добавлено
### 1. **Визуальное отображение баланса**
На экране игры появилась **панель информации о игроке**:
```
┌────────────────────────────┐
│ Игрок 💰 1000 │ ← Имя и баланс
│ [K♠] [Q♥] │ ← Карты
│ Две старших карты │ ← Сила руки
└────────────────────────────┘
```
**Элементы:**
- **Имя игрока** - слева
- **💰 Баланс** - справа (зелёная подсветка)
- **Карты** - по центру
- **Сила руки** - снизу
---
## 🎨 Визуальные фичи
### **Анимация изменения баланса:**
1. **При увеличении фишек** (выигрыш):
- Зелёная вспышка
- Увеличение на 5%
- 0.5 секунды
2. **При уменьшении фишек** (ставка/проигрыш):
- Красная вспышка
- Уменьшение на 5%
- 0.5 секунды
**Пример:**
```
Выигрыш +500: 💰 500 → 💰 1000 (зелёная вспышка ✨)
Ставка -100: 💰 1000 → 💰 900 (красная вспышка 🔴)
```
---
## 💻 Технические детали
### **Файлы изменены:**
#### 1. `index.html`
Добавлена строка информации о игроке:
```html
<div class="player-info-row">
<div class="player-name-display" id="player-name-display">Игрок</div>
<div class="player-balance-display">
<span class="balance-icon">💰</span>
<span class="balance-amount" id="player-balance">1000</span>
</div>
</div>
```
#### 2. `styles.css`
Добавлены стили:
- `.player-info-row` - контейнер для имени и баланса
- `.player-name-display` - стиль имени
- `.player-balance-display` - значок и сумма
- `@keyframes balanceIncrease` - анимация увеличения
- `@keyframes balanceDecrease` - анимация уменьшения
#### 3. `main.js`
Добавлена функция `updatePlayerBalance()`:
```javascript
function updatePlayerBalance(player) {
// Обновляет имя и баланс
// Добавляет анимацию при изменении
// Отслеживает предыдущее значение
}
```
Интеграция в:
- `updateGameUI()` - одиночная игра
- `updateGameUIFromServer()` - мультиплеер
---
## 🎮 Примеры в действии
### **Префлоп:**
```
┌────────────────────────────┐
│ Игрок 💰 1000 │
│ [A♠] [K♠] │
│ Старшие карты │
└────────────────────────────┘
Ставка Big Blind 10 →
┌────────────────────────────┐
│ Игрок 💰 990 🔴 │ ← Красная вспышка
│ [A♠] [K♠] │
│ Старшие карты │
└────────────────────────────┘
```
### **Выигрыш банка:**
```
Банк: 500 фишек
Победа с флешем!
┌────────────────────────────┐
│ Игрок 💰 1490 ✨ │ ← Зелёная вспышка
│ [A♠] [K♠] │
│ Флеш, туз старший │
└────────────────────────────┘
```
### **All-in:**
```
┌────────────────────────────┐
│ Игрок 💰 0 🔴 │ ← Баланс 0 после all-in
│ [Q♥] [Q♦] │
│ Пара дам │
└────────────────────────────┘
```
---
## 📊 Отличия от ботов
| Элемент | Бот (на столе) | Игрок (снизу) |
|---------|----------------|---------------|
| Имя | Маленькое, внутри бокса | Крупное, отдельная строка |
| Баланс | Под именем, серый текст | Отдельный блок, зелёный, с иконкой |
| Карты | Мини (40x56px) | Большие (70x98px) |
| Анимация | Нет | Есть (при изменении) |
| Позиция | По кругу стола | Внизу по центру |
---
## ✅ Результат
Теперь игрок **всегда видит**:
- ✅ Своё имя
- ✅ Актуальный баланс фишек
- ✅ Визуальную обратную связь при выигрыше/проигрыше
- ✅ Красивую анимацию изменения баланса
🎰💰 **Следите за своим стеком!**

View File

@ -150,6 +150,38 @@ async function createDefaultAdmin() {
saveDatabase();
console.log('👑 Создан администратор по умолчанию: admin / admin123');
}
// Инициализируем дефолтные админские настройки
initDefaultAdminSettings();
}
/**
* Инициализировать дефолтные админские настройки
*/
function initDefaultAdminSettings() {
const settingsCheck = db.exec("SELECT COUNT(*) as count FROM admin_settings");
const count = settingsCheck.length > 0 ? settingsCheck[0].values[0][0] : 0;
if (count === 0) {
const defaults = {
llmEnabled: 'false',
llmProvider: 'ollama',
llmApiUrl: 'http://localhost:11434',
llmModel: 'llama3.2',
llmApiKey: '',
serverUrl: 'ws://localhost:3000'
};
Object.entries(defaults).forEach(([key, value]) => {
db.run(`
INSERT INTO admin_settings (key, value, updated_at)
VALUES (?, ?, datetime('now'))
`, [key, value]);
});
saveDatabase();
console.log('⚙️ Инициализированы дефолтные админские настройки');
}
}
/**

BIN
poker.db

Binary file not shown.

View File

@ -27,10 +27,12 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения, как будто говоришь вслух за столом
- Реагируй на действия игрока и ситуацию в игре
- Реагируй на действия игрока и ситуацию в игре (смотри контекст ниже!)
- Комментируй КОНКРЕТНУЮ игровую ситуацию: свои карты, карты на столе, ставки соперников
- Можешь подначивать, блефовать словами, пугать олл-ином
- НЕ объясняй правила покера, НЕ давай советы как ассистент
- Говори по-русски, неформально`
- Говори по-русски, неформально
- Используй информацию о раздаче для правдоподобных комментариев`
},
{
name: 'Анна "Блефер"',
@ -49,9 +51,11 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
- Держи интригу, отвечай загадочно
- Смотри на карты на столе, банк, ставки намекай исходя из ситуации
- Можешь намекать на силу/слабость руки (это тоже блеф)
- Улыбайся мысленно, будь обаятельно-опасной
- Говори по-русски`
- Говори по-русски
- Используй игровой контекст для загадочных намёков`
},
{
name: 'Дед Михалыч',
@ -63,15 +67,19 @@ const pokerAI = {
Твой характер:
- Добродушный, мудрый, любишь поболтать
- Вспоминаешь "а вот раньше в преферанс..."
- Часто вспоминаешь старые времена, "а вот в СССР..."
- Говоришь просто, с народными выражениями
- Не спешишь, играешь обстоятельно
- Немного путаешь термины, но играешь хитро
- К молодёжи относишься снисходительно, но по-доброму
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
- Можешь вставить "эх, молодёжь" или байку из прошлого
- Реагируй на игру неспешно, философски
- Говори по-русски, по-простому`
- 1-2 коротких предложения
- Можешь вспомнить историю из прошлого или вставить "эх, молодёжь"
- Реагируй на ситуацию за столом, смотри на карты и ставки
- Комментируй ход игры по-стариковски мудро
- Говори тепло и по-человечески
- Используй контекст игры для житейских комментариев`
},
{
name: 'Макс "ГТО"',
@ -89,8 +97,9 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
- Можешь упомянуть шансы или +EV
- Можешь упомянуть шансы или +EV, опираясь на карты на столе и банк
- Реагируй на игру с точки зрения математики
- Анализируй конкретную ситуацию (смотри контекст)
- Говори по-русски, можно с англицизмами`
},
{
@ -110,7 +119,8 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
- Используй эмодзи уместно: 🍀😊🎲
- Реагируй эмоционально на игру
- Реагируй эмоционально на игру и карты на столе
- Комментируй удачу/неудачу исходя из ситуации
- Говори по-русски, живо`
},
{
@ -125,10 +135,11 @@ const pokerAI = {
- Говоришь ОЧЕНЬ мало
- Загадочный, никто не знает что у тебя на уме
- Отвечаешь односложно или просто молчишь
- Можешь кивнуть на конкретную карту или ситуацию
Правила ответов:
- МАКСИМУМ 1-3 слова или многоточие
- Примеры: "Хм.", "Нет.", "Посмотрим.", "...", "Да."
- Примеры: "Хм.", "Нет.", "Посмотрим.", "...", "Да.", "Флоп интересный."
- НИКОГДА не говори длинно
- Молчание твоё оружие`
},
@ -148,8 +159,9 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
- Можешь ворчать, возмущаться, жаловаться
- Реагируй эмоционально на плохие карты/биты
- Можешь ворчать, возмущаться, жаловаться на конкретные карты
- Реагируй эмоционально на плохие карты/биты, смотря на ситуацию
- Комментируй несправедливость раздачи
- Говори по-русски, экспрессивно`
},
{
@ -169,7 +181,8 @@ const pokerAI = {
Правила ответов:
- ТОЛЬКО 1-2 коротких предложения
- Говори спокойно, профессионально
- Можешь прокомментировать интересный розыгрыш
- Можешь прокомментировать интересный розыгрыш, ссылаясь на ситуацию
- Анализируй конкретную раздачу если нужно
- Говори по-русски, корректно`
}
],
@ -920,31 +933,87 @@ ${gameContextStr}
let context = '';
if (ctx.phase) {
const phases = {
'preflop': 'Префлоп',
'flop': 'Флоп',
'turn': 'Тёрн',
'river': 'Ривер',
'showdown': 'Вскрытие'
};
context += `Фаза: ${phases[ctx.phase] || ctx.phase}\n`;
}
if (ctx.pot !== undefined) {
context += `Банк: ${ctx.pot} фишек\n`;
}
if (ctx.myChips !== undefined) {
context += `Мои фишки: ${ctx.myChips}\n`;
}
if (ctx.lastAction) {
context += `Последнее действие игрока: ${ctx.lastAction}\n`;
// Фаза игры
if (ctx.phaseRu) {
context += `📍 Фаза: ${ctx.phaseRu}\n`;
}
// Общие карты на столе
if (ctx.communityCards && ctx.communityCards.length > 0) {
context += `Общие карты: ${ctx.communityCards.join(', ')}\n`;
context += `🃏 Карты на столе: ${ctx.communityCards.join(' ')}\n`;
} else {
context += `🃏 Карты на столе: пока нет\n`;
}
// Банк и ставки
if (ctx.pot !== undefined) {
context += `💰 Банк: ${ctx.pot} фишек\n`;
}
if (ctx.currentBet !== undefined && ctx.currentBet > 0) {
context += `💵 Текущая ставка: ${ctx.currentBet} фишек\n`;
}
// Мои данные (данные бота)
if (ctx.myName) {
context += `\n🤖 Мои данные:\n`;
context += ` - Имя: ${ctx.myName}\n`;
if (ctx.myCards && ctx.myCards.length > 0) {
context += ` - Карты: ${ctx.myCards.join(' ')}\n`;
}
context += ` - Фишки: ${ctx.myChips}\n`;
if (ctx.myBet > 0) {
context += ` - Моя ставка: ${ctx.myBet} (всего в раздаче: ${ctx.myTotalBet})\n`;
}
if (ctx.myPosition) {
context += ` - Позиция: ${ctx.myPosition}\n`;
}
}
// Другие игроки
if (ctx.players && ctx.players.length > 0) {
context += `\n👥 Другие игроки:\n`;
ctx.players.forEach(p => {
if (p.isMe) return; // Пропускаем себя
let playerStatus = '';
if (p.folded) {
playerStatus = ' [спасовал ❌]';
} else if (p.allIn) {
playerStatus = ' [ва-банк 💥]';
} else if (p.lastAction) {
const actionRu = {
'fold': 'пас',
'check': 'чек',
'call': 'колл',
'bet': 'бет',
'raise': 'рейз',
'allin': 'ва-банк'
}[p.lastAction] || p.lastAction;
playerStatus = ` [последнее: ${actionRu}]`;
}
context += ` - ${p.name}: ${p.chips} фишек`;
if (p.bet > 0) {
context += `, ставка ${p.bet}`;
}
if (p.isDealer) {
context += ` 🎲`;
}
context += playerStatus + `\n`;
});
}
// Активные игроки
if (ctx.activePlayers && ctx.activePlayers.length > 0) {
context += `\n✅ Активные: ${ctx.activePlayers.join(', ')}\n`;
}
if (ctx.foldedPlayers && ctx.foldedPlayers.length > 0) {
context += `❌ Спасовали: ${ctx.foldedPlayers.join(', ')}\n`;
}
// Последнее сообщение игрока
if (ctx.lastPlayerAction) {
context += `\n💬 Игрок написал: "${ctx.lastPlayerAction}"\n`;
}
return context || 'Идёт игра.';
@ -1282,6 +1351,102 @@ ${isWin ? 'Покажи радость, удовлетворение или са
*/
clearAllHistory() {
this.messageHistory.clear();
},
/**
* Сгенерировать поздравление игрока с сильной рукой
*/
async generatePlayerCongratulation(bot, botPersonality, playerName, handRank, handName, potSize, gameContext = {}) {
const settings = this.getSettings();
// Определяем, насколько сильная рука (для редкости поздравлений)
const isStrongHand = handRank >= 7; // Фулл-хаус и выше
const isVeryStrongHand = handRank >= 9; // Стрит-флеш и роял-флеш
// Формируем промпт для LLM
const congratsPrompt = `${botPersonality.systemPrompt}
СИТУАЦИЯ: Игрок ${playerName} только что ВЫИГРАЛ раздачу с рукой "${handName}"! Банк: ${potSize} фишек.
${isVeryStrongHand ? 'Это ОЧЕНЬ редкая и сильная рука!' : (isStrongHand ? 'Это сильная рука!' : 'Это хорошая рука!')}
Поздравь игрока КРАТКО (максимум 5-7 слов), в своём стиле.
${isVeryStrongHand ? 'Покажи восхищение и уважение!' : 'Будь доброжелательным.'}`;
// Если LLM включён, используем его
if (settings.llmEnabled && typeof this.sendToLLM === 'function') {
try {
const messages = [{ role: 'user', content: 'Поздрави игрока' }];
const response = await this.sendToLLM(congratsPrompt, messages, settings);
if (response) return response;
} catch (error) {
console.error('Ошибка генерации поздравления:', error);
}
}
// Запасные поздравления
return this.getFallbackCongratulation(botPersonality.style, handRank, handName);
},
/**
* Запасные поздравления
*/
getFallbackCongratulation(style, handRank, handName) {
const congratulations = {
aggressive: {
normal: ['Неплохо сыграно!', 'Уважаю!', 'Сильная рука!', 'Молодец!'],
strong: ['Вау, фулл-хаус! 💪', 'Монстр-рука!', 'Красавчик!', 'Вот это да!'],
veryStrong: [`${handName}?! Респект! 🔥`, 'НЕВЕРОЯТНО!', 'Легенда!', 'Вот это покер!']
},
tricky: {
normal: ['Интересно сыграно... 😏', 'Неплохо, неплохо', 'Красиво'],
strong: ['Фулл-хаус? Впечатляет! 👏', 'Загадочная игра...', 'Сильно!'],
veryStrong: [`${handName}... Я в восторге! 😮`, 'Невероятная рука!', 'Потрясающе!']
},
oldschool: {
normal: ['Молодец, голубчик!', 'Хорошо, хорошо!', 'Эх, красота!'],
strong: ['Фулл-хаус! Красота! 👴', 'Вот это рука!', 'Как в старые времена!'],
veryStrong: [`${handName}! За 50 лет такое редко видел! 😲`, 'Ух ты!', 'Легенда!']
},
mathematical: {
normal: ['Хороший EV!', 'Статистически сильно', '+EV рука'],
strong: ['Фулл-хаус! Rare! 📊', 'Топ 0.1% рук!', 'Вероятность низкая!'],
veryStrong: [`${handName}... 0.00154%! 🤯`, 'Математическое чудо!', 'Редчайший случай!']
},
lucky: {
normal: ['Ура! Молодец! 😊', 'Везунчик! 🍀', 'Здорово!'],
strong: ['Фулл-хаус! Удача! 🍀✨', 'Карты тебя любят!', 'Талисман работает! 💫'],
veryStrong: [`${handName}! ЭТО МАГИЯ! 🎉🍀`, 'Невероятная удача!', 'Чудо! ✨💚']
},
silent: {
normal: ['Хм. Неплохо.', 'Да.', '...👍'],
strong: ['Фулл-хаус... Ого.', 'Впечатляет.', '...!'],
veryStrong: [`${handName}?! ...`, 'Вау.', '😮']
},
tilted: {
normal: ['Ну везёт же... 😒', 'Тебе повезло!', 'Конечно...'],
strong: ['Фулл-хаус?! Ладно, красиво 😤', 'Повезло сильно!', 'Не может быть!'],
veryStrong: [`${handName}... Я не верю! 😱`, 'КАК?!', 'Это читерство! (шучу)']
},
professional: {
normal: ['GG WP!', 'Nice hand!', 'Хорошо сыграно'],
strong: ['Фулл-хаус. Респект! 💎', 'Strong hand, WP!', 'Impressive!'],
veryStrong: [`${handName}! Incredible! 🎯`, 'Amazing hand!', 'Legendary!']
}
};
const styleCongrats = congratulations[style] || congratulations.professional;
let pool;
if (handRank >= 9) {
pool = styleCongrats.veryStrong;
} else if (handRank >= 7) {
pool = styleCongrats.strong;
} else {
pool = styleCongrats.normal;
}
return pool[Math.floor(Math.random() * pool.length)];
}
};

View File

@ -197,6 +197,7 @@ function updateUserDisplay() {
const nameEl = document.getElementById('user-display-name');
const badgeEl = document.getElementById('user-role-badge');
const adminBtn = document.getElementById('admin-btn');
const serverUrlGroup = document.getElementById('server-url-group');
if (currentUser) {
nameEl.textContent = currentUser.username;
@ -205,9 +206,11 @@ function updateUserDisplay() {
badgeEl.style.display = 'inline';
badgeEl.textContent = 'ADMIN';
if (adminBtn) adminBtn.style.display = 'block';
if (serverUrlGroup) serverUrlGroup.style.display = 'block';
} else {
badgeEl.style.display = 'none';
if (adminBtn) adminBtn.style.display = 'none';
if (serverUrlGroup) serverUrlGroup.style.display = 'none';
}
// Обновляем поля имени в формах

View File

@ -275,6 +275,13 @@
<!-- Карты игрока -->
<div class="player-hand-container glass-card">
<div class="player-info-row">
<div class="player-name-display" id="player-name-display">Игрок</div>
<div class="player-balance-display">
<span class="balance-icon">💰</span>
<span class="balance-amount" id="player-balance">1000</span>
</div>
</div>
<div class="player-cards" id="player-cards">
<div class="card card-back"></div>
<div class="card card-back"></div>
@ -443,7 +450,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group admin-only" id="server-url-group" style="display: none;">
<label>Сервер WebSocket</label>
<input type="text" id="server-url" class="input" value="ws://localhost:3000" onchange="updateSettings()">
</div>

View File

@ -138,6 +138,13 @@ function showScreen(screenId) {
if (screenId === 'leaderboard-screen') {
renderLeaderboard();
}
// Загрузка админских настроек
if (screenId === 'admin-screen') {
if (typeof loadAdminSettings === 'function') {
loadAdminSettings();
}
}
}
/**
@ -559,6 +566,7 @@ function updateGameUI() {
const player = game.players.find(p => p.id === currentPlayerId);
if (player) {
renderPlayerHand(player.hand, game.communityCards);
updatePlayerBalance(player);
}
updateActionPanel(player, game);
@ -598,6 +606,7 @@ function updateGameUIFromServer(room) {
const myPlayer = players.find(p => p.id === currentPlayerId);
if (myPlayer) {
renderPlayerHand(myPlayer.hand, communityCards);
updatePlayerBalance(myPlayer);
updateActionPanelFromServer(myPlayer, room);
}
@ -780,6 +789,24 @@ function applyCardBackToNewCards() {
*/
function renderPlayerHand(hand, communityCards) {
const container = document.getElementById('player-cards');
// Проверяем, изменились ли карты
const currentHandKey = hand && hand.length > 0
? hand.map(c => `${c.suit}-${c.rank}`).join(',')
: 'empty';
// Если карты не изменились, не перерисовываем
if (container.dataset.currentHand === currentHandKey) {
// Только обновляем силу руки, если изменились общие карты
if (settings.showHandStrength !== false && hand && hand.length > 0) {
const strength = getHandStrength(hand, communityCards || []);
document.getElementById('hand-strength').textContent = strength || '';
}
return;
}
// Запоминаем текущие карты
container.dataset.currentHand = currentHandKey;
container.innerHTML = '';
if (!hand || hand.length === 0) {
@ -805,6 +832,42 @@ function renderPlayerHand(hand, communityCards) {
}
}
/**
* Обновить баланс игрока
*/
function updatePlayerBalance(player) {
if (!player) return;
// Обновляем имя игрока
const nameDisplay = document.getElementById('player-name-display');
if (nameDisplay) {
nameDisplay.textContent = player.name || 'Игрок';
}
// Обновляем баланс
const balanceDisplay = document.getElementById('player-balance');
if (balanceDisplay) {
const chips = player.chips || 0;
balanceDisplay.textContent = chips;
// Добавляем анимацию при изменении баланса
if (balanceDisplay.dataset.lastValue && balanceDisplay.dataset.lastValue !== chips.toString()) {
const oldValue = parseInt(balanceDisplay.dataset.lastValue);
const newValue = chips;
if (newValue > oldValue) {
balanceDisplay.parentElement.classList.add('balance-increase');
setTimeout(() => balanceDisplay.parentElement.classList.remove('balance-increase'), 500);
} else if (newValue < oldValue) {
balanceDisplay.parentElement.classList.add('balance-decrease');
setTimeout(() => balanceDisplay.parentElement.classList.remove('balance-decrease'), 500);
}
}
balanceDisplay.dataset.lastValue = chips.toString();
}
}
/**
* Обновить панель действий
*/
@ -1034,6 +1097,9 @@ function onHandEnd(result) {
// НОВОЕ: Генерируем эмоциональные реакции ботов
if (!isMultiplayer && game) {
generateBotEmotions(result);
// НОВОЕ: Поздравления игрока от ботов при сильной руке
generatePlayerCongratulations(result);
}
// Показываем кнопку новой раздачи
@ -1107,6 +1173,107 @@ async function generateBotEmotions(result) {
}, 500 + Math.random() * 1000);
}
/**
* Генерировать поздравления игрока с сильной рукой
*/
async function generatePlayerCongratulations(result) {
if (!game || !result || !result.winners || !result.hands) return;
const winner = result.winners[0];
const potSize = result.pot || game.pot || 0;
// Проверяем, что победитель - игрок (не бот)
const winnerData = game.players.find(p => p.id === winner.id);
if (!winnerData || winnerData.isAI) return;
// Получаем руку победителя
const winnerHand = result.hands?.find(h => h.player.id === winner.id)?.hand;
if (!winnerHand) return;
const handRank = winnerHand.rank;
const handName = winnerHand.name;
// Определяем вероятность поздравления в зависимости от силы руки
let congratsProbability = 0;
if (handRank >= 10) {
// Роял-флеш - 90% вероятность поздравления
congratsProbability = 0.9;
} else if (handRank >= 9) {
// Стрит-флеш - 80% вероятность
congratsProbability = 0.8;
} else if (handRank >= 8) {
// Каре - 60% вероятность
congratsProbability = 0.6;
} else if (handRank >= 7) {
// Фулл-хаус - 40% вероятность
congratsProbability = 0.4;
} else if (handRank >= 6) {
// Флеш - 25% вероятность
congratsProbability = 0.25;
} else if (handRank >= 5) {
// Стрит - 15% вероятность
congratsProbability = 0.15;
} else {
// Слабые руки - не поздравляем
return;
}
// Решаем, будем ли поздравлять
if (Math.random() > congratsProbability) return;
// Выбираем случайного бота для поздравления
const availableBots = game.players.filter(p => p.isAI && !p.folded);
if (availableBots.length === 0) return;
const bot = availableBots[Math.floor(Math.random() * availableBots.length)];
// Получаем личность бота
let botPersonality;
if (typeof botPersonalities !== 'undefined' && bot.personalityId) {
botPersonality = botPersonalities[bot.personalityId];
}
if (!botPersonality) {
botPersonality = { style: 'professional' };
}
// Задержка перед поздравлением (1000-2500ms, чтобы не конфликтовать с эмоциями)
setTimeout(async () => {
try {
let congratulation;
if (typeof llmChat !== 'undefined' && llmChat.generatePlayerCongratulation) {
congratulation = await llmChat.generatePlayerCongratulation(
bot,
botPersonality,
winnerData.name,
handRank,
handName,
potSize,
{ phase: currentGamePhase }
);
} else {
// Запасной вариант
const congrats = [
`${handName}! Респект!`,
'Сильная рука!',
'Впечатляет!',
'Молодец!',
'GG WP!'
];
congratulation = congrats[Math.floor(Math.random() * congrats.length)];
}
if (congratulation) {
addChatMessage('game', bot.name, congratulation);
}
} catch (error) {
console.error('Ошибка генерации поздравления:', error);
}
}, 1000 + Math.random() * 1500);
}
/**
* Закрыть модальное окно результата
*/
@ -1221,13 +1388,53 @@ function sendGameChat() {
// Если обращение не найдено, выбираем случайного бота
const bot = targetBot || bots[Math.floor(Math.random() * bots.length)];
// Формируем контекст игры для LLM
// Формируем расширенный контекст игры для LLM
const gameContext = {
// Фаза игры
phase: currentGamePhase,
phaseRu: {
'preflop': 'префлоп (карты только что розданы)',
'flop': 'флоп (3 карты на столе)',
'turn': 'терн (4 карты на столе)',
'river': 'ривер (5 карт на столе)',
'showdown': 'вскрытие карт'
}[currentGamePhase] || currentGamePhase,
// Банк и текущая ставка
pot: game.pot,
currentBet: game.currentBet,
// Общие карты на столе
communityCards: game.communityCards?.map(c => `${c.rank}${SUIT_SYMBOLS[c.suit]}`) || [],
communityCardsCount: game.communityCards?.length || 0,
// Мои данные
myName: bot.name,
myChips: bot.chips,
lastAction: message,
communityCards: game.communityCards?.map(c => `${c.rank}${c.suit}`) || [],
myBet: bot.bet,
myTotalBet: bot.totalBet,
myCards: bot.hand?.map(c => `${c.rank}${SUIT_SYMBOLS[c.suit]}`) || [],
myPosition: bot.isDealer ? 'дилер' : (bot.isSmallBlind ? 'малый блайнд' : (bot.isBigBlind ? 'большой блайнд' : 'обычная позиция')),
// Информация о других игроках
players: game.players.map(p => ({
name: p.name,
chips: p.chips,
bet: p.bet,
totalBet: p.totalBet,
folded: p.folded,
allIn: p.allIn,
lastAction: p.lastAction,
isDealer: p.isDealer,
isMe: p.id === bot.id
})),
// Активные игроки (не спасовали)
activePlayers: game.players.filter(p => !p.folded).map(p => p.name),
foldedPlayers: game.players.filter(p => p.folded).map(p => p.name),
// Последние действия
lastPlayerAction: message,
mentionedByName: targetBot !== null // Указываем, что игрок обратился по имени
};

View File

@ -1077,6 +1077,78 @@ body::before {
gap: 8px;
}
.player-info-row {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 8px 16px;
gap: 16px;
}
.player-name-display {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.player-balance-display {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 12px;
transition: all var(--transition-fast);
}
.player-balance-display.balance-increase {
animation: balanceIncrease 0.5s ease;
}
.player-balance-display.balance-decrease {
animation: balanceDecrease 0.5s ease;
}
@keyframes balanceIncrease {
0%, 100% {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
transform: scale(1);
}
50% {
background: rgba(16, 185, 129, 0.3);
border-color: rgba(16, 185, 129, 0.6);
transform: scale(1.05);
}
}
@keyframes balanceDecrease {
0%, 100% {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
transform: scale(1);
}
50% {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
transform: scale(0.95);
}
}
.balance-icon {
font-size: 16px;
}
.balance-amount {
font-size: 16px;
font-weight: 700;
color: var(--success);
min-width: 50px;
text-align: right;
}
.player-cards {
display: flex;
gap: 8px;