diff --git a/danding_bot/plugins/group_horse_racing/commands.py b/danding_bot/plugins/group_horse_racing/commands.py index 3658814..c0af9d2 100644 --- a/danding_bot/plugins/group_horse_racing/commands.py +++ b/danding_bot/plugins/group_horse_racing/commands.py @@ -97,40 +97,55 @@ async def settle_race(room: Room): room_store.save_race_result(result) -async def run_race_with_settlement(bot: Bot, room: Room, scope: str): - """Run race and handle settlement + cleanup.""" - task = await race_engine.start_race(room) - - # Send start message - horse_list = "\n".join(f" {h.name} (主人: {h.owner_id})" for h in room.horses.values()) +async def _send_to_scope(bot: Bot, scope: str, message: str): + """Send message to group or private chat based on scope.""" try: await bot.send_msg( message_type="group" if scope.startswith("group_") else "private", group_id=int(scope.split("_", 1)[1]) if scope.startswith("group_") else None, user_id=int(scope.split("_", 1)[1]) if scope.startswith("test_") else None, - message=f"比赛开始!参赛马匹:\n{horse_list}", + message=message, ) except Exception: pass - # Wait for race to finish + +async def run_race_with_settlement(bot: Bot, room: Room, scope: str): + """Run race with live progress updates and settlement.""" + room.state = RoomState.RUNNING + + # Send start message with horse list + horse_list = "\n".join(f" {h.name}" for h in room.horses.values()) + await _send_to_scope(bot, scope, f"比赛开始!\n{horse_list}") + + # Race loop with progress updates try: - await task + while room.state == RoomState.RUNNING: + await asyncio.sleep(config.RACE_TICK_INTERVAL) + + finished = race_engine.tick(room) + progress = race_engine.format_progress(room) + await _send_to_scope(bot, scope, progress) + + if finished: + champion = race_engine.determine_champion(finished) + room.champion_name = champion.name + room.state = RoomState.FINISHED + break except asyncio.CancelledError: room.state = RoomState.INTERRUPTED - # Refund all bets on interruption for bet in room.bets: await points_service.refund_bet_points(bet.user_id, bet.amount, "比赛中断退还") return - # Race finished - settle + # Settle await settle_race(room) # Build result message odds = calculate_odds(room) champion = room.horses.get(room.champion_name) result_lines = [ - f"比赛结束!冠军:{room.champion_name} 🏆", + f"比赛结束!冠军:{room.champion_name}", f"马主 {champion.owner_id if champion else '?'} 获得 {config.CHAMPION_REWARD} 积分", ] winning_bets = [b for b in room.bets if b.horse_name == room.champion_name] @@ -138,17 +153,9 @@ async def run_race_with_settlement(bot: Bot, room: Room, scope: str): result_lines.append("下注中奖:") for b in winning_bets: payout = int(b.amount * odds.get(b.horse_name, config.MIN_ODDS)) - result_lines.append(f" {b.user_id} 下注 {b.amount} → 获得 {payout}") + result_lines.append(f" {b.user_id} 下注 {b.amount} -> 获得 {payout}") - try: - await bot.send_msg( - message_type="group" if scope.startswith("group_") else "private", - group_id=int(scope.split("_", 1)[1]) if scope.startswith("group_") else None, - user_id=int(scope.split("_", 1)[1]) if scope.startswith("test_") else None, - message="\n".join(result_lines), - ) - except Exception: - pass + await _send_to_scope(bot, scope, "\n".join(result_lines)) # Cleanup race_engine.stop_race(scope) @@ -391,7 +398,8 @@ async def handle_start(bot: Bot, event: Event): await start_cmd.finish("比赛开始!") # Run race in background (outside command handler) - asyncio.create_task(run_race_with_settlement(bot, room, scope)) + task = asyncio.create_task(run_race_with_settlement(bot, room, scope)) + race_engine.register_task(scope, task) help_cmd = on_command("赛马帮助", priority=5) diff --git a/danding_bot/plugins/group_horse_racing/race_engine.py b/danding_bot/plugins/group_horse_racing/race_engine.py index 7a3a197..85da2a7 100644 --- a/danding_bot/plugins/group_horse_racing/race_engine.py +++ b/danding_bot/plugins/group_horse_racing/race_engine.py @@ -11,37 +11,23 @@ class RaceEngine: self.config = config self.active_tasks: dict[str, asyncio.Task] = {} - async def start_race(self, room: Room) -> asyncio.Task: - """Start race progression loop.""" - task = asyncio.create_task(self._race_loop(room)) - self.active_tasks[room.scope] = task - return task + def tick(self, room: Room): + """Advance race by one tick. Returns list of finished horses.""" + room.tick_count += 1 - async def _race_loop(self, room: Room): - """Main race progression loop.""" - room.state = RoomState.RUNNING + for horse in room.horses.values(): + if horse.state == HorseState.RACING: + distance = max(0, random.gauss(10, 3)) + horse.position += distance - while room.state == RoomState.RUNNING: - await asyncio.sleep(self.config.RACE_TICK_INTERVAL) - room.tick_count += 1 + finished_horses = [ + h for h in room.horses.values() + if h.position >= self.config.RACE_DISTANCE + ] - for horse in room.horses.values(): - if horse.state == HorseState.RACING: - distance = max(0, random.gauss(10, 3)) - horse.position += distance + return finished_horses - finished_horses = [ - h for h in room.horses.values() - if h.position >= self.config.RACE_DISTANCE - ] - - if finished_horses: - champion = self._determine_champion(finished_horses) - room.champion_name = champion.name - room.state = RoomState.FINISHED - break - - def _determine_champion(self, horses: list[Horse]) -> Horse: + def determine_champion(self, horses: list[Horse]) -> Horse: """Determine champion from tied horses.""" if len(horses) == 1: return horses[0] @@ -53,6 +39,10 @@ class RaceEngine: return horses[0] + def register_task(self, scope: str, task: asyncio.Task): + """Register an active race task.""" + self.active_tasks[scope] = task + def stop_race(self, scope: str): """Stop race and cancel task.""" if scope in self.active_tasks: @@ -60,3 +50,19 @@ class RaceEngine: if not task.done(): task.cancel() del self.active_tasks[scope] + + def format_progress(self, room: Room) -> str: + """Format race progress as visual bar for each horse.""" + distance = self.config.RACE_DISTANCE + bar_width = 20 + lines = [f"【第{room.tick_count}回合】"] + + # Sort by position descending for readability + sorted_horses = sorted(room.horses.values(), key=lambda h: h.position, reverse=True) + for horse in sorted_horses: + progress = min(horse.position / distance, 1.0) + filled = int(progress * bar_width) + bar = "█" * filled + "░" * (bar_width - filled) + lines.append(f" {horse.name:<6} |{bar}| {horse.position:.1f}m") + + return "\n".join(lines)