Играть в игры весело, а ещё веселее их разрабатывать!

Сегодня мы создадим простейшую игру для Telegram, представляющую собой Mini App.

В нашем случае, это игра с «бизнес-уклоном». Часто клиенты (и не только наши) хотят бонус, но раздавать по запросу бонусы не очень правильно. Гораздо лучше, чтобы пользователи его «заработали», выполнив какие-то действия, взаимодействуя с вашим брендом. А что может быть веселее небольшой игры с призом в конце.

В нашей игре нужно будет прыгать по платформам и собирать звёзды. Если собрать 10 звёзд -- это победа.

Ссылка на игру – вы можете пройти её сами. Игра будет доступна около месяца после выхода статьи.

Выглядит игра так.

Скрин интерфейса игры
Скрин интерфейса игры

В статье мы приведем код Mini App игры. Разберем, как кастомизировать игру под бренд и менять баланс, чтобы проходить её было интереснее. Так, в игре по ссылке выше, вы можете играть нашим талисманом – Пушмастером. 

А в завершение запустим нашу игру на удаленном сервере тремя командами в IDE в облаке для простого деплоя Amvera. Это позволит обновлять проект через git push (или загрузкой файла в интерфейсе) и даст бесплатный https домен для нашего Telegram Web App. Запустим код бесплатно, используя промо-баланс в 111 р.

Как устроен код проекта Mini App игры

Так как это Telegram Mini App, нам потребуется два проекта – телеграм-бот и сам web app.

Бот не очень сложный и выполняет роль бэкенда. Сама же игра будет реализована как простой html, запускаемый python-скриптом.

Код бота доступен по ссылке №1 на GitHub, а код фронта по ссылке №2.

Приступим к разработке.

Создание игры

Ввиду большого обема кода, я вынес примеры и основные шаги в спойлеры.

Шаг 1. Создание Telegram-бота

Для начала необходимо создать бота через @BotFather. Отправьте команду /newbot и следуйте инструкциям. Полученный токен понадобится при настройке.

Создайте файл game_bot.py:

# game_bot.py
# Telegram бот с мини-игрой для Amvera Cloud

import os
import sys
import logging
import sqlite3
import json
from datetime import datetime
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo, BotCommand
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters, ApplicationBuilder

# ============== Настройка логирования ==============
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO,
    stream=sys.stdout  # Важно для отображения логов в Amvera
)
logger = logging.getLogger(__name__)

# ============== Конфигурация ==============
# Токен бота (обязательная переменная)
BOT_TOKEN = os.environ.get('BOT_TOKEN')
if not BOT_TOKEN:
    logger.error(" BOT_TOKEN не установлен!")
    sys.exit(1)

# ID администраторов (опционально)
ADMIN_IDS_STR = os.environ.get('ADMIN_IDS', '')
ADMIN_IDS = [int(x.strip()) for x in ADMIN_IDS_STR.split(',') if x.strip()]

# URL Web App с игрой (обязательная переменная)
WEBAPP_URL = os.environ.get('WEBAPP_URL', '')
if not WEBAPP_URL:
    logger.warning(" WEBAPP_URL не установлен! Кнопка игры не будет работать.")

# Настройки игры
BONUSES_TO_WIN = int(os.environ.get('BONUSES_TO_WIN', '5'))
PROMO_CODE = os.environ.get('PROMO_CODE', 'You_Code')
PROMO_URL = os.environ.get('PROMO_URL', 'https://amvera.ru/')

# Путь к базе данных (ВАЖНО: /data/ для сохранения при пересборке)
if os.environ.get('AMVERA') == '1':
    DB_PATH = '/data/game_bot.db'
else:
    DB_PATH = 'game_bot.db'

logger.info(f" Путь к БД: {DB_PATH}")
logger.info(f" WEBAPP_URL: {WEBAPP_URL}")

# ============== База данных ==============

def get_connection():
    """Создаёт подключение к SQLite"""
    return sqlite3.connect(DB_PATH, check_same_thread=False)

def init_db():
    """Инициализация базы данных"""
    conn = get_connection()
    cursor = conn.cursor()
    
    # Таблица пользователей
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS game_users (
        user_id INTEGER PRIMARY KEY,
        username TEXT,
        first_name TEXT,
        last_name TEXT,
        registered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        games_played INTEGER DEFAULT 0,
        games_won INTEGER DEFAULT 0,
        last_win_at DATETIME
    )
    ''')
    
    # Таблица результатов игр
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS game_results (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        bonuses_collected INTEGER,
        score INTEGER,
        won BOOLEAN,
        played_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (user_id) REFERENCES game_users (user_id)
    )
    ''')
    
    conn.commit()
    conn.close()
    logger.info(" База данных инициализирована")

def add_user(user_id, username, first_name, last_name):
    """Добавление или обновление пользователя"""
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute('''
    INSERT OR IGNORE INTO game_users (user_id, username, first_name, last_name)
    VALUES (?, ?, ?, ?)
    ''', (user_id, username, first_name, last_name))
    conn.commit()
    conn.close()

def record_game_result(user_id, bonuses_collected, score, won):
    """Запись результата игры"""
    conn = get_connection()
    cursor = conn.cursor()
    
    # Добавляем результат игры
    cursor.execute('''
    INSERT INTO game_results (user_id, bonuses_collected, score, won)
    VALUES (?, ?, ?, ?)
    ''', (user_id, bonuses_collected, score, won))
    
    # Обновляем статистику пользователя
    cursor.execute('''
    UPDATE game_users 
    SET games_played = games_played + 1,
        games_won = games_won + ?
    WHERE user_id = ?
    ''', (1 if won else 0, user_id))
    
    if won:
        cursor.execute('''
        UPDATE game_users SET last_win_at = CURRENT_TIMESTAMP WHERE user_id = ?
        ''', (user_id,))
    
    conn.commit()
    conn.close()
    logger.info(f" Результат игры записан: user={user_id}, won={won}, score={score}")

def get_user_stats(user_id):
    """Получение статистики пользователя"""
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute('''
    SELECT games_played, games_won, last_win_at FROM game_users WHERE user_id = ?
    ''', (user_id,))
    result = cursor.fetchone()
    conn.close()
    return result if result else (0, 0, None)

def get_global_stats():
    """Получение общей статистики"""
    conn = get_connection()
    cursor = conn.cursor()
    
    cursor.execute('SELECT COUNT(*) FROM game_users')
    total_users = cursor.fetchone()[0]
    
    cursor.execute('SELECT COUNT(*) FROM game_results')
    total_games = cursor.fetchone()[0]
    
    cursor.execute('SELECT COUNT(*) FROM game_results WHERE won = 1')
    total_wins = cursor.fetchone()[0]
    
    cursor.execute('SELECT MAX(score) FROM game_results')
    max_score = cursor.fetchone()[0] or 0
    
    conn.close()
    return {
        'total_users': total_users,
        'total_games': total_games,
        'total_wins': total_wins,
        'max_score': max_score
    }

# ============== Обработчики команд ==============

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Обработчик команды /start"""
    user = update.effective_user
    add_user(user.id, user.username, user.first_name, user.last_name)
    logger.info(f" Новый пользователь: {user.id} (@{user.username})")
    
    welcome_text = (
        f" <b>Привет, {user.first_name}!</b>\n\n"
        f"Добро пожаловать в <b>Amvera Jump</b> — захватывающую игру, "
        f"где тебе предстоит прыгать по платформам и собирать бонусы!\n\n"
        f" <b>Цель игры:</b>\n"
        f"Собери <b>{BONUSES_TO_WIN} бонуса</b>, чтобы выиграть приз!\n\n"
        f" <b>Приз:</b>\n"
        f"Удвоение платежа от <a href='{PROMO_URL}'>Amvera</a> — "
        f"облачной платформы для деплоя твоих проектов!\n\n"
        f" Нажми кнопку, чтобы начать игру!"
    )
    
    keyboard = []
    
    # Кнопка игры только если URL установлен
    if WEBAPP_URL:
        keyboard.append([InlineKeyboardButton(" Играть!", web_app=WebAppInfo(url=WEBAPP_URL))])
    else:
        keyboard.append([InlineKeyboardButton(" Игра недоступна", callback_data="no_game")])
    
    keyboard.append([InlineKeyboardButton(" Моя статистика", callback_data="stats")])
    keyboard.append([InlineKeyboardButton(" Как играть", callback_data="help")])
    
    reply_markup = InlineKeyboardMarkup(keyboard)
    
    await update.message.reply_text(
        welcome_text,
        parse_mode='HTML',
        reply_markup=reply_markup,
        disable_web_page_preview=True
    )

async def play(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Команда для запуска игры"""
    if not WEBAPP_URL:
        await update.message.reply_text(" Игра временно недоступна. WEBAPP_URL не настроен.")
        return
        
    keyboard = [[InlineKeyboardButton(" Играть!", web_app=WebAppInfo(url=WEBAPP_URL))]]
    reply_markup = InlineKeyboardMarkup(keyboard)
    
    await update.message.reply_text(
        " <b>Готов к игре?</b>\n\nНажми кнопку ниже, чтобы начать!",
        parse_mode='HTML',
        reply_markup=reply_markup
    )

