diff --git a/danding_bot/plugins/group_horse_racing/README.md b/danding_bot/plugins/group_horse_racing/README.md index 47df75a..b45b390 100644 --- a/danding_bot/plugins/group_horse_racing/README.md +++ b/danding_bot/plugins/group_horse_racing/README.md @@ -1,215 +1,223 @@ -# Group Horse Racing 插件 - -群赛马插件 - 一个支持群组内多人参与的赛马游戏,集成积分系统和下注功能。 - -## 功能概述 - -这是一个完整的群组赛马游戏系统,支持以下核心功能: - -- **马匹报名**:用户可以报名参加赛马比赛 -- **下注系统**:支持用户对任意参赛马匹进行下注(含自己的马) -- **自动比赛**:基于随机算法的赛马进程模拟 -- **积分集成**:与 danding_points 积分系统无缝集成 -- **自动撤回**:支持配置消息自动撤回时间 -- **多房间支持**:支持群组和私聊两种模式 - -## 命令列表 - -### 基础命令 - -| 命令 | 说明 | 示例 | -|------|------|------| -| `/赛马报名 <马匹名>` | 报名参加赛马比赛。若不输入则复用上次马名或使用昵称 | `/赛马报名 绝地赤兔` | -| `/赛马取消报名` | 取消报名并退还下注(限开始前) | `/赛马取消报名` | -| `/赛马下注 <序号|马名> <金额>` | 为马匹下注 | `/赛马下注 01 100` | -| `/赛马开赛` | 开始比赛(需要至少2匹马) | `/赛马开赛` | -| `/赛马帮助` | 显示帮助信息 | `/赛马帮助` | - -## 配置说明 - -### 环境变量 - -所有配置项都可通过环境变量设置,前缀为 `GROUP_HORSE_RACING_` - -```python -# 测试模式 -GROUP_HORSE_RACING_TEST_MODE=false - -# 测试用户ID集合 -GROUP_HORSE_RACING_TESTERS=[] - -# 测试群组ID集合 -GROUP_HORSE_RACING_TEST_GROUPS=[] - -# 允许的群组ID集合 -GROUP_HORSE_RACING_ALLOWED_GROUPS=[] -``` - -### 游戏配置 - -| 配置项 | 默认值 | 说明 | -|--------|--------|------| -| `PARTICIPANT_REWARD` | 50 | 参赛者奖励积分 | -| `CHAMPION_REWARD` | 200 | 冠军奖励积分 | -| `MIN_BET` | 10 | 最小下注积分 | -| `MIN_ODDS` | 1.2 | 最小赔率 | -| `RACE_DISTANCE` | 100 | 比赛距离 | -| `RACE_TICK_INTERVAL` | 5 | 比赛更新间隔(秒) | - -### 消息撤回配置 - -可配置不同类型消息的自动撤回时间(秒,0表示不撤回): - -```python -MESSAGE_RECALL = { - "race_update": 30, # 比赛更新消息 - "registration": 180, # 报名确认消息 - "bet_confirm": 180, # 下注确认消息 - "cancel_confirm": 60, # 取消确认消息 - "error": 60, # 错误消息 - "race_result": 0, # 比赛结果(不撤回) - "leaderboard": 0, # 排行榜(不撤回) - "help": 0, # 帮助信息(不撤回) - "odds_display": 0, # 赔率显示(不撤回) -} -``` - -### 数据库配置 - -```python -RACE_DB_FILE = "data/group_horse_racing/race.db" -``` - -## 核心模块 - -### models.py - -定义了游戏中的数据模型: - -- **RoomState**:房间状态枚举(WAITING、RUNNING、FINISHED、INTERRUPTED) -- **HorseState**:马匹状态枚举(READY、RACING、FINISHED) -- **Horse**:马匹数据类,包含所有者ID、名称、位置、状态 -- **Bet**:下注数据类,包含用户ID、马匹名称、下注金额 -- **Room**:房间数据类,管理马匹、下注、比赛状态 -- **RaceResult**:比赛结果数据类,记录比赛统计信息 - -### race_engine.py - -比赛引擎,负责比赛逻辑: - -- **start_race()**:启动比赛循环 -- **_race_loop()**:主比赛循环,每个tick更新马匹位置 -- **_determine_champion()**:确定冠军(处理平局情况) - -比赛采用高斯分布随机算法,每个tick马匹随机前进一定距离。 - -### points_service.py - -积分服务,与 danding_points 插件集成: - -- **spend_bet_points()**:扣除下注积分(支持重试) -- **refund_bet_points()**:退还下注积分 -- **payout_winnings()**:支付中奖积分 -- **reward_participant()**:奖励参赛者 -- **reward_champion()**:奖励冠军 -- **get_balance()**:获取用户余额 - -### message_service.py - -消息服务,处理消息发送和自动撤回: - -- **send_with_recall()**:发送消息并根据配置自动撤回 -- **_schedule_recall()**:异步调度消息撤回 -- **clear_pending_recalls()**:清除待撤回消息任务 - -### room_store.py - -房间存储,管理比赛房间的生命周期: - -- 支持并发访问控制(使用asyncio.Lock) -- 房间持久化存储 -- 房间清理和过期处理 - -### commands.py - -命令处理器,实现所有用户命令: - -- **handle_register()**:处理报名命令 -- **handle_start()**:处理开赛命令 -- **handle_help()**:处理帮助命令 - -## 使用流程 - -### 基本游戏流程 - -1. **报名阶段** - - 用户执行 `/赛马报名` 命令 - - 系统检查权限和房间容量(最多8匹马) - - 成功报名 - -2. **下注阶段** - - 用户可对参赛马匹进行下注 - - 下注金额需满足最小下注要求 - - 下注积分从用户账户扣除 - - 可以给自己的马下注 - -3. **比赛阶段** - - 房主执行 `/赛马开赛` 命令 - - 系统启动比赛引擎 - - 每个tick更新马匹位置 - - 首先到达终点的马匹为冠军 - -4. **结算阶段** - - 冠军获得冠军奖励 - - 中奖用户获得下注奖金(下注金额 × 赔率) - - 比赛结果保存到数据库 - -## 权限控制 - -插件支持两种权限模式: - -### 测试模式(TEST_MODE=true) - -- 仅允许 `TEST_GROUPS` 中的群组使用 -- 仅允许 `TESTERS` 中的用户在私聊中使用 - -### 正常模式(TEST_MODE=false) - -- 仅允许 `ALLOWED_GROUPS` 中的群组使用 -- 私聊中禁用 - -## 依赖关系 - -- **必需**:`danding_bot.plugins.danding_points` - 积分系统插件 - -## 数据存储 - -比赛数据存储在SQLite数据库中: - -- 位置:`data/group_horse_racing/race.db` -- 存储内容:比赛历史、结果统计、用户数据 - -## 并发控制 - -- 使用 `asyncio.Lock` 保证房间操作的线程安全 -- 支持多个房间同时进行比赛 -- 每个房间有独立的锁和异步任务 - -## 错误处理 - -- 权限检查:无权限时返回错误提示 -- 房间检查:房间不存在或已满时返回错误 -- 参赛人数检查:少于2匹马时无法开赛 -- 积分检查:积分不足时下注失败 - -## 测试 - -项目包含 `test_commands.py` 用于测试各项功能。 - -## 扩展建议 - -- 支持更多赛马属性(速度、耐力等) -- 实现赔率动态计算 -- 添加排行榜功能 -- 支持马匹升级系统 -- 实现更复杂的下注规则 +# Group Horse Racing 插件 + +群赛马插件 - 一个支持群组内多人参与的赛马游戏,集成积分系统和下注功能。 + +## 功能概述 + +这是一个完整的群组赛马游戏系统,支持以下核心功能: + +- **马匹报名**:用户可以报名参加赛马比赛 +- **下注系统**:支持用户对任意参赛马匹进行下注(含自己的马) +- **自动比赛**:基于随机算法的赛马进程模拟 +- **积分集成**:与 danding_points 积分系统无缝集成 +- **自动撤回**:支持配置消息自动撤回时间 +- **多房间支持**:支持群组和私聊两种模式 + +## 命令列表 + +### 基础命令 + +| 命令 | 说明 | 示例 | +|------|------|------| +| `/赛马报名 <马匹名>` | 报名参加赛马比赛。若不输入则复用上次马名或使用昵称 | `/赛马报名 绝地赤兔` | +| `/赛马取消报名` | 取消报名并退还下注(限开始前) | `/赛马取消报名` | +| `/赛马下注 <序号|马名> <金额>` | 为马匹下注 | `/赛马下注 01 100` | +| `/赛马取消下注` | 取消本人所有下注并退还积分(限开始前) | `/赛马取消下注` | +| `/赛马赔率` | 查看当前赔率和下注池 | `/赛马赔率` | +| `/赛马列表` | 查看当前报名马匹列表 | `/赛马列表` | +| `/赛马开赛` | 开始比赛(需参赛者或管理员,至少2匹马) | `/赛马开赛` | +| `/赛马帮助` | 显示帮助信息 | `/赛马帮助` | + +## 配置说明 + +### 环境变量 + +所有配置项都可通过环境变量设置,前缀为 `GROUP_HORSE_RACING_` + +```python +# 测试模式 +GROUP_HORSE_RACING_TEST_MODE=false + +# 测试用户ID集合 +GROUP_HORSE_RACING_TESTERS=[] + +# 测试群组ID集合 +GROUP_HORSE_RACING_TEST_GROUPS=[] + +# 允许的群组ID集合 +GROUP_HORSE_RACING_ALLOWED_GROUPS=[] +``` + +### 游戏配置 + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| `PARTICIPANT_REWARD` | 20 | 参赛者奖励积分 | +| `CHAMPION_REWARD` | 150 | 冠军奖励积分 | +| `MIN_BET` | 10 | 最小下注积分 | +| `MIN_ODDS` | 1.2 | 最小赔率 | +| `RACE_DISTANCE` | 100 | 比赛距离 | +| `RACE_TICK_INTERVAL` | 5 | 比赛更新间隔(秒) | + +### 消息撤回配置 + +可配置不同类型消息的自动撤回时间(秒,0表示不撤回): + +```python +MESSAGE_RECALL = { + "race_update": 30, # 比赛更新消息 + "registration": 180, # 报名确认消息 + "bet_confirm": 180, # 下注确认消息 + "cancel_confirm": 60, # 取消确认消息 + "error": 60, # 错误消息 + "race_result": 0, # 比赛结果(不撤回) + "leaderboard": 0, # 排行榜(不撤回) + "help": 0, # 帮助信息(不撤回) + "odds_display": 0, # 赔率显示(不撤回) +} +``` + +### 数据库配置 + +```python +RACE_DB_FILE = "data/group_horse_racing/race.db" +``` + +## 核心模块 + +### models.py + +定义了游戏中的数据模型: + +- **RoomState**:房间状态枚举(WAITING、RUNNING、FINISHED、INTERRUPTED) +- **HorseState**:马匹状态枚举(READY、RACING、FINISHED) +- **Horse**:马匹数据类,包含所有者ID、名称、位置、状态 +- **Bet**:下注数据类,包含用户ID、马匹名称、下注金额 +- **Room**:房间数据类,管理马匹、下注、比赛状态 +- **RaceResult**:比赛结果数据类,记录比赛统计信息 + +### race_engine.py + +比赛引擎,负责比赛逻辑: + +- **start_race()**:启动比赛循环 +- **_race_loop()**:主比赛循环,每个tick更新马匹位置 +- **_determine_champion()**:确定冠军(处理平局情况) + +比赛采用高斯分布随机算法,每个tick马匹随机前进一定距离。 + +### points_service.py + +积分服务,与 danding_points 插件集成: + +- **spend_bet_points()**:扣除下注积分(支持重试) +- **refund_bet_points()**:退还下注积分 +- **payout_winnings()**:支付中奖积分 +- **reward_participant()**:奖励参赛者 +- **reward_champion()**:奖励冠军 +- **get_balance()**:获取用户余额 + +### message_service.py + +消息服务,处理消息发送和自动撤回: + +- **send_with_recall()**:发送消息并根据配置自动撤回 +- **_schedule_recall()**:异步调度消息撤回 +- **clear_pending_recalls()**:清除待撤回消息任务 + +### room_store.py + +房间存储,管理比赛房间的生命周期: + +- 支持并发访问控制(使用asyncio.Lock) +- 房间持久化存储 +- 房间清理和过期处理 + +### commands.py + +命令处理器,实现所有用户命令: + +- **handle_register()**:处理报名命令 +- **handle_cancel()**:处理取消报名命令 +- **handle_bet()**:处理下注命令 +- **handle_cancel_bet()**:处理取消下注命令 +- **handle_odds()**:处理赔率显示命令 +- **handle_race_list()**:处理马匹列表命令 +- **handle_start()**:处理开赛命令(仅参赛者或管理员可操作) +- **handle_help()**:处理帮助命令 + +## 使用流程 + +### 基本游戏流程 + +1. **报名阶段** + - 用户执行 `/赛马报名` 命令 + - 系统检查权限和房间容量(最多8匹马) + - 成功报名 + +2. **下注阶段** + - 用户可对参赛马匹进行下注 + - 下注金额需满足最小下注要求 + - 下注积分从用户账户扣除 + - 可以给自己的马下注 + +3. **比赛阶段** + - 房主执行 `/赛马开赛` 命令 + - 系统启动比赛引擎 + - 每个tick更新马匹位置 + - 首先到达终点的马匹为冠军 + +4. **结算阶段** + - 冠军获得冠军奖励 + - 中奖用户获得下注奖金(下注金额 × 赔率) + - 比赛结果保存到数据库 + +## 权限控制 + +插件支持两种权限模式: + +### 测试模式(TEST_MODE=true) + +- 仅允许 `TEST_GROUPS` 中的群组使用 +- 仅允许 `TESTERS` 中的用户在私聊中使用 + +### 正常模式(TEST_MODE=false) + +- 仅允许 `ALLOWED_GROUPS` 中的群组使用 +- 私聊中禁用 + +## 依赖关系 + +- **必需**:`danding_bot.plugins.danding_points` - 积分系统插件 + +## 数据存储 + +比赛数据存储在SQLite数据库中: + +- 位置:`data/group_horse_racing/race.db` +- 存储内容:比赛历史、结果统计、用户数据 + +## 并发控制 + +- 使用 `asyncio.Lock` 保证房间操作的线程安全 +- 支持多个房间同时进行比赛 +- 每个房间有独立的锁和异步任务 + +## 错误处理 + +- 权限检查:无权限时返回错误提示 +- 房间检查:房间不存在或已满时返回错误 +- 参赛人数检查:少于2匹马时无法开赛 +- 积分检查:积分不足时下注失败 + +## 测试 + +项目包含 `test_commands.py` 用于测试各项功能。 + +## 扩展建议 + +- 支持更多赛马属性(速度、耐力等) +- 实现赔率动态计算 +- 添加排行榜功能 +- 支持马匹升级系统 +- 实现更复杂的下注规则 diff --git a/danding_bot/plugins/group_horse_racing/commands.py b/danding_bot/plugins/group_horse_racing/commands.py index 7a5aaec..0f2d030 100644 --- a/danding_bot/plugins/group_horse_racing/commands.py +++ b/danding_bot/plugins/group_horse_racing/commands.py @@ -444,6 +444,57 @@ async def handle_cancel(bot: Bot, event: Event): bet_cmd = on_command("赛马下注", priority=5) +cancel_bet_cmd = on_command("赛马取消下注", priority=5) + + +@cancel_bet_cmd.handle() +async def handle_cancel_bet(bot: Bot, event: Event): + """Handle cancel bet - refund all bets placed by the user in current room.""" + if not await check_access(bot, event): + await cancel_bet_cmd.finish("无权限访问此功能") + return + + scope = get_scope(event) + user_id = get_event_id(event) + lock = room_store.get_lock(scope) + + async with lock: + room = room_store.get_room(scope) + if not room: + await cancel_bet_cmd.finish("房间不存在") + return + + if room.state != RoomState.WAITING: + await cancel_bet_cmd.finish("比赛已开始,无法取消下注") + return + + user_bets = [b for b in room.bets if b.user_id == user_id] + if not user_bets: + await cancel_bet_cmd.finish("你还没有下注") + return + + total_refund = 0 + refund_errors = [] + for bet in user_bets: + try: + await points_service.refund_bet_points(bet.user_id, bet.amount, "取消下注退还") + total_refund += bet.amount + except Exception as e: + logger.error(f"退还下注失败 user={bet.user_id} amount={bet.amount}: {e}") + refund_errors.append(bet) + + # 只移除已成功退还的下注 + if refund_errors: + failed_amount = sum(b.amount for b in refund_errors) + room.bets = [b for b in room.bets if b.user_id != user_id or b in refund_errors] + await cancel_bet_cmd.finish(f"退还部分失败:成功退还 {total_refund} 积分,{len(refund_errors)} 笔退还失败({failed_amount} 积分),请联系管理员") + return + else: + room.bets = [b for b in room.bets if b.user_id != user_id] + + await cancel_bet_cmd.finish(f"已取消 {len(user_bets)} 笔下注,退还 {total_refund} 积分") + + @bet_cmd.handle() async def handle_bet(bot: Bot, event: Event): """Handle bet placement.""" @@ -530,17 +581,43 @@ async def handle_odds(bot: Bot, event: Event): await odds_cmd.finish("\n".join(lines)) +race_list_cmd = on_command("赛马列表", priority=5) + + +@race_list_cmd.handle() +async def handle_race_list(bot: Bot, event: Event): + """显示当前房间所有报名马匹信息。""" + if not await check_access(bot, event): + await race_list_cmd.finish("无权限访问此功能") + return + + scope = get_scope(event) + room = room_store.get_room(scope) + if not room or not room.horses: + await race_list_cmd.finish("暂无报名马匹") + return + + lines = ["🏇 当前报名马匹:"] + for horse in _get_horses_in_order(room): + owner_display = await _get_user_name(bot, scope, horse.owner_id) + lines.append(f" {horse.index}. {horse.name} - 主人: {owner_display}") + lines.append(f"\n共 {len(room.horses)} 匹马") + + await race_list_cmd.finish("\n".join(lines)) + + start_cmd = on_command("赛马开赛", priority=5) @start_cmd.handle() async def handle_start(bot: Bot, event: Event): - """Handle race start.""" + """Handle race start - only participants or admins can start.""" if not await check_access(bot, event): await start_cmd.finish("无权限访问此功能") return scope = get_scope(event) + user_id = get_event_id(event) lock = room_store.get_lock(scope) async with lock: @@ -557,6 +634,24 @@ async def handle_start(bot: Bot, event: Event): await start_cmd.finish("至少需要2匹马才能开赛") return + # 开赛权限限制:仅参赛者或群管理员可手动开赛(满8匹自动开赛不受影响) + is_participant = user_id in [h.owner_id for h in room.horses.values()] + is_admin = False + if isinstance(event, GroupMessageEvent): + try: + member_info = await bot.get_group_member_info( + group_id=event.group_id, + user_id=int(user_id) + ) + role = member_info.get("role", "") + is_admin = role in ("admin", "owner") + except Exception: + pass + + if not is_participant and not is_admin: + await start_cmd.finish("只有参赛者或群管理员可以开赛") + return + # Set all horses to racing state for horse in room.horses.values(): horse.state = HorseState.RACING @@ -581,7 +676,9 @@ async def handle_help(bot: Bot, event: Event): /赛马报名 - 复用上次绑定的马名,若无则使用群昵称 /赛马取消报名 - 取消报名并退还下注 /赛马下注 <序号|马匹名> <金额> - 下注 +/赛马取消下注 - 取消本人在当前房间的所有下注并退还积分 /赛马赔率 - 查看当前赔率和下注池 +/赛马列表 - 查看当前报名马匹列表 /赛马开赛 - 开始比赛(至少2匹马) /赛马帮助 - 显示此帮助 @@ -589,8 +686,10 @@ async def handle_help(bot: Bot, event: Event): • 最低下注金额:{config.MIN_BET} 积分 • 参赛马匹上限:8匹 • 开赛要求:至少2匹马报名 +• 手动开赛权限:仅当前参赛者或群管理员可操作 💰 奖励机制: +• 参赛奖励:参赛者均可获得 {config.PARTICIPANT_REWARD} 积分 • 冠军马主:获得 {config.CHAMPION_REWARD} 积分 • 下注中奖:下注金额 × 赔率 @@ -602,7 +701,7 @@ async def handle_help(bot: Bot, event: Event): 🎮 游戏流程: 1️⃣ 玩家报名并绑定马匹名 2️⃣ 玩家可以给任意马匹下注 -3️⃣ 满足开赛条件后,任意玩家可开赛 +3️⃣ 满足开赛后,由参赛者或管理员开赛 4️⃣ 比赛实时进行,定期播报进度 5️⃣ 比赛结束后结算积分和奖金""" await help_cmd.finish(help_text)