Skip to main content

Ludicé Development Guide

Guide for developers contributing to or extending the Ludicé Telegram bot.

Table of Contents​

  1. Development Environment
  2. Code Structure
  3. Adding Features
  4. Testing
  5. Code Style
  6. Git Workflow
  7. Common Development Tasks

Development Environment​

Required Tools​

  • Python 3.9+
  • Go 1.19+
  • Redis
  • Git
  • Code Editor (VS Code, PyCharm, etc.)
  • Python
  • Go
  • Pylance
  • GitLens
  • Better Comments
  • Markdown All in One

Setup Development Environment​

# Clone repository
git clone https://github.com/Wizer27/Ludice.git
cd Ludice

# Create virtual environment
python3 -m venv venv
source venv/bin/activate # macOS/Linux
# or: venv\Scripts\activate # Windows

# Install dependencies
pip install -r requirements.txt
cd frontend && pip install -r requirements.txt && cd ..

# Install pre-commit hooks (optional but recommended)
pip install pre-commit
pre-commit install

Code Structure​

Frontend (Telegram Bot)​

frontend/
├── app.py # Main entry point
│ - Bot initialization
│ - Dispatcher setup
│ - Router registration
│
├── routers/
│ └── private_user.py # Main user handlers
│ - /start command
│ - Terms acceptance flow
│ - Payment handlers
│ - Game handlers
│ - Backend API integration
│
├── keyboard/
│ └── start.py # UI components
│ - Main menu keyboard
│ - Payment keyboard
│ - Game selection keyboard
│
├── common/
│ ├── legal_text.py # Legal text constants
│ └── bot_cmds_list.py # Bot commands
│
└── auth/
└── auth.py # JWT utilities (legacy)

Backend (Game API)​

backend/
├── new.py # Main FastAPI application
│ - Signature verification
│ - Rate limiting
│ - Game logic
│ - User management
│ - JSON file operations
│
├── financne.py # Payment API
│ - TON payment integration
│
├── redis/
│ └── main.go # Balance service (Go)
│ - User creation
│ - Balance queries
│ - Balance modifications
│
└── secrets.json # Shared secret key

Adding Features​

Add a New Bot Command​

Example: Add a /stats command to show user statistics

Step 1: Add Handler

Edit frontend/routers/private_user.py:

@start_router.message(Command("stats"))
async def cmd_stats(message: types.Message):
user_id = message.from_user.id
username = message.from_user.username or f"user_{user_id}"

# Call backend to get stats
data = {"username": username}
response = await send_to_backend(
"http://localhost:8080/get_stats",
data
)

if response:
wins = response.get("wins", 0)
total = response.get("total_games", 0)
win_rate = (wins / total * 100) if total > 0 else 0

await message.answer(
f"📊 Your Statistics:\n\n"
f"Total Games: {total}\n"
f"Wins: {wins}\n"
f"Win Rate: {win_rate:.1f}%"
)
else:
await message.answer("❌ Failed to load statistics.")

Step 2: Add Backend Endpoint

Edit backend/new.py:

@app.post("/get_stats")
async def get_stats(request: StatsRequest):
# Verify signature
if not verify_signature(request.dict(), request.signature):
raise HTTPException(status_code=401, detail="Invalid signature")

# Load stats from JSON
with open("data/stats.json", "r") as f:
stats = json.load(f)

user_stats = stats.get(request.username, {"wins": 0, "total_games": 0})

return user_stats

Step 3: Update BotFather

Message @BotFather:

/setcommands

start - Start the bot
stats - View your statistics
help - Get help

Add a New Payment Tier​

Example: Add 500 stars option

Edit frontend/keyboard/start.py:

keyboard_stars = InlineKeyboardMarkup(
inline_keyboard=[
# ... existing options ...
[
InlineKeyboardButton(text="500 ⭐", callback_data="star500"),
],
# ... rest of options ...
]
)

Edit frontend/routers/private_user.py:

@start_router.callback_query(F.data == "star500")
async def send_invoice_500(callback: types.CallbackQuery):
# 500 stars for 667 stars (25% fee)
prices = [LabeledPrice(label="500 ⭐", amount=667)]

pay_kb = InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text="Pay 667 ⭐", pay=True)]]
)

await callback.message.answer_invoice(
title="❖ Telegram Stars",
description="Your account will be credited with 500 stars for 667 stars.",
payload="topup_667",
provider_token="",
prices=prices,
currency="XTR",
reply_markup=pay_kb
)

await callback.answer()
await callback.message.delete()

Add a New Game Mode​

Example: Add a "Target Number" game

Step 1: Create FSM State

Edit frontend/routers/private_user.py:

class TargetStates(StatesGroup):
waiting_for_bet = State()
waiting_for_target = State()

Step 2: Add Game Handler

@game_router.message(F.text == "Target 🎯")
async def play_target(message: types.Message, state: FSMContext):
await show_gambling_reminder(message)
await message.answer("🎯 Target Number Game!\n\nEnter your bet amount:")
await state.set_state(TargetStates.waiting_for_bet)

@game_router.message(TargetStates.waiting_for_bet)
async def target_bet(message: types.Message, state: FSMContext):
bet = int(message.text)

if bet < 10:
await message.answer("❌ Minimum bet is 10 stars.")
return

await state.update_data(bet=bet)
await message.answer("Choose a target number (1-6):")
await state.set_state(TargetStates.waiting_for_target)

@game_router.message(TargetStates.waiting_for_target)
async def target_number(message: types.Message, state: FSMContext):
target = int(message.text)

if target < 1 or target > 6:
await message.answer("❌ Target must be between 1 and 6.")
return

# Get bet from state
data = await state.get_data()
bet = data["bet"]

# Call backend to start target game
user_id = message.from_user.id
username = message.from_user.username or f"user_{user_id}"

game_data = {
"username": username,
"bet": bet,
"target": target,
"game_type": "target"
}

response = await send_to_backend(
"http://localhost:8080/start/target_game",
game_data
)

if response:
await message.answer(
f"🎲 Rolling...\n\n"
f"Target: {target}\n"
f"Result: {response['roll']}\n\n"
f"{response['message']}"
)
else:
await message.answer("❌ Failed to start game.")

await state.clear()

Step 3: Add Backend Endpoint

Edit backend/new.py:

@app.post("/start/target_game")
async def start_target_game(request: TargetGameRequest):
if not verify_signature(request.dict(), request.signature):
raise HTTPException(status_code=401, detail="Invalid signature")

# Roll dice
roll = random.randint(1, 6)

# Check if user hit target
if roll == request.target:
# Win: 5x bet
winnings = request.bet * 5
# Update balance...
return {
"status": "win",
"roll": roll,
"message": f"🎉 You won {winnings} stars!",
"winnings": winnings
}
else:
# Loss
return {
"status": "loss",
"roll": roll,
"message": f"💔 You lost {request.bet} stars.",
"loss": request.bet
}

Testing​

Unit Tests (Python)​

Create tests/test_signature.py:

import pytest
import hmac
import hashlib
import json

def create_signature(data, secret_key):
data_str = json.dumps(data, sort_keys=True, separators=(',', ':'))
return hmac.new(
secret_key.encode(),
data_str.encode(),
hashlib.sha256
).hexdigest()

def test_signature_generation():
data = {"username": "test", "bet": 10}
secret = "test-key"

sig1 = create_signature(data, secret)
sig2 = create_signature(data, secret)

assert sig1 == sig2

def test_signature_order_independence():
data1 = {"bet": 10, "username": "test"}
data2 = {"username": "test", "bet": 10}
secret = "test-key"

sig1 = create_signature(data1, secret)
sig2 = create_signature(data2, secret)

assert sig1 == sig2 # Order shouldn't matter

Run tests:

pytest tests/

Integration Tests​

Create tests/test_api.py:

import requests
import pytest

BASE_URL = "http://localhost:8080"

def test_register_user():
# Assumes backend is running
data = {
"username": "testuser",
"id": 999999
}

# Create signature...
signature = create_signature(data, SECRET_KEY)

response = requests.post(
f"{BASE_URL}/register",
json=data,
headers={"X-Signature": signature}
)

assert response.status_code == 200
assert response.json()["status"] == "success"

Manual Testing​

Create scripts/test_bot.py:

#!/usr/bin/env python3
"""Manual bot testing script"""

import asyncio
from aiogram import Bot
from dotenv import load_dotenv
import os

load_dotenv()
TOKEN = os.getenv("TOKEN")

async def test_bot():
bot = Bot(TOKEN)

# Test 1: Get bot info
me = await bot.get_me()
print(f"✅ Bot: @{me.username}")

