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