From 670c1249435becad44409756e7b5be7d99ef07fa Mon Sep 17 00:00:00 2001 From: virtus Date: Wed, 10 Sep 2025 10:07:27 +0700 Subject: [PATCH] allow game noi_tu for multiple server/channel --- CONFIG_EXAMPLE.md | 48 ++++++++++++++++++ README_MULTI_CHANNEL.md | 108 ++++++++++++++++++++++++++++++++++++++++ apps/noi_tu.py | 37 ++++++++++---- core/bot.py | 27 ++++++++-- 4 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 CONFIG_EXAMPLE.md create mode 100644 README_MULTI_CHANNEL.md diff --git a/CONFIG_EXAMPLE.md b/CONFIG_EXAMPLE.md new file mode 100644 index 0000000..289f9fa --- /dev/null +++ b/CONFIG_EXAMPLE.md @@ -0,0 +1,48 @@ +# Ví dụ cấu hình .env + +## Format cơ bản + +```bash +# Bot Token +BOT_TOKEN=your_bot_token_here + +# Database Configuration +POSTGRES_URL=postgresql+asyncpg://user:password@localhost:5432/database_name + +# Channel IDs +CHANNEL_HOME_DEBT_ID=1234567890123456789 + +# Game Nối Từ - Hỗ trợ nhiều channel với dấu phẩy +CHANNEL_NOI_TU_ID=1383424686708363336,9876543210987654321,5555555555555555555 +``` + +## Các ví dụ khác nhau + +### 1 channel +```bash +CHANNEL_NOI_TU_ID=1383424686708363336 +``` + +### 2 channel +```bash +CHANNEL_NOI_TU_ID=1383424686708363336,9876543210987654321 +``` + +### 3+ channel +```bash +CHANNEL_NOI_TU_ID=1383424686708363336,9876543210987654321,5555555555555555555 +``` + +## Lưu ý quan trọng + +- ✅ **Đúng**: `CHANNEL_NOI_TU_ID=123,456,789` +- ❌ **Sai**: `CHANNEL_NOI_TU_ID=123, 456, 789` (có khoảng trắng) +- ❌ **Sai**: `CHANNEL_NOI_TU_ID="123,456,789"` (có dấu ngoặc kép) +- ❌ **Sai**: `CHANNEL_NOI_TU_ID=123,abc,789` (có ký tự không phải số) + +## Cách test + +1. Cập nhật file `.env` với format mới +2. Restart bot +3. Kiểm tra log để xem bot có parse đúng channel IDs không +4. Test `!start` trong từng channel để đảm bảo hoạt động diff --git a/README_MULTI_CHANNEL.md b/README_MULTI_CHANNEL.md new file mode 100644 index 0000000..cf97ae3 --- /dev/null +++ b/README_MULTI_CHANNEL.md @@ -0,0 +1,108 @@ +# Hướng dẫn sử dụng Bot với nhiều Channel + +## Tổng quan + +Bot đã được refactor để hỗ trợ chạy game nối từ trên nhiều channel đồng thời với cùng một bot token. + +## Cấu hình Environment Variables + +### Format đơn giản với dấu phẩy + +```bash +# Nhiều channel, phân cách bằng dấu phẩy (không có khoảng trắng) +CHANNEL_NOI_TU_ID=1383424686708363336,9876543210987654321,5555555555555555555 +``` + +### Ví dụ thực tế + +```bash +# 1 channel +CHANNEL_NOI_TU_ID=1383424686708363336 + +# 2 channel +CHANNEL_NOI_TU_ID=1383424686708363336,9876543210987654321 + +# 3 channel +CHANNEL_NOI_TU_ID=1383424686708363336,9876543210987654321,5555555555555555555 +``` + +### Lưu ý +- Không có khoảng trắng xung quanh dấu phẩy +- Mỗi ID phải là số nguyên hợp lệ +- Bot sẽ tự động parse và hỗ trợ tất cả channel trong danh sách + +## Cách hoạt động + +### Game State riêng biệt +- Mỗi channel có game state hoàn toàn độc lập +- Channel A có thể đang chơi game với từ "âm cao" → "cao độ" +- Channel B có thể đang chơi game khác với từ "mặt trời" → "trời mưa" +- Không có xung đột giữa các game + +### Database chung +- Tất cả channel sử dụng chung database PostgreSQL +- Từ điển nối từ được chia sẻ giữa các channel +- Admin có thể thêm/xóa từ từ bất kỳ channel nào + +## Lệnh hỗ trợ + +### Lệnh cơ bản +- `!start` - Bắt đầu game nối từ +- `!end` - Kết thúc game nối từ + +### Lệnh admin (chỉ admin server) +- `!add ` - Thêm từ mới vào database +- `!remove ` - Xóa từ khỏi database + +### Lệnh khác +- `/help` - Hiển thị danh sách lệnh cho channel hiện tại + +## Ví dụ sử dụng + +### Server A (Channel #game-1) +``` +User: !start +Bot: 🎮 Trò chơi Nối Từ đã bắt đầu! + Từ đầu tiên: âm cao + +User: cao độ +Bot: ✅ + +User: độ cao +Bot: ✅ +``` + +### Server B (Channel #game-2) - Đồng thời +``` +User: !start +Bot: 🎮 Trò chơi Nối Từ đã bắt đầu! + Từ đầu tiên: mặt trời + +User: trời mưa +Bot: ✅ + +User: mưa gió +Bot: ✅ +``` + +## Lưu ý quan trọng + +1. **Bot Token**: Chỉ cần 1 bot token duy nhất +2. **Database**: Tất cả channel dùng chung database +3. **Game State**: Mỗi channel có game state riêng biệt +4. **Admin**: Admin của server có thể quản lý từ điển +5. **Timeout**: Mỗi game có timeout 30 giây độc lập + +## Troubleshooting + +### Bot không phản hồi trong channel +- Kiểm tra channel ID có trong `CHANNEL_NOI_TU_IDS` không +- Kiểm tra bot có quyền gửi tin nhắn trong channel không + +### Game không bắt đầu được +- Kiểm tra xem đã có game đang chạy trong channel đó chưa +- Kiểm tra database có từ nào không + +### Lỗi database +- Kiểm tra kết nối PostgreSQL +- Kiểm tra biến môi trường database diff --git a/apps/noi_tu.py b/apps/noi_tu.py index e9e393b..7e878c8 100644 --- a/apps/noi_tu.py +++ b/apps/noi_tu.py @@ -1,7 +1,7 @@ import discord import asyncio -from core.bot import bot, CHANNEL_NOI_TU_ID -from typing import Set +from core.bot import bot, CHANNEL_NOI_TU_IDS +from typing import Set, Dict from datetime import datetime from apps.score import incr @@ -32,8 +32,14 @@ class NoiTuGame: self.start_time = None # Thời gian bắt đầu game self.lock = asyncio.Lock() -# Khởi tạo game state -game = NoiTuGame() +# Dictionary để lưu game state cho từng channel +games: Dict[int, NoiTuGame] = {} + +def get_game_for_channel(channel_id: int) -> NoiTuGame: + """Lấy game state cho channel cụ thể""" + if channel_id not in games: + games[channel_id] = NoiTuGame() + return games[channel_id] def is_admin(ctx): """Kiểm tra xem user có phải là admin không""" @@ -41,7 +47,7 @@ def is_admin(ctx): def is_correct_channel(ctx): """Kiểm tra xem command có được thực hiện trong đúng channel không""" - return ctx.channel.id == CHANNEL_NOI_TU_ID + return ctx.channel.id in CHANNEL_NOI_TU_IDS def get_first_word(word: str) -> str: return word.strip().split()[0] if word else '' @@ -58,7 +64,7 @@ def format_time_remaining(seconds: int) -> str: return "⏰ Hết thời gian!" return f"⏰ Còn lại: {seconds} giây" -async def update_timer_message(): +async def update_timer_message(game: NoiTuGame): """Cập nhật tin nhắn thời gian mỗi 1 giây""" start_time = game.last_message_time if not start_time: @@ -101,6 +107,9 @@ async def start_game(ctx): if not is_correct_channel(ctx): return + # Lấy game state cho channel này + game = get_game_for_channel(ctx.channel.id) + if game.is_active: await ctx.send("❌ Trò chơi đã đang diễn ra!") return @@ -125,7 +134,7 @@ async def start_game(ctx): game.start_time = datetime.now() # Tạo timeout task - game.timeout_task = asyncio.create_task(game_timeout()) + game.timeout_task = asyncio.create_task(game_timeout(game)) embed = discord.Embed( title="🎮 Trò chơi Nối Từ đã bắt đầu!", @@ -147,6 +156,9 @@ async def end_game(ctx): if not is_correct_channel(ctx): return + # Lấy game state cho channel này + game = get_game_for_channel(ctx.channel.id) + if not game.is_active: await ctx.send("❌ Không có trò chơi nào đang diễn ra!") return @@ -257,7 +269,7 @@ async def remove_word(ctx, *, word: str): else: await ctx.send("❌ Có lỗi xảy ra khi xóa từ!") -async def game_timeout(): +async def game_timeout(game: NoiTuGame): """Xử lý timeout của game""" try: # Đợi đúng 30 giây @@ -316,9 +328,12 @@ async def handle_game_message(message): return # Chỉ xử lý trong channel được chỉ định - if message.channel.id != CHANNEL_NOI_TU_ID: + if message.channel.id not in CHANNEL_NOI_TU_IDS: return + # Lấy game state cho channel này + game = get_game_for_channel(message.channel.id) + # Nếu game không active, bỏ qua if not game.is_active: return @@ -372,7 +387,7 @@ async def handle_game_message(message): # Reset timeout if game.timeout_task: game.timeout_task.cancel() - game.timeout_task = asyncio.create_task(game_timeout()) + game.timeout_task = asyncio.create_task(game_timeout(game)) # Dừng timer task cũ nếu có if game.timer_task: @@ -389,5 +404,5 @@ async def handle_game_message(message): game.timer_message = await message.channel.send(embed=embed) # Bắt đầu task cập nhật thời gian - game.timer_task = asyncio.create_task(update_timer_message()) + game.timer_task = asyncio.create_task(update_timer_message(game)) diff --git a/core/bot.py b/core/bot.py index c79acb5..8fe2c9b 100644 --- a/core/bot.py +++ b/core/bot.py @@ -21,7 +21,19 @@ home_debt_repo = HomeDebtRepository() score_repo = ScoreRepository() CHANNEL_HOME_DEBT_ID = int(os.getenv('CHANNEL_HOME_DEBT_ID', 0)) -CHANNEL_NOI_TU_ID = int(os.getenv('CHANNEL_NOI_TU_ID', 0)) + +# Hỗ trợ nhiều channel cho game nối từ với format: ID1,ID2,ID3 +CHANNEL_NOI_TU_IDS = [] +channel_noi_tu_env = os.getenv('CHANNEL_NOI_TU_ID', '') +if channel_noi_tu_env: + for channel_id in channel_noi_tu_env.split(','): + try: + CHANNEL_NOI_TU_IDS.append(int(channel_id.strip())) + except ValueError: + print(f"Invalid channel ID: {channel_id}") + +# Giữ lại CHANNEL_NOI_TU_ID cho backward compatibility (lấy ID đầu tiên) +CHANNEL_NOI_TU_ID = CHANNEL_NOI_TU_IDS[0] if CHANNEL_NOI_TU_IDS else 0 @bot.tree.command(name='help', description='Show help') @@ -37,12 +49,17 @@ async def help(interaction: discord.Interaction): "!hdtra ", "!hdvay " ], - CHANNEL_NOI_TU_ID: [ - "!start", - "!end" - ], } + # Thêm help cho tất cả channel nối từ + for channel_id in CHANNEL_NOI_TU_IDS: + help_commands[channel_id] = [ + "!start", + "!end", + "!add (admin only)", + "!remove (admin only)" + ] + # Kiểm tra xem channel có trong danh sách không if channel.id in help_commands: embed = discord.Embed(