补全赛马插件完整游戏逻辑

实现所有帮助文本中声明的命令和比赛结算流程:
- 赛马报名:解析马匹名、创建Horse对象、防重复报名、发放参赛奖励
- 赛马取消报名:移除马匹、退还相关下注
- 赛马下注:扣积分、记录下注、不能给自己的马下注
- 赛马赔率:基于下注分布动态计算赔率
- 赛马开赛:异步执行比赛、自动结算冠军奖励和下注赔付、保存结果

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 01:18:49 +08:00
parent 633294f6cc
commit e341fc085c

View File

@@ -1,3 +1,7 @@
import asyncio
import uuid
from datetime import datetime
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, PrivateMessageEvent
@@ -5,7 +9,7 @@ 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
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
@@ -40,6 +44,120 @@ async def check_access(bot: Bot, event: Event) -> bool:
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 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())
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}",
)
except Exception:
pass
# Wait for race to finish
try:
await task
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
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}")
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
# Cleanup
race_engine.stop_race(scope)
room_store.delete_room(scope)
message_service.clear_pending_recalls(scope)
# --- Commands ---
register_cmd = on_command("赛马报名", priority=5)
@@ -50,7 +168,21 @@ async def handle_register(bot: Bot, event: 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:
@@ -58,11 +190,174 @@ async def handle_register(bot: Bot, event: Event):
if not room:
room = room_store.create_room(scope)
if len(room.horses) >= 8:
await register_cmd.finish("房间已满")
if room.state != RoomState.WAITING:
await register_cmd.finish("比赛正在进行中,无法报名")
return
await register_cmd.finish("报名成功")
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)
# Reward participant (outside lock to avoid holding lock during API call)
await points_service.reward_participant(user_id)
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)
@@ -81,15 +376,25 @@ async def handle_start(bot: Bot, event: Event):
async with lock:
room = room_store.get_room(scope)
if not room:
await start_cmd.finish("房间不存在")
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
await race_engine.start_race(room)
await start_cmd.finish("比赛开始!")
# 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)
asyncio.create_task(run_race_with_settlement(bot, room, scope))
help_cmd = on_command("赛马帮助", priority=5)
@@ -98,13 +403,11 @@ help_cmd = on_command("赛马帮助", priority=5)
@help_cmd.handle()
async def handle_help(bot: Bot, event: Event):
"""Handle help command."""
help_text = """
赛马命令帮助:
/赛马报名 <马匹名> - 报名参赛
/赛马取消报名 - 取消报名
/赛马下注 <马匹名> <金额> - 下注
/赛马开赛 - 开始比赛
/赛马赔率 - 查看赔率
/赛马帮助 - 显示此帮助
"""
help_text = """赛马命令帮助:
/赛马报名 <马匹名> - 报名参赛获得50积分奖励
/赛马取消报名 - 取消报名并退还下注
/赛马下注 <马匹名> <金额> - 下注(不能给自己的马下注)
/赛马赔率 - 查看当前赔率
/赛马开赛 - 开始比赛至少2匹马
/赛马帮助 - 显示此帮助"""
await help_cmd.finish(help_text)