discord.py机器人井字棋指令:等待玩家2加入并设置30秒超时
问题说明
你当前的井字棋指令存在核心逻辑缺失,无法满足需求:
- 指令强制要求传入x、y两个坐标参数,玩家不带参数发起对局、加入对局的入口不存在
- 未实现等待第二玩家加入、30秒超时自动退出的流程
- 原有逻辑存在一处bug:回合提示时误用
ctx.guild.id作为键读取玩家数据,和其他位置用的ctx.channel.id不统一,会导致数据读取失败
实现思路
- 将指令的x、y参数设为可选,区分三种指令触发场景:无参数时处理发起/加入对局逻辑,有参数时处理对局内落子逻辑
- 新增待加入对局的状态存储,记录每个频道的发起玩家、超时监听任务
- 发起对局后启动30秒超时监听,期间有符合要求的玩家加入则取消超时任务,初始化对局数据正式开局;超时未有人加入则清空等待状态,提示发起失败
- 修正原有逻辑的键不统一bug,优化冗余的条件判断写法
修正后完整代码
import asyncio # 提前在Bot类初始化方法中新增待对局存储,和你原有其他bot级变量放在一起即可 # self.pending_ttt = {} # 存储结构: {channel_id: {"host": 发起者id, "timeout_task": 异步任务对象}} # 原有的bot.x、bot.y、bot.boards、bot.players存储保持不变即可 @commands.command(aliases=['ristinolla', 'ttt', 'rn']) async def tictactoe(ctx, x: str = None, y: str = None): lang = await ctx.bot.get_lang(ctx) channel_id = ctx.channel.id # 场景1:当前频道有正在进行的对局,走落子逻辑 if channel_id in ctx.bot.players: # 没传坐标直接提示用法 if not x or not y: await ctx.send(f"Usage:\n{ctx.bot.prefix}tictactoe <top/middle/bottom> <left/middle/right>") return turn = ctx.bot.players[channel_id][2] X = ctx.bot.players[channel_id][0] O = ctx.bot.players[channel_id][1] if ctx.author.id != turn: msg = "❌ **|** Ei ole sinun vuorosi" if lang == "fi" else "❌ **|** It is not your turn" await ctx.send(msg) return if x not in ctx.bot.x or y not in ctx.bot.y: await ctx.send(f"Usage:\n{ctx.bot.prefix}tictactoe <top/middle/bottom> <left/middle/right>") return piece = "X" if turn == X else "O" lauta = ctx.bot.boards[channel_id] x_idx = int(ctx.bot.x[x]) y_idx = int(ctx.bot.y[y]) if lauta[x_idx][y_idx] != " ": msg = "❌ **|** Paikka varattu, kokeile toista paikkaa" if lang == "fi" else "❌ **|** Spot is already taken, try another spot" await ctx.send(msg) return # 执行落子 lauta[x_idx][y_idx] = piece await ctx.send(await ctx.bot.lautavisual(lauta)) # 胜负判定 win_condition = ( lauta[0] == [piece, piece, piece], lauta[1] == [piece, piece, piece], lauta[2] == [piece, piece, piece], lauta[0][0] == piece and lauta[1][1] == piece and lauta[2][2] == piece, lauta[0][2] == piece and lauta[1][1] == piece and lauta[2][0] == piece, lauta[0][0] == piece and lauta[1][0] == piece and lauta[2][0] == piece, lauta[0][1] == piece and lauta[1][1] == piece and lauta[2][1] == piece, lauta[0][2] == piece and lauta[1][2] == piece and lauta[2][2] == piece ) if any(win_condition): msg = f"🎲 **|** {ctx.author.mention} on voittanut!" if lang == "fi" else f"🎲 **|** {ctx.author.mention} has won!" await ctx.send(msg) ctx.bot.boards.pop(channel_id) ctx.bot.players.pop(channel_id) return # 平局判定 full_board = all(cell != " " for row in lauta for cell in row) if full_board: msg = "🎲 **|** Tasapeli!" if lang == "fi" else "🎲 **|** Tie!" await ctx.send(msg) ctx.bot.boards.pop(channel_id) ctx.bot.players.pop(channel_id) return # 切换回合 next_turn = O if turn == X else X ctx.bot.players[channel_id][2] = next_turn msg = f"🎲 **|** <@{next_turn}>, nyt on sinun vuorosi" if lang == "fi" else f"🎲 **|** <@{next_turn}>, it is now your turn" await ctx.send(msg) return # 场景2:当前频道有等待加入的对局 if channel_id in ctx.bot.pending_ttt: pending = ctx.bot.pending_ttt[channel_id] # 发起者自己不能加入 if ctx.author.id == pending["host"]: msg = "❌ **|** Odotetaan toista pelaajaa liittymään" if lang == "fi" else "❌ **|** Waiting for another player to join" await ctx.send(msg) return # 有效玩家加入,取消超时任务,初始化对局 pending["timeout_task"].cancel() ctx.bot.pending_ttt.pop(channel_id) # 初始化3x3空棋盘 ctx.bot.boards[channel_id] = [[" " for _ in range(3)] for _ in range(3)] # 玩家列表结构:[X玩家id, O玩家id, 当前回合id] 发起者默认先手X ctx.bot.players[channel_id] = [pending["host"], ctx.author.id, pending["host"]] msg = f"🎲 **|** Peli alkoi! <@{pending['host']}> on X, <@{ctx.author.id}> on O. <@{pending['host']}> aloittaa." if lang == "fi" else f"🎲 **|** Game started! <@{pending['host']}> is X, <@{ctx.author.id}> is O. <@{pending['host']}> goes first." await ctx.send(msg) await ctx.send(await ctx.bot.lautavisual(ctx.bot.boards[channel_id])) return # 场景3:没有进行中/等待中的对局,玩家不带参数调用则发起对局 if not x and not y: # 超时处理逻辑 async def ttt_timeout(): await asyncio.sleep(30) # 超时后如果等待状态还存在,清空状态并提示 if channel_id in ctx.bot.pending_ttt: ctx.bot.pending_ttt.pop(channel_id) timeout_msg = "⏳ **|** Peliä ei alkanut, aikakatkaisu" if lang == "fi" else "⏳ **|** Game timed out, no player joined" await ctx.send(timeout_msg) # 创建异步超时任务 timeout_task = asyncio.create_task(ttt_timeout()) ctx.bot.pending_ttt[channel_id] = {"host": ctx.author.id, "timeout_task": timeout_task} msg = f"🎲 **|** {ctx.author.mention} aloitti ristinollapelin! Toinen pelaaja voi liittyä kirjoittamalla {ctx.bot.prefix}tictactoe 30 sekunnin sisällä." if lang == "fi" else f"🎲 **|** {ctx.author.mention} started a tictactoe game! Another player can join by typing {ctx.bot.prefix}tictactoe within 30 seconds." await ctx.send(msg) return # 其他异常情况:传了坐标但没有对应对局,提示用法 await ctx.send(f"Usage:\n{ctx.bot.prefix}tictactoe <top/middle/bottom> <left/middle/right>")
注意事项
- 必须在Bot类初始化时加上
self.pending_ttt = {},否则存储待加入对局状态时会报属性不存在的错误 - 原有的坐标映射
bot.x、bot.y不需要修改,保持原有映射规则即可 - 棋盘渲染方法
bot.lautavisual可以直接复用,不需要调整 - 超时任务用
asyncio.create_task创建,玩家加入时主动调用cancel()取消,不会产生残留异步任务
内容的提问来源于stack exchange,提问作者Väinö Alatalo
相关产品推荐
相关产品推荐

