Skip to content

Implement UNO game bot with Telegram integration - #45

Open
piojomd86-jpg wants to merge 1 commit into
aeither:mainfrom
piojomd86-jpg:patch-1
Open

piojomd86-jpg wants to merge 1 commit into
aeither:mainfrom
piojomd86-jpg:patch-1

Conversation

@piojomd86-jpg

@piojomd86-jpg piojomd86-jpg commented Mar 29, 2026 •

Copy link
Copy Markdown

Added a Telegram bot implementation for a UNO game with commands for creating, joining, and playing the game.

Summary by CodeRabbit

Release Notes

  • New Features
    • Telegram UNO game bot with full multiplayer gameplay support
    • Game commands: create games, join games, start matches, check game status, play cards, and draw cards
    • Special card mechanics: stacking draw cards, reversing turn order, and skipping players
    • Turn-based gameplay with automatic win detection

Added a Telegram bot implementation for a UNO game with commands for creating, joining, and playing the game.
@coderabbitai

coderabbitai Bot commented Mar 29, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The railway.toml configuration file was replaced with a complete Python implementation of a Telegram UNO game bot, including game model classes, in-memory state management, and asynchronous command handlers for game lifecycle operations.

Changes

Cohort / File(s) Summary
Telegram UNO Game Bot Implementation
railway.toml
Replaced configuration file with full bot implementation: added Carta and Juego model classes for card/game management, global games dict and bot TOKEN, and seven async command handlers (/start, /crear_juego, /unirse, /empezar, /estado, /jugar, /robar) with deck creation, turn management, special card effects (+2, REVERSA, SALTA), play validation, and win detection logic. Includes async main() for python-telegram-bot Application setup and polling.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Bot as Telegram Bot
    participant Handler as Command Handler
    participant GameState as Games State

    User->>Bot: /start
    Bot->>Handler: start()
    Handler->>User: Welcome message

    User->>Bot: /crear_juego
    Bot->>Handler: crear_juego()
    Handler->>GameState: Initialize Juego instance
    GameState-->>Handler: Game created
    Handler->>User: Game created, share ID

    User2->>Bot: /unirse <game_id>
    Bot->>Handler: unirse()
    Handler->>GameState: Add player to game
    GameState-->>Handler: Player added
    Handler->>User2: Joined game

    User->>Bot: /empezar
    Bot->>Handler: empezar()
    Handler->>GameState: iniciar_juego(), repartir_cartas()
    GameState-->>Handler: Game started
    Handler->>User: Game started, your turn

    loop Game Loop
        User->>Bot: /jugar <card> or /robar
        Bot->>Handler: jugar() or robar()
        Handler->>GameState: Validate & update game state
        GameState-->>Handler: Move completed
        Handler->>GameState: pasar_turno()
        GameState-->>Handler: Next player's turn
        Handler->>User: State update
    end

    GameState-->>Handler: verificar_ganador()
    Handler->>User: Winner announced
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A bot is born to shuffle and deal,
With cards and turns of UNO's appeal,
Commands stack high, the logic runs deep,
Special effects make players leap,
Hop through the game, from start until win! 🎮✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement UNO game bot with Telegram integration' directly and accurately describes the main change: adding a complete Telegram UNO game bot implementation replacing the previous deployment configuration file.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@railway.toml`:
- Around line 345-349: The current lookup sets game_id by finding the first
games[gid] where any(j["user_id"] == user_id for j in games[gid].jugadores),
which ignores the chat and can pick the wrong game if the same user is in
multiple chats; change the search to match both update.effective_chat.id (or
chat_id) and user_id (e.g., require games[gid].chat_id ==
update.effective_chat.id AND any(j["user_id"] == user_id ...)) so the handler
resolves a game by chat+player; apply the same fix in the other occurrence
referenced (the /jugar and /robar handlers at the second block).
- Line 10: Remove the hard-coded Telegram bot token from railway.toml and
rotate/revoke the exposed token immediately; instead add a reference to the
environment variable TELEGRAM_BOT_TOKEN (used in main.py) in the deployment
config so the runtime reads os.getenv('TELEGRAM_BOT_TOKEN'); update any
CI/deploy env settings to store the new secret and ensure no literal token
remains in the repo or configs.
- Around line 315-316: Handlers that call obtener_estado(), jugar_carta(), and
robar must first verify the match has started; currently /estado, /jugar and
/robar run against juego = games[game_id] even in the lobby and crash because
there is no discard pile. Update each handler that uses juego (the places
invoking obtener_estado(), jugar_carta(), or robar) to check a single game-start
flag or state (e.g., juego.partida_empezada or juego.estado === 'in_progress')
and return an appropriate error/message if the match hasn’t started (allow
/empezar to flip the flag); ensure the same guard is added at the other similar
sites referenced (the other two diffs mentioned).
- Around line 75-95: agregar_jugador currently only blocks full tables and will
append duplicate user_id entries; change agregar_jugador to first check
self.jugadores for an existing entry with the same user_id and return False if
found (use self.jugadores and user_id to locate duplicates) instead of mutating
membership during /empezar, and keep the existing capacity check. Also modify
iniciar_juego to guard against restarts by checking self.iniciado at the top and
returning False if the game is already started; only call repartir_cartas, pop
from self.mazo, append to self.descarte and flip self.iniciado when the game was
not started. Ensure the same duplicate-check and iniciado-guard fixes are
applied to the analogous code at the other occurrence mentioned.
- Around line 210-216: The current creation uses a user-scoped key (game_id =
f"{chat_id}_{user_id}") causing multiple lobbies per chat; change the game key
to be chat-scoped (use chat_id or a string derived only from chat_id) when
checking and storing in the games dict so /crear_juego, /unirse, /empezar and
/estado all reference the same chat-level entry; update the creation code that
sets games[game_id] = Juego(...) and any lookups that currently expect
f"{chat_id}_{user_id}" to instead use the chat-scoped key, and ensure the Juego
constructor still receives the creator or owner id if needed (pass user_id
separately rather than in the dict key).
- Around line 1-10: The railway.toml currently contains Python bot code (e.g.,
declarations like TOKEN and games) which breaks the deployment config; move all
Python implementation (imports, TOKEN, games dict and bot handlers) into a
proper .py module (e.g., bot.py or inside the existing main.py) and remove it
from railway.toml, restore railway.toml to valid TOML that declares the project
and start command used by the runtime, and ensure the runtime still starts
main.py (as expected by the Dockerfile) and that TOKEN is read from an
environment variable rather than being hard-coded.
- Around line 291-297: The bot currently attempts to DM each player in the
juego.jugadores loop using context.bot.send_message (used in /empezar and the
turn-notification sites) which will raise "chat not found" for users who haven't
started the bot; wrap those send_message calls in a try/cept that catches
telegram.error.BadRequest (and fall back to a generic Exception), and on failure
send the same content to the group chat (use the game's chat id from juego or
the current context chat) with a mention of the player (username or display name
from jugador) so they still receive their hand/turn notice; update all
occurrences (the mano send in the juego.jugadores loop and the two
turn-notification send_message calls) to use this try/fallback pattern and log
the error for monitoring.
- Around line 4-6: The project imports from python-telegram-bot (e.g., "from
telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup" and uses
Application.run_polling()), but pyproject.toml only includes pytelegrambotapi;
update the pyproject.toml dependencies to include "python-telegram-bot>=20.0"
(and optionally remove "pytelegrambotapi" if you are fully migrating), ensuring
the dependency list matches the imports and runtime usage of Application,
CommandHandler, CallbackQueryHandler, MessageHandler, filters, and ChatAction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e02e289-3905-45ff-82e0-101c8d50d892

