init
This commit is contained in:
commit
d9b9716432
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
venv/
|
||||||
|
*.db
|
||||||
|
__pycache__
|
||||||
|
.DS_Store
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Установка рабочей директории
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копирование файлов проекта
|
||||||
|
COPY bot/ ./bot/
|
||||||
|
COPY .env .
|
||||||
|
|
||||||
|
# Создание директории для базы данных
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Запуск бота
|
||||||
|
CMD ["python", "-m", "bot.main"]
|
||||||
7
bot/config.py
Normal file
7
bot/config.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
BOT_TOKEN = os.getenv('BOT_TOKEN')
|
||||||
|
WEBAPP_URL = os.getenv('WEBAPP_URL')
|
||||||
99
bot/database.py
Normal file
99
bot/database.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self):
|
||||||
|
self.db_path = '/app/data/nard_dice.db'
|
||||||
|
self._init_db()
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""Инициализация базы данных"""
|
||||||
|
if not os.path.exists(self.db_path):
|
||||||
|
os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
with open('bot/schema.sql', 'r') as f:
|
||||||
|
conn.executescript(f.read())
|
||||||
|
|
||||||
|
def _get_connection(self):
|
||||||
|
"""Получение соединения с базой данных"""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def create_game(self, user_id: int, username: str) -> int:
|
||||||
|
"""Создание новой игры"""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO games (user_id, username)
|
||||||
|
VALUES (?, ?)
|
||||||
|
''', (user_id, username))
|
||||||
|
conn.commit()
|
||||||
|
return cursor.lastrowid
|
||||||
|
|
||||||
|
def add_throw(self, game_id: int, dice1: int, dice2: int):
|
||||||
|
"""Добавление броска"""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO throws (game_id, dice1, dice2)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
''', (game_id, dice1, dice2))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def get_active_game(self, user_id: int):
|
||||||
|
"""Получение активной игры пользователя"""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
game = cursor.execute('''
|
||||||
|
SELECT id, user_id, username, start_time
|
||||||
|
FROM games
|
||||||
|
WHERE user_id = ? AND status = 'active'
|
||||||
|
ORDER BY start_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
''', (user_id,)).fetchone()
|
||||||
|
|
||||||
|
if game:
|
||||||
|
return dict(game)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def end_game(self, game_id: int):
|
||||||
|
"""Завершение игры"""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
UPDATE games
|
||||||
|
SET status = 'completed', end_time = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
''', (game_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def get_statistics(self, user_id: int):
|
||||||
|
"""Получение статистики пользователя"""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
stats = cursor.execute('''
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_throws,
|
||||||
|
AVG(dice1 + dice2) as avg_sum,
|
||||||
|
COUNT(DISTINCT game_id) as total_games
|
||||||
|
FROM throws t
|
||||||
|
JOIN games g ON t.game_id = g.id
|
||||||
|
WHERE g.user_id = ?
|
||||||
|
''', (user_id,)).fetchone()
|
||||||
|
|
||||||
|
return dict(stats)
|
||||||
|
|
||||||
|
def get_game_throws(self, game_id: int):
|
||||||
|
"""Получение всех бросков игры"""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
throws = cursor.execute('''
|
||||||
|
SELECT dice1, dice2, throw_time
|
||||||
|
FROM throws
|
||||||
|
WHERE game_id = ?
|
||||||
|
ORDER BY throw_time DESC
|
||||||
|
''', (game_id,)).fetchall()
|
||||||
|
|
||||||
|
return [dict(throw) for throw in throws]
|
||||||
133
bot/handlers.py
Normal file
133
bot/handlers.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
from telegram import Update, WebAppInfo, KeyboardButton, ReplyKeyboardMarkup
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
from database import Database
|
||||||
|
import json
|
||||||
|
|
||||||
|
db = Database()
|
||||||
|
|
||||||
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
KeyboardButton(
|
||||||
|
"Открыть счётчик 🎲",
|
||||||
|
web_app=WebAppInfo(url=context.bot_data['webapp_url'])
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[KeyboardButton("Новая игра 🎮"), KeyboardButton("Статистика 📊")],
|
||||||
|
[KeyboardButton("Завершить игру 🏁")]
|
||||||
|
]
|
||||||
|
|
||||||
|
reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
"🎲 Добро пожаловать в счётчик кубиков для нард!\n\n"
|
||||||
|
"Выберите действие:",
|
||||||
|
reply_markup=reply_markup
|
||||||
|
)
|
||||||
|
|
||||||
|
async def new_game_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
user = update.effective_user
|
||||||
|
|
||||||
|
# Проверяем, нет ли уже активной игры
|
||||||
|
active_game = db.get_active_game(user.id)
|
||||||
|
if active_game:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"❗️ У вас уже есть активная игра. "
|
||||||
|
"Сначала завершите её командой /end_game"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
game_id = db.create_game(user.id, user.username)
|
||||||
|
context.user_data['current_game'] = game_id
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
"🎮 Новая игра начата!\n"
|
||||||
|
"Используйте кнопку «Открыть счётчик» для записи бросков."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_webapp_data(update, context):
|
||||||
|
try:
|
||||||
|
data = json.loads(update.effective_message.web_app_data.data)
|
||||||
|
|
||||||
|
if data['type'] == 'game_session':
|
||||||
|
# Получаем активную игру пользователя
|
||||||
|
game = db.get_active_game(update.effective_user.id)
|
||||||
|
|
||||||
|
if game:
|
||||||
|
# Записываем все броски
|
||||||
|
for throw in data['throws']:
|
||||||
|
db.add_throw(
|
||||||
|
game['id'],
|
||||||
|
throw['dice'][0],
|
||||||
|
throw['dice'][1]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Завершаем игру
|
||||||
|
db.end_game(game['id'])
|
||||||
|
|
||||||
|
# Отправляем сообщение об успешном завершении
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Игра завершена! Записано {len(data['throws'])} бросков.\n"
|
||||||
|
f"Используйте /statistics для просмотра статистики."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Ошибка: активная игра не найдена.\n"
|
||||||
|
"Используйте /new_game для начала новой игры."
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error handling webapp data: {e}")
|
||||||
|
await update.message.reply_text("Произошла ошибка при обработке данных.")
|
||||||
|
|
||||||
|
async def statistics_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
stats = db.get_statistics(user_id)
|
||||||
|
|
||||||
|
# Получаем активную игру
|
||||||
|
active_game = db.get_active_game(user_id)
|
||||||
|
|
||||||
|
response = "📊 Ваша статистика:\n\n"
|
||||||
|
|
||||||
|
if stats['total_throws']:
|
||||||
|
response += (
|
||||||
|
f"Всего игр: {stats['total_games']}\n"
|
||||||
|
f"Всего бросков: {stats['total_throws']}\n"
|
||||||
|
f"Средняя сумма: {stats['avg_sum']:.2f}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if active_game:
|
||||||
|
game_throws = db.get_game_throws(active_game['id'])
|
||||||
|
response += f"\nТекущая игра:\n"
|
||||||
|
response += f"Количество бросков: {len(game_throws)}\n"
|
||||||
|
if game_throws:
|
||||||
|
last_throw = game_throws[0]
|
||||||
|
response += f"Последний бросок: {last_throw['dice1']}-{last_throw['dice2']}"
|
||||||
|
else:
|
||||||
|
response += "У вас пока нет записанных бросков."
|
||||||
|
|
||||||
|
await update.message.reply_text(response)
|
||||||
|
|
||||||
|
async def end_game_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
game_id = context.user_data.get('current_game')
|
||||||
|
if not game_id:
|
||||||
|
await update.message.reply_text("❌ У вас нет активной игры!")
|
||||||
|
return
|
||||||
|
|
||||||
|
game_throws = db.get_game_throws(game_id)
|
||||||
|
db.end_game(game_id)
|
||||||
|
del context.user_data['current_game']
|
||||||
|
|
||||||
|
response = "🏁 Игра завершена!\n\n"
|
||||||
|
if game_throws:
|
||||||
|
total_throws = len(game_throws)
|
||||||
|
avg_sum = sum(t['dice1'] + t['dice2'] for t in game_throws) / total_throws
|
||||||
|
response += (
|
||||||
|
f"Статистика игры:\n"
|
||||||
|
f"Всего бросков: {total_throws}\n"
|
||||||
|
f"Средняя сумма: {avg_sum:.2f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response += "В игре не было записано ни одного броска."
|
||||||
|
|
||||||
|
await update.message.reply_text(response)
|
||||||
143
bot/main.py
Normal file
143
bot/main.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
from aiogram import Bot, Dispatcher, types, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.types import WebAppInfo, KeyboardButton, ReplyKeyboardMarkup
|
||||||
|
from database import Database
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from config import BOT_TOKEN, WEBAPP_URL
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Инициализация бота и диспетчера
|
||||||
|
bot = Bot(token=BOT_TOKEN)
|
||||||
|
dp = Dispatcher()
|
||||||
|
db = Database()
|
||||||
|
|
||||||
|
# Упрощенная клавиатура
|
||||||
|
def get_main_keyboard():
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
KeyboardButton(
|
||||||
|
text="Записать броски 🎲",
|
||||||
|
web_app=WebAppInfo(url=WEBAPP_URL)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[KeyboardButton(text="Статистика 📊")]
|
||||||
|
]
|
||||||
|
return ReplyKeyboardMarkup(keyboard=keyboard, resize_keyboard=True)
|
||||||
|
|
||||||
|
@dp.message(Command("start"))
|
||||||
|
async def start_command(message: types.Message):
|
||||||
|
logger.info(f"User {message.from_user.id} ({message.from_user.username}) started the bot")
|
||||||
|
await message.answer(
|
||||||
|
"🎲 Добро пожаловать в счётчик кубиков для нард!\n\n"
|
||||||
|
"Нажмите «Записать броски» чтобы начать новую игру.",
|
||||||
|
reply_markup=get_main_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем функцию подсчета статистики в handle_webapp_data
|
||||||
|
def calculate_throw_sum(throw):
|
||||||
|
dice1, dice2 = throw['dice']
|
||||||
|
if dice1 == dice2:
|
||||||
|
return (dice1 + dice2) * 2
|
||||||
|
return dice1 + dice2
|
||||||
|
|
||||||
|
@dp.message(F.web_app_data)
|
||||||
|
async def handle_webapp_data(message: types.Message):
|
||||||
|
try:
|
||||||
|
user = message.from_user
|
||||||
|
logger.info(f"Received webapp data from user {user.id} ({user.username})")
|
||||||
|
|
||||||
|
data = json.loads(message.web_app_data.data)
|
||||||
|
|
||||||
|
if data['type'] == 'game_session':
|
||||||
|
# Создаем новую игру
|
||||||
|
game_id = db.create_game(user.id, user.username)
|
||||||
|
logger.info(f"Created new game {game_id} for user {user.id}")
|
||||||
|
|
||||||
|
# Записываем все броски
|
||||||
|
throws = data['throws']
|
||||||
|
for throw in throws:
|
||||||
|
db.add_throw(
|
||||||
|
game_id,
|
||||||
|
throw['dice'][0],
|
||||||
|
throw['dice'][1]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Завершаем игру
|
||||||
|
db.end_game(game_id)
|
||||||
|
logger.info(f"Game {game_id} completed with {len(throws)} throws")
|
||||||
|
|
||||||
|
# Обновляем подсчет статистики
|
||||||
|
total_throws = len(throws)
|
||||||
|
total_sum = sum(calculate_throw_sum(t) for t in throws)
|
||||||
|
avg_sum = total_sum / total_throws if total_throws > 0 else 0
|
||||||
|
doubles = sum(1 for t in throws if t['dice'][0] == t['dice'][1])
|
||||||
|
|
||||||
|
# Находим максимальный и минимальный броски с учетом дублей
|
||||||
|
throws_with_sums = [(t, calculate_throw_sum(t)) for t in throws]
|
||||||
|
max_throw = max(throws_with_sums, key=lambda x: x[1])
|
||||||
|
min_throw = min(throws_with_sums, key=lambda x: x[1])
|
||||||
|
|
||||||
|
# Формируем сообщение
|
||||||
|
response = "🎯 Игра завершена!\n\n"
|
||||||
|
response += f"📊 Статистика игры:\n"
|
||||||
|
response += f"• Всего бросков: {total_throws}\n"
|
||||||
|
response += f"• Общая сумма: {total_sum}\n"
|
||||||
|
response += f"• Средняя сумма: {avg_sum:.1f}\n"
|
||||||
|
response += f"• Дублей выпало: {doubles}\n"
|
||||||
|
response += f"• Максимальный бросок: {max_throw[0]['dice'][0]}-{max_throw[0]['dice'][1]} (сумма: {max_throw[1]})\n"
|
||||||
|
response += f"• Минимальный бросок: {min_throw[0]['dice'][0]}-{min_throw[0]['dice'][1]} (сумма: {min_throw[1]})\n\n"
|
||||||
|
|
||||||
|
# Добавляем последние броски с учетом дублей
|
||||||
|
response += "🎲 Последние броски:\n"
|
||||||
|
for i, throw in enumerate(throws[:5]):
|
||||||
|
sum_value = calculate_throw_sum(throw)
|
||||||
|
is_double = throw['dice'][0] == throw['dice'][1]
|
||||||
|
response += f"{i+1}. {throw['dice'][0]}-{throw['dice'][1]} {'🎯' if is_double else ''} (сумма: {sum_value})\n"
|
||||||
|
|
||||||
|
if total_throws > 5:
|
||||||
|
response += f"... и ещё {total_throws - 5} бросков"
|
||||||
|
|
||||||
|
await message.answer(response)
|
||||||
|
logger.info(f"Sent game statistics to user {user.id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling webapp data: {e}", exc_info=True)
|
||||||
|
await message.answer("❌ Произошла ошибка при обработке данных.")
|
||||||
|
|
||||||
|
@dp.message(Command("statistics"))
|
||||||
|
@dp.message(F.text == "Статистика 📊")
|
||||||
|
async def statistics_command(message: types.Message):
|
||||||
|
user_id = message.from_user.id
|
||||||
|
logger.info(f"User {user_id} requested statistics")
|
||||||
|
|
||||||
|
stats = db.get_statistics(user_id)
|
||||||
|
|
||||||
|
response = "📊 Общая статистика:\n\n"
|
||||||
|
|
||||||
|
if stats['total_throws']:
|
||||||
|
response += (
|
||||||
|
f"• Всего игр: {stats['total_games']}\n"
|
||||||
|
f"• Всего бросков: {stats['total_throws']}\n"
|
||||||
|
f"• Средняя сумма: {stats['avg_sum']:.1f}\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response += "У вас пока нет записанных бросков."
|
||||||
|
|
||||||
|
await message.answer(response)
|
||||||
|
logger.info(f"Sent statistics to user {user_id}")
|
||||||
|
|
||||||
|
# Запуск бота
|
||||||
|
async def main():
|
||||||
|
logger.info("Starting bot")
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
17
bot/schema.sql
Normal file
17
bot/schema.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS games (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
username TEXT,
|
||||||
|
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
end_time TIMESTAMP,
|
||||||
|
status TEXT DEFAULT 'active'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS throws (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
game_id INTEGER,
|
||||||
|
dice1 INTEGER NOT NULL,
|
||||||
|
dice2 INTEGER NOT NULL,
|
||||||
|
throw_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (game_id) REFERENCES games(id)
|
||||||
|
);
|
||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nard_bot:
|
||||||
|
build: .
|
||||||
|
container_name: nard_bot
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data # Том для базы данных
|
||||||
|
- .env:/app/.env # Монтируем .env файл
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1 # Для корректного вывода логов
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
python-telegram-bot==20.7
|
||||||
|
pymongo==4.6.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn==0.27.0
|
||||||
|
aiogram==3.0.0
|
||||||
Loading…
Reference in New Issue
Block a user