Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3679d60e0c | ||
|
|
96513c6e60 | ||
|
|
9625fa7808 | ||
|
|
131fabeec2 | ||
|
|
86f87b440e |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -183,5 +183,4 @@ cython_debug/
|
||||
.vscode/
|
||||
|
||||
# Database
|
||||
data/bot.db
|
||||
liubai_web.pid
|
||||
data/bot.db
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
|
||||
<option name="progress" value="0.93" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/WPSBot.iml
generated
Normal file
9
.idea/WPSBot.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (pythonProject)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/codeStyles/Project.xml
generated
Normal file
7
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<ScalaCodeStyleSettings>
|
||||
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
|
||||
</ScalaCodeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
12
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
12
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredIdentifiers">
|
||||
<list>
|
||||
<option value="list.*" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (pythonProject)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (pythonProject)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/WPSBot.iml" filepath="$PROJECT_DIR$/.idea/WPSBot.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/vcs.xml
generated
Normal file
7
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/Convention" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -122,24 +122,6 @@ WPSBotGame/
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_10:27:59
|
||||
- 已修改:
|
||||
- games/alchemy.py(修复冒险任务完成后自动删除状态导致奖励丢失的bug)
|
||||
- 更改:
|
||||
1. **Bug修复**:修复冒险任务完成后奖励丢失问题
|
||||
- **问题**:在 `_perform_alchemy` 中,当检测到冒险任务已完成时,代码会自动删除冒险状态(`self.db.delete_game_state`),但没有发放奖励,导致用户奖励丢失
|
||||
- **修复**:移除自动删除逻辑,改为提示用户先使用 `.adventure` 回收奖励
|
||||
- **修改前**:冒险完成后自动删除状态,允许炼金(导致奖励丢失)
|
||||
- **修改后**:冒险完成后提示用户先回收奖励,不允许炼金,确保奖励只能通过 `.adventure` 命令回收
|
||||
2. **行为变更**:
|
||||
- 冒险进行中:提示剩余时间,不允许炼金(保持不变)
|
||||
- 冒险已完成:提示先回收奖励,不允许炼金(修复后)
|
||||
- 用户使用 `.adventure`:发放奖励并删除状态(保持不变)
|
||||
- 状态已删除:可以正常炼金(保持不变)
|
||||
- 原因:修复冒险任务完成后自动删除状态导致奖励丢失的严重bug,确保用户必须先主动回收奖励才能继续其他操作
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 详细实施记录
|
||||
|
||||
## 文件修改清单
|
||||
@@ -168,7 +150,7 @@ WPSBotGame/
|
||||
- 使用 `get_game_state(0, user_id, 'adventure')` 查询状态
|
||||
- 如果存在状态:
|
||||
* 计算剩余时间
|
||||
* 如果已完成:提示用户先使用 `.adventure` 回收奖励,不允许炼金(2025-10-31修复:避免奖励丢失,移除自动删除逻辑)
|
||||
* 如果已完成:自动删除状态,允许继续
|
||||
* 如果未完成:返回错误消息,显示剩余时间(X分Y秒)
|
||||
- 异常处理:捕获状态数据异常,自动清理损坏状态
|
||||
|
||||
@@ -224,7 +206,7 @@ state_data = {
|
||||
### 游戏互斥机制
|
||||
- 炼金前检查:查询冒险状态
|
||||
- 如果冒险进行中:返回错误,显示剩余时间
|
||||
- 如果冒险已完成:提示用户先使用 `.adventure` 回收奖励,不允许炼金(修复后:确保奖励不会丢失)
|
||||
- 如果冒险已完成:自动清理状态,允许炼金
|
||||
- 状态异常:自动清理,允许继续操作
|
||||
|
||||
# 最终审查
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
# 背景
|
||||
文件名: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:纯数据库表方案(推荐)
|
||||
**优点**:
|
||||
- 数据结构清晰,便于查询统计
|
||||
- 支持历史记录追踪
|
||||
- 并发安全,利用数据库事务
|
||||
- 易于扩展复杂查询
|
||||
|
||||
**缺点**:
|
||||
- 需要维护额外的表结构
|
||||
- 稍微复杂一些
|
||||
|
||||
**决策**:采用此方案
|
||||
|
||||
### 方案B:game_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个平局玩家未被结算
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_17:24:08]
|
||||
- 已修改:games/casino.py
|
||||
- 更改:重构21点游戏指令流程,改为更符合标准的玩法
|
||||
- 修改_open_blackjack:改为`.赌场 21点 open <底注> <黑杰克倍数>`,移除max_bet参数
|
||||
- 新增_join_blackjack:添加`.赌场 21点 join`指令,玩家加入游戏时扣除底注,检查积分是否足够
|
||||
- 修改_bet_blackjack:改为加注功能,仅在playing阶段可用,加注金额必须不低于底注
|
||||
- 修改_deal_blackjack:实现标准发牌顺序(先玩家1张→庄家明牌→玩家第2张→庄家暗牌),庄家隐藏一张暗牌
|
||||
- 修改_status_blackjack:游戏阶段隐藏庄家暗牌,只显示明牌,结算后显示完整手牌
|
||||
- 修改_stand_blackjack:检查所有玩家是否都已完成(停牌或爆牌),如果所有玩家都完成则自动触发结算
|
||||
- 修改_hit_blackjack:如果爆牌后所有玩家都完成,也自动触发结算
|
||||
- 更新_get_blackjack_help:反映新的指令流程和规则
|
||||
- 原因:用户要求新的指令流程:启动(open)→加入(join)→发牌(deal)→操作(hit/stand/bet加注)→自动结算
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
[2025-10-31_17:24:08]
|
||||
- 已修改:games/casino.py
|
||||
- 更改:修复停牌和要牌功能中的字典键访问错误
|
||||
- 修复_hit_blackjack中自动结算检查:将`player_hand['hand_status']`改为`player_hand['status']`
|
||||
- 修复_stand_blackjack中自动结算检查:将`player_hand['hand_status']`改为`player_hand['status']`
|
||||
- 原因:`get_all_blackjack_hands`返回的字典结构为`{user_id: {'cards': [...], 'status': ...}}`,应使用`status`而不是`hand_status`
|
||||
- 原因:用户测试停牌功能时遇到KeyError: 'hand_status'错误
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
待完成
|
||||
@@ -1,93 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-10-31_1_change-adventure-time-to-seconds.md
|
||||
创建于:2025-10-31_17:30:37
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:main
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
将冒险系统的计时单位从分钟更改为秒。需要修改所有相关的时间计算、验证逻辑、显示文本和帮助信息。
|
||||
|
||||
## 后续需求
|
||||
支持时间格式输入(如 `1h30m10s`),并在显示时按时分秒格式显示,避免只显示秒数过于冗余。
|
||||
|
||||
# 项目概览
|
||||
修改 `games/adventure.py` 文件,将冒险时间单位从分钟改为秒。
|
||||
|
||||
# 分析
|
||||
当前代码中所有时间相关逻辑都以分钟为单位:
|
||||
- 默认值:1分钟
|
||||
- 参数验证:至少1分钟
|
||||
- 时间计算:`cost_time * 60` 转换为秒
|
||||
- 显示文本:所有提示信息都显示"分钟"
|
||||
- 奖励计算:倍率 × 分钟数
|
||||
- 放弃冒险:按分钟计算已消耗时间
|
||||
|
||||
需要将所有相关逻辑改为秒为单位。
|
||||
|
||||
# 提议的解决方案
|
||||
将所有时间单位从分钟改为秒:
|
||||
1. 修改默认值和参数验证
|
||||
2. 移除时间计算中的 `* 60` 转换
|
||||
3. 更新所有显示文本
|
||||
4. 简化剩余时间显示逻辑(直接显示秒)
|
||||
5. 更新帮助信息
|
||||
|
||||
# 当前执行步骤:"已完成"
|
||||
|
||||
# 任务进度
|
||||
|
||||
## 2025-10-31_17:32:32
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 修改默认值注释:从"1分钟"改为"1秒"
|
||||
2. 修改函数文档:cost_time参数从"(分钟)"改为"(秒)"
|
||||
3. 修改参数验证:错误提示从"至少需要1分钟"改为"至少需要1秒"
|
||||
4. 移除时间转换:将`end_time = start_time + saved_cost_time * 60`改为`end_time = start_time + saved_cost_time`
|
||||
5. 更新显示文本:所有"分钟"改为"秒"(冒险结果、冒险进行中、冒险开始、冒险放弃)
|
||||
6. 简化剩余时间显示:移除分钟/秒的转换逻辑,直接显示秒数
|
||||
7. 修改放弃冒险逻辑:时间计算改为直接使用秒数(elapsed_seconds),移除分钟转换
|
||||
8. 更新帮助信息:所有"分钟"改为"秒"
|
||||
- 原因:将冒险系统计时单位从分钟改为秒,使时间控制更精确
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_17:35:06
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 添加 `re` 模块导入,用于正则表达式解析
|
||||
2. 新增 `_parse_time_string` 方法:解析时间格式字符串,支持以下格式:
|
||||
- 纯数字(按秒):`60` -> 60秒
|
||||
- 时分秒组合:`1h30m10s` -> 5410秒
|
||||
- 分钟秒组合:`30m10s` -> 1810秒
|
||||
- 只有小时:`1h` -> 3600秒
|
||||
- 只有分钟:`30m` -> 1800秒
|
||||
- 只有秒:`10s` -> 10秒
|
||||
3. 新增 `_format_time` 方法:将秒数格式化为 "X时X分X秒" 格式,自动省略为0的部分
|
||||
4. 修改 `handle` 方法:使用 `_parse_time_string` 解析时间参数,提供格式错误提示
|
||||
5. 更新所有时间显示位置:
|
||||
- 冒险结果:使用 `_format_time` 格式化消耗时间
|
||||
- 冒险进行中:使用 `_format_time` 格式化剩余时间和总时长
|
||||
- 冒险开始:使用 `_format_time` 格式化持续时间
|
||||
- 冒险放弃:使用 `_format_time` 格式化已计入时间
|
||||
6. 更新帮助信息:添加时间格式说明和示例
|
||||
- 原因:支持更灵活的时间输入格式,提升用户体验;时间显示按时分秒格式,避免冗长的秒数显示
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
## 2025-10-31_17:49:24
|
||||
- 已修改:games/adventure.py
|
||||
- 更改:
|
||||
1. 修复预计完成时间显示问题:
|
||||
- 原问题:只显示小时时刻(`%H:%M:%S`),跨天的冒险无法正确显示,且秒数显示不够明确
|
||||
- 第一次尝试:根据冒险时长是否超过24小时判断(不准确)
|
||||
- 最终解决方案:根据完成时间是否跨天来判断
|
||||
- 跨天或跨年:显示完整日期时间 `YYYY-MM-DD HH:MM:SS`(包含年月日和时分秒)
|
||||
- 同一天:显示时间 `HH:MM:SS`(包含时分秒)
|
||||
- 原因:修复跨天冒险无法正确显示完成时间的问题,只要跨天就显示完整日期,确保秒数清晰显示
|
||||
- 阻碍因素:无
|
||||
- 状态:成功
|
||||
|
||||
# 最终审查
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-11-03_1_user-webhook-url.md
|
||||
创建于:2025-11-03_09:38:30
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/user-webhook-url_2025-11-03_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中添加用户专属webhook URL功能,允许每个用户注册自己的个人webhook URL作为私聊途径。
|
||||
|
||||
## 核心需求
|
||||
1. 用户可以通过 `.register url <url>` 指令注册个人webhook URL
|
||||
2. 私聊消息发送功能将被封装为API接口,供其他系统调用
|
||||
3. 提供检测用户是否具有个人URL的接口,用于系统运行时确保参与用户都能被私聊
|
||||
4. 服务器启动时使用的webhook URL称为主URL,私聊用的URL称为个人URL
|
||||
|
||||
## 术语定义
|
||||
- **主URL**: 服务器启动时使用的webhook URL,用于群聊消息发送
|
||||
- **个人URL**: 用户注册的专属webhook URL,用于私聊消息发送
|
||||
|
||||
## 功能要求
|
||||
1. **注册功能**: 支持 `.register url <url>` 指令注册/更新个人URL
|
||||
2. **私聊接口**: 封装私聊消息发送功能为API接口(暂不对用户开放命令)
|
||||
3. **检测接口**: 提供单个和批量检测用户是否有个人URL的接口
|
||||
4. **数据库支持**: 在users表中添加webhook_url字段
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── routers/
|
||||
│ ├── callback.py # Callback路由处理
|
||||
│ ├── health.py # 健康检查
|
||||
│ └── private.py # 私聊相关API(新增)
|
||||
├── games/ # 游戏模块
|
||||
│ └── ... # 各种游戏
|
||||
└── utils/
|
||||
├── parser.py # 指令解析
|
||||
└── message.py # 消息发送
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. `users` 表已有基础字段:user_id, username, created_at, last_active
|
||||
2. `routers/callback.py` 中已有 `.register` 命令处理名称注册
|
||||
3. `utils/message.py` 中的 `MessageSender` 类使用全局webhook URL发送消息
|
||||
4. 数据库已支持动态添加列(`_add_column_if_not_exists`方法)
|
||||
5. `init_tables()` 方法在表创建后会进行兼容性检查,使用 `_add_column_if_not_exists` 安全添加新列
|
||||
|
||||
## 关键技术点
|
||||
1. **数据库层**:
|
||||
- 在`init_tables()`中使用`_add_column_if_not_exists`添加`webhook_url`字段(TEXT类型,可为NULL)
|
||||
- 确保兼容性:如果表已存在且没有该列,会自动添加
|
||||
- 添加`set_user_webhook_url(user_id, webhook_url)`方法
|
||||
- 添加`get_user_webhook_url(user_id)`方法
|
||||
- 添加`has_webhook_url(user_id)`方法
|
||||
- 添加`check_users_webhook_urls(user_ids)`批量检查方法
|
||||
|
||||
2. **注册命令扩展**:
|
||||
- 修改`handle_register_command`支持`.register url <url>`子命令
|
||||
- 保留原有的`.register <name>`功能
|
||||
- URL验证(基本格式检查)
|
||||
|
||||
3. **私聊消息发送**:
|
||||
- 封装私聊消息发送功能到`utils/message.py`
|
||||
- 创建`send_private_message(user_id, content, msg_type='text')`函数
|
||||
- 如果用户有个人URL则使用个人URL,否则返回错误
|
||||
|
||||
4. **API接口**:
|
||||
- 创建`routers/private.py`路由文件
|
||||
- `POST /api/private/send` - 发送私聊消息
|
||||
- `GET /api/private/check/{user_id}` - 检查单个用户是否有个人URL
|
||||
- `POST /api/private/check-batch` - 批量检查多个用户
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. **数据库扩展**: 在users表添加webhook_url字段,并实现相关CRUD方法
|
||||
2. **注册命令扩展**: 扩展`.register`命令支持`url`子命令
|
||||
3. **私聊功能封装**: 创建私聊消息发送工具函数
|
||||
4. **API接口**: 创建私聊相关的RESTful API接口
|
||||
|
||||
## 设计决策
|
||||
- 个人URL存储在users表中,与用户信息关联
|
||||
- 私聊功能暂不提供用户命令,仅作为API接口供系统调用
|
||||
- URL验证采用基本格式检查(http/https开头)
|
||||
- 批量检查接口支持传入用户ID列表,返回每个用户的URL状态
|
||||
|
||||
# 当前执行步骤:"3. 执行阶段完成"
|
||||
|
||||
实施清单:
|
||||
1. 在core/database.py的init_tables()方法末尾添加webhook_url字段兼容性检查
|
||||
2. 在core/database.py中添加set_user_webhook_url方法
|
||||
3. 在core/database.py中添加get_user_webhook_url方法
|
||||
4. 在core/database.py中添加has_webhook_url方法
|
||||
5. 在core/database.py中添加check_users_webhook_urls方法
|
||||
6. 在core/models.py文件末尾添加PrivateMessageRequest模型
|
||||
7. 在core/models.py中添加CheckBatchRequest模型
|
||||
8. 在core/models.py中添加CheckBatchResponse模型
|
||||
9. 在core/models.py的导入中添加List类型
|
||||
10. 修改routers/callback.py的handle_register_command函数支持url子命令
|
||||
11. 在utils/message.py文件末尾添加send_private_message函数
|
||||
12. 创建新文件routers/private.py,包含所有私聊相关API接口
|
||||
13. 在app.py中导入private路由模块
|
||||
14. 在app.py中注册private路由
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: core/database.py
|
||||
|
||||
### 修改点1: 在init_tables()方法中添加webhook_url字段兼容性检查
|
||||
|
||||
**位置**: 在`init_tables()`方法的末尾,第324行`logger.info("数据库表初始化完成")`之前
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
# 兼容性检查:为users表添加webhook_url字段
|
||||
self._add_column_if_not_exists('users', 'webhook_url', 'TEXT')
|
||||
```
|
||||
|
||||
### 修改点2: 添加set_user_webhook_url方法
|
||||
|
||||
**位置**: 在`# ===== 用户相关操作 =====`部分,`update_user_name`方法之后(约第414行之后)
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def set_user_webhook_url(self, user_id: int, webhook_url: str) -> bool:
|
||||
"""设置用户webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
webhook_url: Webhook URL
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 使用try-except包装
|
||||
- 确保用户存在(调用get_or_create_user)
|
||||
- UPDATE users SET webhook_url = ? WHERE user_id = ?
|
||||
- 记录成功/失败日志
|
||||
- 返回True/False,异常时返回False
|
||||
|
||||
### 修改点3: 添加get_user_webhook_url方法
|
||||
|
||||
**位置**: 紧接`set_user_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def get_user_webhook_url(self, user_id: int) -> Optional[str]:
|
||||
"""获取用户webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
Webhook URL,如果不存在返回None
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- SELECT webhook_url FROM users WHERE user_id = ?
|
||||
- 如果查询结果为None,返回None
|
||||
- 如果webhook_url为None或空字符串,返回None
|
||||
- 否则返回URL字符串
|
||||
|
||||
### 修改点4: 添加has_webhook_url方法
|
||||
|
||||
**位置**: 紧接`get_user_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def has_webhook_url(self, user_id: int) -> bool:
|
||||
"""检查用户是否有个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
是否有个人URL
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用get_user_webhook_url
|
||||
- 检查返回值是否不为None且不为空字符串
|
||||
|
||||
### 修改点5: 添加check_users_webhook_urls方法(批量检查)
|
||||
|
||||
**位置**: 紧接`has_webhook_url`方法之后
|
||||
|
||||
**方法签名**:
|
||||
```python
|
||||
def check_users_webhook_urls(self, user_ids: List[int]) -> Dict[int, bool]:
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
Args:
|
||||
user_ids: 用户ID列表
|
||||
|
||||
Returns:
|
||||
字典 {user_id: has_url}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 如果user_ids为空,返回空字典
|
||||
- 使用IN子句查询:SELECT user_id, webhook_url FROM users WHERE user_id IN (?)
|
||||
- 构建结果字典:初始化为所有user_id为False
|
||||
- 遍历查询结果,如果webhook_url不为None且不为空字符串,则设为True
|
||||
- 返回结果字典
|
||||
|
||||
## 文件2: routers/callback.py
|
||||
|
||||
### 修改点1: 修改handle_register_command函数支持url子命令
|
||||
|
||||
**位置**: 第226-260行的`handle_register_command`函数
|
||||
|
||||
**修改内容**:
|
||||
- 提取命令和参数后,检查第一个参数是否为"url"
|
||||
- 如果是"url",提取URL参数,验证URL格式(http/https开头),调用`db.set_user_webhook_url`
|
||||
- 如果不是"url",保持原有逻辑(注册名称)
|
||||
- 更新帮助信息,包含两种用法
|
||||
|
||||
**新的函数逻辑**:
|
||||
```python
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 检查是否为url子命令
|
||||
parts = args.split(maxsplit=1)
|
||||
if parts and parts[0].lower() == 'url':
|
||||
# 处理URL注册
|
||||
if len(parts) < 2:
|
||||
return "❌ 请提供webhook URL!\n\n正确格式:`.register url <URL>`\n\n示例:\n`.register url https://example.com/webhook?key=xxx`"
|
||||
webhook_url = parts[1].strip()
|
||||
# URL验证
|
||||
if not webhook_url.startswith(('http://', 'https://')):
|
||||
return "❌ URL格式无效!必须以 http:// 或 https:// 开头。"
|
||||
# 设置URL
|
||||
db = get_db()
|
||||
success = db.set_user_webhook_url(user_id, webhook_url)
|
||||
if success:
|
||||
return f"✅ Webhook URL注册成功!\n\n**您的个人URL**:{webhook_url}\n\n私聊消息将发送到此URL。"
|
||||
else:
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
else:
|
||||
# 原有的名称注册逻辑
|
||||
...
|
||||
```
|
||||
|
||||
## 文件3: utils/message.py
|
||||
|
||||
### 修改点1: 添加send_private_message函数
|
||||
|
||||
**位置**: 在文件末尾,`get_message_sender`函数之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
async def send_private_message(user_id: int, content: str, msg_type: str = 'text') -> bool:
|
||||
"""发送私聊消息到用户个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 目标用户ID
|
||||
content: 消息内容
|
||||
msg_type: 消息类型 ('text' 或 'markdown')
|
||||
|
||||
Returns:
|
||||
是否发送成功,如果用户没有个人URL则返回False
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 从数据库获取用户webhook URL
|
||||
- 如果URL不存在,记录日志并返回False
|
||||
- 创建MessageSender实例(使用用户的个人URL)
|
||||
- 根据msg_type调用send_text或send_markdown
|
||||
- 返回发送结果
|
||||
|
||||
## 文件4: core/models.py (新增数据模型)
|
||||
|
||||
### 修改点1: 添加PrivateMessageRequest模型
|
||||
|
||||
**位置**: 文件末尾
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class PrivateMessageRequest(BaseModel):
|
||||
"""私聊消息请求模型"""
|
||||
user_id: int = Field(..., description="目标用户ID")
|
||||
content: str = Field(..., description="消息内容")
|
||||
msg_type: str = Field(default="text", description="消息类型: text 或 markdown")
|
||||
```
|
||||
|
||||
### 修改点2: 添加CheckBatchRequest模型
|
||||
|
||||
**位置**: 紧接PrivateMessageRequest之后
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class CheckBatchRequest(BaseModel):
|
||||
"""批量检查请求模型"""
|
||||
user_ids: List[int] = Field(..., description="用户ID列表")
|
||||
```
|
||||
|
||||
### 修改点3: 添加CheckBatchResponse模型
|
||||
|
||||
**位置**: 紧接CheckBatchRequest之后
|
||||
|
||||
**模型定义**:
|
||||
```python
|
||||
class CheckBatchResponse(BaseModel):
|
||||
"""批量检查响应模型"""
|
||||
results: Dict[int, bool] = Field(..., description="用户ID到是否有URL的映射")
|
||||
```
|
||||
|
||||
**注意**: core/models.py需要添加`from typing import List`导入(如果尚未导入)
|
||||
|
||||
## 文件5: routers/private.py (新建文件)
|
||||
|
||||
### 文件结构:
|
||||
```python
|
||||
"""私聊相关API路由"""
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from core.database import get_db
|
||||
from core.models import PrivateMessageRequest, CheckBatchRequest, CheckBatchResponse
|
||||
from utils.message import send_private_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
```
|
||||
|
||||
### 接口1: POST /api/private/send
|
||||
|
||||
**位置**: router定义之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.post("/private/send")
|
||||
async def send_private(request: PrivateMessageRequest):
|
||||
"""发送私聊消息
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_id": 123456,
|
||||
"content": "消息内容",
|
||||
"msg_type": "text" // 可选,默认为"text"
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 验证msg_type(必须是"text"或"markdown"),否则返回400错误
|
||||
- 调用send_private_message
|
||||
- 如果返回False(用户没有个人URL或发送失败),返回400错误和相应消息
|
||||
- 成功则返回JSONResponse({"success": True, "message": "消息发送成功"})
|
||||
|
||||
### 接口2: GET /api/private/check/{user_id}
|
||||
|
||||
**位置**: send_private之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.get("/private/check/{user_id}")
|
||||
async def check_user_webhook(user_id: int):
|
||||
"""检查用户是否有个人webhook URL"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用db.has_webhook_url(user_id)
|
||||
- 返回JSONResponse({"user_id": user_id, "has_webhook_url": bool})
|
||||
|
||||
### 接口3: POST /api/private/check-batch
|
||||
|
||||
**位置**: check_user_webhook之后
|
||||
|
||||
**函数签名**:
|
||||
```python
|
||||
@router.post("/private/check-batch")
|
||||
async def check_users_webhook_batch(request: CheckBatchRequest):
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_ids": [123456, 789012, ...]
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
- 调用db.check_users_webhook_urls(request.user_ids)
|
||||
- 返回CheckBatchResponse(results=...)
|
||||
|
||||
## 文件6: app.py
|
||||
|
||||
### 修改点1: 导入private路由
|
||||
|
||||
**位置**: 第13行,导入语句中
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
from routers import callback, health, private
|
||||
```
|
||||
|
||||
### 修改点2: 注册private路由
|
||||
|
||||
**位置**: 第75-76行,路由注册部分
|
||||
|
||||
**修改内容**:
|
||||
```python
|
||||
app.include_router(callback.router, prefix="/api", tags=["callback"])
|
||||
app.include_router(health.router, tags=["health"])
|
||||
app.include_router(private.router, prefix="/api", tags=["private"])
|
||||
```
|
||||
|
||||
# 任务进度
|
||||
|
||||
[2025-11-03_09:45:56]
|
||||
- 已修改:
|
||||
1. core/database.py - 添加webhook_url字段兼容性检查和4个数据库方法
|
||||
2. core/models.py - 添加3个API数据模型和List类型导入
|
||||
3. routers/callback.py - 扩展handle_register_command支持url子命令
|
||||
4. utils/message.py - 添加send_private_message函数
|
||||
5. routers/private.py - 新建文件,包含3个私聊相关API接口
|
||||
6. app.py - 导入并注册private路由
|
||||
|
||||
- 更改:
|
||||
1. 在users表中添加webhook_url字段支持(兼容性检查)
|
||||
2. 实现用户webhook URL的CRUD操作(设置、获取、检查、批量检查)
|
||||
3. 扩展.register命令支持`.register url <url>`子命令
|
||||
4. 封装私聊消息发送功能为独立函数
|
||||
5. 创建私聊相关的RESTful API接口(发送、单个检查、批量检查)
|
||||
6. 注册新的API路由到FastAPI应用
|
||||
|
||||
- 原因:
|
||||
实现用户专属webhook URL注册和私聊消息发送功能,为其他系统提供API接口调用
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-03_后续]
|
||||
- 已修改:
|
||||
1. utils/parser.py - 添加.talk和.私聊指令映射
|
||||
2. routers/callback.py - 添加handle_talk_command函数实现私聊指令
|
||||
|
||||
- 更改:
|
||||
1. 添加.talk <username> <content>指令,允许用户通过用户名发送私聊消息
|
||||
2. 实现用户名和URL验证,确保目标用户已注册名称和个人URL
|
||||
3. 私聊消息发送成功时不向主URL发送提示消息,保持私密性
|
||||
|
||||
- 原因:
|
||||
实现用户可用的私聊功能,作为私聊功能的开始
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功(测试通过)
|
||||
|
||||
# 最终审查
|
||||
|
||||
待审查阶段完成...
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
# 背景
|
||||
文件名:2025-11-03_2_werewolf-game.md
|
||||
创建于:2025-11-03_12:20:10
|
||||
创建者:admin
|
||||
主分支:main
|
||||
任务分支:task/werewolf_2025-11-03_1
|
||||
Yolo模式:Off
|
||||
|
||||
# 任务描述
|
||||
在WPS Bot Game项目中添加狼人杀游戏系统,支持6-12人游戏,包含身份分配、私聊功能、技能使用等核心功能。
|
||||
|
||||
## 核心需求
|
||||
1. 支持6-12人狼人杀游戏(配置:2-4狼 1预言家 1女巫 2-6平民)
|
||||
2. 主持人开房:`.狼人杀 open`
|
||||
3. 玩家加入:`.狼人杀 join`(必须注册用户名和个人URL)
|
||||
4. 开始游戏:`.狼人杀 start`,自动分配身份并通过私聊发送
|
||||
5. 私聊功能:`.狼人杀 <玩家代号> <消息>`
|
||||
6. 狼人群聊:`.狼人杀 狼人 <消息>`
|
||||
7. 技能系统:杀、验、救、毒
|
||||
8. 游戏状态查询:`.狼人杀 status`
|
||||
9. 结束游戏:`.狼人杀 end`
|
||||
|
||||
## 游戏规则
|
||||
**人数配置**:
|
||||
- 6人:2狼 1预言家 1女巫 2平民
|
||||
- 8人:2狼 1预言家 1女巫 4平民
|
||||
- 10人:3狼 1预言家 1女巫 5平民
|
||||
- 12人:4狼 1预言家 1女巫 6平民
|
||||
|
||||
**胜利条件**:
|
||||
- 狼人阵营:杀死所有神职和平民
|
||||
- 好人阵营:消灭所有狼人
|
||||
|
||||
**技能**:
|
||||
- 狼人:每晚投票刀人
|
||||
- 预言家:每晚查验一个玩家身份
|
||||
- 女巫:拥有一瓶解药(仅可使用一次)和一瓶毒药(仅可使用一次)
|
||||
- 平民:无特殊技能
|
||||
|
||||
# 项目概览
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
WPSBotGame/
|
||||
├── app.py # FastAPI主应用
|
||||
├── config.py # 配置管理
|
||||
├── core/
|
||||
│ ├── database.py # SQLite数据库操作
|
||||
│ ├── middleware.py # 中间件
|
||||
│ └── models.py # 数据模型
|
||||
├── routers/
|
||||
│ ├── callback.py # Callback路由处理
|
||||
│ ├── health.py # 健康检查
|
||||
│ └── private.py # 私聊相关API
|
||||
├── games/ # 游戏模块
|
||||
│ ├── werewolf.py # 狼人杀游戏(新增)
|
||||
│ └── ... # 其他游戏
|
||||
└── utils/
|
||||
├── parser.py # 指令解析
|
||||
└── message.py # 消息发送
|
||||
```
|
||||
|
||||
# 分析
|
||||
|
||||
## 当前状态
|
||||
1. 已有私聊功能支持,用户可注册个人webhook URL
|
||||
2. 已有游戏架构:BaseGame基类、数据库状态管理
|
||||
3. 已有成语接龙等多人类游戏参考
|
||||
4. 数据库支持game_states表存储游戏状态
|
||||
|
||||
## 关键技术点
|
||||
1. **数据库层**:
|
||||
- 使用game_states表存储游戏状态(user_id=0表示群级别状态)
|
||||
- 通过state_data JSON字段存储玩家列表、身份、阶段等信息
|
||||
|
||||
2. **私聊系统**:
|
||||
- 利用现有的send_private_message函数
|
||||
- 身份信息、技能结果等通过私聊发送
|
||||
- 支持发送者标识显示
|
||||
|
||||
3. **游戏状态管理**:
|
||||
- 游戏状态存储在state_data中
|
||||
- 包含:玩家列表、身份映射、狼人列表、技能使用记录等
|
||||
|
||||
4. **技能系统**:
|
||||
- 狼人刀人:投票机制
|
||||
- 预言家验人:私聊返回结果
|
||||
- 女巫用药:限制使用次数
|
||||
|
||||
5. **指令路由**:
|
||||
- 在parser.py注册.werewolf和.狼人杀指令
|
||||
- 在callback.py中导入并注册游戏处理器
|
||||
|
||||
# 提议的解决方案
|
||||
|
||||
## 方案概述
|
||||
1. **创建狼人杀游戏类**:`games/werewolf.py`,继承BaseGame
|
||||
2. **状态数据结构**:使用JSON存储在state_data中
|
||||
3. **身份分配**:随机分配角色,狼人互相认识
|
||||
4. **私聊通知**:游戏开始时通过私聊发送身份信息
|
||||
5. **技能系统**:支持杀、验、救、毒四种技能
|
||||
6. **指令注册**:添加解析和路由支持
|
||||
|
||||
## 设计决策
|
||||
- 使用user_id=0存储群级别游戏状态(参考成语接龙)
|
||||
- 通过私聊发送敏感信息(身份、技能结果)
|
||||
- 简化实现:主持人手动推进阶段(暂不实现自动流转)
|
||||
- 使用数字代号(1-N)标识玩家
|
||||
- 支持狼人群聊功能
|
||||
|
||||
# 当前执行步骤:"实施完成"
|
||||
|
||||
# 详细实施计划
|
||||
|
||||
## 文件1: games/werewolf.py(新建文件)
|
||||
|
||||
### 主要方法
|
||||
1. **handle()** - 主处理逻辑,指令路由
|
||||
2. **get_help()** - 帮助信息
|
||||
3. **_open_game()** - 主持人开房
|
||||
4. **_join_game()** - 玩家加入
|
||||
5. **_start_game()** - 开始游戏,分配身份
|
||||
6. **_send_identities()** - 私聊发送身份信息
|
||||
7. **_private_chat()** - 玩家私聊
|
||||
8. **_wolf_group_chat()** - 狼人群聊
|
||||
9. **_handle_skill()** - 技能处理
|
||||
10. **_wolf_kill()** - 狼人刀人
|
||||
11. **_seer_check()** - 预言家验人
|
||||
12. **_witch_save()** - 女巫救人
|
||||
13. **_witch_poison()** - 女巫毒人
|
||||
14. **_show_status()** - 显示游戏状态
|
||||
15. **_end_game()** - 结束游戏
|
||||
|
||||
### 数据结构设计
|
||||
```python
|
||||
state_data = {
|
||||
'creator_id': int, # 主持人ID
|
||||
'status': str, # 'open', 'playing', 'finished'
|
||||
'players': [
|
||||
{
|
||||
'user_id': int,
|
||||
'name': str, # 注册的用户名
|
||||
'id': int, # 游戏内代号 1-N
|
||||
'role': str, # 'wolf', 'seer', 'witch', 'civilian'
|
||||
'alive': bool,
|
||||
'id_label': str # "1号玩家"
|
||||
}
|
||||
],
|
||||
'phase': str, # 当前阶段
|
||||
'round': int, # 当前回合数
|
||||
'wolves': [int], # 狼人ID列表
|
||||
'kill_target': int, # 狼人票决目标
|
||||
'seer_result': {}, # 预言家验人结果
|
||||
'witch_save': bool, # 女巫是否救人
|
||||
'witch_poison': int, # 女巫毒杀目标
|
||||
'discussed': False, # 讨论阶段是否完成
|
||||
'wolf_know_each_other': False
|
||||
}
|
||||
```
|
||||
|
||||
## 文件2: utils/parser.py
|
||||
|
||||
### 修改点:添加指令映射
|
||||
在COMMAND_MAP中添加:
|
||||
```python
|
||||
'.werewolf': 'werewolf',
|
||||
'.狼人杀': 'werewolf',
|
||||
```
|
||||
|
||||
## 文件3: routers/callback.py
|
||||
|
||||
### 修改点:添加路由处理
|
||||
在handle_command函数中添加:
|
||||
```python
|
||||
# 狼人杀系统
|
||||
if game_type == 'werewolf':
|
||||
from games.werewolf import WerewolfGame
|
||||
game = WerewolfGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
```
|
||||
|
||||
## 文件4: games/base.py
|
||||
|
||||
### 修改点:添加帮助信息
|
||||
在get_help_message()函数中添加狼人杀帮助说明
|
||||
|
||||
# 任务进度
|
||||
|
||||
[2025-11-03_12:20:10]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 新建狼人杀游戏类,实现所有核心功能
|
||||
2. utils/parser.py - 添加.werewolf和.狼人杀指令映射
|
||||
3. routers/callback.py - 添加狼人杀路由处理
|
||||
4. games/base.py - 添加狼人杀帮助信息
|
||||
|
||||
- 更改:
|
||||
1. 创建完整的狼人杀游戏系统
|
||||
2. 支持开房、加入、开始、私聊、技能使用等所有核心功能
|
||||
3. 实现6-12人游戏配置和角色分配
|
||||
4. 集成私聊系统发送身份信息
|
||||
5. 支持狼人群聊功能
|
||||
6. 添加帮助信息和指令注册
|
||||
|
||||
- 原因:
|
||||
实现完整的狼人杀游戏系统,支持多人游戏、身份隐藏、技能使用等核心功能
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-04_17:41:14]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 改进阶段提示和自动流转功能
|
||||
|
||||
- 更改:
|
||||
1. 添加阶段名称映射系统(phase_configs),定义各阶段的中文名称、行动角色和指令说明
|
||||
2. 实现阶段管理方法:_get_phase_description()、_get_next_phase()、_advance_phase()
|
||||
3. 改进游戏开始提示,明确显示"第一夜 - 狼人行动阶段"和操作指令
|
||||
4. 实现自动阶段流转:狼人刀人后自动进入预言家阶段,预言家验人后自动进入女巫阶段
|
||||
5. 新增女巫跳过功能:支持`.werewolf 跳过`指令,女巫可以选择不行动
|
||||
6. 改进状态显示:_show_status()现在显示中文阶段名称、当前行动角色和操作指令
|
||||
7. 更新身份说明和帮助信息,添加女巫跳过选项说明
|
||||
8. 各技能方法添加阶段验证,确保在正确的阶段使用技能
|
||||
|
||||
- 原因:
|
||||
解决用户反馈的游戏阶段不明显的问题,让玩家清楚知道当前是什么阶段、谁应该行动、下一步是什么阶段
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_10:59:09]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 支持在私聊中使用狼人杀技能(方案2实施)
|
||||
|
||||
- 更改:
|
||||
1. 新增 _find_player_game(user_id) 方法,根据玩家ID查找其参与的游戏
|
||||
2. 修改 _handle_skill() 方法,支持从私聊中使用技能指令
|
||||
3. 修改 _wolf_group_chat() 方法,支持从私聊中发送狼人群聊
|
||||
4. 修改 _private_chat() 方法,支持从私聊中发送玩家私聊
|
||||
5. 修改 _witch_pass() 方法,支持从私聊中跳过女巫行动
|
||||
6. 添加日志输出,显示在私聊中使用功能的情况
|
||||
7. 实现逻辑:先尝试用当前chat_id查找游戏(群聊场景),找不到则通过user_id查找玩家游戏(私聊场景)
|
||||
|
||||
- 原因:
|
||||
解决私聊中无法使用技能的问题。游戏在群里创建,但玩家需要在私聊中使用技能以保密。
|
||||
之前的设计只能在游戏所在群使用技能,现在支持在私聊中使用,查找玩家参与的游戏并操作。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:06:58]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 改为全局唯一游戏模式(不再按chat_id区分)
|
||||
|
||||
- 更改:
|
||||
1. 修改 _get_game_state() 方法,查询全局唯一游戏而非根据chat_id查询
|
||||
2. 新增 _get_game_chat_id() 方法,获取全局游戏所在的chat_id
|
||||
3. 简化 _find_player_game() 方法,使用全局游戏查询
|
||||
4. 修改 _open_game() 方法,检查全局是否已有游戏(而非仅检查当前群)
|
||||
5. 简化所有需要查找玩家游戏的方法(_handle_skill、_wolf_group_chat、_private_chat、_witch_pass)
|
||||
6. 保持数据库兼容性:chat_id列仍然存在并记录游戏创建的群,但查询时忽略
|
||||
7. 实现逻辑:所有查询都获取全局最新的一个狼人杀游戏,不再区分群组
|
||||
|
||||
- 原因:
|
||||
Bot全局只需要一个狼人杀游戏,不同群的玩家可以参与同一个游戏。
|
||||
简化逻辑,消除按chat_id区分的复杂性,同时保持数据库结构兼容。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:13:56]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 改进狼人投票机制
|
||||
|
||||
- 更改:
|
||||
1. 在游戏状态数据中添加 wolf_votes 字段,记录每个狼人的投票
|
||||
2. 修改 _wolf_kill() 方法,实现完整的投票流程:
|
||||
- 记录每个狼人的投票(支持改票)
|
||||
- 检查是否所有存活狼人都已投票
|
||||
- 未全部投票时提示等待其他狼人
|
||||
- 全部投票后统计票数
|
||||
- 票数唯一时确定目标并推进阶段
|
||||
- 平票时清除投票记录并要求重新投票
|
||||
3. 优化投票提示信息,显示投票进度和结果
|
||||
|
||||
- 原因:
|
||||
解决之前"只要一个狼人投票就立即刀人"的问题。
|
||||
现在要求所有狼人都投票,统计票数最多的目标,平票则重新投票,符合狼人杀游戏规则。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:22:53]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 狼人投票结果保密处理
|
||||
|
||||
- 更改:
|
||||
1. 修改狼人投票反馈机制,不在群里播报投票目标
|
||||
2. 单个狼人投票时,通过私聊确认投票,群消息只显示"投票已记录"
|
||||
3. 投票平票时,通过私聊通知狼人具体平票目标,群消息只显示"平票"
|
||||
4. 投票完成时,通过私聊通知所有狼人击杀目标,群消息只显示"投票完成"
|
||||
5. 所有敏感信息(投票目标、平票详情、击杀决定)均通过私聊发送给狼人
|
||||
|
||||
- 原因:
|
||||
符合狼人杀游戏规则,狼人刀人的决定应该保密,不能在群里公开播报。
|
||||
只有狼人自己知道投票情况和击杀目标,其他玩家在天亮时才知道结果。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-07_11:40:44]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 新增空刀机制
|
||||
|
||||
- 更改:
|
||||
1. 允许狼人投票给0号(表示空刀,不杀人)
|
||||
2. 修改 _wolf_kill() 方法,对target_id=0不验证目标存在性
|
||||
3. 修改投票确认消息,区分空刀和正常投票
|
||||
4. 修改票数统计逻辑,投票0不计入击杀目标统计
|
||||
5. 新增全部空刀处理:如果所有狼人都空刀,kill_target设为0(平安夜)
|
||||
6. 修改 _advance_phase() 为async,进入女巫阶段时私聊通知女巫刀人情况
|
||||
7. 女巫有解药时可知道今晚是否有人被刀(0号为平安夜)
|
||||
|
||||
- 原因:
|
||||
符合狼人杀游戏规则,狼人可以选择空刀(不杀人)。
|
||||
女巫在有解药时需要知道今晚是否有人被刀以决定是否使用解药。
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:成功
|
||||
|
||||
[2025-11-10_10:20:38]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 调整status指令的房间名单展示
|
||||
|
||||
- 更改:
|
||||
1. 房间开放阶段现在显示已加入玩家的房内ID与用户名
|
||||
2. 游戏进行阶段的玩家状态显示同时包含房内ID和用户名
|
||||
|
||||
- 原因:
|
||||
提升`.werewolf status`指令提供的信息量,方便玩家识别房间成员
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:未确认
|
||||
|
||||
[2025-11-10_10:27:07]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 统一狼人投票记录的键类型
|
||||
|
||||
- 更改:
|
||||
1. `_wolf_kill()` 读取和保存 `wolf_votes` 时转换为字符串键
|
||||
2. 统计投票进度时将键重新转换为整数,确保与 `alive_wolves` 对齐
|
||||
|
||||
- 原因:
|
||||
修复狼人全部投票后仍提示有人未投票的问题
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:未确认
|
||||
|
||||
[2025-11-10_10:36:39]
|
||||
- 已修改:
|
||||
1. games/werewolf.py - 修复狼人投票流程与阶段推进
|
||||
|
||||
- 更改:
|
||||
1. `_wolf_kill()` 读取 `wolf_votes` 时统一转换为整数键,存储与统计均使用整数
|
||||
2. `_wolf_kill()`、`_seer_check()`、`_witch_save()`、`_witch_poison()`、`_witch_pass()` 中的 `_advance_phase` 调用改为 `await`
|
||||
|
||||
- 原因:
|
||||
修复狼人投票完成后仍判定未投票、平安夜提示错误及阶段推进信息显示 `<coroutine>` 的问题
|
||||
|
||||
- 阻碍因素:
|
||||
无
|
||||
|
||||
- 状态:未确认
|
||||
|
||||
# 最终审查
|
||||
|
||||
待审查阶段完成...
|
||||
|
||||
3
app.py
3
app.py
@@ -10,7 +10,7 @@ import asyncio
|
||||
from config import APP_CONFIG, SESSION_TIMEOUT, SetWebhookURL, GetWebhookURL
|
||||
from core.middleware import ConcurrencyLimitMiddleware
|
||||
from core.database import get_db
|
||||
from routers import callback, health, private
|
||||
from routers import callback, health
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
@@ -74,7 +74,6 @@ app.add_middleware(ConcurrencyLimitMiddleware)
|
||||
# 注册路由
|
||||
app.include_router(callback.router, prefix="/api", tags=["callback"])
|
||||
app.include_router(health.router, tags=["health"])
|
||||
app.include_router(private.router, prefix="/api", tags=["private"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
||||
1033
core/database.py
1033
core/database.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
"""数据模型定义"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class CallbackRequest(BaseModel):
|
||||
@@ -76,20 +76,3 @@ class QuizGameState(GameState):
|
||||
attempts: int = Field(0, description="尝试次数")
|
||||
max_attempts: int = Field(3, description="最大尝试次数")
|
||||
|
||||
|
||||
class PrivateMessageRequest(BaseModel):
|
||||
"""私聊消息请求模型"""
|
||||
user_id: int = Field(..., description="目标用户ID")
|
||||
content: str = Field(..., description="消息内容")
|
||||
msg_type: str = Field(default="text", description="消息类型: text 或 markdown")
|
||||
|
||||
|
||||
class CheckBatchRequest(BaseModel):
|
||||
"""批量检查请求模型"""
|
||||
user_ids: List[int] = Field(..., description="用户ID列表")
|
||||
|
||||
|
||||
class CheckBatchResponse(BaseModel):
|
||||
"""批量检查响应模型"""
|
||||
results: Dict[int, bool] = Field(..., description="用户ID到是否有URL的映射")
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from games.base import BaseGame
|
||||
from utils.parser import CommandParser
|
||||
@@ -23,86 +22,19 @@ class AdventureGame(BaseGame):
|
||||
# 奖品池配置
|
||||
self.prize_pool: List[Tuple[int, float, str]] = [
|
||||
# (权重, 倍率, 描述)
|
||||
(300, 1, "少量积分"),
|
||||
(250, 2, "中等积分"),
|
||||
(500, 0.5, "少量积分"),
|
||||
(350, 1, "中等积分"),
|
||||
(200, 2, "大量积分"),
|
||||
(150, 5, "丰厚积分"),
|
||||
(100, 10, "丰厚积分"),
|
||||
(50, 100, "🌟 巨额积分"),
|
||||
(10, 1000, "💎 传说积分"),
|
||||
(100, 5, "丰厚积分"),
|
||||
(50, 10, "丰厚积分"),
|
||||
(10, 100, "🌟 巨额积分"),
|
||||
(1, 1000, "💎 传说积分"),
|
||||
]
|
||||
|
||||
self.total_weight: int = 0
|
||||
for weight,_,_ in self.prize_pool:
|
||||
self.total_weight += weight
|
||||
|
||||
def _parse_time_string(self, time_str: str) -> int:
|
||||
"""解析时间字符串,支持 h/m/s 格式
|
||||
|
||||
支持的格式示例:
|
||||
- "1h30m10s" -> 5410秒
|
||||
- "30m" -> 1800秒
|
||||
- "10s" -> 10秒
|
||||
- "1h30m" -> 5400秒
|
||||
- "3600" -> 3600秒(纯数字,按秒处理)
|
||||
|
||||
Args:
|
||||
time_str: 时间字符串
|
||||
|
||||
Returns:
|
||||
解析后的秒数,如果解析失败返回None
|
||||
"""
|
||||
if not time_str:
|
||||
return None
|
||||
|
||||
# 如果是纯数字,直接返回
|
||||
if time_str.isdigit():
|
||||
return int(time_str)
|
||||
|
||||
# 使用正则表达式匹配 h/m/s 格式,确保整个字符串匹配
|
||||
pattern = r'^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$'
|
||||
match = re.match(pattern, time_str.lower())
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
hours = int(match.group(1) or 0)
|
||||
minutes = int(match.group(2) or 0)
|
||||
seconds = int(match.group(3) or 0)
|
||||
|
||||
# 如果所有值都是0,返回None
|
||||
if hours == 0 and minutes == 0 and seconds == 0:
|
||||
return None
|
||||
|
||||
total_seconds = hours * 3600 + minutes * 60 + seconds
|
||||
return total_seconds
|
||||
|
||||
def _format_time(self, seconds: int) -> str:
|
||||
"""将秒数格式化为 "X时X分X秒" 格式
|
||||
|
||||
Args:
|
||||
seconds: 秒数
|
||||
|
||||
Returns:
|
||||
格式化的时间字符串,如 "1时30分10秒"、"30分10秒"、"10秒"
|
||||
"""
|
||||
if seconds < 0:
|
||||
seconds = 0
|
||||
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
secs = seconds % 60
|
||||
|
||||
parts = []
|
||||
if hours > 0:
|
||||
parts.append(f"{hours}时")
|
||||
if minutes > 0:
|
||||
parts.append(f"{minutes}分")
|
||||
if secs > 0 or not parts:
|
||||
parts.append(f"{secs}秒")
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理冒险相关指令
|
||||
|
||||
@@ -128,16 +60,12 @@ class AdventureGame(BaseGame):
|
||||
if args in ['abandon', '放弃']:
|
||||
return await self._abandon_adventure(chat_id, user_id)
|
||||
|
||||
# 默认:冒险耗时1秒
|
||||
# 默认:冒险耗时1分钟
|
||||
else:
|
||||
# 解析消耗时间
|
||||
cost_time = 1 # 默认消耗1秒
|
||||
if args:
|
||||
parsed_time = self._parse_time_string(args)
|
||||
if parsed_time is not None:
|
||||
cost_time = parsed_time
|
||||
else:
|
||||
return f"❌ 时间格式错误!请使用以下格式:\n- 纯数字(秒):`.adventure 60`\n- 时分秒格式:`.adventure 1h30m10s`\n- 分钟秒格式:`.adventure 30m10s`\n- 只有秒:`.adventure 10s`"
|
||||
cost_time = 1 # 默认消耗1分钟
|
||||
if args.isdigit():
|
||||
cost_time = int(args)
|
||||
|
||||
return await self._perform_adventure(chat_id, user_id, cost_time)
|
||||
|
||||
@@ -151,14 +79,14 @@ class AdventureGame(BaseGame):
|
||||
Args:
|
||||
chat_id: 会话ID(使用0作为用户级标识)
|
||||
user_id: 用户ID
|
||||
cost_time: 消耗时间(秒)
|
||||
cost_time: 消耗时间(分钟)
|
||||
|
||||
Returns:
|
||||
抽奖结果消息
|
||||
"""
|
||||
# 参数验证
|
||||
if cost_time < 1:
|
||||
return "❌ 冒险时间至少需要1秒!"
|
||||
return "❌ 冒险时间至少需要1分钟!"
|
||||
|
||||
# 查询冒险状态(使用chat_id=0表示用户级状态)
|
||||
state = self.db.get_game_state(0, user_id, 'adventure')
|
||||
@@ -170,7 +98,7 @@ class AdventureGame(BaseGame):
|
||||
state_data = state['state_data']
|
||||
start_time = state_data.get('start_time', 0)
|
||||
saved_cost_time = state_data.get('cost_time', 1)
|
||||
end_time = start_time + saved_cost_time
|
||||
end_time = start_time + saved_cost_time * 60
|
||||
remaining_seconds = end_time - current_time
|
||||
|
||||
# 情况1.1:冒险已完成(时间已到或过期)
|
||||
@@ -192,9 +120,8 @@ class AdventureGame(BaseGame):
|
||||
updated_points = self.db.get_user_points(user_id)
|
||||
|
||||
# 格式化输出
|
||||
time_str = self._format_time(saved_cost_time)
|
||||
text = f"## ⚡️ 冒险结果\n\n"
|
||||
text += f"**消耗时间**: {time_str}\n\n"
|
||||
text += f"**消耗时间**: {saved_cost_time} 分钟\n\n"
|
||||
text += f"**{reward['description']}**: 获得 {reward_points} 积分\n\n"
|
||||
text += f"**当前积分**: {updated_points['points']} 分\n\n"
|
||||
text += "---\n\n"
|
||||
@@ -203,12 +130,17 @@ class AdventureGame(BaseGame):
|
||||
return text
|
||||
|
||||
# 情况1.2:冒险未完成,返回等待提示
|
||||
wait_msg = self._format_time(remaining_seconds)
|
||||
saved_time_str = self._format_time(saved_cost_time)
|
||||
remaining_minutes = remaining_seconds // 60
|
||||
remaining_secs = remaining_seconds % 60
|
||||
|
||||
if remaining_minutes > 0:
|
||||
wait_msg = f"{remaining_minutes} 分 {remaining_secs} 秒"
|
||||
else:
|
||||
wait_msg = f"{remaining_secs} 秒"
|
||||
|
||||
text = f"## ⚡️ 冒险进行中\n\n"
|
||||
text += f"你正在进行一次冒险,还需等待 **{wait_msg}** 才能完成。\n\n"
|
||||
text += f"**当前冒险时长**: {saved_time_str}\n\n"
|
||||
text += f"**当前冒险时长**: {saved_cost_time} 分钟\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:冒险期间无法进行炼金,请耐心等待!"
|
||||
|
||||
@@ -230,19 +162,12 @@ class AdventureGame(BaseGame):
|
||||
self.db.save_game_state(0, user_id, 'adventure', state_data)
|
||||
|
||||
# 计算预计完成时间
|
||||
end_time = current_time + cost_time
|
||||
end_time = current_time + cost_time * 60
|
||||
end_datetime = datetime.fromtimestamp(end_time)
|
||||
current_datetime = datetime.fromtimestamp(current_time)
|
||||
# 判断是否跨天:如果完成日期和当前日期不同,或跨年,则显示完整日期时间
|
||||
if (end_datetime.date() != current_datetime.date() or
|
||||
end_datetime.year != current_datetime.year):
|
||||
end_time_str = end_datetime.strftime('%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
end_time_str = end_datetime.strftime('%H:%M:%S')
|
||||
cost_time_str = self._format_time(cost_time)
|
||||
end_time_str = end_datetime.strftime('%H:%M:%S')
|
||||
|
||||
text = f"## ⚡️ 冒险开始\n\n"
|
||||
text += f"你已经开始了冒险之旅,本次冒险将持续 **{cost_time_str}**。\n\n"
|
||||
text += f"你已经开始了冒险之旅,本次冒险将持续 **{cost_time}** 分钟。\n\n"
|
||||
text += f"**预计完成时间**: {end_time_str}\n\n"
|
||||
text += "---\n\n"
|
||||
text += "💡 提示:冒险期间无法进行炼金,完成后使用 `.adventure` 获取奖励!"
|
||||
@@ -275,8 +200,9 @@ class AdventureGame(BaseGame):
|
||||
|
||||
current_time = int(time.time())
|
||||
elapsed_seconds = max(0, current_time - int(start_time))
|
||||
if elapsed_seconds < 1:
|
||||
elapsed_seconds = 1
|
||||
elapsed_minutes = elapsed_seconds // 60
|
||||
if elapsed_minutes < 1:
|
||||
elapsed_minutes = 1
|
||||
|
||||
# 计算最低倍率
|
||||
try:
|
||||
@@ -285,7 +211,7 @@ class AdventureGame(BaseGame):
|
||||
# 兜底:若奖池异常,按0.5处理
|
||||
min_multiplier = 0.5
|
||||
|
||||
reward_points = int(min_multiplier * elapsed_seconds)
|
||||
reward_points = int(min_multiplier * elapsed_minutes)
|
||||
if reward_points < 0:
|
||||
reward_points = 0
|
||||
|
||||
@@ -298,9 +224,8 @@ class AdventureGame(BaseGame):
|
||||
updated_points = self.db.get_user_points(user_id)
|
||||
|
||||
# 输出
|
||||
elapsed_time_str = self._format_time(elapsed_seconds)
|
||||
text = f"## ⚡️ 冒险放弃\n\n"
|
||||
text += f"**已计入时间**: {elapsed_time_str}\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"
|
||||
@@ -347,12 +272,9 @@ class AdventureGame(BaseGame):
|
||||
"""
|
||||
text = f"## ⚡️ 冒险系统\n\n"
|
||||
text += f"### 基础用法\n"
|
||||
text += f"- `.adventure` - 消耗1秒进行冒险\n"
|
||||
text += f"- `.adventure 60` - 消耗60秒进行冒险\n"
|
||||
text += f"- `.adventure 1h30m10s` - 消耗1小时30分10秒进行冒险\n"
|
||||
text += f"- `.adventure 30m` - 消耗30分钟进行冒险\n"
|
||||
text += f"- `.adventure 10s` - 消耗10秒进行冒险\n\n"
|
||||
text += f"**时间格式说明**:支持时分秒组合,如 `1h30m10s`、`30m`、`10s`,也支持纯数字(按秒计算)。\n\n"
|
||||
text += f"- `.adventure` - 消耗1分钟进行冒险\n"
|
||||
text += f"- `.adventure time` - 消耗time分钟进行冒险, 最少一分钟\n"
|
||||
|
||||
text += f"### 其他功能\n"
|
||||
text += f"- `.adventure abandon` - 放弃当前冒险,按最低倍率结算已冒险时间\n"
|
||||
text += f"- `.adventure 放弃` - 放弃当前冒险,按最低倍率结算已冒险时间\n"
|
||||
|
||||
@@ -23,12 +23,12 @@ class AlchemyGame(BaseGame):
|
||||
self.prize_pool: List[Tuple[int, str, float, str]] = [
|
||||
# (权重, 类型, 倍率, 描述)
|
||||
(500, "penalty", 0, "炼金失败"),
|
||||
(160, "penalty", -1, "炼金爆炸"),
|
||||
(110, "points", 0.1, "少量积分"),
|
||||
(100, "penalty", -1, "炼金爆炸"),
|
||||
(100, "points", 0.1, "少量积分"),
|
||||
(390, "points", 0.5, "少量积分"),
|
||||
(500, "points", 1, "等值积分"),
|
||||
(390, "points", 2, "丰厚积分"),
|
||||
(136, "points", 5, "丰厚积分"),
|
||||
(200, "points", 5, "丰厚积分"),
|
||||
(9, "points", 10, "🌟 巨额积分"),
|
||||
(1, "points", 100, "💎 传说积分"),
|
||||
]
|
||||
@@ -92,9 +92,9 @@ class AlchemyGame(BaseGame):
|
||||
end_time = start_time + cost_time * 60
|
||||
remaining_seconds = end_time - current_time
|
||||
|
||||
# 如果冒险已完成,提示用户先回收奖励,不允许炼金
|
||||
# 如果冒险已完成,自动清理状态,允许炼金
|
||||
if remaining_seconds <= 0:
|
||||
return f"❌ 你有待回收的冒险奖励!\n\n💡 请先使用 `.adventure` 回收冒险奖励后再进行炼金。"
|
||||
self.db.delete_game_state(0, user_id, 'adventure')
|
||||
else:
|
||||
# 冒险未完成,返回错误提示
|
||||
remaining_minutes = remaining_seconds // 60
|
||||
|
||||
@@ -82,6 +82,13 @@ def get_help_message() -> str:
|
||||
- `.gomoku list` - 列出所有对战
|
||||
- `.gomoku stats` - 查看战绩
|
||||
|
||||
### ⚔️ 三国杀
|
||||
- `.sgs create` - 创建游戏
|
||||
- `.sgs join` - 加入游戏
|
||||
- `.sgs start` - 开始游戏
|
||||
- `.sgs status` - 查看状态
|
||||
- `.sgs help` - 查看帮助
|
||||
|
||||
### 💎 积分系统
|
||||
- `.points` - 查看个人积分
|
||||
- `.积分` - 查看个人积分
|
||||
@@ -113,43 +120,6 @@ 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` - 庄家放弃游戏(返还下注)
|
||||
|
||||
### 🐺 狼人杀
|
||||
- `.狼人杀 open` - 主持人创建房间
|
||||
- `.狼人杀 join` - 加入游戏
|
||||
- `.狼人杀 start` - 主持人开始游戏
|
||||
- `.狼人杀 <id> <消息>` - 私聊指定玩家
|
||||
- `.狼人杀 狼人 <消息>` - 狼人群聊
|
||||
- `.狼人杀 杀 <id>` - 狼人投票杀人
|
||||
- `.狼人杀 验 <id>` - 预言家验人
|
||||
- `.狼人杀 救 <id>` - 女巫救人
|
||||
- `.狼人杀 毒 <id>` - 女巫毒人
|
||||
- `.狼人杀 status` - 查看状态
|
||||
|
||||
### 其他
|
||||
- `.help` - 显示帮助
|
||||
- `.stats` - 查看个人统计
|
||||
|
||||
1473
games/casino.py
1473
games/casino.py
File diff suppressed because it is too large
Load Diff
@@ -82,15 +82,20 @@ class GiftGame(BaseGame):
|
||||
receiver_id = user['user_id']
|
||||
|
||||
# 获取接收者名称用于显示
|
||||
receiver_name = self.db.get_user_display_name(receiver_id)
|
||||
receiver_user = self.db.get_or_create_user(receiver_id)
|
||||
receiver_name = receiver_user.get('username', f"用户{receiver_id}")
|
||||
|
||||
# 获取发送者名称用于显示
|
||||
sender_name = self.db.get_user_display_name(sender_id)
|
||||
sender_user = self.db.get_or_create_user(sender_id)
|
||||
sender_name = sender_user.get('username', f"用户{sender_id}")
|
||||
|
||||
# 验证参数
|
||||
if points <= 0:
|
||||
return "❌ 赠送积分数量必须大于0!"
|
||||
|
||||
if points > 1000:
|
||||
return "❌ 单次赠送积分不能超过1000!"
|
||||
|
||||
if sender_id == receiver_id:
|
||||
return "❌ 不能赠送积分给自己!"
|
||||
|
||||
@@ -179,7 +184,7 @@ class GiftGame(BaseGame):
|
||||
|
||||
for record in records:
|
||||
timestamp = datetime.fromtimestamp(record['created_at']).strftime('%m-%d %H:%M')
|
||||
receiver_name = self.db.get_user_display_name(record['receiver_id'])
|
||||
receiver_name = record.get('receiver_name', f"用户{record['receiver_id']}")
|
||||
points = record['points']
|
||||
message = record.get('message', '')
|
||||
|
||||
@@ -209,7 +214,7 @@ class GiftGame(BaseGame):
|
||||
|
||||
for record in records:
|
||||
timestamp = datetime.fromtimestamp(record['created_at']).strftime('%m-%d %H:%M')
|
||||
sender_name = self.db.get_user_display_name(record['sender_id'])
|
||||
sender_name = record.get('sender_name', f"用户{record['sender_id']}")
|
||||
points = record['points']
|
||||
message = record.get('message', '')
|
||||
|
||||
|
||||
@@ -379,12 +379,10 @@ class IdiomGame(BaseGame):
|
||||
text += f"**当前链长**:{state_data['chain_length']}\n\n"
|
||||
|
||||
user_count = state_data['participants'][str(user_id)]
|
||||
user_display_name = self.db.get_user_display_name(user_id)
|
||||
text += f"@{user_display_name} 成功次数:{user_count}\n\n"
|
||||
text += f"@用户{user_id} 成功次数:{user_count}\n\n"
|
||||
|
||||
if mentioned_user_id:
|
||||
mentioned_display_name = self.db.get_user_display_name(mentioned_user_id)
|
||||
text += f"已指定 @{mentioned_display_name} 接龙\n\n"
|
||||
text += f"已指定 @用户{mentioned_user_id} 接龙\n\n"
|
||||
else:
|
||||
text += "任何人都可以接龙\n\n"
|
||||
|
||||
@@ -418,8 +416,7 @@ class IdiomGame(BaseGame):
|
||||
state_data['next_user_id'] = next_user_id
|
||||
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
|
||||
|
||||
next_user_display_name = self.db.get_user_display_name(next_user_id)
|
||||
return f"✅ 已指定 @{next_user_display_name} 接龙"
|
||||
return f"✅ 已指定 @用户{next_user_id} 接龙"
|
||||
|
||||
def _reject_idiom(self, chat_id: int, user_id: int, idiom: str) -> str:
|
||||
"""裁判拒绝词语
|
||||
@@ -512,8 +509,7 @@ class IdiomGame(BaseGame):
|
||||
|
||||
# 下一位
|
||||
if state_data.get('next_user_id'):
|
||||
next_user_display_name = self.db.get_user_display_name(state_data['next_user_id'])
|
||||
text += f"**下一位**:@{next_user_display_name}\n\n"
|
||||
text += f"**下一位**:@用户{state_data['next_user_id']}\n\n"
|
||||
else:
|
||||
text += f"**下一位**:任何人都可以接龙\n\n"
|
||||
|
||||
@@ -526,8 +522,7 @@ class IdiomGame(BaseGame):
|
||||
reverse=True
|
||||
)
|
||||
for idx, (uid, count) in enumerate(sorted_participants[:5], 1):
|
||||
user_display_name = self.db.get_user_display_name(int(uid))
|
||||
text += f"{idx}. @{user_display_name} - {count}次\n"
|
||||
text += f"{idx}. @用户{uid} - {count}次\n"
|
||||
text += "\n"
|
||||
|
||||
# 最近成语
|
||||
@@ -593,8 +588,7 @@ class IdiomGame(BaseGame):
|
||||
reverse=True
|
||||
)
|
||||
for idx, (uid, count) in enumerate(sorted_participants, 1):
|
||||
user_display_name = self.db.get_user_display_name(int(uid))
|
||||
text += f"{idx}. @{user_display_name} - {count}次\n"
|
||||
text += f"{idx}. @用户{uid} - {count}次\n"
|
||||
# 更新统计
|
||||
try:
|
||||
for _ in range(count):
|
||||
|
||||
@@ -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 = self.db.get_user_display_name(user['user_id'])
|
||||
username = user.get('username', f"用户{user['user_id']}")
|
||||
points = user.get('points', 0)
|
||||
|
||||
text += f"{medal} **第 {rank} 名** {username}\n"
|
||||
|
||||
534
games/sanguosha.py
Normal file
534
games/sanguosha.py
Normal file
@@ -0,0 +1,534 @@
|
||||
"""三国杀游戏主控制器"""
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
from games.base import BaseGame
|
||||
from games.sgs_game import GameState, get_game_manager
|
||||
from games.sgs_core import Phase, Role
|
||||
from utils.parser import CommandParser
|
||||
from core.database import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SanguoshaGame(BaseGame):
|
||||
"""三国杀游戏"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化游戏"""
|
||||
super().__init__()
|
||||
self.manager = get_game_manager()
|
||||
|
||||
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理游戏指令
|
||||
|
||||
Args:
|
||||
command: 指令
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
回复消息
|
||||
"""
|
||||
try:
|
||||
# 提取指令和参数
|
||||
cmd, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip().lower()
|
||||
|
||||
# 获取用户信息
|
||||
user = self.db.get_or_create_user(user_id)
|
||||
username = user.get('username') or f'用户{user_id}'
|
||||
|
||||
# 路由指令
|
||||
if not args or args == 'help':
|
||||
return self.get_help()
|
||||
|
||||
elif args == 'create' or args == '创建':
|
||||
return await self._handle_create(chat_id, user_id, username)
|
||||
|
||||
elif args == 'join' or args == '加入':
|
||||
return await self._handle_join(chat_id, user_id, username)
|
||||
|
||||
elif args == 'leave' or args == '离开':
|
||||
return await self._handle_leave(chat_id, user_id)
|
||||
|
||||
elif args == 'start' or args == '开始':
|
||||
return await self._handle_start(chat_id, user_id)
|
||||
|
||||
elif args == 'status' or args == '状态':
|
||||
return await self._handle_status(chat_id, user_id)
|
||||
|
||||
elif args == 'players' or args == '玩家':
|
||||
return await self._handle_players(chat_id)
|
||||
|
||||
elif args == 'hand' or args == '手牌':
|
||||
return await self._handle_hand(chat_id, user_id)
|
||||
|
||||
elif args == 'next' or args == '下一阶段':
|
||||
return await self._handle_next_phase(chat_id, user_id)
|
||||
|
||||
elif args.startswith('play ') or args.startswith('出牌 '):
|
||||
card_name = args.split(maxsplit=1)[1]
|
||||
return await self._handle_play_card(chat_id, user_id, card_name)
|
||||
|
||||
elif args.startswith('use ') or args.startswith('使用 '):
|
||||
card_name = args.split(maxsplit=1)[1]
|
||||
return await self._handle_use_card(chat_id, user_id, card_name)
|
||||
|
||||
elif args == 'draw' or args == '摸牌':
|
||||
return await self._handle_draw(chat_id, user_id)
|
||||
|
||||
elif args == 'discard' or args == '弃牌':
|
||||
return await self._handle_discard(chat_id, user_id)
|
||||
|
||||
elif args == 'cancel' or args == '取消':
|
||||
return await self._handle_cancel(chat_id, user_id)
|
||||
|
||||
elif args == 'stats' or args == '战绩':
|
||||
return await self._handle_stats(user_id)
|
||||
|
||||
else:
|
||||
return f"❌ 未知指令: {args}\n\n使用 `.sgs help` 查看帮助"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理三国杀指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
async def _handle_create(self, chat_id: int, user_id: int, username: str) -> str:
|
||||
"""创建游戏"""
|
||||
# 检查是否已有游戏
|
||||
if self.manager.has_active_game(chat_id):
|
||||
return "❌ 当前已有进行中的游戏"
|
||||
|
||||
# 创建游戏
|
||||
game = self.manager.create_game(chat_id, user_id, username)
|
||||
|
||||
return f"""✅ 三国杀游戏已创建!
|
||||
|
||||
**房主**: {username}
|
||||
**游戏ID**: {game.game_id}
|
||||
|
||||
📝 其他玩家使用 `.sgs join` 加入游戏
|
||||
📝 人数达到 {game.min_players}-{game.max_players} 人后,房主使用 `.sgs start` 开始游戏
|
||||
|
||||
当前玩家: 1/{game.max_players}"""
|
||||
|
||||
async def _handle_join(self, chat_id: int, user_id: int, username: str) -> str:
|
||||
"""加入游戏"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏,使用 `.sgs create` 创建游戏"
|
||||
|
||||
if game.is_started:
|
||||
return "❌ 游戏已经开始,无法加入"
|
||||
|
||||
if game.get_player_by_id(user_id):
|
||||
return "❌ 你已经在游戏中了"
|
||||
|
||||
if not game.add_player(user_id, username):
|
||||
return f"❌ 加入失败,游戏已满({game.max_players}人)"
|
||||
|
||||
return f"""✅ {username} 加入游戏!
|
||||
|
||||
当前玩家: {len(game.players)}/{game.max_players}
|
||||
玩家列表: {', '.join(p.username for p in game.players)}
|
||||
|
||||
{f'📝 人数已满,房主可以使用 `.sgs start` 开始游戏' if len(game.players) >= game.min_players else f'📝 还需要 {game.min_players - len(game.players)} 人才能开始'}"""
|
||||
|
||||
async def _handle_leave(self, chat_id: int, user_id: int) -> str:
|
||||
"""离开游戏"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if game.is_started:
|
||||
return "❌ 游戏已经开始,无法离开"
|
||||
|
||||
player = game.get_player_by_id(user_id)
|
||||
if not player:
|
||||
return "❌ 你不在游戏中"
|
||||
|
||||
if not game.remove_player(user_id):
|
||||
return "❌ 离开失败"
|
||||
|
||||
# 如果房主离开且还有其他玩家,转移房主
|
||||
if user_id == game.host_id and game.players:
|
||||
game.host_id = game.players[0].user_id
|
||||
return f"✅ {player.username} 离开游戏,房主已转移给 {game.players[0].username}"
|
||||
|
||||
# 如果没有玩家了,删除游戏
|
||||
if not game.players:
|
||||
self.manager.remove_game(chat_id)
|
||||
return f"✅ {player.username} 离开游戏,游戏已解散"
|
||||
|
||||
return f"✅ {player.username} 离开游戏\n\n当前玩家: {len(game.players)}/{game.max_players}"
|
||||
|
||||
async def _handle_start(self, chat_id: int, user_id: int) -> str:
|
||||
"""开始游戏"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if user_id != game.host_id:
|
||||
return "❌ 只有房主可以开始游戏"
|
||||
|
||||
success, message = game.start_game()
|
||||
if not success:
|
||||
return f"❌ {message}"
|
||||
|
||||
# 构建游戏开始消息
|
||||
result = "## 🎮 三国杀游戏开始!\n\n"
|
||||
|
||||
# 显示主公信息
|
||||
lord = game.lord_player
|
||||
if lord:
|
||||
result += f"### 👑 主公\n"
|
||||
result += f"**{lord.username}** - {lord.general.name}({lord.general.kingdom})\n"
|
||||
result += f"体力: {lord.hp}/{lord.general.max_hp}\n"
|
||||
result += f"技能: {', '.join(s.name for s in lord.general.skills)}\n\n"
|
||||
|
||||
# 显示其他玩家信息(不显示身份)
|
||||
result += f"### 👥 玩家列表\n"
|
||||
for idx, player in enumerate(game.players, 1):
|
||||
if player.role != Role.LORD:
|
||||
result += f"{idx}. **{player.username}** - {player.general.name}({player.general.kingdom})\n"
|
||||
result += f" 体力: {player.hp}/{player.general.max_hp}\n"
|
||||
|
||||
result += f"\n### 📋 游戏信息\n"
|
||||
result += f"- 玩家数: {len(game.players)}\n"
|
||||
result += f"- 当前回合: {game.current_player.username}\n"
|
||||
result += f"- 当前阶段: {game.current_phase.value}\n\n"
|
||||
|
||||
result += "💡 使用 `.sgs status` 查看游戏状态\n"
|
||||
result += "💡 使用 `.sgs hand` 查看手牌\n"
|
||||
result += "💡 使用 `.sgs next` 进入下一阶段"
|
||||
|
||||
return result
|
||||
|
||||
async def _handle_status(self, chat_id: int, user_id: int) -> str:
|
||||
"""查看游戏状态"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if not game.is_started:
|
||||
return f"""## 📋 游戏状态 (准备中)
|
||||
|
||||
**房主**: {game.players[0].username if game.players else '无'}
|
||||
**玩家数**: {len(game.players)}/{game.max_players}
|
||||
**玩家列表**: {', '.join(p.username for p in game.players)}
|
||||
|
||||
📝 使用 `.sgs start` 开始游戏"""
|
||||
|
||||
# 游戏进行中
|
||||
result = f"## 📋 游戏状态\n\n"
|
||||
result += f"### 🕒 当前回合\n"
|
||||
result += f"- 玩家: **{game.current_player.username}**\n"
|
||||
result += f"- 阶段: **{game.current_phase.value}**\n"
|
||||
result += f"- 回合数: {game.round_number}\n\n"
|
||||
|
||||
result += f"### 👥 玩家状态\n"
|
||||
for idx, player in enumerate(game.players, 1):
|
||||
status = "💀" if not player.is_alive else "✅"
|
||||
role_display = player.role.value if player.role == Role.LORD or not player.is_alive else "???"
|
||||
|
||||
result += f"{idx}. {status} **{player.username}** ({role_display})\n"
|
||||
result += f" 武将: {player.general.name}({player.general.kingdom})\n"
|
||||
result += f" 体力: {'❤️' * player.hp}{'🖤' * (player.general.max_hp - player.hp)} {player.hp}/{player.general.max_hp}\n"
|
||||
result += f" 手牌: {player.hand_count}张\n"
|
||||
|
||||
if player.equipment:
|
||||
equip_list = ', '.join(f"{k}:{v.name}" for k, v in player.equipment.items())
|
||||
result += f" 装备: {equip_list}\n"
|
||||
|
||||
result += "\n"
|
||||
|
||||
return result
|
||||
|
||||
async def _handle_players(self, chat_id: int) -> str:
|
||||
"""查看玩家列表"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
result = f"## 👥 玩家列表\n\n"
|
||||
for idx, player in enumerate(game.players, 1):
|
||||
result += f"{idx}. **{player.username}**"
|
||||
if game.is_started:
|
||||
result += f" - {player.general.name}"
|
||||
if player.role == Role.LORD or not player.is_alive:
|
||||
result += f"({player.role.value})"
|
||||
result += "\n"
|
||||
|
||||
return result
|
||||
|
||||
async def _handle_hand(self, chat_id: int, user_id: int) -> str:
|
||||
"""查看手牌"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if not game.is_started:
|
||||
return "❌ 游戏还未开始"
|
||||
|
||||
player = game.get_player_by_id(user_id)
|
||||
if not player:
|
||||
return "❌ 你不在游戏中"
|
||||
|
||||
if not player.is_alive:
|
||||
return "❌ 你已经阵亡"
|
||||
|
||||
if not player.hand_cards:
|
||||
return "📋 你的手牌为空"
|
||||
|
||||
result = f"## 🃏 你的手牌({len(player.hand_cards)}张)\n\n"
|
||||
|
||||
# 按类型分组
|
||||
basic_cards = [c for c in player.hand_cards if c.card_type.value == "基本牌"]
|
||||
trick_cards = [c for c in player.hand_cards if c.card_type.value == "锦囊牌"]
|
||||
equip_cards = [c for c in player.hand_cards if c.card_type.value == "装备牌"]
|
||||
|
||||
if basic_cards:
|
||||
result += "### 基本牌\n"
|
||||
for idx, card in enumerate(basic_cards, 1):
|
||||
result += f"{idx}. {card}\n"
|
||||
result += "\n"
|
||||
|
||||
if trick_cards:
|
||||
result += "### 锦囊牌\n"
|
||||
for idx, card in enumerate(trick_cards, 1):
|
||||
result += f"{idx}. {card}\n"
|
||||
result += "\n"
|
||||
|
||||
if equip_cards:
|
||||
result += "### 装备牌\n"
|
||||
for idx, card in enumerate(equip_cards, 1):
|
||||
result += f"{idx}. {card}\n"
|
||||
result += "\n💡 使用 `.sgs play 卡牌名` 出牌"
|
||||
|
||||
return result
|
||||
|
||||
async def _handle_next_phase(self, chat_id: int, user_id: int) -> str:
|
||||
"""进入下一阶段"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if not game.is_started:
|
||||
return "❌ 游戏还未开始"
|
||||
|
||||
if game.current_player.user_id != user_id:
|
||||
return "❌ 不是你的回合"
|
||||
|
||||
# 执行阶段转换
|
||||
new_phase, current_player = game.next_phase()
|
||||
|
||||
# 检查游戏是否结束
|
||||
is_over, winner_role = game.check_game_over()
|
||||
if is_over:
|
||||
game.is_finished = True
|
||||
game.winner_role = winner_role
|
||||
return await self._handle_game_over(game)
|
||||
|
||||
result = f"✅ 进入下一阶段\n\n"
|
||||
result += f"**当前玩家**: {current_player.username}\n"
|
||||
result += f"**当前阶段**: {new_phase.value}\n\n"
|
||||
|
||||
# 阶段提示
|
||||
if new_phase == Phase.DRAW:
|
||||
result += "💡 摸牌阶段,使用 `.sgs draw` 摸牌"
|
||||
elif new_phase == Phase.PLAY:
|
||||
result += "💡 出牌阶段,使用 `.sgs play 卡牌名` 出牌"
|
||||
elif new_phase == Phase.DISCARD:
|
||||
result += "💡 弃牌阶段,使用 `.sgs discard` 弃牌"
|
||||
else:
|
||||
result += "请使用正确的语法"
|
||||
|
||||
return result
|
||||
|
||||
async def _handle_draw(self, chat_id: int, user_id: int) -> str:
|
||||
"""摸牌"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if game.current_player.user_id != user_id:
|
||||
return "❌ 不是你的回合"
|
||||
|
||||
if game.current_phase != Phase.DRAW:
|
||||
return f"❌ 当前不是摸牌阶段(当前: {game.current_phase.value})"
|
||||
|
||||
# 摸2张牌
|
||||
cards = game.deck.draw(2)
|
||||
game.current_player.hand_cards.extend(cards)
|
||||
|
||||
result = f"✅ 摸牌成功!\n\n"
|
||||
result += f"摸到: {', '.join(str(c) for c in cards)}\n"
|
||||
result += f"当前手牌数: {game.current_player.hand_count}\n\n"
|
||||
result += "💡 使用 `.sgs next` 进入出牌阶段"
|
||||
|
||||
return result
|
||||
|
||||
async def _handle_play_card(self, chat_id: int, user_id: int, card_name: str) -> str:
|
||||
"""出牌"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if game.current_player.user_id != user_id:
|
||||
return "❌ 不是你的回合"
|
||||
|
||||
if game.current_phase != Phase.PLAY:
|
||||
return f"❌ 当前不是出牌阶段(当前: {game.current_phase.value})"
|
||||
|
||||
player = game.current_player
|
||||
|
||||
# 查找卡牌
|
||||
card = None
|
||||
for c in player.hand_cards:
|
||||
if c.name == card_name:
|
||||
card = c
|
||||
break
|
||||
|
||||
if not card:
|
||||
return f"❌ 你没有【{card_name}】这张牌"
|
||||
|
||||
# 简化处理:直接打出(实际游戏需要选择目标等)
|
||||
player.remove_card(card)
|
||||
game.deck.discard([card])
|
||||
|
||||
return f"✅ 使用了【{card}】\n\n💡 继续出牌或使用 `.sgs next` 进入弃牌阶段"
|
||||
|
||||
async def _handle_use_card(self, chat_id: int, user_id: int, card_name: str) -> str:
|
||||
"""使用卡牌(同play_card)"""
|
||||
return await self._handle_play_card(chat_id, user_id, card_name)
|
||||
|
||||
async def _handle_discard(self, chat_id: int, user_id: int) -> str:
|
||||
"""弃牌"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if game.current_player.user_id != user_id:
|
||||
return "❌ 不是你的回合"
|
||||
|
||||
if game.current_phase != Phase.DISCARD:
|
||||
return f"❌ 当前不是弃牌阶段(当前: {game.current_phase.value})"
|
||||
|
||||
player = game.current_player
|
||||
|
||||
# 检查是否需要弃牌
|
||||
max_hand = player.hp
|
||||
if player.hand_count <= max_hand:
|
||||
return f"✅ 手牌数({player.hand_count})未超过体力值({max_hand}),无需弃牌\n\n💡 使用 `.sgs next` 结束回合"
|
||||
|
||||
# 简化处理:自动弃掉多余的牌
|
||||
discard_count = player.hand_count - max_hand
|
||||
discarded = player.hand_cards[:discard_count]
|
||||
player.hand_cards = player.hand_cards[discard_count:]
|
||||
game.deck.discard(discarded)
|
||||
|
||||
return f"✅ 弃置了 {discard_count} 张牌\n\n💡 使用 `.sgs next` 结束回合"
|
||||
|
||||
async def _handle_cancel(self, chat_id: int, user_id: int) -> str:
|
||||
"""取消游戏"""
|
||||
game = self.manager.get_game(chat_id)
|
||||
if not game:
|
||||
return "❌ 当前没有游戏"
|
||||
|
||||
if user_id != game.host_id:
|
||||
return "❌ 只有房主可以取消游戏"
|
||||
|
||||
self.manager.remove_game(chat_id)
|
||||
return "✅ 游戏已取消"
|
||||
|
||||
async def _handle_stats(self, user_id: int) -> str:
|
||||
"""查看战绩"""
|
||||
stats = self.db.get_game_stats(user_id, 'sanguosha')
|
||||
|
||||
if stats['total_plays'] == 0:
|
||||
return "三国杀游戏记录"
|
||||
|
||||
win_rate = (stats['wins'] / stats['total_plays'] * 100) if stats['total_plays'] > 0 else 0
|
||||
|
||||
result ="战绩\n\n"
|
||||
result += f"- 总局数: {stats['total_plays']}\n"
|
||||
result += f"- 胜利: {stats['wins']} 次\n"
|
||||
result += f"- 失败: {stats['losses']} 次\n"
|
||||
result += f"- 胜率: {win_rate:.1f}%\n"
|
||||
|
||||
return result
|
||||
|
||||
async def _handle_game_over(self, game: GameState) -> str:
|
||||
"""处理游戏结束"""
|
||||
result = "## 🎉 游戏结束!\n\n"
|
||||
|
||||
if game.winner_role == Role.LORD:
|
||||
result += "## 🎉公和忠臣获胜!\n\n"
|
||||
winners = [p for p in game.players if p.role in [Role.LORD, Role.LOYAL]]
|
||||
losers = [p for p in game.players if p.role in [Role.REBEL, Role.SPY]]
|
||||
else:
|
||||
result += "### ⚔️ 反贼获胜!\n\n"
|
||||
winners = [p for p in game.players if p.role == Role.REBEL]
|
||||
losers = [p for p in game.players if p.role in [Role.LORD, Role.LOYAL, Role.SPY]]
|
||||
|
||||
result += "**获胜方**:\n"
|
||||
for player in winners:
|
||||
result += f"- {player.username} ({player.role.value}) - {player.general.name}\n"
|
||||
# 更新战绩
|
||||
self.db.update_game_stats(player.user_id, 'sanguosha', win=True)
|
||||
|
||||
result += "\n**失败方**:\n"
|
||||
for player in losers:
|
||||
result += f"- {player.username} ({player.role.value}) - {player.general.name}\n"
|
||||
# 更新战绩
|
||||
self.db.update_game_stats(player.user_id, 'sanguosha', loss=True)
|
||||
|
||||
result += f"\n游戏时长: {game.round_number} 回合"
|
||||
|
||||
# 清理游戏
|
||||
self.manager.remove_game(game.chat_id)
|
||||
|
||||
return result
|
||||
|
||||
def get_help(self) -> str:
|
||||
"""获取帮助信息"""
|
||||
return """## 🎮 三国杀游戏帮助
|
||||
|
||||
### 游戏准备
|
||||
- `.sgs create` - 创建游戏房间
|
||||
- `.sgs join` - 加入游戏
|
||||
- `.sgs leave` - 离开游戏
|
||||
- `.sgs start` - 开始游戏(房主)
|
||||
- `.sgs cancel` - 取消游戏(房主)
|
||||
|
||||
### 游戏中
|
||||
- `.sgs status` - 查看游戏状态
|
||||
- `.sgs players` - 查看玩家列表
|
||||
- `.sgs hand` - 查看手牌
|
||||
- `.sgs next` - 进入下一阶段
|
||||
- `.sgs draw` - 摸牌(摸牌阶段)
|
||||
- `.sgs play 卡牌名` - 出牌(出牌阶段)
|
||||
- `.sgs discard` - 弃牌(弃牌阶段)
|
||||
|
||||
### 其他
|
||||
- `.sgs stats` - 查看个人战绩
|
||||
- `.sgs help` - 显示帮助
|
||||
|
||||
### 游戏规则
|
||||
1. **身份**: 主公、忠臣、反贼、内奸
|
||||
2. **胜利条件**:
|
||||
- 主公+忠臣: 消灭所有反贼和内奸
|
||||
- 反贼: 击杀主公
|
||||
- 内奸: 成为最后存活的人
|
||||
3. **回合流程**:
|
||||
- 准备阶段 → 判定阶段 → 摸牌阶段 → 出牌阶段 → 弃牌阶段 → 结束阶段
|
||||
|
||||
### 卡牌类型
|
||||
- **基本牌**: 杀、闪、桃
|
||||
- **锦囊牌**: 决斗、过河拆桥、顺手牵羊、南蛮入侵、万箭齐发等
|
||||
- **装备牌**: 武器、防具、马
|
||||
|
||||
---
|
||||
💡 提示:游戏支持 2-8 人,建议 5-8 人游戏体验最佳
|
||||
"""
|
||||
|
||||
529
games/sgs_core.py
Normal file
529
games/sgs_core.py
Normal file
@@ -0,0 +1,529 @@
|
||||
"""三国杀游戏核心逻辑模块"""
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Set
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, field
|
||||
import random
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CardType(Enum):
|
||||
"""卡牌类型"""
|
||||
BASIC = "基本牌"
|
||||
TRICK = "锦囊牌"
|
||||
EQUIPMENT = "装备牌"
|
||||
|
||||
|
||||
class CardSuit(Enum):
|
||||
"""卡牌花色"""
|
||||
SPADE = "♠" # 黑桃
|
||||
HEART = "♥" # 红桃
|
||||
CLUB = "♣" # 梅花
|
||||
DIAMOND = "♦" # 方块
|
||||
|
||||
|
||||
class CardColor(Enum):
|
||||
"""卡牌颜色"""
|
||||
RED = "红色"
|
||||
BLACK = "黑色"
|
||||
|
||||
|
||||
class Role(Enum):
|
||||
"""角色身份"""
|
||||
LORD = "主公"
|
||||
LOYAL = "忠臣"
|
||||
REBEL = "反贼"
|
||||
SPY = "内奸"
|
||||
|
||||
|
||||
class Phase(Enum):
|
||||
"""回合阶段"""
|
||||
PREPARE = "准备阶段"
|
||||
JUDGE = "判定阶段"
|
||||
DRAW = "摸牌阶段"
|
||||
PLAY = "出牌阶段"
|
||||
DISCARD = "弃牌阶段"
|
||||
END = "结束阶段"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Card:
|
||||
"""卡牌"""
|
||||
name: str # 卡牌名称
|
||||
card_type: CardType # 卡牌类型
|
||||
suit: CardSuit # 花色
|
||||
number: int # 点数 (1-13)
|
||||
description: str = "" # 描述
|
||||
|
||||
@property
|
||||
def color(self) -> CardColor:
|
||||
"""获取卡牌颜色"""
|
||||
if self.suit in [CardSuit.HEART, CardSuit.DIAMOND]:
|
||||
return CardColor.RED
|
||||
return CardColor.BLACK
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""字符串表示"""
|
||||
return f"{self.suit.value}{self.number} {self.name}"
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"type": self.card_type.value,
|
||||
"suit": self.suit.value,
|
||||
"number": self.number,
|
||||
"color": self.color.value,
|
||||
"description": self.description
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Skill:
|
||||
"""技能"""
|
||||
name: str # 技能名称
|
||||
description: str # 技能描述
|
||||
skill_type: str = "主动" # 技能类型: 主动/锁定/限定/觉醒
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"type": self.skill_type
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class General:
|
||||
"""武将"""
|
||||
name: str # 武将名称
|
||||
max_hp: int # 体力上限
|
||||
skills: List[Skill] # 技能列表
|
||||
kingdom: str = "魏" # 势力
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"max_hp": self.max_hp,
|
||||
"kingdom": self.kingdom,
|
||||
"skills": [skill.to_dict() for skill in self.skills]
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Player:
|
||||
"""玩家"""
|
||||
user_id: int # 用户ID
|
||||
username: str # 用户名
|
||||
general: Optional[General] = None # 武将
|
||||
role: Optional[Role] = None # 身份
|
||||
hp: int = 0 # 当前体力
|
||||
hand_cards: List[Card] = field(default_factory=list) # 手牌
|
||||
equipment: Dict[str, Card] = field(default_factory=dict) # 装备区
|
||||
judge_area: List[Card] = field(default_factory=list) # 判定区
|
||||
is_alive: bool = True # 是否存活
|
||||
is_chained: bool = False # 是否横置
|
||||
|
||||
def __post_init__(self):
|
||||
"""初始化后处理"""
|
||||
if self.general and self.hp == 0:
|
||||
self.hp = self.general.max_hp
|
||||
|
||||
@property
|
||||
def hand_count(self) -> int:
|
||||
"""手牌数量"""
|
||||
return len(self.hand_cards)
|
||||
|
||||
@property
|
||||
def attack_range(self) -> int:
|
||||
"""攻击距离"""
|
||||
weapon = self.equipment.get("武器")
|
||||
if weapon:
|
||||
# 根据武器名称返回距离
|
||||
weapon_ranges = {
|
||||
"诸葛连弩": 1,
|
||||
"青釭剑": 2,
|
||||
"雌雄双股剑": 2,
|
||||
"青龙偃月刀": 3,
|
||||
"丈八蛇矛": 3,
|
||||
"方天画戟": 4,
|
||||
"麒麟弓": 5
|
||||
}
|
||||
return weapon_ranges.get(weapon.name, 1)
|
||||
return 1
|
||||
|
||||
def add_card(self, card: Card):
|
||||
"""添加手牌"""
|
||||
self.hand_cards.append(card)
|
||||
|
||||
def remove_card(self, card: Card) -> bool:
|
||||
"""移除手牌"""
|
||||
if card in self.hand_cards:
|
||||
self.hand_cards.remove(card)
|
||||
return True
|
||||
return False
|
||||
|
||||
def take_damage(self, damage: int) -> bool:
|
||||
"""受到伤害
|
||||
|
||||
Returns:
|
||||
是否死亡
|
||||
"""
|
||||
self.hp -= damage
|
||||
if self.hp <= 0:
|
||||
self.is_alive = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def recover(self, amount: int):
|
||||
"""回复体力"""
|
||||
if self.general:
|
||||
self.hp = min(self.hp + amount, self.general.max_hp)
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"user_id": self.user_id,
|
||||
"username": self.username,
|
||||
"general": self.general.to_dict() if self.general else None,
|
||||
"role": self.role.value if self.role else None,
|
||||
"hp": self.hp,
|
||||
"max_hp": self.general.max_hp if self.general else 0,
|
||||
"hand_count": self.hand_count,
|
||||
"equipment": {k: v.to_dict() for k, v in self.equipment.items()},
|
||||
"is_alive": self.is_alive,
|
||||
"is_chained": self.is_chained
|
||||
}
|
||||
|
||||
|
||||
class CardDeck:
|
||||
"""牌堆"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化牌堆"""
|
||||
self.cards: List[Card] = []
|
||||
self.discard_pile: List[Card] = []
|
||||
self._init_standard_deck()
|
||||
|
||||
def _init_standard_deck(self):
|
||||
"""初始化标准牌堆(简化版)"""
|
||||
# 杀 (30张)
|
||||
for suit, numbers in [
|
||||
(CardSuit.SPADE, [7, 8, 8, 9, 9, 10, 10]),
|
||||
(CardSuit.CLUB, [2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11]),
|
||||
(CardSuit.HEART, [10, 10, 11]),
|
||||
(CardSuit.DIAMOND, [6, 7, 8, 9, 10, 13])
|
||||
]:
|
||||
for num in numbers:
|
||||
self.cards.append(Card("杀", CardType.BASIC, suit, num, "对攻击范围内的一名角色造成1点伤害"))
|
||||
|
||||
# 闪 (15张)
|
||||
for suit, numbers in [
|
||||
(CardSuit.HEART, [2, 2, 13]),
|
||||
(CardSuit.DIAMOND, [2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11])
|
||||
]:
|
||||
for num in numbers:
|
||||
self.cards.append(Card("闪", CardType.BASIC, suit, num, "抵消一张【杀】的效果"))
|
||||
|
||||
# 桃 (8张)
|
||||
for suit, numbers in [
|
||||
(CardSuit.HEART, [3, 4, 6, 7, 8, 9, 12]),
|
||||
(CardSuit.DIAMOND, [12])
|
||||
]:
|
||||
for num in numbers:
|
||||
self.cards.append(Card("桃", CardType.BASIC, suit, num, "回复1点体力"))
|
||||
|
||||
# 锦囊牌
|
||||
# 无懈可击 (3张)
|
||||
for suit, num in [(CardSuit.SPADE, 11), (CardSuit.CLUB, 12), (CardSuit.CLUB, 13)]:
|
||||
self.cards.append(Card("无懈可击", CardType.TRICK, suit, num, "抵消一张锦囊牌的效果"))
|
||||
|
||||
# 决斗 (3张)
|
||||
for suit, num in [(CardSuit.SPADE, 1), (CardSuit.CLUB, 1), (CardSuit.DIAMOND, 1)]:
|
||||
self.cards.append(Card("决斗", CardType.TRICK, suit, num, "与目标角色拼点,失败者受到1点伤害"))
|
||||
|
||||
# 过河拆桥 (6张)
|
||||
for suit, numbers in [
|
||||
(CardSuit.SPADE, [3, 4, 12]),
|
||||
(CardSuit.CLUB, [3, 4]),
|
||||
(CardSuit.HEART, [12])
|
||||
]:
|
||||
for num in numbers:
|
||||
self.cards.append(Card("过河拆桥", CardType.TRICK, suit, num, "弃置目标角色的一张牌"))
|
||||
|
||||
# 顺手牵羊 (5张)
|
||||
for suit, numbers in [
|
||||
(CardSuit.SPADE, [3, 4, 11]),
|
||||
(CardSuit.DIAMOND, [3, 4])
|
||||
]:
|
||||
for num in numbers:
|
||||
self.cards.append(Card("顺手牵羊", CardType.TRICK, suit, num, "获得目标角色的一张牌"))
|
||||
|
||||
# 南蛮入侵 (3张)
|
||||
for suit, num in [(CardSuit.SPADE, 7), (CardSuit.SPADE, 13), (CardSuit.CLUB, 7)]:
|
||||
self.cards.append(Card("南蛮入侵", CardType.TRICK, suit, num, "所有其他角色需打出【杀】,否则受到1点伤害"))
|
||||
|
||||
# 万箭齐发 (1张)
|
||||
self.cards.append(Card("万箭齐发", CardType.TRICK, CardSuit.HEART, 1, "所有其他角色需打出【闪】,否则受到1点伤害"))
|
||||
|
||||
# 桃园结义 (1张)
|
||||
self.cards.append(Card("桃园结义", CardType.TRICK, CardSuit.HEART, 1, "所有角色回复1点体力"))
|
||||
|
||||
# 五谷丰登 (2张)
|
||||
for num in [3, 4]:
|
||||
self.cards.append(Card("五谷丰登", CardType.TRICK, CardSuit.HEART, num, "所有角色依次获得一张牌"))
|
||||
|
||||
# 装备牌(简化版,只添加几种)
|
||||
# 诸葛连弩
|
||||
self.cards.append(Card("诸葛连弩", CardType.EQUIPMENT, CardSuit.CLUB, 1, "武器,攻击范围1,出牌阶段可以使用任意张【杀】"))
|
||||
self.cards.append(Card("诸葛连弩", CardType.EQUIPMENT, CardSuit.DIAMOND, 1, "武器,攻击范围1,出牌阶段可以使用任意张【杀】"))
|
||||
|
||||
# 青釭剑
|
||||
self.cards.append(Card("青釭剑", CardType.EQUIPMENT, CardSuit.SPADE, 6, "武器,攻击范围2,无视目标防具"))
|
||||
|
||||
# 八卦阵
|
||||
self.cards.append(Card("八卦阵", CardType.EQUIPMENT, CardSuit.SPADE, 2, "防具,判定为红色时视为使用了【闪】"))
|
||||
self.cards.append(Card("八卦阵", CardType.EQUIPMENT, CardSuit.CLUB, 2, "防具,判定为红色时视为使用了【闪】"))
|
||||
|
||||
# 的卢
|
||||
self.cards.append(Card("的卢", CardType.EQUIPMENT, CardSuit.CLUB, 5, "+1马,其他角色计算与你的距离+1"))
|
||||
|
||||
# 赤兔
|
||||
self.cards.append(Card("赤兔", CardType.EQUIPMENT, CardSuit.HEART, 5, "-1马,你计算与其他角色的距离-1"))
|
||||
|
||||
# 洗牌
|
||||
self.shuffle()
|
||||
|
||||
def shuffle(self):
|
||||
"""洗牌"""
|
||||
random.shuffle(self.cards)
|
||||
logger.info(f"牌堆已洗牌,共 {len(self.cards)} 张牌")
|
||||
|
||||
def draw(self, count: int = 1) -> List[Card]:
|
||||
"""摸牌
|
||||
|
||||
Args:
|
||||
count: 摸牌数量
|
||||
|
||||
Returns:
|
||||
摸到的牌列表
|
||||
"""
|
||||
if len(self.cards) < count:
|
||||
# 牌不够,将弃牌堆洗入牌堆
|
||||
self.cards.extend(self.discard_pile)
|
||||
self.discard_pile.clear()
|
||||
self.shuffle()
|
||||
|
||||
drawn = self.cards[:count]
|
||||
self.cards = self.cards[count:]
|
||||
return drawn
|
||||
|
||||
def discard(self, cards: List[Card]):
|
||||
"""弃牌"""
|
||||
self.discard_pile.extend(cards)
|
||||
|
||||
|
||||
class GeneralPool:
|
||||
"""武将池"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化武将池"""
|
||||
self.generals: List[General] = []
|
||||
self._init_standard_generals()
|
||||
|
||||
def _init_standard_generals(self):
|
||||
"""初始化标准武将(简化版)"""
|
||||
# 刘备
|
||||
self.generals.append(General(
|
||||
name="刘备",
|
||||
max_hp=4,
|
||||
kingdom="蜀",
|
||||
skills=[
|
||||
Skill("仁德", "出牌阶段,你可以将任意张手牌交给其他角色,若你给出的牌达到两张或更多,你回复1点体力", "主动"),
|
||||
Skill("激将", "主公技,当你需要使用或打出【杀】时,你可以令其他蜀势力角色打出一张【杀】(视为由你使用或打出)", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 关羽
|
||||
self.generals.append(General(
|
||||
name="关羽",
|
||||
max_hp=4,
|
||||
kingdom="蜀",
|
||||
skills=[
|
||||
Skill("武圣", "你可以将一张红色牌当【杀】使用或打出", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 张飞
|
||||
self.generals.append(General(
|
||||
name="张飞",
|
||||
max_hp=4,
|
||||
kingdom="蜀",
|
||||
skills=[
|
||||
Skill("咆哮", "锁定技,出牌阶段,你使用【杀】无次数限制", "锁定")
|
||||
]
|
||||
))
|
||||
|
||||
# 诸葛亮
|
||||
self.generals.append(General(
|
||||
name="诸葛亮",
|
||||
max_hp=3,
|
||||
kingdom="蜀",
|
||||
skills=[
|
||||
Skill("观星", "准备阶段,你可以观看牌堆顶的X张牌(X为存活角色数且至多为5),将任意数量的牌置于牌堆顶,其余的牌置于牌堆底", "主动"),
|
||||
Skill("空城", "锁定技,当你没有手牌时,你不能成为【杀】或【决斗】的目标", "锁定")
|
||||
]
|
||||
))
|
||||
|
||||
# 赵云
|
||||
self.generals.append(General(
|
||||
name="赵云",
|
||||
max_hp=4,
|
||||
kingdom="蜀",
|
||||
skills=[
|
||||
Skill("龙胆", "你可以将【杀】当【闪】、【闪】当【杀】使用或打出", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 曹操
|
||||
self.generals.append(General(
|
||||
name="曹操",
|
||||
max_hp=4,
|
||||
kingdom="魏",
|
||||
skills=[
|
||||
Skill("奸雄", "当你受到伤害后,你可以获得对你造成伤害的牌", "主动"),
|
||||
Skill("护驾", "主公技,当你需要使用或打出【闪】时,你可以令其他魏势力角色打出一张【闪】(视为由你使用或打出)", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 司马懿
|
||||
self.generals.append(General(
|
||||
name="司马懿",
|
||||
max_hp=3,
|
||||
kingdom="魏",
|
||||
skills=[
|
||||
Skill("反馈", "当你受到1点伤害后,你可以获得伤害来源的一张牌", "主动"),
|
||||
Skill("鬼才", "在任意角色的判定牌生效前,你可以打出一张手牌代替之", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 夏侯惇
|
||||
self.generals.append(General(
|
||||
name="夏侯惇",
|
||||
max_hp=4,
|
||||
kingdom="魏",
|
||||
skills=[
|
||||
Skill("刚烈", "当你受到伤害后,你可以进行判定:若结果不为♥,则伤害来源选择一项:弃置两张手牌,或受到你造成的1点伤害", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 甄姬
|
||||
self.generals.append(General(
|
||||
name="甄姬",
|
||||
max_hp=3,
|
||||
kingdom="魏",
|
||||
skills=[
|
||||
Skill("洛神", "准备阶段,你可以进行判定:当黑色判定牌生效后,你获得之。若结果为黑色,你可以重复此流程", "主动"),
|
||||
Skill("倾国", "你可以将一张黑色手牌当【闪】使用或打出", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 孙权
|
||||
self.generals.append(General(
|
||||
name="孙权",
|
||||
max_hp=4,
|
||||
kingdom="吴",
|
||||
skills=[
|
||||
Skill("制衡", "出牌阶段限一次,你可以弃置任意张牌,然后摸等量的牌", "主动"),
|
||||
Skill("救援", "主公技,锁定技,其他吴势力角色对你使用【桃】时,该角色摸一张牌", "锁定")
|
||||
]
|
||||
))
|
||||
|
||||
# 周瑜
|
||||
self.generals.append(General(
|
||||
name="周瑜",
|
||||
max_hp=3,
|
||||
kingdom="吴",
|
||||
skills=[
|
||||
Skill("英姿", "摸牌阶段,你可以额外摸一张牌", "主动"),
|
||||
Skill("反间", "出牌阶段限一次,你可以令一名其他角色选择一种花色,然后该角色获得你的一张手牌并展示之,若此牌与所选花色不同,你对其造成1点伤害", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 吕蒙
|
||||
self.generals.append(General(
|
||||
name="吕蒙",
|
||||
max_hp=4,
|
||||
kingdom="吴",
|
||||
skills=[
|
||||
Skill("克己", "若你于出牌阶段内没有使用或打出过【杀】,你可以跳过此回合的弃牌阶段", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 黄盖
|
||||
self.generals.append(General(
|
||||
name="黄盖",
|
||||
max_hp=4,
|
||||
kingdom="吴",
|
||||
skills=[
|
||||
Skill("苦肉", "出牌阶段,你可以失去1点体力,然后摸两张牌", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 吕布
|
||||
self.generals.append(General(
|
||||
name="吕布",
|
||||
max_hp=4,
|
||||
kingdom="群",
|
||||
skills=[
|
||||
Skill("无双", "锁定技,当你使用【杀】指定一个目标后,该角色需依次使用两张【闪】才能抵消此【杀】;当你使用【决斗】指定一个目标后,该角色每次响应此【决斗】需依次打出两张【杀】", "锁定")
|
||||
]
|
||||
))
|
||||
|
||||
# 貂蝉
|
||||
self.generals.append(General(
|
||||
name="貂蝉",
|
||||
max_hp=3,
|
||||
kingdom="群",
|
||||
skills=[
|
||||
Skill("离间", "出牌阶段限一次,你可以弃置一张牌并选择两名男性角色,后选择的角色视为对先选择的角色使用一张【决斗】(此【决斗】不能被【无懈可击】响应)", "主动"),
|
||||
Skill("闭月", "结束阶段,你可以摸一张牌", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
# 华佗
|
||||
self.generals.append(General(
|
||||
name="华佗",
|
||||
max_hp=3,
|
||||
kingdom="群",
|
||||
skills=[
|
||||
Skill("急救", "你的回合外,你可以将一张红色牌当【桃】使用", "主动"),
|
||||
Skill("青囊", "出牌阶段限一次,你可以弃置一张手牌并令一名角色回复1点体力", "主动")
|
||||
]
|
||||
))
|
||||
|
||||
def get_random_generals(self, count: int, exclude: List[str] = None) -> List[General]:
|
||||
"""随机获取武将
|
||||
|
||||
Args:
|
||||
count: 数量
|
||||
exclude: 排除的武将名称列表
|
||||
|
||||
Returns:
|
||||
武将列表
|
||||
"""
|
||||
available = [g for g in self.generals if not exclude or g.name not in exclude]
|
||||
if len(available) < count:
|
||||
return available
|
||||
return random.sample(available, count)
|
||||
|
||||
def get_general_by_name(self, name: str) -> Optional[General]:
|
||||
"""根据名称获取武将"""
|
||||
for general in self.generals:
|
||||
if general.name == name:
|
||||
return general
|
||||
return None
|
||||
|
||||
337
games/sgs_game.py
Normal file
337
games/sgs_game.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""三国杀游戏状态管理"""
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
import random
|
||||
import time
|
||||
|
||||
from games.sgs_core import (
|
||||
Player, Card, CardDeck, GeneralPool, Role, Phase,
|
||||
CardType, CardSuit, General
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GameState:
|
||||
"""游戏状态"""
|
||||
game_id: str # 游戏ID
|
||||
chat_id: int # 会话ID
|
||||
host_id: int # 房主ID
|
||||
players: List[Player] = field(default_factory=list) # 玩家列表
|
||||
deck: CardDeck = field(default_factory=CardDeck) # 牌堆
|
||||
current_player_idx: int = 0 # 当前玩家索引
|
||||
current_phase: Phase = Phase.PREPARE # 当前阶段
|
||||
round_number: int = 1 # 回合数
|
||||
is_started: bool = False # 是否已开始
|
||||
is_finished: bool = False # 是否已结束
|
||||
winner_role: Optional[Role] = None # 获胜身份
|
||||
created_at: int = field(default_factory=lambda: int(time.time()))
|
||||
|
||||
# 游戏配置
|
||||
max_players: int = 8 # 最大玩家数
|
||||
min_players: int = 2 # 最小玩家数(简化版,标准是5人)
|
||||
|
||||
# 临时状态
|
||||
pending_action: Optional[Dict] = None # 等待的操作
|
||||
action_queue: List[Dict] = field(default_factory=list) # 操作队列
|
||||
|
||||
@property
|
||||
def current_player(self) -> Optional[Player]:
|
||||
"""获取当前玩家"""
|
||||
if 0 <= self.current_player_idx < len(self.players):
|
||||
return self.players[self.current_player_idx]
|
||||
return None
|
||||
|
||||
@property
|
||||
def alive_players(self) -> List[Player]:
|
||||
"""获取存活玩家"""
|
||||
return [p for p in self.players if p.is_alive]
|
||||
|
||||
@property
|
||||
def lord_player(self) -> Optional[Player]:
|
||||
"""获取主公"""
|
||||
for player in self.players:
|
||||
if player.role == Role.LORD:
|
||||
return player
|
||||
return None
|
||||
|
||||
def get_player_by_id(self, user_id: int) -> Optional[Player]:
|
||||
"""根据用户ID获取玩家"""
|
||||
for player in self.players:
|
||||
if player.user_id == user_id:
|
||||
return player
|
||||
return None
|
||||
|
||||
def get_next_alive_player(self, from_idx: int = None) -> Optional[Player]:
|
||||
"""获取下一个存活玩家"""
|
||||
if from_idx is None:
|
||||
from_idx = self.current_player_idx
|
||||
|
||||
idx = (from_idx + 1) % len(self.players)
|
||||
checked = 0
|
||||
|
||||
while checked < len(self.players):
|
||||
if self.players[idx].is_alive:
|
||||
return self.players[idx]
|
||||
idx = (idx + 1) % len(self.players)
|
||||
checked += 1
|
||||
|
||||
return None
|
||||
|
||||
def add_player(self, user_id: int, username: str) -> bool:
|
||||
"""添加玩家
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
if self.is_started:
|
||||
return False
|
||||
|
||||
if len(self.players) >= self.max_players:
|
||||
return False
|
||||
|
||||
if self.get_player_by_id(user_id):
|
||||
return False
|
||||
|
||||
player = Player(user_id=user_id, username=username)
|
||||
self.players.append(player)
|
||||
logger.info(f"玩家 {username} 加入游戏")
|
||||
return True
|
||||
|
||||
def remove_player(self, user_id: int) -> bool:
|
||||
"""移除玩家(仅限游戏未开始时)"""
|
||||
if self.is_started:
|
||||
return False
|
||||
|
||||
player = self.get_player_by_id(user_id)
|
||||
if player:
|
||||
self.players.remove(player)
|
||||
logger.info(f"玩家 {player.username} 离开游戏")
|
||||
return True
|
||||
return False
|
||||
|
||||
def start_game(self) -> Tuple[bool, str]:
|
||||
"""开始游戏
|
||||
|
||||
Returns:
|
||||
(是否成功, 消息)
|
||||
"""
|
||||
if self.is_started:
|
||||
return False, "游戏已经开始"
|
||||
|
||||
if len(self.players) < self.min_players:
|
||||
return False, f"人数不足,至少需要 {self.min_players} 人"
|
||||
|
||||
if len(self.players) > self.max_players:
|
||||
return False, f"人数过多,最多 {self.max_players} 人"
|
||||
|
||||
# 分配身份
|
||||
self._assign_roles()
|
||||
|
||||
# 选择武将
|
||||
self._assign_generals()
|
||||
|
||||
# 初始化体力
|
||||
for player in self.players:
|
||||
if player.general:
|
||||
player.hp = player.general.max_hp
|
||||
# 主公额外+1体力上限
|
||||
if player.role == Role.LORD:
|
||||
player.hp += 1
|
||||
if player.general:
|
||||
player.general.max_hp += 1
|
||||
|
||||
# 发初始手牌
|
||||
self._deal_initial_cards()
|
||||
|
||||
# 确定起始玩家(主公先手)
|
||||
for idx, player in enumerate(self.players):
|
||||
if player.role == Role.LORD:
|
||||
self.current_player_idx = idx
|
||||
break
|
||||
|
||||
self.is_started = True
|
||||
self.current_phase = Phase.PREPARE
|
||||
|
||||
logger.info(f"游戏开始,共 {len(self.players)} 名玩家")
|
||||
return True, "游戏开始!"
|
||||
|
||||
def _assign_roles(self):
|
||||
"""分配身份"""
|
||||
player_count = len(self.players)
|
||||
|
||||
# 根据人数分配身份
|
||||
role_distribution = {
|
||||
2: [Role.LORD, Role.REBEL],
|
||||
3: [Role.LORD, Role.REBEL, Role.SPY],
|
||||
4: [Role.LORD, Role.LOYAL, Role.REBEL, Role.SPY],
|
||||
5: [Role.LORD, Role.LOYAL, Role.REBEL, Role.REBEL, Role.SPY],
|
||||
6: [Role.LORD, Role.LOYAL, Role.REBEL, Role.REBEL, Role.REBEL, Role.SPY],
|
||||
7: [Role.LORD, Role.LOYAL, Role.LOYAL, Role.REBEL, Role.REBEL, Role.REBEL, Role.SPY],
|
||||
8: [Role.LORD, Role.LOYAL, Role.LOYAL, Role.REBEL, Role.REBEL, Role.REBEL, Role.REBEL, Role.SPY]
|
||||
}
|
||||
|
||||
roles = role_distribution.get(player_count, [Role.LORD] + [Role.REBEL] * (player_count - 1))
|
||||
random.shuffle(roles)
|
||||
|
||||
for player, role in zip(self.players, roles):
|
||||
player.role = role
|
||||
logger.info(f"玩家 {player.username} 的身份是 {role.value}")
|
||||
|
||||
def _assign_generals(self):
|
||||
"""分配武将"""
|
||||
general_pool = GeneralPool()
|
||||
|
||||
# 主公先选
|
||||
lord = self.lord_player
|
||||
if lord:
|
||||
# 主公从3个武将中选择(这里简化为随机)
|
||||
lord_options = general_pool.get_random_generals(3)
|
||||
lord.general = lord_options[0] # 简化:直接选第一个
|
||||
logger.info(f"主公 {lord.username} 选择了武将 {lord.general.name}")
|
||||
|
||||
# 其他玩家随机分配
|
||||
used_generals = [lord.general.name] if lord and lord.general else []
|
||||
for player in self.players:
|
||||
if player.role != Role.LORD:
|
||||
generals = general_pool.get_random_generals(1, exclude=used_generals)
|
||||
if generals:
|
||||
player.general = generals[0]
|
||||
used_generals.append(player.general.name)
|
||||
logger.info(f"玩家 {player.username} 获得武将 {player.general.name}")
|
||||
|
||||
def _deal_initial_cards(self):
|
||||
"""发初始手牌"""
|
||||
for player in self.players:
|
||||
# 每人发4张手牌
|
||||
cards = self.deck.draw(4)
|
||||
player.hand_cards.extend(cards)
|
||||
logger.info(f"玩家 {player.username} 获得 {len(cards)} 张初始手牌")
|
||||
|
||||
def next_phase(self) -> Tuple[Phase, Optional[Player]]:
|
||||
"""进入下一阶段
|
||||
|
||||
Returns:
|
||||
(新阶段, 当前玩家)
|
||||
"""
|
||||
current = self.current_player
|
||||
if not current:
|
||||
return self.current_phase, None
|
||||
|
||||
# 阶段流转
|
||||
phase_order = [
|
||||
Phase.PREPARE,
|
||||
Phase.JUDGE,
|
||||
Phase.DRAW,
|
||||
Phase.PLAY,
|
||||
Phase.DISCARD,
|
||||
Phase.END
|
||||
]
|
||||
|
||||
current_idx = phase_order.index(self.current_phase)
|
||||
|
||||
if current_idx < len(phase_order) - 1:
|
||||
# 进入下一阶段
|
||||
self.current_phase = phase_order[current_idx + 1]
|
||||
else:
|
||||
# 回合结束,轮到下一个玩家
|
||||
next_player = self.get_next_alive_player()
|
||||
if next_player:
|
||||
for idx, p in enumerate(self.players):
|
||||
if p.user_id == next_player.user_id:
|
||||
self.current_player_idx = idx
|
||||
break
|
||||
self.current_phase = Phase.PREPARE
|
||||
self.round_number += 1
|
||||
|
||||
return self.current_phase, self.current_player
|
||||
|
||||
def check_game_over(self) -> Tuple[bool, Optional[Role]]:
|
||||
"""检查游戏是否结束
|
||||
|
||||
Returns:
|
||||
(是否结束, 获胜身份)
|
||||
"""
|
||||
lord = self.lord_player
|
||||
|
||||
# 主公死亡
|
||||
if not lord or not lord.is_alive:
|
||||
# 反贼胜利
|
||||
return True, Role.REBEL
|
||||
|
||||
# 检查是否所有反贼和内奸都死亡
|
||||
rebels_alive = any(p.is_alive and p.role == Role.REBEL for p in self.players)
|
||||
spies_alive = any(p.is_alive and p.role == Role.SPY for p in self.players)
|
||||
|
||||
if not rebels_alive and not spies_alive:
|
||||
# 主公和忠臣胜利
|
||||
return True, Role.LORD
|
||||
|
||||
return False, None
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"game_id": self.game_id,
|
||||
"chat_id": self.chat_id,
|
||||
"host_id": self.host_id,
|
||||
"players": [p.to_dict() for p in self.players],
|
||||
"current_player_idx": self.current_player_idx,
|
||||
"current_phase": self.current_phase.value,
|
||||
"round_number": self.round_number,
|
||||
"is_started": self.is_started,
|
||||
"is_finished": self.is_finished,
|
||||
"winner_role": self.winner_role.value if self.winner_role else None,
|
||||
"created_at": self.created_at
|
||||
}
|
||||
|
||||
|
||||
class GameManager:
|
||||
"""游戏管理器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化"""
|
||||
self.games: Dict[int, GameState] = {} # chat_id -> GameState
|
||||
|
||||
def create_game(self, chat_id: int, host_id: int, host_name: str) -> GameState:
|
||||
"""创建游戏"""
|
||||
game_id = f"sgs_{chat_id}_{int(time.time())}"
|
||||
game = GameState(
|
||||
game_id=game_id,
|
||||
chat_id=chat_id,
|
||||
host_id=host_id
|
||||
)
|
||||
game.add_player(host_id, host_name)
|
||||
self.games[chat_id] = game
|
||||
logger.info(f"创建游戏: {game_id}")
|
||||
return game
|
||||
|
||||
def get_game(self, chat_id: int) -> Optional[GameState]:
|
||||
"""获取游戏"""
|
||||
return self.games.get(chat_id)
|
||||
|
||||
def remove_game(self, chat_id: int):
|
||||
"""移除游戏"""
|
||||
if chat_id in self.games:
|
||||
del self.games[chat_id]
|
||||
logger.info(f"移除游戏: chat_id={chat_id}")
|
||||
|
||||
def has_active_game(self, chat_id: int) -> bool:
|
||||
"""是否有活跃游戏"""
|
||||
game = self.get_game(chat_id)
|
||||
return game is not None and not game.is_finished
|
||||
|
||||
|
||||
# 全局游戏管理器
|
||||
_game_manager: Optional[GameManager] = None
|
||||
|
||||
|
||||
def get_game_manager() -> GameManager:
|
||||
"""获取全局游戏管理器"""
|
||||
global _game_manager
|
||||
if _game_manager is None:
|
||||
_game_manager = GameManager()
|
||||
return _game_manager
|
||||
|
||||
1261
games/werewolf.py
1261
games/werewolf.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
"""Callback路由处理"""
|
||||
import logging
|
||||
import re
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
@@ -49,15 +48,6 @@ async def callback_receive(request: Request):
|
||||
game_type, command = parse_result
|
||||
logger.info(f"识别指令: game_type={game_type}, command={command}")
|
||||
|
||||
# 检查是否包含 @s 参数(私聊标志)
|
||||
use_private_url = False
|
||||
# 使用正则表达式匹配独立的 @s 参数(前后有空格或字符串边界)
|
||||
if re.search(r'\s+@s\s+|\s+@s$|^@s\s+|^@s$', command):
|
||||
use_private_url = True
|
||||
# 从命令中移除 @s 参数,保持其他参数不变
|
||||
command = re.sub(r'\s+@s(\s+|$)|^@s\s+', ' ', command).strip()
|
||||
logger.info(f"检测到 @s 参数,将优先使用个人URL发送反馈,清理后的命令: {command}")
|
||||
|
||||
# 检查限流
|
||||
rate_limiter = get_rate_limiter()
|
||||
if not rate_limiter.is_allowed():
|
||||
@@ -85,75 +75,21 @@ async def callback_receive(request: Request):
|
||||
|
||||
# 发送回复
|
||||
if response_text:
|
||||
# 如果使用了 @s 参数,优先发送到个人URL
|
||||
if use_private_url:
|
||||
db = get_db()
|
||||
user_webhook_url = db.get_user_webhook_url(callback_data.creator)
|
||||
|
||||
if user_webhook_url:
|
||||
# 有个人URL,发送到个人URL
|
||||
from utils.message import send_private_message
|
||||
# 判断消息类型
|
||||
if game_type == 'ai_chat':
|
||||
msg_type = 'markdown'
|
||||
elif response_text.startswith('#'):
|
||||
msg_type = 'markdown'
|
||||
else:
|
||||
msg_type = 'text'
|
||||
|
||||
success = await send_private_message(
|
||||
user_id=callback_data.creator,
|
||||
content=response_text,
|
||||
msg_type=msg_type
|
||||
)
|
||||
if not success:
|
||||
# 如果私聊发送失败,回退到主URL
|
||||
logger.warning(f"个人URL发送失败,回退到主URL: user_id={callback_data.creator}")
|
||||
sender = get_message_sender()
|
||||
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:
|
||||
if response_text.startswith('#'):
|
||||
await sender.send_markdown(response_text)
|
||||
else:
|
||||
await sender.send_text(response_text)
|
||||
# 成功发送到个人URL,不向主URL发送
|
||||
else:
|
||||
# 没有个人URL,回退到主URL
|
||||
logger.info(f"用户 {callback_data.creator} 没有注册个人URL,使用主URL发送")
|
||||
sender = get_message_sender()
|
||||
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:
|
||||
if response_text.startswith('#'):
|
||||
await sender.send_markdown(response_text)
|
||||
else:
|
||||
await sender.send_text(response_text)
|
||||
else:
|
||||
# 没有 @s 参数,正常发送到主URL
|
||||
sender = get_message_sender()
|
||||
sender = get_message_sender()
|
||||
|
||||
# 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)
|
||||
# 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:
|
||||
# 其他模块保持原有启发式:以 # 开头视为 Markdown,否则文本
|
||||
if response_text.startswith('#'):
|
||||
await sender.send_markdown(response_text)
|
||||
else:
|
||||
# 其他模块保持原有启发式:以 # 开头视为 Markdown,否则文本
|
||||
if response_text.startswith('#'):
|
||||
await sender.send_markdown(response_text)
|
||||
else:
|
||||
await sender.send_text(response_text)
|
||||
await sender.send_text(response_text)
|
||||
|
||||
return JSONResponse({"result": "ok"})
|
||||
|
||||
@@ -232,6 +168,12 @@ async def handle_command(game_type: str, command: str,
|
||||
from games.gomoku import GomokuGame
|
||||
game = GomokuGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 三国杀
|
||||
if game_type == 'sanguosha':
|
||||
from games.sanguosha import SanguoshaGame
|
||||
game = SanguoshaGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 积分系统
|
||||
if game_type == 'points':
|
||||
@@ -266,28 +208,12 @@ async def handle_command(game_type: str, command: str,
|
||||
return "用法:.say 你想让我说的话\n别名:.说 / .复述"
|
||||
return args
|
||||
|
||||
# 私聊功能
|
||||
if game_type == 'talk':
|
||||
return await handle_talk_command(command, chat_id, user_id)
|
||||
|
||||
# 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)
|
||||
|
||||
# 狼人杀系统
|
||||
if game_type == 'werewolf':
|
||||
from games.werewolf import WerewolfGame
|
||||
game = WerewolfGame()
|
||||
return await game.handle(command, chat_id, user_id)
|
||||
|
||||
# 未知游戏类型
|
||||
logger.warning(f"未知游戏类型: {game_type}")
|
||||
return "❌ 未知的游戏类型"
|
||||
@@ -301,7 +227,7 @@ async def handle_register_command(command: str, chat_id: int, user_id: int) -> s
|
||||
"""处理注册命令
|
||||
|
||||
Args:
|
||||
command: 完整指令 ".register name" 或 ".register url <url>"
|
||||
command: 完整指令 ".register name"
|
||||
chat_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
@@ -315,114 +241,21 @@ async def handle_register_command(command: str, chat_id: int, user_id: int) -> s
|
||||
|
||||
# 验证参数
|
||||
if not args:
|
||||
return "❌ 请提供要注册的内容!\n\n正确格式:\n`.register <名称>` - 注册用户名\n`.register url <URL>` - 注册webhook URL\n\n示例:\n`.register 张三`\n`.register url https://example.com/webhook?key=xxx`"
|
||||
return "❌ 请提供要注册的名称!\n\n正确格式:`.register <名称>`\n\n示例:\n`.register 张三`\n`.register 小明`"
|
||||
|
||||
# 检查是否为url子命令
|
||||
parts = args.split(maxsplit=1)
|
||||
if parts and parts[0].lower() == 'url':
|
||||
# 处理URL注册
|
||||
if len(parts) < 2:
|
||||
return "❌ 请提供webhook URL!\n\n正确格式:`.register url <URL>`\n\n示例:\n`.register url https://example.com/webhook?key=xxx`"
|
||||
|
||||
webhook_url = parts[1].strip()
|
||||
|
||||
# URL验证
|
||||
if not webhook_url.startswith(('http://', 'https://')):
|
||||
return "❌ URL格式无效!必须以 http:// 或 https:// 开头。"
|
||||
|
||||
# 设置URL
|
||||
db = get_db()
|
||||
success = db.set_user_webhook_url(user_id, webhook_url)
|
||||
|
||||
if success:
|
||||
return f"✅ Webhook URL注册成功!\n\n**您的个人URL**:{webhook_url}\n\n私聊消息将发送到此URL。"
|
||||
else:
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
if len(args) > 20:
|
||||
return "❌ 名称过长!最多支持20个字符。"
|
||||
|
||||
# 更新用户名称
|
||||
db = get_db()
|
||||
success = db.update_user_name(user_id, args)
|
||||
|
||||
if success:
|
||||
return f"✅ 注册成功!\n\n**您的名称**:{args}\n\n之后您可以使用这个名称参与各种游戏和功能。"
|
||||
else:
|
||||
# 原有的名称注册逻辑
|
||||
if len(args) > 20:
|
||||
return "❌ 名称过长!最多支持20个字符。"
|
||||
|
||||
# 更新用户名称
|
||||
db = get_db()
|
||||
success = db.update_user_name(user_id, args)
|
||||
|
||||
if success:
|
||||
return f"✅ 注册成功!\n\n**您的名称**:{args}\n\n之后您可以使用这个名称参与各种游戏和功能。"
|
||||
else:
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
return "❌ 注册失败!请稍后重试。"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理注册指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
|
||||
async def handle_talk_command(command: str, chat_id: int, user_id: int) -> str:
|
||||
"""处理私聊命令
|
||||
|
||||
Args:
|
||||
command: 完整指令 ".talk <username> <content>"
|
||||
chat_id: 会话ID
|
||||
user_id: 发送者用户ID
|
||||
|
||||
Returns:
|
||||
处理结果消息
|
||||
"""
|
||||
try:
|
||||
# 提取参数
|
||||
_, args = CommandParser.extract_command_args(command)
|
||||
args = args.strip()
|
||||
|
||||
# 验证参数
|
||||
if not args:
|
||||
return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`\n`.talk 李四 这是一条私聊消息`"
|
||||
|
||||
# 解析username和content(第一个单词是username,剩余部分是content)
|
||||
parts = args.split(maxsplit=1)
|
||||
if len(parts) < 2:
|
||||
return "❌ 请提供用户名和消息内容!\n\n正确格式:`.talk <用户名> <消息内容>`\n\n示例:\n`.talk 张三 你好,想和你聊聊`"
|
||||
|
||||
target_username = parts[0].strip()
|
||||
content = parts[1].strip()
|
||||
|
||||
if not target_username:
|
||||
return "❌ 用户名不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`"
|
||||
if not content:
|
||||
return "❌ 消息内容不能为空!\n\n正确格式:`.talk <用户名> <消息内容>`"
|
||||
|
||||
# 通过用户名查找目标用户
|
||||
db = get_db()
|
||||
target_user = db.get_user_by_name(target_username)
|
||||
|
||||
if not target_user:
|
||||
return f"❌ 找不到用户名为「{target_username}」的用户!\n\n提示:目标用户需要使用 `.register <名称>` 注册用户名。"
|
||||
|
||||
target_user_id = target_user['user_id']
|
||||
|
||||
# 检查目标用户是否有注册名称(应该有,因为是通过名称找到的)
|
||||
if not target_user.get('username'):
|
||||
return f"❌ 用户「{target_username}」尚未注册用户名!"
|
||||
|
||||
# 检查目标用户是否有个人webhook URL
|
||||
if not db.has_webhook_url(target_user_id):
|
||||
return f"❌ 用户「{target_username}」尚未注册个人webhook URL!\n\n提示:目标用户需要使用 `.register url <URL>` 注册个人URL后才能接收私聊消息。"
|
||||
|
||||
# 发送私聊消息
|
||||
from utils.message import send_private_message
|
||||
success = await send_private_message(
|
||||
user_id=target_user_id,
|
||||
content=content,
|
||||
msg_type='text'
|
||||
)
|
||||
|
||||
if success:
|
||||
# 私聊消息发送成功,不向主URL发送提示消息
|
||||
return ""
|
||||
else:
|
||||
# 发送失败时仍然需要提示用户
|
||||
return f"❌ 发送私聊消息失败,请稍后重试。"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理私聊指令错误: {e}", exc_info=True)
|
||||
return f"❌ 处理指令出错: {str(e)}"
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
"""私聊相关API路由"""
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from core.database import get_db
|
||||
from core.models import PrivateMessageRequest, CheckBatchRequest, CheckBatchResponse
|
||||
from utils.message import send_private_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/private/send")
|
||||
async def send_private(request: PrivateMessageRequest):
|
||||
"""发送私聊消息
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_id": 123456,
|
||||
"content": "消息内容",
|
||||
"msg_type": "text" // 可选,默认为"text"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 验证msg_type
|
||||
if request.msg_type not in ['text', 'markdown']:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="msg_type必须是'text'或'markdown'"
|
||||
)
|
||||
|
||||
# 调用send_private_message
|
||||
success = await send_private_message(
|
||||
user_id=request.user_id,
|
||||
content=request.content,
|
||||
msg_type=request.msg_type
|
||||
)
|
||||
|
||||
if not success:
|
||||
# 检查用户是否有个人URL
|
||||
db = get_db()
|
||||
has_url = db.has_webhook_url(request.user_id)
|
||||
if not has_url:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"用户 {request.user_id} 没有注册个人webhook URL"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="消息发送失败,请稍后重试"
|
||||
)
|
||||
|
||||
return JSONResponse({
|
||||
"success": True,
|
||||
"message": "消息发送成功"
|
||||
})
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"发送私聊消息API错误: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"服务器内部错误: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/private/check/{user_id}")
|
||||
async def check_user_webhook(user_id: int):
|
||||
"""检查用户是否有个人webhook URL"""
|
||||
try:
|
||||
db = get_db()
|
||||
has_webhook_url = db.has_webhook_url(user_id)
|
||||
|
||||
return JSONResponse({
|
||||
"user_id": user_id,
|
||||
"has_webhook_url": has_webhook_url
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"检查用户webhook URL错误: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"服务器内部错误: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/private/check-batch")
|
||||
async def check_users_webhook_batch(request: CheckBatchRequest):
|
||||
"""批量检查用户是否有个人webhook URL
|
||||
|
||||
请求体:
|
||||
{
|
||||
"user_ids": [123456, 789012, ...]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
db = get_db()
|
||||
results = db.check_users_webhook_urls(request.user_ids)
|
||||
|
||||
return CheckBatchResponse(results=results)
|
||||
except Exception as e:
|
||||
logger.error(f"批量检查用户webhook URL错误: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"服务器内部错误: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LiuBai网站后台启动脚本
|
||||
|
||||
PID_FILE="./liubai_web.pid"
|
||||
LOG_FILE="./liubai_web.log"
|
||||
|
||||
# 检查是否已经在运行
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "服务已经在运行 (PID: $PID)"
|
||||
exit 1
|
||||
else
|
||||
echo "删除旧的PID文件..."
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "正在启动LiuBaiBlog网站服务器..."
|
||||
|
||||
# 使用nohup在后台运行
|
||||
nohup venv/bin/python3 -m jurigged -v app.py > "$LOG_FILE" 2>&1 &
|
||||
PID=$!
|
||||
|
||||
# 保存PID
|
||||
echo $PID > "$PID_FILE"
|
||||
|
||||
echo "服务器已在后台启动!"
|
||||
echo "PID: $PID"
|
||||
echo "日志文件: $LOG_FILE"
|
||||
echo "要停止服务,请运行: ./stop_background.sh"
|
||||
|
||||
# 等待一下确保服务器启动
|
||||
sleep 2
|
||||
|
||||
# 检查进程是否还在运行
|
||||
if kill -0 "$PID" 2>/dev/null; then
|
||||
echo "服务器启动成功!"
|
||||
else
|
||||
echo "服务器启动失败,请检查日志文件: $LOG_FILE"
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LiuBai网站后台停止脚本
|
||||
|
||||
PID_FILE="./liubai_web.pid"
|
||||
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
echo "服务未运行或PID文件不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
echo "正在停止服务器 (PID: $PID)..."
|
||||
|
||||
# 尝试优雅地停止进程
|
||||
if kill -TERM "$PID" 2>/dev/null; then
|
||||
echo "发送停止信号..."
|
||||
|
||||
# 等待进程结束
|
||||
for i in {1..10}; do
|
||||
if ! kill -0 "$PID" 2>/dev/null; then
|
||||
echo "服务器已停止"
|
||||
rm -f "$PID_FILE"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 如果进程还在运行,强制杀死
|
||||
echo "强制停止进程..."
|
||||
kill -KILL "$PID" 2>/dev/null
|
||||
rm -f "$PID_FILE"
|
||||
echo "服务器已强制停止"
|
||||
else
|
||||
echo "进程不存在或已停止"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
@@ -135,40 +135,3 @@ def get_message_sender() -> MessageSender:
|
||||
_sender_instance.webhook_url = GetWebhookURL()
|
||||
return _sender_instance
|
||||
|
||||
|
||||
async def send_private_message(user_id: int, content: str, msg_type: str = 'text') -> bool:
|
||||
"""发送私聊消息到用户个人webhook URL
|
||||
|
||||
Args:
|
||||
user_id: 目标用户ID
|
||||
content: 消息内容
|
||||
msg_type: 消息类型 ('text' 或 'markdown')
|
||||
|
||||
Returns:
|
||||
是否发送成功,如果用户没有个人URL则返回False
|
||||
"""
|
||||
from core.database import get_db
|
||||
|
||||
# 从数据库获取用户webhook URL
|
||||
db = get_db()
|
||||
webhook_url = db.get_user_webhook_url(user_id)
|
||||
|
||||
if not webhook_url:
|
||||
logger.warning(f"用户 {user_id} 没有注册个人webhook URL,无法发送私聊消息")
|
||||
return False
|
||||
|
||||
# 创建MessageSender实例(使用用户的个人URL)
|
||||
sender = MessageSender(webhook_url=webhook_url)
|
||||
|
||||
try:
|
||||
# 根据msg_type调用相应方法
|
||||
if msg_type == 'markdown':
|
||||
return await sender.send_markdown(content)
|
||||
else:
|
||||
return await sender.send_text(content)
|
||||
except Exception as e:
|
||||
logger.error(f"发送私聊消息失败: user_id={user_id}, error={e}", exc_info=True)
|
||||
return False
|
||||
finally:
|
||||
# 关闭HTTP客户端
|
||||
await sender.close()
|
||||
|
||||
@@ -43,6 +43,10 @@ class CommandParser:
|
||||
'.gomoku': 'gomoku',
|
||||
'.五子棋': 'gomoku',
|
||||
'.gobang': 'gomoku',
|
||||
|
||||
# 三国杀
|
||||
'.sgs': 'sanguosha',
|
||||
'.三国杀': 'sanguosha',
|
||||
|
||||
# 积分系统
|
||||
'.points': 'points',
|
||||
@@ -73,10 +77,6 @@ class CommandParser:
|
||||
'.说': 'say',
|
||||
'.复述': 'say',
|
||||
|
||||
# 私聊
|
||||
'.talk': 'talk',
|
||||
'.私聊': 'talk',
|
||||
|
||||
# 帮助
|
||||
'.help': 'help',
|
||||
'.帮助': 'help',
|
||||
@@ -84,14 +84,6 @@ class CommandParser:
|
||||
# 统计
|
||||
'.stats': 'stats',
|
||||
'.统计': 'stats',
|
||||
|
||||
# 赌场系统
|
||||
'.赌场': 'casino',
|
||||
'.casino': 'casino',
|
||||
|
||||
# 狼人杀系统
|
||||
'.werewolf': 'werewolf',
|
||||
'.狼人杀': 'werewolf',
|
||||
}
|
||||
|
||||
# 机器人名称模式(用于从@消息中提取)
|
||||
@@ -117,9 +109,9 @@ class CommandParser:
|
||||
|
||||
# 拦截全角空格与全角标点(不允许)
|
||||
# 范围包含:全角空格\u3000、全角标点\uFF01-\uFF60、兼容区\uFFE0-\uFFEE
|
||||
# if re.search(r"[\u3000\uFF01-\uFF60\uFFE0-\uFFEE]", content):
|
||||
# logger.debug(f"包含全角字符,忽略: {content}")
|
||||
# return None
|
||||
if re.search(r"[\u3000\uFF01-\uFF60\uFFE0-\uFFEE]", content):
|
||||
logger.debug(f"包含全角字符,忽略: {content}")
|
||||
return None
|
||||
|
||||
# 大小写不敏感匹配(仅用于匹配,不改变返回的原始内容)
|
||||
content_lower = content.lower()
|
||||
|
||||
Reference in New Issue
Block a user