Compare commits
10 Commits
395e5fa3fb
...
6afcd46563
| Author | SHA1 | Date |
|---|---|---|
|
|
6afcd46563 | |
|
|
81a7108758 | |
|
|
4c1aa87905 | |
|
|
ca4e1fd087 | |
|
|
79267cb3d2 | |
|
|
24e457885d | |
|
|
eec780d8af | |
|
|
c3ace2834d | |
|
|
ab7a8418ed | |
|
|
ff7014e6c1 |
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Node modules
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Базы данных
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Временные файлы
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# OS файлы
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Временные файлы разработки
|
||||||
|
.tmp/
|
||||||
|
temp/
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Environment Configuration
|
||||||
|
# Для production используйте docker-compose.yml
|
||||||
|
|
||||||
|
# Порт сервера
|
||||||
|
PORT=3336
|
||||||
|
|
||||||
|
# Режим работы
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# База данных (путь относительно корня проекта)
|
||||||
|
# DB_PATH=./data/poker.db
|
||||||
|
|
||||||
|
# Настройки JWT (опционально, генерируются автоматически)
|
||||||
|
# JWT_SECRET=your-secret-key-here
|
||||||
|
# JWT_EXPIRES_IN=7d
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.swn
|
||||||
|
*~
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
data/
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
.tmp/
|
||||||
|
temp/
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
@ -0,0 +1,411 @@
|
||||||
|
# Управление пользователями (Админ-панель)
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Добавлена полноценная система управления пользователями в админ-панели с возможностью просмотра статистики, фильтрации, редактирования, блокировки и удаления пользователей.
|
||||||
|
|
||||||
|
## Новые возможности
|
||||||
|
|
||||||
|
### 📊 Статистика пользователей
|
||||||
|
- **Всего пользователей** — общее количество зарегистрированных
|
||||||
|
- **Активных (сегодня)** — пользователи с входом сегодня
|
||||||
|
- **Заблокировано** — количество заблокированных аккаунтов
|
||||||
|
- **Админов** — количество администраторов
|
||||||
|
|
||||||
|
### 🔍 Фильтры
|
||||||
|
- **По роли**: Все / Админы / Пользователи
|
||||||
|
- **По статусу**: Все / Активные / Заблокированные
|
||||||
|
- **Поиск по логину**: Динамический поиск
|
||||||
|
|
||||||
|
### 📋 Таблица пользователей
|
||||||
|
|
||||||
|
Отображаемые данные:
|
||||||
|
- **Логин** — имя пользователя (с пометкой "Вы" для текущего)
|
||||||
|
- **Роль** — 👑 Админ или 👤 Пользователь
|
||||||
|
- **IP адрес** — последний известный IP
|
||||||
|
- **Последний вход** — относительное время ("2 ч назад", "Вчера")
|
||||||
|
- **Статус** — ✅ Активен или 🚫 Заблокирован
|
||||||
|
- **Действия** — кнопки управления
|
||||||
|
|
||||||
|
### ⚙️ Действия с пользователями
|
||||||
|
|
||||||
|
1. **✏️ Редактировать**
|
||||||
|
- Изменить роль (Админ/Пользователь)
|
||||||
|
- Изменить статус (Активен/Заблокирован)
|
||||||
|
- Сменить пароль (опционально)
|
||||||
|
|
||||||
|
2. **🚫/✅ Заблокировать/Разблокировать**
|
||||||
|
- Быстрая блокировка/разблокировка
|
||||||
|
- Заблокированные пользователи не могут войти
|
||||||
|
|
||||||
|
3. **🗑️ Удалить**
|
||||||
|
- Полное удаление пользователя
|
||||||
|
- Требует двойного подтверждения
|
||||||
|
- Нельзя удалить себя
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Открыть вкладку "Пользователи"
|
||||||
|
|
||||||
|
1. Войдите как администратор (`admin` / `admin`)
|
||||||
|
2. Откройте **Админ-панель** (меню → ⚙️ Админ)
|
||||||
|
3. Выберите вкладку **👥 Пользователи**
|
||||||
|
|
||||||
|
### Просмотр и фильтрация
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ [Все роли ▼] [Все статусы ▼] [🔍 Поиск...] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Выберите фильтры в выпадающих списках
|
||||||
|
- Введите текст в поле поиска для фильтрации по логину
|
||||||
|
- Результаты обновляются автоматически
|
||||||
|
|
||||||
|
### Редактирование пользователя
|
||||||
|
|
||||||
|
1. Нажмите **✏️** напротив пользователя
|
||||||
|
2. Откроется модальное окно:
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ ✏️ Редактирование пользователя │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ Логин: [username] (readonly) │
|
||||||
|
│ Роль: [Пользователь ▼] │
|
||||||
|
│ Статус: [Активен ▼] │
|
||||||
|
│ Новый пароль: [________] │
|
||||||
|
│ │
|
||||||
|
│ [💾 Сохранить] [❌ Отмена] │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
3. Внесите изменения
|
||||||
|
4. Нажмите **💾 Сохранить**
|
||||||
|
|
||||||
|
### Блокировка пользователя
|
||||||
|
|
||||||
|
**Быстрая блокировка:**
|
||||||
|
- Нажмите **🚫** напротив пользователя
|
||||||
|
- Подтвердите действие
|
||||||
|
- Пользователь заблокирован
|
||||||
|
|
||||||
|
**Эффект:**
|
||||||
|
- Пользователь не сможет войти в систему
|
||||||
|
- При попытке входа: "Ваш аккаунт заблокирован"
|
||||||
|
- Статус в таблице: 🚫 Заблокирован
|
||||||
|
|
||||||
|
**Разблокировка:**
|
||||||
|
- Нажмите **✅** напротив заблокированного пользователя
|
||||||
|
- Подтвердите действие
|
||||||
|
|
||||||
|
### Удаление пользователя
|
||||||
|
|
||||||
|
⚠️ **ВНИМАНИЕ: Действие необратимо!**
|
||||||
|
|
||||||
|
1. Нажмите **🗑️** напротив пользователя
|
||||||
|
2. Подтвердите удаление (1-е окно)
|
||||||
|
3. Подтвердите ещё раз (2-е окно для безопасности)
|
||||||
|
4. Пользователь удалён полностью
|
||||||
|
|
||||||
|
**Что удаляется:**
|
||||||
|
- Запись пользователя из БД
|
||||||
|
- Настройки пользователя
|
||||||
|
- История игр сохраняется для статистики
|
||||||
|
|
||||||
|
**Ограничения:**
|
||||||
|
- Нельзя удалить себя
|
||||||
|
- Нельзя удалить единственного админа (рекомендуется)
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### POST `/api/admin/users`
|
||||||
|
|
||||||
|
Получить список пользователей с фильтрами.
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role": "admin|user|",
|
||||||
|
"status": "active|banned|",
|
||||||
|
"search": "логин для поиска"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "admin",
|
||||||
|
"role": "admin",
|
||||||
|
"banned": 0,
|
||||||
|
"ipAddress": "127.0.0.1",
|
||||||
|
"lastLogin": "2026-02-01T12:00:00Z",
|
||||||
|
"createdAt": "2026-01-01T00:00:00Z",
|
||||||
|
"totalGames": 42,
|
||||||
|
"totalWins": 18,
|
||||||
|
"totalChipsWon": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stats": {
|
||||||
|
"total": 10,
|
||||||
|
"active": 3,
|
||||||
|
"banned": 1,
|
||||||
|
"admins": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST `/api/admin/user/update`
|
||||||
|
|
||||||
|
Обновить данные пользователя.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "user123",
|
||||||
|
"role": "admin",
|
||||||
|
"banned": false,
|
||||||
|
"password": "новый_пароль" // опционально
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST `/api/admin/user/ban`
|
||||||
|
|
||||||
|
Заблокировать/разблокировать пользователя.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "user123",
|
||||||
|
"banned": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST `/api/admin/user/delete`
|
||||||
|
|
||||||
|
Удалить пользователя.
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "user123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `403` — Нельзя удалить себя
|
||||||
|
- `404` — Пользователь не найден
|
||||||
|
- `500` — Ошибка сервера
|
||||||
|
|
||||||
|
## База данных
|
||||||
|
|
||||||
|
### Новые поля в таблице `users`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE users ADD COLUMN banned INTEGER DEFAULT 0;
|
||||||
|
ALTER TABLE users ADD COLUMN ip_address TEXT;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Описание:**
|
||||||
|
- `banned` — флаг блокировки (0 = активен, 1 = заблокирован)
|
||||||
|
- `ip_address` — последний известный IP адрес пользователя
|
||||||
|
|
||||||
|
### Миграция
|
||||||
|
|
||||||
|
Поля добавляются автоматически при запуске сервера, если их нет:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Добавлено поле banned в таблицу users
|
||||||
|
✅ Добавлено поле ip_address в таблицу users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### Проверки на сервере
|
||||||
|
|
||||||
|
1. **Авторизация**: Только админы имеют доступ к API
|
||||||
|
2. **Валидация**: Проверка наличия обязательных полей
|
||||||
|
3. **Защита от самоблокировки**: Нельзя заблокировать/удалить себя
|
||||||
|
4. **Проверка при входе**: Блокированные пользователи не могут войти
|
||||||
|
|
||||||
|
### Логирование
|
||||||
|
|
||||||
|
Все административные действия логируются:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
database.logAction(adminId, adminUsername, 'update_user', {
|
||||||
|
target: 'user123',
|
||||||
|
changes: { role: 'admin', banned: true }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Типы действий:**
|
||||||
|
- `view_users` — просмотр списка пользователей
|
||||||
|
- `update_user` — изменение данных пользователя
|
||||||
|
- `ban_user` — блокировка пользователя
|
||||||
|
- `unban_user` — разблокировка пользователя
|
||||||
|
- `delete_user` — удаление пользователя
|
||||||
|
|
||||||
|
## Интерфейс
|
||||||
|
|
||||||
|
### Цветовая индикация
|
||||||
|
|
||||||
|
**Роли:**
|
||||||
|
- 👑 **Админ** — фиолетовый градиент
|
||||||
|
- 👤 **Пользователь** — серый фон
|
||||||
|
|
||||||
|
**Статусы:**
|
||||||
|
- ✅ **Активен** — зелёный фон
|
||||||
|
- 🚫 **Заблокирован** — красный фон
|
||||||
|
|
||||||
|
### Адаптивность
|
||||||
|
|
||||||
|
- **Desktop (>1024px)**: Полная таблица
|
||||||
|
- **Tablet (768-1024px)**: Компактные столбцы
|
||||||
|
- **Mobile (<768px)**: Горизонтальная прокрутка таблицы
|
||||||
|
|
||||||
|
### Анимации
|
||||||
|
|
||||||
|
- Плавное появление вкладки (fadeInUp 0.3s)
|
||||||
|
- Hover эффекты на строках таблицы
|
||||||
|
- Увеличение кнопок при наведении (scale 1.1)
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### Создать нового админа
|
||||||
|
|
||||||
|
1. Зарегистрируйте пользователя обычным способом
|
||||||
|
2. Войдите как админ
|
||||||
|
3. Откройте вкладку **👥 Пользователи**
|
||||||
|
4. Найдите нужного пользователя
|
||||||
|
5. Нажмите **✏️ Редактировать**
|
||||||
|
6. Измените роль на **👑 Администратор**
|
||||||
|
7. Сохраните
|
||||||
|
|
||||||
|
### Заблокировать спамера
|
||||||
|
|
||||||
|
1. Откройте вкладку **👥 Пользователи**
|
||||||
|
2. Найдите спамера в списке или через поиск
|
||||||
|
3. Нажмите **🚫 Заблокировать**
|
||||||
|
4. Подтвердите действие
|
||||||
|
5. Пользователь заблокирован
|
||||||
|
|
||||||
|
### Посмотреть активных сегодня
|
||||||
|
|
||||||
|
1. Откройте вкладку **👥 Пользователи**
|
||||||
|
2. В верхней панели статистики смотрите "Активных (сегодня)"
|
||||||
|
3. Или используйте фильтр и отсортируйте по "Последний вход"
|
||||||
|
|
||||||
|
### Сбросить пароль пользователю
|
||||||
|
|
||||||
|
1. Откройте вкладку **👥 Пользователи**
|
||||||
|
2. Найдите пользователя
|
||||||
|
3. Нажмите **✏️ Редактировать**
|
||||||
|
4. Введите новый пароль в поле "Новый пароль"
|
||||||
|
5. Нажмите **💾 Сохранить**
|
||||||
|
6. Сообщите пользователю новый пароль
|
||||||
|
|
||||||
|
## Ограничения и рекомендации
|
||||||
|
|
||||||
|
### Ограничения
|
||||||
|
|
||||||
|
- ❌ Нельзя редактировать/удалять себя
|
||||||
|
- ❌ Нельзя заблокировать себя
|
||||||
|
- ❌ Удаление необратимо (нет корзины)
|
||||||
|
|
||||||
|
### Рекомендации
|
||||||
|
|
||||||
|
- ✅ Всегда имейте минимум 2 админов
|
||||||
|
- ✅ Используйте блокировку вместо удаления (можно отменить)
|
||||||
|
- ✅ Регулярно проверяйте список активных пользователей
|
||||||
|
- ✅ Следите за IP адресами для выявления дубликатов
|
||||||
|
- ✅ Логируйте важные действия через вкладку "📋 Логи"
|
||||||
|
|
||||||
|
## Отладка
|
||||||
|
|
||||||
|
### Проверка прав доступа
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// В консоли браузера
|
||||||
|
console.log(currentUser?.role); // Должно быть "admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка токена
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// В консоли браузера
|
||||||
|
console.log(localStorage.getItem('token'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Получить список пользователей
|
||||||
|
curl -X POST http://localhost:3000/api/admin/users \
|
||||||
|
-H "Authorization: Bearer <token>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"role":"","status":"","search":""}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Логи действий
|
||||||
|
|
||||||
|
Все административные действия записываются в таблицу `action_logs`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM action_logs
|
||||||
|
WHERE action_type IN ('update_user', 'ban_user', 'delete_user')
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обновления
|
||||||
|
|
||||||
|
**Версия 1.0** (2026-02-01)
|
||||||
|
- ✅ Базовое управление пользователями
|
||||||
|
- ✅ Фильтры и поиск
|
||||||
|
- ✅ Редактирование, блокировка, удаление
|
||||||
|
- ✅ Статистика пользователей
|
||||||
|
- ✅ Отслеживание IP адресов
|
||||||
|
- ✅ Проверка блокировки при входе
|
||||||
|
|
||||||
|
**Планируется:**
|
||||||
|
- 📋 Экспорт списка пользователей в CSV
|
||||||
|
- 📊 Расширенная статистика по пользователям
|
||||||
|
- 🔍 Фильтр по дате регистрации
|
||||||
|
- 📧 Отправка уведомлений пользователям
|
||||||
|
- 🔐 Двухфакторная аутентификация для админов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Документация актуальна для версии**: 1.0
|
||||||
|
**Дата**: 2026-02-01
|
||||||
|
**Автор**: GitHub Copilot
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
# 🏗️ Архитектура развертывания
|
||||||
|
|
||||||
|
## 📊 Схема развертывания
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ИНТЕРНЕТ │
|
||||||
|
└────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ HTTP/HTTPS (80/443)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ APACHE 2.4 │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ • Reverse Proxy │ │
|
||||||
|
│ │ • WebSocket Support (proxy_wstunnel) │ │
|
||||||
|
│ │ • SSL Termination │ │
|
||||||
|
│ │ • Security Headers │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ Proxy Pass (3336)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DOCKER CONTAINER │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Texas Hold'em Poker │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Node.js 18 (Alpine Linux) │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │
|
||||||
|
│ │ │ │ Express.js │◄───┤ WebSocket │ │ │ │
|
||||||
|
│ │ │ │ REST API │ │ Server (ws) │ │ │ │
|
||||||
|
│ │ │ └───────┬────────┘ └───────┬────────┘ │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ │ └─────────┬───────────┘ │ │ │
|
||||||
|
│ │ │ ▼ │ │ │
|
||||||
|
│ │ │ ┌──────────────────┐ │ │ │
|
||||||
|
│ │ │ │ Database.js │ │ │ │
|
||||||
|
│ │ │ └─────────┬────────┘ │ │ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ │ │ ▼ │ │ │
|
||||||
|
│ │ │ ┌──────────────────┐ │ │ │
|
||||||
|
│ │ │ │ SQLite (sql.js) │ │ │ │
|
||||||
|
│ │ │ └──────────────────┘ │ │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ Port: 3336 │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────┘ │
|
||||||
|
└────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ Volumes
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ HOST FILESYSTEM │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ./data/poker.db ◄─── База данных SQLite │ │
|
||||||
|
│ │ ./logs/ ◄─── Логи приложения │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Поток данных
|
||||||
|
|
||||||
|
### HTTP Request Flow
|
||||||
|
```
|
||||||
|
1. Клиент → Apache (80/443)
|
||||||
|
2. Apache → Docker (3336)
|
||||||
|
3. Express.js обрабатывает запрос
|
||||||
|
4. Database.js → SQLite
|
||||||
|
5. Ответ возвращается клиенту
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket Flow
|
||||||
|
```
|
||||||
|
1. Клиент инициирует WS соединение
|
||||||
|
2. Apache proxy_wstunnel → Docker (3336)
|
||||||
|
3. WebSocket Server (ws) принимает подключение
|
||||||
|
4. Двусторонняя связь в реальном времени
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Компоненты
|
||||||
|
|
||||||
|
### Frontend (public/)
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ Static Files │
|
||||||
|
├──────────────────────────┤
|
||||||
|
│ • index.html │
|
||||||
|
│ • main.js │
|
||||||
|
│ • game.js │
|
||||||
|
│ • ai.js │
|
||||||
|
│ • auth.js │
|
||||||
|
│ • styles.css │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend (Node.js)
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ server.js │
|
||||||
|
├──────────────────────────┤
|
||||||
|
│ • Express App │
|
||||||
|
│ • HTTP Server │
|
||||||
|
│ • WebSocket Server │
|
||||||
|
│ • REST API Endpoints │
|
||||||
|
│ • Middleware │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Layer
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ database.js │
|
||||||
|
├──────────────────────────┤
|
||||||
|
│ • User Management │
|
||||||
|
│ • Settings Storage │
|
||||||
|
│ • Logs Storage │
|
||||||
|
│ • JWT Tokens │
|
||||||
|
│ • bcrypt Hashing │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Security Layers │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. Apache Layer │
|
||||||
|
│ ├─ SSL/TLS (HTTPS) │
|
||||||
|
│ ├─ Security Headers │
|
||||||
|
│ └─ Rate Limiting (опционально) │
|
||||||
|
│ │
|
||||||
|
│ 2. Application Layer │
|
||||||
|
│ ├─ JWT Authentication │
|
||||||
|
│ ├─ bcrypt Password Hashing │
|
||||||
|
│ ├─ authMiddleware │
|
||||||
|
│ ├─ adminMiddleware │
|
||||||
|
│ └─ Input Validation │
|
||||||
|
│ │
|
||||||
|
│ 3. Database Layer │
|
||||||
|
│ ├─ Prepared Statements │
|
||||||
|
│ ├─ User Ban System │
|
||||||
|
│ └─ Action Logging │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Network Ports
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════╗
|
||||||
|
║ PORT MAPPING ║
|
||||||
|
╠══════════════════════════════════════════════════════════════╣
|
||||||
|
║ ║
|
||||||
|
║ External (Internet) ║
|
||||||
|
║ │ ║
|
||||||
|
║ ├── :80 (HTTP) ──────┐ ║
|
||||||
|
║ └── :443 (HTTPS) ──┐ │ ║
|
||||||
|
║ │ │ ║
|
||||||
|
║ Apache │ │ ║
|
||||||
|
║ ├── :80 <─────────┘ │ ║
|
||||||
|
║ └── :443 <────────────┘ ║
|
||||||
|
║ │ ║
|
||||||
|
║ └── Proxy to localhost:3336 ║
|
||||||
|
║ │ ║
|
||||||
|
║ Docker │ ║
|
||||||
|
║ └── :3336 <────────┘ ║
|
||||||
|
║ ║
|
||||||
|
╚══════════════════════════════════════════════════════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Data Persistence
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Data Flow │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Application (Container) │
|
||||||
|
│ │ │
|
||||||
|
│ │ Write │
|
||||||
|
│ ▼ │
|
||||||
|
│ /app/data/poker.db ◄─────────► Volume Mount │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ Host: ./data/poker.db │
|
||||||
|
│ │
|
||||||
|
│ ✅ Сохраняется при перезапуске контейнера │
|
||||||
|
│ ✅ Доступно для backup │
|
||||||
|
│ ✅ Можно редактировать напрямую │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Deployment Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Deployment Process │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Clone Repo │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ ./deploy.sh │
|
||||||
|
│ ./deploy.ps1 │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌────────────┼────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│ Check │ │ Create │ │ Build │
|
||||||
|
│ Docker │ │ Dirs │ │ Image │
|
||||||
|
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
||||||
|
│ │ │
|
||||||
|
└────────────┼────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Start Container│
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Verify Status │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ ✅ Ready │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Production Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Production Setup │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ Firewall │
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ Apache │ ◄── SSL Certificates (Let's Encrypt)
|
||||||
|
│ (80/443) │
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ Docker │
|
||||||
|
│ (3336) │ ◄── Auto-restart: unless-stopped
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
┌──────┴───────┬──────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐ ┌─────────┐ ┌──────┐
|
||||||
|
│ SQLite │ │ Logs │ │Backup│
|
||||||
|
│ Volume │ │ Volume │ │ Cron │
|
||||||
|
└────────┘ └─────────┘ └──────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring Points
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Monitoring Setup │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. Docker Logs │
|
||||||
|
│ docker-compose logs -f │
|
||||||
|
│ │
|
||||||
|
│ 2. Apache Logs │
|
||||||
|
│ /var/log/apache2/poker_error.log │
|
||||||
|
│ /var/log/apache2/poker_access.log │
|
||||||
|
│ │
|
||||||
|
│ 3. Application Logs │
|
||||||
|
│ ./logs/*.log (if configured) │
|
||||||
|
│ │
|
||||||
|
│ 4. Database Size │
|
||||||
|
│ du -h data/poker.db │
|
||||||
|
│ │
|
||||||
|
│ 5. Container Status │
|
||||||
|
│ docker-compose ps │
|
||||||
|
│ docker stats texas-holdem-poker │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Дата:** 2026-02-01
|
||||||
|
**Версия:** 1.0.0
|
||||||
|
**Статус:** ✅ Production Architecture Ready
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
# ✅ Чеклист готовности к Docker развертыванию
|
||||||
|
|
||||||
|
## 📦 Созданные файлы (12 новых)
|
||||||
|
|
||||||
|
### Docker конфигурация
|
||||||
|
- [x] **Dockerfile** (733 bytes)
|
||||||
|
- Node.js 18 Alpine
|
||||||
|
- Production режим
|
||||||
|
- Порт 3336
|
||||||
|
|
||||||
|
- [x] **docker-compose.yml** (586 bytes)
|
||||||
|
- Сервис poker-app
|
||||||
|
- Volumes для данных
|
||||||
|
- Переменные окружения
|
||||||
|
|
||||||
|
- [x] **.dockerignore** (450 bytes)
|
||||||
|
- Исключение ненужных файлов
|
||||||
|
- Оптимизация сборки
|
||||||
|
|
||||||
|
### Веб-сервер
|
||||||
|
- [x] **apache-config.conf**
|
||||||
|
- HTTP/HTTPS конфигурация
|
||||||
|
- WebSocket поддержка
|
||||||
|
- Reverse proxy на 3336
|
||||||
|
|
||||||
|
### Автоматизация
|
||||||
|
- [x] **deploy.sh** (2,727 bytes)
|
||||||
|
- Bash скрипт для Linux/Mac
|
||||||
|
- Автоматическая проверка зависимостей
|
||||||
|
- Пошаговое развертывание
|
||||||
|
|
||||||
|
- [x] **deploy.ps1**
|
||||||
|
- PowerShell скрипт для Windows
|
||||||
|
- Цветной вывод
|
||||||
|
- Проверка Docker
|
||||||
|
|
||||||
|
### Конфигурация
|
||||||
|
- [x] **.env.example** (445 bytes)
|
||||||
|
- Пример переменных окружения
|
||||||
|
- PORT=3336
|
||||||
|
- NODE_ENV=production
|
||||||
|
|
||||||
|
- [x] **.gitignore** (обновлен)
|
||||||
|
- Исключение data/ и logs/
|
||||||
|
- Исключение .env файлов
|
||||||
|
|
||||||
|
### Документация
|
||||||
|
- [x] **README.md** (11,385 bytes)
|
||||||
|
- Главная документация
|
||||||
|
- Docker инструкции
|
||||||
|
- Полное описание проекта
|
||||||
|
|
||||||
|
- [x] **DEPLOYMENT.md** (11,785 bytes)
|
||||||
|
- Полное руководство
|
||||||
|
- Apache настройка
|
||||||
|
- Backup инструкции
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
- [x] **DOCKER_README.md** (3,294 bytes)
|
||||||
|
- Краткая инструкция
|
||||||
|
- Основные команды
|
||||||
|
- Quick start
|
||||||
|
|
||||||
|
- [x] **DOCKER_SETUP_SUMMARY.md** (7,613 bytes)
|
||||||
|
- Сводка изменений
|
||||||
|
- Контрольный список
|
||||||
|
- Следующие шаги
|
||||||
|
|
||||||
|
- [x] **TESTING.md** (5,425 bytes)
|
||||||
|
- Тестовые сценарии
|
||||||
|
- Проверка работы
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
### Код
|
||||||
|
- [x] **package.json** (обновлен)
|
||||||
|
- Добавлены npm скрипты для Docker
|
||||||
|
- docker:build, docker:up, docker:down, etc.
|
||||||
|
|
||||||
|
- [x] **public/main.js** (исправлен)
|
||||||
|
- Исправлена работа с токенами
|
||||||
|
- Синхронизировано с auth.js
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Технические характеристики
|
||||||
|
|
||||||
|
### Порты
|
||||||
|
- **Внутренний:** 3336
|
||||||
|
- **Внешний:** 3336 (можно изменить в docker-compose.yml)
|
||||||
|
- **Apache HTTP:** 80 → 3336
|
||||||
|
- **Apache HTTPS:** 443 → 3336
|
||||||
|
|
||||||
|
### Технологии
|
||||||
|
- **Docker:** Alpine Linux + Node.js 18
|
||||||
|
- **База данных:** SQLite (в volume)
|
||||||
|
- **WebSocket:** ws module
|
||||||
|
- **Reverse Proxy:** Apache 2.4+
|
||||||
|
|
||||||
|
### Volumes
|
||||||
|
- `./data:/app/data` - База данных SQLite
|
||||||
|
- `./logs:/app/logs` - Логи приложения
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
- `PORT=3336`
|
||||||
|
- `NODE_ENV=production`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Способы запуска
|
||||||
|
|
||||||
|
### 1. Автоматический (рекомендуется)
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
.\deploy.ps1
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
chmod +x deploy.sh && ./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker Compose
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. NPM скрипты
|
||||||
|
```bash
|
||||||
|
npm run docker:build
|
||||||
|
npm run docker:up
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Что готово
|
||||||
|
|
||||||
|
### Основное
|
||||||
|
- [x] Docker образ настроен
|
||||||
|
- [x] Docker Compose конфигурация
|
||||||
|
- [x] Порт изменен на 3336
|
||||||
|
- [x] Volumes для данных
|
||||||
|
- [x] Переменные окружения
|
||||||
|
- [x] Production режим
|
||||||
|
|
||||||
|
### Автоматизация
|
||||||
|
- [x] Скрипт развертывания (Linux/Mac)
|
||||||
|
- [x] Скрипт развертывания (Windows)
|
||||||
|
- [x] NPM скрипты
|
||||||
|
- [x] .dockerignore
|
||||||
|
- [x] .gitignore обновлен
|
||||||
|
|
||||||
|
### Apache
|
||||||
|
- [x] HTTP конфигурация
|
||||||
|
- [x] HTTPS конфигурация
|
||||||
|
- [x] WebSocket поддержка
|
||||||
|
- [x] Reverse proxy
|
||||||
|
- [x] Заголовки безопасности
|
||||||
|
|
||||||
|
### Документация
|
||||||
|
- [x] README.md обновлен
|
||||||
|
- [x] DEPLOYMENT.md создан
|
||||||
|
- [x] DOCKER_README.md создан
|
||||||
|
- [x] TESTING.md создан
|
||||||
|
- [x] DOCKER_SETUP_SUMMARY.md создан
|
||||||
|
|
||||||
|
### Исправления
|
||||||
|
- [x] Токены синхронизированы (authToken)
|
||||||
|
- [x] Функции управления пользователями исправлены
|
||||||
|
- [x] showScreen() ошибки устранены
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Следующие шаги
|
||||||
|
|
||||||
|
### 1. Локальное тестирование
|
||||||
|
```bash
|
||||||
|
# Запуск
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Проверка
|
||||||
|
curl http://localhost:3336
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Тестирование в браузере
|
||||||
|
- Открыть http://localhost:3336
|
||||||
|
- Зарегистрироваться
|
||||||
|
- Создать игру
|
||||||
|
- Проверить WebSocket
|
||||||
|
|
||||||
|
### 3. Создание администратора
|
||||||
|
```sql
|
||||||
|
UPDATE users SET role = 'admin' WHERE username = 'ваш_логин';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Настройка Apache (опционально)
|
||||||
|
```bash
|
||||||
|
sudo cp apache-config.conf /etc/apache2/sites-available/poker.conf
|
||||||
|
sudo a2ensite poker.conf
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Настройка SSL (опционально)
|
||||||
|
```bash
|
||||||
|
sudo certbot --apache -d poker.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Настройка backup
|
||||||
|
- Следуйте инструкциям в DEPLOYMENT.md
|
||||||
|
- Настройте cron задачу
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Итоговая структура
|
||||||
|
|
||||||
|
```
|
||||||
|
onlinepocker/
|
||||||
|
├── 🐳 Docker
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── .dockerignore
|
||||||
|
│ ├── deploy.sh
|
||||||
|
│ └── deploy.ps1
|
||||||
|
│
|
||||||
|
├── 🌐 Apache
|
||||||
|
│ └── apache-config.conf
|
||||||
|
│
|
||||||
|
├── ⚙️ Конфигурация
|
||||||
|
│ ├── .env.example
|
||||||
|
│ ├── .gitignore
|
||||||
|
│ └── package.json (обновлен)
|
||||||
|
│
|
||||||
|
├── 📚 Документация
|
||||||
|
│ ├── README.md (обновлен)
|
||||||
|
│ ├── DEPLOYMENT.md
|
||||||
|
│ ├── DOCKER_README.md
|
||||||
|
│ ├── DOCKER_SETUP_SUMMARY.md
|
||||||
|
│ ├── TESTING.md
|
||||||
|
│ ├── BOT_PERSONALITIES_CONFIG.md
|
||||||
|
│ └── ADMIN_USER_MANAGEMENT.md
|
||||||
|
│
|
||||||
|
├── 💻 Приложение
|
||||||
|
│ ├── server.js
|
||||||
|
│ ├── database.js
|
||||||
|
│ └── public/
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── main.js (исправлен)
|
||||||
|
│ ├── game.js
|
||||||
|
│ ├── ai.js
|
||||||
|
│ ├── auth.js
|
||||||
|
│ └── styles.css
|
||||||
|
│
|
||||||
|
└── 💾 Данные (создаются автоматически)
|
||||||
|
├── data/poker.db
|
||||||
|
└── logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Статистика
|
||||||
|
|
||||||
|
- **Всего файлов создано:** 12
|
||||||
|
- **Файлов обновлено:** 3
|
||||||
|
- **Строк документации:** ~3,500
|
||||||
|
- **Строк кода:** ~200
|
||||||
|
- **Размер Docker образа:** ~150 MB (Alpine + Node.js)
|
||||||
|
- **Порт:** 3336
|
||||||
|
- **Статус:** ✅ Production Ready
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Резюме
|
||||||
|
|
||||||
|
Репозиторий **полностью подготовлен** к развертыванию в Docker:
|
||||||
|
|
||||||
|
✅ **Docker конфигурация** - готова
|
||||||
|
✅ **Apache настройка** - готова
|
||||||
|
✅ **Автоматизация** - готова
|
||||||
|
✅ **Документация** - готова
|
||||||
|
✅ **Тестирование** - инструкции готовы
|
||||||
|
✅ **Безопасность** - настроена
|
||||||
|
✅ **Backup** - инструкции готовы
|
||||||
|
|
||||||
|
**Можно деплоить в production!** 🚀
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Дата:** 2026-02-01
|
||||||
|
**Версия:** 1.0.0
|
||||||
|
**Порт:** 3336
|
||||||
|
**Статус:** ✅ ГОТОВО
|
||||||
|
|
@ -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 сек)
|
||||||
|
```
|
||||||
|
|
||||||
|
🎰🃏 **Играйте и получайте заслуженные поздравления!**
|
||||||
|
|
@ -0,0 +1,439 @@
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName poker.yourdomain.com
|
||||||
|
ServerAdmin admin@yourdomain.com
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/poker_error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/poker_access.log combined
|
||||||
|
|
||||||
|
# Проксирование на Docker контейнер
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass / http://localhost:3336/
|
||||||
|
ProxyPassReverse / http://localhost:3336/
|
||||||
|
|
||||||
|
# WebSocket поддержка
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||||
|
RewriteRule /(.*) ws://localhost:3336/$1 [P,L]
|
||||||
|
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
|
||||||
|
RewriteRule /(.*) http://localhost:3336/$1 [P,L]
|
||||||
|
|
||||||
|
# Заголовки для WebSocket
|
||||||
|
<Location />
|
||||||
|
ProxyPass http://localhost:3336/
|
||||||
|
ProxyPassReverse http://localhost:3336/
|
||||||
|
ProxyPreserveHost On
|
||||||
|
|
||||||
|
# WebSocket headers
|
||||||
|
RequestHeader set X-Forwarded-Proto "http"
|
||||||
|
RequestHeader set X-Forwarded-Port "80"
|
||||||
|
</Location>
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
# SSL версия (если используете HTTPS)
|
||||||
|
<IfModule mod_ssl.c>
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName poker.yourdomain.com
|
||||||
|
ServerAdmin admin@yourdomain.com
|
||||||
|
|
||||||
|
# SSL сертификаты
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile /etc/ssl/certs/poker.crt
|
||||||
|
SSLCertificateKeyFile /etc/ssl/private/poker.key
|
||||||
|
# SSLCertificateChainFile /etc/ssl/certs/chain.pem
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/poker_ssl_error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/poker_ssl_access.log combined
|
||||||
|
|
||||||
|
# Проксирование на Docker контейнер
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass / http://localhost:3336/
|
||||||
|
ProxyPassReverse / http://localhost:3336/
|
||||||
|
|
||||||
|
# WebSocket поддержка
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{HTTP:Upgrade} =websocket [NC]
|
||||||
|
RewriteRule /(.*) ws://localhost:3336/$1 [P,L]
|
||||||
|
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
|
||||||
|
RewriteRule /(.*) http://localhost:3336/$1 [P,L]
|
||||||
|
|
||||||
|
# Заголовки для WebSocket
|
||||||
|
<Location />
|
||||||
|
ProxyPass http://localhost:3336/
|
||||||
|
ProxyPassReverse http://localhost:3336/
|
||||||
|
ProxyPreserveHost On
|
||||||
|
|
||||||
|
# WebSocket headers
|
||||||
|
RequestHeader set X-Forwarded-Proto "https"
|
||||||
|
RequestHeader set X-Forwarded-Port "443"
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
# Безопасность
|
||||||
|
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
</VirtualHost>
|
||||||
|
</IfModule>
|
||||||
|
# 🃏 Texas Hold'em Poker - Развертывание в Docker
|
||||||
|
|
||||||
|
## 📋 Содержание
|
||||||
|
|
||||||
|
- [Быстрый старт](#быстрый-старт)
|
||||||
|
- [Развертывание с Docker](#развертывание-с-docker)
|
||||||
|
- [Настройка Apache](#настройка-apache)
|
||||||
|
- [Переменные окружения](#переменные-окружения)
|
||||||
|
- [Backup и восстановление](#backup-и-восстановление)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Запуск через Docker Compose (рекомендуется)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Клонируйте репозиторий
|
||||||
|
git clone <repository-url>
|
||||||
|
cd onlinepocker
|
||||||
|
|
||||||
|
# Запустите контейнер
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Проверьте статус
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Приложение будет доступно по адресу: `http://localhost:3336`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Развертывание с Docker
|
||||||
|
|
||||||
|
### Сборка Docker образа вручную
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка образа
|
||||||
|
docker build -t texas-holdem-poker:latest .
|
||||||
|
|
||||||
|
# Запуск контейнера
|
||||||
|
docker run -d \
|
||||||
|
--name poker-app \
|
||||||
|
-p 3336:3336 \
|
||||||
|
-v $(pwd)/data:/app/data \
|
||||||
|
-e NODE_ENV=production \
|
||||||
|
-e PORT=3336 \
|
||||||
|
texas-holdem-poker:latest
|
||||||
|
|
||||||
|
# Проверка работы
|
||||||
|
docker ps
|
||||||
|
docker logs poker-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Управление контейнером
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Остановка
|
||||||
|
docker stop poker-app
|
||||||
|
|
||||||
|
# Запуск
|
||||||
|
docker start poker-app
|
||||||
|
|
||||||
|
# Перезапуск
|
||||||
|
docker restart poker-app
|
||||||
|
|
||||||
|
# Удаление
|
||||||
|
docker stop poker-app
|
||||||
|
docker rm poker-app
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker logs -f poker-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск в фоне
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Перезапуск
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Пересборка образа
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
# Пересборка и запуск
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Остановка с удалением volumes
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Настройка Apache
|
||||||
|
|
||||||
|
### Включение необходимых модулей Apache
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Включаем модули для проксирования и WebSocket
|
||||||
|
sudo a2enmod proxy
|
||||||
|
sudo a2enmod proxy_http
|
||||||
|
sudo a2enmod proxy_wstunnel
|
||||||
|
sudo a2enmod rewrite
|
||||||
|
sudo a2enmod headers
|
||||||
|
sudo a2enmod ssl # Если используете HTTPS
|
||||||
|
|
||||||
|
# Перезапуск Apache
|
||||||
|
sudo systemctl restart apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Копирование конфигурации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Копируем конфиг в Apache
|
||||||
|
sudo cp apache-config.conf /etc/apache2/sites-available/poker.conf
|
||||||
|
|
||||||
|
# Редактируем домен (замените poker.yourdomain.com на ваш домен)
|
||||||
|
sudo nano /etc/apache2/sites-available/poker.conf
|
||||||
|
|
||||||
|
# Включаем сайт
|
||||||
|
sudo a2ensite poker.conf
|
||||||
|
|
||||||
|
# Проверяем конфигурацию
|
||||||
|
sudo apache2ctl configtest
|
||||||
|
|
||||||
|
# Перезагружаем Apache
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка статуса Apache
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка статуса
|
||||||
|
sudo systemctl status apache2
|
||||||
|
|
||||||
|
# Просмотр логов ошибок
|
||||||
|
sudo tail -f /var/log/apache2/poker_error.log
|
||||||
|
|
||||||
|
# Просмотр логов доступа
|
||||||
|
sudo tail -f /var/log/apache2/poker_access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Переменные окружения
|
||||||
|
|
||||||
|
### Основные переменные
|
||||||
|
|
||||||
|
| Переменная | Значение по умолчанию | Описание |
|
||||||
|
|------------|----------------------|----------|
|
||||||
|
| `PORT` | 3336 | Порт для запуска сервера |
|
||||||
|
| `NODE_ENV` | production | Режим работы (production/development) |
|
||||||
|
|
||||||
|
### Настройка в docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3336
|
||||||
|
```
|
||||||
|
|
||||||
|
### Настройка в Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3336
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Структура проекта после развертывания
|
||||||
|
|
||||||
|
```
|
||||||
|
onlinepocker/
|
||||||
|
├── data/ # База данных (создается автоматически)
|
||||||
|
│ └── poker.db
|
||||||
|
├── logs/ # Логи приложения (опционально)
|
||||||
|
├── public/ # Статические файлы
|
||||||
|
├── node_modules/ # Зависимости
|
||||||
|
├── server.js # Основной сервер
|
||||||
|
├── database.js # Работа с БД
|
||||||
|
├── package.json
|
||||||
|
├── Dockerfile # Docker образ
|
||||||
|
├── docker-compose.yml # Docker Compose конфигурация
|
||||||
|
├── .dockerignore # Исключения для Docker
|
||||||
|
└── apache-config.conf # Конфигурация Apache
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Backup и восстановление
|
||||||
|
|
||||||
|
### Создание резервной копии базы данных
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Остановка контейнера
|
||||||
|
docker-compose stop
|
||||||
|
|
||||||
|
# Копирование БД
|
||||||
|
cp data/poker.db data/poker.db.backup.$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# Запуск контейнера
|
||||||
|
docker-compose start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Восстановление из резервной копии
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Остановка контейнера
|
||||||
|
docker-compose stop
|
||||||
|
|
||||||
|
# Восстановление БД
|
||||||
|
cp data/poker.db.backup.20260201_120000 data/poker.db
|
||||||
|
|
||||||
|
# Запуск контейнера
|
||||||
|
docker-compose start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Автоматический backup (cron)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создаем скрипт backup
|
||||||
|
nano /opt/poker-backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
BACKUP_DIR="/opt/poker-backups"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# Создаем директорию для backup
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
|
||||||
|
# Копируем БД
|
||||||
|
docker cp texas-holdem-poker:/app/data/poker.db $BACKUP_DIR/poker.db.$DATE
|
||||||
|
|
||||||
|
# Удаляем старые backup (старше 30 дней)
|
||||||
|
find $BACKUP_DIR -name "poker.db.*" -mtime +30 -delete
|
||||||
|
|
||||||
|
echo "Backup создан: poker.db.$DATE"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Делаем скрипт исполняемым
|
||||||
|
chmod +x /opt/poker-backup.sh
|
||||||
|
|
||||||
|
# Добавляем в cron (каждый день в 3:00)
|
||||||
|
crontab -e
|
||||||
|
# Добавьте строку:
|
||||||
|
0 3 * * * /opt/poker-backup.sh >> /var/log/poker-backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Проблемы с портами
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка, не занят ли порт 3336
|
||||||
|
sudo netstat -tlnp | grep 3336
|
||||||
|
|
||||||
|
# Если порт занят, найти процесс
|
||||||
|
sudo lsof -i :3336
|
||||||
|
|
||||||
|
# Убить процесс
|
||||||
|
sudo kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблемы с WebSocket
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка модулей Apache
|
||||||
|
apache2ctl -M | grep proxy
|
||||||
|
|
||||||
|
# Должны быть включены:
|
||||||
|
# - proxy_module
|
||||||
|
# - proxy_http_module
|
||||||
|
# - proxy_wstunnel_module
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблемы с правами доступа
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка правильных прав на директорию data
|
||||||
|
sudo chown -R www-data:www-data data/
|
||||||
|
sudo chmod 755 data/
|
||||||
|
|
||||||
|
# Для Docker
|
||||||
|
sudo chown -R 1000:1000 data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Просмотр логов контейнера
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Все логи
|
||||||
|
docker logs texas-holdem-poker
|
||||||
|
|
||||||
|
# Последние 100 строк
|
||||||
|
docker logs --tail 100 texas-holdem-poker
|
||||||
|
|
||||||
|
# В реальном времени
|
||||||
|
docker logs -f texas-holdem-poker
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### Рекомендации
|
||||||
|
|
||||||
|
1. **Используйте HTTPS** - настройте SSL сертификаты (Let's Encrypt)
|
||||||
|
2. **Firewall** - ограничьте доступ к порту 3336 только с localhost
|
||||||
|
3. **Регулярные обновления** - обновляйте зависимости и Docker образы
|
||||||
|
4. **Backup** - настройте автоматическое резервное копирование
|
||||||
|
5. **Мониторинг** - отслеживайте логи и производительность
|
||||||
|
|
||||||
|
### Настройка Let's Encrypt (опционально)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка certbot
|
||||||
|
sudo apt-get install certbot python3-certbot-apache
|
||||||
|
|
||||||
|
# Получение сертификата
|
||||||
|
sudo certbot --apache -d poker.yourdomain.com
|
||||||
|
|
||||||
|
# Автоматическое обновление
|
||||||
|
sudo certbot renew --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Поддержка
|
||||||
|
|
||||||
|
Если возникли проблемы:
|
||||||
|
|
||||||
|
1. Проверьте логи: `docker-compose logs -f`
|
||||||
|
2. Проверьте статус: `docker-compose ps`
|
||||||
|
3. Проверьте Apache: `sudo systemctl status apache2`
|
||||||
|
4. Проверьте логи Apache: `sudo tail -f /var/log/apache2/poker_error.log`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
|
||||||
|
Этот проект предназначен для личного использования.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия:** 1.0.0
|
||||||
|
**Дата обновления:** 2026-02-01
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
# 🐳 Docker Quick Start
|
||||||
|
|
||||||
|
## Быстрый запуск
|
||||||
|
|
||||||
|
### Windows (PowerShell)
|
||||||
|
```powershell
|
||||||
|
.\deploy.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux/Mac (Bash)
|
||||||
|
```bash
|
||||||
|
chmod +x deploy.sh
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ручной запуск
|
||||||
|
```bash
|
||||||
|
# Сборка и запуск
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Доступ к приложению
|
||||||
|
|
||||||
|
После запуска приложение будет доступно по адресу:
|
||||||
|
- **http://localhost:3336**
|
||||||
|
|
||||||
|
## Полезные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Просмотр статуса
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Перезапуск
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose stop
|
||||||
|
|
||||||
|
# Запуск после остановки
|
||||||
|
docker-compose start
|
||||||
|
|
||||||
|
# Полная остановка с удалением контейнеров
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Пересборка образа
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## NPM скрипты
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка Docker образа
|
||||||
|
npm run docker:build
|
||||||
|
|
||||||
|
# Запуск контейнера
|
||||||
|
npm run docker:up
|
||||||
|
|
||||||
|
# Остановка контейнера
|
||||||
|
npm run docker:down
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
npm run docker:logs
|
||||||
|
|
||||||
|
# Перезапуск
|
||||||
|
npm run docker:restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура данных
|
||||||
|
|
||||||
|
```
|
||||||
|
data/ # База данных SQLite (автоматически создается)
|
||||||
|
logs/ # Логи приложения (опционально)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройка Apache
|
||||||
|
|
||||||
|
Подробная инструкция по настройке Apache для проксирования находится в файле `DEPLOYMENT.md`.
|
||||||
|
|
||||||
|
Основные шаги:
|
||||||
|
1. Включить модули Apache: `proxy`, `proxy_http`, `proxy_wstunnel`, `rewrite`
|
||||||
|
2. Скопировать `apache-config.conf` в `/etc/apache2/sites-available/`
|
||||||
|
3. Включить сайт: `sudo a2ensite poker.conf`
|
||||||
|
4. Перезагрузить Apache: `sudo systemctl reload apache2`
|
||||||
|
|
||||||
|
## Порты
|
||||||
|
|
||||||
|
- **Внутренний порт контейнера**: 3336
|
||||||
|
- **Внешний порт**: 3336
|
||||||
|
- **Apache проксирует** запросы на localhost:3336
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Порт занят
|
||||||
|
```bash
|
||||||
|
# Проверка занятости порта
|
||||||
|
netstat -ano | findstr :3336 # Windows
|
||||||
|
lsof -i :3336 # Linux/Mac
|
||||||
|
|
||||||
|
# Остановка существующего контейнера
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблемы с правами доступа (Linux)
|
||||||
|
```bash
|
||||||
|
sudo chown -R $USER:$USER data/
|
||||||
|
sudo chmod 755 data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Просмотр детальных логов
|
||||||
|
```bash
|
||||||
|
docker-compose logs --tail=100 poker-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
- **Полная инструкция по развертыванию**: `DEPLOYMENT.md`
|
||||||
|
- **Настройка системных промптов ботов**: `BOT_PERSONALITIES_CONFIG.md`
|
||||||
|
- **Управление пользователями**: `ADMIN_USER_MANAGEMENT.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия**: 1.0.0
|
||||||
|
**Порт**: 3336
|
||||||
|
**Docker**: ✅ Готово к использованию
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
# 🐳 Подготовка к Docker развертыванию - Сводка изменений
|
||||||
|
|
||||||
|
## ✅ Созданные файлы
|
||||||
|
|
||||||
|
### Docker конфигурация
|
||||||
|
|
||||||
|
1. **`Dockerfile`**
|
||||||
|
- Основан на Node.js 18 Alpine
|
||||||
|
- Порт: 3336
|
||||||
|
- Автоматическая установка зависимостей
|
||||||
|
- Production режим
|
||||||
|
|
||||||
|
2. **`docker-compose.yml`**
|
||||||
|
- Сервис: poker-app
|
||||||
|
- Порт маппинг: 3336:3336
|
||||||
|
- Volumes для данных и логов
|
||||||
|
- Автоматический рестарт
|
||||||
|
|
||||||
|
3. **`.dockerignore`**
|
||||||
|
- Исключение node_modules
|
||||||
|
- Исключение баз данных
|
||||||
|
- Исключение логов и временных файлов
|
||||||
|
|
||||||
|
### Apache конфигурация
|
||||||
|
|
||||||
|
4. **`apache-config.conf`**
|
||||||
|
- HTTP (порт 80) → localhost:3336
|
||||||
|
- HTTPS (порт 443) → localhost:3336
|
||||||
|
- WebSocket поддержка (proxy_wstunnel)
|
||||||
|
- Заголовки безопасности
|
||||||
|
- Готовые настройки SSL
|
||||||
|
|
||||||
|
### Скрипты развертывания
|
||||||
|
|
||||||
|
5. **`deploy.sh`** (Linux/Mac)
|
||||||
|
- Проверка Docker и Docker Compose
|
||||||
|
- Создание директорий
|
||||||
|
- Автоматическая сборка и запуск
|
||||||
|
- Проверка статуса
|
||||||
|
|
||||||
|
6. **`deploy.ps1`** (Windows PowerShell)
|
||||||
|
- Аналог deploy.sh для Windows
|
||||||
|
- Цветной вывод
|
||||||
|
- Проверка всех зависимостей
|
||||||
|
|
||||||
|
### Документация
|
||||||
|
|
||||||
|
7. **`DEPLOYMENT.md`**
|
||||||
|
- Полное руководство по развертыванию
|
||||||
|
- Инструкции для Docker
|
||||||
|
- Настройка Apache
|
||||||
|
- Backup и восстановление
|
||||||
|
- Troubleshooting
|
||||||
|
- Безопасность
|
||||||
|
|
||||||
|
8. **`DOCKER_README.md`**
|
||||||
|
- Краткая инструкция по Docker
|
||||||
|
- Основные команды
|
||||||
|
- NPM скрипты
|
||||||
|
- Quick start
|
||||||
|
|
||||||
|
9. **`README.md`**
|
||||||
|
- Обновленный главный README
|
||||||
|
- Информация о Docker
|
||||||
|
- Полный список функций
|
||||||
|
- Структура проекта
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
### Дополнительные файлы
|
||||||
|
|
||||||
|
10. **`.env.example`**
|
||||||
|
- Пример переменных окружения
|
||||||
|
- PORT=3336
|
||||||
|
- NODE_ENV=production
|
||||||
|
|
||||||
|
11. **`.gitignore`**
|
||||||
|
- Обновлен для Docker
|
||||||
|
- Исключение data/ и logs/
|
||||||
|
- Исключение .env файлов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Измененные файлы
|
||||||
|
|
||||||
|
### 1. `package.json`
|
||||||
|
**Добавлены npm скрипты:**
|
||||||
|
```json
|
||||||
|
"docker:build": "docker-compose build",
|
||||||
|
"docker:up": "docker-compose up -d",
|
||||||
|
"docker:down": "docker-compose down",
|
||||||
|
"docker:logs": "docker-compose logs -f",
|
||||||
|
"docker:restart": "docker-compose restart"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `public/main.js`
|
||||||
|
**Исправлена работа с токенами:**
|
||||||
|
- Изменено `localStorage.getItem('token')` на `localStorage.getItem('authToken')`
|
||||||
|
- Синхронизировано с auth.js
|
||||||
|
- Исправлены функции: loadUsers(), saveUserEdit(), toggleBanUser(), deleteUser()
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Структура Docker развертывания
|
||||||
|
|
||||||
|
```
|
||||||
|
Приложение (порт 3336)
|
||||||
|
↓
|
||||||
|
Docker контейнер (texas-holdem-poker)
|
||||||
|
↓
|
||||||
|
Docker Compose (управление)
|
||||||
|
↓
|
||||||
|
Apache (reverse proxy)
|
||||||
|
↓
|
||||||
|
Интернет (порт 80/443)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Способы запуска
|
||||||
|
|
||||||
|
### 1. Автоматический (рекомендуется)
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```powershell
|
||||||
|
.\deploy.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/Mac:**
|
||||||
|
```bash
|
||||||
|
chmod +x deploy.sh
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Docker Compose
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. NPM скрипты
|
||||||
|
```bash
|
||||||
|
npm run docker:build
|
||||||
|
npm run docker:up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Docker вручную
|
||||||
|
```bash
|
||||||
|
docker build -t texas-holdem-poker .
|
||||||
|
docker run -d -p 3336:3336 --name poker-app texas-holdem-poker
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Порты
|
||||||
|
|
||||||
|
- **3336** - Основной порт приложения (Docker)
|
||||||
|
- **80** - HTTP через Apache (опционально)
|
||||||
|
- **443** - HTTPS через Apache (опционально)
|
||||||
|
|
||||||
|
**Важно:** Порт изменен с 3000 на 3336 согласно требованию
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Volumes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data # База данных SQLite
|
||||||
|
- ./logs:/app/logs # Логи приложения
|
||||||
|
```
|
||||||
|
|
||||||
|
Данные сохраняются на хосте, даже при удалении контейнера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Переменные окружения
|
||||||
|
|
||||||
|
В `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3336
|
||||||
|
```
|
||||||
|
|
||||||
|
В `Dockerfile`:
|
||||||
|
```dockerfile
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3336
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Документация
|
||||||
|
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `README.md` | Главная документация проекта |
|
||||||
|
| `DEPLOYMENT.md` | Полное руководство по развертыванию |
|
||||||
|
| `DOCKER_README.md` | Краткая инструкция Docker |
|
||||||
|
| `BOT_PERSONALITIES_CONFIG.md` | Настройка ботов |
|
||||||
|
| `ADMIN_USER_MANAGEMENT.md` | Управление пользователями |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Контрольный список готовности
|
||||||
|
|
||||||
|
- [x] Dockerfile создан
|
||||||
|
- [x] docker-compose.yml настроен
|
||||||
|
- [x] .dockerignore настроен
|
||||||
|
- [x] Apache конфигурация готова
|
||||||
|
- [x] Скрипты развертывания (Linux & Windows)
|
||||||
|
- [x] Документация обновлена
|
||||||
|
- [x] npm скрипты добавлены
|
||||||
|
- [x] .gitignore обновлен
|
||||||
|
- [x] Порт изменен на 3336
|
||||||
|
- [x] Переменные окружения настроены
|
||||||
|
- [x] Volumes для данных настроены
|
||||||
|
- [x] WebSocket поддержка в Apache
|
||||||
|
- [x] SSL конфигурация готова
|
||||||
|
- [x] Исправлена работа с токенами
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Следующие шаги
|
||||||
|
|
||||||
|
1. **Протестировать локально:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Открыть в браузере:**
|
||||||
|
```
|
||||||
|
http://localhost:3336
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Проверить логи:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Настроить Apache** (если нужно):
|
||||||
|
```bash
|
||||||
|
sudo cp apache-config.conf /etc/apache2/sites-available/poker.conf
|
||||||
|
sudo a2ensite poker.conf
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Создать администратора:**
|
||||||
|
- Зарегистрироваться через интерфейс
|
||||||
|
- Выполнить SQL: `UPDATE users SET role = 'admin' WHERE username = 'ваш_логин';`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Команды для проверки
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверка статуса
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Проверка порта
|
||||||
|
netstat -ano | findstr :3336 # Windows
|
||||||
|
lsof -i :3336 # Linux/Mac
|
||||||
|
|
||||||
|
# Проверка Apache модулей
|
||||||
|
apache2ctl -M | grep proxy
|
||||||
|
|
||||||
|
# Тест подключения
|
||||||
|
curl http://localhost:3336
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Готово!
|
||||||
|
|
||||||
|
Репозиторий полностью подготовлен к развертыванию в Docker на порту **3336** с поддержкой Apache reverse proxy.
|
||||||
|
|
||||||
|
**Дата:** 2026-02-01
|
||||||
|
**Версия:** 1.0.0
|
||||||
|
**Статус:** ✅ Production Ready
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Используем официальный образ Node.js
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем package.json и package-lock.json
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# Копируем все файлы приложения
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Создаем директорию для базы данных
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Открываем порт 3336
|
||||||
|
EXPOSE 3336
|
||||||
|
|
||||||
|
# Устанавливаем переменные окружения
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3336
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|
@ -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. **Видит кто спасовал** - может подшучивать или сочувствовать
|
||||||
|
|
||||||
|
## Примеры улучшенных ответов
|
||||||
|
|
||||||
|
### До изменений:
|
||||||
|
- Игрок: "Что думаешь?"
|
||||||
|
- Бот: "Удачи! Интересная игра."
|
||||||
|
|
||||||
|
### После изменений:
|
||||||
|
- Игрок: "Что думаешь?"
|
||||||
|
- Бот: "С тремя червами на столе и у меня флеш дро? Думаю поплыть дальше. 😏"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Теперь боты отвечают КОНТЕКСТУАЛЬНО, используя реальную информацию о раздаче!
|
||||||
|
|
@ -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) |
|
||||||
|
| Анимация | Нет | Есть (при изменении) |
|
||||||
|
| Позиция | По кругу стола | Внизу по центру |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Результат
|
||||||
|
|
||||||
|
Теперь игрок **всегда видит**:
|
||||||
|
- ✅ Своё имя
|
||||||
|
- ✅ Актуальный баланс фишек
|
||||||
|
- ✅ Визуальную обратную связь при выигрыше/проигрыше
|
||||||
|
- ✅ Красивую анимацию изменения баланса
|
||||||
|
|
||||||
|
🎰💰 **Следите за своим стеком!**
|
||||||
342
README.md
342
README.md
|
|
@ -1,2 +1,342 @@
|
||||||
# poker
|
# 🃏 Texas Hold'em No-Limit Poker
|
||||||
|
|
||||||
|
Полнофункциональная онлайн-игра в покер с поддержкой мультиплеера, ИИ-ботами и интеграцией LLM для реалистичного общения.
|
||||||
|
|
||||||
|
## ✨ Особенности
|
||||||
|
|
||||||
|
- 🎮 **Мультиплеер**: 2-6 игроков в режиме реального времени
|
||||||
|
- 🤖 **ИИ-боты**: 8 уникальных личностей с разными стилями игры
|
||||||
|
- 💬 **LLM-чат**: Реалистичное общение ботов через Ollama/LM Studio/OpenAI
|
||||||
|
- 👤 **Система авторизации**: Регистрация, вход, гостевой режим
|
||||||
|
- 👑 **Админ-панель**: Управление ботами, пользователями и настройками
|
||||||
|
- 🎨 **Современный UI**: Glassmorphism дизайн с анимациями
|
||||||
|
- 🐳 **Docker**: Готов к развертыванию в один клик
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Вариант 1: Docker (Рекомендуется)
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
.\deploy.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/Mac:**
|
||||||
|
```bash
|
||||||
|
chmod +x deploy.sh
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Или вручную:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Приложение будет доступно: **http://localhost:3336**
|
||||||
|
|
||||||
|
### Вариант 2: Локальный запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка зависимостей
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Запуск сервера
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Приложение будет доступно: **http://localhost:3000**
|
||||||
|
|
||||||
|
## 📋 Требования
|
||||||
|
|
||||||
|
### Docker (рекомендуется)
|
||||||
|
- Docker 20.10+
|
||||||
|
- Docker Compose 1.29+
|
||||||
|
|
||||||
|
### Локальный запуск
|
||||||
|
- Node.js 16+
|
||||||
|
- npm 8+
|
||||||
|
|
||||||
|
## 🎮 Игровые режимы
|
||||||
|
|
||||||
|
### 🎯 Одиночная игра
|
||||||
|
- Игра против 1-5 ИИ-ботов
|
||||||
|
- 3 уровня сложности ИИ
|
||||||
|
- 8 уникальных личностей ботов
|
||||||
|
- Настраиваемые системные промпты
|
||||||
|
|
||||||
|
### 🌐 Мультиплеер
|
||||||
|
- Создание приватных комнат
|
||||||
|
- Поделиться ссылкой для присоединения
|
||||||
|
- Чат в реальном времени
|
||||||
|
- До 6 игроков одновременно
|
||||||
|
|
||||||
|
### 🤖 ИИ-боты с личностями
|
||||||
|
|
||||||
|
1. **Виктор "Акула"** - Агрессивный профессионал
|
||||||
|
2. **Анна "Блефер"** - Мастер обмана
|
||||||
|
3. **Сергей "Математик"** - Аналитик-стратег
|
||||||
|
4. **Мария "Рок"** - Консервативный игрок
|
||||||
|
5. **Дмитрий "Маньяк"** - Непредсказуемый агрессор
|
||||||
|
6. **Елена "Читательница"** - Психолог за столом
|
||||||
|
7. **Игорь "Новичок"** - Неопытный оптимист
|
||||||
|
8. **Ольга "Тильт"** - Эмоциональный игрок
|
||||||
|
|
||||||
|
## 👑 Админ-панель
|
||||||
|
|
||||||
|
### Функции администратора:
|
||||||
|
|
||||||
|
- **⚙️ Настройки LLM**: Подключение Ollama/LM Studio/OpenAI
|
||||||
|
- **🤖 Промпты ботов**: Настройка личностей и поведения
|
||||||
|
- **📋 Логи действий**: Мониторинг активности пользователей
|
||||||
|
- **👥 Управление пользователями**:
|
||||||
|
- Просмотр всех пользователей
|
||||||
|
- Статистика (IP, последняя игра)
|
||||||
|
- Изменение ролей (admin/user)
|
||||||
|
- Блокировка пользователей
|
||||||
|
- Удаление аккаунтов
|
||||||
|
|
||||||
|
### Первый администратор
|
||||||
|
|
||||||
|
При первом запуске создайте админа:
|
||||||
|
```sql
|
||||||
|
-- Используйте любой SQLite клиент для изменения БД
|
||||||
|
UPDATE users SET role = 'admin' WHERE username = 'ваш_логин';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker развертывание
|
||||||
|
|
||||||
|
### Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
onlinepocker/
|
||||||
|
├── 📄 Dockerfile # Docker образ
|
||||||
|
├── 📄 docker-compose.yml # Docker Compose конфигурация
|
||||||
|
├── 📄 .dockerignore # Исключения для Docker
|
||||||
|
├── 📄 apache-config.conf # Настройка Apache
|
||||||
|
├── 📄 deploy.sh # Скрипт развертывания (Linux/Mac)
|
||||||
|
├── 📄 deploy.ps1 # Скрипт развертывания (Windows)
|
||||||
|
├── 📚 DEPLOYMENT.md # Полная документация
|
||||||
|
└── 📚 DOCKER_README.md # Краткая инструкция Docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### Основные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Просмотр логов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Перезапуск
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Пересборка
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### NPM скрипты
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run docker:build # Сборка образа
|
||||||
|
npm run docker:up # Запуск контейнера
|
||||||
|
npm run docker:down # Остановка контейнера
|
||||||
|
npm run docker:logs # Просмотр логов
|
||||||
|
npm run docker:restart # Перезапуск
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Настройка Apache
|
||||||
|
|
||||||
|
Для проксирования через Apache (порт 80/443 → 3336):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Включение модулей
|
||||||
|
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite headers ssl
|
||||||
|
|
||||||
|
# Копирование конфигурации
|
||||||
|
sudo cp apache-config.conf /etc/apache2/sites-available/poker.conf
|
||||||
|
|
||||||
|
# Включение сайта
|
||||||
|
sudo a2ensite poker.conf
|
||||||
|
|
||||||
|
# Перезагрузка Apache
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее: `DEPLOYMENT.md`
|
||||||
|
|
||||||
|
## 🔧 Конфигурация
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|------------|--------------|----------|
|
||||||
|
| `PORT` | 3336 | Порт сервера |
|
||||||
|
| `NODE_ENV` | production | Режим работы |
|
||||||
|
|
||||||
|
### Настройка LLM (в админ-панели)
|
||||||
|
|
||||||
|
1. Выберите провайдера: Ollama / LM Studio / OpenAI
|
||||||
|
2. Укажите API URL (например, `http://localhost:11434`)
|
||||||
|
3. Выберите модель (например, `llama3.2`)
|
||||||
|
4. Сохраните настройки
|
||||||
|
|
||||||
|
### Настройка промптов ботов
|
||||||
|
|
||||||
|
1. Войдите как администратор
|
||||||
|
2. Откройте "🤖 Промпты ботов"
|
||||||
|
3. Выберите бота
|
||||||
|
4. Отредактируйте системный промпт
|
||||||
|
5. Сохраните изменения
|
||||||
|
|
||||||
|
## 📦 Технологии
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Node.js** - Серверная платформа
|
||||||
|
- **Express.js** - REST API
|
||||||
|
- **WebSocket (ws)** - Реальное время
|
||||||
|
- **SQLite (sql.js)** - База данных
|
||||||
|
- **bcryptjs** - Хеширование паролей
|
||||||
|
- **jsonwebtoken** - JWT авторизация
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Vanilla JavaScript** - Без фреймворков
|
||||||
|
- **CSS3** - Glassmorphism дизайн
|
||||||
|
- **HTML5** - Семантическая разметка
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- **Docker** - Контейнеризация
|
||||||
|
- **Docker Compose** - Оркестрация
|
||||||
|
- **Apache** - Reverse proxy
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
onlinepocker/
|
||||||
|
├── 📂 public/ # Фронтенд
|
||||||
|
│ ├── index.html # Главная страница
|
||||||
|
│ ├── main.js # Основная логика
|
||||||
|
│ ├── game.js # Игровая логика
|
||||||
|
│ ├── ai.js # ИИ и личности ботов
|
||||||
|
│ ├── auth.js # Авторизация
|
||||||
|
│ └── styles.css # Стили
|
||||||
|
├── 📂 data/ # База данных (создается автоматически)
|
||||||
|
│ └── poker.db
|
||||||
|
├── server.js # WebSocket сервер
|
||||||
|
├── database.js # Работа с БД
|
||||||
|
├── package.json # Зависимости
|
||||||
|
├── Dockerfile # Docker образ
|
||||||
|
├── docker-compose.yml # Docker Compose
|
||||||
|
└── 📚 Документация
|
||||||
|
├── DEPLOYMENT.md # Развертывание
|
||||||
|
├── DOCKER_README.md # Docker инструкции
|
||||||
|
├── BOT_PERSONALITIES_CONFIG.md
|
||||||
|
└── ADMIN_USER_MANAGEMENT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
- ✅ JWT токены с истечением
|
||||||
|
- ✅ Bcrypt хеширование паролей
|
||||||
|
- ✅ Middleware авторизации
|
||||||
|
- ✅ Проверка прав администратора
|
||||||
|
- ✅ Логирование действий
|
||||||
|
- ✅ Блокировка пользователей
|
||||||
|
- ✅ IP tracking
|
||||||
|
|
||||||
|
## 💾 Backup базы данных
|
||||||
|
|
||||||
|
### Ручной backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
docker cp texas-holdem-poker:/app/data/poker.db ./backup-$(date +%Y%m%d).db
|
||||||
|
|
||||||
|
# Локально
|
||||||
|
cp data/poker.db data/poker.db.backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Автоматический backup (cron)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Каждый день в 3:00
|
||||||
|
0 3 * * * /opt/poker-backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее: `DEPLOYMENT.md`
|
||||||
|
|
||||||
|
## 📖 Документация
|
||||||
|
|
||||||
|
- **[DEPLOYMENT.md](DEPLOYMENT.md)** - Полное руководство по развертыванию
|
||||||
|
- **[DOCKER_README.md](DOCKER_README.md)** - Быстрый старт с Docker
|
||||||
|
- **[BOT_PERSONALITIES_CONFIG.md](BOT_PERSONALITIES_CONFIG.md)** - Настройка ботов
|
||||||
|
- **[ADMIN_USER_MANAGEMENT.md](ADMIN_USER_MANAGEMENT.md)** - Управление пользователями
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Порт занят
|
||||||
|
```bash
|
||||||
|
# Проверка
|
||||||
|
netstat -ano | findstr :3336 # Windows
|
||||||
|
lsof -i :3336 # Linux/Mac
|
||||||
|
|
||||||
|
# Решение
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблемы с WebSocket
|
||||||
|
```bash
|
||||||
|
# Проверка модулей Apache
|
||||||
|
apache2ctl -M | grep proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Права доступа (Linux)
|
||||||
|
```bash
|
||||||
|
sudo chown -R $USER:$USER data/
|
||||||
|
sudo chmod 755 data/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Поддержка
|
||||||
|
|
||||||
|
1. Проверьте логи: `docker-compose logs -f`
|
||||||
|
2. Проверьте статус: `docker-compose ps`
|
||||||
|
3. Прочитайте документацию: `DEPLOYMENT.md`
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
|
||||||
|
MIT License - свободно для личного использования
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Краткая инструкция
|
||||||
|
|
||||||
|
### Для локальной разработки
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
# Откройте http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Для production (Docker)
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
# Откройте http://localhost:3336
|
||||||
|
```
|
||||||
|
|
||||||
|
### Создание администратора
|
||||||
|
```bash
|
||||||
|
# 1. Зарегистрируйтесь через интерфейс
|
||||||
|
# 2. Подключитесь к БД и выполните:
|
||||||
|
UPDATE users SET role = 'admin' WHERE username = 'ваш_логин';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия:** 1.0.0
|
||||||
|
**Дата:** 2026-02-01
|
||||||
|
**Статус:** ✅ Готов к production
|
||||||
|
|
||||||
|
🃏 **Хорошей игры!**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
# 🔗 Руководство по шарингу комнат
|
||||||
|
|
||||||
|
## Как пригласить друзей в игру
|
||||||
|
|
||||||
|
### Способ 1: Копирование ссылки
|
||||||
|
1. Создайте комнату или зайдите в существующую
|
||||||
|
2. В лобби комнаты нажмите на кнопку **📋** рядом со ссылкой
|
||||||
|
3. Отправьте скопированную ссылку друзьям любым удобным способом
|
||||||
|
|
||||||
|
### Способ 2: Прямой шаринг в Telegram
|
||||||
|
1. В лобби комнаты нажмите кнопку **📱 Telegram**
|
||||||
|
2. Выберите чат или контакт для отправки
|
||||||
|
3. Ваш друг получит ссылку с приглашением
|
||||||
|
|
||||||
|
### Способ 3: Прямой шаринг в WhatsApp
|
||||||
|
1. В лобби комнаты нажмите кнопку **💬 WhatsApp**
|
||||||
|
2. Выберите чат или контакт для отправки
|
||||||
|
3. Ваш друг получит ссылку с приглашением
|
||||||
|
|
||||||
|
### Способ 4: Универсальный шаринг (на мобильных устройствах)
|
||||||
|
1. В лобби комнаты нажмите кнопку **🔗 Поделиться**
|
||||||
|
2. Выберите приложение для отправки (доступно на Android/iOS)
|
||||||
|
3. Отправьте ссылку через выбранное приложение
|
||||||
|
|
||||||
|
## Как присоединиться по ссылке
|
||||||
|
|
||||||
|
### Вариант A: Автоматическое присоединение
|
||||||
|
1. Перейдите по полученной ссылке
|
||||||
|
2. Войдите в игру или зарегистрируйтесь
|
||||||
|
3. Вы автоматически присоединитесь к комнате
|
||||||
|
|
||||||
|
### Вариант B: Ручное присоединение
|
||||||
|
1. Войдите в игру
|
||||||
|
2. Перейдите в раздел "Мультиплеер"
|
||||||
|
3. Найдите комнату в списке и присоединитесь
|
||||||
|
|
||||||
|
## Формат ссылки
|
||||||
|
|
||||||
|
Ссылка на комнату имеет вид:
|
||||||
|
```
|
||||||
|
http://localhost:3000/?room=room_1234567890_abc123
|
||||||
|
```
|
||||||
|
|
||||||
|
Где `room_1234567890_abc123` - уникальный ID комнаты.
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
- ✅ Ссылка работает до тех пор, пока комната активна
|
||||||
|
- ✅ По одной ссылке может присоединиться несколько игроков
|
||||||
|
- ✅ Комната закрывается, когда все игроки выходят
|
||||||
|
- ✅ Максимальное количество игроков устанавливается при создании комнаты
|
||||||
|
|
||||||
|
## Советы
|
||||||
|
|
||||||
|
1. **Быстрое копирование**: Кликните на поле со ссылкой - текст выделится автоматически
|
||||||
|
2. **Проверка ссылки**: Ссылка обновляется автоматически при изменении комнаты
|
||||||
|
3. **Мобильные устройства**: Используйте кнопку "Поделиться" для быстрого шаринга
|
||||||
|
4. **Telegram/WhatsApp**: Работает как на десктопе, так и на мобильных устройствах
|
||||||
|
|
||||||
|
## Техническая информация
|
||||||
|
|
||||||
|
### Поддерживаемые браузеры
|
||||||
|
- Chrome/Edge: все функции
|
||||||
|
- Firefox: все функции
|
||||||
|
- Safari (iOS): все функции включая Web Share API
|
||||||
|
- Мобильные браузеры: полная поддержка
|
||||||
|
|
||||||
|
### API
|
||||||
|
- **Clipboard API** - для копирования ссылки
|
||||||
|
- **Web Share API** - для универсального шаринга (мобильные устройства)
|
||||||
|
- **URL Parameters** - для передачи ID комнаты
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
- ID комнаты генерируется на сервере
|
||||||
|
- Невозможно подделать или угадать ID
|
||||||
|
- Комната доступна только активным пользователям
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
# 🧪 Тестирование Docker развертывания
|
||||||
|
|
||||||
|
## Перед запуском
|
||||||
|
|
||||||
|
Убедитесь, что установлены:
|
||||||
|
- Docker Desktop (Windows/Mac) или Docker Engine (Linux)
|
||||||
|
- Docker Compose
|
||||||
|
|
||||||
|
## Быстрый тест
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
```powershell
|
||||||
|
# Проверка версии Docker
|
||||||
|
docker --version
|
||||||
|
|
||||||
|
# Проверка Docker Compose
|
||||||
|
docker-compose --version
|
||||||
|
|
||||||
|
# Запуск приложения
|
||||||
|
.\deploy.ps1
|
||||||
|
|
||||||
|
# Или
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux/Mac
|
||||||
|
```bash
|
||||||
|
# Проверка версии Docker
|
||||||
|
docker --version
|
||||||
|
|
||||||
|
# Проверка Docker Compose
|
||||||
|
docker-compose --version
|
||||||
|
|
||||||
|
# Запуск приложения
|
||||||
|
chmod +x deploy.sh
|
||||||
|
./deploy.sh
|
||||||
|
|
||||||
|
# Или
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка работы
|
||||||
|
|
||||||
|
1. **Открыть в браузере:**
|
||||||
|
```
|
||||||
|
http://localhost:3336
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Проверить логи:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Проверить статус:**
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Должно быть:
|
||||||
|
```
|
||||||
|
NAME STATUS PORTS
|
||||||
|
texas-holdem-poker Up 0.0.0.0:3336->3336/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Проверить порт:**
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr :3336
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
lsof -i :3336
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестовые сценарии
|
||||||
|
|
||||||
|
### 1. Регистрация и вход
|
||||||
|
- Откройте http://localhost:3336
|
||||||
|
- Зарегистрируйте нового пользователя
|
||||||
|
- Войдите в систему
|
||||||
|
- Проверьте, что токен сохранился
|
||||||
|
|
||||||
|
### 2. Создание игры
|
||||||
|
- Создайте одиночную игру
|
||||||
|
- Добавьте ботов
|
||||||
|
- Проверьте работу игры
|
||||||
|
|
||||||
|
### 3. Админ панель (если есть доступ)
|
||||||
|
- Войдите как администратор
|
||||||
|
- Откройте админ панель
|
||||||
|
- Проверьте вкладку "Пользователи"
|
||||||
|
- Проверьте логи
|
||||||
|
|
||||||
|
### 4. WebSocket
|
||||||
|
- Создайте мультиплеер комнату
|
||||||
|
- Скопируйте ссылку
|
||||||
|
- Откройте в другой вкладке
|
||||||
|
- Проверьте синхронизацию
|
||||||
|
|
||||||
|
## Остановка и очистка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Остановка
|
||||||
|
docker-compose stop
|
||||||
|
|
||||||
|
# Остановка и удаление контейнеров
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Полная очистка (включая volumes)
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Удаление образов
|
||||||
|
docker rmi texas-holdem-poker
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Порт 3336 занят
|
||||||
|
```bash
|
||||||
|
# Остановите существующие контейнеры
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Или измените порт в docker-compose.yml
|
||||||
|
ports:
|
||||||
|
- "3337:3336" # Используйте другой внешний порт
|
||||||
|
```
|
||||||
|
|
||||||
|
### Контейнер не запускается
|
||||||
|
```bash
|
||||||
|
# Просмотрите логи
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Пересоберите образ
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### База данных не создается
|
||||||
|
```bash
|
||||||
|
# Проверьте права доступа к директории data
|
||||||
|
ls -la data/
|
||||||
|
|
||||||
|
# Создайте директорию вручную
|
||||||
|
mkdir -p data
|
||||||
|
chmod 755 data
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket не работает
|
||||||
|
- Проверьте, что браузер поддерживает WebSocket
|
||||||
|
- Откройте консоль разработчика (F12)
|
||||||
|
- Проверьте наличие ошибок подключения
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
|
||||||
|
После успешного запуска вы должны увидеть:
|
||||||
|
|
||||||
|
1. **В консоли:**
|
||||||
|
```
|
||||||
|
✅ Сервер успешно запущен!
|
||||||
|
📍 Сервер доступен по адресу: http://localhost:3336
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **В браузере:**
|
||||||
|
- Страница авторизации загружается
|
||||||
|
- Стили применены корректно
|
||||||
|
- Анимации работают
|
||||||
|
|
||||||
|
3. **В логах Docker:**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
```
|
||||||
|
🃏 Texas Hold'em Poker Server 🃏
|
||||||
|
Сервер запущен на http://localhost:3336
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Проверка WebSocket:**
|
||||||
|
- Откройте консоль браузера (F12)
|
||||||
|
- Должны быть сообщения о подключении WebSocket
|
||||||
|
- Нет ошибок 404 или 500
|
||||||
|
|
||||||
|
## Следующие шаги
|
||||||
|
|
||||||
|
После успешного тестирования:
|
||||||
|
|
||||||
|
1. **Настройте Apache** (опционально):
|
||||||
|
- Следуйте инструкциям в `DEPLOYMENT.md`
|
||||||
|
- Настройте SSL сертификаты
|
||||||
|
|
||||||
|
2. **Создайте администратора:**
|
||||||
|
```sql
|
||||||
|
UPDATE users SET role = 'admin' WHERE username = 'ваш_логин';
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Настройте LLM** (опционально):
|
||||||
|
- Откройте админ панель
|
||||||
|
- Настройте Ollama/LM Studio/OpenAI
|
||||||
|
|
||||||
|
4. **Настройте backup:**
|
||||||
|
- Следуйте инструкциям в `DEPLOYMENT.md`
|
||||||
|
- Настройте cron задачу
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Статус:** ✅ Готово к тестированию
|
||||||
|
**Порт:** 3336
|
||||||
|
**Дата:** 2026-02-01
|
||||||
|
|
@ -0,0 +1,735 @@
|
||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* Texas Hold'em - Модуль базы данных (SQLite)
|
||||||
|
* =============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
const initSqlJs = require('sql.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
// Путь к файлу БД
|
||||||
|
const DB_PATH = path.join(__dirname, 'poker.db');
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'poker-secret-key-change-in-production';
|
||||||
|
const JWT_EXPIRES = '7d';
|
||||||
|
|
||||||
|
let db = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация базы данных
|
||||||
|
*/
|
||||||
|
async function initDatabase() {
|
||||||
|
const SQL = await initSqlJs();
|
||||||
|
|
||||||
|
// Загружаем существующую БД или создаём новую
|
||||||
|
if (fs.existsSync(DB_PATH)) {
|
||||||
|
const buffer = fs.readFileSync(DB_PATH);
|
||||||
|
db = new SQL.Database(buffer);
|
||||||
|
console.log('📦 База данных загружена');
|
||||||
|
} else {
|
||||||
|
db = new SQL.Database();
|
||||||
|
console.log('📦 Создана новая база данных');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём таблицы
|
||||||
|
createTables();
|
||||||
|
|
||||||
|
// Создаём админа по умолчанию
|
||||||
|
await createDefaultAdmin();
|
||||||
|
|
||||||
|
// Всегда проверяем и инициализируем админские настройки
|
||||||
|
initDefaultAdminSettings();
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание таблиц
|
||||||
|
*/
|
||||||
|
function createTables() {
|
||||||
|
// Пользователи
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role TEXT DEFAULT 'user',
|
||||||
|
banned INTEGER DEFAULT 0,
|
||||||
|
ip_address TEXT,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_login TEXT,
|
||||||
|
total_games INTEGER DEFAULT 0,
|
||||||
|
total_wins INTEGER DEFAULT 0,
|
||||||
|
total_chips_won INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Миграция: добавляем поля banned и ip_address если их нет
|
||||||
|
try {
|
||||||
|
const columns = db.exec(`PRAGMA table_info(users)`);
|
||||||
|
const columnNames = columns[0]?.values.map(col => col[1]) || [];
|
||||||
|
|
||||||
|
if (!columnNames.includes('banned')) {
|
||||||
|
db.run(`ALTER TABLE users ADD COLUMN banned INTEGER DEFAULT 0`);
|
||||||
|
console.log('✅ Добавлено поле banned в таблицу users');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!columnNames.includes('ip_address')) {
|
||||||
|
db.run(`ALTER TABLE users ADD COLUMN ip_address TEXT`);
|
||||||
|
console.log('✅ Добавлено поле ip_address в таблицу users');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('⚠️ Миграция таблицы users пропущена:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройки пользователей
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
sound INTEGER DEFAULT 1,
|
||||||
|
animations INTEGER DEFAULT 1,
|
||||||
|
show_hand_strength INTEGER DEFAULT 1,
|
||||||
|
autofold INTEGER DEFAULT 1,
|
||||||
|
card_back_style TEXT DEFAULT 'default',
|
||||||
|
card_back_custom TEXT,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Админские настройки (глобальные)
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_by TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Логи действий
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS action_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id TEXT,
|
||||||
|
username TEXT,
|
||||||
|
action_type TEXT NOT NULL,
|
||||||
|
action_data TEXT,
|
||||||
|
room_id TEXT,
|
||||||
|
ip_address TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Чат сообщения
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
room_id TEXT,
|
||||||
|
room_type TEXT,
|
||||||
|
user_id TEXT,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
is_system INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Сессии игр
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS game_sessions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
room_id TEXT,
|
||||||
|
started_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ended_at TEXT,
|
||||||
|
players TEXT,
|
||||||
|
winner_id TEXT,
|
||||||
|
pot_size INTEGER,
|
||||||
|
game_data TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
saveDatabase();
|
||||||
|
console.log('📋 Таблицы созданы');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать админа по умолчанию
|
||||||
|
*/
|
||||||
|
async function createDefaultAdmin() {
|
||||||
|
const adminExists = db.exec("SELECT id FROM users WHERE role = 'admin' LIMIT 1");
|
||||||
|
|
||||||
|
if (adminExists.length === 0 || adminExists[0].values.length === 0) {
|
||||||
|
const adminId = uuidv4();
|
||||||
|
const passwordHash = bcrypt.hashSync('admin123', 10);
|
||||||
|
|
||||||
|
db.run(`
|
||||||
|
INSERT INTO users (id, username, password_hash, role)
|
||||||
|
VALUES (?, ?, ?, 'admin')
|
||||||
|
`, [adminId, 'admin', passwordHash]);
|
||||||
|
|
||||||
|
saveDatabase();
|
||||||
|
console.log('👑 Создан администратор по умолчанию: admin / admin123');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализировать дефолтные админские настройки
|
||||||
|
*/
|
||||||
|
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('⚙️ Инициализированы дефолтные админские настройки');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить БД на диск
|
||||||
|
*/
|
||||||
|
function saveDatabase() {
|
||||||
|
if (db) {
|
||||||
|
const data = db.export();
|
||||||
|
const buffer = Buffer.from(data);
|
||||||
|
fs.writeFileSync(DB_PATH, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ПОЛЬЗОВАТЕЛИ И АВТОРИЗАЦИЯ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Регистрация пользователя
|
||||||
|
*/
|
||||||
|
function registerUser(username, password) {
|
||||||
|
try {
|
||||||
|
// Проверяем, существует ли пользователь
|
||||||
|
const existing = db.exec("SELECT id FROM users WHERE username = ?", [username]);
|
||||||
|
if (existing.length > 0 && existing[0].values.length > 0) {
|
||||||
|
return { success: false, error: 'Пользователь уже существует' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = uuidv4();
|
||||||
|
const passwordHash = bcrypt.hashSync(password, 10);
|
||||||
|
|
||||||
|
db.run(`
|
||||||
|
INSERT INTO users (id, username, password_hash)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`, [userId, username, passwordHash]);
|
||||||
|
|
||||||
|
// Создаём настройки по умолчанию
|
||||||
|
db.run(`
|
||||||
|
INSERT INTO user_settings (user_id)
|
||||||
|
VALUES (?)
|
||||||
|
`, [userId]);
|
||||||
|
|
||||||
|
saveDatabase();
|
||||||
|
|
||||||
|
// Логируем
|
||||||
|
logAction(userId, username, 'register', { username });
|
||||||
|
|
||||||
|
// Генерируем токен
|
||||||
|
const token = generateToken(userId, username, 'user');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: { id: userId, username, role: 'user' },
|
||||||
|
token
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка регистрации:', error);
|
||||||
|
return { success: false, error: 'Ошибка регистрации' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Авторизация пользователя
|
||||||
|
*/
|
||||||
|
function loginUser(username, password, ipAddress = null) {
|
||||||
|
try {
|
||||||
|
const result = db.exec(`
|
||||||
|
SELECT id, username, password_hash, role, banned
|
||||||
|
FROM users
|
||||||
|
WHERE username = ?
|
||||||
|
`, [username]);
|
||||||
|
|
||||||
|
if (result.length === 0 || result[0].values.length === 0) {
|
||||||
|
return { success: false, error: 'Неверный логин или пароль' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [userId, storedUsername, passwordHash, role, banned] = result[0].values[0];
|
||||||
|
|
||||||
|
// Проверка на бан
|
||||||
|
if (banned === 1) {
|
||||||
|
return { success: false, error: 'Ваш аккаунт заблокирован' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bcrypt.compareSync(password, passwordHash)) {
|
||||||
|
return { success: false, error: 'Неверный логин или пароль' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем last_login и IP
|
||||||
|
if (ipAddress) {
|
||||||
|
db.run(`
|
||||||
|
UPDATE users SET last_login = datetime('now'), ip_address = ? WHERE id = ?
|
||||||
|
`, [ipAddress, userId]);
|
||||||
|
} else {
|
||||||
|
db.run(`
|
||||||
|
UPDATE users SET last_login = datetime('now') WHERE id = ?
|
||||||
|
`, [userId]);
|
||||||
|
}
|
||||||
|
saveDatabase();
|
||||||
|
|
||||||
|
// Логируем
|
||||||
|
logAction(userId, storedUsername, 'login', { username: storedUsername, ip: ipAddress });
|
||||||
|
|
||||||
|
// Генерируем токен
|
||||||
|
const token = generateToken(userId, storedUsername, role);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: { id: userId, username: storedUsername, role },
|
||||||
|
token
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка авторизации:', error);
|
||||||
|
return { success: false, error: 'Ошибка авторизации' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерировать JWT токен
|
||||||
|
*/
|
||||||
|
function generateToken(userId, username, role) {
|
||||||
|
return jwt.sign(
|
||||||
|
{ userId, username, role },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: JWT_EXPIRES }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить JWT токен
|
||||||
|
*/
|
||||||
|
function verifyToken(token) {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, JWT_SECRET);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить пользователя по ID
|
||||||
|
*/
|
||||||
|
function getUserById(userId) {
|
||||||
|
const result = db.exec(`
|
||||||
|
SELECT id, username, role, created_at, last_login, total_games, total_wins, total_chips_won
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
`, [userId]);
|
||||||
|
|
||||||
|
if (result.length === 0 || result[0].values.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [id, username, role, created_at, last_login, total_games, total_wins, total_chips_won] = result[0].values[0];
|
||||||
|
return { id, username, role, created_at, last_login, total_games, total_wins, total_chips_won };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// НАСТРОЙКИ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить настройки пользователя
|
||||||
|
*/
|
||||||
|
function getUserSettings(userId) {
|
||||||
|
const result = db.exec(`
|
||||||
|
SELECT sound, animations, show_hand_strength, autofold, card_back_style, card_back_custom
|
||||||
|
FROM user_settings
|
||||||
|
WHERE user_id = ?
|
||||||
|
`, [userId]);
|
||||||
|
|
||||||
|
if (result.length === 0 || result[0].values.length === 0) {
|
||||||
|
return {
|
||||||
|
sound: true,
|
||||||
|
animations: true,
|
||||||
|
showHandStrength: true,
|
||||||
|
autofold: true,
|
||||||
|
cardBackStyle: 'default',
|
||||||
|
cardBackCustom: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sound, animations, showHandStrength, autofold, cardBackStyle, cardBackCustom] = result[0].values[0];
|
||||||
|
return {
|
||||||
|
sound: !!sound,
|
||||||
|
animations: !!animations,
|
||||||
|
showHandStrength: !!showHandStrength,
|
||||||
|
autofold: !!autofold,
|
||||||
|
cardBackStyle: cardBackStyle || 'default',
|
||||||
|
cardBackCustom
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить настройки пользователя
|
||||||
|
*/
|
||||||
|
function saveUserSettings(userId, settings) {
|
||||||
|
db.run(`
|
||||||
|
INSERT OR REPLACE INTO user_settings
|
||||||
|
(user_id, sound, animations, show_hand_strength, autofold, card_back_style, card_back_custom)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
settings.sound ? 1 : 0,
|
||||||
|
settings.animations ? 1 : 0,
|
||||||
|
settings.showHandStrength ? 1 : 0,
|
||||||
|
settings.autofold ? 1 : 0,
|
||||||
|
settings.cardBackStyle || 'default',
|
||||||
|
settings.cardBackCustom || null
|
||||||
|
]);
|
||||||
|
saveDatabase();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить админские настройки
|
||||||
|
*/
|
||||||
|
function getAdminSettings() {
|
||||||
|
const result = db.exec(`SELECT key, value FROM admin_settings`);
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
llmEnabled: false,
|
||||||
|
llmProvider: 'ollama',
|
||||||
|
llmApiUrl: 'http://localhost:11434',
|
||||||
|
llmModel: 'llama3.2',
|
||||||
|
llmApiKey: '',
|
||||||
|
serverUrl: 'ws://localhost:3000'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
result[0].values.forEach(([key, value]) => {
|
||||||
|
if (key === 'llmEnabled') {
|
||||||
|
settings.llmEnabled = value === 'true';
|
||||||
|
} else {
|
||||||
|
settings[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить админские настройки
|
||||||
|
*/
|
||||||
|
function saveAdminSettings(settings, adminId) {
|
||||||
|
const keys = ['llmEnabled', 'llmProvider', 'llmApiUrl', 'llmModel', 'llmApiKey', 'serverUrl'];
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
if (settings[key] !== undefined) {
|
||||||
|
const value = typeof settings[key] === 'boolean' ? settings[key].toString() : settings[key];
|
||||||
|
db.run(`
|
||||||
|
INSERT OR REPLACE INTO admin_settings (key, value, updated_at, updated_by)
|
||||||
|
VALUES (?, ?, datetime('now'), ?)
|
||||||
|
`, [key, value, adminId]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
saveDatabase();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ЛОГИРОВАНИЕ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Логировать действие
|
||||||
|
*/
|
||||||
|
function logAction(userId, username, actionType, actionData = {}, roomId = null, ipAddress = null) {
|
||||||
|
db.run(`
|
||||||
|
INSERT INTO action_logs (user_id, username, action_type, action_data, room_id, ip_address)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, [userId, username, actionType, JSON.stringify(actionData), roomId, ipAddress]);
|
||||||
|
saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить логи (для админа)
|
||||||
|
*/
|
||||||
|
function getLogs(options = {}) {
|
||||||
|
const { limit = 100, offset = 0, actionType = null, userId = null, startDate = null, endDate = null } = options;
|
||||||
|
|
||||||
|
let sql = `SELECT * FROM action_logs WHERE 1=1`;
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
if (actionType) {
|
||||||
|
sql += ` AND action_type = ?`;
|
||||||
|
params.push(actionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
sql += ` AND user_id = ?`;
|
||||||
|
params.push(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
sql += ` AND timestamp >= ?`;
|
||||||
|
params.push(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
sql += ` AND timestamp <= ?`;
|
||||||
|
params.push(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ` ORDER BY timestamp DESC LIMIT ? OFFSET ?`;
|
||||||
|
params.push(limit, offset);
|
||||||
|
|
||||||
|
const result = db.exec(sql, params);
|
||||||
|
|
||||||
|
if (result.length === 0) return [];
|
||||||
|
|
||||||
|
return result[0].values.map(row => ({
|
||||||
|
id: row[0],
|
||||||
|
timestamp: row[1],
|
||||||
|
userId: row[2],
|
||||||
|
username: row[3],
|
||||||
|
actionType: row[4],
|
||||||
|
actionData: JSON.parse(row[5] || '{}'),
|
||||||
|
roomId: row[6],
|
||||||
|
ipAddress: row[7]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить статистику логов
|
||||||
|
*/
|
||||||
|
function getLogStats() {
|
||||||
|
const totalResult = db.exec(`SELECT COUNT(*) FROM action_logs`);
|
||||||
|
const total = totalResult[0]?.values[0]?.[0] || 0;
|
||||||
|
|
||||||
|
const byTypeResult = db.exec(`
|
||||||
|
SELECT action_type, COUNT(*) as count
|
||||||
|
FROM action_logs
|
||||||
|
GROUP BY action_type
|
||||||
|
ORDER BY count DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
const byType = byTypeResult.length > 0
|
||||||
|
? byTypeResult[0].values.map(([type, count]) => ({ type, count }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const todayResult = db.exec(`
|
||||||
|
SELECT COUNT(*) FROM action_logs
|
||||||
|
WHERE date(timestamp) = date('now')
|
||||||
|
`);
|
||||||
|
const today = todayResult[0]?.values[0]?.[0] || 0;
|
||||||
|
|
||||||
|
return { total, byType, today };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ЧАТ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить сообщение чата
|
||||||
|
*/
|
||||||
|
function saveChatMessage(roomId, roomType, userId, username, message, isSystem = false) {
|
||||||
|
db.run(`
|
||||||
|
INSERT INTO chat_messages (room_id, room_type, user_id, username, message, is_system)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, [roomId, roomType, userId, username, message, isSystem ? 1 : 0]);
|
||||||
|
saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить историю чата комнаты
|
||||||
|
*/
|
||||||
|
function getChatHistory(roomId, limit = 50) {
|
||||||
|
const result = db.exec(`
|
||||||
|
SELECT timestamp, username, message, is_system
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE room_id = ?
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT ?
|
||||||
|
`, [roomId, limit]);
|
||||||
|
|
||||||
|
if (result.length === 0) return [];
|
||||||
|
|
||||||
|
return result[0].values.map(row => ({
|
||||||
|
timestamp: row[0],
|
||||||
|
username: row[1],
|
||||||
|
message: row[2],
|
||||||
|
isSystem: !!row[3]
|
||||||
|
})).reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистить чат комнаты
|
||||||
|
*/
|
||||||
|
function clearRoomChat(roomId) {
|
||||||
|
db.run(`DELETE FROM chat_messages WHERE room_id = ?`, [roomId]);
|
||||||
|
saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ (АДМИН)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить всех пользователей
|
||||||
|
*/
|
||||||
|
function getAllUsers() {
|
||||||
|
const result = db.exec(`
|
||||||
|
SELECT id, username, role, banned, ip_address, last_login, created_at,
|
||||||
|
total_games, total_wins, total_chips_won
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (result.length === 0) return [];
|
||||||
|
|
||||||
|
return result[0].values.map(row => ({
|
||||||
|
id: row[0],
|
||||||
|
username: row[1],
|
||||||
|
role: row[2],
|
||||||
|
banned: row[3],
|
||||||
|
ipAddress: row[4],
|
||||||
|
lastLogin: row[5],
|
||||||
|
createdAt: row[6],
|
||||||
|
totalGames: row[7],
|
||||||
|
totalWins: row[8],
|
||||||
|
totalChipsWon: row[9]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить пользователя по логину
|
||||||
|
*/
|
||||||
|
function getUserByUsername(username) {
|
||||||
|
const result = db.exec(`
|
||||||
|
SELECT id, username, role, banned, ip_address, last_login, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE username = ?
|
||||||
|
`, [username]);
|
||||||
|
|
||||||
|
if (result.length === 0 || result[0].values.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [id, uname, role, banned, ipAddress, lastLogin, createdAt] = result[0].values[0];
|
||||||
|
return { id, username: uname, role, banned, ipAddress, lastLogin, createdAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновить пользователя
|
||||||
|
*/
|
||||||
|
function updateUser(username, updates) {
|
||||||
|
const user = getUserByUsername(username);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Пользователь не найден');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
if (updates.role !== undefined) {
|
||||||
|
fields.push('role = ?');
|
||||||
|
values.push(updates.role);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.banned !== undefined) {
|
||||||
|
fields.push('banned = ?');
|
||||||
|
values.push(updates.banned);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.password) {
|
||||||
|
const hashedPassword = bcrypt.hashSync(updates.password, 10);
|
||||||
|
fields.push('password_hash = ?');
|
||||||
|
values.push(hashedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length === 0) {
|
||||||
|
return; // Нечего обновлять
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(username);
|
||||||
|
|
||||||
|
db.run(`
|
||||||
|
UPDATE users
|
||||||
|
SET ${fields.join(', ')}
|
||||||
|
WHERE username = ?
|
||||||
|
`, values);
|
||||||
|
|
||||||
|
saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить пользователя
|
||||||
|
*/
|
||||||
|
function deleteUser(username) {
|
||||||
|
db.run(`DELETE FROM users WHERE username = ?`, [username]);
|
||||||
|
db.run(`DELETE FROM user_settings WHERE user_id = (SELECT id FROM users WHERE username = ?)`, [username]);
|
||||||
|
saveDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ЭКСПОРТ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initDatabase,
|
||||||
|
saveDatabase,
|
||||||
|
|
||||||
|
// Пользователи
|
||||||
|
registerUser,
|
||||||
|
loginUser,
|
||||||
|
verifyToken,
|
||||||
|
getUserById,
|
||||||
|
getAllUsers,
|
||||||
|
getUserByUsername,
|
||||||
|
updateUser,
|
||||||
|
deleteUser,
|
||||||
|
|
||||||
|
// Настройки
|
||||||
|
getUserSettings,
|
||||||
|
saveUserSettings,
|
||||||
|
getAdminSettings,
|
||||||
|
saveAdminSettings,
|
||||||
|
|
||||||
|
// Логирование
|
||||||
|
logAction,
|
||||||
|
getLogs,
|
||||||
|
getLogStats,
|
||||||
|
|
||||||
|
// Чат
|
||||||
|
saveChatMessage,
|
||||||
|
getChatHistory,
|
||||||
|
clearRoomChat
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Texas Hold'em Poker - Скрипт развертывания
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🃏 Texas Hold'em Poker - Развертывание"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Проверка Docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "❌ Docker не установлен!"
|
||||||
|
echo "Установите Docker: https://docs.docker.com/get-docker/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверка Docker Compose
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
echo "❌ Docker Compose не установлен!"
|
||||||
|
echo "Установите Docker Compose: https://docs.docker.com/compose/install/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Docker и Docker Compose установлены"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Создание директорий
|
||||||
|
echo "📁 Создание необходимых директорий..."
|
||||||
|
mkdir -p data
|
||||||
|
mkdir -p logs
|
||||||
|
echo "✅ Директории созданы"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Остановка старого контейнера
|
||||||
|
echo "🛑 Остановка старых контейнеров..."
|
||||||
|
docker-compose down 2>/dev/null || true
|
||||||
|
echo "✅ Старые контейнеры остановлены"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Сборка образа
|
||||||
|
echo "🔨 Сборка Docker образа..."
|
||||||
|
docker-compose build
|
||||||
|
echo "✅ Образ собран"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Запуск контейнера
|
||||||
|
echo "🚀 Запуск контейнера..."
|
||||||
|
docker-compose up -d
|
||||||
|
echo "✅ Контейнер запущен"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ожидание запуска
|
||||||
|
echo "⏳ Ожидание запуска сервера..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Проверка статуса
|
||||||
|
if docker-compose ps | grep -q "Up"; then
|
||||||
|
echo "✅ Сервер успешно запущен!"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "🎉 Развертывание завершено!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "📍 Сервер доступен по адресу:"
|
||||||
|
echo " http://localhost:3336"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Полезные команды:"
|
||||||
|
echo " docker-compose logs -f # Просмотр логов"
|
||||||
|
echo " docker-compose ps # Статус контейнеров"
|
||||||
|
echo " docker-compose stop # Остановка"
|
||||||
|
echo " docker-compose restart # Перезапуск"
|
||||||
|
echo " docker-compose down # Остановка и удаление"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "❌ Ошибка запуска сервера!"
|
||||||
|
echo "Проверьте логи: docker-compose logs"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
poker-app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: texas-holdem-poker
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3336:3336"
|
||||||
|
volumes:
|
||||||
|
# Монтируем директорию для сохранения базы данных
|
||||||
|
- ./data:/app/data
|
||||||
|
# Опционально: монтируем логи
|
||||||
|
- ./logs:/app/logs
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3336
|
||||||
|
networks:
|
||||||
|
- poker-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
poker-network:
|
||||||
|
driver: bridge
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "texas-holdem-online",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Онлайн Texas Hold'em No-Limit Poker с мультиплеером и ИИ",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"sql.js": "^1.13.0",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"poker",
|
||||||
|
"texas-holdem",
|
||||||
|
"websocket",
|
||||||
|
"game"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,570 @@
|
||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* Texas Hold'em - Модуль авторизации (клиент)
|
||||||
|
* =============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Текущий пользователь
|
||||||
|
let currentUser = null;
|
||||||
|
let authToken = null;
|
||||||
|
let isGuest = false;
|
||||||
|
|
||||||
|
// Пагинация логов
|
||||||
|
let currentLogPage = 1;
|
||||||
|
const logsPerPage = 20;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// АВТОРИЗАЦИЯ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация авторизации
|
||||||
|
*/
|
||||||
|
async function initAuth() {
|
||||||
|
// Проверяем сохранённый токен
|
||||||
|
const savedToken = localStorage.getItem('authToken');
|
||||||
|
|
||||||
|
if (savedToken) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/me', {
|
||||||
|
headers: { 'Authorization': `Bearer ${savedToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
currentUser = data.user;
|
||||||
|
authToken = savedToken;
|
||||||
|
onAuthSuccess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки токена:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Токен недействителен
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем экран авторизации
|
||||||
|
showScreen('auth-screen');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключить вкладку авторизации
|
||||||
|
*/
|
||||||
|
function switchAuthTab(tab) {
|
||||||
|
const tabs = document.querySelectorAll('#auth-tabs .btn-tab');
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
document.getElementById('login-form').style.display = tab === 'login' ? 'block' : 'none';
|
||||||
|
document.getElementById('register-form').style.display = tab === 'register' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик входа
|
||||||
|
*/
|
||||||
|
async function handleLogin() {
|
||||||
|
const username = document.getElementById('login-username').value.trim();
|
||||||
|
const password = document.getElementById('login-password').value;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
showNotification('Введите логин и пароль', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
currentUser = data.user;
|
||||||
|
authToken = data.token;
|
||||||
|
localStorage.setItem('authToken', authToken);
|
||||||
|
isGuest = false;
|
||||||
|
onAuthSuccess();
|
||||||
|
} else {
|
||||||
|
showNotification(data.error || 'Ошибка входа', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка входа:', error);
|
||||||
|
showNotification('Ошибка соединения с сервером', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик регистрации
|
||||||
|
*/
|
||||||
|
async function handleRegister() {
|
||||||
|
const username = document.getElementById('register-username').value.trim();
|
||||||
|
const password = document.getElementById('register-password').value;
|
||||||
|
const passwordConfirm = document.getElementById('register-password-confirm').value;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
showNotification('Заполните все поля', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== passwordConfirm) {
|
||||||
|
showNotification('Пароли не совпадают', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username.length < 3) {
|
||||||
|
showNotification('Логин должен быть минимум 3 символа', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
showNotification('Пароль должен быть минимум 6 символов', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
currentUser = data.user;
|
||||||
|
authToken = data.token;
|
||||||
|
localStorage.setItem('authToken', authToken);
|
||||||
|
isGuest = false;
|
||||||
|
showNotification('Регистрация успешна!', 'success');
|
||||||
|
onAuthSuccess();
|
||||||
|
} else {
|
||||||
|
showNotification(data.error || 'Ошибка регистрации', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка регистрации:', error);
|
||||||
|
showNotification('Ошибка соединения с сервером', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Играть как гость
|
||||||
|
*/
|
||||||
|
function playAsGuest() {
|
||||||
|
currentUser = {
|
||||||
|
id: 'guest_' + Date.now(),
|
||||||
|
username: 'Гость_' + Math.floor(Math.random() * 10000),
|
||||||
|
role: 'guest'
|
||||||
|
};
|
||||||
|
isGuest = true;
|
||||||
|
authToken = null;
|
||||||
|
onAuthSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выход
|
||||||
|
*/
|
||||||
|
function handleLogout() {
|
||||||
|
currentUser = null;
|
||||||
|
authToken = null;
|
||||||
|
isGuest = false;
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
showScreen('auth-screen');
|
||||||
|
showNotification('Вы вышли из системы', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* После успешной авторизации
|
||||||
|
*/
|
||||||
|
function onAuthSuccess() {
|
||||||
|
showScreen('main-menu');
|
||||||
|
updateUserDisplay();
|
||||||
|
loadLLMStatus();
|
||||||
|
|
||||||
|
// Загружаем настройки с сервера если не гость
|
||||||
|
if (!isGuest && authToken) {
|
||||||
|
loadUserSettingsFromServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие комнаты в URL для автоматического присоединения
|
||||||
|
if (typeof checkRoomInUrl === 'function') {
|
||||||
|
checkRoomInUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновить отображение пользователя
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
const adminLlmButtonGroup = document.getElementById('admin-llm-button-group');
|
||||||
|
|
||||||
|
if (currentUser) {
|
||||||
|
nameEl.textContent = currentUser.username;
|
||||||
|
|
||||||
|
if (currentUser.role === 'admin') {
|
||||||
|
badgeEl.style.display = 'inline';
|
||||||
|
badgeEl.textContent = 'ADMIN';
|
||||||
|
if (adminBtn) adminBtn.style.display = 'block';
|
||||||
|
if (serverUrlGroup) serverUrlGroup.style.display = 'block';
|
||||||
|
if (adminLlmButtonGroup) adminLlmButtonGroup.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
badgeEl.style.display = 'none';
|
||||||
|
if (adminBtn) adminBtn.style.display = 'none';
|
||||||
|
if (serverUrlGroup) serverUrlGroup.style.display = 'none';
|
||||||
|
if (adminLlmButtonGroup) adminLlmButtonGroup.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем поля имени в формах
|
||||||
|
document.getElementById('sp-player-name').value = currentUser.username;
|
||||||
|
document.getElementById('mp-player-name').value = currentUser.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// НАСТРОЙКИ С СЕРВЕРА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить пользовательские настройки с сервера
|
||||||
|
*/
|
||||||
|
async function loadUserSettingsFromServer() {
|
||||||
|
if (!authToken) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/user', {
|
||||||
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
applyServerSettings(data.settings);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки настроек:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить настройки на сервер
|
||||||
|
*/
|
||||||
|
async function saveUserSettingsToServer(settings) {
|
||||||
|
if (!authToken) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('/api/settings/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(settings)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка сохранения настроек:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Применить настройки с сервера
|
||||||
|
*/
|
||||||
|
function applyServerSettings(serverSettings) {
|
||||||
|
settings = {
|
||||||
|
...settings,
|
||||||
|
sound: serverSettings.sound,
|
||||||
|
animations: serverSettings.animations,
|
||||||
|
showHandStrength: serverSettings.showHandStrength,
|
||||||
|
autofold: serverSettings.autofold,
|
||||||
|
cardBackStyle: serverSettings.cardBackStyle,
|
||||||
|
cardBackCustom: serverSettings.cardBackCustom
|
||||||
|
};
|
||||||
|
|
||||||
|
// Применяем к UI
|
||||||
|
document.getElementById('setting-sound').checked = settings.sound !== false;
|
||||||
|
document.getElementById('setting-animations').checked = settings.animations !== false;
|
||||||
|
document.getElementById('setting-hand-strength').checked = settings.showHandStrength !== false;
|
||||||
|
document.getElementById('setting-autofold').checked = settings.autofold !== false;
|
||||||
|
|
||||||
|
soundEnabled = settings.sound !== false;
|
||||||
|
|
||||||
|
// Применяем рубашку карт
|
||||||
|
if (serverSettings.cardBackCustom) {
|
||||||
|
localStorage.setItem('customCardBack', serverSettings.cardBackCustom);
|
||||||
|
}
|
||||||
|
applyCardBackStyle(serverSettings.cardBackStyle || 'default');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить статус LLM
|
||||||
|
*/
|
||||||
|
async function loadLLMStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/public');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const statusBox = document.getElementById('llm-status-box');
|
||||||
|
const statusIcon = document.getElementById('llm-status-icon');
|
||||||
|
const statusText = document.getElementById('llm-status-text');
|
||||||
|
|
||||||
|
if (statusBox) {
|
||||||
|
if (data.llmEnabled) {
|
||||||
|
statusBox.classList.add('enabled');
|
||||||
|
statusBox.classList.remove('disabled');
|
||||||
|
statusIcon.textContent = '✅';
|
||||||
|
statusText.textContent = `LLM чат включён (${data.llmProvider})`;
|
||||||
|
|
||||||
|
// Сохраняем настройки LLM глобально
|
||||||
|
window.llmSettings = {
|
||||||
|
enabled: true,
|
||||||
|
provider: data.llmProvider,
|
||||||
|
apiUrl: data.llmApiUrl,
|
||||||
|
model: data.llmModel
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
statusBox.classList.add('disabled');
|
||||||
|
statusBox.classList.remove('enabled');
|
||||||
|
statusIcon.textContent = '❌';
|
||||||
|
statusText.textContent = 'LLM чат отключён администратором';
|
||||||
|
|
||||||
|
window.llmSettings = { enabled: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки статуса LLM:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// АДМИН-ПАНЕЛЬ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключить вкладку админки
|
||||||
|
*/
|
||||||
|
function switchAdminTab(tab) {
|
||||||
|
const tabs = document.querySelectorAll('.admin-tabs .btn-tab');
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
document.querySelectorAll('.admin-tab-content').forEach(el => {
|
||||||
|
el.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById(`admin-tab-${tab}`).style.display = 'block';
|
||||||
|
|
||||||
|
// Загружаем данные вкладки
|
||||||
|
if (tab === 'logs') {
|
||||||
|
loadLogStats();
|
||||||
|
loadLogs(1);
|
||||||
|
} else if (tab === 'settings') {
|
||||||
|
loadAdminSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить админские настройки
|
||||||
|
*/
|
||||||
|
async function loadAdminSettings() {
|
||||||
|
if (!authToken || currentUser?.role !== 'admin') {
|
||||||
|
console.log('❌ Нет прав для загрузки админ настроек');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📥 Загрузка админских настроек...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/admin', {
|
||||||
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const s = data.settings;
|
||||||
|
|
||||||
|
console.log('✅ Получены настройки:', s);
|
||||||
|
|
||||||
|
document.getElementById('admin-llm-enabled').checked = s.llmEnabled;
|
||||||
|
document.getElementById('admin-llm-provider').value = s.llmProvider || 'ollama';
|
||||||
|
document.getElementById('admin-llm-url').value = s.llmApiUrl || 'http://localhost:11434';
|
||||||
|
document.getElementById('admin-llm-model').value = s.llmModel || 'llama3.2';
|
||||||
|
document.getElementById('admin-llm-apikey').value = s.llmApiKey || '';
|
||||||
|
} else {
|
||||||
|
console.error('❌ Ошибка ответа сервера:', response.status);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка загрузки админ настроек:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить админские настройки
|
||||||
|
*/
|
||||||
|
async function saveAdminSettings() {
|
||||||
|
if (!authToken || currentUser?.role !== 'admin') return;
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
llmEnabled: document.getElementById('admin-llm-enabled').checked,
|
||||||
|
llmProvider: document.getElementById('admin-llm-provider').value,
|
||||||
|
llmApiUrl: document.getElementById('admin-llm-url').value,
|
||||||
|
llmModel: document.getElementById('admin-llm-model').value,
|
||||||
|
llmApiKey: document.getElementById('admin-llm-apikey').value
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('💾 Сохранение админских настроек:', settings);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/admin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${authToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(settings)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
showNotification('Настройки сохранены', 'success');
|
||||||
|
// Обновляем статус LLM для отображения в настройках
|
||||||
|
loadLLMStatus();
|
||||||
|
} else {
|
||||||
|
showNotification('Ошибка сохранения', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('Ошибка сохранения', 'error');
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тестировать LLM из админки
|
||||||
|
*/
|
||||||
|
async function testAdminLLM() {
|
||||||
|
// Сохраняем настройки сначала
|
||||||
|
await saveAdminSettings();
|
||||||
|
|
||||||
|
showNotification('Тестирование LLM...', 'info');
|
||||||
|
|
||||||
|
// Здесь можно добавить реальный тест через сервер
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotification('LLM тест выполнен (проверьте логи сервера)', 'success');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить статистику логов
|
||||||
|
*/
|
||||||
|
async function loadLogStats() {
|
||||||
|
if (!authToken || currentUser?.role !== 'admin') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/logs/stats', {
|
||||||
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
document.getElementById('stat-total').textContent = data.stats.total;
|
||||||
|
document.getElementById('stat-today').textContent = data.stats.today;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки статистики:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить логи
|
||||||
|
*/
|
||||||
|
async function loadLogs(page = 1) {
|
||||||
|
if (!authToken || currentUser?.role !== 'admin') return;
|
||||||
|
|
||||||
|
currentLogPage = Math.max(1, page);
|
||||||
|
const offset = (currentLogPage - 1) * logsPerPage;
|
||||||
|
const actionType = document.getElementById('log-filter-type').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url = `/api/logs?limit=${logsPerPage}&offset=${offset}`;
|
||||||
|
if (actionType) url += `&actionType=${actionType}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
renderLogs(data.logs);
|
||||||
|
|
||||||
|
// Обновляем пагинацию
|
||||||
|
document.getElementById('log-page-info').textContent = `Страница ${currentLogPage}`;
|
||||||
|
document.getElementById('log-prev-btn').disabled = currentLogPage === 1;
|
||||||
|
document.getElementById('log-next-btn').disabled = data.logs.length < logsPerPage;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки логов:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовать логи
|
||||||
|
*/
|
||||||
|
function renderLogs(logs) {
|
||||||
|
const tbody = document.getElementById('log-table-body');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if (logs.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: var(--text-muted);">Логи не найдены</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.forEach(log => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
const time = new Date(log.timestamp).toLocaleString('ru-RU');
|
||||||
|
const details = typeof log.actionData === 'object'
|
||||||
|
? JSON.stringify(log.actionData).substring(0, 50) + '...'
|
||||||
|
: log.actionData;
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${time}</td>
|
||||||
|
<td>${log.username || 'Гость'}</td>
|
||||||
|
<td><span class="log-action-badge ${log.actionType}">${log.actionType}</span></td>
|
||||||
|
<td title="${JSON.stringify(log.actionData)}">${details}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ОЧИСТКА ЧАТА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистить чат при смене режима игры
|
||||||
|
*/
|
||||||
|
function clearGameChat() {
|
||||||
|
const container = document.getElementById('game-chat-messages');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистить чат лобби
|
||||||
|
*/
|
||||||
|
function clearLobbyChat() {
|
||||||
|
const container = document.getElementById('lobby-chat-messages');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспортируем для глобального использования
|
||||||
|
window.currentUser = currentUser;
|
||||||
|
window.authToken = authToken;
|
||||||
|
window.isGuest = isGuest;
|
||||||
|
|
@ -0,0 +1,912 @@
|
||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* Texas Hold'em - Игровая логика (клиентская сторона)
|
||||||
|
* Колода, раздача, оценка рук, определение победителя
|
||||||
|
* =============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// КОНСТАНТЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const SUITS = ['hearts', 'diamonds', 'clubs', 'spades'];
|
||||||
|
const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
|
||||||
|
|
||||||
|
const SUIT_SYMBOLS = {
|
||||||
|
hearts: '♥',
|
||||||
|
diamonds: '♦',
|
||||||
|
clubs: '♣',
|
||||||
|
spades: '♠'
|
||||||
|
};
|
||||||
|
|
||||||
|
const HAND_RANKINGS = {
|
||||||
|
1: 'Старшая карта',
|
||||||
|
2: 'Пара',
|
||||||
|
3: 'Две пары',
|
||||||
|
4: 'Сет',
|
||||||
|
5: 'Стрит',
|
||||||
|
6: 'Флеш',
|
||||||
|
7: 'Фулл-хаус',
|
||||||
|
8: 'Каре',
|
||||||
|
9: 'Стрит-флеш',
|
||||||
|
10: 'Роял-флеш'
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// КЛАСС КАРТЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class Card {
|
||||||
|
constructor(suit, rank) {
|
||||||
|
this.suit = suit;
|
||||||
|
this.rank = rank;
|
||||||
|
this.value = RANKS.indexOf(rank) + 2; // 2-14 (A=14)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить HTML элемент карты
|
||||||
|
*/
|
||||||
|
toHTML(isSmall = false, isBack = false) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `card ${this.suit}${isSmall ? ' card-small' : ''}${isBack ? ' card-back' : ''}`;
|
||||||
|
|
||||||
|
if (!isBack) {
|
||||||
|
card.innerHTML = `
|
||||||
|
<span class="card-rank">${this.rank}</span>
|
||||||
|
<span class="card-suit">${SUIT_SYMBOLS[this.suit]}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `${this.rank}${SUIT_SYMBOLS[this.suit]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// КЛАСС КОЛОДЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class Deck {
|
||||||
|
constructor() {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сбросить и перемешать колоду
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.cards = [];
|
||||||
|
for (const suit of SUITS) {
|
||||||
|
for (const rank of RANKS) {
|
||||||
|
this.cards.push(new Card(suit, rank));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.shuffle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перемешать колоду (Fisher-Yates)
|
||||||
|
*/
|
||||||
|
shuffle() {
|
||||||
|
for (let i = this.cards.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[this.cards[i], this.cards[j]] = [this.cards[j], this.cards[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Раздать карты
|
||||||
|
*/
|
||||||
|
deal(count = 1) {
|
||||||
|
return this.cards.splice(0, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// КЛАСС ИГРОКА
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class Player {
|
||||||
|
constructor(id, name, chips = 1000, isAI = false, aiLevel = 1) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.chips = chips;
|
||||||
|
this.hand = [];
|
||||||
|
this.bet = 0;
|
||||||
|
this.totalBet = 0;
|
||||||
|
this.folded = false;
|
||||||
|
this.allIn = false;
|
||||||
|
this.isDealer = false;
|
||||||
|
this.isSmallBlind = false;
|
||||||
|
this.isBigBlind = false;
|
||||||
|
this.isAI = isAI;
|
||||||
|
this.aiLevel = aiLevel;
|
||||||
|
this.lastAction = null;
|
||||||
|
this.isConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сбросить состояние для новой раздачи
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.hand = [];
|
||||||
|
this.bet = 0;
|
||||||
|
this.totalBet = 0;
|
||||||
|
this.folded = false;
|
||||||
|
this.allIn = false;
|
||||||
|
this.isDealer = false;
|
||||||
|
this.isSmallBlind = false;
|
||||||
|
this.isBigBlind = false;
|
||||||
|
this.lastAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// КЛАСС ИГРЫ
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class PokerGame {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.players = [];
|
||||||
|
this.deck = new Deck();
|
||||||
|
this.communityCards = [];
|
||||||
|
this.pot = 0;
|
||||||
|
this.sidePots = [];
|
||||||
|
this.currentBet = 0;
|
||||||
|
this.dealerIndex = -1;
|
||||||
|
this.currentPlayerIndex = 0;
|
||||||
|
this.gamePhase = 'waiting'; // waiting, preflop, flop, turn, river, showdown
|
||||||
|
this.smallBlind = options.smallBlind || 5;
|
||||||
|
this.bigBlind = options.bigBlind || 10;
|
||||||
|
this.minRaise = this.bigBlind;
|
||||||
|
this.lastRaiseAmount = this.bigBlind;
|
||||||
|
this.isGameStarted = false;
|
||||||
|
this.onUpdate = options.onUpdate || (() => {});
|
||||||
|
this.onAction = options.onAction || (() => {});
|
||||||
|
this.onHandEnd = options.onHandEnd || (() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавить игрока
|
||||||
|
*/
|
||||||
|
addPlayer(player) {
|
||||||
|
this.players.push(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить активных игроков
|
||||||
|
*/
|
||||||
|
getActivePlayers() {
|
||||||
|
return this.players.filter(p => !p.folded && p.chips >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить игроков с фишками
|
||||||
|
*/
|
||||||
|
getPlayersWithChips() {
|
||||||
|
return this.players.filter(p => p.chips > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Начать новую раздачу
|
||||||
|
*/
|
||||||
|
startNewHand() {
|
||||||
|
if (this.getPlayersWithChips().length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сброс состояния
|
||||||
|
this.deck.reset();
|
||||||
|
this.communityCards = [];
|
||||||
|
this.pot = 0;
|
||||||
|
this.sidePots = [];
|
||||||
|
this.currentBet = 0;
|
||||||
|
this.minRaise = this.bigBlind;
|
||||||
|
this.lastRaiseAmount = this.bigBlind;
|
||||||
|
|
||||||
|
// Сброс игроков
|
||||||
|
for (const player of this.players) {
|
||||||
|
player.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определение позиций
|
||||||
|
this.moveDealer();
|
||||||
|
this.assignBlinds();
|
||||||
|
|
||||||
|
// Раздача карт
|
||||||
|
this.dealHoleCards();
|
||||||
|
|
||||||
|
// Начало игры
|
||||||
|
this.gamePhase = 'preflop';
|
||||||
|
this.isGameStarted = true;
|
||||||
|
|
||||||
|
// Ставки блайндов
|
||||||
|
this.postBlinds();
|
||||||
|
|
||||||
|
// Первый ход
|
||||||
|
this.setFirstPlayer();
|
||||||
|
|
||||||
|
this.onUpdate();
|
||||||
|
|
||||||
|
// Если первый ход - ИИ
|
||||||
|
this.checkAITurn();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Передвинуть баттон дилера
|
||||||
|
*/
|
||||||
|
moveDealer() {
|
||||||
|
const playersWithChips = this.getPlayersWithChips();
|
||||||
|
if (playersWithChips.length < 2) return;
|
||||||
|
|
||||||
|
// Находим следующего дилера
|
||||||
|
do {
|
||||||
|
this.dealerIndex = (this.dealerIndex + 1) % this.players.length;
|
||||||
|
} while (this.players[this.dealerIndex].chips <= 0);
|
||||||
|
|
||||||
|
this.players[this.dealerIndex].isDealer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Назначить блайнды
|
||||||
|
*/
|
||||||
|
assignBlinds() {
|
||||||
|
const playersWithChips = this.getPlayersWithChips();
|
||||||
|
|
||||||
|
if (playersWithChips.length === 2) {
|
||||||
|
// Heads-up: дилер = SB
|
||||||
|
this.players[this.dealerIndex].isSmallBlind = true;
|
||||||
|
let bbIndex = this.getNextActivePlayerIndex(this.dealerIndex);
|
||||||
|
this.players[bbIndex].isBigBlind = true;
|
||||||
|
} else {
|
||||||
|
// 3+ игроков
|
||||||
|
let sbIndex = this.getNextActivePlayerIndex(this.dealerIndex);
|
||||||
|
this.players[sbIndex].isSmallBlind = true;
|
||||||
|
|
||||||
|
let bbIndex = this.getNextActivePlayerIndex(sbIndex);
|
||||||
|
this.players[bbIndex].isBigBlind = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поставить блайнды
|
||||||
|
*/
|
||||||
|
postBlinds() {
|
||||||
|
const sbPlayer = this.players.find(p => p.isSmallBlind);
|
||||||
|
const bbPlayer = this.players.find(p => p.isBigBlind);
|
||||||
|
|
||||||
|
if (sbPlayer) {
|
||||||
|
const sbAmount = Math.min(sbPlayer.chips, this.smallBlind);
|
||||||
|
sbPlayer.chips -= sbAmount;
|
||||||
|
sbPlayer.bet = sbAmount;
|
||||||
|
sbPlayer.totalBet = sbAmount;
|
||||||
|
this.pot += sbAmount;
|
||||||
|
sbPlayer.lastAction = `SB ${sbAmount}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bbPlayer) {
|
||||||
|
const bbAmount = Math.min(bbPlayer.chips, this.bigBlind);
|
||||||
|
bbPlayer.chips -= bbAmount;
|
||||||
|
bbPlayer.bet = bbAmount;
|
||||||
|
bbPlayer.totalBet = bbAmount;
|
||||||
|
this.pot += bbAmount;
|
||||||
|
this.currentBet = bbAmount;
|
||||||
|
bbPlayer.lastAction = `BB ${bbAmount}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Раздать карманные карты
|
||||||
|
*/
|
||||||
|
dealHoleCards() {
|
||||||
|
for (const player of this.getPlayersWithChips()) {
|
||||||
|
player.hand = this.deck.deal(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить первого игрока для хода
|
||||||
|
*/
|
||||||
|
setFirstPlayer() {
|
||||||
|
if (this.gamePhase === 'preflop') {
|
||||||
|
// UTG - после BB
|
||||||
|
const bbPlayer = this.players.find(p => p.isBigBlind);
|
||||||
|
const bbIndex = this.players.indexOf(bbPlayer);
|
||||||
|
this.currentPlayerIndex = this.getNextActivePlayerIndex(bbIndex);
|
||||||
|
} else {
|
||||||
|
// После флопа - первый активный после дилера
|
||||||
|
this.currentPlayerIndex = this.getNextActivePlayerIndex(this.dealerIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить индекс следующего активного игрока
|
||||||
|
*/
|
||||||
|
getNextActivePlayerIndex(fromIndex) {
|
||||||
|
let index = (fromIndex + 1) % this.players.length;
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
while (attempts < this.players.length) {
|
||||||
|
const player = this.players[index];
|
||||||
|
if (!player.folded && player.chips >= 0 && !player.allIn) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
index = (index + 1) % this.players.length;
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Текущий игрок
|
||||||
|
*/
|
||||||
|
getCurrentPlayer() {
|
||||||
|
return this.players[this.currentPlayerIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить доступные действия для игрока
|
||||||
|
*/
|
||||||
|
getAvailableActions(player) {
|
||||||
|
const actions = [];
|
||||||
|
const toCall = this.currentBet - player.bet;
|
||||||
|
|
||||||
|
// Фолд всегда доступен
|
||||||
|
actions.push('fold');
|
||||||
|
|
||||||
|
if (toCall === 0) {
|
||||||
|
// Можно чекнуть
|
||||||
|
actions.push('check');
|
||||||
|
|
||||||
|
// Можно поставить
|
||||||
|
if (player.chips > 0) {
|
||||||
|
actions.push('bet');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Нужно колл или рейз
|
||||||
|
if (player.chips > toCall) {
|
||||||
|
actions.push('call');
|
||||||
|
actions.push('raise');
|
||||||
|
} else {
|
||||||
|
// Можно только колл (олл-ин)
|
||||||
|
actions.push('call');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All-in всегда доступен если есть фишки
|
||||||
|
if (player.chips > 0) {
|
||||||
|
actions.push('allin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработать действие игрока
|
||||||
|
*/
|
||||||
|
processAction(playerId, action, amount = 0) {
|
||||||
|
const player = this.players.find(p => p.id === playerId);
|
||||||
|
if (!player) return { success: false, error: 'Игрок не найден' };
|
||||||
|
|
||||||
|
const currentPlayer = this.players[this.currentPlayerIndex];
|
||||||
|
if (currentPlayer.id !== playerId) {
|
||||||
|
return { success: false, error: 'Сейчас не ваш ход' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { success: true, action, amount: 0 };
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'fold':
|
||||||
|
player.folded = true;
|
||||||
|
player.lastAction = 'Фолд';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'check':
|
||||||
|
if (player.bet < this.currentBet) {
|
||||||
|
return { success: false, error: 'Нельзя чекнуть' };
|
||||||
|
}
|
||||||
|
player.lastAction = 'Чек';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'call':
|
||||||
|
const callAmount = Math.min(this.currentBet - player.bet, player.chips);
|
||||||
|
player.chips -= callAmount;
|
||||||
|
player.bet += callAmount;
|
||||||
|
player.totalBet += callAmount;
|
||||||
|
this.pot += callAmount;
|
||||||
|
result.amount = callAmount;
|
||||||
|
player.lastAction = `Колл ${callAmount}`;
|
||||||
|
|
||||||
|
if (player.chips === 0) {
|
||||||
|
player.allIn = true;
|
||||||
|
player.lastAction = 'Олл-ин';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bet':
|
||||||
|
if (this.currentBet > 0) {
|
||||||
|
return { success: false, error: 'Используйте рейз' };
|
||||||
|
}
|
||||||
|
if (amount < this.bigBlind) {
|
||||||
|
amount = this.bigBlind;
|
||||||
|
}
|
||||||
|
if (amount > player.chips) {
|
||||||
|
amount = player.chips;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.chips -= amount;
|
||||||
|
player.bet = amount;
|
||||||
|
player.totalBet += amount;
|
||||||
|
this.pot += amount;
|
||||||
|
this.currentBet = amount;
|
||||||
|
this.lastRaiseAmount = amount;
|
||||||
|
this.minRaise = amount;
|
||||||
|
result.amount = amount;
|
||||||
|
player.lastAction = `Бет ${amount}`;
|
||||||
|
|
||||||
|
if (player.chips === 0) {
|
||||||
|
player.allIn = true;
|
||||||
|
player.lastAction = 'Олл-ин';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'raise':
|
||||||
|
const toCall = this.currentBet - player.bet;
|
||||||
|
const minRaiseTotal = this.currentBet + this.lastRaiseAmount;
|
||||||
|
|
||||||
|
if (amount < minRaiseTotal && amount < player.chips + player.bet) {
|
||||||
|
amount = minRaiseTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raiseTotal = Math.min(amount, player.chips + player.bet);
|
||||||
|
const raiseAmount = raiseTotal - player.bet;
|
||||||
|
|
||||||
|
player.chips -= raiseAmount;
|
||||||
|
this.pot += raiseAmount;
|
||||||
|
this.lastRaiseAmount = raiseTotal - this.currentBet;
|
||||||
|
this.currentBet = raiseTotal;
|
||||||
|
player.bet = raiseTotal;
|
||||||
|
player.totalBet += raiseAmount;
|
||||||
|
result.amount = raiseTotal;
|
||||||
|
player.lastAction = `Рейз до ${raiseTotal}`;
|
||||||
|
|
||||||
|
if (player.chips === 0) {
|
||||||
|
player.allIn = true;
|
||||||
|
player.lastAction = 'Олл-ин';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'allin':
|
||||||
|
const allInAmount = player.chips;
|
||||||
|
this.pot += allInAmount;
|
||||||
|
player.bet += allInAmount;
|
||||||
|
player.totalBet += allInAmount;
|
||||||
|
result.amount = player.bet;
|
||||||
|
|
||||||
|
if (player.bet > this.currentBet) {
|
||||||
|
this.lastRaiseAmount = player.bet - this.currentBet;
|
||||||
|
this.currentBet = player.bet;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.chips = 0;
|
||||||
|
player.allIn = true;
|
||||||
|
player.lastAction = `Олл-ин ${player.bet}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { success: false, error: 'Неизвестное действие' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Уведомляем о действии
|
||||||
|
this.onAction(player, action, result.amount);
|
||||||
|
|
||||||
|
// Проверка завершения раунда ставок
|
||||||
|
if (this.isBettingRoundComplete()) {
|
||||||
|
this.nextPhase();
|
||||||
|
} else {
|
||||||
|
this.moveToNextPlayer();
|
||||||
|
this.onUpdate();
|
||||||
|
this.checkAITurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перейти к следующему игроку
|
||||||
|
*/
|
||||||
|
moveToNextPlayer() {
|
||||||
|
const nextIndex = this.getNextActivePlayerIndex(this.currentPlayerIndex);
|
||||||
|
if (nextIndex !== -1) {
|
||||||
|
this.currentPlayerIndex = nextIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить, ход ли ИИ
|
||||||
|
*/
|
||||||
|
checkAITurn() {
|
||||||
|
if (!this.isGameStarted) return;
|
||||||
|
|
||||||
|
const currentPlayer = this.getCurrentPlayer();
|
||||||
|
if (currentPlayer && currentPlayer.isAI) {
|
||||||
|
// Задержка для реалистичности
|
||||||
|
setTimeout(() => {
|
||||||
|
this.makeAIMove(currentPlayer);
|
||||||
|
}, 1000 + Math.random() * 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ход ИИ
|
||||||
|
*/
|
||||||
|
makeAIMove(player) {
|
||||||
|
if (!this.isGameStarted || player.folded || player.allIn) return;
|
||||||
|
|
||||||
|
const decision = pokerAI.makeDecision(player, this);
|
||||||
|
this.processAction(player.id, decision.action, decision.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить завершение раунда ставок
|
||||||
|
*/
|
||||||
|
isBettingRoundComplete() {
|
||||||
|
const activePlayers = this.getActivePlayers().filter(p => !p.allIn);
|
||||||
|
|
||||||
|
// Все фолднули кроме одного
|
||||||
|
if (this.getActivePlayers().length === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все активные (не all-in) игроки уравняли ставку
|
||||||
|
if (activePlayers.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все поставили одинаково и сделали действие
|
||||||
|
const allEqual = activePlayers.every(p => p.bet === this.currentBet);
|
||||||
|
const allActed = activePlayers.every(p => p.lastAction !== null &&
|
||||||
|
!p.lastAction.includes('SB') && !p.lastAction.includes('BB'));
|
||||||
|
|
||||||
|
return allEqual && allActed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переход к следующей фазе
|
||||||
|
*/
|
||||||
|
nextPhase() {
|
||||||
|
// Сброс ставок для новой улицы
|
||||||
|
for (const player of this.players) {
|
||||||
|
player.bet = 0;
|
||||||
|
if (!player.folded && !player.allIn) {
|
||||||
|
player.lastAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.currentBet = 0;
|
||||||
|
|
||||||
|
const activePlayers = this.getActivePlayers();
|
||||||
|
|
||||||
|
// Один игрок остался - он победитель
|
||||||
|
if (activePlayers.length === 1) {
|
||||||
|
this.endHand([activePlayers[0]]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.gamePhase) {
|
||||||
|
case 'preflop':
|
||||||
|
this.gamePhase = 'flop';
|
||||||
|
this.communityCards = this.deck.deal(3);
|
||||||
|
break;
|
||||||
|
case 'flop':
|
||||||
|
this.gamePhase = 'turn';
|
||||||
|
this.communityCards.push(...this.deck.deal(1));
|
||||||
|
break;
|
||||||
|
case 'turn':
|
||||||
|
this.gamePhase = 'river';
|
||||||
|
this.communityCards.push(...this.deck.deal(1));
|
||||||
|
break;
|
||||||
|
case 'river':
|
||||||
|
this.gamePhase = 'showdown';
|
||||||
|
this.determineWinner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Установка первого игрока для новой улицы
|
||||||
|
this.setFirstPlayer();
|
||||||
|
|
||||||
|
this.onUpdate();
|
||||||
|
|
||||||
|
// Если все в all-in или только один активный, автоматически переходим
|
||||||
|
const nonAllInPlayers = this.getActivePlayers().filter(p => !p.allIn);
|
||||||
|
if (nonAllInPlayers.length <= 1) {
|
||||||
|
setTimeout(() => this.nextPhase(), 1500);
|
||||||
|
} else {
|
||||||
|
this.checkAITurn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определить победителя
|
||||||
|
*/
|
||||||
|
determineWinner() {
|
||||||
|
const activePlayers = this.getActivePlayers();
|
||||||
|
|
||||||
|
if (activePlayers.length === 1) {
|
||||||
|
this.endHand([activePlayers[0]]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Оценка рук
|
||||||
|
const playerHands = activePlayers.map(player => ({
|
||||||
|
player,
|
||||||
|
handResult: evaluateHand([...player.hand, ...this.communityCards])
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Сортировка по силе руки
|
||||||
|
playerHands.sort((a, b) => {
|
||||||
|
if (b.handResult.rank !== a.handResult.rank) {
|
||||||
|
return b.handResult.rank - a.handResult.rank;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < a.handResult.kickers.length; i++) {
|
||||||
|
if (b.handResult.kickers[i] !== a.handResult.kickers[i]) {
|
||||||
|
return b.handResult.kickers[i] - a.handResult.kickers[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Находим победителей (может быть сплит)
|
||||||
|
const winners = [playerHands[0]];
|
||||||
|
for (let i = 1; i < playerHands.length; i++) {
|
||||||
|
const cmp = this.compareHands(playerHands[0].handResult, playerHands[i].handResult);
|
||||||
|
if (cmp === 0) {
|
||||||
|
winners.push(playerHands[i]);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.endHand(
|
||||||
|
winners.map(w => w.player),
|
||||||
|
playerHands.map(ph => ({ player: ph.player, hand: ph.handResult }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сравнение двух рук
|
||||||
|
*/
|
||||||
|
compareHands(h1, h2) {
|
||||||
|
if (h1.rank !== h2.rank) return h2.rank - h1.rank;
|
||||||
|
for (let i = 0; i < h1.kickers.length; i++) {
|
||||||
|
if (h1.kickers[i] !== h2.kickers[i]) {
|
||||||
|
return h2.kickers[i] - h1.kickers[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Завершить раздачу
|
||||||
|
*/
|
||||||
|
endHand(winners, allHands = null) {
|
||||||
|
const winAmount = Math.floor(this.pot / winners.length);
|
||||||
|
|
||||||
|
for (const winner of winners) {
|
||||||
|
winner.chips += winAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Остаток при нечётном сплите
|
||||||
|
const remainder = this.pot % winners.length;
|
||||||
|
if (remainder > 0) {
|
||||||
|
winners[0].chips += remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gamePhase = 'showdown';
|
||||||
|
this.isGameStarted = false;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
winners: winners.map(w => ({
|
||||||
|
id: w.id,
|
||||||
|
name: w.name,
|
||||||
|
amount: winAmount,
|
||||||
|
hand: allHands ? allHands.find(h => h.player.id === w.id)?.hand : null
|
||||||
|
})),
|
||||||
|
pot: this.pot,
|
||||||
|
hands: allHands
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onHandEnd(result);
|
||||||
|
this.onUpdate();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ОЦЕНКА РУК
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Оценить покерную руку из 7 карт
|
||||||
|
*/
|
||||||
|
function evaluateHand(cards) {
|
||||||
|
const allCombinations = getCombinations(cards, 5);
|
||||||
|
let bestHand = null;
|
||||||
|
|
||||||
|
for (const combo of allCombinations) {
|
||||||
|
const hand = evaluateFiveCards(combo);
|
||||||
|
if (!bestHand || compareHandResults(hand, bestHand) > 0) {
|
||||||
|
bestHand = hand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestHand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить все комбинации из n элементов
|
||||||
|
*/
|
||||||
|
function getCombinations(arr, n) {
|
||||||
|
if (n === 0) return [[]];
|
||||||
|
if (arr.length === 0) return [];
|
||||||
|
|
||||||
|
const [first, ...rest] = arr;
|
||||||
|
const withFirst = getCombinations(rest, n - 1).map(c => [first, ...c]);
|
||||||
|
const withoutFirst = getCombinations(rest, n);
|
||||||
|
|
||||||
|
return [...withFirst, ...withoutFirst];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Оценить 5 карт
|
||||||
|
*/
|
||||||
|
function evaluateFiveCards(cards) {
|
||||||
|
const sortedCards = [...cards].sort((a, b) => b.value - a.value);
|
||||||
|
const values = sortedCards.map(c => c.value);
|
||||||
|
const suits = sortedCards.map(c => c.suit);
|
||||||
|
|
||||||
|
const isFlush = suits.every(s => s === suits[0]);
|
||||||
|
const isStraight = checkStraight(values);
|
||||||
|
const isWheel = values.join(',') === '14,5,4,3,2'; // A-2-3-4-5
|
||||||
|
|
||||||
|
const valueCounts = {};
|
||||||
|
for (const v of values) {
|
||||||
|
valueCounts[v] = (valueCounts[v] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = Object.values(valueCounts).sort((a, b) => b - a);
|
||||||
|
const uniqueValues = Object.keys(valueCounts)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (valueCounts[b] !== valueCounts[a]) {
|
||||||
|
return valueCounts[b] - valueCounts[a];
|
||||||
|
}
|
||||||
|
return b - a;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Рояль-флеш
|
||||||
|
if (isFlush && isStraight && values[0] === 14) {
|
||||||
|
return { rank: 10, name: 'Роял-флеш', kickers: values };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Стрит-флеш
|
||||||
|
if (isFlush && (isStraight || isWheel)) {
|
||||||
|
return { rank: 9, name: 'Стрит-флеш', kickers: isWheel ? [5, 4, 3, 2, 1] : values };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Каре
|
||||||
|
if (counts[0] === 4) {
|
||||||
|
return { rank: 8, name: 'Каре', kickers: uniqueValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фулл-хаус
|
||||||
|
if (counts[0] === 3 && counts[1] === 2) {
|
||||||
|
return { rank: 7, name: 'Фулл-хаус', kickers: uniqueValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Флеш
|
||||||
|
if (isFlush) {
|
||||||
|
return { rank: 6, name: 'Флеш', kickers: values };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Стрит
|
||||||
|
if (isStraight || isWheel) {
|
||||||
|
return { rank: 5, name: 'Стрит', kickers: isWheel ? [5, 4, 3, 2, 1] : values };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сет
|
||||||
|
if (counts[0] === 3) {
|
||||||
|
return { rank: 4, name: 'Сет', kickers: uniqueValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Две пары
|
||||||
|
if (counts[0] === 2 && counts[1] === 2) {
|
||||||
|
return { rank: 3, name: 'Две пары', kickers: uniqueValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пара
|
||||||
|
if (counts[0] === 2) {
|
||||||
|
return { rank: 2, name: 'Пара', kickers: uniqueValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Старшая карта
|
||||||
|
return { rank: 1, name: 'Старшая карта', kickers: values };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка на стрит
|
||||||
|
*/
|
||||||
|
function checkStraight(values) {
|
||||||
|
for (let i = 0; i < values.length - 1; i++) {
|
||||||
|
if (values[i] - values[i + 1] !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сравнение результатов рук
|
||||||
|
*/
|
||||||
|
function compareHandResults(h1, h2) {
|
||||||
|
if (h1.rank !== h2.rank) return h1.rank - h2.rank;
|
||||||
|
for (let i = 0; i < h1.kickers.length; i++) {
|
||||||
|
if (h1.kickers[i] !== h2.kickers[i]) {
|
||||||
|
return h1.kickers[i] - h2.kickers[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить силу руки для отображения
|
||||||
|
*/
|
||||||
|
function getHandStrength(cards, communityCards) {
|
||||||
|
if (cards.length < 2) return null;
|
||||||
|
|
||||||
|
const allCards = [...cards];
|
||||||
|
if (communityCards && communityCards.length > 0) {
|
||||||
|
allCards.push(...communityCards);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allCards.length < 5) {
|
||||||
|
// Только карманные карты - оцениваем префлоп силу
|
||||||
|
return getPreflopHandName(cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hand = evaluateHand(allCards);
|
||||||
|
return hand.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить название префлоп руки
|
||||||
|
*/
|
||||||
|
function getPreflopHandName(cards) {
|
||||||
|
if (cards.length !== 2) return '';
|
||||||
|
|
||||||
|
const [c1, c2] = cards;
|
||||||
|
const highCard = c1.value > c2.value ? c1 : c2;
|
||||||
|
const lowCard = c1.value > c2.value ? c2 : c1;
|
||||||
|
const suited = c1.suit === c2.suit;
|
||||||
|
|
||||||
|
let name = '';
|
||||||
|
|
||||||
|
if (c1.value === c2.value) {
|
||||||
|
name = `Пара ${c1.rank}`;
|
||||||
|
} else {
|
||||||
|
name = `${highCard.rank}${lowCard.rank}${suited ? 's' : 'o'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспорт для использования в других модулях
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = { Card, Deck, Player, PokerGame, evaluateHand, getHandStrength };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,899 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>🃏 Texas Hold'em Poker</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Экран авторизации -->
|
||||||
|
<div id="auth-screen" class="screen active">
|
||||||
|
<div class="glass-container menu-container">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🃏</span>
|
||||||
|
<h1>Texas Hold'em</h1>
|
||||||
|
<p class="subtitle">No-Limit Poker</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="auth-tabs" class="auth-tabs">
|
||||||
|
<button class="btn btn-tab active" onclick="switchAuthTab('login')">Вход</button>
|
||||||
|
<button class="btn btn-tab" onclick="switchAuthTab('register')">Регистрация</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Форма входа -->
|
||||||
|
<div id="login-form" class="auth-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Логин</label>
|
||||||
|
<input type="text" id="login-username" class="input" placeholder="Введите логин" autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Пароль</label>
|
||||||
|
<input type="password" id="login-password" class="input" placeholder="Введите пароль" autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-large" onclick="handleLogin()">Войти</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Форма регистрации -->
|
||||||
|
<div id="register-form" class="auth-form" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Логин</label>
|
||||||
|
<input type="text" id="register-username" class="input" placeholder="Минимум 3 символа" autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Пароль</label>
|
||||||
|
<input type="password" id="register-password" class="input" placeholder="Минимум 6 символов" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Повторите пароль</label>
|
||||||
|
<input type="password" id="register-password-confirm" class="input" placeholder="Повторите пароль" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-large" onclick="handleRegister()">Зарегистрироваться</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-guest">
|
||||||
|
<span>или</span>
|
||||||
|
<button class="btn btn-outline" onclick="playAsGuest()">Играть как гость</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Главное меню -->
|
||||||
|
<div id="main-menu" class="screen">
|
||||||
|
<div class="glass-container menu-container">
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icon">🃏</span>
|
||||||
|
<h1>Texas Hold'em</h1>
|
||||||
|
<p class="subtitle">No-Limit Poker</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Информация о пользователе -->
|
||||||
|
<div id="user-info" class="user-info">
|
||||||
|
<span id="user-display-name">Гость</span>
|
||||||
|
<span id="user-role-badge" class="role-badge" style="display: none;">ADMIN</span>
|
||||||
|
<button class="btn btn-small btn-outline" onclick="handleLogout()">Выход</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="menu-buttons">
|
||||||
|
<button class="btn btn-primary btn-large" onclick="showScreen('single-player-menu')">
|
||||||
|
<span class="btn-icon">🤖</span>
|
||||||
|
Одиночная игра
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-large" onclick="showScreen('multiplayer-menu')">
|
||||||
|
<span class="btn-icon">👥</span>
|
||||||
|
Мультиплеер
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline btn-large" onclick="showScreen('leaderboard-screen')">
|
||||||
|
<span class="btn-icon">🏆</span>
|
||||||
|
Таблица лидеров
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-row">
|
||||||
|
<button class="btn btn-icon-only" onclick="toggleSound()" id="sound-toggle">
|
||||||
|
🔊
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-icon-only" onclick="showScreen('settings-screen')">
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
|
<!-- Кнопка админ-панели (только для админов) -->
|
||||||
|
<button class="btn btn-icon-only admin-only" onclick="showScreen('admin-screen')" style="display: none;" id="admin-btn">
|
||||||
|
👑
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Меню одиночной игры -->
|
||||||
|
<div id="single-player-menu" class="screen">
|
||||||
|
<div class="glass-container menu-container">
|
||||||
|
<button class="btn-back" onclick="showScreen('main-menu')">← Назад</button>
|
||||||
|
<h2>Одиночная игра</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ваше имя</label>
|
||||||
|
<input type="text" id="sp-player-name" class="input" placeholder="Введите имя" value="Игрок">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Количество ботов</label>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-option active" data-value="1" onclick="selectOption(this, 'bot-count')">1</button>
|
||||||
|
<button class="btn btn-option" data-value="2" onclick="selectOption(this, 'bot-count')">2</button>
|
||||||
|
<button class="btn btn-option" data-value="3" onclick="selectOption(this, 'bot-count')">3</button>
|
||||||
|
<button class="btn btn-option" data-value="4" onclick="selectOption(this, 'bot-count')">4</button>
|
||||||
|
<button class="btn btn-option" data-value="5" onclick="selectOption(this, 'bot-count')">5</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Сложность ИИ</label>
|
||||||
|
<div class="btn-group difficulty-group">
|
||||||
|
<button class="btn btn-option active" data-value="1" onclick="selectOption(this, 'ai-difficulty')">
|
||||||
|
<span class="diff-icon">🎲</span>
|
||||||
|
<span class="diff-name">Новичок</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-option" data-value="2" onclick="selectOption(this, 'ai-difficulty')">
|
||||||
|
<span class="diff-icon">🎯</span>
|
||||||
|
<span class="diff-name">Опытный</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-option" data-value="3" onclick="selectOption(this, 'ai-difficulty')">
|
||||||
|
<span class="diff-icon">🧠</span>
|
||||||
|
<span class="diff-name">Эксперт</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Начальный стек</label>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-option" data-value="500" onclick="selectOption(this, 'starting-stack')">500</button>
|
||||||
|
<button class="btn btn-option active" data-value="1000" onclick="selectOption(this, 'starting-stack')">1000</button>
|
||||||
|
<button class="btn btn-option" data-value="2000" onclick="selectOption(this, 'starting-stack')">2000</button>
|
||||||
|
<button class="btn btn-option" data-value="5000" onclick="selectOption(this, 'starting-stack')">5000</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-large" onclick="startSinglePlayer()">
|
||||||
|
Начать игру
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Меню мультиплеера -->
|
||||||
|
<div id="multiplayer-menu" class="screen">
|
||||||
|
<div class="glass-container menu-container">
|
||||||
|
<button class="btn-back" onclick="showScreen('main-menu')">← Назад</button>
|
||||||
|
<h2>Мультиплеер</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ваше имя</label>
|
||||||
|
<input type="text" id="mp-player-name" class="input" placeholder="Введите имя" value="Игрок">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab active" onclick="switchTab('join-tab')">Присоединиться</button>
|
||||||
|
<button class="tab" onclick="switchTab('create-tab')">Создать комнату</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="join-tab" class="tab-content active">
|
||||||
|
<div class="room-list" id="room-list">
|
||||||
|
<div class="room-list-loading">Подключение к серверу...</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline" onclick="refreshRooms()">🔄 Обновить</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="create-tab" class="tab-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Название комнаты</label>
|
||||||
|
<input type="text" id="room-name" class="input" placeholder="Моя комната">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Блайнды</label>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-option active" data-value="5/10" onclick="selectOption(this, 'blinds')">5/10</button>
|
||||||
|
<button class="btn btn-option" data-value="10/20" onclick="selectOption(this, 'blinds')">10/20</button>
|
||||||
|
<button class="btn btn-option" data-value="25/50" onclick="selectOption(this, 'blinds')">25/50</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Макс. игроков</label>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-option" data-value="2" onclick="selectOption(this, 'max-players')">2</button>
|
||||||
|
<button class="btn btn-option" data-value="4" onclick="selectOption(this, 'max-players')">4</button>
|
||||||
|
<button class="btn btn-option active" data-value="6" onclick="selectOption(this, 'max-players')">6</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-large" onclick="createRoom()">
|
||||||
|
Создать комнату
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Лобби комнаты -->
|
||||||
|
<div id="room-lobby" class="screen">
|
||||||
|
<div class="glass-container lobby-container">
|
||||||
|
<button class="btn-back" onclick="leaveRoom()">← Выйти</button>
|
||||||
|
<h2 id="lobby-room-name">Комната</h2>
|
||||||
|
|
||||||
|
<!-- Блок для шаринга ссылки на комнату -->
|
||||||
|
<div class="room-share-section">
|
||||||
|
<div class="room-link-display">
|
||||||
|
<input type="text" id="room-link-input" class="input" readonly onclick="this.select()">
|
||||||
|
<button class="btn btn-secondary btn-icon-only" onclick="copyRoomLink()" title="Копировать ссылку">
|
||||||
|
📋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="share-buttons">
|
||||||
|
<button class="btn btn-outline btn-small" onclick="shareToTelegram()">
|
||||||
|
<span>📱 Telegram</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline btn-small" onclick="shareToWhatsApp()">
|
||||||
|
<span>💬 WhatsApp</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline btn-small" onclick="shareGeneric()" id="share-generic-btn">
|
||||||
|
<span>🔗 Поделиться</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lobby-players" id="lobby-players">
|
||||||
|
<!-- Игроки будут добавлены динамически -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lobby-info">
|
||||||
|
<span>Блайнды: <strong id="lobby-blinds">5/10</strong></span>
|
||||||
|
<span>Игроки: <strong id="lobby-player-count">1/6</strong></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-large" id="start-game-btn" onclick="startMultiplayerGame()">
|
||||||
|
Начать игру
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="lobby-chat">
|
||||||
|
<div class="chat-messages" id="lobby-chat-messages"></div>
|
||||||
|
<div class="chat-input-row">
|
||||||
|
<input type="text" id="lobby-chat-input" class="input" placeholder="Сообщение..." onkeypress="handleLobbyChatKey(event)">
|
||||||
|
<button class="btn btn-primary" onclick="sendLobbyChat()">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Игровой экран -->
|
||||||
|
<div id="game-screen" class="screen">
|
||||||
|
<div class="game-container">
|
||||||
|
<!-- Информация о столе -->
|
||||||
|
<div class="table-info">
|
||||||
|
<div class="pot-display glass-card">
|
||||||
|
<span class="pot-label">Банк</span>
|
||||||
|
<span class="pot-amount" id="pot-amount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="phase-display glass-card" id="game-phase">
|
||||||
|
Ожидание
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Покерный стол -->
|
||||||
|
<div class="poker-table">
|
||||||
|
<div class="table-felt">
|
||||||
|
<!-- Общие карты -->
|
||||||
|
<div class="community-cards" id="community-cards">
|
||||||
|
<!-- Карты будут добавлены динамически -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Позиции игроков -->
|
||||||
|
<div class="player-positions" id="player-positions">
|
||||||
|
<!-- Игроки будут добавлены динамически -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Карты игрока -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="hand-strength" id="hand-strength"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель действий -->
|
||||||
|
<div class="action-panel glass-card" id="action-panel">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-fold" onclick="playerAction('fold')">
|
||||||
|
Фолд
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-check" onclick="playerAction('check')" id="btn-check">
|
||||||
|
Чек
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-call" onclick="playerAction('call')" id="btn-call">
|
||||||
|
Колл <span id="call-amount"></span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-bet" onclick="showBetSlider()" id="btn-bet">
|
||||||
|
Бет
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-raise" onclick="showBetSlider()" id="btn-raise">
|
||||||
|
Рейз
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-allin" onclick="playerAction('allin')">
|
||||||
|
All-In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bet-slider-container" id="bet-slider-container" style="display: none;">
|
||||||
|
<input type="range" id="bet-slider" class="bet-slider" min="0" max="1000" value="0" oninput="updateBetValue()">
|
||||||
|
<div class="bet-value-display">
|
||||||
|
<input type="number" id="bet-value" class="bet-input" value="0" onchange="updateSliderFromInput()">
|
||||||
|
</div>
|
||||||
|
<div class="bet-presets">
|
||||||
|
<button class="btn btn-small" onclick="setBetPreset(0.5)">½ Пот</button>
|
||||||
|
<button class="btn btn-small" onclick="setBetPreset(0.75)">¾ Пот</button>
|
||||||
|
<button class="btn btn-small" onclick="setBetPreset(1)">Пот</button>
|
||||||
|
</div>
|
||||||
|
<div class="bet-confirm">
|
||||||
|
<button class="btn btn-secondary" onclick="hideBetSlider()">Отмена</button>
|
||||||
|
<button class="btn btn-primary" onclick="confirmBet()">Подтвердить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Чат в игре -->
|
||||||
|
<div class="game-chat glass-card" id="game-chat">
|
||||||
|
<div class="chat-header" onclick="toggleChat()">
|
||||||
|
💬 Чат
|
||||||
|
<span class="chat-toggle">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-body">
|
||||||
|
<div class="chat-messages" id="game-chat-messages"></div>
|
||||||
|
<div class="chat-input-row">
|
||||||
|
<input type="text" id="game-chat-input" class="input" placeholder="Сообщение..." onkeypress="handleGameChatKey(event)">
|
||||||
|
<button class="btn btn-primary" onclick="sendGameChat()">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Правила игры и статистика -->
|
||||||
|
<div class="game-info-panel glass-card" id="game-info-panel">
|
||||||
|
<div class="info-header" onclick="toggleGameInfo()">
|
||||||
|
📊 Правила & Статистика
|
||||||
|
<span class="info-toggle">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-body">
|
||||||
|
<div class="info-tabs">
|
||||||
|
<button class="info-tab active" onclick="switchInfoTab('rules')">Правила</button>
|
||||||
|
<button class="info-tab" onclick="switchInfoTab('stats')">Статистика</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладка правил -->
|
||||||
|
<div class="info-tab-content active" id="info-tab-rules">
|
||||||
|
<div class="rules-section">
|
||||||
|
<h4>Комбинации (от старшей к младшей):</h4>
|
||||||
|
<div class="hand-rankings">
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">10</span>
|
||||||
|
<span class="rank-name">Роял-флеш</span>
|
||||||
|
<span class="rank-example">A♠ K♠ Q♠ J♠ 10♠</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">9</span>
|
||||||
|
<span class="rank-name">Стрит-флеш</span>
|
||||||
|
<span class="rank-example">9♥ 8♥ 7♥ 6♥ 5♥</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">8</span>
|
||||||
|
<span class="rank-name">Каре</span>
|
||||||
|
<span class="rank-example">K♠ K♥ K♦ K♣ 3♠</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">7</span>
|
||||||
|
<span class="rank-name">Фулл-хаус</span>
|
||||||
|
<span class="rank-example">A♠ A♥ A♦ 8♣ 8♠</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">6</span>
|
||||||
|
<span class="rank-name">Флеш</span>
|
||||||
|
<span class="rank-example">Q♦ 9♦ 7♦ 4♦ 2♦</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">5</span>
|
||||||
|
<span class="rank-name">Стрит</span>
|
||||||
|
<span class="rank-example">J♠ 10♥ 9♦ 8♣ 7♠</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">4</span>
|
||||||
|
<span class="rank-name">Сет (тройка)</span>
|
||||||
|
<span class="rank-example">7♠ 7♥ 7♦ K♣ 2♠</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">3</span>
|
||||||
|
<span class="rank-name">Две пары</span>
|
||||||
|
<span class="rank-example">Q♠ Q♥ 5♦ 5♣ 9♠</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">2</span>
|
||||||
|
<span class="rank-name">Пара</span>
|
||||||
|
<span class="rank-example">10♠ 10♥ A♦ 6♣ 3♠</span>
|
||||||
|
</div>
|
||||||
|
<div class="hand-rank-item">
|
||||||
|
<span class="rank-number">1</span>
|
||||||
|
<span class="rank-name">Старшая карта</span>
|
||||||
|
<span class="rank-example">A♠ J♥ 8♦ 6♣ 2♠</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладка статистики -->
|
||||||
|
<div class="info-tab-content" id="info-tab-stats">
|
||||||
|
<div class="stats-section">
|
||||||
|
<h4>Ваша текущая рука:</h4>
|
||||||
|
<div class="current-hand-display" id="current-hand-display">
|
||||||
|
<div class="hand-cards-preview" id="hand-cards-preview">
|
||||||
|
Карты не розданы
|
||||||
|
</div>
|
||||||
|
<div class="hand-strength-display" id="hand-strength-display">
|
||||||
|
—
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Шансы на победу:</h4>
|
||||||
|
<div class="win-probability" id="win-probability">
|
||||||
|
<div class="probability-bar-container">
|
||||||
|
<div class="probability-bar" id="probability-bar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="probability-text" id="probability-text">0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Вероятности улучшения:</h4>
|
||||||
|
<div class="improvement-odds" id="improvement-odds">
|
||||||
|
<div class="odds-item">
|
||||||
|
<span class="odds-label">Следующая карта:</span>
|
||||||
|
<span class="odds-value" id="odds-next">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="odds-item">
|
||||||
|
<span class="odds-label">До вскрытия:</span>
|
||||||
|
<span class="odds-value" id="odds-river">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Статистика сессии:</h4>
|
||||||
|
<div class="session-stats">
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Раздач сыграно:</span>
|
||||||
|
<span class="stat-value" id="stat-hands-played">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Раздач выиграно:</span>
|
||||||
|
<span class="stat-value" id="stat-hands-won">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Всего выиграно:</span>
|
||||||
|
<span class="stat-value stat-highlight" id="stat-total-won">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Лучшая рука:</span>
|
||||||
|
<span class="stat-value" id="stat-best-hand">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопки управления -->
|
||||||
|
<div class="game-controls">
|
||||||
|
<button class="btn btn-icon-only" onclick="toggleSound()">🔊</button>
|
||||||
|
<button class="btn btn-icon-only" onclick="leaveGame()">🚪</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка новой раздачи (для одиночной игры) -->
|
||||||
|
<button class="btn btn-primary btn-large new-hand-btn" id="new-hand-btn" style="display: none;" onclick="startNewHand()">
|
||||||
|
Новая раздача
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Таблица лидеров -->
|
||||||
|
<div id="leaderboard-screen" class="screen">
|
||||||
|
<div class="glass-container menu-container">
|
||||||
|
<button class="btn-back" onclick="showScreen('main-menu')">← Назад</button>
|
||||||
|
<h2>🏆 Таблица лидеров</h2>
|
||||||
|
|
||||||
|
<div class="leaderboard-tabs">
|
||||||
|
<button class="tab active" onclick="switchLeaderboardTab('local')">Локальные</button>
|
||||||
|
<button class="tab" onclick="switchLeaderboardTab('global')">Глобальные</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="leaderboard-list" id="leaderboard-list">
|
||||||
|
<!-- Записи будут добавлены динамически -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-outline" onclick="clearLeaderboard()">Очистить записи</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Настройки -->
|
||||||
|
<div id="settings-screen" class="screen">
|
||||||
|
<div class="glass-container menu-container">
|
||||||
|
<button class="btn-back" onclick="showScreen('main-menu')">← Назад</button>
|
||||||
|
<h2>⚙️ Настройки</h2>
|
||||||
|
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>Звуковые эффекты</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="setting-sound" checked onchange="updateSettings()">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>Анимации</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="setting-animations" checked onchange="updateSettings()">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>Показывать силу руки</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="setting-hand-strength" checked onchange="updateSettings()">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>Автофолд при отключении</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="setting-autofold" checked onchange="updateSettings()">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 24px; margin-bottom: 16px;">🎴 Рубашка карт</h3>
|
||||||
|
|
||||||
|
<div class="card-back-settings">
|
||||||
|
<div class="card-back-preview">
|
||||||
|
<div class="card card-back" id="card-back-preview">
|
||||||
|
<div class="card-back-pattern"></div>
|
||||||
|
</div>
|
||||||
|
<span class="preview-label">Превью</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-back-options">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Выберите стиль рубашки</label>
|
||||||
|
<div class="btn-group card-back-styles">
|
||||||
|
<button class="btn btn-option active" data-style="default" onclick="selectCardBack(this)">🎰 Классика</button>
|
||||||
|
<button class="btn btn-option" data-style="red" onclick="selectCardBack(this)">🔴 Красная</button>
|
||||||
|
<button class="btn btn-option" data-style="blue" onclick="selectCardBack(this)">🔵 Синяя</button>
|
||||||
|
<button class="btn btn-option" data-style="custom" onclick="selectCardBack(this)">🖼️ Своя</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="custom-card-back-group" style="display: none;">
|
||||||
|
<label>Загрузить изображение</label>
|
||||||
|
<input type="file" id="card-back-file" class="input" accept="image/*" onchange="loadCustomCardBack(this)">
|
||||||
|
<small style="color: var(--text-muted); display: block; margin-top: 4px;">Рекомендуемый размер: 70×100 px</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="card-back-url-group" style="display: none;">
|
||||||
|
<label>Или вставьте URL изображения</label>
|
||||||
|
<input type="text" id="card-back-url" class="input" placeholder="https://example.com/card-back.png" onchange="setCardBackUrl(this.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- LLM статус (только чтение для пользователей) -->
|
||||||
|
<div class="llm-status-box" id="llm-status-box">
|
||||||
|
<span class="llm-status-icon" id="llm-status-icon">🤖</span>
|
||||||
|
<span id="llm-status-text">LLM чат: загрузка...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка настройки LLM для админа -->
|
||||||
|
<div class="admin-only" id="admin-llm-button-group" style="display: none; margin-top: 12px;">
|
||||||
|
<button class="btn btn-primary" onclick="showScreen('admin-screen')">
|
||||||
|
👑 Настроить LLM (Админ-панель)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 16px;"></div>
|
||||||
|
<button class="btn btn-outline" onclick="resetSettings()">Сбросить настройки</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Админ-панель -->
|
||||||
|
<div id="admin-screen" class="screen">
|
||||||
|
<div class="glass-container admin-container">
|
||||||
|
<button class="btn-back" onclick="showScreen('main-menu')">← Назад</button>
|
||||||
|
<h2>👑 Админ-панель</h2>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Вкладка настроек -->
|
||||||
|
<div id="admin-tab-settings" class="admin-tab-content">
|
||||||
|
<h3>🤖 Настройки LLM чата</h3>
|
||||||
|
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="setting-item">
|
||||||
|
<span>Включить LLM чат для всех</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="admin-llm-enabled" onchange="saveAdminSettings()">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>LLM провайдер</label>
|
||||||
|
<select id="admin-llm-provider" class="input" onchange="saveAdminSettings()">
|
||||||
|
<option value="ollama">Ollama</option>
|
||||||
|
<option value="lmstudio">LM Studio</option>
|
||||||
|
<option value="openai">OpenAI</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>URL API</label>
|
||||||
|
<input type="text" id="admin-llm-url" class="input" value="http://localhost:11434" onchange="saveAdminSettings()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Модель</label>
|
||||||
|
<input type="text" id="admin-llm-model" class="input" value="llama3.2" onchange="saveAdminSettings()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API Key (для OpenAI)</label>
|
||||||
|
<input type="password" id="admin-llm-apikey" class="input" placeholder="sk-..." onchange="saveAdminSettings()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="log-stats" id="log-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value" id="stat-total">0</span>
|
||||||
|
<span class="stat-label">Всего</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value" id="stat-today">0</span>
|
||||||
|
<span class="stat-label">Сегодня</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-filters">
|
||||||
|
<select id="log-filter-type" class="input" onchange="loadLogs()">
|
||||||
|
<option value="">Все действия</option>
|
||||||
|
<option value="login">Входы</option>
|
||||||
|
<option value="register">Регистрации</option>
|
||||||
|
<option value="room_created">Создание комнат</option>
|
||||||
|
<option value="chat_message">Сообщения чата</option>
|
||||||
|
<option value="game_action">Игровые действия</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-secondary" onclick="loadLogs()">🔄 Обновить</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-table-container">
|
||||||
|
<table class="log-table" id="log-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Время</th>
|
||||||
|
<th>Пользователь</th>
|
||||||
|
<th>Действие</th>
|
||||||
|
<th>Детали</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="log-table-body">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-pagination">
|
||||||
|
<button class="btn btn-small" onclick="loadLogs(currentLogPage - 1)" id="log-prev-btn">← Назад</button>
|
||||||
|
<span id="log-page-info">Страница 1</span>
|
||||||
|
<button class="btn btn-small" onclick="loadLogs(currentLogPage + 1)" id="log-next-btn">Вперёд →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Вкладка пользователей -->
|
||||||
|
<div id="admin-tab-users" class="admin-tab-content" style="display: none;">
|
||||||
|
<h3>👥 Управление пользователями</h3>
|
||||||
|
|
||||||
|
<!-- Статистика -->
|
||||||
|
<div class="user-stats" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 20px;">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-total-users">0</div>
|
||||||
|
<div class="stat-label">Всего</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-active-users">0</div>
|
||||||
|
<div class="stat-label">Активных (сегодня)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-banned-users">0</div>
|
||||||
|
<div class="stat-label">Заблокировано</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value" id="stat-admin-users">0</div>
|
||||||
|
<div class="stat-label">Админов</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Фильтры -->
|
||||||
|
<div class="user-filters" style="display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap;">
|
||||||
|
<select id="user-filter-role" class="input" onchange="loadUsers()" style="flex: 1; min-width: 150px;">
|
||||||
|
<option value="">Все роли</option>
|
||||||
|
<option value="admin">Админы</option>
|
||||||
|
<option value="user">Пользователи</option>
|
||||||
|
</select>
|
||||||
|
<select id="user-filter-status" class="input" onchange="loadUsers()" style="flex: 1; min-width: 150px;">
|
||||||
|
<option value="">Все статусы</option>
|
||||||
|
<option value="active">Активные</option>
|
||||||
|
<option value="banned">Заблокированные</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="user-search" class="input" placeholder="🔍 Поиск по логину..."
|
||||||
|
oninput="loadUsers()" style="flex: 2; min-width: 200px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Таблица пользователей -->
|
||||||
|
<div class="users-table-container" style="overflow-x: auto;">
|
||||||
|
<table class="users-table" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: var(--bg-secondary); text-align: left;">
|
||||||
|
<th style="padding: 12px; border-bottom: 2px solid var(--border-color);">Логин</th>
|
||||||
|
<th style="padding: 12px; border-bottom: 2px solid var(--border-color);">Роль</th>
|
||||||
|
<th style="padding: 12px; border-bottom: 2px solid var(--border-color);">IP адрес</th>
|
||||||
|
<th style="padding: 12px; border-bottom: 2px solid var(--border-color);">Последний вход</th>
|
||||||
|
<th style="padding: 12px; border-bottom: 2px solid var(--border-color);">Статус</th>
|
||||||
|
<th style="padding: 12px; border-bottom: 2px solid var(--border-color); text-align: center;">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="users-table-body">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" style="padding: 20px; text-align: center; color: var(--text-muted);">
|
||||||
|
Загрузка пользователей...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно редактирования пользователя -->
|
||||||
|
<div id="edit-user-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content glass-container" style="max-width: 500px;">
|
||||||
|
<h3>✏️ Редактирование пользователя</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Логин</label>
|
||||||
|
<input type="text" id="edit-user-username" class="input" readonly style="background: var(--bg-secondary);">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Роль</label>
|
||||||
|
<select id="edit-user-role" class="input">
|
||||||
|
<option value="user">👤 Пользователь</option>
|
||||||
|
<option value="admin">👑 Администратор</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Статус</label>
|
||||||
|
<select id="edit-user-status" class="input">
|
||||||
|
<option value="active">✅ Активен</option>
|
||||||
|
<option value="banned">🚫 Заблокирован</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Новый пароль (оставьте пустым, чтобы не менять)</label>
|
||||||
|
<input type="password" id="edit-user-password" class="input" placeholder="Новый пароль...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 12px; margin-top: 20px;">
|
||||||
|
<button class="btn btn-success" onclick="saveUserEdit()">💾 Сохранить</button>
|
||||||
|
<button class="btn btn-secondary" onclick="closeEditUserModal()">❌ Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Результаты раздачи -->
|
||||||
|
<div id="hand-result-modal" class="modal">
|
||||||
|
<div class="modal-content glass-container">
|
||||||
|
<h3 id="result-title">Результат</h3>
|
||||||
|
<div class="result-details" id="result-details"></div>
|
||||||
|
<button class="btn btn-primary" onclick="closeResultModal()">Продолжить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Уведомления -->
|
||||||
|
<div class="notifications" id="notifications"></div>
|
||||||
|
|
||||||
|
<script src="game.js"></script>
|
||||||
|
<script src="ai.js"></script>
|
||||||
|
<script src="auth.js"></script>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue