19 Commits

Author SHA1 Message Date
3f3c21c3d6 修复停牌操作的字典key错误 2025-10-31 16:27:15 +08:00
b07b044035 21点游戏规则更新 2025-10-31 15:57:26 +08:00
9e16774d2f 修复21点中的结算错误 2025-10-31 15:39:53 +08:00
17da2e6111 修复数据库错误 2025-10-31 12:19:00 +08:00
1f10799562 实现21点与轮盘游戏 2025-10-31 12:09:07 +08:00
cef684f64b Save 2025-10-31 11:58:35 +08:00
7962852685 feat: 鎵╁睍鏁版嵁搴撴敮鎸佽疆鐩樺拰21鐐规父鎴?- 娣诲姞鍒楀瓨鍦ㄦ€ф鏌ヨ緟鍔╂柟娉?- 鎵╁睍casino_sessions鍜宑asino_bets琛ㄦ坊鍔犳柊瀛楁 - 鍒涘缓casino_blackjack_hands琛ㄥ瓨鍌?1鐐规墜鐗?- 淇敼create_casino_session鏀寔鍗曞満闄愬埗鍜屾柊瀛楁 - 鎵╁睍create_casino_bet鏀寔杞洏鍜?1鐐逛笓鐢ㄥ瓧娈?- 娣诲姞21鐐规墜鐗岀鐞嗘柟娉曢泦 2025-10-31 11:46:29 +08:00
cc97374e98 同步get_user_display_name 2025-10-31 11:16:38 +08:00
a62d5d66b7 t branch
Merge branch 'task/add-idiom-chain-game_2025-10-28_1'
2025-10-31 11:10:55 +08:00
19cde88acf 更新统一接口BaseGame.db.get_user_display_name获取用户名称 2025-10-31 11:07:41 +08:00
6f05ca98f1 修复冒险完成后不回收奖励就进行炼金会丢失奖励的问题 2025-10-31 10:30:29 +08:00
5a8a4ac026 比大小游戏修改 2025-10-30 17:27:15 +08:00
12aac846cc 新增赌场系统 2025-10-30 16:11:50 +08:00
4bddd4339f 发现的加分覆盖签到与长时间任务被清理的错误, 已经在数据库修复中尝试解决 2025-10-30 15:03:00 +08:00
927c16e1fc 新增say指令 2025-10-30 14:27:38 +08:00
8ffd261bb0 1.修改ai返回格式为markdown2.新增冒险放弃指令 2025-10-30 11:57:46 +08:00
3d4f754a0a Merge branch 'main' of http://www.liubai.site:3000/ninemine/WPSBot 2025-10-30 11:05:27 +08:00
0a60a7fc4b 修复指令解析错误 2025-10-30 11:05:08 +08:00
82dc616495 Merge feature: AI瀵硅瘽鍔熻兘瀹炵幇瀹屾垚 2025-10-30 01:11:29 +08:00
13 changed files with 3218 additions and 40 deletions

View File

