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.
@@ -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
+ ![Python Tests](https://github.com/zhuyiming0208/SkillUNO/actions/workflows/test.yml/badge.svg)
13
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
14
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)
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
+ ![游戏界面 1](https://cdn.jsdelivr.net/gh/zhuyiming0208/SkillUNO@main/docs/screenshots/gameplay1.jpg)
47
+
48
+ ![游戏界面 2](https://cdn.jsdelivr.net/gh/zhuyiming0208/SkillUNO@main/docs/screenshots/gameplay2.jpg)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ skilluno = uno.main:cli
@@ -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} 尚无战绩记录。")
uno/combo_base.py ADDED
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class ComboBase(ABC):
4
+ @abstractmethod
5
+ def execute(self, player, skill_manager, game):
6
+ """执行组合技效果"""
7
+ pass