async def stats_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Показать статистику пользователя"""
    user_id = update.effective_user.id
    games_played, games_won, last_win = get_user_stats(user_id)
    
    win_rate = (games_won / games_played * 100) if games_played > 0 else 0
    
    stats_text = (
        f" <b>Твоя статистика:</b>\n\n"
        f" Игр сыграно: <b>{games_played}</b>\n"
        f" Побед: <b>{games_won}</b>\n"
        f" Процент побед: <b>{win_rate:.1f}%</b>\n"
    )
    
    if last_win:
        stats_text += f" Последняя победа: <b>{last_win}</b>"
    
    await update.message.reply_text(stats_text, parse_mode='HTML')

async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Обработчик нажатий на кнопки"""
    query = update.callback_query
    await query.answer()
    
    if query.data == "stats":
        user_id = query.from_user.id
        games_played, games_won, last_win = get_user_stats(user_id)
        win_rate = (games_won / games_played * 100) if games_played > 0 else 0
        
        stats_text = (
            f" <b>Твоя статистика:</b>\n\n"
            f" Игр сыграно: <b>{games_played}</b>\n"
            f" Побед: <b>{games_won}</b>\n"
            f" Процент побед: <b>{win_rate:.1f}%</b>\n"
        )
        
        if last_win:
            stats_text += f"🕐 Последняя победа: <b>{last_win}</b>"
        
        keyboard = [[InlineKeyboardButton(" Назад", callback_data="back")]]
        await query.edit_message_text(stats_text, parse_mode='HTML', reply_markup=InlineKeyboardMarkup(keyboard))
        
    elif query.data == "help":
        help_text = (
            "" <b>Как играть:</b>\n\n"
            " Нажми кнопку <b>«Играть»</b>\n\n"
            " Управляй персонажем:\n"
            "   • На телефоне: наклоняй устройство влево/вправо\n"
            "   • На компьютере: используй стрелки ← →\n\n"
            f" Прыгай по платформам и собирай звёзды ⭐\n\n"
            f" Собери <b>{BONUSES_TO_WIN} звезды</b>, чтобы выиграть!\n\n"
            " Не падай вниз — игра начнётся заново!\n\n"
            " <b>Приз:</b> бонус от Amvera!"
        )
        
        keyboard = []
        if WEBAPP_URL:
            keyboard.append([InlineKeyboardButton(" Играть!", web_app=WebAppInfo(url=WEBAPP_URL))])
        keyboard.append([InlineKeyboardButton(" Назад", callback_data="back")])
        reply_markup = InlineKeyboardMarkup(keyboard)
        
        await query.edit_message_text(help_text, parse_mode='HTML', reply_markup=reply_markup)
        
    elif query.data == "back":
        user = query.from_user
        welcome_text = (
            f" <b>Привет, {user.first_name}!</b>\n\n"
            f"Добро пожаловать в <b>Amvera Jump</b>!\n\n"
            f" Собери <b>{BONUSES_TO_WIN} бонуса</b>, чтобы выиграть приз!\n\n"
            f" Нажми кнопку, чтобы начать игру!"
        )
        
        keyboard = []
        if WEBAPP_URL:
            keyboard.append([InlineKeyboardButton(" Играть!", web_app=WebAppInfo(url=WEBAPP_URL))])
        keyboard.append([InlineKeyboardButton(" Моя статистика", callback_data="stats")])
        keyboard.append([InlineKeyboardButton(" Как играть", callback_data="help")])
        reply_markup = InlineKeyboardMarkup(keyboard)
        
        await query.edit_message_text(welcome_text, parse_mode='HTML', reply_markup=reply_markup)
    
    elif query.data == "no_game":
        await query.answer("Игра временно недоступна", show_alert=True)