@@ -107,6 +107,39 @@ WPSBotGame/
- 阻碍因素:无
- 状态:成功
## 2025-10-30_00:00:00
- 已修改:
- games/adventure.py新增放弃指令分支新增 `_abandon_adventure`,更新帮助)
- games/base.py在全局帮助中添加放弃用法
- 更改:
1. 新增冒险放弃功能:
- 支持指令:`.adventure abandon``.adventure 放弃`
- 结算规则:最低倍率 × 已冒险分钟向下取整至少1分钟
- 发放奖励后删除状态
2. 帮助信息更新:
- 本地帮助与全局帮助均加入放弃用法说明
- 原因:允许用户在冒险过程中主动放弃并按最低倍率获得奖励
- 阻碍因素:无
- 状态:成功
## 2025-10-31_10:27:59
- 已修改:
- games/alchemy.py修复冒险任务完成后自动删除状态导致奖励丢失的bug
- 更改:
1. **Bug修复**:修复冒险任务完成后奖励丢失问题
- **问题**:在 `_perform_alchemy` 中,当检测到冒险任务已完成时,代码会自动删除冒险状态(`self.db.delete_game_state`),但没有发放奖励,导致用户奖励丢失
- **修复**:移除自动删除逻辑,改为提示用户先使用 `.adventure` 回收奖励
- **修改前**:冒险完成后自动删除状态,允许炼金(导致奖励丢失)
- **修改后**:冒险完成后提示用户先回收奖励,不允许炼金,确保奖励只能通过 `.adventure` 命令回收
2. **行为变更**
- 冒险进行中:提示剩余时间,不允许炼金(保持不变)
- 冒险已完成:提示先回收奖励,不允许炼金(修复后)
- 用户使用 `.adventure`:发放奖励并删除状态(保持不变)
- 状态已删除:可以正常炼金(保持不变)
- 原因修复冒险任务完成后自动删除状态导致奖励丢失的严重bug确保用户必须先主动回收奖励才能继续其他操作
- 阻碍因素:无
- 状态:成功
# 详细实施记录
## 文件修改清单
@@ -135,7 +168,7 @@ WPSBotGame/
- 使用 `get_game_state(0, user_id, 'adventure')` 查询状态
- 如果存在状态:
* 计算剩余时间
* 如果已完成:自动删除状态,允许继续
* 如果已完成:提示用户先使用 `.adventure` 回收奖励不允许炼金2025-10-31修复避免奖励丢失移除自动删除逻辑
* 如果未完成返回错误消息显示剩余时间X分Y秒
- 异常处理:捕获状态数据异常,自动清理损坏状态
@@ -191,7 +224,7 @@ state_data = {
### 游戏互斥机制
- 炼金前检查:查询冒险状态
- 如果冒险进行中:返回错误,显示剩余时间
- 如果冒险已完成:自动清理状态,允许炼金
- 如果冒险已完成:提示用户先使用 `.adventure` 回收奖励,不允许炼金(修复后:确保奖励不会丢失)
- 状态异常:自动清理,允许继续操作
# 最终审查
@@ -205,6 +238,7 @@ state_data = {
- ✅ 游戏互斥:冒险期间阻止炼金操作
- ✅ 指令注册:`.adventure` 和 `.冒险` 指令正常工作
- ✅ 帮助信息:显示在全局帮助中
- ✅ 冒险放弃:`.adventure abandon` / `.adventure 放弃` 按最低倍率结算已冒险分钟并清理状态
## 代码质量
- ✅ 所有语法检查通过

View File

@@ -364,3 +364,65 @@ llama-index-llms-ollama>=0.1.0
## 实施与计划匹配度
实施与计划完全匹配 ✅
## 补充分析Markdown 渲染与发送通道2025-10-30
### 现状观察
- `routers/callback.py` 中仅当 `response_text.startswith('#')` 时才通过 `send_markdown()` 发送,否则使用 `send_text()`。这意味着即使 AI 返回了合法的 Markdown但不以 `#` 开头例如代码块、列表、表格、普通段落等也会被按纯文本通道发送导致下游WPS 侧)不进行 Markdown 渲染。
- `games/ai_chat.py` 的 `_generate_response()` 直接返回 `str(response)`,未对内容类型进行标注或判定,上层仅依赖首字符为 `#` 的启发式判断来选择发送通道。
- `utils/message.py` 已具备 `send_markdown()` 与 `send_text()` 两种发送方式,对应 `{"msgtype":"markdown"}` 与 `{"msgtype":"text"}` 消息结构;当前缺少自动识别 Markdown 的逻辑。
### 影响
- 当 AI 返回包含 Markdown 元素但非标题(不以 `#` 开头)的内容时,用户端看到的是未渲染的原始 Markdown 文本,表现为“格式不能成功排版”。
### 待确认问题(不含解决方案,需产品/实现口径)
1. 目标平台WPS 机器人)对 Markdown 的要求是否仅需 `msgtype=markdown` 即可渲染?是否存在必须以标题开头的限制?
2. 期望策略:
- 是否希望“.ai 的所有回复”统一走 Markdown 通道?
- 还是需要基于 Markdown 特征进行判定(如代码块、列表、链接、表格、行内格式等)?
3. 兼容性:若统一改为 Markdown 通道,是否会影响既有纯文本展示(例如换行、转义、表情)?
4. 其他指令模块是否也可能返回 Markdown若有是否一并纳入同一策略
### 相关代码参照点(路径)
- `routers/callback.py`:回复通道选择逻辑(基于 `startswith('#')`
- `games/ai_chat.py`AI 回复内容生成与返回(直接返回字符串)
- `utils/message.py``send_markdown()` 与 `send_text()` 的消息结构
### 决策结论与范围2025-10-30
- 分支策略:不创建新分支,继续在当前任务上下文内推进。
- 发送策略:`.ai` 产生的回复统一按 Markdown 发送。
- 影响范围:仅限 AI 对话功能(`.ai`/`ai_chat`),不扩展到其他指令模块。
# 任务进度(补充)
## [2025-10-30_??:??:??] 标注 Markdown 渲染问题(记录现状与待确认项)
- 已修改:
- `.tasks/2025-10-29_3_ai_chat.md`补充“Markdown 渲染与发送通道”分析与待确认清单(仅问题陈述,无解决方案)。
- 更改:
- 明确当前仅以标题开头触发 Markdown 发送的启发式导致部分 Markdown 未被渲染。
- 原因:
- 用户反馈“AI 返回内容支持 Markdown但当前直接当作文本返回导致无法正确排版”。
- 阻碍因素:
- 目标平台的 Markdown 渲染细节与统一策略选择待确认。
- 状态:
- 未确认(等待策略口径与平台渲染规范确认)。
## [2025-10-30_11:40:31] 执行AI 回复统一按 Markdown 发送(仅限 AI
- 已修改:
- `routers/callback.py`:在 `callback_receive()` 的发送阶段,当 `game_type == 'ai_chat'` 且存在 `response_text` 时,无条件调用 `send_markdown(response_text)`;若发送异常,记录日志并回退到 `send_text(response_text)`;其他指令模块继续沿用 `startswith('#')` 的启发式逻辑。
- 更改:
- 使 `.ai` 产生的回复在 WPS 端稳定触发 Markdown 渲染,不再依赖以 `#` 开头。
- 原因:
- 对齐“统一按 Markdown 发送(仅限 AI”的决策解决 Markdown 文本被当作纯文本发送导致的排版问题。
- 阻碍因素:
- 暂无。
- 状态:
- 成功。

View File

@@ -0,0 +1,506 @@
# 背景
文件名2025-10-30_1_add_casino_games.md
创建于2025-10-30_15:16:56
创建者admin
主分支main
任务分支task/add_casino_games_2025-01-14_1
Yolo模式Off
# 任务描述
在项目中新增赌场常见游戏,支持多个玩家下注等功能,使用积分系统模拟真实的赌场环境。
要求:
- 指令格式:`.赌场 <游戏类型> <参数>`
- 采用模块化设计,便于扩展多种赌场游戏
- 支持多人同时下注
- 集成现有积分系统
- 记录下注和收益数据
# 项目概览
基于WPS协作开放平台的自定义机器人游戏系统。使用FastAPI + SQLite架构已有完善的积分系统和多款游戏五子棋、成语接龙等。需要通过模块化设计添加赌场游戏功能。
# 分析
## 现有系统分析
1. **积分系统**:已实现 `add_points()``consume_points()` 方法
2. **游戏基类**`BaseGame` 提供统一的接口
3. **路由系统**:通过 `CommandParser` 解析指令,在 `callback.py` 中路由
4. **数据库**SQLite已有用户表、游戏状态表、统计表
## 需要新增的内容
1. **数据库表**
- `casino_bets`:记录所有下注
- `casino_results`:记录游戏结果和结算
- `casino_games`:记录游戏房间(可选)
2. **游戏模块**
- `games/casino.py`:主赌场模块
- 第一期支持:大小游戏
- 第二期计划:轮盘、二十一点等
3. **指令映射**
- `.赌场` -> casino 游戏类型
- 子指令:`轮盘``大小``21点`
## 设计要点
1. **模块化设计**:每种赌场游戏作为独立类
2. **下注流程**:创建房间 -> 玩家下注 -> 结算 -> 分发奖励
3. **安全性**:下注前检查积分,结算时原子性操作
4. **多玩家支持**:以 chat_id 为单位创建游戏房间
# 提议的解决方案
## 指令设计
采用 `.赌场 <游戏类型> <操作> <参数>` 的模块化结构:
### 大小游戏
- **庄家开启游戏**`.赌场 大小 open <最小下注> <最大下注> <赔率>`
- 示例:`.赌场 大小 open 10 100 2.0` 最小10分最大100分赔率2.0倍)
- **玩家下注**`.赌场 大小 bet <大小/小> <下注金额>`
- 示例:`.赌场 大小 bet 大 50` 下注50分压大
- 示例:`.赌场 大小 bet 小 30` 下注30分压小
- **查看状态**`.赌场 大小 status`
- **庄家结算**`.赌场 大小 settle <结果>`
- 示例:`.赌场 大小 settle 大` (开大)
- 示例:`.赌场 大小 settle 小` (开小)
### 轮盘游戏(二期实现)
- 暂不实现,等大小游戏完善后再扩展
### 21点游戏二期实现
- 暂不实现,等大小游戏完善后再扩展
## 游戏流程
1. 庄家开启游戏(指定下注限额和赔率参数)
2. 玩家下注(可多人同时参与)
3. 庄家确认结算(手动触发结果)
4. 系统自动分发奖励
## 数据库设计
### 新增表casino_bets下注记录表
```sql
CREATE TABLE IF NOT EXISTS casino_bets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
game_type TEXT NOT NULL,
user_id INTEGER NOT NULL,
bet_type TEXT NOT NULL, -- '大' 或 '小'
amount INTEGER NOT NULL,
multiplier REAL NOT NULL, -- 赔率
status TEXT DEFAULT 'pending', -- pending/settled/cancelled
result TEXT, -- 游戏结果
win_amount INTEGER, -- 赢得金额
created_at INTEGER NOT NULL,
settled_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users(user_id)
)
```
### 新增表casino_sessions游戏会话表
```sql
CREATE TABLE IF NOT EXISTS casino_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
game_type TEXT NOT NULL,
banker_id INTEGER NOT NULL, -- 庄家ID
min_bet INTEGER NOT NULL,
max_bet INTEGER NOT NULL,
multiplier REAL NOT NULL,
house_fee REAL DEFAULT 0.05, -- 抽水率默认5%
status TEXT DEFAULT 'open', -- open/settling/closed
created_at INTEGER NOT NULL,
settled_at INTEGER,
UNIQUE(chat_id, game_type, status)
)
```
### 索引
- `casino_bets(chat_id, game_type, status)` - 快速查询待结算下注
- `casino_sessions(chat_id, game_type, status)` - 快速查询活跃游戏
## 方案对比
### 方案A纯数据库表方案推荐
**优点**
- 数据结构清晰,便于查询统计
- 支持历史记录追踪
- 并发安全,利用数据库事务
- 易于扩展复杂查询
**缺点**
- 需要维护额外的表结构
- 稍微复杂一些
**决策**:采用此方案
### 方案Bgame_states + JSON方案
**优点**
- 复用现有系统
- 实现简单
**缺点**
- 难以进行复杂统计查询
- JSON解析性能较差
- 数据格式不够规范化
## 核心实现细节
### 1. 游戏流程控制
- **开启游戏**检查是否已有活跃游戏同一chat_id只能有一个进行中的游戏
- **下注限制**检查session状态、下注金额范围、玩家积分
- **结算控制**只有庄家可以结算结算后自动关闭session
### 2. 下注流程
1. 检查是否有活跃的session
2. 检查下注金额是否符合min/max限制
3. 检查用户积分是否充足
4. 扣除下注金额consume_points
5. 记录下注到casino_bets表
### 3. 结算流程
1. 验证是否为庄家操作
2. 查询所有pending状态的下注
3. 计算每个玩家的输赢
4. 使用数据库事务确保原子性:
- 更新bets状态
- 发放/扣除积分
- 更新session状态
5. 返回结算报告
### 4. 抽水机制
- **抽水率**5%可配置存储在session.house_fee中
- **抽水时机**:从玩家的赢得金额中扣除
- **抽水归属**:归系统所有(不返还给庄家)
- **计算方式**
- 玩家赢得 = 下注金额 × 赔率
- 实际发放 = 赢得金额 × (1 - 抽水率)
- 抽水金额 = 赢得金额 × 抽水率
### 5. 错误处理
- 下注时积分不足:给出明确提示
- 重复下注:允许(可下多注)
- 非法下注金额:给出范围提示
- 非庄家尝试结算:拒绝
## 安全性
- 下注前检查积分
- 结算时使用数据库事务保证原子性
- 抽水机制保护庄家(虽然抽水归系统)
- 验证庄家身份
- 防止重复结算
# 详细实施计划
## 文件1: core/database.py
### 修改函数: init_tables()
在现有表创建之后约第130行添加赌场相关表的创建
位置:在 `user_points` 表创建之后约第130行添加
```python
# 赌场下注记录表
cursor.execute("""
CREATE TABLE IF NOT EXISTS casino_bets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
game_type TEXT NOT NULL,
user_id INTEGER NOT NULL,
bet_type TEXT NOT NULL,
amount INTEGER NOT NULL,
multiplier REAL NOT NULL,
status TEXT DEFAULT 'pending',
result TEXT,
win_amount INTEGER,
created_at INTEGER NOT NULL,
settled_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users (user_id)
)
""")
# 赌场游戏会话表
cursor.execute("""
CREATE TABLE IF NOT EXISTS casino_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
game_type TEXT NOT NULL,
banker_id INTEGER NOT NULL,
min_bet INTEGER NOT NULL,
max_bet INTEGER NOT NULL,
multiplier REAL NOT NULL,
house_fee REAL DEFAULT 0.05,
status TEXT DEFAULT 'open',
created_at INTEGER NOT NULL,
settled_at INTEGER,
UNIQUE(chat_id, game_type, status)
)
""")
# 创建索引
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_casino_bets
ON casino_bets(chat_id, game_type, status)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_casino_sessions
ON casino_sessions(chat_id, game_type, status)
""")
```
### 新增函数: create_casino_session()
函数签名:
```python
def create_casino_session(self, chat_id: int, game_type: str, banker_id: int,
min_bet: int, max_bet: int, multiplier: float,
house_fee: float = 0.05) -> int:
```
功能创建新的赌场游戏会话返回session_id
### 新增函数: get_active_casino_session()
函数签名:
```python
def get_active_casino_session(self, chat_id: int, game_type: str) -> Optional[Dict]:
```
功能:获取活跃的游戏会话
### 新增函数: create_casino_bet()
函数签名:
```python
def create_casino_bet(self, chat_id: int, game_type: str, user_id: int,
bet_type: str, amount: int, multiplier: float) -> int:
```
功能创建下注记录返回bet_id
### 新增函数: get_pending_bets()
函数签名:
```python
def get_pending_bets(self, chat_id: int, game_type: str) -> List[Dict]:
```
功能:获取待结算的下注列表
### 新增函数: settle_casino_bets()
函数签名:
```python
def settle_casino_bets(self, chat_id: int, game_type: str, result: str,
banker_id: int) -> Dict:
```
功能结算所有下注返回结算详情字典winners, losers, total_win等
### 新增函数: close_casino_session()
函数签名:
```python
def close_casino_session(self, chat_id: int, game_type: str):
```
功能:关闭游戏会话
## 文件2: games/casino.py新建
### 类: CasinoGame
继承自 `BaseGame`
### 方法: __init__()
初始化数据库连接
### 方法: async handle(command, chat_id, user_id) -> str
主处理函数,解析指令并调用相应的处理方法
解析逻辑:
- 提取命令参数,格式:`.赌场 <游戏类型> <操作> <参数>`
- 识别游戏类型(第一期只支持"大小"
- 分发到相应的处理方法
### 方法: async _handle_bigsmall(command, args, chat_id, user_id) -> str
处理大小游戏的各种操作
支持的操作:
- open: 开启游戏
- bet: 下注
- status: 查看状态
- settle: 结算
### 方法: async _open_bigsmall(args, chat_id, user_id) -> str
庄家开启大小游戏
参数解析:`<最小下注> <最大下注> <赔率>`
参数验证和限制
### 方法: async _bet_bigsmall(args, chat_id, user_id) -> str
玩家下注
参数解析:`<大小/小> <下注金额>`
检查session、金额范围、用户积分
### 方法: async _status_bigsmall(chat_id, game_type) -> str
查看当前游戏状态
### 方法: async _settle_bigsmall(args, chat_id, user_id) -> str
庄家结算游戏
参数解析:`<大/小>`
验证庄家身份,结算所有下注
### 方法: get_help() -> str
返回帮助信息
## 文件3: utils/parser.py
### 修改: COMMAND_MAP
添加赌场指令映射:
```python
# 赌场系统
'.赌场': 'casino',
'.casino': 'casino',
```
## 文件4: routers/callback.py
### 修改: async handle_command()
在AI对话系统之后约第209行添加
```python
# 赌场系统
if game_type == 'casino':
from games.casino import CasinoGame
game = CasinoGame()
return await game.handle(command, chat_id, user_id)
```
## 文件5: games/base.py
### 修改: get_help_message()
在积分赠送系统之后添加赌场游戏帮助:
```python
### 🎰 赌场系统
- `.赌场 大小 open <最小> <最大> <赔率>` - 庄家开启大小游戏
- `.赌场 大小 bet </> <金额>` - 下注
- `.赌场 大小 status` - 查看状态
- `.赌场 大小 settle </>` - 庄家结算
```
## 实施清单
1. 修改 `core/database.py``init_tables()` 方法,添加赌场表创建和索引
2.`core/database.py` 中添加 `create_casino_session()` 方法
3.`core/database.py` 中添加 `get_active_casino_session()` 方法
4.`core/database.py` 中添加 `create_casino_bet()` 方法
5.`core/database.py` 中添加 `get_pending_bets()` 方法
6.`core/database.py` 中添加 `settle_casino_bets()` 方法
7.`core/database.py` 中添加 `close_casino_session()` 方法
8. 创建文件 `games/casino.py`,定义 `CasinoGame`
9.`games/casino.py` 中实现 `__init__()` 方法
10.`games/casino.py` 中实现 `async handle()` 方法
11.`games/casino.py` 中实现 `async _handle_bigsmall()` 方法
12.`games/casino.py` 中实现 `async _open_bigsmall()` 方法
13.`games/casino.py` 中实现 `async _bet_bigsmall()` 方法
14.`games/casino.py` 中实现 `async _status_bigsmall()` 方法
15.`games/casino.py` 中实现 `async _settle_bigsmall()` 方法
16.`games/casino.py` 中实现 `get_help()` 方法
17. 修改 `utils/parser.py`,在 COMMAND_MAP 中添加赌场指令映射
18. 修改 `routers/callback.py`,在 `handle_command()` 中添加赌场路由
19. 修改 `games/base.py`,在 `get_help_message()` 中添加赌场帮助信息
20. 测试所有功能点,确保无错误
# 当前执行步骤:"2. 详细技术规划完成,等待进入实现阶段"
# 任务进度
[2025-10-30_15:16:56]
- 已修改:创建任务文件 `.tasks/2025-10-30_1_add_casino_games.md`
- 更改:创建任务分支 `task/add_casino_games_2025-01-14_1` 和任务文件
- 原因按照RIPER-5协议建立工作基础
- 阻碍因素:无
- 状态:成功
[2025-10-30_16:30:00](预估时间)
- 已修改:完成详细技术规划
- 更改:设计数据库表结构、游戏流程、抽水机制等细节
- 原因:为实施阶段提供详细技术规范
- 阻碍因素:无
- 状态:成功
[2025-10-30_16:07:57]
- 已修改core/database.py, games/casino.py, utils/parser.py, routers/callback.py, games/base.py
- 更改完成所有实施步骤1-19
- 添加赌场表创建和索引
- 实现6个数据库方法create_casino_session, get_active_casino_session, create_casino_bet, get_pending_bets, settle_casino_bets, close_casino_session
- 创建完整的CasinoGame类实现大小游戏所有功能
- 注册指令映射和路由
- 添加帮助信息
- 原因:按照详细实施计划完成全部功能开发
- 阻碍因素:无
- 状态:成功
[2025-10-30_17:20:00](预估时间)
- 已修改games/casino.py
- 更改:修改结算逻辑,从庄家指定结果改为系统随机生成
- 移除庄家输入种子/结果的参数
- 使用random.random()生成随机结果50%大/50%小)
- 更新帮助信息settle命令不再需要参数
- 原因:用户反馈庄家不应该能够操控游戏结果,庄家也是玩家
- 阻碍因素:无
- 状态:成功
[2025-10-30_17:26:19]
- 已修改games/casino.py, games/base.py
- 更改:添加庄家放弃游戏功能
- 新增_cancel_bigsmall()方法处理放弃逻辑
- 放弃时返还所有玩家下注
- 关闭会话并标记下注为cancelled
- 添加cancel命令支持cancel/放弃/关闭)
- 更新帮助信息和base.py中的帮助
- 原因:用户要求庄家可以放弃本轮游戏
- 阻碍因素:无
- 状态:成功
[2025-10-31_11:35:18]
- 已修改core/database.py
- 更改扩展数据库支持轮盘和21点游戏
- 添加列存在性检查辅助方法_column_exists, _add_column_if_not_exists
- 扩展casino_sessions表添加current_phase和blackjack_multiplier字段兼容性检查
- 扩展casino_bets表添加bet_category、bet_number、bet_value、hand_status字段兼容性检查
- 创建casino_blackjack_hands表存储21点游戏手牌数据
- 修改create_casino_session()支持单场限制检查get_any_active_casino_session和新字段
- 扩展create_casino_bet()支持轮盘和21点专用字段参数
- 添加21点手牌管理方法create_blackjack_hand、get_blackjack_hand、update_blackjack_hand、get_all_blackjack_hands
- 原因为轮盘和21点游戏提供数据库支持确保字段分离和向后兼容
- 阻碍因素:无
- 状态:成功
[2025-10-31_15:15:08]
- 已修改core/database.py
- 更改修复大小游戏结算时的UNIQUE约束冲突问题
- 移除casino_sessions表的UNIQUE(chat_id, game_type, status)约束
- 原因status='closed'时需要允许多条历史记录UNIQUE约束阻止了结算时更新status
- 添加兼容性迁移逻辑检测旧版本表结构自动重建表以移除UNIQUE约束
- 迁移时复制所有历史数据,处理外键关系(临时禁用/启用外键检查)
- 单场限制通过应用层逻辑get_any_active_casino_session保证
- 原因:用户测试大小游戏结算时遇到"UNIQUE constraint failed"错误
- 阻碍因素:无
- 状态:成功
[2025-10-31_15:15:08]
- 已修改core/database.py
- 更改修复21点游戏结算逻辑问题
- 修正losers统计逻辑将条件从`not player_is_busted and player_points != banker_points`改为`player_points != banker_points`
- 原因原条件排除了爆牌玩家导致爆牌玩家未被统计到losers列表
- 修正数据库更新逻辑:明确区分三种情况
- 赢家:发放奖励并更新数据库
- 平局player_points == banker_points已返还下注更新数据库
- 输家else分支包括爆牌和点数小于庄家更新数据库
- 改进结果字符串显示:包含玩家和庄家的状态信息(爆牌、黑杰克等)
- 例如:"庄家19点 vs 玩家爆牌" 或 "庄家19点 vs 玩家20点黑杰克"
- 原因用户测试21点游戏时发现3人游戏中只有1个赢家被结算1个爆牌玩家和1个平局玩家未被结算
- 阻碍因素:无
- 状态:成功
# 最终审查
待完成

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,10 @@ class AdventureGame(BaseGame):
if args in ['help', '帮助', 'info']:
return self._get_adventure_help()
# 放弃当前冒险(按最低倍率结算已冒险时间)
if args in ['abandon', '放弃']:
return await self._abandon_adventure(chat_id, user_id)
# 默认冒险耗时1分钟
else:
# 解析消耗时间
@@ -170,6 +174,69 @@ class AdventureGame(BaseGame):
return text
async def _abandon_adventure(self, chat_id: int, user_id: int) -> str:
"""放弃当前冒险,按最低倍率结算已冒险时间
Args:
chat_id: 会话ID使用0作为用户级标识
user_id: 用户ID
Returns:
放弃结果消息
"""
try:
# 查询冒险状态
state = self.db.get_game_state(0, user_id, 'adventure')
if not state:
return "❌ 当前没有进行中的冒险,可使用 `.adventure` 开始新的冒险。"
state_data = state.get('state_data', {})
start_time = state_data.get('start_time')
cost_time = state_data.get('cost_time')
if start_time is None or cost_time is None:
# 状态异常,清理并提示
self.db.delete_game_state(0, user_id, 'adventure')
return "⚠️ 冒险状态异常已清理,请使用 `.adventure` 重新开始。"
current_time = int(time.time())
elapsed_seconds = max(0, current_time - int(start_time))
elapsed_minutes = elapsed_seconds // 60
if elapsed_minutes < 1:
elapsed_minutes = 1
# 计算最低倍率
try:
min_multiplier = min(m for _, m, _ in self.prize_pool)
except Exception:
# 兜底若奖池异常按0.5处理
min_multiplier = 0.5
reward_points = int(min_multiplier * elapsed_minutes)
if reward_points < 0:
reward_points = 0
# 发放奖励并清理状态
if reward_points > 0:
self.db.add_points(user_id, reward_points, "adventure", "冒险放弃奖励")
self.db.delete_game_state(0, user_id, 'adventure')
# 查询当前积分
updated_points = self.db.get_user_points(user_id)
# 输出
text = f"## ⚡️ 冒险放弃\n\n"
text += f"**已计入时间**: {elapsed_minutes} 分钟\n\n"
text += f"**最低倍率**: {min_multiplier}\n\n"
text += f"**获得积分**: {reward_points}\n\n"
text += f"**当前积分**: {updated_points['points']}\n\n"
text += "---\n\n"
text += "💡 提示:可随时使用 `.adventure` 再次踏上冒险之旅!"
return text
except Exception as e:
logger.error(f"放弃冒险时出错: {e}", exc_info=True)
# 失败时不影响原状态,返回提示
return f"❌ 放弃冒险失败:{str(e)}"
def _draw_prize(self, prize_pool: list) -> dict:
"""从奖品池中抽取奖品
@@ -209,6 +276,8 @@ class AdventureGame(BaseGame):
text += f"- `.adventure time` - 消耗time分钟进行冒险, 最少一分钟\n"
text += f"### 其他功能\n"
text += f"- `.adventure abandon` - 放弃当前冒险,按最低倍率结算已冒险时间\n"
text += f"- `.adventure 放弃` - 放弃当前冒险,按最低倍率结算已冒险时间\n"
text += f"- `.adventure help` - 查看帮助\n\n"
return text

View File

@@ -92,9 +92,9 @@ class AlchemyGame(BaseGame):
end_time = start_time + cost_time * 60
remaining_seconds = end_time - current_time
# 如果冒险已完成,自动清理状态,允许炼金
# 如果冒险已完成,提示用户先回收奖励,不允许炼金
if remaining_seconds <= 0:
self.db.delete_game_state(0, user_id, 'adventure')
return f"❌ 你有待回收的冒险奖励!\n\n💡 请先使用 `.adventure` 回收冒险奖励后再进行炼金。"
else:
# 冒险未完成,返回错误提示
remaining_minutes = remaining_seconds // 60

View File

@@ -100,6 +100,8 @@ def get_help_message() -> str:
- `.adventure` - 消耗1分钟进行冒险
- `.冒险` - 消耗1分钟进行冒险
- `.adventure 5` - 消耗5分钟进行冒险
- `.adventure abandon` - 放弃当前冒险,按最低倍率结算已冒险时间
- `.adventure 放弃` - 放弃当前冒险,按最低倍率结算已冒险时间
- `.adventure help` - 查看冒险帮助
### 🎁 积分赠送系统
@@ -111,6 +113,31 @@ def get_help_message() -> str:
- `.ai <问题>` - 向AI提问支持多用户对话等待10秒后回答
- `.aiconfig host=xxx port=xxx model=xxx` - 配置Ollama服务地址和模型
### 🎰 赌场系统
**大小游戏**
- `.赌场 大小 open <最小> <最大> <赔率>` - 庄家开启大小游戏
- `.赌场 大小 bet <大/小> <金额>` - 下注
- `.赌场 大小 status` - 查看状态
- `.赌场 大小 settle` - 庄家结算(系统随机)
- `.赌场 大小 cancel` - 庄家放弃游戏(返还下注)
**轮盘游戏**
- `.赌场 轮盘 open <最小> <最大>` - 庄家开启轮盘游戏
- `.赌场 轮盘 bet <类型> <选项> <金额>` - 下注(数字/颜色/奇偶/大小/区间)
- `.赌场 轮盘 status` - 查看状态
- `.赌场 轮盘 settle` - 庄家结算系统随机0-36
- `.赌场 轮盘 cancel` - 庄家放弃游戏(返还下注)
**21点游戏**
- `.赌场 21点 open <最小> <最大> [黑杰克倍数]` - 庄家开启21点游戏
- `.赌场 21点 bet <金额>` - 下注
- `.赌场 21点 deal` - 庄家发牌
- `.赌场 21点 hit` - 玩家要牌
- `.赌场 21点 stand` - 玩家停牌
- `.赌场 21点 status` - 查看状态
- `.赌场 21点 settle` - 庄家结算
- `.赌场 21点 cancel` - 庄家放弃游戏(返还下注)
### 其他
- `.help` - 显示帮助
- `.stats` - 查看个人统计

1473
games/casino.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -82,12 +82,10 @@ class GiftGame(BaseGame):
receiver_id = user['user_id']
# 获取接收者名称用于显示
receiver_user = self.db.get_or_create_user(receiver_id)
receiver_name = receiver_user.get('username', f"用户{receiver_id}")
receiver_name = self.db.get_user_display_name(receiver_id)
# 获取发送者名称用于显示
sender_user = self.db.get_or_create_user(sender_id)
sender_name = sender_user.get('username', f"用户{sender_id}")
sender_name = self.db.get_user_display_name(sender_id)
# 验证参数
if points <= 0:
@@ -184,7 +182,7 @@ class GiftGame(BaseGame):
for record in records:
timestamp = datetime.fromtimestamp(record['created_at']).strftime('%m-%d %H:%M')
receiver_name = record.get('receiver_name', f"用户{record['receiver_id']}")
receiver_name = self.db.get_user_display_name(record['receiver_id'])
points = record['points']
message = record.get('message', '')
@@ -214,7 +212,7 @@ class GiftGame(BaseGame):
for record in records:
timestamp = datetime.fromtimestamp(record['created_at']).strftime('%m-%d %H:%M')
sender_name = record.get('sender_name', f"用户{record['sender_id']}")
sender_name = self.db.get_user_display_name(record['sender_id'])
points = record['points']
message = record.get('message', '')

View File

@@ -379,10 +379,12 @@ class IdiomGame(BaseGame):
text += f"**当前链长**{state_data['chain_length']}\n\n"
user_count = state_data['participants'][str(user_id)]
text += f"@用户{user_id} 成功次数:{user_count}\n\n"
user_display_name = self.db.get_user_display_name(user_id)
text += f"@{user_display_name} 成功次数:{user_count}\n\n"
if mentioned_user_id:
text += f"已指定 @用户{mentioned_user_id} 接龙\n\n"
mentioned_display_name = self.db.get_user_display_name(mentioned_user_id)
text += f"已指定 @{mentioned_display_name} 接龙\n\n"
else:
text += "任何人都可以接龙\n\n"
@@ -416,7 +418,8 @@ class IdiomGame(BaseGame):
state_data['next_user_id'] = next_user_id
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
return f"✅ 已指定 @用户{next_user_id} 接龙"
next_user_display_name = self.db.get_user_display_name(next_user_id)
return f"✅ 已指定 @{next_user_display_name} 接龙"
def _reject_idiom(self, chat_id: int, user_id: int, idiom: str) -> str:
"""裁判拒绝词语
@@ -509,7 +512,8 @@ class IdiomGame(BaseGame):
# 下一位
if state_data.get('next_user_id'):
text += f"**下一位**@用户{state_data['next_user_id']}\n\n"
next_user_display_name = self.db.get_user_display_name(state_data['next_user_id'])
text += f"**下一位**@{next_user_display_name}\n\n"
else:
text += f"**下一位**:任何人都可以接龙\n\n"
@@ -522,7 +526,8 @@ class IdiomGame(BaseGame):
reverse=True
)
for idx, (uid, count) in enumerate(sorted_participants[:5], 1):
text += f"{idx}. @用户{uid} - {count}\n"
user_display_name = self.db.get_user_display_name(int(uid))
text += f"{idx}. @{user_display_name} - {count}\n"
text += "\n"
# 最近成语
@@ -588,7 +593,8 @@ class IdiomGame(BaseGame):
reverse=True
)
for idx, (uid, count) in enumerate(sorted_participants, 1):
text += f"{idx}. @用户{uid} - {count}\n"
user_display_name = self.db.get_user_display_name(int(uid))
text += f"{idx}. @{user_display_name} - {count}\n"
# 更新统计
try:
for _ in range(count):

