diff --git a/handlers/__init__.py b/.env.expert similarity index 100% rename from handlers/__init__.py rename to .env.expert diff --git a/.gitignore b/.gitignore index ae2cef7..55c1e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,89 +1,7 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -venv/ -ENV/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# Environment variables .env -.env.local -.env.*.local - -# Database -data/ -*.db -*.sqlite -*.sqlite3 - -# IDE & editors -.idea/ -.vscode/ -*.swp -*.swo -*~ -.DS_Store - -# Logs +__pycache__/ +*.pyc *.log -logs/ - -# Temporary files -tmp/ -temp/ -*.tmp - -# Build artifacts -*.egg-info/ -.eggs/ - -# OS-specific -Thumbs.db -.DS_Store - -# Sensitive config -config.json -config.yml -credentials.json -keys.json -bot_secrets.json - -# Mini App (если будете хостить) -client-side/ -frontend/ -build/ -dist/ - -# Docker (если добавите позже) -Dockerfile -docker-compose.yml - -# Testing -.pytest_cache/ -.coverage -htmlcov/ -.tox/ -.nox/ - -# Documentation (опционально) -docs/_build/ -docs/_static/ -docs/_templates/ \ No newline at end of file +.venv/ +.idea/ +.vscode/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dceceb6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "-m", "app.main"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..fc48316 --- /dev/null +++ b/alembic.ini @@ -0,0 +1 @@ +sqlalchemy.url = postgresql+asyncpg://provider:provider@db:5432/provider \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..5f2d11f --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,36 @@ +from logging.config import fileConfig +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config +from alembic import context +import asyncio + +from app.models.base import Base +from app.models import player, district +from app.config import settings + +config = context.config +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +target_metadata = Base.metadata + +def run_migrations_online(): + connectable = async_engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async def do_migrations(): + async with connectable.connect() as connection: + await connection.run_sync( + lambda conn: context.configure( + connection=conn, + target_metadata=target_metadata, + ) + ) + await connection.run_sync(context.run_migrations) + + asyncio.run(do_migrations()) + +run_migrations_online() \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..7919661 --- /dev/null +++ b/app/config.py @@ -0,0 +1,10 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + BOT_TOKEN: str + DATABASE_URL: str + + class Config: + env_file = ".env" + +settings = Settings() \ No newline at end of file diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..04cfd7f --- /dev/null +++ b/app/database.py @@ -0,0 +1,5 @@ +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker +from app.config import settings + +engine = create_async_engine(settings.DATABASE_URL, echo=False) +async_session = async_sessionmaker(engine, expire_on_commit=False) \ No newline at end of file diff --git a/app/handlers/start.py b/app/handlers/start.py new file mode 100644 index 0000000..8be76fc --- /dev/null +++ b/app/handlers/start.py @@ -0,0 +1,15 @@ +from aiogram import Router +from aiogram.types import Message +from aiogram.filters import Command +from app.services.player_service import get_or_create_player + +router = Router() + +@router.message(Command("start")) +async def start_handler(message: Message): + player = await get_or_create_player(message.from_user.id) + + await message.answer( + f"🚀 Добро пожаловать в Digital Provider!\n" + f"💰 Баланс: {player.money} ₡" + ) \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..2278416 --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,4 @@ +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass \ No newline at end of file diff --git a/app/models/district.py b/app/models/district.py new file mode 100644 index 0000000..1cb7d5c --- /dev/null +++ b/app/models/district.py @@ -0,0 +1,11 @@ +from sqlalchemy import String, Integer, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column +from app.models.base import Base + +class District(Base): + __tablename__ = "districts" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(50)) + max_clients: Mapped[int] = mapped_column(Integer) + owner_id: Mapped[int | None] = mapped_column(ForeignKey("players.id"), nullable=True) \ No newline at end of file diff --git a/app/models/player.py b/app/models/player.py new file mode 100644 index 0000000..267cd9f --- /dev/null +++ b/app/models/player.py @@ -0,0 +1,11 @@ +from sqlalchemy import BigInteger, Integer, Float +from sqlalchemy.orm import Mapped, mapped_column +from app.models.base import Base + +class Player(Base): + __tablename__ = "players" + + id: Mapped[int] = mapped_column(primary_key=True) + telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True) + money: Mapped[int] = mapped_column(Integer, default=10000) + reputation: Mapped[float] = mapped_column(Float, default=1.0) \ No newline at end of file diff --git a/app/services/player_service.py b/app/services/player_service.py new file mode 100644 index 0000000..8ed7a93 --- /dev/null +++ b/app/services/player_service.py @@ -0,0 +1,17 @@ +from sqlalchemy import select +from app.database import async_session +from app.models.player import Player + +async def get_or_create_player(telegram_id: int): + async with async_session() as session: + result = await session.execute( + select(Player).where(Player.telegram_id == telegram_id) + ) + player = result.scalar_one_or_none() + + if not player: + player = Player(telegram_id=telegram_id) + session.add(player) + await session.commit() + + return player \ No newline at end of file diff --git a/config.py b/config.py deleted file mode 100644 index d29155f..0000000 --- a/config.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -from dotenv import load_dotenv - -load_dotenv() - -BOT_TOKEN = os.getenv("BOT_TOKEN") -ADMIN_ID = os.getenv("ADMIN_ID") - -# Стартовые значения -INITIAL_BALANCE = 10_000 -INITIAL_SERVERS = 1 -INITIAL_COVERAGE_CUSTOMERS = 500 -INITIAL_MAX_CUSTOMERS = 500 -INITIAL_SPEED = 10 -INITIAL_CHANNEL_LEVEL = 1 -INITIAL_POWER_CAPACITY = 5.0 -INITIAL_NETWORK_CHANNEL_CAPACITY = 10 # Мбит/с (база) - -# Параметры инфраструктуры -SERVER_POWER = 2.0 # кВт/сервер -SERVER_COST = 3_000 -SERVER_CAPACITY_ADD = 500 -CABLE_COST = 800 -CABLE_COVERAGE_ADD = 500 -BACKUP_CHANNEL_COST = 2_500 -BACKUP_CHANNEL_POWER_ADD = 100 - -# Тарифы (в ₡/день/клиент) -TARIFFS = { - "home": 10, - "office": 30, - "gamer": 50, - "botfarm": 15 -} - -# Уровни канала -CHANNEL_LEVELS = { - 1: 10, - 2: 50, - 3: 100, - 4: 250, - 5: 500, - 6: 1000 -} \ No newline at end of file diff --git a/database.py b/database.py deleted file mode 100644 index 00b95e6..0000000 --- a/database.py +++ /dev/null @@ -1,93 +0,0 @@ -# database.py — работа с SQLite -import sqlite3 -from typing import Optional, Dict, Any - -DB_PATH = "data/db.sqlite3" - - -def init_db(): - conn = sqlite3.connect(DB_PATH) - cur = conn.cursor() - cur.execute(""" - CREATE TABLE IF NOT EXISTS players ( - user_id INTEGER PRIMARY KEY, - balance REAL DEFAULT 10000, - servers INTEGER DEFAULT 1, - cables INTEGER DEFAULT 0, - coverage_customers INTEGER DEFAULT 500, - max_customers INTEGER DEFAULT 500, - speed INTEGER DEFAULT 10, - rating INTEGER DEFAULT 0, - rating_threshold INTEGER DEFAULT 100, - next_build_end_ts INTEGER DEFAULT 0, - last_login_ts INTEGER DEFAULT 0, - total_income REAL DEFAULT 0, - infrastructure_level INTEGER DEFAULT 1, - power_capacity REAL DEFAULT 5.0, - power_usage REAL DEFAULT 0.0, - outage_end_ts INTEGER DEFAULT 0, - backup_channel_active INTEGER DEFAULT 0, - network_channel_level INTEGER DEFAULT 1, - channel_capacity INTEGER DEFAULT 10 - ) - """) - conn.commit() - conn.close() - - -def get_player(user_id: int) -> Optional[Dict[str, Any]]: - conn = sqlite3.connect(DB_PATH) - cur = conn.cursor() - cur.execute("SELECT * FROM players WHERE user_id = ?", (user_id,)) - row = cur.fetchone() - conn.close() - - if not row: - return None - - keys = [ - "user_id", "balance", "servers", "cables", "coverage_customers", - "max_customers", "speed", "rating", "rating_threshold", - "next_build_end_ts", "last_login_ts", "total_income", "infrastructure_level", - "power_capacity", "power_usage", "outage_end_ts", "backup_channel_active", - "network_channel_level", "channel_capacity" - ] - return dict(zip(keys, row)) - - -def update_player(user_id: int, updates: Dict[str, Any]): - conn = sqlite3.connect(DB_PATH) - cur = conn.cursor() - fields = ", ".join(f"{k} = ?" for k in updates.keys()) - values = list(updates.values()) + [user_id] - cur.execute(f"UPDATE players SET {fields} WHERE user_id = ?", values) - conn.commit() - conn.close() - - -def create_player_if_not_exists(user_id: int): - player = get_player(user_id) - if player: - return - - conn = sqlite3.connect(DB_PATH) - cur = conn.cursor() - cur.execute(""" - INSERT INTO players - (user_id, balance, servers, cables, coverage_customers, max_customers, speed, - power_capacity, network_channel_level, channel_capacity) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - user_id, - 10_000, # balance - 1, # servers - 0, # cables - 500, # coverage_customers - 500, # max_customers - 10, # speed - 5.0, # power_capacity - 1, # network_channel_level - 10 # channel_capacity - )) - conn.commit() - conn.close() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7cad741 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.9" + +services: + db: + image: postgres:15 + container_name: provider_db + restart: always + environment: + POSTGRES_USER: provider + POSTGRES_PASSWORD: provider + POSTGRES_DB: provider + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + bot: + build: . + container_name: provider_bot + command: sh -c "alembic upgrade head && python -m app.main" + depends_on: + - db + env_file: + - .env + +volumes: + postgres_data: \ No newline at end of file diff --git a/economy.py b/economy.py deleted file mode 100644 index a47a22b..0000000 --- a/economy.py +++ /dev/null @@ -1,101 +0,0 @@ -import time -from config import * - -def calculate_effective_bandwidth(player: dict) -> int: - base = player["channel_capacity"] - server_boost = min(0.5, player["servers"] * 0.1) - cable_boost = player["cables"] * 0.2 - return int(base * (1 + server_boost + min(1.0, cable_boost))) - -def calculate_rating(player: dict) -> float: - monthly_income = player["coverage_customers"] * 15 * 30 - income_score = 10 * (monthly_income / 100_000) ** 0.5 - churn_ratio = (player["max_customers"] - player["coverage_customers"]) / max(player["max_customers"], 1) - stability_score = max(0, 20 - (churn_ratio * 100)) - coverage_score = 20 * min(1, player["max_customers"] / 100_000) - return round(income_score * 0.4 + stability_score * 0.3 + coverage_score * 0.3, 2) - -def calculate_client_retention(player: dict) -> tuple[int, str]: - effective_bw = calculate_effective_bandwidth(player) - required_bw_per_client = 2.0 - max_supported = effective_bw / required_bw_per_client - - if player["coverage_customers"] > max_supported: - churn_ratio = min(0.9, (player["coverage_customers"] - max_supported) / max_supported) - lost = int(player["coverage_customers"] * churn_ratio) - return lost, f"⚠️ Канала не хватает! Отток: {lost} клиентов/сутки" - - if player["network_channel_level"] >= 3 and effective_bw >= 100: - return 0, "✅ Высокий канал: отток снижен на 15%" - return 0, "🌐 Связь стабильна." - -def calculate_daily_income(player: dict) -> float: - supported_tiers = ["home"] - if player["network_channel_level"] >= 2: - supported_tiers.append("office") - if player["network_channel_level"] >= 3: - supported_tiers.append("gamer") - if player["network_channel_level"] >= 4: - supported_tiers.append("botfarm") - - base_rate = sum(TARIFFS[t] for t in supported_tiers) / len(supported_tiers) - effective_bw = calculate_effective_bandwidth(player) - ratio = effective_bw / max(player["coverage_customers"], 1) - multiplier = 1.0 + 0.2 * min(1, ratio / 5) - - return round(player["coverage_customers"] * base_rate * multiplier, 2) - -def calculate_channel_cost(level: int) -> int: - base = 2000 - return int(base * (1.8 ** (level - 1))) - -def calculate_daily_offline_earnings(player: dict) -> float: - now = int(time.time()) - if player["last_login_ts"] >= now: - return 0.0 - offline_hours = max((now - player["last_login_ts"]) / 3600, 0) - if offline_hours > 48: - offline_hours = 48 - - effective_bw = calculate_effective_bandwidth(player) - server_capacity = player["servers"] * 500 - stability = min(1.0, server_capacity / max(player["coverage_customers"], 1)) - tariff = 15 # ₡/час/клиент (усреднённый) - - income = player["coverage_customers"] * tariff * stability * offline_hours - return round(income, 2) - -def handle_outage(player: dict) -> dict: - if player["outage_end_ts"] > 0: - now = int(time.time()) - if now > player["outage_end_ts"]: - # Восстановление - return {**player, "outage_end_ts": 0, "backup_channel_active": False} - else: - if player["backup_channel_active"]: - return {**player, "speed": max(5, player["speed"] * 0.75)} - else: - churn = int(player["coverage_customers"] * 0.35) - return { - **player, - "coverage_customers": max(0, player["coverage_customers"] - churn), - "max_customers": max(0, player["max_customers"] - churn) - } - return player - -def check_power_overload(player: dict) -> tuple[bool, str]: - power_usage = player["servers"] * SERVER_POWER - if power_usage > player["power_capacity"] * 1.2: - return True, "⚠️ КРИТИЧЕСКАЯ ПЕРЕНАГРУЗКА! Серверы отключаются..." - elif power_usage > player["power_capacity"]: - return True, "⚠️ Перегрузка! Некоторые серверы работают с лагами." - return False, "" - -def sync_power(player: dict) -> dict: - power_usage = player["servers"] * SERVER_POWER - if power_usage > player["power_capacity"] * 1.2: - disabled = max(1, int(player["servers"] * 0.1)) - new_servers = max(0, player["servers"] - disabled) - new_max = max(0, player["max_customers"] - disabled * 100) - return {**player, "servers": new_servers, "max_customers": new_max} - return {**player, "power_usage": power_usage} \ No newline at end of file diff --git a/handlers/build.py b/handlers/build.py deleted file mode 100644 index 908bbb6..0000000 --- a/handlers/build.py +++ /dev/null @@ -1,74 +0,0 @@ -from aiogram import types, Router -from database import get_player, update_player -from economy import sync_power -from config import CABLE_COST, SERVER_COST, BACKUP_CHANNEL_COST, BACKUP_CHANNEL_POWER_ADD - -router = Router() - -@router.callback_query(lambda c: c.data == "build_cable") -async def build_cable(callback: types.CallbackQuery): - user_id = callback.from_user.id - player = get_player(user_id) - if player["balance"] < CABLE_COST: - await callback.answer("❌ Не хватает средств!", show_alert=True) - return - - new_power = player["power_capacity"] + 2 - update_player(user_id, { - "balance": player["balance"] - CABLE_COST, - "power_capacity": new_power, - "cables": player["cables"] + 1 - }) - await callback.message.edit_text( - f"✅ Кабель проложен!\n" - f"⚡ Лимит питания: {new_power} кВт\n" - f"💰 Остаток: {player['balance'] - CABLE_COST} ₡" - ) - await callback.answer("Кабель установлен 🛠️") - -@router.callback_query(lambda c: c.data == "buy_server") -async def buy_server(callback: types.CallbackQuery): - user_id = callback.from_user.id - player = get_player(user_id) - if player["balance"] < SERVER_COST: - await callback.answer("❌ Не хватает средств!", show_alert=True) - return - - new_servers = player["servers"] + 1 - new_power = new_servers * 2.0 # SERVER_POWER = 2.0 из config.py - - update_player(user_id, { - "balance": player["balance"] - SERVER_COST, - "servers": new_servers, - "max_customers": player["max_customers"] + 500, - "power_capacity": new_power, # обновляем лимит - "power_usage": player["servers"] * 2.0 # обновляем нагрузку (была на старом количестве) - }) - - await callback.message.edit_text( - f"✅ Сервер установлен!\n" - f"🖥 Теперь: {new_servers} шт.\n" - f"⚡ Потребление: {new_power:.1f}/{new_power:.1f} кВт" - ) - await callback.answer("Сервер включён!") - -@router.callback_query(lambda c: c.data == "buy_backup_channel") -async def buy_backup_channel(callback: types.CallbackQuery): - user_id = callback.from_user.id - player = get_player(user_id) - if player["balance"] < BACKUP_CHANNEL_COST: - await callback.answer("❌ Не хватает средств!", show_alert=True) - return - - new_power = player["power_capacity"] + BACKUP_CHANNEL_POWER_ADD - update_player(user_id, { - "balance": player["balance"] - BACKUP_CHANNEL_COST, - "power_capacity": new_power, - "backup_channel_active": 0 - }) - await callback.message.edit_text( - f"⚡ Резервный канал построен! ✅\n" - f"Лимит питания: {new_power} кВт\n" - f"💰 Остаток: {player['balance'] - BACKUP_CHANNEL_COST} ₡" - ) - await callback.answer("Резерв готов!") \ No newline at end of file diff --git a/handlers/channel.py b/handlers/channel.py deleted file mode 100644 index 15620bb..0000000 --- a/handlers/channel.py +++ /dev/null @@ -1,70 +0,0 @@ -from aiogram import types, Router -from database import get_player, update_player -from economy import calculate_channel_cost - -router = Router() # ← ВАЖНО! - -@router.callback_query(lambda c: c.data == "upgrade_channel") -async def upgrade_channel(callback: types.CallbackQuery): - user_id = callback.from_user.id - player = get_player(user_id) - - current_level = player["network_channel_level"] - if current_level >= 6: - await callback.answer("⚠️ Максимальный уровень канала достигнут! 🌍", show_alert=True) - return - - cost = calculate_channel_cost(current_level) - if player["balance"] < cost: - await callback.answer(f"❌ Не хватает {cost - player['balance']} ₡", show_alert=True) - return - - next_level = current_level + 1 - next_capacity = int(player["channel_capacity"] * 2.5) - upgrade_hours = 4 * current_level - - update_player(user_id, { - "balance": player["balance"] - cost, - "next_build_end_ts": int(__import__("time").time()) + upgrade_hours * 3600, - "infrastructure_level": player["infrastructure_level"] + 0.4 - }) - - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton( - text="🚀 Ускорить (x2 за 500 ₡)", - callback_data=f"speedup_channel_{current_level}" - )] - ]) - - await callback.message.edit_text( - f"🚀 Прокачка канала до уровня {next_level} (до {next_capacity} Мбит/с)...\n" - f"⏳ Ожидание: {upgrade_hours} ч\n" - f"💰 Остаток: {player['balance'] - cost} ₡", - reply_markup=keyboard - ) - await callback.answer("Процесс запущен 🛠️") - -@router.callback_query(lambda c: c.data.startswith("speedup_channel_")) -async def speedup_channel(callback: types.CallbackQuery): - user_id = callback.from_user.id - player = get_player(user_id) - - if player["balance"] < 500: - await callback.answer("❌ Нет средств на ускорение", show_alert=True) - return - - new_end = max( - 0, - int(__import__("time").time()) + - (player["next_build_end_ts"] - int(__import__("time").time())) // 2 - ) - - update_player(user_id, { - "balance": player["balance"] - 500, - "next_build_end_ts": new_end - }) - - await callback.message.edit_text( - f"⚡ Ускорено! Канал будет готов через ~{(new_end - __import__('time').time()) / 3600:.1f} ч" - ) - await callback.answer("🚀 Апгрейд ускорен!") \ No newline at end of file diff --git a/handlers/events.py b/handlers/events.py deleted file mode 100644 index 4123f3e..0000000 --- a/handlers/events.py +++ /dev/null @@ -1,32 +0,0 @@ -from aiogram import types, Router -import random -import time -from database import get_player, update_player - -router = Router() - -@router.callback_query(lambda c: c.data == "activate_backup") -async def activate_backup(callback: types.CallbackQuery): - user_id = callback.from_user.id - player = get_player(user_id) - - if player["backup_channel_active"]: - await callback.answer("✅ Резерв уже активен!", show_alert=True) - return - - power_used = player["servers"] * 2.0 - if power_used > player["power_capacity"]: - await callback.answer( - f"⚡ Недостаточно мощности: {power_used:.1f}/{player['power_capacity']} кВт", - show_alert=True - ) - return - - update_player(user_id, { - "backup_channel_active": 1, - "speed": max(5, player["speed"] * 0.75) - }) - await callback.message.edit_text( - "✅ Резервный канал активирован! Сеть работает на 75% скорости." - ) - await callback.answer("⚡ Резерв включён!") \ No newline at end of file diff --git a/handlers/map.py b/handlers/map.py deleted file mode 100644 index 35604b2..0000000 --- a/handlers/map.py +++ /dev/null @@ -1,27 +0,0 @@ -# handlers/map.py -from aiogram import types, Router -from database import get_player - -router = Router() # ← ВАЖНО! Регистрируем роутер - -@router.callback_query(lambda c: c.data == "map") -async def show_map(callback: types.CallbackQuery): - player = get_player(callback.from_user.id) - districts = [ - ("🏠 Гараж (ваша база)", "✅ Подключён"), - ("🏢 Центральный район", "🟡 В процессе" if player.get("next_build_end_ts", 0) > 0 else "🔒 Недоступен"), - ("🎮 Западный квартал", "🔒 Недоступен"), - ] - - text = "🗺 *Карта города*\n\n" - for dist, status in districts: - text += f"{status} — {dist}\n" - text += "\n💡 *Подсказка:* Прокачайте канал, чтобы подключить офисы и геймеров." - - await callback.message.edit_text( - text, - parse_mode="Markdown", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="⬅️ Назад", callback_data="back_menu")] - ]) - ) \ No newline at end of file diff --git a/handlers/start.py b/handlers/start.py deleted file mode 100644 index d2015a7..0000000 --- a/handlers/start.py +++ /dev/null @@ -1,57 +0,0 @@ -from aiogram import types, Router -from aiogram.filters import CommandStart -from database import create_player_if_not_exists, get_player, update_player # ← добавь update_player -from economy import ( - calculate_effective_bandwidth, calculate_rating, calculate_client_retention, - calculate_daily_income, calculate_daily_offline_earnings -) - -router = Router() - -@router.message(CommandStart()) -async def cmd_start(message: types.Message): - user_id = message.from_user.id - create_player_if_not_exists(user_id) - player = get_player(user_id) - - # Оффлайн доход - offline_earn = calculate_daily_offline_earnings(player) - if offline_earn > 0: - player["balance"] += offline_earn - player["total_income"] += offline_earn - player["last_login_ts"] = int(__import__("time").time()) - update_player(user_id, { - "balance": player["balance"], - "total_income": player["total_income"], - "last_login_ts": player["last_login_ts"] - }) - - effective_bw = calculate_effective_bandwidth(player) - lost, churn_msg = calculate_client_retention(player) - daily_income = calculate_daily_income(player) - - status = "" - if player["outage_end_ts"] > 0: - remaining = player["outage_end_ts"] - int(__import__("time").time()) - status = f"\n⚠️ АВАРИЯ: Отключение до {remaining // 3600}ч {remaining % 3600 // 60}м" - if player["backup_channel_active"]: - status += " (резерв активен)" - - text = ( - f"🌐 *ISP Tycoon* | 📡 Канал: {player['network_channel_level']} ✨\n\n" - f"⚡ *Канал:* {player['channel_capacity']} → {effective_bw} Мбит/с\n" - f"🔋 *Сеть:* {player['power_usage']:.1f}/{player['power_capacity']} кВт\n" - f"🖥 *Серверы:* {player['servers']} (ёмкость: {player['max_customers']} клиентов)\n" - f"📡 *Клиенты:* {player['coverage_customers']} (отток: {lost}/сутки)\n" - f"💰 *Бюджет:* {player['balance']:,.0f} ₡\n" - f"📊 *Доход/день:* {daily_income:,.0f} ₡\n" - f"🌟 *Рейтинг:* {calculate_rating(player)}\n" - f"{churn_msg}\n{status}" - ) - - from keyboards import get_main_menu - await message.answer( - text, - reply_markup=get_main_menu(player), - parse_mode="Markdown" - ) \ No newline at end of file diff --git a/handlers/stats.py b/handlers/stats.py deleted file mode 100644 index 9456ec4..0000000 --- a/handlers/stats.py +++ /dev/null @@ -1,43 +0,0 @@ -from aiogram import types, Router -from database import get_player -from economy import ( - calculate_effective_bandwidth, - calculate_rating, - calculate_daily_income # ← ВОТ ЭТОТ ИМПОРТ НУЖЕН! -) - -router = Router() - -@router.callback_query(lambda c: c.data == "stats") -async def stats(callback: types.CallbackQuery): - player = get_player(callback.from_user.id) - if not player: - await callback.answer("❌ Игрок не найден", show_alert=True) - return - - bw = calculate_effective_bandwidth(player) - daily = calculate_daily_income(player) - - text = ( - f"📊 *Ваша статистика*\n\n" - f"🔹 Канал: {player['network_channel_level']} → {bw} Мбит/с\n" - f"🔹 Серверы: {player['servers']} шт. (ёмкость: {player['max_customers']})\n" - f"🔹 Клиенты: {player['coverage_customers']} / {player['max_customers']}\n" - f"🔹 Доход/день: {daily:,.0f} ₡\n" - f"🔹 Общий доход: {player['total_income']:,.0f} ₡\n" - f"🔹 Рейтинг: {calculate_rating(player)} 🌟" - ) - - await callback.message.edit_text( - text, - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="⬅️ Назад", callback_data="back_menu")] - ]), - parse_mode="Markdown" - ) - -@router.callback_query(lambda c: c.data == "back_menu") -async def back_to_menu(callback: types.CallbackQuery): - await callback.message.delete() - from handlers.start import cmd_start - await cmd_start(callback.message) \ No newline at end of file diff --git a/keyboards.py b/keyboards.py deleted file mode 100644 index 6a938cb..0000000 --- a/keyboards.py +++ /dev/null @@ -1,18 +0,0 @@ -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton - -def get_main_menu(player: dict) -> InlineKeyboardMarkup: - return InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🚀 Прокачать канал", callback_data="upgrade_channel")], - [InlineKeyboardButton(text="🛠 Построить кабель", callback_data="build_cable")], - [InlineKeyboardButton(text="🖥 Купить сервер", callback_data="buy_server")], - [InlineKeyboardButton(text="💡 Ускорить постройку (x2 за 500 ₡)", callback_data="speedup_construction")], - [InlineKeyboardButton(text="📊 Статистика", callback_data="stats")], - [InlineKeyboardButton(text="🗺 Карта города", callback_data="map")] - ]) - -def get_backup_activation(player: dict) -> InlineKeyboardMarkup: - if player["backup_channel_active"] or player["outage_end_ts"] == 0: - return None - return InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="⚡ Активировать резервный канал", callback_data="activate_backup")] - ]) \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 8243b4a..0000000 --- a/main.py +++ /dev/null @@ -1,53 +0,0 @@ -from aiogram import Bot, Dispatcher, types -from aiogram.enums import ParseMode -from aiogram import Router -import asyncio -import logging - -from config import BOT_TOKEN -from database import init_db -from handlers.start import router as start_router -from handlers.build import router as build_router -from handlers.channel import router as channel_router -from handlers.stats import router as stats_router -from handlers.map import router as map_router -from handlers.events import router as events_router - -router = Router() - -dp = Dispatcher() -dp.include_router(start_router) -dp.include_router(build_router) -dp.include_router(channel_router) -dp.include_router(stats_router) -dp.include_router(map_router) -dp.include_router(events_router) - -@dp.message() -async def echo(message: types.Message): # ← теперь types.Message — валидно - if message.text == "/check": - from database import get_player - from economy import calculate_effective_bandwidth, handle_outage, sync_power - player = get_player(message.from_user.id) - if not player: - await message.answer("❌ Игрок не найден") - return - - player = handle_outage(player) - player = sync_power(player) - - await message.answer( - f"⚡ Состояние проверено!\n" - f"Канал: {player['channel_capacity']} → {calculate_effective_bandwidth(player)} Мбит/с\n" - f"Лимит: {player['power_usage']:.1f}/{player['power_capacity']} кВт" - ) - -async def main(): - init_db() - bot = Bot(token=BOT_TOKEN, parse_mode=ParseMode.HTML) - print("🤖 ISP Tycoon bot started...") - await dp.start_polling(bot) - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - asyncio.run(main()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 95ae5b9..e767230 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ -aiogram>=3.1.0 -python-dotenv>=1.0.0 \ No newline at end of file +aiogram==3.4.1 +SQLAlchemy==2.0.25 +asyncpg +alembic +python-dotenv +pydantic-settings \ No newline at end of file