Files
DanDingNoneBot/danding_bot/plugins/group_horse_racing/commands.py
Mr.Xia adccbfebb5 添加赛马实时进度播报
将比赛循环从race_engine移到commands中,每回合发送进度条:
  马匹名  |████████░░░░░░| 45.2m
race_engine改为提供tick/determine_champion/format_progress方法

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:32:42 +08:00

419 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import uuid
from datetime import datetime
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, PrivateMessageEvent
from .room_store import RoomStore
from .points_service import PointsService
from .race_engine import RaceEngine
from .message_service import MessageService
from .models import Room, Horse, Bet, HorseState, RoomState, RaceResult
# Import config from __init__ to ensure it's loaded through NoneBot driver
from . import plugin_config as config
room_store = RoomStore(config)
points_service = PointsService(config)
race_engine = RaceEngine(config)
message_service = MessageService(config)
def get_scope(event: Event) -> str:
"""Get room scope from event."""
if isinstance(event, GroupMessageEvent):
return f"group_{event.group_id}"
elif isinstance(event, PrivateMessageEvent):
return f"test_{event.user_id}"
return ""
async def check_access(bot: Bot, event: Event) -> bool:
"""Check if user has access to horse racing."""
if isinstance(event, PrivateMessageEvent):
if not config.TEST_MODE:
return False
return event.user_id in config.TESTERS
if isinstance(event, GroupMessageEvent):
if config.TEST_MODE:
return event.group_id in config.TEST_GROUPS
return event.group_id in config.ALLOWED_GROUPS
return False
def get_event_id(event: Event) -> str:
"""Get user id as string from event."""
return str(event.user_id)
def calculate_odds(room: Room) -> dict[str, float]:
"""Calculate odds for each horse based on bet distribution."""
total_bet = sum(b.amount for b in room.bets)
odds = {}
for name in room.horses:
horse_bet = sum(b.amount for b in room.bets if b.horse_name == name)
if horse_bet == 0:
odds[name] = config.MIN_ODDS
else:
raw_odds = total_bet / horse_bet
odds[name] = max(config.MIN_ODDS, round(raw_odds, 2))
return odds
async def settle_race(room: Room):
"""Settle bets and rewards after race finishes."""
champion = room.horses.get(room.champion_name)
if not champion:
return
# Reward champion owner
await points_service.reward_champion(champion.owner_id)
# Reward all participants
for horse in room.horses.values():
if horse.owner_id != champion.owner_id:
await points_service.reward_participant(horse.owner_id)
# Settle bets
odds = calculate_odds(room)
for bet in room.bets:
if bet.horse_name == room.champion_name:
await points_service.payout_winnings(bet.user_id, bet.amount, odds.get(bet.horse_name, config.MIN_ODDS))
# Save race result
result = RaceResult(
race_id=str(uuid.uuid4()),
scope=room.scope,
champion_name=champion.name,
champion_owner=champion.owner_id,
participants=[h.name for h in room.horses.values()],
bet_distribution={name: sum(b.amount for b in room.bets if b.horse_name == name) for name in room.horses},
duration_ticks=room.tick_count,
completed_at=datetime.now(),
)
room_store.save_race_result(result)
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=message,
)
except Exception:
pass
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:
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
for bet in room.bets:
await points_service.refund_bet_points(bet.user_id, bet.amount, "比赛中断退还")
return
# 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"马主 {champion.owner_id if champion else '?'} 获得 {config.CHAMPION_REWARD} 积分",
]
winning_bets = [b for b in room.bets if b.horse_name == room.champion_name]
if winning_bets:
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}")
await _send_to_scope(bot, scope, "\n".join(result_lines))
# Cleanup
race_engine.stop_race(scope)
room_store.delete_room(scope)
message_service.clear_pending_recalls(scope)
# --- Commands ---
register_cmd = on_command("赛马报名", priority=5)
@register_cmd.handle()
async def handle_register(bot: Bot, event: Event):
"""Handle horse registration."""
if not await check_access(bot, event):
await register_cmd.finish("无权限访问此功能")
return
# Parse horse name from message
msg = str(event.get_message()).strip()
parts = msg.split(None, 1)
horse_name = parts[1].strip() if len(parts) > 1 else ""
if not horse_name:
await register_cmd.finish("请输入马匹名:/赛马报名 <马匹名>")
return
if len(horse_name) > 10:
await register_cmd.finish("马匹名不能超过10个字符")
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:
room = room_store.create_room(scope)
if room.state != RoomState.WAITING:
await register_cmd.finish("比赛正在进行中,无法报名")
return
if len(room.horses) >= 8:
await register_cmd.finish("房间已满最多8匹马")
return
if horse_name in room.horses:
await register_cmd.finish(f"马匹名 \"{horse_name}\" 已被使用")
return
# Check if user already registered
for h in room.horses.values():
if h.owner_id == user_id:
await register_cmd.finish("你已经报名了,不能重复报名")
return
# Create horse
room.horses[horse_name] = Horse(owner_id=user_id, name=horse_name)
count = len(room.horses)
await register_cmd.finish(f"报名成功!马匹 \"{horse_name}\" 已加入比赛({count}/8")
cancel_cmd = on_command("赛马取消报名", priority=5)
@cancel_cmd.handle()
async def handle_cancel(bot: Bot, event: Event):
"""Handle cancel registration."""
if not await check_access(bot, event):
await cancel_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_cmd.finish("房间不存在")
return
if room.state != RoomState.WAITING:
await cancel_cmd.finish("比赛正在进行中,无法取消报名")
return
# Find user's horse
user_horse = None
for name, horse in room.horses.items():
if horse.owner_id == user_id:
user_horse = name
break
if not user_horse:
await cancel_cmd.finish("你还没有报名")
return
# Refund bets on this horse
bets_to_refund = [b for b in room.bets if b.horse_name == user_horse]
for bet in bets_to_refund:
await points_service.refund_bet_points(bet.user_id, bet.amount, "取消报名退还下注")
room.bets = [b for b in room.bets if b.horse_name != user_horse]
# Remove horse
del room.horses[user_horse]
await cancel_cmd.finish(f"已取消报名,马匹 \"{user_horse}\" 已退出")
bet_cmd = on_command("赛马下注", priority=5)
@bet_cmd.handle()
async def handle_bet(bot: Bot, event: Event):
"""Handle bet placement."""
if not await check_access(bot, event):
await bet_cmd.finish("无权限访问此功能")
return
# Parse arguments: /赛马下注 <马匹名> <金额>
msg = str(event.get_message()).strip()
parts = msg.split()
if len(parts) < 3:
await bet_cmd.finish("请使用:/赛马下注 <马匹名> <金额>")
return
horse_name = parts[1]
try:
amount = int(parts[2])
except ValueError:
await bet_cmd.finish("金额必须是正整数")
return
if amount < config.MIN_BET:
await bet_cmd.finish(f"最低下注金额为 {config.MIN_BET}")
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 bet_cmd.finish("房间不存在,请先报名")
return
if room.state != RoomState.WAITING:
await bet_cmd.finish("比赛正在进行中,无法下注")
return
if horse_name not in room.horses:
await bet_cmd.finish(f"马匹 \"{horse_name}\" 不存在")
return
# Can't bet on your own horse
if room.horses[horse_name].owner_id == user_id:
await bet_cmd.finish("不能给自己的马下注")
return
# Deduct points first
success, balance = await points_service.spend_bet_points(user_id, amount, f"下注 {horse_name}")
if not success:
await bet_cmd.finish(f"积分不足(当前余额:{balance}")
return
# Record bet
room.bets.append(Bet(user_id=user_id, horse_name=horse_name, amount=amount))
odds = calculate_odds(room)
await bet_cmd.finish(f"下注成功!{horse_name} {amount}积分(当前赔率:{odds.get(horse_name, config.MIN_ODDS):.2f}")
odds_cmd = on_command("赛马赔率", priority=5)
@odds_cmd.handle()
async def handle_odds(bot: Bot, event: Event):
"""Handle odds display."""
if not await check_access(bot, event):
await odds_cmd.finish("无权限访问此功能")
return
scope = get_scope(event)
room = room_store.get_room(scope)
if not room:
await odds_cmd.finish("房间不存在,请先报名")
return
if not room.horses:
await odds_cmd.finish("还没有马匹报名")
return
odds = calculate_odds(room)
lines = ["当前赔率:"]
total_bet = sum(b.amount for b in room.bets)
for name, odd in odds.items():
horse_bet = sum(b.amount for b in room.bets if b.horse_name == name)
lines.append(f" {name} - {odd:.2f}倍 (总下注: {horse_bet})")
lines.append(f"总下注池: {total_bet}")
await odds_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."""
if not await check_access(bot, event):
await start_cmd.finish("无权限访问此功能")
return
scope = get_scope(event)
lock = room_store.get_lock(scope)
async with lock:
room = room_store.get_room(scope)
if not room:
await start_cmd.finish("房间不存在,请先报名")
return
if room.state != RoomState.WAITING:
await start_cmd.finish("比赛已经在进行中")
return
if len(room.horses) < 2:
await start_cmd.finish("至少需要2匹马才能开赛")
return
# Set all horses to racing state
for horse in room.horses.values():
horse.state = HorseState.RACING
await start_cmd.finish("比赛开始!")
# Run race in background (outside command handler)
task = asyncio.create_task(run_race_with_settlement(bot, room, scope))
race_engine.register_task(scope, task)
help_cmd = on_command("赛马帮助", priority=5)
@help_cmd.handle()
async def handle_help(bot: Bot, event: Event):
"""Handle help command."""
help_text = """赛马命令帮助:
/赛马报名 <马匹名> - 报名参赛
/赛马取消报名 - 取消报名并退还下注
/赛马下注 <马匹名> <金额> - 下注(不能给自己的马下注)
/赛马赔率 - 查看当前赔率
/赛马开赛 - 开始比赛至少2匹马
/赛马帮助 - 显示此帮助"""
await help_cmd.finish(help_text)