Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52c67e1639 | |||
| 3071e5a1a1 |
92
.gitignore
vendored
92
.gitignore
vendored
@@ -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
|
||||||
.env.local
|
__pycache__/
|
||||||
.env.*.local
|
*.pyc
|
||||||
|
|
||||||
# Database
|
|
||||||
data/
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
|
|
||||||
# IDE & editors
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
*.log
|
||||||
logs/
|
.venv/
|
||||||
|
.idea/
|
||||||
# Temporary files
|
.vscode/
|
||||||
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/
|
|
||||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -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"]
|
||||||
38
alembic.ini
Normal file
38
alembic.ini
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
prepend_sys_path = .
|
||||||
|
version_path_separator = os
|
||||||
|
sqlalchemy.url = postgresql+asyncpg://provider:provider@db:5432/provider
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
36
alembic/env.py
Normal file
36
alembic/env.py
Normal file
@@ -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()
|
||||||
17
app/config.py
Normal file
17
app/config.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class DbSettings(BaseSettings):
|
||||||
|
DATABASE_URL: str
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(env_file=".env")
|
||||||
|
|
||||||
|
|
||||||
|
class BotSettings(BaseSettings):
|
||||||
|
BOT_TOKEN: str
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(env_file=".env")
|
||||||
|
|
||||||
|
|
||||||
|
db_settings = DbSettings()
|
||||||
|
bot_settings = BotSettings()
|
||||||
5
app/database.py
Normal file
5
app/database.py
Normal file
@@ -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)
|
||||||
15
app/handlers/start.py
Normal file
15
app/handlers/start.py
Normal file
@@ -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} ₡"
|
||||||
|
)
|
||||||
18
app/main.py
Normal file
18
app/main.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import asyncio
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from app.config import settings
|
||||||
|
from app.handlers import start
|
||||||
|
from app.config import bot_settings
|
||||||
|
|
||||||
|
bot = Bot(bot_settings.BOT_TOKEN)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
bot = Bot(settings.BOT_TOKEN)
|
||||||
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
dp.include_router(start.router)
|
||||||
|
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
4
app/models/base.py
Normal file
4
app/models/base.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
11
app/models/district.py
Normal file
11
app/models/district.py
Normal file
@@ -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)
|
||||||
11
app/models/player.py
Normal file
11
app/models/player.py
Normal file
@@ -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)
|
||||||
17
app/services/player_service.py
Normal file
17
app/services/player_service.py
Normal file
@@ -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
|
||||||
44
config.py
44
config.py
@@ -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
|
|
||||||
}
|
|
||||||
93
database.py
93
database.py
@@ -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()
|
|
||||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -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:
|
||||||
101
economy.py
101
economy.py
@@ -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}
|
|
||||||
@@ -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("Резерв готов!")
|
|
||||||
@@ -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("🚀 Апгрейд ускорен!")
|
|
||||||
@@ -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("⚡ Резерв включён!")
|
|
||||||
@@ -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")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
@@ -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"
|
|
||||||
)
|
|
||||||
@@ -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)
|
|
||||||
18
keyboards.py
18
keyboards.py
@@ -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")]
|
|
||||||
])
|
|
||||||
53
main.py
53
main.py
@@ -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())
|
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
aiogram>=3.1.0
|
aiogram==3.4.1
|
||||||
python-dotenv>=1.0.0
|
SQLAlchemy==2.0.25
|
||||||
|
asyncpg
|
||||||
|
alembic
|
||||||
|
python-dotenv
|
||||||
|
pydantic-settings
|
||||||
Reference in New Issue
Block a user