import asyncio import random from typing import Optional from .models import Room, RoomState, Horse, HorseState from .config import Config class RaceEngine: def __init__(self, config: Config): self.config = config self.active_tasks: dict[str, asyncio.Task] = {} def tick(self, room: Room): """Advance race by one tick. Returns list of finished horses.""" room.tick_count += 1 for horse in room.horses.values(): if horse.state == HorseState.RACING: distance = max(0, random.gauss(10, 3)) horse.position += distance finished_horses = [ h for h in room.horses.values() if h.position >= self.config.RACE_DISTANCE ] return finished_horses def determine_champion(self, horses: list[Horse]) -> Horse: """Determine champion from tied horses.""" if len(horses) == 1: return horses[0] while len(horses) > 1: distances = [max(0, random.gauss(10, 3)) for _ in horses] max_distance = max(distances) horses = [h for h, d in zip(horses, distances) if d == max_distance] 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: task = self.active_tasks[scope] 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}回合】"] sorted_horses = sorted(room.horses.values(), key=lambda h: h.index) # Calculate max display width for name alignment # Each CJK/fullwidth char counts as 2, others as 1 def _display_width(s: str) -> int: w = 0 for ch in s: w += 2 if '\u4e00' <= ch <= '\u9fff' or '\u3000' <= ch <= '\u303f' or '\uff00' <= ch <= '\uffef' else 1 return w max_name_width = max(_display_width(h.name) for h in sorted_horses) for horse in sorted_horses: progress = min(horse.position / distance, 1.0) filled = int(progress * bar_width) bar = "█" * filled + "░" * (bar_width - filled) # Pad with fullwidth spaces (1 fullwidth space = 2 columns = 1 CJK char width) diff = max_name_width - _display_width(horse.name) pad = "\u3000" * (diff // 2) + (" " if diff % 2 else "") lines.append(f" {horse.index:02d}号 {horse.name}{pad} |{bar}| {horse.position:.1f}m") return "\n".join(lines)