View File

@@ -126,7 +126,7 @@ class PointsGame(BaseGame):
for i, user in enumerate(leaderboard):
rank = i + 1
medal = medals[i] if i < len(medals) else "🏅"
username = user.get('username', f"用户{user['user_id']}")
username = self.db.get_user_display_name(user['user_id'])
points = user.get('points', 0)
text += f"{medal} **第 {rank} 名** {username}\n"

View File

@@ -76,14 +76,20 @@ async def callback_receive(request: Request):
# 发送回复
if response_text:
sender = get_message_sender()
# 根据内容选择消息类型
if response_text.startswith('#'):
# Markdown格式
await sender.send_markdown(response_text)
# AI 对话:统一按 Markdown 发送(按任务决策)
if game_type == 'ai_chat':
try:
await sender.send_markdown(response_text)
except Exception as send_md_err:
logger.error(f"发送Markdown消息失败改用文本发送: {send_md_err}")
await sender.send_text(response_text)
else:
# 普通文本
await sender.send_text(response_text)
# 其他模块保持原有启发式:以 # 开头视为 Markdown否则文本
if response_text.startswith('#'):
await sender.send_markdown(response_text)
else:
await sender.send_text(response_text)
return JSONResponse({"result": "ok"})
@@ -187,12 +193,27 @@ async def handle_command(game_type: str, command: str,
game = GiftGame()
return await game.handle(command, chat_id, user_id)
# 复述功能
if game_type == 'say':
# 提取参数并原样返回
_, args = CommandParser.extract_command_args(command)
args = args.strip()
if not args:
return "用法:.say 你想让我说的话\n别名:.说 / .复述"
return args
# AI对话系统
if game_type == 'ai_chat':
from games.ai_chat import AIChatGame
game = AIChatGame()
return await game.handle(command, chat_id, user_id)
# 赌场系统
if game_type == 'casino':
from games.casino import CasinoGame
game = CasinoGame()
return await game.handle(command, chat_id, user_id)
# 未知游戏类型
logger.warning(f"未知游戏类型: {game_type}")
return "❌ 未知的游戏类型"

View File

@@ -68,6 +68,11 @@ class CommandParser:
'.ai': 'ai_chat',
'.aiconfig': 'ai_chat',
# 复述
'.say': 'say',
'.说': 'say',
'.复述': 'say',
# 帮助
'.help': 'help',
'.帮助': 'help',
@@ -75,6 +80,10 @@ class CommandParser:
# 统计
'.stats': 'stats',
'.统计': 'stats',
# 赌场系统
'.赌场': 'casino',
'.casino': 'casino',
}
# 机器人名称模式(用于从@消息中提取)
@@ -98,11 +107,20 @@ class CommandParser:
if at_match:
content = at_match.group(1).strip()
# 检查是否以指令开头
for cmd_prefix, game_type in cls.COMMAND_MAP.items():
if content == cmd_prefix:
# 返回游戏类型和完整指令
return game_type, content
# 拦截全角空格与全角标点(不允许)
# 范围包含:全角空格\u3000、全角标点\uFF01-\uFF60、兼容区\uFFE0-\uFFEE
if re.search(r"[\u3000\uFF01-\uFF60\uFFE0-\uFFEE]", content):
logger.debug(f"包含全角字符,忽略: {content}")
return None
# 大小写不敏感匹配(仅用于匹配,不改变返回的原始内容)
content_lower = content.lower()
# 使用最长前缀优先,避免 .r 误匹配 .roll 等更长前缀
command_keys_sorted = sorted(cls.COMMAND_MAP.keys(), key=len, reverse=True)
for cmd_prefix in command_keys_sorted:
if content_lower.startswith(cmd_prefix.lower()):
return cls.COMMAND_MAP[cmd_prefix], content
# 特殊处理:.ai 和 .aiconfig 指令支持参数
if content.startswith('.ai '):