async def web_app_data(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Обработчик данных от Web App"""
    user_id = update.effective_user.id
    data = update.effective_message.web_app_data.data
    
    logger.info(f" Получены данные от Web App: user={user_id}, data={data}")
    
    try:
        game_data = json.loads(data)
        
        bonuses = game_data.get('bonuses', 0)
        score = game_data.get('score', 0)
        won = game_data.get('won', False)
        
        # Записываем результат
        record_game_result(user_id, bonuses, score, won)
        
        if won:
            promo_text = (
                f" <b>ПОЗДРАВЛЯЕМ!</b>\n\n"
                f"Ты собрал все бонусы и выиграл!\n\n"
                f" <b>Твой приз:</b>\n"
                f"Код: <code>{PROMO_CODE}</code>\n\n"
                f"Используй его на <a href='{PROMO_URL}'>Amvera</a> "
                f"для получения бонуса!\n\n"
                f" Amvera — облачная платформа для деплоя проектов"
            )
            
            keyboard = [
                [InlineKeyboardButton(" Перейти на Amvera", url=PROMO_URL)],
            ]
            if WEBAPP_URL:
                keyboard.append([InlineKeyboardButton("Играть ещё", web_app=WebAppInfo(url=WEBAPP_URL))])
            
            reply_markup = InlineKeyboardMarkup(keyboard)
            
            await update.message.reply_text(promo_text, parse_mode='HTML', reply_markup=reply_markup, disable_web_page_preview=True)
        else:
            keyboard = []
            if WEBAPP_URL:
                keyboard.append([InlineKeyboardButton(" Играть снова", web_app=WebAppInfo(url=WEBAPP_URL))])
            
            await update.message.reply_text(
                f"Результат сохранён!\n"
                f"Собрано бонусов: {bonuses}/{BONUSES_TO_WIN}\n"
                f"Очки: {score}\n\n"
                f"Попробуй ещё раз! ",
                reply_markup=InlineKeyboardMarkup(keyboard) if keyboard else None
            )
            
    except Exception as e:
        logger.error(f" Ошибка обработки данных Web App: {e}")
        await update.message.reply_text("Произошла ошибка при сохранении результата.")

# ============== Админские команды ==============

async def admin_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Статистика для админов"""
    user_id = update.effective_user.id
    
    if ADMIN_IDS and user_id not in ADMIN_IDS:
        await update.message.reply_text(" У вас нет доступа к этой команде")
        return
    
    stats = get_global_stats()
    
    admin_text = (
        f" <b>Общая статистика бота:</b>\n\n"
        f" Всего пользователей: <b>{stats['total_users']}</b>\n"
        f" Всего игр: <b>{stats['total_games']}</b>\n"
        f" Всего побед: <b>{stats['total_wins']}</b>\n"
        f" Лучший счёт: <b>{stats['max_score']}</b>\n"
        f"\n <b>Конфигурация:</b>\n"
        f"WEBAPP_URL: {WEBAPP_URL or 'не установлен'}\n"
        f"DB_PATH: {DB_PATH}\n"
        f"AMVERA: {os.environ.get('AMVERA', 'не установлен')}"
    )
    
    await update.message.reply_text(admin_text, parse_mode='HTML')

# ============== Установка команд меню ==============

async def post_init(application):
    """Установка команд бота"""
    commands = [
        BotCommand("start", "Начать игру"),
        BotCommand("play", "Запустить игру"),
        BotCommand("stats", "Моя статистика"),
    ]
    await application.bot.set_my_commands(commands)
    logger.info(" Команды бота установлены")

# ============== Запуск бота ==============

def main():
    logger.info(" Запуск бота...")
    
    # Инициализация БД
    init_db()
    
    # Создаем приложение
    application = ApplicationBuilder().token(BOT_TOKEN).post_init(post_init).build()
    
    # Добавляем обработчики
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("play", play))
    application.add_handler(CommandHandler("stats", stats_command))
    application.add_handler(CommandHandler("admin", admin_stats))
    
    # Обработчик кнопок
    application.add_handler(CallbackQueryHandler(button_handler))
    
    # Обработчик данных от Web App
    application.add_handler(MessageHandler(filters.StatusUpdate.WEB_APP_DATA, web_app_data))
    
    # Запуск
    logger.info(" Бот запущен и готов к работе!")
    application.run_polling(drop_pending_updates=True)

if __name__ == '__main__':
    main()

Важные особенности:

  • Все настройки выносятся в переменные окружения

  • Используется путь /data/ для сохранения базы данных между перезапусками

  • Логи выводятся в stdout для просмотра в интерфейсе Amvera

Шаг 2. Создание веб-приложения с игрой

Создаем папку static и файл static/game.html.

Рассмотрим ключевые элементы кода.

