skilluno 1.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skilluno-1.12.1.dist-info/METADATA +291 -0
- skilluno-1.12.1.dist-info/RECORD +46 -0
- skilluno-1.12.1.dist-info/WHEEL +5 -0
- skilluno-1.12.1.dist-info/entry_points.txt +2 -0
- skilluno-1.12.1.dist-info/licenses/LICENSE +21 -0
- skilluno-1.12.1.dist-info/top_level.txt +1 -0
- uno/__init__.py +1 -0
- uno/achievements.py +105 -0
- uno/archive.py +63 -0
- uno/combo_base.py +7 -0
- uno/core.py +912 -0
- uno/enabled.py +61 -0
- uno/main.py +148 -0
- uno/mod_index.py +208 -0
- uno/mod_store/__init__.py +4 -0
- uno/mod_store/cli.py +368 -0
- uno/mod_store/collection_view.py +69 -0
- uno/mod_store/collections.py +52 -0
- uno/mod_store/confirm.py +125 -0
- uno/mod_store/context.py +12 -0
- uno/mod_store/detail.py +147 -0
- uno/mod_store/downloader.py +232 -0
- uno/mod_store/enable.py +55 -0
- uno/mod_store/history.py +98 -0
- uno/mod_store/history_view.py +93 -0
- uno/mod_store/input_handler.py +218 -0
- uno/mod_store/main.py +65 -0
- uno/mod_store/menu.py +236 -0
- uno/mod_store/render.py +158 -0
- uno/mod_store/router.py +389 -0
- uno/mod_store/search.py +131 -0
- uno/mod_store/uninstall_confirm.py +59 -0
- uno/mod_store/uninstaller.py +48 -0
- uno/mod_store/updater.py +113 -0
- uno/mod_store/validator.py +81 -0
- uno/mod_store/web_open.py +22 -0
- uno/paths.py +12 -0
- uno/presets.py +59 -0
- uno/replay.py +145 -0
- uno/season1.py +546 -0
- uno/season2.py +448 -0
- uno/season3.py +3 -0
- uno/season4.py +420 -0
- uno/skills_uno.py +133 -0
- uno/stats.py +289 -0
- uno/ui.py +97 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skilluno
|
|
3
|
+
Version: 1.12.1
|
|
4
|
+
Summary: 一个基于 Python 的 UNO 卡牌游戏框架
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
# SkillUNO
|
|
17
|
+
|
|
18
|
+
> 一个基于 Python 的 UNO 卡牌游戏框架,支持 Mod 化技能、赛季、成就、回放、热座模式、CLI 商店与网页版商店。
|
|
19
|
+
|
|
20
|
+
*(部分代码由 AI 辅助生成)*
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 目录
|
|
25
|
+
|
|
26
|
+
- [游戏截图](#游戏截图)
|
|
27
|
+
- [特性](#特性)
|
|
28
|
+
- [快速开始](#快速开始)
|
|
29
|
+
- [使用方式](#使用方式)
|
|
30
|
+
- [交互式菜单](#交互式菜单)
|
|
31
|
+
- [CLI 子命令](#cli-子命令)
|
|
32
|
+
- [模组系统](#模组系统)
|
|
33
|
+
- [安装社区模组](#安装社区模组)
|
|
34
|
+
- [制作自己的模组](#制作自己的模组)
|
|
35
|
+
- [网页版商店](#网页版商店)
|
|
36
|
+
- [项目结构](#项目结构)
|
|
37
|
+
- [开发](#开发)
|
|
38
|
+
- [兼容性](#兼容性)
|
|
39
|
+
- [文档](#文档)
|
|
40
|
+
- [许可证](#许可证)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 游戏截图
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 特性
|
|
53
|
+
|
|
54
|
+
| 特性 | 说明 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| **事件驱动技能系统** | 技能通过订阅游戏事件(GAME_START / TURN_START / BEING_ADDED_CARDS 等)触发,支持自动与手动激活 |
|
|
57
|
+
| **赛季系统** | 内置 S1 经典 / S2 被动觉醒 / S3 同盟 / S4 补丁 四个官方赛季,含 30+ 技能 |
|
|
58
|
+
| **Mod 化扩展** | 支持社区作者发布自定义赛季与技能,通过 CLI 商店或网页商店安装 |
|
|
59
|
+
| **成就系统** | 内置成就 + 隐藏成就,记录在存档中,跨局累计 |
|
|
60
|
+
| **完整对局回放** | 每局可录制,支持交互式回放,逐事件查看 |
|
|
61
|
+
| **热座模式** | 2~4 人本地轮流同屏操作 |
|
|
62
|
+
| **统计面板** | 玩家胜率排行、技能使用次数、牌型频率、对局统计 |
|
|
63
|
+
| **可选彩色 UI** | 基于 `rich` 库,未安装时自动降级为纯文本 |
|
|
64
|
+
| **CLI 模组商店** | 交互式菜单 + 命令行子命令双入口 |
|
|
65
|
+
| **网页版商店** | 深色终端风,支持浏览、搜索、查看详情、一键安装命令 |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 快速开始
|
|
70
|
+
|
|
71
|
+
### 方式一:可编辑安装(推荐)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/zhuyiming0208/SkillUNO.git
|
|
75
|
+
cd SkillUNO
|
|
76
|
+
pip install -e .
|
|
77
|
+
skilluno
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
方式二:不安装,直接运行
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/zhuyiming0208/SkillUNO.git
|
|
84
|
+
cd SkillUNO
|
|
85
|
+
PYTHONPATH=src python -m uno.main
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
方式三:Termux(Android)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pkg install python git
|
|
92
|
+
git clone https://github.com/zhuyiming0208/SkillUNO.git
|
|
93
|
+
cd SkillUNO
|
|
94
|
+
pip install -e .
|
|
95
|
+
skilluno
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Termux 提示:软键盘没有方向键,商店内使用 u/d/l/r 代替上下左右,~ 代替 Esc。
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
使用方式
|
|
103
|
+
|
|
104
|
+
交互式菜单
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
skilluno
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
进入主菜单:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
SkillUNO vX.Y.Z
|
|
114
|
+
1. 新游戏
|
|
115
|
+
2. 回放录像
|
|
116
|
+
3. 统计面板
|
|
117
|
+
4. 模组商店
|
|
118
|
+
0. 退出
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
· 新游戏:选择赛季组合、房间规则、是否录制
|
|
122
|
+
· 回放:从 src/uno/records/ 里选择录像播放
|
|
123
|
+
· 统计面板:读取存档与回放,显示胜率排行、技能使用、牌型频率
|
|
124
|
+
· 模组商店:浏览、搜索、安装、卸载、启用/禁用模组(按 W 打开网页版)
|
|
125
|
+
|
|
126
|
+
CLI 子命令
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
skilluno mod list # 列出全部模组
|
|
130
|
+
skilluno mod search <关键词> # 模糊搜索
|
|
131
|
+
skilluno mod info <ID> # 查看模组详情
|
|
132
|
+
skilluno mod install <ID> # 下载 + 验证 + 启用
|
|
133
|
+
skilluno mod uninstall <ID> # 卸载
|
|
134
|
+
skilluno mod list-collections # 列出模组集
|
|
135
|
+
skilluno mod install-collection <ID> # 一键安装模组集
|
|
136
|
+
skilluno mod --help # 查看帮助
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
模组系统
|
|
142
|
+
|
|
143
|
+
安装社区模组
|
|
144
|
+
|
|
145
|
+
方式一:CLI 商店
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
skilluno
|
|
149
|
+
# 选 4 进入商店
|
|
150
|
+
# 按 S 搜索、Enter 查看详情、I 安装
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
方式二:命令行
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
skilluno mod install EXAMPLE
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
方式三:网页版商店
|
|
160
|
+
|
|
161
|
+
访问 https://zhuyiming0208.github.io/SkillUNO/ ,复制安装命令。
|
|
162
|
+
|
|
163
|
+
安全机制
|
|
164
|
+
|
|
165
|
+
下载的模组经过多重验证:
|
|
166
|
+
|
|
167
|
+
1. URL 白名单:只允许 raw.githubusercontent.com 和 cdn.jsdelivr.net
|
|
168
|
+
2. 大小限制:单文件 ≤ 5MB
|
|
169
|
+
3. 静态验证:用 ast.parse 解析 SEASON_ID,绝不 import 执行
|
|
170
|
+
4. 隔离区:下载到 mods/_pending/,用户显式确认后才启用
|
|
171
|
+
5. 备份:覆盖已安装模组前先备份 .bak
|
|
172
|
+
|
|
173
|
+
制作自己的模组
|
|
174
|
+
|
|
175
|
+
见 模组制作指南。
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
网页版商店
|
|
180
|
+
|
|
181
|
+
网址:https://zhuyiming0208.github.io/SkillUNO/
|
|
182
|
+
|
|
183
|
+
纯静态 HTML/CSS/JS,无构建依赖。包含:
|
|
184
|
+
|
|
185
|
+
· 首页:官方模组 + 精选模组 + 模组集
|
|
186
|
+
· 列表页:全部模组 + 搜索
|
|
187
|
+
· 详情页:模组信息、依赖、权限声明、一键复制安装命令
|
|
188
|
+
· 模组集:浏览和批量安装
|
|
189
|
+
|
|
190
|
+
源码位于 web/ 目录。改完后:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
cd web
|
|
194
|
+
python -m http.server 8000
|
|
195
|
+
# 浏览器访问 http://localhost:8000
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
项目结构
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
SkillUNO/
|
|
204
|
+
├── docs/ 项目文档
|
|
205
|
+
│ ├── DEVELOPER_GUIDE.md
|
|
206
|
+
│ ├── FAQ.md
|
|
207
|
+
│ ├── VersionBriefHistory.md
|
|
208
|
+
│ └── screenshots/
|
|
209
|
+
├── mods/ 模组元数据
|
|
210
|
+
│ ├── mod_idx.json 第三方模组索引
|
|
211
|
+
│ ├── official.json 官方模组列表
|
|
212
|
+
│ ├── S1~S4.json 官方赛季元数据
|
|
213
|
+
│ ├── example.json 示例模组
|
|
214
|
+
│ └── collections.json 模组集
|
|
215
|
+
├── src/uno/ 核心代码
|
|
216
|
+
│ ├── core.py 游戏核心(牌、玩家、回合、事件)
|
|
217
|
+
│ ├── season1~4.py 官方赛季技能
|
|
218
|
+
│ ├── skills_uno.py 技能加载器
|
|
219
|
+
│ ├── achievements.py 成就系统
|
|
220
|
+
│ ├── archive.py 存档读写
|
|
221
|
+
│ ├── replay.py 回放录制与播放
|
|
222
|
+
│ ├── stats.py 统计面板
|
|
223
|
+
│ ├── ui.py UI 抽象(ConsoleUI / RichUI / NullUI)
|
|
224
|
+
│ ├── mod_index.py 模组索引读取
|
|
225
|
+
│ └── mod_store/ CLI 模组商店
|
|
226
|
+
│ ├── menu.py / search.py / detail.py / history_view.py
|
|
227
|
+
│ ├── downloader.py / validator.py
|
|
228
|
+
│ ├── uninstaller.py / updater.py
|
|
229
|
+
│ └── cli.py 命令行子命令
|
|
230
|
+
├── tests/ 测试(700+ 用例)
|
|
231
|
+
├── web/ 网页版商店
|
|
232
|
+
├── tools/bump.py 版本号升级工具
|
|
233
|
+
└── .github/workflows/ CI(测试 + 网页部署)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
开发
|
|
239
|
+
|
|
240
|
+
运行测试
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
python -m pytest tests/ -v
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
带覆盖率:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
python -m pytest tests/ --cov=src/uno --cov-report=term-missing
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
贡献
|
|
253
|
+
|
|
254
|
+
欢迎提交 Issue 或 PR。添加新功能时请:
|
|
255
|
+
|
|
256
|
+
1. 保持测试覆盖率
|
|
257
|
+
2. 更新 docs/VersionBriefHistory.md
|
|
258
|
+
3. 用 setver X.Y.Z 升级版本号
|
|
259
|
+
|
|
260
|
+
版本号工具
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
setver 1.13.0 --new-entry "新功能说明"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
(setver 是 tools/bump.py 的别名,需自行配置)
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
兼容性
|
|
271
|
+
|
|
272
|
+
项 支持
|
|
273
|
+
Python 3.10 / 3.11 / 3.12 / 3.14
|
|
274
|
+
平台 Linux / macOS / Android (Termux) / Windows
|
|
275
|
+
可选依赖 rich(彩色 UI)
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
文档
|
|
280
|
+
|
|
281
|
+
· 开发者指南
|
|
282
|
+
· FAQ
|
|
283
|
+
· 从小白到高手
|
|
284
|
+
· 版本历史
|
|
285
|
+
· 模组制作
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
许可证
|
|
290
|
+
|
|
291
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
skilluno-1.12.1.dist-info/licenses/LICENSE,sha256=fjj5026cQR7rwfl5vh4nFmllfGKsgtJt7eOUaB9J_TM,1070
|
|
2
|
+
uno/__init__.py,sha256=bIfXiAO75WU_3FEyu_zf3qEikXKCMNxrs9XtUBx8noQ,31
|
|
3
|
+
uno/achievements.py,sha256=IdySkCjVMK4EGaGMuLek-D2j7z5figwpxxZjtAQ03FY,3473
|
|
4
|
+
uno/archive.py,sha256=uRPU5d-FAYKVZtRKnpsNguN8Kgtj85afSp06u1OX9VM,2177
|
|
5
|
+
uno/combo_base.py,sha256=E4JhpGfbc7Qh5Dj_gvgp7-s_EgANTvw01BSR3tRMhMA,179
|
|
6
|
+
uno/core.py,sha256=xxy-FcGnn0eAH4kQc7BOygnQfqGDFhe67Lvd4bZpp4k,36594
|
|
7
|
+
uno/enabled.py,sha256=80FaUYYOO2wKdbgKXvNKhiLk0ejYfG6u1wiv7oiVS2c,1851
|
|
8
|
+
uno/main.py,sha256=GSUNrW9vuTs79-Mcm_J3SSRpMqre5M1ywyTYkc2J7yY,4235
|
|
9
|
+
uno/mod_index.py,sha256=XiCmc9hFukX39rkPiJs8R2pLLsLgfMcQXSCp3Xm0BAY,7452
|
|
10
|
+
uno/paths.py,sha256=vuyX89G5lnAv1y_PJNDN-2W0KYrOmORr6In2zMkCb8w,333
|
|
11
|
+
uno/presets.py,sha256=uv51GpZt5U0nj2BWbEEy4hpSJagA7jJO1eHihGfrtuE,1977
|
|
12
|
+
uno/replay.py,sha256=lBNIkgniiL93HQ7a5lguALWKN0YPeD3wkYDs9OyxWm8,5402
|
|
13
|
+
uno/season1.py,sha256=G9NkFNIcbAV6OXdnzyyc8b1XkT8qFLAW6dOPA5JrduY,22533
|
|
14
|
+
uno/season2.py,sha256=FuaKebshGoU4M3w_ejIiKO_cLI44jzleUQ9RgcwmJGo,20092
|
|
15
|
+
uno/season3.py,sha256=mi66k9iL7RR_6c3tdIkc1uzOCjsOsyPoDroiME3edhs,67
|
|
16
|
+
uno/season4.py,sha256=rNzrJ4gHpwM27yDP8nIouq7DSYZ6a5M7N9G2sCR61ss,17979
|
|
17
|
+
uno/skills_uno.py,sha256=BRyL7E-nMdLPjMTK9qecfPWgTCkQygzw3GHGpWLQs_s,6094
|
|
18
|
+
uno/stats.py,sha256=RG9xE495pQwBMdNk72ZjmWJR2Vmme-f8O_VLKZOr65U,11164
|
|
19
|
+
uno/ui.py,sha256=S-kF4uPglw1rDqDyW5jnwobpov7cV8GnSXMVoXUPQOI,2827
|
|
20
|
+
uno/mod_store/__init__.py,sha256=q7V-HBTGugbrCdKQInXQzdreoUzMatL-uIB9_H853BI,112
|
|
21
|
+
uno/mod_store/cli.py,sha256=lzdyop1uMPBPMIn70ZmLPCX6L23CrZZsR9qxIBYUu7s,12532
|
|
22
|
+
uno/mod_store/collection_view.py,sha256=4UEAtTn-YbdShMyLzHb0smjV0nj65fEiTo_ss1Mk4Xo,2297
|
|
23
|
+
uno/mod_store/collections.py,sha256=FBK6db3lyok6sA8_jV7YPsIAcRW7xVQxjjq275gNp1U,1376
|
|
24
|
+
uno/mod_store/confirm.py,sha256=0B5YMUDvLW-_-_zhetdfyGQhm5djMgAkWNELfIyYZ40,5009
|
|
25
|
+
uno/mod_store/context.py,sha256=kRFMH55IvugJnvYViRYht4no2yQP8x-dHYOjvbEn4M8,243
|
|
26
|
+
uno/mod_store/detail.py,sha256=HWGcaLBpzauXnSmQCRSDXPHvkCx0FYiw5pw3NWSuM-o,5154
|
|
27
|
+
uno/mod_store/downloader.py,sha256=odlGqQwcsRrAYL79rcmbNH1N4eorf08b_eKX3h_J-Dc,7649
|
|
28
|
+
uno/mod_store/enable.py,sha256=rUeOBi1Uv2fpyR1C1lzyRwNXyJDtlvnzOI2Ev5WWui4,1883
|
|
29
|
+
uno/mod_store/history.py,sha256=gRi9t46Y--ZB762yyw2vhCJgxGGDm08-9mbkBSR93vk,2968
|
|
30
|
+
uno/mod_store/history_view.py,sha256=Y9OleHq3hWMg2p7A9sHjYu-1yIyvonb73GoSL94sHc0,3516
|
|
31
|
+
uno/mod_store/input_handler.py,sha256=tWl5TGFDI0_GcoAcA4IGPwj4ZNnO6NzSTd9s2E2RxP8,5600
|
|
32
|
+
uno/mod_store/main.py,sha256=Szu2AhVaDIh4e7Zwsetc57SIUmcKWQ3PR5qkVmy3Zn0,1828
|
|
33
|
+
uno/mod_store/menu.py,sha256=G0qUFpsUjrgTeq--w3JVvCFBK049E1M71JWZEnoP6YY,8772
|
|
34
|
+
uno/mod_store/render.py,sha256=vWX5q84rU-_LrCZ7GckcpbryKfg-fTNBMZ5AR9eZ49k,4137
|
|
35
|
+
uno/mod_store/router.py,sha256=60iMDy-xMzT4JUggtMzuZTphxXTOJUlHzVgGUZJkzgU,13186
|
|
36
|
+
uno/mod_store/search.py,sha256=Gsj9_GWjf-dnQkrWJjNInFO31jv83W6oN1D-4bzUSbg,4586
|
|
37
|
+
uno/mod_store/uninstall_confirm.py,sha256=_o3cK-N350zFBBoUKp4duAt01OmsDyEHUpq_8OzCaTc,2275
|
|
38
|
+
uno/mod_store/uninstaller.py,sha256=u_ZxDDsZ_oGDu4jT4syARChig5uzEhsQ1BPY9KlMI0w,1590
|
|
39
|
+
uno/mod_store/updater.py,sha256=sVhIvOUDXaX7rH3Q2MZRLvUeAE8GOhgFVu0auGjaseY,3447
|
|
40
|
+
uno/mod_store/validator.py,sha256=DgPH9ogLgWvAjLuwmHJB_oDHI0bb8kOI_XqBVHrNnlQ,2726
|
|
41
|
+
uno/mod_store/web_open.py,sha256=x_kfnyKlvsoYLLm_7fUjvn0a53s9umACRqxm-5IzNis,617
|
|
42
|
+
skilluno-1.12.1.dist-info/METADATA,sha256=nQnv2yo0rc8yk0KflUKfNo_pCAaOL_MwiQNiEgdPjvA,7480
|
|
43
|
+
skilluno-1.12.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
44
|
+
skilluno-1.12.1.dist-info/entry_points.txt,sha256=bREmTDYdBvyzvaT4RbKiS2Ur3kri3gRtflGCf5_GntQ,42
|
|
45
|
+
skilluno-1.12.1.dist-info/top_level.txt,sha256=2fhtNLCw4x9ZX7CTLAbHez8Y6jK5-HD1MotnSKhE4hA,4
|
|
46
|
+
skilluno-1.12.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zhuyiming0208
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uno
|
uno/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# 空文件,标记 uno 为包
|
uno/achievements.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class Achievement(ABC):
|
|
4
|
+
def __init__(self, id: str, name: str, description: str, hidden: bool = False):
|
|
5
|
+
self.id = id
|
|
6
|
+
self.name = name
|
|
7
|
+
self.description = description
|
|
8
|
+
self.hidden = hidden
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def check(self, game, events: list) -> bool:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def __repr__(self):
|
|
15
|
+
return f"{self.name}({self.id})"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Comeback1v3Achievement(Achievement):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__('comeback_1v3', '一穿三', '在只剩自己一人的情况下逆转获胜')
|
|
21
|
+
|
|
22
|
+
def check(self, game, events):
|
|
23
|
+
human = game.players[0]
|
|
24
|
+
if any(e['type'] == 'player_eliminated' and e['player'] == human.name for e in events):
|
|
25
|
+
return False
|
|
26
|
+
all_others_eliminated = all(
|
|
27
|
+
any(e['type'] == 'player_eliminated' and e['player'] == p.name for e in events)
|
|
28
|
+
for p in game.players if p != human
|
|
29
|
+
)
|
|
30
|
+
if not all_others_eliminated:
|
|
31
|
+
return False
|
|
32
|
+
if not any(e['type'] == 'game_end' and e.get('winner') == human.name for e in events):
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CounterMasterAchievement(Achievement):
|
|
38
|
+
def __init__(self):
|
|
39
|
+
super().__init__('counter_master', '反制大师', '单局成功反制 3 次以上')
|
|
40
|
+
|
|
41
|
+
def check(self, game, events):
|
|
42
|
+
counter_events = [
|
|
43
|
+
e for e in events
|
|
44
|
+
if e['type'] == 'skill_use' and e.get('skill_name') in ('破万法', '储能')
|
|
45
|
+
]
|
|
46
|
+
return len(counter_events) >= 3
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InstantKillAchievement(Achievement):
|
|
50
|
+
def __init__(self):
|
|
51
|
+
super().__init__('instant_kill', '秒杀', '在对手起手阶段就使其手牌超过 15 张')
|
|
52
|
+
|
|
53
|
+
def check(self, game, events):
|
|
54
|
+
for e in events:
|
|
55
|
+
if e['type'] == 'player_eliminated' and e.get('turn_number', 0) <= 2:
|
|
56
|
+
if e.get('reason') == 'hand_limit':
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NoDrawVictoryAchievement(Achievement):
|
|
62
|
+
def __init__(self):
|
|
63
|
+
super().__init__(
|
|
64
|
+
'no_draw_victory',
|
|
65
|
+
'被遗忘的战术',
|
|
66
|
+
'你从未主动抽取过一张牌,仿佛早已预知一切。',
|
|
67
|
+
hidden=True
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def check(self, game, events):
|
|
71
|
+
human = game.players[0]
|
|
72
|
+
if not any(e['type'] == 'game_end' and e.get('winner') == human.name for e in events):
|
|
73
|
+
return False
|
|
74
|
+
draw_events = [e for e in events if e['type'] == 'draw_card' and e.get('player') == human.name]
|
|
75
|
+
return len(draw_events) == 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_core_achievements():
|
|
79
|
+
"""工厂函数:每次调用返回全新的成就类列表"""
|
|
80
|
+
return [
|
|
81
|
+
Comeback1v3Achievement,
|
|
82
|
+
CounterMasterAchievement,
|
|
83
|
+
InstantKillAchievement,
|
|
84
|
+
NoDrawVictoryAchievement,
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AchievementManager:
|
|
89
|
+
def __init__(self, game, achievement_classes):
|
|
90
|
+
self.game = game
|
|
91
|
+
self.achievements = [cls() for cls in achievement_classes]
|
|
92
|
+
|
|
93
|
+
def add_achievements(self, achievement_classes):
|
|
94
|
+
for cls in achievement_classes:
|
|
95
|
+
self.achievements.append(cls())
|
|
96
|
+
|
|
97
|
+
def evaluate(self, events):
|
|
98
|
+
achieved = set()
|
|
99
|
+
for ach in self.achievements:
|
|
100
|
+
try:
|
|
101
|
+
if ach.check(self.game, events):
|
|
102
|
+
achieved.add(ach.name)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"[成就] 评估 {ach.name} 时出错:{e}")
|
|
105
|
+
return achieved
|
uno/archive.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
|
|
5
|
+
ARCHIVE_FILE = os.path.join(os.path.dirname(__file__), 'archive.txt')
|
|
6
|
+
|
|
7
|
+
def load_archive():
|
|
8
|
+
if not os.path.exists(ARCHIVE_FILE):
|
|
9
|
+
return {}
|
|
10
|
+
try:
|
|
11
|
+
with open(ARCHIVE_FILE, 'r', encoding='utf-8') as f:
|
|
12
|
+
encoded = f.read()
|
|
13
|
+
if not encoded:
|
|
14
|
+
return {}
|
|
15
|
+
decoded = base64.b64decode(encoded).decode('utf-8')
|
|
16
|
+
data = json.loads(decoded)
|
|
17
|
+
# 兼容旧存档:确保每个玩家都有 achievements 列表
|
|
18
|
+
for name in data:
|
|
19
|
+
if 'achievements' not in data[name]:
|
|
20
|
+
data[name]['achievements'] = []
|
|
21
|
+
return data
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f"[存档] 读取失败,使用空存档。错误:{e}")
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
def save_archive(data):
|
|
27
|
+
try:
|
|
28
|
+
json_str = json.dumps(data, ensure_ascii=False, indent=2)
|
|
29
|
+
encoded = base64.b64encode(json_str.encode('utf-8')).decode('ascii')
|
|
30
|
+
with open(ARCHIVE_FILE, 'w', encoding='ascii') as f:
|
|
31
|
+
f.write(encoded)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(f"[存档] 保存失败。错误:{e}")
|
|
34
|
+
|
|
35
|
+
def update_record(name: str, won: bool, achievements: list = None):
|
|
36
|
+
data = load_archive()
|
|
37
|
+
if name not in data:
|
|
38
|
+
data[name] = {'wins': 0, 'losses': 0, 'achievements': []}
|
|
39
|
+
if won:
|
|
40
|
+
data[name]['wins'] += 1
|
|
41
|
+
else:
|
|
42
|
+
data[name]['losses'] += 1
|
|
43
|
+
if achievements:
|
|
44
|
+
existing = set(data[name].get('achievements', []))
|
|
45
|
+
for ach in achievements:
|
|
46
|
+
existing.add(ach)
|
|
47
|
+
data[name]['achievements'] = list(existing)
|
|
48
|
+
save_archive(data)
|
|
49
|
+
print(f"[存档] {name} 的战绩已更新。")
|
|
50
|
+
|
|
51
|
+
def show_record(name: str):
|
|
52
|
+
data = load_archive()
|
|
53
|
+
if name in data:
|
|
54
|
+
wins = data[name]['wins']
|
|
55
|
+
losses = data[name]['losses']
|
|
56
|
+
total = wins + losses
|
|
57
|
+
rate = f"{wins/total*100:.7f}%" if total > 0 else "N/A"
|
|
58
|
+
print(f"{name} 战绩:{wins}胜 {losses}负,胜率 {rate}")
|
|
59
|
+
ach = data[name].get('achievements', [])
|
|
60
|
+
if ach:
|
|
61
|
+
print(f" 成就:{', '.join(ach)}")
|
|
62
|
+
else:
|
|
63
|
+
print(f"{name} 尚无战绩记录。")
|