5 Commits

Author SHA1 Message Date
借我清欢与鹤梦
3679d60e0c fix:jion未注册加入问题 2025-10-31 10:25:38 +08:00
借我清欢与鹤梦
96513c6e60 fix:语法 2025-10-30 16:49:46 +08:00
借我清欢与鹤梦
9625fa7808 fix:占位符问题以及缩进问题 2025-10-30 16:33:25 +08:00
借我清欢与鹤梦
131fabeec2 配置文件 2025-10-30 16:24:35 +08:00
借我清欢与鹤梦
86f87b440e 新增三国杀系统 2025-10-30 16:19:02 +08:00
23 changed files with 1503 additions and 2986 deletions

8
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View 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
View 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
View 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
View 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>

View File

@@ -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` 回收奖励,不允许炼金(修复后:确保奖励不会丢失)
- 如果冒险已完成:自动清理状态,允许炼金
- 状态异常:自动清理,允许继续操作
# 最终审查

View File

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

View File

@@ -54,37 +54,6 @@ class Database:
raise
return self._conn
def _column_exists(self, table_name: str, column_name: str) -> bool:
"""检查表中列是否存在
Args:
table_name: 表名
column_name: 列名
Returns:
是否存在
"""
cursor = self.conn.cursor()
cursor.execute(f"PRAGMA table_info({table_name})")
columns = [row[1] for row in cursor.fetchall()]
return column_name in columns
def _add_column_if_not_exists(self, table_name: str, column_name: str, column_def: str):
"""安全地添加列(如果不存在)
Args:
table_name: 表名
column_name: 列名
column_def: 列定义(如 "INTEGER""TEXT DEFAULT ''"
"""
if not self._column_exists(table_name, column_name):
try:
cursor = self.conn.cursor()
cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_def}")
logger.info(f"为表 {table_name} 添加列 {column_name}")
except Exception as e:
logger.warning(f"添加列失败: {e}")
def init_tables(self):
"""初始化数据库表"""
cursor = self.conn.cursor()
@@ -160,167 +129,6 @@ class Database:
)
""")
# 赌场下注记录表
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)
)
""")
# 赌场游戏会话表
# 注意移除了UNIQUE(chat_id, game_type, status)约束
# 因为status='closed'时需要允许多条历史记录
# 单场限制通过应用层逻辑get_any_active_casino_session保证
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约束需要重建表
# 检查是否已有旧表通过检查是否有UNIQUE约束的索引
try:
# 尝试查询表结构检查是否有UNIQUE约束相关的索引
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='casino_sessions'")
old_sql = cursor.fetchone()
if old_sql and 'UNIQUE(chat_id, game_type, status)' in old_sql[0]:
# 需要重建表以移除UNIQUE约束
logger.info("检测到旧版本的casino_sessions表需要重建以移除UNIQUE约束")
# 禁用外键检查SQLite默认可能未启用但为了安全
cursor.execute("PRAGMA foreign_keys=OFF")
# 创建临时表
cursor.execute("""
CREATE TABLE IF NOT EXISTS casino_sessions_new (
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,
current_phase TEXT DEFAULT 'betting',
blackjack_multiplier REAL DEFAULT 1.5
)
""")
# 复制所有数据(包括历史记录)
# 检查旧表是否有新字段
old_columns = [row[1] for row in cursor.execute("PRAGMA table_info(casino_sessions)").fetchall()]
has_current_phase = 'current_phase' in old_columns
has_blackjack_multiplier = 'blackjack_multiplier' in old_columns
if has_current_phase and has_blackjack_multiplier:
# 旧表已有新字段,直接复制所有列
cursor.execute("""
INSERT INTO casino_sessions_new
SELECT id, chat_id, game_type, banker_id, min_bet, max_bet,
multiplier, house_fee, status, created_at, settled_at,
current_phase, blackjack_multiplier
FROM casino_sessions
""")
elif has_current_phase:
# 只有current_phase字段
cursor.execute("""
INSERT INTO casino_sessions_new
SELECT id, chat_id, game_type, banker_id, min_bet, max_bet,
multiplier, house_fee, status, created_at, settled_at,
current_phase, 1.5 as blackjack_multiplier
FROM casino_sessions
""")
elif has_blackjack_multiplier:
# 只有blackjack_multiplier字段
cursor.execute("""
INSERT INTO casino_sessions_new
SELECT id, chat_id, game_type, banker_id, min_bet, max_bet,
multiplier, house_fee, status, created_at, settled_at,
'betting' as current_phase, blackjack_multiplier
FROM casino_sessions
""")
else:
# 都没有新字段,使用默认值
cursor.execute("""
INSERT INTO casino_sessions_new
SELECT id, chat_id, game_type, banker_id, min_bet, max_bet,
multiplier, house_fee, status, created_at, settled_at,
'betting' as current_phase, 1.5 as blackjack_multiplier
FROM casino_sessions
""")
# 删除旧表
cursor.execute("DROP TABLE casino_sessions")
# 重命名新表
cursor.execute("ALTER TABLE casino_sessions_new RENAME TO casino_sessions")
# 重新启用外键检查
cursor.execute("PRAGMA foreign_keys=ON")
logger.info("成功重建casino_sessions表移除UNIQUE约束")
except Exception as e:
# 如果迁移失败,记录日志但不影响正常运行
logger.warning(f"迁移casino_sessions表时出现错误可能表结构已更新: {e}")
# 21点手牌表
cursor.execute("""
CREATE TABLE IF NOT EXISTS casino_blackjack_hands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
hand_data TEXT NOT NULL,
hand_status TEXT DEFAULT 'playing',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES casino_sessions(id)
)
""")
# 创建索引
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)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_casino_blackjack_hands
ON casino_blackjack_hands(session_id, user_id)
""")
# 兼容性检查为casino_sessions表添加新字段
self._add_column_if_not_exists('casino_sessions', 'current_phase', "TEXT DEFAULT 'betting'")
self._add_column_if_not_exists('casino_sessions', 'blackjack_multiplier', 'REAL DEFAULT 1.5')
# 兼容性检查为casino_bets表添加新字段轮盘和21点专用
self._add_column_if_not_exists('casino_bets', 'bet_category', "TEXT")
self._add_column_if_not_exists('casino_bets', 'bet_number', 'INTEGER')
self._add_column_if_not_exists('casino_bets', 'bet_value', "TEXT")
self._add_column_if_not_exists('casino_bets', 'hand_status', "TEXT")
logger.info("数据库表初始化完成")
# ===== 用户相关操作 =====
@@ -412,23 +220,6 @@ class Database:
logger.error(f"更新用户名失败: user_id={user_id}, username={username}, error={e}", exc_info=True)
return False
def get_user_display_name(self, user_id: int) -> str:
"""获取用户显示名称
如果用户已注册username不为None返回用户名否则返回"用户{user_id}"
Args:
user_id: 用户ID
Returns:
用户显示名称(用户名或"用户{user_id}"
"""
user_dict = self.get_or_create_user(user_id)
username = user_dict.get('username')
if username:
return username
else:
return f"用户{user_id}"
# ===== 游戏状态相关操作 =====
def get_game_state(self, chat_id: int, user_id: int, game_type: str) -> Optional[Dict]:
@@ -796,730 +587,6 @@ class Database:
rows = cursor.fetchall()
return [dict(row) for row in rows]
# ===== 赌场相关操作 =====
def get_any_active_casino_session(self, chat_id: int) -> Optional[Dict]:
"""获取任意活跃的游戏会话(用于单场限制检查)
Args:
chat_id: 会话ID
Returns:
会话信息字典或None
"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT * FROM casino_sessions
WHERE chat_id = ? AND status = 'open'
ORDER BY id DESC LIMIT 1
""", (chat_id,))
row = cursor.fetchone()
if row:
return dict(row)
return None
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, current_phase: str = 'betting',
blackjack_multiplier: float = 1.5) -> int:
"""创建新的赌场游戏会话
Args:
chat_id: 会话ID
game_type: 游戏类型
banker_id: 庄家ID
min_bet: 最小下注金额
max_bet: 最大下注金额
multiplier: 赔率
house_fee: 抽水率
current_phase: 当前阶段(默认'betting'
blackjack_multiplier: 21点黑杰克倍数默认1.5
Returns:
session_id
"""
cursor = self.conn.cursor()
current_time = int(time.time())
# 检查是否已有活跃的会话单场限制同一chat_id只能有一个活跃游戏
existing = self.get_any_active_casino_session(chat_id)
if existing:
# 如果已有活跃游戏返回其ID保持向后兼容但实际应该在应用层阻止
return existing['id']
# 检查是否已有相同game_type的活跃会话向后兼容
cursor.execute("""
SELECT id FROM casino_sessions
WHERE chat_id = ? AND game_type = ? AND status = 'open'
""", (chat_id, game_type))
existing_same_type = cursor.fetchone()
if existing_same_type:
return existing_same_type['id']
cursor.execute("""
INSERT INTO casino_sessions
(chat_id, game_type, banker_id, min_bet, max_bet, multiplier, house_fee,
status, created_at, current_phase, blackjack_multiplier)
VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, ?)
""", (chat_id, game_type, banker_id, min_bet, max_bet, multiplier, house_fee,
current_time, current_phase, blackjack_multiplier))
return cursor.lastrowid
def get_active_casino_session(self, chat_id: int, game_type: str) -> Optional[Dict]:
"""获取活跃的游戏会话
Args:
chat_id: 会话ID
game_type: 游戏类型
Returns:
会话信息字典或None
"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT * FROM casino_sessions
WHERE chat_id = ? AND game_type = ? AND status = 'open'
ORDER BY id DESC LIMIT 1
""", (chat_id, game_type))
row = cursor.fetchone()
if row:
return dict(row)
return None
def create_casino_bet(self, chat_id: int, game_type: str, user_id: int,
bet_type: str, amount: int, multiplier: float,
bet_category: str = None, bet_number: int = None,
bet_value: str = None, hand_status: str = None) -> int:
"""创建下注记录
Args:
chat_id: 会话ID
game_type: 游戏类型
user_id: 用户ID
bet_type: 下注类型(大小游戏:""/""21点"标准"等)
amount: 下注金额
multiplier: 赔率
bet_category: 下注类别(轮盘游戏:["数字"/"颜色"/"奇偶"/"大小"/"区间"]
bet_number: 数字下注轮盘游戏0-36
bet_value: 下注值(轮盘游戏:如"红色""奇数""1-12"等)
hand_status: 手牌状态21点游戏["playing"/"stood"/"busted"/"blackjack"]
Returns:
bet_id
"""
cursor = self.conn.cursor()
current_time = int(time.time())
cursor.execute("""
INSERT INTO casino_bets
(chat_id, game_type, user_id, bet_type, amount, multiplier, status, created_at,
bet_category, bet_number, bet_value, hand_status)
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)
""", (chat_id, game_type, user_id, bet_type, amount, multiplier, current_time,
bet_category, bet_number, bet_value, hand_status))
return cursor.lastrowid
def get_pending_bets(self, chat_id: int, game_type: str) -> List[Dict]:
"""获取待结算的下注列表
Args:
chat_id: 会话ID
game_type: 游戏类型
Returns:
下注列表
"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT * FROM casino_bets
WHERE chat_id = ? AND game_type = ? AND status = 'pending'
ORDER BY created_at ASC
""", (chat_id, game_type))
rows = cursor.fetchall()
return [dict(row) for row in rows]
def settle_casino_bets(self, chat_id: int, game_type: str, result: str,
banker_id: int, **kwargs) -> Dict:
"""结算所有下注(根据游戏类型分发到不同结算方法)
Args:
chat_id: 会话ID
game_type: 游戏类型
result: 游戏结果(大小游戏:""/""轮盘数字字符串21点特殊格式
banker_id: 庄家ID
**kwargs: 其他参数轮盘result_number21点hands_dict等
Returns:
结算详情字典
"""
if game_type == '大小':
return self._settle_bigsmall_bets(chat_id, game_type, result, banker_id)
elif game_type == '轮盘':
result_number = kwargs.get('result_number', int(result) if result.isdigit() else 0)
return self._settle_roulette_bets(chat_id, game_type, result_number, banker_id)
elif game_type == '21点':
hands_dict = kwargs.get('hands_dict', {})
return self._settle_blackjack_bets(chat_id, game_type, hands_dict, banker_id)
else:
# 兼容旧的大小游戏逻辑
return self._settle_bigsmall_bets(chat_id, game_type, result, banker_id)
def _settle_bigsmall_bets(self, chat_id: int, game_type: str, result: str,
banker_id: int) -> Dict:
"""结算大小游戏下注
Args:
chat_id: 会话ID
game_type: 游戏类型
result: 游戏结果(""""
banker_id: 庄家ID
Returns:
结算详情字典
"""
cursor = self.conn.cursor()
current_time = int(time.time())
# 获取活跃会话
session = self.get_active_casino_session(chat_id, game_type)
if not session:
raise ValueError("没有活跃的游戏会话")
if session['banker_id'] != banker_id:
raise ValueError("只有庄家可以结算游戏")
# 获取所有待结算下注
bets = self.get_pending_bets(chat_id, game_type)
winners = []
losers = []
total_win = 0
# 计算输赢
for bet in bets:
is_win = (bet['bet_type'] == result)
if is_win:
# 计算赢得金额
win_amount = int(bet['amount'] * bet['multiplier'])
# 扣除抽水
house_fee = session['house_fee']
actual_win = int(win_amount * (1 - house_fee))
winners.append({
'user_id': bet['user_id'],
'amount': bet['amount'],
'win_amount': actual_win,
'bet_id': bet['id']
})
total_win += actual_win
else:
losers.append({
'user_id': bet['user_id'],
'amount': bet['amount'],
'bet_id': bet['id']
})
# 使用事务确保原子性
try:
# 更新下注状态
for bet in bets:
is_win = (bet['bet_type'] == result)
if is_win:
win_amount = int(bet['amount'] * bet['multiplier'])
actual_win = int(win_amount * (1 - session['house_fee']))
# 发放奖励
self.add_points(bet['user_id'], actual_win, 'casino_win',
f"赌场游戏{game_type}赢得")
cursor.execute("""
UPDATE casino_bets
SET status = 'settled', result = ?, win_amount = ?, settled_at = ?
WHERE id = ?
""", (result, actual_win, current_time, bet['id']))
else:
cursor.execute("""
UPDATE casino_bets
SET status = 'settled', result = ?, settled_at = ?
WHERE id = ?
""", (result, current_time, bet['id']))
# 关闭会话
cursor.execute("""
UPDATE casino_sessions
SET status = 'closed', settled_at = ?
WHERE id = ?
""", (current_time, session['id']))
return {
'winners': winners,
'losers': losers,
'total_win': total_win,
'result': result
}
except Exception as e:
logger.error(f"结算失败: {e}", exc_info=True)
raise
def _settle_roulette_bets(self, chat_id: int, game_type: str, result_number: int,
banker_id: int) -> Dict:
"""结算轮盘游戏下注
Args:
chat_id: 会话ID
game_type: 游戏类型
result_number: 结果数字0-36
banker_id: 庄家ID
Returns:
结算详情字典
"""
cursor = self.conn.cursor()
current_time = int(time.time())
session = self.get_active_casino_session(chat_id, game_type)
if not session:
raise ValueError("没有活跃的游戏会话")
if session['banker_id'] != banker_id:
raise ValueError("只有庄家可以结算游戏")
bets = self.get_pending_bets(chat_id, game_type)
winners = []
losers = []
total_win = 0
# 轮盘数字对应的颜色欧式轮盘0为绿色
roulette_red = {1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36}
result_color = '绿色' if result_number == 0 else ('红色' if result_number in roulette_red else '黑色')
result_odd_even = None if result_number == 0 else ('奇数' if result_number % 2 == 1 else '偶数')
result_big_small = None if result_number == 0 else ('' if 1 <= result_number <= 18 else '')
result_range = None
if 1 <= result_number <= 12:
result_range = '1-12'
elif 13 <= result_number <= 24:
result_range = '13-24'
elif 25 <= result_number <= 36:
result_range = '25-36'
for bet in bets:
is_win = False
multiplier = bet['multiplier']
bet_category = bet.get('bet_category')
if bet_category == '数字':
if bet.get('bet_number') == result_number:
is_win = True
elif bet_category == '颜色':
if bet.get('bet_value') == result_color:
is_win = True
elif bet_category == '奇偶':
if result_odd_even and bet.get('bet_value') == result_odd_even:
is_win = True
elif bet_category == '大小':
if result_big_small and bet.get('bet_value') == result_big_small:
is_win = True
elif bet_category == '区间':
if result_range and bet.get('bet_value') == result_range:
is_win = True
if is_win:
win_amount = int(bet['amount'] * multiplier)
house_fee = session['house_fee']
actual_win = int(win_amount * (1 - house_fee))
winners.append({
'user_id': bet['user_id'],
'amount': bet['amount'],
'win_amount': actual_win,
'bet_id': bet['id']
})
total_win += actual_win
else:
losers.append({
'user_id': bet['user_id'],
'amount': bet['amount'],
'bet_id': bet['id']
})
try:
result_str = str(result_number)
for bet in bets:
is_win = False
multiplier = bet['multiplier']
bet_category = bet.get('bet_category')
if bet_category == '数字':
if bet.get('bet_number') == result_number:
is_win = True
elif bet_category == '颜色':
if bet.get('bet_value') == result_color:
is_win = True
elif bet_category == '奇偶':
if result_odd_even and bet.get('bet_value') == result_odd_even:
is_win = True
elif bet_category == '大小':
if result_big_small and bet.get('bet_value') == result_big_small:
is_win = True
elif bet_category == '区间':
if result_range and bet.get('bet_value') == result_range:
is_win = True
if is_win:
win_amount = int(bet['amount'] * multiplier)
actual_win = int(win_amount * (1 - session['house_fee']))
self.add_points(bet['user_id'], actual_win, 'casino_win',
f"赌场游戏{game_type}赢得")
cursor.execute("""
UPDATE casino_bets
SET status = 'settled', result = ?, win_amount = ?, settled_at = ?
WHERE id = ?
""", (result_str, actual_win, current_time, bet['id']))
else:
cursor.execute("""
UPDATE casino_bets
SET status = 'settled', result = ?, settled_at = ?
WHERE id = ?
""", (result_str, current_time, bet['id']))
cursor.execute("""
UPDATE casino_sessions
SET status = 'closed', settled_at = ?
WHERE id = ?
""", (current_time, session['id']))
return {
'winners': winners,
'losers': losers,
'total_win': total_win,
'result': result_str,
'result_number': result_number
}
except Exception as e:
logger.error(f"结算失败: {e}", exc_info=True)
raise
def _settle_blackjack_bets(self, chat_id: int, game_type: str, hands_dict: Dict,
banker_id: int) -> Dict:
"""结算21点游戏下注
Args:
chat_id: 会话ID
game_type: 游戏类型
hands_dict: 手牌字典 {user_id: {'cards': [2,3,4], 'status': 'stood'}, ...}
banker_id: 庄家ID
Returns:
结算详情字典
"""
cursor = self.conn.cursor()
current_time = int(time.time())
session = self.get_active_casino_session(chat_id, game_type)
if not session:
raise ValueError("没有活跃的游戏会话")
if session['banker_id'] != banker_id:
raise ValueError("只有庄家可以结算游戏")
bets = self.get_pending_bets(chat_id, game_type)
banker_hand = hands_dict.get(0, {}) # 0表示庄家
banker_cards = banker_hand.get('cards', [])
banker_status = banker_hand.get('status', 'stood')
banker_points = self._calculate_blackjack_points(banker_cards)
banker_is_busted = banker_status == 'busted'
banker_is_blackjack = banker_status == 'blackjack'
winners = []
losers = []
total_win = 0
for bet in bets:
user_id = bet['user_id']
player_hand = hands_dict.get(user_id, {})
player_cards = player_hand.get('cards', [])
player_status = player_hand.get('status', 'stood')
player_points = self._calculate_blackjack_points(player_cards)
player_is_busted = player_status == 'busted'
player_is_blackjack = player_status == 'blackjack'
is_win = False
multiplier = bet['multiplier']
if player_is_busted:
is_win = False
elif player_is_blackjack:
if banker_is_blackjack:
# 双方都是黑杰克,平局(返还下注)
is_win = False
self.add_points(user_id, bet['amount'], 'casino_blackjack_push',
"21点游戏平局返还下注")
else:
# 玩家黑杰克赢得1.5倍
is_win = True
multiplier = session.get('blackjack_multiplier', 1.5)
elif banker_is_busted:
is_win = True
elif player_points > banker_points:
is_win = True
elif player_points == banker_points:
# 平局,返还下注
is_win = False
self.add_points(user_id, bet['amount'], 'casino_blackjack_push',
"21点游戏平局返还下注")
if is_win:
win_amount = int(bet['amount'] * multiplier)
house_fee = session['house_fee']
actual_win = int(win_amount * (1 - house_fee))
winners.append({
'user_id': user_id,
'amount': bet['amount'],
'win_amount': actual_win,
'bet_id': bet['id']
})
total_win += actual_win
elif player_points != banker_points:
# 输家:包括爆牌或点数小于庄家
losers.append({
'user_id': user_id,
'amount': bet['amount'],
'bet_id': bet['id']
})
try:
for bet in bets:
user_id = bet['user_id']
player_hand = hands_dict.get(user_id, {})
player_cards = player_hand.get('cards', [])
player_status = player_hand.get('status', 'stood')
player_points = self._calculate_blackjack_points(player_cards)
player_is_busted = player_status == 'busted'
player_is_blackjack = player_status == 'blackjack'
is_win = False
multiplier = bet['multiplier']
if player_is_busted:
is_win = False
elif player_is_blackjack:
if banker_is_blackjack:
is_win = False
self.add_points(user_id, bet['amount'], 'casino_blackjack_push',
"21点游戏平局返还下注")
else:
is_win = True
multiplier = session.get('blackjack_multiplier', 1.5)
elif banker_is_busted:
is_win = True
elif player_points > banker_points:
is_win = True
elif player_points == banker_points:
is_win = False
self.add_points(user_id, bet['amount'], 'casino_blackjack_push',
"21点游戏平局返还下注")
# 生成结果字符串,包含玩家状态信息
if player_is_busted:
player_desc = "爆牌"
elif player_is_blackjack:
player_desc = f"{player_points}点(黑杰克)"
else:
player_desc = f"{player_points}"
banker_desc = "爆牌" if banker_is_busted else (f"{banker_points}点(黑杰克)" if banker_is_blackjack else f"{banker_points}")
result_str = f"庄家{banker_desc} vs 玩家{player_desc}"
if is_win:
win_amount = int(bet['amount'] * multiplier)
actual_win = int(win_amount * (1 - session['house_fee']))
self.add_points(user_id, actual_win, 'casino_win',
f"赌场游戏{game_type}赢得")
cursor.execute("""
UPDATE casino_bets
SET status = 'settled', result = ?, win_amount = ?, settled_at = ?
WHERE id = ?
""", (result_str, actual_win, current_time, bet['id']))
elif player_points == banker_points:
# 平局,已返还下注(在上面的逻辑中已处理)
cursor.execute("""
UPDATE casino_bets
SET status = 'settled', result = ?, settled_at = ?
WHERE id = ?
""", (result_str, current_time, bet['id']))
else:
# 输家:包括爆牌或点数小于庄家
cursor.execute("""
UPDATE casino_bets
SET status = 'settled', result = ?, settled_at = ?
WHERE id = ?
""", (result_str, current_time, bet['id']))
cursor.execute("""
UPDATE casino_sessions
SET status = 'closed', settled_at = ?
WHERE id = ?
""", (current_time, session['id']))
return {
'winners': winners,
'losers': losers,
'total_win': total_win,
'result': f"庄家{banker_points}"
}
except Exception as e:
logger.error(f"结算失败: {e}", exc_info=True)
raise
def _calculate_blackjack_points(self, cards: List[int]) -> int:
"""计算21点手牌点数
Args:
cards: 手牌列表A=1J/Q/K=10其他为本身值1-13其中11=J, 12=Q, 13=K
Returns:
点数
"""
points = 0
ace_count = 0
for card in cards:
if card == 1:
ace_count += 1
points += 11
elif card >= 11:
points += 10
else:
points += card
# 处理A的1/11选择
while points > 21 and ace_count > 0:
points -= 10
ace_count -= 1
return points
def close_casino_session(self, chat_id: int, game_type: str):
"""关闭游戏会话
Args:
chat_id: 会话ID
game_type: 游戏类型
"""
cursor = self.conn.cursor()
current_time = int(time.time())
cursor.execute("""
UPDATE casino_sessions
SET status = 'closed', settled_at = ?
WHERE chat_id = ? AND game_type = ? AND status = 'open'
""", (current_time, chat_id, game_type))
# ===== 21点手牌管理 =====
def create_blackjack_hand(self, session_id: int, user_id: int,
hand_data: List[int], hand_status: str = 'playing') -> int:
"""创建21点手牌记录
Args:
session_id: 会话ID
user_id: 用户ID0表示庄家
hand_data: 手牌列表
hand_status: 手牌状态
Returns:
hand_id
"""
cursor = self.conn.cursor()
current_time = int(time.time())
hand_data_json = json.dumps(hand_data)
cursor.execute("""
INSERT INTO casino_blackjack_hands
(session_id, user_id, hand_data, hand_status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (session_id, user_id, hand_data_json, hand_status, current_time, current_time))
return cursor.lastrowid
def get_blackjack_hand(self, session_id: int, user_id: int) -> Optional[Dict]:
"""获取21点手牌
Args:
session_id: 会话ID
user_id: 用户ID
Returns:
手牌信息字典或None
"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT * FROM casino_blackjack_hands
WHERE session_id = ? AND user_id = ?
ORDER BY id DESC LIMIT 1
""", (session_id, user_id))
row = cursor.fetchone()
if row:
hand_dict = dict(row)
hand_dict['hand_data'] = json.loads(hand_dict['hand_data'])
return hand_dict
return None
def update_blackjack_hand(self, session_id: int, user_id: int,
hand_data: List[int], hand_status: str):
"""更新21点手牌
Args:
session_id: 会话ID
user_id: 用户ID
hand_data: 手牌列表
hand_status: 手牌状态
"""
cursor = self.conn.cursor()
current_time = int(time.time())
hand_data_json = json.dumps(hand_data)
cursor.execute("""
UPDATE casino_blackjack_hands
SET hand_data = ?, hand_status = ?, updated_at = ?
WHERE session_id = ? AND user_id = ?
""", (hand_data_json, hand_status, current_time, session_id, user_id))
def get_all_blackjack_hands(self, session_id: int) -> Dict[int, Dict]:
"""获取所有21点手牌用于结算
Args:
session_id: 会话ID
Returns:
手牌字典 {user_id: {'cards': [...], 'status': ...}, ...}
"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT * FROM casino_blackjack_hands
WHERE session_id = ?
ORDER BY user_id, id DESC
""", (session_id,))
rows = cursor.fetchall()
hands_dict = {}
for row in rows:
hand_dict = dict(row)
user_id = hand_dict['user_id']
# 只保留最新的手牌(每个用户)
if user_id not in hands_dict:
hands_dict[user_id] = {
'cards': json.loads(hand_dict['hand_data']),
'status': hand_dict['hand_status']
}
return hands_dict
def close(self):
"""关闭数据库连接"""
if self._conn:

View File

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

View File

@@ -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,31 +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` - 庄家放弃游戏(返还下注)
### 其他
- `.help` - 显示帮助
- `.stats` - 查看个人统计

