feat(赛马): 为马匹添加序号并优化积分结算展示
- 为 Horse 模型添加 index 字段,用于唯一标识马匹序号 - 在报名时自动分配递增序号,并在所有展示中使用固定序号排序 - 新增积分变化计算功能,在比赛结果中展示每位用户的积分变化和总结描述 - 支持通过序号或马匹名下注,优化用户交互体验 - 添加用户上次马名记忆功能,允许重复使用马名报名 - 更新测试用例以验证序号展示和积分变化功能
This commit is contained in:
@@ -84,6 +84,108 @@ def get_event_id(event: Event) -> str:
|
||||
return str(event.user_id)
|
||||
|
||||
|
||||
def _normalize_horse_name(horse_name: str) -> str:
|
||||
return horse_name.strip().casefold()
|
||||
|
||||
|
||||
def _get_horses_in_order(room: Room) -> list[Horse]:
|
||||
return sorted(room.horses.values(), key=lambda horse: horse.index)
|
||||
|
||||
|
||||
def _format_horse_label(horse: Horse) -> str:
|
||||
return f"{horse.index:02d}号 {horse.name}"
|
||||
|
||||
|
||||
def _find_user_horse(room: Room, user_id: str) -> Horse | None:
|
||||
for horse in _get_horses_in_order(room):
|
||||
if horse.owner_id == user_id:
|
||||
return horse
|
||||
return None
|
||||
|
||||
|
||||
def _find_duplicate_horse(room: Room, horse_name: str) -> Horse | None:
|
||||
normalized_name = _normalize_horse_name(horse_name)
|
||||
for horse in room.horses.values():
|
||||
if _normalize_horse_name(horse.name) == normalized_name:
|
||||
return horse
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_horse_selector(room: Room, selector: str) -> Horse | None:
|
||||
selector = selector.strip()
|
||||
if selector.isdigit():
|
||||
target_index = int(selector)
|
||||
for horse in room.horses.values():
|
||||
if horse.index == target_index:
|
||||
return horse
|
||||
return None
|
||||
|
||||
normalized_selector = _normalize_horse_name(selector)
|
||||
for horse in room.horses.values():
|
||||
if _normalize_horse_name(horse.name) == normalized_selector:
|
||||
return horse
|
||||
return None
|
||||
|
||||
|
||||
def _describe_points_delta(delta: int) -> str:
|
||||
if delta >= 300:
|
||||
return "血赚翻倍"
|
||||
if delta >= 150:
|
||||
return "大赚特赚"
|
||||
if delta > 0:
|
||||
return "小有收获"
|
||||
if delta == 0:
|
||||
return "稳住不亏"
|
||||
if delta <= -300:
|
||||
return "倾家荡产"
|
||||
if delta <= -150:
|
||||
return "伤筋动骨"
|
||||
return "略有损失"
|
||||
|
||||
|
||||
def _build_point_changes(room: Room, odds: dict[str, float]) -> tuple[dict[str, int], dict[str, str]]:
|
||||
point_changes: dict[str, int] = {}
|
||||
|
||||
for bet in room.bets:
|
||||
point_changes[bet.user_id] = point_changes.get(bet.user_id, 0) - bet.amount
|
||||
|
||||
champion = room.horses.get(room.champion_name)
|
||||
if champion:
|
||||
point_changes[champion.owner_id] = point_changes.get(champion.owner_id, 0) + config.CHAMPION_REWARD
|
||||
|
||||
for horse in room.horses.values():
|
||||
if champion and horse.owner_id != champion.owner_id:
|
||||
point_changes[horse.owner_id] = point_changes.get(horse.owner_id, 0) + config.PARTICIPANT_REWARD
|
||||
|
||||
for bet in room.bets:
|
||||
if bet.horse_name == room.champion_name:
|
||||
payout = int(bet.amount * odds.get(bet.horse_name, config.MIN_ODDS))
|
||||
point_changes[bet.user_id] = point_changes.get(bet.user_id, 0) + payout
|
||||
|
||||
point_summaries = {
|
||||
user_id: _describe_points_delta(delta)
|
||||
for user_id, delta in point_changes.items()
|
||||
}
|
||||
return point_changes, point_summaries
|
||||
|
||||
|
||||
def _format_point_change_lines(room: Room, point_changes: dict[str, int], point_summaries: dict[str, str]) -> list[str]:
|
||||
ordered_user_ids: list[str] = []
|
||||
for horse in _get_horses_in_order(room):
|
||||
if horse.owner_id not in ordered_user_ids:
|
||||
ordered_user_ids.append(horse.owner_id)
|
||||
for bet in room.bets:
|
||||
if bet.user_id not in ordered_user_ids:
|
||||
ordered_user_ids.append(bet.user_id)
|
||||
|
||||
lines = ["积分变化:"]
|
||||
for user_id in ordered_user_ids:
|
||||
delta = point_changes.get(user_id, 0)
|
||||
summary = point_summaries.get(user_id, _describe_points_delta(delta))
|
||||
lines.append(f" {user_id} {delta:+d} 积分 · {summary}")
|
||||
return lines
|
||||
|
||||
|
||||
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)
|
||||
@@ -98,11 +200,11 @@ def calculate_odds(room: Room) -> dict[str, float]:
|
||||
return odds
|
||||
|
||||
|
||||
async def settle_race(room: Room):
|
||||
async def settle_race(room: Room) -> RaceResult | None:
|
||||
"""Settle bets and rewards after race finishes."""
|
||||
champion = room.horses.get(room.champion_name)
|
||||
if not champion:
|
||||
return
|
||||
return None
|
||||
|
||||
# Reward champion owner
|
||||
await points_service.reward_champion(champion.owner_id)
|
||||
@@ -119,17 +221,21 @@ async def settle_race(room: Room):
|
||||
await points_service.payout_winnings(bet.user_id, bet.amount, odds.get(bet.horse_name, config.MIN_ODDS))
|
||||
|
||||
# Save race result
|
||||
point_changes, point_summaries = _build_point_changes(room, odds)
|
||||
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()],
|
||||
participants=[h.name for h in _get_horses_in_order(room)],
|
||||
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(),
|
||||
point_changes=point_changes,
|
||||
point_change_summaries=point_summaries,
|
||||
)
|
||||
room_store.save_race_result(result)
|
||||
return result
|
||||
|
||||
|
||||
async def _send_to_scope(bot: Bot, scope: str, message: str):
|
||||
@@ -152,8 +258,7 @@ 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())
|
||||
horse_list = "\n".join(f" {_format_horse_label(h)}" for h in _get_horses_in_order(room))
|
||||
await _send_to_scope(bot, scope, f"比赛开始!\n{horse_list}")
|
||||
|
||||
# Race loop with progress updates
|
||||
@@ -177,13 +282,12 @@ async def run_race_with_settlement(bot: Bot, room: Room, scope: str):
|
||||
return
|
||||
|
||||
# Settle
|
||||
await settle_race(room)
|
||||
result = 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"比赛结束!冠军:{_format_horse_label(champion) if champion else 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]
|
||||
@@ -192,6 +296,8 @@ async def run_race_with_settlement(bot: Bot, room: Room, scope: str):
|
||||
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}")
|
||||
if result:
|
||||
result_lines.extend(_format_point_change_lines(room, result.point_changes, result.point_change_summaries))
|
||||
|
||||
await _send_to_scope(bot, scope, "\n".join(result_lines))
|
||||
|
||||
@@ -213,13 +319,13 @@ 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 ""
|
||||
user_id = get_event_id(event)
|
||||
horse_name = parts[1].strip() if len(parts) > 1 else room_store.get_last_horse_name(user_id) or ""
|
||||
|
||||
if not horse_name:
|
||||
await register_cmd.finish("请输入马匹名:/赛马报名 <马匹名>")
|
||||
await register_cmd.finish("请输入马匹名:/赛马报名 <马匹名>。若你之前报过名,也可以直接发送 /赛马报名 复用上一次绑定的马名")
|
||||
return
|
||||
|
||||
if len(horse_name) > 10:
|
||||
@@ -227,7 +333,6 @@ async def handle_register(bot: Bot, event: Event):
|
||||
return
|
||||
|
||||
scope = get_scope(event)
|
||||
user_id = get_event_id(event)
|
||||
lock = room_store.get_lock(scope)
|
||||
|
||||
async with lock:
|
||||
@@ -243,21 +348,24 @@ async def handle_register(bot: Bot, event: Event):
|
||||
await register_cmd.finish("房间已满(最多8匹马)")
|
||||
return
|
||||
|
||||
if horse_name in room.horses:
|
||||
await register_cmd.finish(f"马匹名 \"{horse_name}\" 已被使用")
|
||||
duplicate_horse = _find_duplicate_horse(room, horse_name)
|
||||
if duplicate_horse:
|
||||
await register_cmd.finish(f"马匹名 \"{horse_name}\" 已被 {_format_horse_label(duplicate_horse)} 使用")
|
||||
return
|
||||
|
||||
# Check if user already registered
|
||||
for h in room.horses.values():
|
||||
if h.owner_id == user_id:
|
||||
await register_cmd.finish("你已经报名了,不能重复报名")
|
||||
return
|
||||
existing_user_horse = _find_user_horse(room, user_id)
|
||||
if existing_user_horse:
|
||||
await register_cmd.finish(f"你已经报名了,当前马匹为 {_format_horse_label(existing_user_horse)}")
|
||||
return
|
||||
|
||||
# Create horse
|
||||
room.horses[horse_name] = Horse(owner_id=user_id, name=horse_name)
|
||||
horse_index = room.next_horse_index
|
||||
room.next_horse_index += 1
|
||||
room.horses[horse_name] = Horse(owner_id=user_id, name=horse_name, index=horse_index)
|
||||
room_store.set_last_horse_name(user_id, horse_name)
|
||||
|
||||
count = len(room.horses)
|
||||
await register_cmd.finish(f"报名成功!马匹 \"{horse_name}\" 已加入比赛({count}/8)")
|
||||
registered_horse = room.horses[horse_name]
|
||||
await register_cmd.finish(f"报名成功!{_format_horse_label(registered_horse)} 已加入比赛({count}/8)")
|
||||
|
||||
|
||||
cancel_cmd = on_command("赛马取消报名", priority=5)
|
||||
@@ -284,27 +392,19 @@ async def handle_cancel(bot: Bot, event: Event):
|
||||
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
|
||||
|
||||
user_horse = _find_user_horse(room, user_id)
|
||||
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]
|
||||
bets_to_refund = [b for b in room.bets if b.horse_name == user_horse.name]
|
||||
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]
|
||||
room.bets = [b for b in room.bets if b.horse_name != user_horse.name]
|
||||
|
||||
# Remove horse
|
||||
del room.horses[user_horse]
|
||||
del room.horses[user_horse.name]
|
||||
|
||||
await cancel_cmd.finish(f"已取消报名,马匹 \"{user_horse}\" 已退出")
|
||||
await cancel_cmd.finish(f"已取消报名,{_format_horse_label(user_horse)} 已退出")
|
||||
|
||||
|
||||
bet_cmd = on_command("赛马下注", priority=5)
|
||||
@@ -317,14 +417,13 @@ async def handle_bet(bot: Bot, event: 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("请使用:/赛马下注 <马匹名> <金额>")
|
||||
await bet_cmd.finish("请使用:/赛马下注 <序号|马匹名> <金额>")
|
||||
return
|
||||
|
||||
horse_name = parts[1]
|
||||
horse_selector = parts[1]
|
||||
try:
|
||||
amount = int(parts[2])
|
||||
except ValueError:
|
||||
@@ -349,26 +448,24 @@ async def handle_bet(bot: Bot, event: Event):
|
||||
await bet_cmd.finish("比赛正在进行中,无法下注")
|
||||
return
|
||||
|
||||
if horse_name not in room.horses:
|
||||
await bet_cmd.finish(f"马匹 \"{horse_name}\" 不存在")
|
||||
horse = _resolve_horse_selector(room, horse_selector)
|
||||
if not horse:
|
||||
await bet_cmd.finish(f"马匹序号/名称 \"{horse_selector}\" 不存在")
|
||||
return
|
||||
|
||||
# Can't bet on your own horse
|
||||
if room.horses[horse_name].owner_id == user_id:
|
||||
if horse.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}")
|
||||
success, balance = await points_service.spend_bet_points(user_id, amount, f"下注 {_format_horse_label(horse)}")
|
||||
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))
|
||||
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})")
|
||||
await bet_cmd.finish(f"下注成功!{_format_horse_label(horse)} {amount}积分(当前赔率:{odds.get(horse.name, config.MIN_ODDS):.2f})")
|
||||
|
||||
|
||||
odds_cmd = on_command("赛马赔率", priority=5)
|
||||
@@ -394,9 +491,10 @@ async def handle_odds(bot: Bot, event: Event):
|
||||
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})")
|
||||
for horse in _get_horses_in_order(room):
|
||||
odd = odds.get(horse.name, config.MIN_ODDS)
|
||||
horse_bet = sum(b.amount for b in room.bets if b.horse_name == horse.name)
|
||||
lines.append(f" {_format_horse_label(horse)} - {odd:.2f}倍 (总下注: {horse_bet})")
|
||||
lines.append(f"总下注池: {total_bet}")
|
||||
|
||||
await odds_cmd.finish("\n".join(lines))
|
||||
@@ -449,7 +547,8 @@ async def handle_help(bot: Bot, event: Event):
|
||||
help_text = """赛马命令帮助:
|
||||
/赛马报名 <马匹名> - 报名参赛
|
||||
/赛马取消报名 - 取消报名并退还下注
|
||||
/赛马下注 <马匹名> <金额> - 下注(不能给自己的马下注)
|
||||
/赛马报名 - 复用上一次绑定的马名报名
|
||||
/赛马下注 <序号|马匹名> <金额> - 下注(不能给自己的马下注)
|
||||
/赛马赔率 - 查看当前赔率
|
||||
/赛马开赛 - 开始比赛(至少2匹马)
|
||||
/赛马帮助 - 显示此帮助"""
|
||||
|
||||
Reference in New Issue
Block a user