Инициализация Telegram Web App

// Подключение к Telegram Web App API
const tg = window.Telegram.WebApp;
tg.expand(); // Разворачиваем приложение на весь экран

// Настройка размера canvas под устройство
const canvas = document.getElementById('gameCanvas');
canvas.width = Math.min(window.innerWidth - 40, 450);
canvas.height = window.innerHeight - 150;

Основная игровая логика

// Игровые объекты
const player = {
    x: canvas.width / 2,
    y: canvas.height - 100,
    width: 40,
    height: 40,
    velocityY: 0,
    velocityX: 0
};

const platforms = [];
const bonuses = [];
let score = 0;
let bonusesCollected = 0;
const gravity = 0.5;

function update() {
    // Движение игрока
    if (keys.left) player.velocityX = -5;
    else if (keys.right) player.velocityX = 5;
    else player.velocityX = 0;
    
    player.x += player.velocityX;
    player.velocityY += gravity;
    player.y += player.velocityY;
    
    // Проход сквозь края экрана
    if (player.x < -player.width / 2) player.x = canvas.width;
    if (player.x > canvas.width) player.x = -player.width / 2;
    
    // Столкновение с платформами
    platforms.forEach(platform => {
        if (player.velocityY > 0 &&
            player.y + player.height > platform.y &&
            player.y + player.height < platform.y + platform.height + 5 &&
            player.x + player.width > platform.x &&
            player.x < platform.x + platform.width) {
            
            player.velocityY = -12; // Прыжок
            gameState.score += 10;
        }
    });
    
    // Проверка условий окончания игры
    if (player.y > canvas.height + 100) {
        gameOver();
    }
    
    if (gameState.bonusesCollected >= BONUSES_TO_WIN) {
        winGame();
    }
}

Отправка результатов в бота

function sendGameData(won) {
    const data = {
        bonuses: gameState.bonusesCollected,
        score: gameState.score,
        won: won
    };
    
    try {
        tg.sendData(JSON.stringify(data));
    } catch (e) {
        console.log('Ошибка отправки данных:', e);
    }
}

Полный код игры включает дополнительные элементы:

  • Графику с градиентами и анимацией

  • Систему сбора бонусов на платформах

  • Экраны начала игры, победы и поражения

  • Загрузку изображения персонажа

Шаг 3. Варианты управления в игре

Игра поддерживает три варианта управления, которые определяются автоматически в зависимости от устройства.

1. Управление клавиатурой (для компьютера)

Используются клавиши-стрелки или A/D для перемещения персонажа:

const keys = { left: false, right: false };

document.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
    if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
});

document.addEventListener('keyup', (e) => {
    if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
    if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
});

2. Управление наклоном телефона (акселерометр)

На мобильных устройствах можно управлять персонажем, наклоняя телефон влево или вправо:

if (window.DeviceOrientationEvent) {
    window.addEventListener('deviceorientation', (e) => {
        if (!gameState.isPlaying) return;
        
        // e.gamma — угол наклона телефона влево/вправо (от -90 до 90 градусов)
        const tilt = e.gamma;
        
        if (tilt < -10) {
            // Наклон влево — персонаж движется влево
            keys.left = true;
            keys.right = false;
        } else if (tilt > 10) {
            // Наклон вправо — персонаж движется вправо
            keys.right = true;
            keys.left = false;
        } else {
            // Телефон в вертикальном положении — персонаж останавливается
            keys.left = false;
            keys.right = false;
        }
    });
}

ЗдесьDeviceOrientationEvent — API браузера для работы с датчиками устройства,e.gamma — показывает угол наклона в градусах.

Порог в 10 градусов создаёт зону нечувствительности, чтобы персонаж не дёргался при небольших движениях руки. При наклоне более 10° в любую сторону активируется соответствующее направление движения

3. Экранные кнопки (для мобильных устройств)

Для тех, кто предпочитает не наклонять телефон, добавлены кнопки управления на экране:

// Создание кнопок управления
const leftBtn = document.getElementById('leftBtn');
const rightBtn = document.getElementById('rightBtn');

// Обработка нажатий
leftBtn.addEventListener('touchstart', (e) => {
    e.preventDefault();
    keys.left = true;
});

leftBtn.addEventListener('touchend', (e) => {
    e.preventDefault();
    keys.left = false;
});

rightBtn.addEventListener('touchstart', (e) => {
    e.preventDefault();
    keys.right = true;
});