File diff suppressed because it is too large Load Diff

View File

@@ -82,10 +82,12 @@ 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:
@@ -182,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', '')
@@ -212,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', '')

View File

@@ -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):

View File

@@ -126,7 +126,7 @@ class PointsGame(BaseGame):
for i, user in enumerate(leaderboard):
rank = i + 1
medal = medals[i] if i < len(medals) else "🏅"
username = 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
View 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
View 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
View 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

View File

@@ -168,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':
@@ -208,12 +214,6 @@ async def handle_command(game_type: str, command: str,
game = AIChatGame()
return await game.handle(command, chat_id, user_id)
# 赌场系统
if game_type == 'casino':
from games.casino import CasinoGame
game = CasinoGame()
return await game.handle(command, chat_id, user_id)
# 未知游戏类型
logger.warning(f"未知游戏类型: {game_type}")
return "❌ 未知的游戏类型"

View File

@@ -43,6 +43,10 @@ class CommandParser:
'.gomoku': 'gomoku',
'.五子棋': 'gomoku',
'.gobang': 'gomoku',
# 三国杀
'.sgs': 'sanguosha',
'.三国杀': 'sanguosha',
# 积分系统
'.points': 'points',
@@ -80,10 +84,6 @@ class CommandParser:
# 统计
'.stats': 'stats',
'.统计': 'stats',
# 赌场系统
'.赌场': 'casino',
'.casino': 'casino',
}
# 机器人名称模式(用于从@消息中提取)