📥 Commits

Reviewing files that changed from the base of the PR and between 5cfd740 and 03e1427.

📒 Files selected for processing (1)
  • railway.toml

Comment thread railway.toml
Comment on lines +1 to +10
import os
import json
import random
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes
from telegram.constants import ChatAction

# Variables globales
games = {} # {game_id: game_object}
TOKEN = "8688837364:AAHMpSjqLCy7WLYyaDSY7sxBI2nD7n9ZdFw"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Move this implementation out of railway.toml.

Railway expects this file to be valid TOML. Replacing it with Python makes the deployment config unreadable, and Dockerfile:34-35 still starts main.py, so this bot code is not wired into the actual runtime path. Restore a real railway.toml and move the bot implementation into a .py module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@railway.toml` around lines 1 - 10, The railway.toml currently contains Python
bot code (e.g., declarations like TOKEN and games) which breaks the deployment
config; move all Python implementation (imports, TOKEN, games dict and bot
handlers) into a proper .py module (e.g., bot.py or inside the existing main.py)
and remove it from railway.toml, restore railway.toml to valid TOML that
declares the project and start command used by the runtime, and ensure the
runtime still starts main.py (as expected by the Dockerfile) and that TOKEN is
read from an environment variable rather than being hard-coded.

Comment thread railway.toml
Comment on lines +4 to +6
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes
from telegram.constants import ChatAction

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In the current python-telegram-bot documentation, which PyPI package provides telegram.ext.Application, and is Application.run_polling() a synchronous method or an async coroutine?

💡 Result:

The PyPI package is python-telegram-bot. telegram.ext.Application is provided by this package, as confirmed by the official PyPI page, GitHub repository, and documentation for v22.7 (latest as of 2026-03-16). Application.run_polling is an async coroutine method. Documentation describes it with parameters and usage in async contexts (e.g., await application.run_polling), source code context shows async methods, GitHub discussions confirm it's a coroutine that blocks the event loop via asyncio.run or similar until shutdown signals, and example code uses it directly in main without explicit await but in asyncio-based library.

Citations:


🏁 Script executed:

# Find pyproject.toml and examine its structure
find . -name "pyproject.toml" -type f | head -5

Repository: aeither/telegram-bot-python

Length of output: 87


🏁 Script executed:

# Search for files that might contain the imports mentioned in the review
rg "from telegram import Update" --type python

Repository: aeither/telegram-bot-python

Length of output: 101


🏁 Script executed:

# Look for Application.run_polling usage
rg "Application.run_polling" --type python -B 2 -A 2

Repository: aeither/telegram-bot-python

Length of output: 101


🏁 Script executed:

# Check pyproject.toml to see dependencies
cat ./pyproject.toml

Repository: aeither/telegram-bot-python

Length of output: 497


🏁 Script executed:

# Find Python files in the repo
fd -e py | head -20

Repository: aeither/telegram-bot-python

Length of output: 90


🏁 Script executed:

# Check main.py - see full content and line numbers
cat -n main.py

Repository: aeither/telegram-bot-python

Length of output: 1691


🏁 Script executed:

# Check commands.py - see full content and line numbers
cat -n commands.py

Repository: aeither/telegram-bot-python

Length of output: 544


🏁 Script executed:

# Search for railway.toml
find . -name "railway.toml"

Repository: aeither/telegram-bot-python

Length of output: 85


🏁 Script executed:

# Search for the exact imports mentioned in the review
rg "from telegram import Update, InlineKeyboardButton"

Repository: aeither/telegram-bot-python

Length of output: 155


🏁 Script executed:

# Check if there are files with 400+ lines that might contain lines 424-442
find . -name "*.py" -exec wc -l {} + | sort -rn | head -10

Repository: aeither/telegram-bot-python

Length of output: 115


🏁 Script executed:

# View the entire railway.toml file with line numbers
cat -n railway.toml

Repository: aeither/telegram-bot-python

Length of output: 17754


Add python-telegram-bot to pyproject.toml dependencies

The code imports from telegram.* (lines 4-6 and throughout railway.toml), which requires the python-telegram-bot package. However, pyproject.toml only lists pytelegrambotapi as a dependency. This will cause an ImportError at runtime. Add python-telegram-bot to the dependencies:

dependencies = [
    "python-telegram-bot>=20.0",
    "pytelegrambotapi>=4.30.0,<5",
    "python-dotenv>=1.2.1,<2",
]

Also remove the unused pytelegrambotapi dependency if migrating fully to python-telegram-bot.

Note: The code correctly awaits application.run_polling() at line 438, which is the correct usage since Application.run_polling() is an async coroutine method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@railway.toml` around lines 4 - 6, The project imports from
python-telegram-bot (e.g., "from telegram import Update, InlineKeyboardButton,
InlineKeyboardMarkup" and uses Application.run_polling()), but pyproject.toml
only includes pytelegrambotapi; update the pyproject.toml dependencies to
include "python-telegram-bot>=20.0" (and optionally remove "pytelegrambotapi" if
you are fully migrating), ensuring the dependency list matches the imports and
runtime usage of Application, CommandHandler, CallbackQueryHandler,
MessageHandler, filters, and ChatAction.

Comment thread railway.toml

# Variables globales
games = {} # {game_id: game_object}
TOKEN = "8688837364:AAHMpSjqLCy7WLYyaDSY7sxBI2nD7n9ZdFw"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove and rotate the committed bot token.

This is a live credential in source control. main.py:1-13 already uses os.getenv('TELEGRAM_BOT_TOKEN'); follow that pattern here and revoke the leaked token before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@railway.toml` at line 10, Remove the hard-coded Telegram bot token from
railway.toml and rotate/revoke the exposed token immediately; instead add a
reference to the environment variable TELEGRAM_BOT_TOKEN (used in main.py) in
the deployment config so the runtime reads os.getenv('TELEGRAM_BOT_TOKEN');
update any CI/deploy env settings to store the new secret and ensure no literal
token remains in the repo or configs.

Comment thread railway.toml
Comment on lines +75 to +95
def agregar_jugador(self, user_id, nombre):
"""Agrega un jugador al juego"""
if len(self.jugadores) >= 4:
return False
self.jugadores.append({
"user_id": user_id,
"nombre": nombre,
"mano": []
})
return True

def iniciar_juego(self):
"""Inicia el juego"""
if len(self.jugadores) < 2:
return False

self.repartir_cartas()
# Primera carta del descarte
self.descarte.append(self.mazo.pop())
self.iniciado = True
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't mutate lobby membership in /empezar, and block restarts.

If an existing player calls /empezar while the table still has free seats, agregar_jugador() adds the same user_id again because it only rejects full lobbies. Also, iniciar_juego() never checks self.iniciado, so /empezar can redeal a live match and corrupt hands and turn order.

Also applies to: 277-282

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@railway.toml` around lines 75 - 95, agregar_jugador currently only blocks
full tables and will append duplicate user_id entries; change agregar_jugador to
first check self.jugadores for an existing entry with the same user_id and
return False if found (use self.jugadores and user_id to locate duplicates)
instead of mutating membership during /empezar, and keep the existing capacity
check. Also modify iniciar_juego to guard against restarts by checking
self.iniciado at the top and returning False if the game is already started;
only call repartir_cartas, pop from self.mazo, append to self.descarte and flip
self.iniciado when the game was not started. Ensure the same duplicate-check and
iniciado-guard fixes are applied to the analogous code at the other occurrence
mentioned.

Comment thread railway.toml
Comment on lines +210 to +216
game_id = f"{chat_id}_{user_id}"

if game_id in games:
await update.message.reply_text("❌ Ya hay un juego creado. Usa /unirse para unirte.")
return

games[game_id] = Juego(game_id, chat_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use one active-game key per chat.

/unirse, /empezar, and /estado all resolve “the game in this chat”, but /crear_juego only checks the creator-specific key. A second user can create another lobby in the same chat, after which chat-scoped commands hit whichever dict entry comes first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@railway.toml` around lines 210 - 216, The current creation uses a user-scoped
key (game_id = f"{chat_id}_{user_id}") causing multiple lobbies per chat; change
the game key to be chat-scoped (use chat_id or a string derived only from
chat_id) when checking and storing in the games dict so /crear_juego, /unirse,
/empezar and /estado all reference the same chat-level entry; update the
creation code that sets games[game_id] = Juego(...) and any lookups that
currently expect f"{chat_id}_{user_id}" to instead use the chat-scoped key, and
ensure the Juego constructor still receives the creator or owner id if needed
(pass user_id separately rather than in the dict key).

Comment thread railway.toml
Comment on lines +291 to +297
# Enviar manos privadas
for jugador in juego.jugadores:
mano_texto = "\n".join([f"{i}: {c}" for i, c in enumerate(jugador["mano"])])
await context.bot.send_message(
chat_id=jugador["user_id"],
text=f"Tu mano:\n{mano_texto}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

According to the Telegram Bot API, can a bot send a private message to a user who has not initiated a chat with the bot first?

💡 Result:

No, according to the Telegram Bot API, a bot cannot send a private message to a user who has not initiated a chat with the bot first. Bots can only respond in private chats after the user has sent a message to the bot, establishing the chat. Attempting to send a message without an existing chat results in errors like "Bad Request: chat not found" or "user not found". This limitation is a core design principle of the Bot API to prevent spam, as stated in official documentation and unchanged in recent updates up to Bot API 9.5 (March 2026).

Citations:


🏁 Script executed:

# Check the other locations mentioned (381-385, 416-418)
# First, let's see the full context around those lines
rg -n "send_message" railway.toml | head -20

Repository: aeither/telegram-bot-python

Length of output: 394


🏁 Script executed:

# Get line counts to understand the file better
wc -l railway.toml

Repository: aeither/telegram-bot-python

Length of output: 87


🏁 Script executed:

# Look at lines 381-385 and 416-418 for context
sed -n '375,390p' railway.toml

Repository: aeither/telegram-bot-python

Length of output: 757


🏁 Script executed:

# Also check 416-418
sed -n '410,425p' railway.toml

Repository: aeither/telegram-bot-python

Length of output: 657


🏁 Script executed:

# Search for any try-catch or error handling around send_message calls
rg -B 3 -A 3 "send_message" railway.toml

Repository: aeither/telegram-bot-python

Length of output: 2083


Telegram bots cannot send private messages to users who haven't started a chat with the bot first.

According to the Telegram Bot API, attempting to send a private message to a user who hasn't initiated a chat results in errors like "Bad Request: chat not found". This prevents /empezar from working for first-time players, as it sends private hands to all players (line 294) without verifying they've started the bot. The same issue affects turn notifications (lines 383 and 416) that send private messages to the current player.

Wrap these send_message calls with error handling or implement a fallback mechanism (e.g., send hands in the group chat instead, or collect acknowledgments before sending private messages).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@railway.toml` around lines 291 - 297, The bot currently attempts to DM each
player in the juego.jugadores loop using context.bot.send_message (used in
/empezar and the turn-notification sites) which will raise "chat not found" for
users who haven't started the bot; wrap those send_message calls in a try/cept
that catches telegram.error.BadRequest (and fall back to a generic Exception),
and on failure send the same content to the group chat (use the game's chat id
from juego or the current context chat) with a mention of the player (username
or display name from jugador) so they still receive their hand/turn notice;
update all occurrences (the mano send in the juego.jugadores loop and the two
turn-notification send_message calls) to use this try/fallback pattern and log
the error for monitoring.

Comment thread railway.toml
Comment on lines +315 to +316
juego = games[game_id]
estado = juego.obtener_estado()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard gameplay commands until the match has started.

obtener_estado() and jugar_carta() assume there is already a discard pile, so /estado and /jugar can fail before /empezar. /robar is also callable in the lobby phase, which lets players mutate hands before the game even starts.

Also applies to: 355-356, 404-405

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@railway.toml` around lines 315 - 316, Handlers that call obtener_estado(),
jugar_carta(), and robar must first verify the match has started; currently
/estado, /jugar and /robar run against juego = games[game_id] even in the lobby
and crash because there is no discard pile. Update each handler that uses juego
(the places invoking obtener_estado(), jugar_carta(), or robar) to check a
single game-start flag or state (e.g., juego.partida_empezada or juego.estado
=== 'in_progress') and return an appropriate error/message if the match hasn’t
started (allow /empezar to flip the flag); ensure the same guard is added at the
other similar sites referenced (the other two diffs mentioned).

Comment thread railway.toml

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant