fix: 赛马插件P0-P2问题修复
- P0: room_store sqlite3→aiosqlite异步化 - P0: points_service统一异常处理+轻量重试 - P0: _send_to_scope加warning日志 - P1: 积分历史记录补充source/reason字段 - P1: 赛马结算写入赔率快照(odds_snapshot) - P1: test_commands改为commands_mod间接引用(测试隔离) - P2: 马名去重统一casefold()比较
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,63 +1,86 @@
|
|||||||
from typing import Tuple
|
import asyncio
|
||||||
from danding_bot.plugins.danding_points import points_api
|
import logging
|
||||||
from .config import Config
|
from typing import Tuple
|
||||||
|
from danding_bot.plugins.danding_points import points_api
|
||||||
|
from .config import Config
|
||||||
class PointsService:
|
|
||||||
def __init__(self, config: Config):
|
logger = logging.getLogger("horse_racing.points")
|
||||||
self.config = config
|
|
||||||
|
MAX_RETRIES = 3
|
||||||
async def spend_bet_points(
|
RETRY_DELAY = 0.5
|
||||||
self, user_id: str, amount: int, reason: str = "赛马下注"
|
|
||||||
) -> Tuple[bool, int]:
|
|
||||||
"""Deduct points for betting with retry."""
|
class PointsService:
|
||||||
success, balance = await points_api.spend_points(
|
def __init__(self, config: Config):
|
||||||
user_id, amount, "horse_race", reason
|
self.config = config
|
||||||
)
|
|
||||||
if not success:
|
async def _call_with_retry(self, func, *args, retries=MAX_RETRIES):
|
||||||
success, balance = await points_api.spend_points(
|
"""Call API function with retry logic."""
|
||||||
user_id, amount, "horse_race", reason
|
last_exc = None
|
||||||
)
|
for attempt in range(retries):
|
||||||
return success, balance
|
try:
|
||||||
|
return await func(*args)
|
||||||
async def refund_bet_points(
|
except Exception as e:
|
||||||
self, user_id: str, amount: int, reason: str = "取消报名退还"
|
last_exc = e
|
||||||
) -> Tuple[bool, int]:
|
logger.warning(
|
||||||
"""Refund bet points."""
|
f"Points API call failed (attempt {attempt + 1}/{retries}): {e}"
|
||||||
return await points_api.add_points(user_id, amount, "horse_race", reason)
|
)
|
||||||
|
if attempt < retries - 1:
|
||||||
async def payout_winnings(
|
await asyncio.sleep(RETRY_DELAY)
|
||||||
self, user_id: str, amount: int, odds: float
|
logger.error(f"Points API call failed after {retries} attempts: {last_exc}")
|
||||||
) -> Tuple[bool, int]:
|
raise last_exc
|
||||||
"""Payout bet winnings."""
|
|
||||||
payout = int(amount * odds)
|
async def spend_bet_points(
|
||||||
reason = f"下注获胜 ×{odds:.2f}"
|
self, user_id: str, amount: int, reason: str = "赛马下注"
|
||||||
return await points_api.add_points(user_id, payout, "horse_race", reason)
|
) -> Tuple[bool, int]:
|
||||||
|
"""Deduct points for betting with retry."""
|
||||||
async def reward_participant(self, user_id: str) -> Tuple[bool, int]:
|
try:
|
||||||
"""Reward race participant."""
|
return await self._call_with_retry(
|
||||||
return await points_api.add_points(
|
points_api.spend_points,
|
||||||
user_id,
|
user_id, amount, "horse_race", reason
|
||||||
self.config.PARTICIPANT_REWARD,
|
)
|
||||||
"horse_race",
|
except Exception as e:
|
||||||
"参赛奖励",
|
logger.error(f"spend_bet_points failed for user {user_id}: {e}")
|
||||||
)
|
return False, 0
|
||||||
|
|
||||||
async def reward_champion(self, user_id: str) -> Tuple[bool, int]:
|
async def refund_bet_points(
|
||||||
"""Reward race champion."""
|
self, user_id: str, amount: int, reason: str = "取消报名退还"
|
||||||
return await points_api.add_points(
|
) -> Tuple[bool, int]:
|
||||||
user_id,
|
"""Refund bet points."""
|
||||||
self.config.CHAMPION_REWARD,
|
return await points_api.add_points(user_id, amount, "horse_race", reason)
|
||||||
"horse_race",
|
|
||||||
"冠军奖励",
|
async def payout_winnings(
|
||||||
)
|
self, user_id: str, amount: int, odds: float
|
||||||
|
) -> Tuple[bool, int]:
|
||||||
async def set_points(
|
"""Payout bet winnings."""
|
||||||
self, user_id: str, amount: int, reason: str = "测试设置积分"
|
payout = int(amount * odds)
|
||||||
) -> Tuple[bool, int]:
|
reason = f"下注获胜 ×{odds:.2f}"
|
||||||
"""Set user points (for testing)."""
|
return await points_api.add_points(user_id, payout, "horse_race", reason)
|
||||||
return await points_api.set_points(user_id, amount, "horse_race", reason)
|
|
||||||
|
async def reward_participant(self, user_id: str) -> Tuple[bool, int]:
|
||||||
async def get_balance(self, user_id: str) -> int:
|
"""Reward race participant."""
|
||||||
"""Get user balance."""
|
return await points_api.add_points(
|
||||||
return await points_api.get_balance(user_id)
|
user_id,
|
||||||
|
self.config.PARTICIPANT_REWARD,
|
||||||
|
"horse_race",
|
||||||
|
"参赛奖励",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def reward_champion(self, user_id: str) -> Tuple[bool, int]:
|
||||||
|
"""Reward race champion."""
|
||||||
|
return await points_api.add_points(
|
||||||
|
user_id,
|
||||||
|
self.config.CHAMPION_REWARD,
|
||||||
|
"horse_race",
|
||||||
|
"冠军奖励",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_points(
|
||||||
|
self, user_id: str, amount: int, reason: str = "测试设置积分"
|
||||||
|
) -> Tuple[bool, int]:
|
||||||
|
"""Set user points (for testing)."""
|
||||||
|
return await points_api.set_points(user_id, amount, "horse_race", reason)
|
||||||
|
|
||||||
|
async def get_balance(self, user_id: str) -> int:
|
||||||
|
"""Get user balance."""
|
||||||
|
return await points_api.get_balance(user_id)
|
||||||
|
|||||||
@@ -1,163 +1,192 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import sqlite3
|
import aiosqlite
|
||||||
from datetime import datetime
|
import json
|
||||||
from pathlib import Path
|
from datetime import datetime
|
||||||
from typing import Optional
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
from .models import Room, RoomState, RaceResult
|
|
||||||
from .config import Config
|
from .models import Room, RoomState, RaceResult
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
class RoomStore:
|
|
||||||
def __init__(self, config: Config):
|
class RoomStore:
|
||||||
self.config = config
|
def __init__(self, config: Config):
|
||||||
self.rooms: dict[str, Room] = {}
|
self.config = config
|
||||||
self._locks: dict[str, asyncio.Lock] = {}
|
self.rooms: dict[str, Room] = {}
|
||||||
self.db_path = Path(config.RACE_DB_FILE)
|
self._locks: dict[str, asyncio.Lock] = {}
|
||||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
self.db_path = Path(config.RACE_DB_FILE)
|
||||||
self._init_db()
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._initialized = False
|
||||||
def _init_db(self):
|
|
||||||
"""Initialize database tables."""
|
async def _init_db(self):
|
||||||
conn = sqlite3.connect(self.db_path)
|
"""Initialize database tables asynchronously."""
|
||||||
cursor = conn.cursor()
|
if self._initialized:
|
||||||
|
return
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS room_snapshots (
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
scope TEXT PRIMARY KEY,
|
await db.execute("""
|
||||||
state TEXT NOT NULL,
|
CREATE TABLE IF NOT EXISTS room_snapshots (
|
||||||
created_at TEXT NOT NULL,
|
scope TEXT PRIMARY KEY,
|
||||||
horses TEXT NOT NULL,
|
state TEXT NOT NULL,
|
||||||
bets TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
champion_name TEXT,
|
horses TEXT NOT NULL,
|
||||||
tick_count INTEGER DEFAULT 0
|
bets TEXT NOT NULL,
|
||||||
)
|
champion_name TEXT,
|
||||||
""")
|
tick_count INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
cursor.execute("""
|
""")
|
||||||
CREATE TABLE IF NOT EXISTS race_history (
|
|
||||||
race_id TEXT PRIMARY KEY,
|
await db.execute("""
|
||||||
scope TEXT NOT NULL,
|
CREATE TABLE IF NOT EXISTS race_history (
|
||||||
champion_name TEXT NOT NULL,
|
race_id TEXT PRIMARY KEY,
|
||||||
champion_owner TEXT NOT NULL,
|
scope TEXT NOT NULL,
|
||||||
participants TEXT NOT NULL,
|
champion_name TEXT NOT NULL,
|
||||||
bet_distribution TEXT NOT NULL,
|
champion_owner TEXT NOT NULL,
|
||||||
duration_ticks INTEGER NOT NULL,
|
participants TEXT NOT NULL,
|
||||||
completed_at TEXT NOT NULL
|
bet_distribution TEXT NOT NULL,
|
||||||
)
|
duration_ticks INTEGER NOT NULL,
|
||||||
""")
|
completed_at TEXT NOT NULL,
|
||||||
|
point_changes TEXT DEFAULT '{}',
|
||||||
cursor.execute("""
|
point_change_summaries TEXT DEFAULT '{}',
|
||||||
CREATE TABLE IF NOT EXISTS user_horse_names (
|
odds_snapshot TEXT DEFAULT '{}'
|
||||||
user_id TEXT PRIMARY KEY,
|
)
|
||||||
horse_name TEXT NOT NULL
|
""")
|
||||||
)
|
|
||||||
""")
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_horse_names (
|
||||||
conn.commit()
|
user_id TEXT PRIMARY KEY,
|
||||||
conn.close()
|
horse_name TEXT NOT NULL
|
||||||
|
)
|
||||||
def get_lock(self, scope: str) -> asyncio.Lock:
|
""")
|
||||||
"""Get or create per-room lock."""
|
|
||||||
if scope not in self._locks:
|
# Add missing columns if they don't exist (for existing databases)
|
||||||
self._locks[scope] = asyncio.Lock()
|
try:
|
||||||
return self._locks[scope]
|
await db.execute("SELECT point_changes FROM race_history LIMIT 1")
|
||||||
|
except aiosqlite.OperationalError:
|
||||||
def get_room(self, scope: str) -> Optional[Room]:
|
await db.execute("ALTER TABLE race_history ADD COLUMN point_changes TEXT DEFAULT '{}'")
|
||||||
"""Get room by scope."""
|
|
||||||
return self.rooms.get(scope)
|
try:
|
||||||
|
await db.execute("SELECT point_change_summaries FROM race_history LIMIT 1")
|
||||||
def create_room(self, scope: str) -> Room:
|
except aiosqlite.OperationalError:
|
||||||
"""Create new room."""
|
await db.execute("ALTER TABLE race_history ADD COLUMN point_change_summaries TEXT DEFAULT '{}'")
|
||||||
room = Room(scope=scope)
|
|
||||||
self.rooms[scope] = room
|
try:
|
||||||
self._save_snapshot(room)
|
await db.execute("SELECT odds_snapshot FROM race_history LIMIT 1")
|
||||||
return room
|
except aiosqlite.OperationalError:
|
||||||
|
await db.execute("ALTER TABLE race_history ADD COLUMN odds_snapshot TEXT DEFAULT '{}'")
|
||||||
def delete_room(self, scope: str):
|
|
||||||
"""Delete room."""
|
await db.commit()
|
||||||
if scope in self.rooms:
|
|
||||||
del self.rooms[scope]
|
self._initialized = True
|
||||||
|
|
||||||
def get_last_horse_name(self, user_id: str) -> Optional[str]:
|
async def ensure_initialized(self):
|
||||||
conn = sqlite3.connect(self.db_path)
|
"""Ensure database is initialized (call before any DB operation)."""
|
||||||
cursor = conn.cursor()
|
if not self._initialized:
|
||||||
cursor.execute("SELECT horse_name FROM user_horse_names WHERE user_id = ?", (user_id,))
|
await self._init_db()
|
||||||
row = cursor.fetchone()
|
|
||||||
conn.close()
|
def get_lock(self, scope: str) -> asyncio.Lock:
|
||||||
return row[0] if row else None
|
"""Get or create per-room lock."""
|
||||||
|
if scope not in self._locks:
|
||||||
def set_last_horse_name(self, user_id: str, horse_name: str):
|
self._locks[scope] = asyncio.Lock()
|
||||||
conn = sqlite3.connect(self.db_path)
|
return self._locks[scope]
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute(
|
def get_room(self, scope: str) -> Optional[Room]:
|
||||||
"INSERT OR REPLACE INTO user_horse_names (user_id, horse_name) VALUES (?, ?)",
|
"""Get room by scope."""
|
||||||
(user_id, horse_name),
|
return self.rooms.get(scope)
|
||||||
)
|
|
||||||
conn.commit()
|
async def create_room(self, scope: str) -> Room:
|
||||||
conn.close()
|
"""Create new room."""
|
||||||
|
room = Room(scope=scope)
|
||||||
def _save_snapshot(self, room: Room):
|
self.rooms[scope] = room
|
||||||
"""Save room snapshot to database."""
|
await self._save_snapshot(room)
|
||||||
import json
|
return room
|
||||||
|
|
||||||
horses_json = json.dumps({
|
def delete_room(self, scope: str):
|
||||||
name: {
|
"""Delete room."""
|
||||||
"owner_id": horse.owner_id,
|
if scope in self.rooms:
|
||||||
"name": horse.name,
|
del self.rooms[scope]
|
||||||
"index": horse.index,
|
|
||||||
"position": horse.position,
|
async def get_last_horse_name(self, user_id: str) -> Optional[str]:
|
||||||
"state": horse.state.value,
|
await self.ensure_initialized()
|
||||||
}
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
for name, horse in room.horses.items()
|
cursor = await db.execute(
|
||||||
})
|
"SELECT horse_name FROM user_horse_names WHERE user_id = ?",
|
||||||
|
(user_id,)
|
||||||
bets_json = json.dumps([
|
)
|
||||||
{
|
row = await cursor.fetchone()
|
||||||
"user_id": bet.user_id,
|
return row[0] if row else None
|
||||||
"horse_name": bet.horse_name,
|
|
||||||
"amount": bet.amount,
|
async def set_last_horse_name(self, user_id: str, horse_name: str):
|
||||||
}
|
await self.ensure_initialized()
|
||||||
for bet in room.bets
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
])
|
await db.execute(
|
||||||
|
"INSERT OR REPLACE INTO user_horse_names (user_id, horse_name) VALUES (?, ?)",
|
||||||
conn = sqlite3.connect(self.db_path)
|
(user_id, horse_name),
|
||||||
cursor = conn.cursor()
|
)
|
||||||
cursor.execute("""
|
await db.commit()
|
||||||
INSERT OR REPLACE INTO room_snapshots
|
|
||||||
(scope, state, created_at, horses, bets, champion_name, tick_count)
|
async def _save_snapshot(self, room: Room):
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
"""Save room snapshot to database."""
|
||||||
""", (
|
await self.ensure_initialized()
|
||||||
room.scope,
|
|
||||||
room.state.value,
|
horses_json = json.dumps({
|
||||||
room.created_at.isoformat(),
|
name: {
|
||||||
horses_json,
|
"owner_id": horse.owner_id,
|
||||||
bets_json,
|
"name": horse.name,
|
||||||
room.champion_name,
|
"index": horse.index,
|
||||||
room.tick_count,
|
"position": horse.position,
|
||||||
))
|
"state": horse.state.value,
|
||||||
conn.commit()
|
}
|
||||||
conn.close()
|
for name, horse in room.horses.items()
|
||||||
|
})
|
||||||
def save_race_result(self, result: RaceResult):
|
|
||||||
"""Save race result to history."""
|
bets_json = json.dumps([
|
||||||
import json
|
{
|
||||||
|
"user_id": bet.user_id,
|
||||||
conn = sqlite3.connect(self.db_path)
|
"horse_name": bet.horse_name,
|
||||||
cursor = conn.cursor()
|
"amount": bet.amount,
|
||||||
cursor.execute("""
|
}
|
||||||
INSERT INTO race_history
|
for bet in room.bets
|
||||||
(race_id, scope, champion_name, champion_owner, participants, bet_distribution, duration_ticks, completed_at)
|
])
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""", (
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
result.race_id,
|
await db.execute("""
|
||||||
result.scope,
|
INSERT OR REPLACE INTO room_snapshots
|
||||||
result.champion_name,
|
(scope, state, created_at, horses, bets, champion_name, tick_count)
|
||||||
result.champion_owner,
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
json.dumps(result.participants),
|
""", (
|
||||||
json.dumps(result.bet_distribution),
|
room.scope,
|
||||||
result.duration_ticks,
|
room.state.value,
|
||||||
result.completed_at.isoformat(),
|
room.created_at.isoformat(),
|
||||||
))
|
horses_json,
|
||||||
conn.commit()
|
bets_json,
|
||||||
conn.close()
|
room.champion_name,
|
||||||
|
room.tick_count,
|
||||||
|
))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
async def save_race_result(self, result: RaceResult):
|
||||||
|
"""Save race result to history."""
|
||||||
|
await self.ensure_initialized()
|
||||||
|
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
await db.execute("""
|
||||||
|
INSERT INTO race_history
|
||||||
|
(race_id, scope, champion_name, champion_owner, participants,
|
||||||
|
bet_distribution, duration_ticks, completed_at,
|
||||||
|
point_changes, point_change_summaries, odds_snapshot)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
result.race_id,
|
||||||
|
result.scope,
|
||||||
|
result.champion_name,
|
||||||
|
result.champion_owner,
|
||||||
|
json.dumps(result.participants),
|
||||||
|
json.dumps(result.bet_distribution),
|
||||||
|
result.duration_ticks,
|
||||||
|
result.completed_at.isoformat(),
|
||||||
|
json.dumps(getattr(result, 'point_changes', {})),
|
||||||
|
json.dumps(getattr(result, 'point_change_summaries', {})),
|
||||||
|
json.dumps(getattr(result, 'odds_snapshot', {})),
|
||||||
|
))
|
||||||
|
await db.commit()
|
||||||
|
|||||||
@@ -1,413 +1,413 @@
|
|||||||
from nonebot import on_command
|
from nonebot import on_command
|
||||||
from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, PrivateMessageEvent
|
from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, PrivateMessageEvent
|
||||||
|
|
||||||
from . import plugin_config as config
|
from . import plugin_config as config
|
||||||
from .commands import get_scope, check_access, room_store, points_service, race_engine
|
from .commands import get_scope, check_access, race_engine
|
||||||
from .models import Horse, HorseState, RoomState, Bet, RaceResult
|
from .models import Horse, HorseState, RoomState, Bet, RaceResult
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from . import commands as commands_mod
|
from . import commands as commands_mod
|
||||||
|
|
||||||
|
|
||||||
async def check_tester(event: Event) -> bool:
|
async def check_tester(event: Event) -> bool:
|
||||||
"""Check if user is a tester."""
|
"""Check if user is a tester."""
|
||||||
if not config.TEST_MODE:
|
if not config.TEST_MODE:
|
||||||
return False
|
return False
|
||||||
return event.user_id in config.TESTERS
|
return event.user_id in config.TESTERS
|
||||||
|
|
||||||
|
|
||||||
test_reset_points_cmd = on_command("测试重置积分", priority=5)
|
test_reset_points_cmd = on_command("测试重置积分", priority=5)
|
||||||
|
|
||||||
|
|
||||||
@test_reset_points_cmd.handle()
|
@test_reset_points_cmd.handle()
|
||||||
async def handle_test_reset_points(bot: Bot, event: Event):
|
async def handle_test_reset_points(bot: Bot, event: Event):
|
||||||
"""Reset user points to 1000 for testing."""
|
"""Reset user points to 1000 for testing."""
|
||||||
if not await check_tester(event):
|
if not await check_tester(event):
|
||||||
await test_reset_points_cmd.finish("权限不足")
|
await test_reset_points_cmd.finish("权限不足")
|
||||||
return
|
return
|
||||||
|
|
||||||
success, _ = await points_service.set_points(event.user_id, 1000, "测试重置积分")
|
success, _ = await commands_mod.points_service.set_points(event.user_id, 1000, "测试重置积分")
|
||||||
if success:
|
if success:
|
||||||
await test_reset_points_cmd.finish("积分已重置为1000")
|
await test_reset_points_cmd.finish("积分已重置为1000")
|
||||||
else:
|
else:
|
||||||
await test_reset_points_cmd.finish("重置失败")
|
await test_reset_points_cmd.finish("重置失败")
|
||||||
|
|
||||||
|
|
||||||
test_set_points_cmd = on_command("测试设置积分", priority=5)
|
test_set_points_cmd = on_command("测试设置积分", priority=5)
|
||||||
|
|
||||||
|
|
||||||
@test_set_points_cmd.handle()
|
@test_set_points_cmd.handle()
|
||||||
async def handle_test_set_points(bot: Bot, event: Event):
|
async def handle_test_set_points(bot: Bot, event: Event):
|
||||||
"""Set user points for testing."""
|
"""Set user points for testing."""
|
||||||
if not await check_tester(event):
|
if not await check_tester(event):
|
||||||
await test_set_points_cmd.finish("权限不足")
|
await test_set_points_cmd.finish("权限不足")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the message text and extract amount
|
# Get the message text and extract amount
|
||||||
msg = str(event.get_message()).strip()
|
msg = str(event.get_message()).strip()
|
||||||
# Remove command prefix
|
# Remove command prefix
|
||||||
parts = msg.split()
|
parts = msg.split()
|
||||||
|
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
await test_set_points_cmd.finish("请使用: /测试设置积分 <金额>")
|
await test_set_points_cmd.finish("请使用: /测试设置积分 <金额>")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
amount = int(parts[1])
|
amount = int(parts[1])
|
||||||
if amount < 0:
|
if amount < 0:
|
||||||
await test_set_points_cmd.finish("金额必须为非负数")
|
await test_set_points_cmd.finish("金额必须为非负数")
|
||||||
return
|
return
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await test_set_points_cmd.finish("金额必须是整数")
|
await test_set_points_cmd.finish("金额必须是整数")
|
||||||
return
|
return
|
||||||
|
|
||||||
success, _ = await points_service.set_points(event.user_id, amount, f"测试设置积分为{amount}")
|
success, _ = await commands_mod.points_service.set_points(event.user_id, amount, f"测试设置积分为{amount}")
|
||||||
if success:
|
if success:
|
||||||
await test_set_points_cmd.finish(f"积分已设置为 {amount}")
|
await test_set_points_cmd.finish(f"积分已设置为 {amount}")
|
||||||
else:
|
else:
|
||||||
await test_set_points_cmd.finish("设置失败")
|
await test_set_points_cmd.finish("设置失败")
|
||||||
|
|
||||||
|
|
||||||
test_query_points_cmd = on_command("测试查询积分", priority=5)
|
test_query_points_cmd = on_command("测试查询积分", priority=5)
|
||||||
|
|
||||||
|
|
||||||
@test_query_points_cmd.handle()
|
@test_query_points_cmd.handle()
|
||||||
async def handle_test_query_points(bot: Bot, event: Event):
|
async def handle_test_query_points(bot: Bot, event: Event):
|
||||||
"""Query user points for testing."""
|
"""Query user points for testing."""
|
||||||
if not await check_tester(event):
|
if not await check_tester(event):
|
||||||
await test_query_points_cmd.finish("权限不足")
|
await test_query_points_cmd.finish("权限不足")
|
||||||
return
|
return
|
||||||
|
|
||||||
balance = await points_service.get_balance(event.user_id)
|
balance = await commands_mod.points_service.get_balance(event.user_id)
|
||||||
await test_query_points_cmd.finish(f"当前积分: {balance}")
|
await test_query_points_cmd.finish(f"当前积分: {balance}")
|
||||||
|
|
||||||
|
|
||||||
test_clear_room_cmd = on_command("测试清空房间", priority=5)
|
test_clear_room_cmd = on_command("测试清空房间", priority=5)
|
||||||
|
|
||||||
|
|
||||||
@test_clear_room_cmd.handle()
|
@test_clear_room_cmd.handle()
|
||||||
async def handle_test_clear_room(bot: Bot, event: Event):
|
async def handle_test_clear_room(bot: Bot, event: Event):
|
||||||
"""Clear test room."""
|
"""Clear test room."""
|
||||||
if not await check_tester(event):
|
if not await check_tester(event):
|
||||||
await test_clear_room_cmd.finish("权限不足")
|
await test_clear_room_cmd.finish("权限不足")
|
||||||
return
|
return
|
||||||
|
|
||||||
scope = get_scope(event)
|
scope = get_scope(event)
|
||||||
room_store.delete_room(scope)
|
commands_mod.room_store.delete_room(scope)
|
||||||
await test_clear_room_cmd.finish("房间已清空")
|
await test_clear_room_cmd.finish("房间已清空")
|
||||||
|
|
||||||
|
|
||||||
test_force_start_cmd = on_command("测试强制开赛", priority=5)
|
test_force_start_cmd = on_command("测试强制开赛", priority=5)
|
||||||
|
|
||||||
|
|
||||||
@test_force_start_cmd.handle()
|
@test_force_start_cmd.handle()
|
||||||
async def handle_test_force_start(bot: Bot, event: Event):
|
async def handle_test_force_start(bot: Bot, event: Event):
|
||||||
"""Force start race for testing."""
|
"""Force start race for testing."""
|
||||||
if not await check_tester(event):
|
if not await check_tester(event):
|
||||||
await test_force_start_cmd.finish("权限不足")
|
await test_force_start_cmd.finish("权限不足")
|
||||||
return
|
return
|
||||||
|
|
||||||
await test_force_start_cmd.finish("测试强制开赛命令")
|
await test_force_start_cmd.finish("测试强制开赛命令")
|
||||||
|
|
||||||
|
|
||||||
def _generate_random_horse_names(count: int) -> list[str]:
|
def _generate_random_horse_names(count: int) -> list[str]:
|
||||||
prefixes = ["赤焰", "踏雪", "追风", "流星", "疾电", "破晓", "青岚", "玄影", "星尘", "霜刃", "烈阳", "苍穹"]
|
prefixes = ["赤焰", "踏雪", "追风", "流星", "疾电", "破晓", "青岚", "玄影", "星尘", "霜刃", "烈阳", "苍穹"]
|
||||||
cores = ["奔", "跃", "影", "翼", "刃", "雷", "岚", "焰", "星", "雪", "风", "光"]
|
cores = ["奔", "跃", "影", "翼", "刃", "雷", "岚", "焰", "星", "雪", "风", "光"]
|
||||||
suffixes = ["号", "骑", "王", "将", "卫", "客", "影", "者", "马", "军"]
|
suffixes = ["号", "骑", "王", "将", "卫", "客", "影", "者", "马", "军"]
|
||||||
|
|
||||||
names: set[str] = set()
|
names: set[str] = set()
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while len(names) < count and attempts < 500:
|
while len(names) < count and attempts < 500:
|
||||||
attempts += 1
|
attempts += 1
|
||||||
name = f"{random.choice(prefixes)}{random.choice(cores)}{random.choice(suffixes)}"
|
name = f"{random.choice(prefixes)}{random.choice(cores)}{random.choice(suffixes)}"
|
||||||
if len(name) > 10:
|
if len(name) > 10:
|
||||||
name = name[:10]
|
name = name[:10]
|
||||||
names.add(name)
|
names.add(name)
|
||||||
|
|
||||||
while len(names) < count:
|
while len(names) < count:
|
||||||
names.add(f"测试马{len(names) + 1}")
|
names.add(f"测试马{len(names) + 1}")
|
||||||
|
|
||||||
return list(names)[:count]
|
return list(names)[:count]
|
||||||
|
|
||||||
|
|
||||||
test_simulate_race_cmd = on_command("测试模拟赛马", aliases={"测试模拟"}, priority=1, block=True)
|
test_simulate_race_cmd = on_command("测试模拟赛马", aliases={"测试模拟"}, priority=1, block=True)
|
||||||
|
|
||||||
|
|
||||||
class _FakeBot:
|
class _FakeBot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.messages: list[dict] = []
|
self.messages: list[dict] = []
|
||||||
self._next_message_id = 1
|
self._next_message_id = 1
|
||||||
|
|
||||||
async def send_msg(self, **kwargs):
|
async def send_msg(self, **kwargs):
|
||||||
self.messages.append(dict(kwargs))
|
self.messages.append(dict(kwargs))
|
||||||
message_id = self._next_message_id
|
message_id = self._next_message_id
|
||||||
self._next_message_id += 1
|
self._next_message_id += 1
|
||||||
return {"message_id": message_id}
|
return {"message_id": message_id}
|
||||||
|
|
||||||
async def delete_msg(self, message_id: int):
|
async def delete_msg(self, message_id: int):
|
||||||
# Simply record the deletion if needed, or do nothing
|
# Simply record the deletion if needed, or do nothing
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class _InMemoryRoomStore:
|
class _InMemoryRoomStore:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.rooms: dict[str, "commands_mod.Room"] = {}
|
self.rooms: dict[str, "commands_mod.Room"] = {}
|
||||||
self.saved_results: list[RaceResult] = []
|
self.saved_results: list[RaceResult] = []
|
||||||
|
|
||||||
def get_room(self, scope: str):
|
def get_room(self, scope: str):
|
||||||
return self.rooms.get(scope)
|
return self.rooms.get(scope)
|
||||||
|
|
||||||
def create_room(self, scope: str):
|
def create_room(self, scope: str):
|
||||||
room = commands_mod.Room(scope=scope)
|
room = commands_mod.Room(scope=scope)
|
||||||
self.rooms[scope] = room
|
self.rooms[scope] = room
|
||||||
return room
|
return room
|
||||||
|
|
||||||
def delete_room(self, scope: str):
|
def delete_room(self, scope: str):
|
||||||
if scope in self.rooms:
|
if scope in self.rooms:
|
||||||
del self.rooms[scope]
|
del self.rooms[scope]
|
||||||
|
|
||||||
def save_race_result(self, result: RaceResult):
|
def save_race_result(self, result: RaceResult):
|
||||||
self.saved_results.append(result)
|
self.saved_results.append(result)
|
||||||
|
|
||||||
|
|
||||||
class _InMemoryPointsService:
|
class _InMemoryPointsService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.calls: list[tuple[str, dict]] = []
|
self.calls: list[tuple[str, dict]] = []
|
||||||
|
|
||||||
async def reward_champion(self, user_id: str):
|
async def reward_champion(self, user_id: str):
|
||||||
self.calls.append(("reward_champion", {"user_id": user_id}))
|
self.calls.append(("reward_champion", {"user_id": user_id}))
|
||||||
return True, 0
|
return True, 0
|
||||||
|
|
||||||
async def reward_participant(self, user_id: str):
|
async def reward_participant(self, user_id: str):
|
||||||
self.calls.append(("reward_participant", {"user_id": user_id}))
|
self.calls.append(("reward_participant", {"user_id": user_id}))
|
||||||
return True, 0
|
return True, 0
|
||||||
|
|
||||||
async def payout_winnings(self, user_id: str, amount: int, odds: float):
|
async def payout_winnings(self, user_id: str, amount: int, odds: float):
|
||||||
self.calls.append(("payout_winnings", {"user_id": user_id, "amount": amount, "odds": odds}))
|
self.calls.append(("payout_winnings", {"user_id": user_id, "amount": amount, "odds": odds}))
|
||||||
return True, 0
|
return True, 0
|
||||||
|
|
||||||
async def refund_bet_points(self, user_id: str, amount: int, reason: str = "比赛中断退还"):
|
async def refund_bet_points(self, user_id: str, amount: int, reason: str = "比赛中断退还"):
|
||||||
self.calls.append(("refund_bet_points", {"user_id": user_id, "amount": amount, "reason": reason}))
|
self.calls.append(("refund_bet_points", {"user_id": user_id, "amount": amount, "reason": reason}))
|
||||||
return True, 0
|
return True, 0
|
||||||
|
|
||||||
async def get_balance(self, user_id: str) -> int:
|
async def get_balance(self, user_id: str) -> int:
|
||||||
self.calls.append(("get_balance", {"user_id": user_id}))
|
self.calls.append(("get_balance", {"user_id": user_id}))
|
||||||
return 8888
|
return 8888
|
||||||
|
|
||||||
|
|
||||||
class _NoopMessageService:
|
class _NoopMessageService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.last_messages: dict[str, dict[str, str]] = {}
|
self.last_messages: dict[str, dict[str, str]] = {}
|
||||||
|
|
||||||
def clear_pending_recalls(self, scope: str):
|
def clear_pending_recalls(self, scope: str):
|
||||||
if scope in self.last_messages:
|
if scope in self.last_messages:
|
||||||
del self.last_messages[scope]
|
del self.last_messages[scope]
|
||||||
|
|
||||||
async def send_with_recall(self, bot, scope, message_type, message):
|
async def send_with_recall(self, bot, scope, message_type, message):
|
||||||
# Support basic recall for race_update to avoid flooding during simulation
|
# Support basic recall for race_update to avoid flooding during simulation
|
||||||
if message_type == "race_update":
|
if message_type == "race_update":
|
||||||
await self.recall_previous_of_type(bot, scope, "race_update")
|
await self.recall_previous_of_type(bot, scope, "race_update")
|
||||||
|
|
||||||
is_group = scope.startswith("group_")
|
is_group = scope.startswith("group_")
|
||||||
result = await bot.send_msg(
|
result = await bot.send_msg(
|
||||||
message_type="group" if is_group else "private",
|
message_type="group" if is_group else "private",
|
||||||
group_id=int(scope.split("_", 1)[1]) if is_group else None,
|
group_id=int(scope.split("_", 1)[1]) if is_group else None,
|
||||||
user_id=int(scope.split("_", 1)[1]) if not is_group else None,
|
user_id=int(scope.split("_", 1)[1]) if not is_group else None,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
if scope not in self.last_messages:
|
if scope not in self.last_messages:
|
||||||
self.last_messages[scope] = {}
|
self.last_messages[scope] = {}
|
||||||
|
|
||||||
if isinstance(result, dict) and "message_id" in result:
|
if isinstance(result, dict) and "message_id" in result:
|
||||||
self.last_messages[scope][message_type] = result["message_id"]
|
self.last_messages[scope][message_type] = result["message_id"]
|
||||||
|
|
||||||
return "fake_msg_id"
|
return "fake_msg_id"
|
||||||
|
|
||||||
async def recall_previous_of_type(self, bot, scope, message_type):
|
async def recall_previous_of_type(self, bot, scope, message_type):
|
||||||
if scope in self.last_messages and message_type in self.last_messages[scope]:
|
if scope in self.last_messages and message_type in self.last_messages[scope]:
|
||||||
msg_id = self.last_messages[scope][message_type]
|
msg_id = self.last_messages[scope][message_type]
|
||||||
try:
|
try:
|
||||||
await bot.delete_msg(message_id=msg_id)
|
await bot.delete_msg(message_id=msg_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
del self.last_messages[scope][message_type]
|
del self.last_messages[scope][message_type]
|
||||||
|
|
||||||
|
|
||||||
@test_simulate_race_cmd.handle()
|
@test_simulate_race_cmd.handle()
|
||||||
async def handle_test_simulate_race(bot: Bot, event: Event):
|
async def handle_test_simulate_race(bot: Bot, event: Event):
|
||||||
if not await check_tester(event):
|
if not await check_tester(event):
|
||||||
await test_simulate_race_cmd.send("权限不足")
|
await test_simulate_race_cmd.send("权限不足")
|
||||||
return
|
return
|
||||||
|
|
||||||
await test_simulate_race_cmd.send("收到:测试模拟赛马,开始执行完全模拟(无真实积分/数据库副作用)")
|
await test_simulate_race_cmd.send("收到:测试模拟赛马,开始执行完全模拟(无真实积分/数据库副作用)")
|
||||||
|
|
||||||
raw_msg = str(event.get_message()).strip()
|
raw_msg = str(event.get_message()).strip()
|
||||||
stream_progress = ("展示" in raw_msg) or ("实时" in raw_msg) or ("慢" in raw_msg)
|
stream_progress = ("展示" in raw_msg) or ("实时" in raw_msg) or ("慢" in raw_msg)
|
||||||
|
|
||||||
scope = get_scope(event)
|
scope = get_scope(event)
|
||||||
try:
|
try:
|
||||||
race_engine.stop_race(scope)
|
race_engine.stop_race(scope)
|
||||||
room_store.delete_room(scope)
|
await commands_mod.room_store.delete_room(scope)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
original_room_store = commands_mod.room_store
|
original_room_store = commands_mod.room_store
|
||||||
original_points_service = commands_mod.points_service
|
original_points_service = commands_mod.points_service
|
||||||
original_message_service = commands_mod.message_service
|
original_message_service = commands_mod.message_service
|
||||||
original_tick_interval = commands_mod.config.RACE_TICK_INTERVAL
|
original_tick_interval = commands_mod.config.RACE_TICK_INTERVAL
|
||||||
original_stop_race = commands_mod.race_engine.stop_race
|
original_stop_race = commands_mod.race_engine.stop_race
|
||||||
original_send_to_scope = commands_mod._send_to_scope
|
original_send_to_scope = commands_mod._send_to_scope
|
||||||
|
|
||||||
|
|
||||||
fake_room_store = _InMemoryRoomStore()
|
fake_room_store = _InMemoryRoomStore()
|
||||||
fake_points_service = _InMemoryPointsService()
|
fake_points_service = _InMemoryPointsService()
|
||||||
fake_message_service = _NoopMessageService()
|
fake_message_service = _NoopMessageService()
|
||||||
fake_bot = _FakeBot()
|
fake_bot = _FakeBot()
|
||||||
|
|
||||||
start_task: asyncio.Task | None = None
|
start_task: asyncio.Task | None = None
|
||||||
room = None
|
room = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await test_simulate_race_cmd.send("阶段:构建沙盒环境与参赛数据")
|
await test_simulate_race_cmd.send("阶段:构建沙盒环境与参赛数据")
|
||||||
commands_mod.room_store = fake_room_store
|
commands_mod.room_store = fake_room_store
|
||||||
commands_mod.points_service = fake_points_service
|
commands_mod.points_service = fake_points_service
|
||||||
commands_mod.message_service = fake_message_service
|
commands_mod.message_service = fake_message_service
|
||||||
commands_mod.config.RACE_TICK_INTERVAL = 1 if stream_progress else 0
|
commands_mod.config.RACE_TICK_INTERVAL = 1 if stream_progress else 0
|
||||||
commands_mod.race_engine.stop_race = lambda _scope: commands_mod.race_engine.active_tasks.pop(_scope, None)
|
commands_mod.race_engine.stop_race = lambda _scope: commands_mod.race_engine.active_tasks.pop(_scope, None)
|
||||||
|
|
||||||
progress_count = 0
|
progress_count = 0
|
||||||
max_progress = 30
|
max_progress = 30
|
||||||
|
|
||||||
async def _test_send_to_scope(_bot: Bot, _scope: str, message: str, *args, **kwargs):
|
async def _test_send_to_scope(_bot: Bot, _scope: str, message: str, *args, **kwargs):
|
||||||
nonlocal progress_count
|
nonlocal progress_count
|
||||||
await fake_bot.send_msg(message_type="private", user_id=event.user_id, message=message)
|
await fake_bot.send_msg(message_type="private", user_id=event.user_id, message=message)
|
||||||
if not stream_progress:
|
if not stream_progress:
|
||||||
return
|
return
|
||||||
|
|
||||||
if message.startswith("【第") and "回合】" in message:
|
if message.startswith("【第") and "回合】" in message:
|
||||||
progress_count += 1
|
progress_count += 1
|
||||||
if progress_count > max_progress:
|
if progress_count > max_progress:
|
||||||
return
|
return
|
||||||
await original_send_to_scope(bot, scope, message, *args, **kwargs)
|
await original_send_to_scope(bot, scope, message, *args, **kwargs)
|
||||||
|
|
||||||
commands_mod._send_to_scope = _test_send_to_scope
|
commands_mod._send_to_scope = _test_send_to_scope
|
||||||
|
|
||||||
room = fake_room_store.create_room(scope)
|
room = fake_room_store.create_room(scope)
|
||||||
horse_names = _generate_random_horse_names(8)
|
horse_names = _generate_random_horse_names(8)
|
||||||
for idx, horse_name in enumerate(horse_names, start=1):
|
for idx, horse_name in enumerate(horse_names, start=1):
|
||||||
owner_id = f"sim_user_{idx}"
|
owner_id = f"sim_user_{idx}"
|
||||||
room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, index=idx, state=HorseState.RACING)
|
room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, index=idx, state=HorseState.RACING)
|
||||||
room.next_horse_index = len(horse_names) + 1
|
room.next_horse_index = len(horse_names) + 1
|
||||||
|
|
||||||
bet_amount = max(commands_mod.config.MIN_BET, 10)
|
bet_amount = max(commands_mod.config.MIN_BET, 10)
|
||||||
room.bets.append(Bet(user_id="bettor_1", horse_name=horse_names[0], amount=bet_amount))
|
room.bets.append(Bet(user_id="bettor_1", horse_name=horse_names[0], amount=bet_amount))
|
||||||
room.bets.append(Bet(user_id="bettor_2", horse_name=horse_names[1], amount=bet_amount * 2))
|
room.bets.append(Bet(user_id="bettor_2", horse_name=horse_names[1], amount=bet_amount * 2))
|
||||||
|
|
||||||
room.state = RoomState.WAITING
|
room.state = RoomState.WAITING
|
||||||
|
|
||||||
for horse in room.horses.values():
|
for horse in room.horses.values():
|
||||||
horse.state = HorseState.RACING
|
horse.state = HorseState.RACING
|
||||||
|
|
||||||
await test_simulate_race_cmd.send("阶段:执行赛程(后台任务)")
|
await test_simulate_race_cmd.send("阶段:执行赛程(后台任务)")
|
||||||
start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope))
|
start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope))
|
||||||
commands_mod.race_engine.register_task(scope, start_task)
|
commands_mod.race_engine.register_task(scope, start_task)
|
||||||
await asyncio.wait_for(start_task, timeout=180 if stream_progress else 30)
|
await asyncio.wait_for(start_task, timeout=180 if stream_progress else 30)
|
||||||
|
|
||||||
messages = [str(m.get("message", "")) for m in fake_bot.messages]
|
messages = [str(m.get("message", "")) for m in fake_bot.messages]
|
||||||
if not messages:
|
if not messages:
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:未捕获到任何消息")
|
await test_simulate_race_cmd.send("完全模拟失败:未捕获到任何消息")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not any("比赛开始!" in msg for msg in messages):
|
if not any("比赛开始!" in msg for msg in messages):
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:未发送开赛消息")
|
await test_simulate_race_cmd.send("完全模拟失败:未发送开赛消息")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Look for the start message to verify horse names
|
# Look for the start message to verify horse names
|
||||||
start_msg = next((msg for msg in messages if "比赛开始!" in msg), "")
|
start_msg = next((msg for msg in messages if "比赛开始!" in msg), "")
|
||||||
for idx, horse_name in enumerate(horse_names, start=1):
|
for idx, horse_name in enumerate(horse_names, start=1):
|
||||||
if f"{idx:02d}号 {horse_name}" not in start_msg:
|
if f"{idx:02d}号 {horse_name}" not in start_msg:
|
||||||
await test_simulate_race_cmd.send(f"完全模拟失败:开赛名单中未找到 {idx:02d}号 {horse_name}")
|
await test_simulate_race_cmd.send(f"完全模拟失败:开赛名单中未找到 {idx:02d}号 {horse_name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
progress_messages = [msg for msg in messages if "【第" in msg and "回合】" in msg]
|
progress_messages = [msg for msg in messages if "【第" in msg and "回合】" in msg]
|
||||||
if not progress_messages:
|
if not progress_messages:
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:未发送回合进度消息")
|
await test_simulate_race_cmd.send("完全模拟失败:未发送回合进度消息")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check first progress message format
|
# Check first progress message format
|
||||||
progress_lines = [line for line in progress_messages[0].splitlines() if "|" in line]
|
progress_lines = [line for line in progress_messages[0].splitlines() if "|" in line]
|
||||||
if len(progress_lines) != len(horse_names):
|
if len(progress_lines) != len(horse_names):
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:回合进度展示条目数量不匹配")
|
await test_simulate_race_cmd.send("完全模拟失败:回合进度展示条目数量不匹配")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not any("比赛结束!冠军:" in msg for msg in messages):
|
if not any("比赛结束!冠军:" in msg for msg in messages):
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:未发送结束结算消息")
|
await test_simulate_race_cmd.send("完全模拟失败:未发送结束结算消息")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not any("积分变化:" in msg for msg in messages):
|
if not any("积分变化:" in msg for msg in messages):
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:未发送积分变化总结")
|
await test_simulate_race_cmd.send("完全模拟失败:未发送积分变化总结")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not fake_room_store.saved_results:
|
if not fake_room_store.saved_results:
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:未写入赛史结果(内存)")
|
await test_simulate_race_cmd.send("完全模拟失败:未写入赛史结果(内存)")
|
||||||
return
|
return
|
||||||
|
|
||||||
saved = fake_room_store.saved_results[-1]
|
saved = fake_room_store.saved_results[-1]
|
||||||
if saved.champion_name not in room.horses:
|
if saved.champion_name not in room.horses:
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:赛史冠军不在参赛马匹中")
|
await test_simulate_race_cmd.send("完全模拟失败:赛史冠军不在参赛马匹中")
|
||||||
return
|
return
|
||||||
if not saved.point_changes:
|
if not saved.point_changes:
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:赛史未记录积分变化")
|
await test_simulate_race_cmd.send("完全模拟失败:赛史未记录积分变化")
|
||||||
return
|
return
|
||||||
if not saved.point_change_summaries:
|
if not saved.point_change_summaries:
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:赛史未记录积分变化总结")
|
await test_simulate_race_cmd.send("完全模拟失败:赛史未记录积分变化总结")
|
||||||
return
|
return
|
||||||
|
|
||||||
champion_owner_id = room.horses[saved.champion_name].owner_id
|
champion_owner_id = room.horses[saved.champion_name].owner_id
|
||||||
reward_champion_calls = [c for c in fake_points_service.calls if c[0] == "reward_champion"]
|
reward_champion_calls = [c for c in fake_points_service.calls if c[0] == "reward_champion"]
|
||||||
if not reward_champion_calls or reward_champion_calls[0][1]["user_id"] != champion_owner_id:
|
if not reward_champion_calls or reward_champion_calls[0][1]["user_id"] != champion_owner_id:
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:未正确发放冠军奖励(内存记录)")
|
await test_simulate_race_cmd.send("完全模拟失败:未正确发放冠军奖励(内存记录)")
|
||||||
return
|
return
|
||||||
|
|
||||||
participant_calls = [c for c in fake_points_service.calls if c[0] == "reward_participant"]
|
participant_calls = [c for c in fake_points_service.calls if c[0] == "reward_participant"]
|
||||||
if len(participant_calls) != len(room.horses):
|
if len(participant_calls) != len(room.horses):
|
||||||
await test_simulate_race_cmd.send("完全模拟失败:参赛奖励次数不匹配(内存记录)")
|
await test_simulate_race_cmd.send("完全模拟失败:参赛奖励次数不匹配(内存记录)")
|
||||||
return
|
return
|
||||||
|
|
||||||
await test_simulate_race_cmd.send(
|
await test_simulate_race_cmd.send(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
[
|
[
|
||||||
"完全模拟赛马完成(无真实积分/数据库副作用)",
|
"完全模拟赛马完成(无真实积分/数据库副作用)",
|
||||||
f"参赛马匹:{', '.join(horse_names)}",
|
f"参赛马匹:{', '.join(horse_names)}",
|
||||||
f"冠军:{saved.champion_name}(马主:{saved.champion_owner})",
|
f"冠军:{saved.champion_name}(马主:{saved.champion_owner})",
|
||||||
f"总回合:{saved.duration_ticks}",
|
f"总回合:{saved.duration_ticks}",
|
||||||
f"消息条数:{len(messages)}(开赛/进度/结算均已覆盖)",
|
f"消息条数:{len(messages)}(开赛/进度/结算均已覆盖)",
|
||||||
f"结算调用:{len(fake_points_service.calls)}(冠军/参赛/下注派奖)",
|
f"结算调用:{len(fake_points_service.calls)}(冠军/参赛/下注派奖)",
|
||||||
f"积分变化用户数:{len(saved.point_changes)}",
|
f"积分变化用户数:{len(saved.point_changes)}",
|
||||||
f"过程展示:{'开启' if stream_progress else '关闭'}",
|
f"过程展示:{'开启' if stream_progress else '关闭'}",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
ticks = room.tick_count if room else 0
|
ticks = room.tick_count if room else 0
|
||||||
await test_simulate_race_cmd.send(f"完全模拟失败:超时未完成(当前回合:{ticks})")
|
await test_simulate_race_cmd.send(f"完全模拟失败:超时未完成(当前回合:{ticks})")
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
ticks = room.tick_count if room else 0
|
ticks = room.tick_count if room else 0
|
||||||
await test_simulate_race_cmd.send(f"完全模拟失败:任务被取消(当前回合:{ticks})")
|
await test_simulate_race_cmd.send(f"完全模拟失败:任务被取消(当前回合:{ticks})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tail = "\n".join(traceback.format_exc().splitlines()[-8:])
|
tail = "\n".join(traceback.format_exc().splitlines()[-8:])
|
||||||
await test_simulate_race_cmd.send(f"完全模拟异常:{type(e).__name__}: {e}\n{tail}")
|
await test_simulate_race_cmd.send(f"完全模拟异常:{type(e).__name__}: {e}\n{tail}")
|
||||||
finally:
|
finally:
|
||||||
if start_task and not start_task.done():
|
if start_task and not start_task.done():
|
||||||
start_task.cancel()
|
start_task.cancel()
|
||||||
commands_mod.room_store = original_room_store
|
commands_mod.room_store = original_room_store
|
||||||
commands_mod.points_service = original_points_service
|
commands_mod.points_service = original_points_service
|
||||||
commands_mod.message_service = original_message_service
|
commands_mod.message_service = original_message_service
|
||||||
commands_mod.config.RACE_TICK_INTERVAL = original_tick_interval
|
commands_mod.config.RACE_TICK_INTERVAL = original_tick_interval
|
||||||
commands_mod.race_engine.stop_race = original_stop_race
|
commands_mod.race_engine.stop_race = original_stop_race
|
||||||
commands_mod._send_to_scope = original_send_to_scope
|
commands_mod._send_to_scope = original_send_to_scope
|
||||||
|
|||||||
Reference in New Issue
Block a user