rightBtn.addEventListener('touchend', (e) => {
    e.preventDefault();
    keys.right = false;
});

HTML-разметка для кнопок:

<div class="mobile-controls">
    <button id="leftBtn" class="control-btn left">◄</button>
    <button id="rightBtn" class="control-btn right">►</button>
</div>

Стилизация кнопок в CSS:

.mobile-controls {
    position: fixed;
    bottom: 30px;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-between;
    padding: 0 30px;
    z-index: 100;
}

.control-btn {
    width: 70px;
    height: 70px;
    border-radius: 50%;
    background: rgba(99, 102, 241, 0.8);
    border: none;
    color: white;
    font-size: 32px;
    cursor: pointer;
}

Автоматическое определение типа управления

Игра определяет устройство и показывает соответствующую подсказку:

const controlHint = document.getElementById('controlHint');

if (window.DeviceOrientationEvent && 'ontouchstart' in window) {
    // Мобильное устройство
    controlHint.textContent = ' Наклоняй телефон или используй кнопки';
    // Показываем экранные кнопки
    document.querySelector('.mobile-controls').style.display = 'flex';
} else {
    // Компьютер
    controlHint.textContent = '⌨️ Используй стрелки ← → или A/D';
    // Скрываем экранные кнопки
    document.querySelector('.mobile-controls').style.display = 'none';
}
Шаг 4. Создание веб-сервера

Создайте файл app.py для обслуживания файлов игры:

# app.py - Flask сервер для раздачи статических файлов игры
# Проект: Web App для Telegram Mini App

import os
import sys
import logging
from flask import Flask, send_from_directory, send_file

# Настройка логирования
logging.basicConfig(
    format='%(asctime)s - %(levelname)s - %(message)s',
    level=logging.INFO,
    stream=sys.stdout
)
logger = logging.getLogger(__name__)

app = Flask(__name__, static_folder='static')

@app.route('/')
def index():
    """Главная страница - редирект на игру"""
    return send_file('static/game.html')

@app.route('/game')
def game():
    """Страница игры"""
    return send_file('static/game.html')

@app.route('/game.html')
def game_html():
    """Прямой доступ к game.html"""
    return send_file('static/game.html')

@app.route('/<path:filename>')
def serve_static(filename):
    """Раздача статических файлов"""
    return send_from_directory('static', filename)

@app.route('/health')
def health():
    """Проверка здоровья для мониторинга"""
    return {'status': 'ok'}, 200

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 80))
    logger.info(f" Запуск веб-сервера на порту {port}")
    app.run(host='0.0.0.0', port=port)
Шаг 5. Подготовка файлов конфигурации

Конфигурация для бота

Создайте файл amvera.yml в папке с ботом:

meta:
  environment: python
  toolchain:
    name: pip
    version: "3.11"

build:
  requirementsPath: requirements.txt

run:
  scriptName: game_bot.py
  persistenceMount: /data

И файл requirements.txt:

python-telegram-bot==20.7

Параметр persistenceMount: /data обеспечивает постоянное хранилище для базы данных SQLite.

Конфигурация для веб-приложения

Создайте файл amvera.yml в папке с игрой:

meta:
  environment: python
  toolchain:
    name: pip
    version: "3.11"

build:
  requirementsPath: requirements.txt

run:
  scriptName: app.py
  containerPort: 80

И файл requirements.txt:

flask==3.0.0
gunicorn==21.2.0
Шаг 6. Размещение веб-приложения (игры) на Amvera

Теперь разместим веб-приложение с игрой. Этот шаг нужно выполнить первым, так как адрес игры понадобится для настройки бота.

6.1. Подготовка файлов

Убедитесь, что структура папки с веб-приложением выглядит следующим образом:

web-app/
├── app.py
├── amvera.yml
├── requirements.txt
└── static/
    ├── game.html
    └── player.png

6.2. Создание проекта в Amvera

Создайте проект и пройдите шаги флоу
Создайте проект и пройдите шаги флоу
Используйте Git или интерфейс для загрузки файлов
Используйте Git или интерфейс для загрузки файлов

6.4. Настройка проекта

Заполните конфигурацию и запустите сборку
Заполните конфигурацию и запустите сборку

Процесс установки занимает 1-3 минуты. Следите за статусом в интерфейсе.

6.5. Получение адреса приложения

После успешного запуска создайте бесплатный https-домен.

Пример подключенного домена
Пример подключенного домена

Важно: скопиру��те Ваш адрес и сохраните в отдельном файле. Он понадобится на следующем шаге для настройки бота.

