Compare commits

...

10 Commits

Author SHA1 Message Date
ur002 6afcd46563 feat: Add comprehensive Docker deployment checklist and configuration files 2026-02-01 21:10:25 +03:00
ur002 81a7108758 feat: Implement user management system in admin panel
- Added user statistics display including total users, active users today, banned users, and admin count.
- Introduced filtering options for users by role and status, along with a dynamic search feature.
- Created a user table with relevant information and actions for editing, banning, and deleting users.
- Implemented REST API endpoints for user management, including fetching, updating, banning, and deleting users.
- Enhanced server-side logging for user management actions.
- Updated styles for the admin panel to improve user experience and responsiveness.
- Added Docker support with a complete setup for deployment, including Apache configuration for reverse proxy and WebSocket support.
- Included environment configuration and backup procedures in the documentation.
2026-02-01 21:10:25 +03:00
ur002 4c1aa87905 Add game info panel styles and bot personality configuration documentation
- Implemented CSS styles for the game info panel, including rules and statistics sections.
- Added responsive design adjustments for mobile and compact views.
- Created a comprehensive documentation file for bot personality configurations, detailing available personalities, customization instructions, and best practices for prompt creation.
2026-02-01 21:09:01 +03:00
ur002 ca4e1fd087 feat(sharing): implement room link sharing functionality and update sharing guide 2026-02-01 21:09:01 +03:00
ur002 79267cb3d2 feat(chat): add function to clear game chat before starting a new game and on exit 2026-02-01 21:09:01 +03:00
ur002 24e457885d feat: add player balance display and congratulatory system for strong hands
- Implemented a player info row displaying the player's name and balance in the game interface.
- Added animations for balance changes (increase/decrease) to enhance user experience.
- Introduced a congratulatory system where bots congratulate players for strong hands based on hand strength probabilities.
- Updated styles for player balance display and added relevant CSS animations.
- Enhanced game logic to trigger congratulatory messages from bots after a hand concludes.
- Documented the new congratulatory system and player balance display features.
2026-02-01 21:09:01 +03:00
ur002 eec780d8af feat(auth): implement client-side authentication module for Texas Hold'em game
- Added functions for user login, registration, and guest play
- Implemented token management and user session handling
- Created UI updates for user display and admin panel settings
- Integrated server communication for user settings and logs
- Added error handling and notifications for user actions
2026-02-01 21:09:01 +03:00
ur002 c3ace2834d Add bot personality handling and emotional reactions after hands 2026-02-01 21:09:01 +03:00
ur002 ab7a8418ed Add card back customization options and styles 2026-02-01 21:09:00 +03:00
ur002 ff7014e6c1 Refactor code structure for improved readability and maintainability 2026-02-01 21:09:00 +03:00
32 changed files with 16696 additions and 1 deletions

37
.dockerignore Normal file
View File

@ -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/

15
.env.example Normal file
View File

@ -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

56
.gitignore vendored Normal file
View File

@ -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/

411
ADMIN_USER_MANAGEMENT.md Normal file
View File

@ -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

326
ARCHITECTURE.md Normal file
View File

@ -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

305
BOT_PERSONALITIES_CONFIG.md Normal file
View File

@ -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

288
CHECKLIST.md Normal file
View File

@ -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
**Статус:** ✅ ГОТОВО

197
CONGRATULATIONS_SYSTEM.md Normal file
View File

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

439
DEPLOYMENT.md Normal file
View File

@ -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

133
DOCKER_README.md Normal file
View File

@ -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**: ✅ Готово к использованию

281
DOCKER_SETUP_SUMMARY.md Normal file
View File

@ -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

27
Dockerfile Normal file
View File

@ -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"]

128
LLM_CONTEXT_EXAMPLE.md Normal file
View File

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

155
PLAYER_BALANCE_DISPLAY.md Normal file
View File

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

342
README.md
View File

@ -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
🃏 **Хорошей игры!**

76
SHARING_GUIDE.md Normal file
View File

@ -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
- Комната доступна только активным пользователям

206
TESTING.md Normal file
View File

@ -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
apache-config.conf Normal file
View File

735
database.js Normal file
View File

@ -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
deploy.ps1 Normal file
View File

81
deploy.sh Normal file
View File

@ -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

25
docker-compose.yml Normal file
View File

@ -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

1000
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -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"
}

BIN
poker.db Normal file

Binary file not shown.

1529
public/ai.js Normal file

File diff suppressed because it is too large Load Diff

570
public/auth.js Normal file
View File

@ -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;

912
public/game.js Normal file
View File

@ -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 };
}

899
public/index.html Normal file
View File

@ -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>

3336
public/main.js Normal file

File diff suppressed because it is too large Load Diff

2574
public/styles.css Normal file

File diff suppressed because it is too large Load Diff

1588
server.js Normal file

File diff suppressed because it is too large Load Diff