389 lines
14 KiB
Python
389 lines
14 KiB
Python
import discord
|
|
import asyncio
|
|
from discord.ext import commands
|
|
from typing import Set, Dict, List, Optional
|
|
from datetime import datetime
|
|
|
|
from repositories.noi_tu import NoiTuRepository
|
|
from repositories.score import ScoreRepository
|
|
from repositories.config import ConfigRepository
|
|
from repositories.feature_toggle import FeatureToggleRepository
|
|
|
|
# Game state class
|
|
class NoiTuGame:
|
|
def __init__(self):
|
|
self.is_active = False
|
|
self.current_word = ""
|
|
self.used_words: Set[str] = set()
|
|
self.last_player_id = None
|
|
self.last_player_name = None
|
|
self.last_message_time = None
|
|
self.timeout_task = None
|
|
self.channel = None
|
|
self.timer_message = None
|
|
self.timer_task = None
|
|
self.start_time = None
|
|
self.lock = asyncio.Lock()
|
|
|
|
class NoiTuCog(commands.Cog):
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
self.noi_tu_repo = NoiTuRepository()
|
|
self.score_repo = ScoreRepository()
|
|
self.config_repo = ConfigRepository()
|
|
self.feature_repo = FeatureToggleRepository()
|
|
self.games: Dict[int, NoiTuGame] = {}
|
|
|
|
def get_game_for_channel(self, channel_id: int) -> NoiTuGame:
|
|
if channel_id not in self.games:
|
|
self.games[channel_id] = NoiTuGame()
|
|
return self.games[channel_id]
|
|
|
|
async def is_enabled(self, guild_id: int) -> bool:
|
|
return await self.feature_repo.get(guild_id, "noi_tu")
|
|
|
|
async def get_allowed_channel_ids(self, guild_id: int) -> List[int]:
|
|
config = await self.config_repo.get(guild_id, "CHANNEL_NOI_TU_IDS", "")
|
|
ids = []
|
|
if config:
|
|
for x in config.split(','):
|
|
try:
|
|
ids.append(int(x.strip()))
|
|
except ValueError:
|
|
pass
|
|
return ids
|
|
|
|
async def get_admin_ids(self, guild_id: int) -> List[int]:
|
|
config = await self.config_repo.get(guild_id, "ADMIN_IDS", "")
|
|
ids = []
|
|
if config:
|
|
for x in config.split(','):
|
|
try:
|
|
ids.append(int(x.strip()))
|
|
except ValueError:
|
|
pass
|
|
return ids
|
|
|
|
async def is_correct_channel(self, ctx) -> bool:
|
|
if not await self.is_enabled(ctx.guild.id):
|
|
return False
|
|
|
|
allowed_ids = await self.get_allowed_channel_ids(ctx.guild.id)
|
|
|
|
# New Rule: If no channels configured, allow ALL channels
|
|
if not allowed_ids:
|
|
return True
|
|
|
|
return ctx.channel.id in allowed_ids
|
|
|
|
async def is_admin(self, ctx) -> bool:
|
|
admin_ids = await self.get_admin_ids(ctx.guild.id)
|
|
return ctx.author.id in admin_ids
|
|
|
|
# Helper functions
|
|
def get_first_word(self, word: str) -> str:
|
|
return word.strip().split()[0] if word else ''
|
|
|
|
def get_last_word(self, word: str) -> str:
|
|
return word.strip().split()[-1] if word else ''
|
|
|
|
def is_valid(self, prev, next: str) -> bool:
|
|
return self.get_last_word(prev) == self.get_first_word(next)
|
|
|
|
def format_time_remaining(self, seconds: int) -> str:
|
|
if seconds <= 0:
|
|
return "⏰ Hết thời gian!"
|
|
return f"⏰ Còn lại: {seconds} giây"
|
|
|
|
async def update_timer_message(self, game: NoiTuGame):
|
|
start_time = game.last_message_time
|
|
if not start_time:
|
|
return
|
|
|
|
for remaining in range(30, -1, -1):
|
|
if not game.is_active or not game.timer_message:
|
|
break
|
|
|
|
try:
|
|
if len(game.timer_message.embeds) == 0:
|
|
break
|
|
|
|
embed = game.timer_message.embeds[0]
|
|
embed.title = self.format_time_remaining(remaining)
|
|
|
|
if remaining <= 5:
|
|
embed.color = discord.Color.red()
|
|
elif remaining <= 10:
|
|
embed.color = discord.Color.orange()
|
|
elif remaining <= 20:
|
|
embed.color = discord.Color.yellow()
|
|
else:
|
|
embed.color = discord.Color.blue()
|
|
|
|
await game.timer_message.edit(embed=embed)
|
|
|
|
if remaining <= 0:
|
|
break
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
except Exception as e:
|
|
print(f"Error updating timer: {e}")
|
|
break
|
|
|
|
async def game_timeout(self, game: NoiTuGame):
|
|
try:
|
|
for i in range(30):
|
|
if not game.is_active:
|
|
return
|
|
await asyncio.sleep(1)
|
|
|
|
if game.is_active and game.last_message_time:
|
|
time_diff = datetime.now() - game.last_message_time
|
|
if time_diff.total_seconds() >= 30:
|
|
embed = discord.Embed(
|
|
title="⏰ Hết thời gian!",
|
|
description=f"Không ai trả lời trong 30 giây.\n"
|
|
f"Từ cuối cùng: **{game.current_word}**\n"
|
|
f"Trò chơi kết thúc!",
|
|
color=discord.Color.orange()
|
|
)
|
|
|
|
if game.last_player_name:
|
|
embed.add_field(
|
|
name="👑 Người chiến thắng",
|
|
value=f"**{game.last_player_name}** - Từ cuối: **{game.current_word}**",
|
|
inline=False
|
|
)
|
|
if len(game.used_words) > 2 and game.channel and game.channel.guild:
|
|
# Update score for the guild
|
|
await self.score_repo.upsert_or_increment_point(game.channel.guild.id, game.last_player_id, game.last_player_name, 1)
|
|
|
|
if game.channel:
|
|
await game.channel.send(embed=embed)
|
|
|
|
self.reset_game(game)
|
|
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
def reset_game(self, game: NoiTuGame):
|
|
game.is_active = False
|
|
game.current_word = ""
|
|
game.used_words.clear()
|
|
game.last_player_id = None
|
|
game.last_player_name = None
|
|
game.channel = None
|
|
game.last_message_time = None
|
|
game.timeout_task = None
|
|
game.timer_message = None
|
|
game.timer_task = None
|
|
game.start_time = None
|
|
|
|
@commands.command(name='start')
|
|
async def start_game(self, ctx):
|
|
if not await self.is_correct_channel(ctx):
|
|
return
|
|
|
|
game = self.get_game_for_channel(ctx.channel.id)
|
|
|
|
if game.is_active:
|
|
await ctx.send("❌ Trò chơi đã đang diễn ra!")
|
|
return
|
|
|
|
start_word = await self.noi_tu_repo.get_random_word()
|
|
if not start_word:
|
|
await ctx.send("❌ Không có từ nào trong cơ sở dữ liệu!")
|
|
return
|
|
|
|
game.is_active = True
|
|
game.current_word = start_word
|
|
game.used_words = {start_word}
|
|
game.last_player_id = None
|
|
game.last_player_name = None
|
|
game.channel = ctx.channel
|
|
game.last_message_time = datetime.now()
|
|
game.start_time = datetime.now()
|
|
|
|
game.timeout_task = asyncio.create_task(self.game_timeout(game))
|
|
|
|
embed = discord.Embed(
|
|
title="🎮 Trò chơi Nối Từ đã bắt đầu!",
|
|
description=f"Từ đầu tiên: **{start_word}**\n\n"
|
|
f"📝 **Luật chơi:**\n"
|
|
f"• Mỗi từ gồm 2 từ ghép tiếng Việt (VD: 'âm cao', 'cao độ')\n"
|
|
f"• Từ đầu của từ mới phải trùng với từ cuối của từ trước\n"
|
|
f"• Không được lặp lại từ đã dùng\n"
|
|
f"• Thời gian trả lời tối đa: 30 giây\n\n"
|
|
f"⏰ Thời gian bắt đầu: {datetime.now().strftime('%H:%M:%S')}",
|
|
color=discord.Color.green()
|
|
)
|
|
|
|
await ctx.send(embed=embed)
|
|
|
|
@commands.command(name='end')
|
|
async def end_game(self, ctx):
|
|
if not await self.is_correct_channel(ctx):
|
|
return
|
|
|
|
game = self.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
|
|
|
|
if game.timeout_task:
|
|
game.timeout_task.cancel()
|
|
if game.timer_task:
|
|
game.timer_task.cancel()
|
|
|
|
game_duration = ""
|
|
if game.start_time:
|
|
duration = datetime.now() - game.start_time
|
|
minutes = int(duration.total_seconds() // 60)
|
|
seconds = int(duration.total_seconds() % 60)
|
|
game_duration = f"{minutes} phút {seconds} giây"
|
|
|
|
embed = discord.Embed(
|
|
title="🏁 Trò chơi Nối Từ đã kết thúc!",
|
|
description=f"📊 **Thống kê:**\n"
|
|
f"• Số từ đã sử dụng: {len(game.used_words)}\n"
|
|
f"• Từ cuối cùng: {game.current_word if game.current_word else 'N/A'}\n"
|
|
f"• Thời gian chơi: {game_duration}",
|
|
color=discord.Color.red()
|
|
)
|
|
|
|
if game.last_player_name:
|
|
embed.add_field(
|
|
name="👑 Người chiến thắng",
|
|
value=f"**{game.last_player_name}** - Từ cuối: **{game.current_word}**",
|
|
inline=False
|
|
)
|
|
|
|
await ctx.send(embed=embed)
|
|
self.reset_game(game)
|
|
|
|
@commands.command(name='add')
|
|
async def add_word(self, ctx, *, word: str):
|
|
if not await self.is_correct_channel(ctx):
|
|
return
|
|
|
|
if not await self.is_admin(ctx):
|
|
await ctx.send("❌ Chỉ admin mới có thể thêm từ!")
|
|
return
|
|
|
|
if not await self.noi_tu_repo.is_valid_word(word):
|
|
await ctx.send("❌ Từ phải có đúng 2 từ ghép!")
|
|
return
|
|
|
|
success = await self.noi_tu_repo.add(word)
|
|
if success:
|
|
embed = discord.Embed(
|
|
title="✅ Thêm từ thành công!",
|
|
description=f"Từ: **{word}**",
|
|
color=discord.Color.green()
|
|
)
|
|
await ctx.send(embed=embed)
|
|
else:
|
|
if await self.noi_tu_repo.is_exist(word):
|
|
await ctx.send(f"❌ Từ '{word}' đã tồn tại trong cơ sở dữ liệu!")
|
|
else:
|
|
await ctx.send("❌ Có lỗi xảy ra khi thêm từ!")
|
|
|
|
@commands.command(name='remove')
|
|
async def remove_word(self, ctx, *, word: str):
|
|
if not await self.is_correct_channel(ctx):
|
|
return
|
|
|
|
if not await self.is_admin(ctx):
|
|
await ctx.send("❌ Chỉ admin mới có thể xóa từ!")
|
|
return
|
|
|
|
if not await self.noi_tu_repo.is_exist(word):
|
|
await ctx.send(f"❌ Từ '{word}' không tồn tại trong cơ sở dữ liệu!")
|
|
return
|
|
|
|
success = await self.noi_tu_repo.remove(word)
|
|
if success:
|
|
embed = discord.Embed(
|
|
title="✅ Xóa từ thành công!",
|
|
description=f"Đã xóa từ: **{word}**",
|
|
color=discord.Color.green()
|
|
)
|
|
await ctx.send(embed=embed)
|
|
else:
|
|
await ctx.send("❌ Có lỗi xảy ra khi xóa từ!")
|
|
|
|
@commands.Cog.listener()
|
|
async def on_message(self, message):
|
|
if message.author.bot or not message.guild:
|
|
return
|
|
|
|
# Check if enabled for this guild
|
|
if not await self.is_enabled(message.guild.id):
|
|
return
|
|
|
|
# Check channel
|
|
allowed_ids = await self.get_allowed_channel_ids(message.guild.id)
|
|
if message.channel.id not in allowed_ids:
|
|
return
|
|
|
|
game = self.get_game_for_channel(message.channel.id)
|
|
|
|
if not game.is_active:
|
|
return
|
|
|
|
word = message.content.strip().lower()
|
|
if len(word.split()) != 2:
|
|
return
|
|
|
|
if not await self.noi_tu_repo.is_valid_word(word):
|
|
return
|
|
|
|
if game.last_player_id == message.author.id:
|
|
return
|
|
|
|
if word in game.used_words:
|
|
await message.add_reaction('❌')
|
|
await message.channel.send(f"❌ Từ '{word}' đã được sử dụng!")
|
|
return
|
|
|
|
if game.current_word:
|
|
if not self.is_valid(game.current_word, word):
|
|
return
|
|
|
|
if not await self.noi_tu_repo.is_exist(word):
|
|
await message.add_reaction('❌')
|
|
return
|
|
|
|
async with game.lock:
|
|
if not self.is_valid(game.current_word, word):
|
|
return
|
|
|
|
await message.add_reaction('✅')
|
|
|
|
game.current_word = word
|
|
game.used_words.add(word)
|
|
game.last_player_id = message.author.id
|
|
game.last_player_name = message.author.display_name
|
|
game.last_message_time = datetime.now()
|
|
|
|
if game.timeout_task:
|
|
game.timeout_task.cancel()
|
|
game.timeout_task = asyncio.create_task(self.game_timeout(game))
|
|
|
|
if game.timer_task:
|
|
game.timer_task.cancel()
|
|
|
|
next_hint = self.get_last_word(word).upper()
|
|
embed = discord.Embed(
|
|
title="⏰ Còn lại: 30 giây",
|
|
color=discord.Color.blue()
|
|
)
|
|
|
|
game.timer_message = await message.channel.send(embed=embed)
|
|
game.timer_task = asyncio.create_task(self.update_timer_message(game))
|
|
|
|
async def setup(bot):
|
|
await bot.add_cog(NoiTuCog(bot))
|