6.6. Проверка работы

Откройте полученный адрес в браузере. Вы должны увидеть экран начала игры. Если игра отображается корректно — веб-приложение размещено успешно.

Шаг 7. Размещение Telegram-бота на Amvera

Теперь разместим бота, который будет запускать игру.

7.1. Подготовка файлов

Структура папки с ботом должна выглядеть так:

telegram-bot/
├── game_bot.py
├── amvera.yml
└── requirements.txt

7.2. Создание второго проекта

Создаем еще один проект, в котором захостим скрипт бота. Делаем все по аналогии с пунктом 6.

7.5. Настройка переменных окружения

После установки необходимо настроить переменные окружения. Это критически важный шаг, без которого бот не запустится.

Задание переменных
Задание переменных

В карточке проекта с ботом нажмите на раздел «Настройки», выберите вкладку «Переменные окружения» и добавьте следующие переменные:

Переменная 1 (обязательная):

  • Название: BOT_TOKEN

  • Значение: токен, который вы получили от BotFather

Переменная 2 (обязательная):

  • Название: WEBAPP_URL

  • Значение: адрес веб-приложения, который вы получили на шаге 6.5

  • Важно: адрес должен начинаться с https:// и не должен заканчиваться на /

Переменная 3:

  • Название: PROMO_CODE

  • Значение: промокод, который получат победители

  • Пример: YouCode2025

Переменная 4:

  • Название: PROMO_URL

  • Значение: адрес вашего сайта или сервиса

Переменная 5:

  • Название: BONUSES_TO_WIN

  • Значение: 5

  • Описание: количество бонусов, необходимое для победы

Переменная 6:

  • Название: AMVERA

  • Значение: 1

  • Описание: индикатор работы на платформе Amvera

Переменные будут выглядеть примерно так
Переменные будут выглядеть примерно так

После задания переменных перезапустим проект.

7.7. Проверка работы бота

Открываем бота в Telegram и пробуем начать игру.

Если бот не отвечает, проверяем раздел «Логи» в интерфейсе Amvera, смотрим, что BOT_TOKEN указан правильно и что статус проекта «Приложение Запущено», а не «Остановлено» или «Работает с ошибкой».

Если игра не открывается, проверьте, что WEBAPP_URL указан правильно в переменных окружения, убедитесь, что веб-приложение работает (откройте его адрес в браузере) и проверьте логи обоих проектов, обычно в них видны причины неисправности.

Шаг 8. Редактирование файла game.html под свои нужды

Файл game.html содержит весь код игры, включая тексты, цвета и настройки. Рассмотрим подробно, что и где можно изменить.

8.1. Изменение заголовка страницы

Откройте файл game.html в текстовом редакторе. Найдите строку 6:

<title>Amvera Jump</title>

Измените на желаемое название:

<title>Моя Супер Игра</title>

Это название будет отображаться в заголовке вкладки браузера.

8.2. Изменение цветовой схемы

Цвета игры задаются в переменных CSS. Найдите строку 17:

:root {
    --primary: #6366f1;        /* Основной фиолетовый цвет */
    --primary-light: #818cf8;  /* Светлый оттенок основного цвета */
    --secondary: #22d3ee;      /* Дополнительный голубой цвет */
    --accent: #f472b6;         /* Акцентный розовый цвет */
    --dark: #0f0f23;           /* Тёмный фон */
    --darker: #070714;         /* Очень тёмный фон */
    --gold: #fbbf24;           /* Золотой цвет для очков */
    --success: #22c55e;        /* Зелёный цвет успеха */
}

Пример изменения на красную схему:

:root {
    --primary: #dc2626;        /* Красный */
    --primary-light: #ef4444;  /* Светло-красный */
    --secondary: #fbbf24;      /* Золотой */
    --accent: #fb923c;         /* Оранжевый */
    --dark: #1f2937;           /* Серый тёмный */
    --darker: #111827;         /* Серый очень тёмный */
    --gold: #fbbf24;           /* Золотой */
    --success: #10b981;        /* Зелёный */
}

После изменения все элементы игры (кнопки, платформы, эффекты) автоматически примут новые цвета.

8.3. Изменение логотипа игры

Логотип отображается в верхнем левом углу во время игры. Найдите строку 86:

<div class="logo">AMVERA JUMP</div>

Измените на название вашей игры:

<div class="logo">МОЯ ИГРА</div>

8.4. Изменение текста на экране начала игры

Экран начала игры содержит приветствие и инструкции. Найдите строки 239-248:

<div class="title">AMVERA JUMP</div>
<div class="subtitle">
    Прыгай по платформам и собирай звёзды!
</div>

<div class="instructions">
    <h3>Управление:</h3>
    <p><span class="emoji"></span> Наклоняй телефон влево/вправо</p>
    <p><span class="emoji">⌨️</span> Или используй стрелки ← →</p>
</div>

Измените на свой текст:

<div class="title">СУПЕР ПЛАТФОРМЕР</div>
<div class="subtitle">
    Покоряй высоты и собирай все бонусы!
</div>

<div class="instructions">
    <h3>Как играть:</h3>
    <p><span class="emoji"></span> Наклоняй устройство или жми кнопки</p>
    <p><span class="emoji"></span> Собери 5 звёзд для победы</p>
</div>

8.5. Изменение экрана победы

Экран победы появляется при выполнении цели. Найдите строки 250-264:

<div class="title"> ПОБЕДА!</div>
<div class="win-message">
    Поздравляем! Ты выиграл промокод
    от <span style="color: #22d3ee;">Amvera</span>!
</div>
<div class="promo-display">
    <div class="promo-label">Твой промокод:</div>
    <div class="promo-code" id="promoCode">AMVERA2025</div>
    <div class="promo-hint">Нажми, чтобы скопировать</div>
</div>
<button class="claim-btn" id="claimBtn">
    🌐 Получить бонус
</button>

Пример изменённого экрана:

<div class="title"> ТЫ ПОБЕДИЛ!</div>
<div class="win-message">
    Отличная работа! Держи свой приз
    от <span style="color: #dc2626;">Моя Компания</span>!
</div>
<div class="promo-display">
    <div class="promo-label">Секретный код:</div>
    <div class="promo-code" id="promoCode">SUPER2025</div>
    <div class="promo-hint">Коснись для копирования</div>
</div>
<button class="claim-btn" id="claimBtn">
     Забрать приз
</button>

8.6. Изменение промокода в коде JavaScript

Промокод также нужно указать в JavaScript-коде. Найдите строку 272:

const promoCode = document.getElementById('promoCode');
promoCode.textContent = 'YouCode';

8.7. Изменение ссылки на ваш сайт

Кнопка "Получить бонус" ведёт на указанный сайт. Найдите строку 1087:

claimBtn.addEventListener('click', function() {
    window.open('https://ваш-сайт.com/', '_blank');
});

8.8. Изменение количества бонусов для победы

Найдите строку 635:

const BONUSES_TO_WIN = 5;

Важно: после изменения этого значения также измените переменную окруженияBONUSES_TO_WIN в настройках бота на Amvera.

8.9. Изменение изображения персонажа

По умолчанию используется файл player.png. Чтобы использовать другое изображение:

  1. Подготовьте изображение:

    • Формат: PNG с прозрачным фоном

    • Размер: примерно 200×200 пикселей

    • Название: например, hero.png

  2. Поместите файл в папку static/

  3. Найдите строку 665 в game.html:

const playerImage = new Image();
playerImage.src = 'player.png';

Измените на название вашего файла:

const playerImage = new Image();
playerImage.src = 'hero.png';

8.10. Изменение скорости и физики игры

Для изменения сложности игры можно настроить параметры физики. Найдите строки 637-640:

const gravity = 0.5;         // Сила гравитации
const jumpForce = -12;       // Сила прыжка
const moveSpeed = 5;         // Скорость движения
const platformSpeed = 2;     // Скорость движения платформ

Примеры настройки:

Для более лёгкой игры:

const gravity = 0.4;         // Меньше гравитация
const jumpForce = -14;       // Сильнее прыжок
const moveSpeed = 6;         // Быстрее движение

Для более сложной игры:

const gravity = 0.6;         // Сильнее гравитация
const jumpForce = -10;       // Слабее прыжок
const moveSpeed = 4;         // Медленнее движение

Применение изменений

После редактирования game.html загрузите его в репозиторий и пересоберите проект.

Итог разработки игры для Telegram

В этой статье мы рассмотрели процесс создания игры для Telegram Mini Apps и её размещения в облаке для простого хостинга приложений – Amvera. Весь процесс от начала до запуска занимает около 20 – 30 минут.

Вы можете изменить любые параметры игры: внешний вид, тексты, бонусы, физику и сложность. Надеюсь, игра оказалась веселой, а инструкция полезной!