# Test 2: Get updates
updates = await bot.get_updates()
print(f"✅ Updates: {len(updates)}")

await bot.session.close()

if __name__ == "__main__":
asyncio.run(test_bot())

Code Style​

Python (PEP 8)​

Use Black formatter:

pip install black
black frontend/ backend/

Linting with flake8:

pip install flake8
flake8 frontend/ backend/

Example good code:

async def process_bet(message: types.Message, state: FSMContext):
"""
Process user's bet amount.

Args:
message: Incoming message with bet amount
state: FSM context

Returns:
None
"""
bet_amount = message.text

# Validate input
if not bet_amount.isdigit():
await message.answer("❌ Please enter a valid number.")
return

bet = int(bet_amount)

# Check minimum
if bet < 10:
await message.answer("❌ Minimum bet is 10 stars.")
return

# Process bet
await state.update_data(bet=bet)
...

Go (gofmt)​

cd backend/redis
go fmt ./...
goimports -w .

Comments​

# ✅ Good: Explain WHY
# We use sorted JSON to ensure signature consistency across requests
data_str = json.dumps(data, sort_keys=True, separators=(',', ':'))

# ❌ Bad: Explain WHAT (code is self-explanatory)
# Create JSON string
data_str = json.dumps(data)

Git Workflow​

Branch Strategy​

  • main - Production-ready code
  • dev - Active development
  • feature/* - New features
  • bugfix/* - Bug fixes

Creating a Feature​

# Create feature branch from dev
git checkout dev
git pull origin dev
git checkout -b feature/add-stats-command

# Make changes...
git add .
git commit -m "feat: add /stats command for user statistics"

# Push to remote
git push origin feature/add-stats-command

Commit Messages​

Use Conventional Commits:

feat: add new feature
fix: bug fix
docs: documentation changes
style: code style changes
refactor: code refactoring
test: add tests
chore: build/config changes

Examples:

git commit -m "feat: add target number game mode"
git commit -m "fix: resolve signature verification issue"
git commit -m "docs: update API documentation for new endpoints"
git commit -m "refactor: extract signature logic to helper function"

Pull Request Process​

  1. Create PR from feature branch to dev
  2. Write description explaining changes
  3. Link issues if applicable
  4. Request review
  5. Address feedback
  6. Merge when approved

Common Development Tasks​

Add New Backend Endpoint​

# 1. Define request model
class MyRequest(BaseModel):
username: str
data: dict
signature: str

# 2. Create endpoint
@app.post("/my_endpoint")
async def my_endpoint(request: MyRequest):
# Verify signature
if not verify_signature(request.dict(), request.signature):
raise HTTPException(status_code=401)

# Rate limiting
if not check_time_seciruty(request.username):
raise HTTPException(status_code=429)

# Process request
result = process_data(request.data)

return {"status": "success", "result": result}

Add FSM State Flow​

# 1. Define states
class MyStates(StatesGroup):
step1 = State()
step2 = State()

# 2. Start flow
@router.message(F.text == "Start Flow")
async def start_flow(message: types.Message, state: FSMContext):
await message.answer("Step 1: Enter name")
await state.set_state(MyStates.step1)

# 3. Handle step 1
@router.message(MyStates.step1)
async def handle_step1(message: types.Message, state: FSMContext):
name = message.text
await state.update_data(name=name)

await message.answer("Step 2: Enter age")
await state.set_state(MyStates.step2)

# 4. Handle step 2
@router.message(MyStates.step2)
async def handle_step2(message: types.Message, state: FSMContext):
age = message.text
data = await state.get_data()

await message.answer(f"Name: {data['name']}, Age: {age}")
await state.clear()

Debug HMAC Signatures​

# Enable debug logging
import logging
logging.basicConfig(level=logging.DEBUG)

def send_to_backend(url, data):
data_str = json.dumps(data, sort_keys=True, separators=(',', ':'))
print(f"DEBUG: Data string: {data_str}")

signature = hmac.new(
SECRET_KEY.encode(),
data_str.encode(),
hashlib.sha256
).hexdigest()
print(f"DEBUG: Signature: {signature}")

# ... rest of function

Add Logging​

import logging

# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Use in code
logger.info(f"User {username} registered successfully")
logger.warning(f"Failed bet attempt: {bet_amount}")
logger.error(f"Backend connection failed: {e}")

Resources​


Last Updated: January 2025