career-emulator 0.0.6__tar.gz
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.
- career_emulator-0.0.6/LICENSE +21 -0
- career_emulator-0.0.6/PKG-INFO +152 -0
- career_emulator-0.0.6/README.md +120 -0
- career_emulator-0.0.6/career_emulator/__init__.py +1 -0
- career_emulator-0.0.6/career_emulator/action_log.py +87 -0
- career_emulator-0.0.6/career_emulator/cli.py +164 -0
- career_emulator-0.0.6/career_emulator/display_handbook.py +200 -0
- career_emulator-0.0.6/career_emulator/ending_score.py +258 -0
- career_emulator-0.0.6/career_emulator/events.py +382 -0
- career_emulator-0.0.6/career_emulator/failure_conditions.py +366 -0
- career_emulator-0.0.6/career_emulator/game.py +630 -0
- career_emulator-0.0.6/career_emulator/lru_dict.py +43 -0
- career_emulator-0.0.6/career_emulator/pretty_print.py +59 -0
- career_emulator-0.0.6/career_emulator/promotion_conditions.py +353 -0
- career_emulator-0.0.6/career_emulator/py.typed +0 -0
- career_emulator-0.0.6/career_emulator/run_mcp.py +18 -0
- career_emulator-0.0.6/career_emulator/salary_conditions.py +269 -0
- career_emulator-0.0.6/career_emulator/server/__init__.py +1 -0
- career_emulator-0.0.6/career_emulator/server/base.py +7 -0
- career_emulator-0.0.6/career_emulator/server/models.py +653 -0
- career_emulator-0.0.6/career_emulator/server/resources.py +16 -0
- career_emulator-0.0.6/career_emulator/server/tools.py +54 -0
- career_emulator-0.0.6/career_emulator/storage.py +283 -0
- career_emulator-0.0.6/career_emulator.egg-info/PKG-INFO +152 -0
- career_emulator-0.0.6/career_emulator.egg-info/SOURCES.txt +40 -0
- career_emulator-0.0.6/career_emulator.egg-info/dependency_links.txt +1 -0
- career_emulator-0.0.6/career_emulator.egg-info/entry_points.txt +3 -0
- career_emulator-0.0.6/career_emulator.egg-info/requires.txt +5 -0
- career_emulator-0.0.6/career_emulator.egg-info/top_level.txt +1 -0
- career_emulator-0.0.6/pyproject.toml +113 -0
- career_emulator-0.0.6/setup.cfg +4 -0
- career_emulator-0.0.6/tests/test_action_log.py +83 -0
- career_emulator-0.0.6/tests/test_ending_score.py +230 -0
- career_emulator-0.0.6/tests/test_events.py +254 -0
- career_emulator-0.0.6/tests/test_failure_conditions.py +359 -0
- career_emulator-0.0.6/tests/test_game.py +326 -0
- career_emulator-0.0.6/tests/test_latest_logs.py +37 -0
- career_emulator-0.0.6/tests/test_models.py +48 -0
- career_emulator-0.0.6/tests/test_promotion_conditions.py +168 -0
- career_emulator-0.0.6/tests/test_salary_conditions.py +103 -0
- career_emulator-0.0.6/tests/test_storage.py +133 -0
- career_emulator-0.0.6/tests/test_tools.py +100 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuwei and Hugo
|
|
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,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: career-emulator
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: A career emulator for AI Agents.
|
|
5
|
+
Author-email: Hugo <hugo@hugohuang.com>, Yuwei <yuwei.willow.z@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://career-emulator.readthedocs.io
|
|
8
|
+
Project-URL: Documentation, https://career-emulator.readthedocs.io
|
|
9
|
+
Project-URL: Issues, https://github.com/Trenza1ore/TomatoEmulator/issues
|
|
10
|
+
Project-URL: Gitcode, https://gitcode.com/SushiNinja/TomatoEmulator
|
|
11
|
+
Keywords: python,httpx,client,mcp,agent,fastmcp,llm,openjiuwen,mcp client,mcp server,model context protocol
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: <4,>=3.11
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: aiosqlite>=0.22.1
|
|
28
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
29
|
+
Provides-Extra: mcp
|
|
30
|
+
Requires-Dist: fastmcp; extra == "mcp"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Career Emulator - CCF BDCI 2026
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/career-emulator/) [](https://github.com/Trenza1ore/TomatoEmulator/blob/main/LICENSE) [](https://github.com/Trenza1ore/TomatoEmulator/actions/workflows/tests.yml)
|
|
36
|
+
|
|
37
|
+
[](https://career-emulator.readthedocs.io)
|
|
38
|
+
|
|
39
|
+
- 所属赛题:`2026 CCF 大数据与计算智能大赛` 的《职场生存与晋升挑战》赛题,说明见[比赛官网](https://www.xir.cn/competition/1165),赛事入口见[2026 CCF 大数据与计算智能大赛](https://www.xir.cn/competition/races/BDCI2026)
|
|
40
|
+
- 参赛方式:使用 [JiuwenSwarm](https://openjiuwen.com/jiuwenswarm) 构建 Agent,并开发一个或多个 [Skill](https://agentskills.io)
|
|
41
|
+
- 交付形式:将一个或多个 Skill 文件夹打包成单个 `zip`
|
|
42
|
+
- 交互方式:通过 `career-emulator` MCP 与环境交互
|
|
43
|
+
|
|
44
|
+
如果你准备做多 Agent 协同参谋,也可以看看 [Swarm Skill 标准](https://swarmskills.openjiuwen.com)。
|
|
45
|
+
|
|
46
|
+
你会从 `L1` 新人入职开始,在 48 个月里处理剧情事件、分配季度体力、选择季度主行动,再按半年节奏迎接绩效和晋升。目标也很直接:别太早出局,顺手把这段职业生涯经营得稍微体面一点。
|
|
47
|
+
|
|
48
|
+
项目目前提供两种入口:
|
|
49
|
+
|
|
50
|
+
- `career-emulator`:本地 CLI,适合直接试玩和调规则。
|
|
51
|
+
- `career-emulator-mcp`:MCP 服务,适合接给 Agent 自动玩。
|
|
52
|
+
|
|
53
|
+
更多公开规则、手册和玩法说明见文档:[career-emulator.readthedocs.io](https://career-emulator.readthedocs.io)。
|
|
54
|
+
|
|
55
|
+
## 这题大概在玩什么
|
|
56
|
+
|
|
57
|
+
每个月,系统会生成剧情事件,你从当前选项里挑一个往下走。每到季度末,还要额外处理两件事:
|
|
58
|
+
|
|
59
|
+
- 分配 3 点 `Energy`
|
|
60
|
+
- 选择 1 个季度主行动
|
|
61
|
+
|
|
62
|
+
半年后会做一次绩效评估,职级、财富、健康、尊严、技能、人脉和产出会一起算总账。很多规则是公开的,很多坑只会通过反馈和日志给你一点暗示。组织很忙,通常不会为你逐条标红。
|
|
63
|
+
|
|
64
|
+
如果你只是想先记住一句话:
|
|
65
|
+
|
|
66
|
+
> 在 48 个月里别把自己玩出局,同时尽量把结局打成一段还能写进简历的经历。
|
|
67
|
+
|
|
68
|
+
## 快速开始
|
|
69
|
+
|
|
70
|
+
先安装依赖:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
同步数据并开一局:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
make play
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
继续上一局:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv run career-emulator play --session-id <session-id>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
CLI 里常用的几个输入:
|
|
89
|
+
|
|
90
|
+
- 输入数字:执行当前选项
|
|
91
|
+
- 输入 `handbook`:查看新员工手册
|
|
92
|
+
- 输入 `logs`:查看当前会话日志
|
|
93
|
+
- 输入 `quit`:退出
|
|
94
|
+
|
|
95
|
+
## 给 Agent 用
|
|
96
|
+
|
|
97
|
+
当前 MCP 能力包括:
|
|
98
|
+
|
|
99
|
+
- `new_game`
|
|
100
|
+
- `observe(session_id)`
|
|
101
|
+
- `take_action(session_id, choice, notes)`
|
|
102
|
+
- `check_latest_logs(session_id, count=10)`
|
|
103
|
+
- `show_employee_handbook()`
|
|
104
|
+
|
|
105
|
+
另外还提供日志资源:
|
|
106
|
+
|
|
107
|
+
- `logs://{session_id}`
|
|
108
|
+
|
|
109
|
+
### JiuwenSwarm
|
|
110
|
+
参考 [JiuwenSwarm 配置赛题模拟器 MCP](https://career-emulator.readthedocs.io/jiuwenswarm-mcp.html)
|
|
111
|
+
|
|
112
|
+
### 支持 MCPB 的 Agent(比如Claude)
|
|
113
|
+
如果你的 Agent 支持 [MCP Bundle](https://blog.modelcontextprotocol.io/posts/2025-11-20-adopting-mcpb),也可以生成本地安装包:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
make mcpb
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
然后双击打开直接安装。
|
|
120
|
+
|
|
121
|
+
### 其他 Agent
|
|
122
|
+
如果你想让其他 Agent 通过 MCP 来玩,可以直接使用 `stdio` 格式的 MCP 服务:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
uv sync --extra mcp
|
|
126
|
+
uv run career-emulator-mcp
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## 开发
|
|
131
|
+
|
|
132
|
+
仓库里常用的本地检查命令:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
make format
|
|
136
|
+
make lint
|
|
137
|
+
make test
|
|
138
|
+
make docstring
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
文档构建:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
make docs
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 文档
|
|
148
|
+
|
|
149
|
+
- 文档首页:[career-emulator.readthedocs.io](https://career-emulator.readthedocs.io/)
|
|
150
|
+
- 快速上手:[quickstart](https://career-emulator.readthedocs.io/quickstart.html)
|
|
151
|
+
- 公开手册:[handbook](https://career-emulator.readthedocs.io/handbook.html)
|
|
152
|
+
- 失败与结局:[failure-and-ending](https://career-emulator.readthedocs.io/failure-and-ending.html)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Career Emulator - CCF BDCI 2026
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/career-emulator/) [](https://github.com/Trenza1ore/TomatoEmulator/blob/main/LICENSE) [](https://github.com/Trenza1ore/TomatoEmulator/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
[](https://career-emulator.readthedocs.io)
|
|
6
|
+
|
|
7
|
+
- 所属赛题:`2026 CCF 大数据与计算智能大赛` 的《职场生存与晋升挑战》赛题,说明见[比赛官网](https://www.xir.cn/competition/1165),赛事入口见[2026 CCF 大数据与计算智能大赛](https://www.xir.cn/competition/races/BDCI2026)
|
|
8
|
+
- 参赛方式:使用 [JiuwenSwarm](https://openjiuwen.com/jiuwenswarm) 构建 Agent,并开发一个或多个 [Skill](https://agentskills.io)
|
|
9
|
+
- 交付形式:将一个或多个 Skill 文件夹打包成单个 `zip`
|
|
10
|
+
- 交互方式:通过 `career-emulator` MCP 与环境交互
|
|
11
|
+
|
|
12
|
+
如果你准备做多 Agent 协同参谋,也可以看看 [Swarm Skill 标准](https://swarmskills.openjiuwen.com)。
|
|
13
|
+
|
|
14
|
+
你会从 `L1` 新人入职开始,在 48 个月里处理剧情事件、分配季度体力、选择季度主行动,再按半年节奏迎接绩效和晋升。目标也很直接:别太早出局,顺手把这段职业生涯经营得稍微体面一点。
|
|
15
|
+
|
|
16
|
+
项目目前提供两种入口:
|
|
17
|
+
|
|
18
|
+
- `career-emulator`:本地 CLI,适合直接试玩和调规则。
|
|
19
|
+
- `career-emulator-mcp`:MCP 服务,适合接给 Agent 自动玩。
|
|
20
|
+
|
|
21
|
+
更多公开规则、手册和玩法说明见文档:[career-emulator.readthedocs.io](https://career-emulator.readthedocs.io)。
|
|
22
|
+
|
|
23
|
+
## 这题大概在玩什么
|
|
24
|
+
|
|
25
|
+
每个月,系统会生成剧情事件,你从当前选项里挑一个往下走。每到季度末,还要额外处理两件事:
|
|
26
|
+
|
|
27
|
+
- 分配 3 点 `Energy`
|
|
28
|
+
- 选择 1 个季度主行动
|
|
29
|
+
|
|
30
|
+
半年后会做一次绩效评估,职级、财富、健康、尊严、技能、人脉和产出会一起算总账。很多规则是公开的,很多坑只会通过反馈和日志给你一点暗示。组织很忙,通常不会为你逐条标红。
|
|
31
|
+
|
|
32
|
+
如果你只是想先记住一句话:
|
|
33
|
+
|
|
34
|
+
> 在 48 个月里别把自己玩出局,同时尽量把结局打成一段还能写进简历的经历。
|
|
35
|
+
|
|
36
|
+
## 快速开始
|
|
37
|
+
|
|
38
|
+
先安装依赖:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv sync
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
同步数据并开一局:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
make play
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
继续上一局:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run career-emulator play --session-id <session-id>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
CLI 里常用的几个输入:
|
|
57
|
+
|
|
58
|
+
- 输入数字:执行当前选项
|
|
59
|
+
- 输入 `handbook`:查看新员工手册
|
|
60
|
+
- 输入 `logs`:查看当前会话日志
|
|
61
|
+
- 输入 `quit`:退出
|
|
62
|
+
|
|
63
|
+
## 给 Agent 用
|
|
64
|
+
|
|
65
|
+
当前 MCP 能力包括:
|
|
66
|
+
|
|
67
|
+
- `new_game`
|
|
68
|
+
- `observe(session_id)`
|
|
69
|
+
- `take_action(session_id, choice, notes)`
|
|
70
|
+
- `check_latest_logs(session_id, count=10)`
|
|
71
|
+
- `show_employee_handbook()`
|
|
72
|
+
|
|
73
|
+
另外还提供日志资源:
|
|
74
|
+
|
|
75
|
+
- `logs://{session_id}`
|
|
76
|
+
|
|
77
|
+
### JiuwenSwarm
|
|
78
|
+
参考 [JiuwenSwarm 配置赛题模拟器 MCP](https://career-emulator.readthedocs.io/jiuwenswarm-mcp.html)
|
|
79
|
+
|
|
80
|
+
### 支持 MCPB 的 Agent(比如Claude)
|
|
81
|
+
如果你的 Agent 支持 [MCP Bundle](https://blog.modelcontextprotocol.io/posts/2025-11-20-adopting-mcpb),也可以生成本地安装包:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
make mcpb
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
然后双击打开直接安装。
|
|
88
|
+
|
|
89
|
+
### 其他 Agent
|
|
90
|
+
如果你想让其他 Agent 通过 MCP 来玩,可以直接使用 `stdio` 格式的 MCP 服务:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv sync --extra mcp
|
|
94
|
+
uv run career-emulator-mcp
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## 开发
|
|
99
|
+
|
|
100
|
+
仓库里常用的本地检查命令:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
make format
|
|
104
|
+
make lint
|
|
105
|
+
make test
|
|
106
|
+
make docstring
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
文档构建:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
make docs
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 文档
|
|
116
|
+
|
|
117
|
+
- 文档首页:[career-emulator.readthedocs.io](https://career-emulator.readthedocs.io/)
|
|
118
|
+
- 快速上手:[quickstart](https://career-emulator.readthedocs.io/quickstart.html)
|
|
119
|
+
- 公开手册:[handbook](https://career-emulator.readthedocs.io/handbook.html)
|
|
120
|
+
- 失败与结局:[failure-and-ending](https://career-emulator.readthedocs.io/failure-and-ending.html)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Career Emulator career simulation package."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Format readable action logs for Career Emulator sessions."""
|
|
2
|
+
|
|
3
|
+
from career_emulator.server.models import EventChoice, RegularActionDefinition, RegularDecisionEvent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def format_status_updates(updates: dict[str, int]) -> str:
|
|
7
|
+
"""Return a compact summary of configured status changes."""
|
|
8
|
+
if not updates:
|
|
9
|
+
return ""
|
|
10
|
+
parts: list[str] = []
|
|
11
|
+
for key, value in updates.items():
|
|
12
|
+
sign = "+" if value >= 0 else ""
|
|
13
|
+
parts.append(f"{key} {sign}{value}")
|
|
14
|
+
return ", ".join(parts)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _truncate(text: str, limit: int = 160) -> str:
|
|
18
|
+
"""Return text truncated for log output."""
|
|
19
|
+
compact = " ".join(text.split())
|
|
20
|
+
if len(compact) <= limit:
|
|
21
|
+
return compact
|
|
22
|
+
return f"{compact[: limit - 3]}..."
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_story_action_log(
|
|
26
|
+
month: int,
|
|
27
|
+
selected: EventChoice,
|
|
28
|
+
choice: int,
|
|
29
|
+
notes: str,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Format a story-event action log entry."""
|
|
32
|
+
lines = [
|
|
33
|
+
f"Month {month} | {selected.event_title}",
|
|
34
|
+
_truncate(selected.event_description),
|
|
35
|
+
f"Chose #{choice}: {selected.action}",
|
|
36
|
+
]
|
|
37
|
+
updates = format_status_updates(selected.status_updates)
|
|
38
|
+
if updates:
|
|
39
|
+
lines.append(updates)
|
|
40
|
+
if notes.strip():
|
|
41
|
+
lines.append(f"Notes: {notes.strip()}")
|
|
42
|
+
return "\n".join(lines)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_fixed_action_log(
|
|
46
|
+
month: int,
|
|
47
|
+
event: RegularDecisionEvent,
|
|
48
|
+
selected_action: RegularActionDefinition,
|
|
49
|
+
choice: int,
|
|
50
|
+
notes: str,
|
|
51
|
+
energy_remaining: int | None = None,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Format a quarterly fixed-event action log entry."""
|
|
54
|
+
lines = [
|
|
55
|
+
f"Month {month} | {event.event_name}",
|
|
56
|
+
_truncate(event.prompt),
|
|
57
|
+
f"Chose #{choice}: {selected_action.action}",
|
|
58
|
+
]
|
|
59
|
+
updates = format_status_updates(selected_action.status_updates)
|
|
60
|
+
if updates:
|
|
61
|
+
lines.append(updates)
|
|
62
|
+
if selected_action.energy_cost:
|
|
63
|
+
lines.append(f"Energy cost: {selected_action.energy_cost}")
|
|
64
|
+
if energy_remaining is not None:
|
|
65
|
+
lines.append(f"Energy remaining: {energy_remaining}")
|
|
66
|
+
if notes.strip():
|
|
67
|
+
lines.append(f"Notes: {notes.strip()}")
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_initialization_log() -> str:
|
|
72
|
+
"""Format the initialization acknowledgement log entry."""
|
|
73
|
+
return "Month 1 | Initialization\nAcknowledged the employee handbook and started the simulation."
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_terminal_node_log(
|
|
77
|
+
month: int,
|
|
78
|
+
event_title: str,
|
|
79
|
+
description: str,
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Format a terminal scenario node log entry."""
|
|
82
|
+
return "\n".join(
|
|
83
|
+
[
|
|
84
|
+
f"Month {month} | {event_title} completed",
|
|
85
|
+
_truncate(description),
|
|
86
|
+
]
|
|
87
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Human-playable command-line interface for Career Emulator."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from career_emulator.game import get_default_engine
|
|
8
|
+
from career_emulator.pretty_print import (
|
|
9
|
+
_blue,
|
|
10
|
+
_cyan,
|
|
11
|
+
_event_header,
|
|
12
|
+
_green,
|
|
13
|
+
_highlight,
|
|
14
|
+
_red,
|
|
15
|
+
_state_header,
|
|
16
|
+
_yellow,
|
|
17
|
+
)
|
|
18
|
+
from career_emulator.storage import update_dataset
|
|
19
|
+
|
|
20
|
+
DEBUG_MODE = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
"""Run the Career Emulator CLI."""
|
|
25
|
+
parser = argparse.ArgumentParser(description="Play Career Emulator from the terminal.")
|
|
26
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
27
|
+
play_parser = subparsers.add_parser("play", help="Start an interactive game.")
|
|
28
|
+
play_parser.add_argument("--session-id", default=None, help="Resume an existing session.")
|
|
29
|
+
subparsers.add_parser("update", help="Clone or force-update the event dataset.")
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
if args.command in {None, "play"}:
|
|
33
|
+
asyncio.run(play(session_id=args.session_id))
|
|
34
|
+
elif args.command == "update":
|
|
35
|
+
dataset_path = update_dataset()
|
|
36
|
+
print(_green(f"Dataset updated at: {dataset_path}"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def play(session_id: str | None = None) -> None:
|
|
40
|
+
"""Play or resume a Career Emulator session.
|
|
41
|
+
|
|
42
|
+
:param session_id: Optional session id to resume.
|
|
43
|
+
"""
|
|
44
|
+
engine = get_default_engine()
|
|
45
|
+
if session_id is None:
|
|
46
|
+
try:
|
|
47
|
+
response = await engine.new_game()
|
|
48
|
+
except ValueError as e:
|
|
49
|
+
raise RuntimeError("Perhaps the data files are missing, maybe try running\n career-emulator update") from e
|
|
50
|
+
session_id = response.session_id
|
|
51
|
+
print(_green(f"Started session: {session_id}"))
|
|
52
|
+
else:
|
|
53
|
+
print(_cyan(f"Resumed session: {session_id}"))
|
|
54
|
+
|
|
55
|
+
while True:
|
|
56
|
+
observation = await engine.observe(session_id)
|
|
57
|
+
state = observation.current_state
|
|
58
|
+
if "error" in state:
|
|
59
|
+
if observation.warning:
|
|
60
|
+
print("\n" + _highlight(observation.warning), end="\n\n")
|
|
61
|
+
if observation.ending_score is not None:
|
|
62
|
+
_print_ending_score(observation.ending_score)
|
|
63
|
+
print(_red(state["error"]))
|
|
64
|
+
return
|
|
65
|
+
_print_state(state)
|
|
66
|
+
if observation.warning:
|
|
67
|
+
print("\n" + _highlight(observation.warning), end="\n\n")
|
|
68
|
+
if not observation.choices:
|
|
69
|
+
print(_yellow("No available choices. The current prototype has no more events to resolve."))
|
|
70
|
+
return
|
|
71
|
+
_print_event(observation.current_event)
|
|
72
|
+
_print_choices(observation.choices)
|
|
73
|
+
invalid_choice = True
|
|
74
|
+
while invalid_choice:
|
|
75
|
+
raw_choice = input("Choice number, 'handbook', 'logs', or 'quit': ").strip()
|
|
76
|
+
if raw_choice.lower() in {"q", "quit", "exit"}:
|
|
77
|
+
return
|
|
78
|
+
if raw_choice.lower() == "handbook":
|
|
79
|
+
_print_handbook(await engine.enrollment_handbook())
|
|
80
|
+
invalid_choice = False
|
|
81
|
+
continue
|
|
82
|
+
if raw_choice.lower() == "logs":
|
|
83
|
+
print(await engine.logs_text(session_id))
|
|
84
|
+
invalid_choice = False
|
|
85
|
+
input(_highlight("> Press ENTER to continue..."))
|
|
86
|
+
continue
|
|
87
|
+
try:
|
|
88
|
+
choice = int(raw_choice)
|
|
89
|
+
invalid_choice = False
|
|
90
|
+
except ValueError:
|
|
91
|
+
print(_yellow("Please enter a choice number, 'handbook', 'logs', or 'quit'."))
|
|
92
|
+
if not invalid_choice:
|
|
93
|
+
result = await engine.take_action(session_id, choice, "")
|
|
94
|
+
invalid_choice = not result.success
|
|
95
|
+
if invalid_choice:
|
|
96
|
+
print(_red(f"Action failed: {result.error}"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _print_state(state: dict[str, Any]) -> None:
|
|
100
|
+
"""Print a compact visible state summary."""
|
|
101
|
+
print()
|
|
102
|
+
session_id = state["session_id"]
|
|
103
|
+
status, time_info = state["status"], state["time"]
|
|
104
|
+
month, quarter, year = time_info["current_month"], time_info["current_quarter"], time_info["current_year"]
|
|
105
|
+
statistics = state.get("statistics")
|
|
106
|
+
print(_state_header(" State "), _blue(f"Session: {session_id}"))
|
|
107
|
+
print(_state_header(" " + status["level"] + " ") + _cyan(f" Month {month} | Quarter {quarter} | Year {year}"))
|
|
108
|
+
print(_red("Energy {energy:3d} | Output {output:3d} | Wealth {wealth:4d}").format(**status))
|
|
109
|
+
print(
|
|
110
|
+
_green("Dignity {dignity:2d} | Health {health:3d} | Network {network:3d} | Skill {skill:3d}").format(**status)
|
|
111
|
+
)
|
|
112
|
+
if DEBUG_MODE and statistics:
|
|
113
|
+
print(_yellow(f"Extra statistics: {statistics}"))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _print_event(current_event: dict[str, Any] | None) -> None:
|
|
117
|
+
"""Print the current event."""
|
|
118
|
+
if current_event is None:
|
|
119
|
+
return
|
|
120
|
+
print()
|
|
121
|
+
print(_event_header(" Event "))
|
|
122
|
+
title, event_id = current_event["title"], current_event["event_id"]
|
|
123
|
+
node_id = current_event.get("node_id", "scripted")
|
|
124
|
+
print(_green(f"{title} ({event_id}:{node_id})"))
|
|
125
|
+
print(current_event["description"])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _print_choices(choices: list[dict[str, Any]]) -> None:
|
|
129
|
+
"""Print currently available choices."""
|
|
130
|
+
print()
|
|
131
|
+
print(_highlight(" Choice "))
|
|
132
|
+
for choice in choices:
|
|
133
|
+
energy = int(choice.get("energy_cost", 0))
|
|
134
|
+
choice_idx = choice["choice"]
|
|
135
|
+
action = choice["action"]
|
|
136
|
+
print(_cyan(f"{choice_idx}. {action}") + (f" ({energy})" if energy else ""))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _print_handbook(handbook: str) -> None:
|
|
140
|
+
"""Print the initialization handbook."""
|
|
141
|
+
print()
|
|
142
|
+
print(_event_header(" Handbook "))
|
|
143
|
+
print(handbook)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _print_ending_score(ending_score: dict[str, Any]) -> None:
|
|
147
|
+
"""Print the ending score summary."""
|
|
148
|
+
print()
|
|
149
|
+
print(_event_header(" Ending Score "))
|
|
150
|
+
print(_event_header(" {grade}: {grade_meaning}").format(**ending_score))
|
|
151
|
+
outcome = "48-month completion" if ending_score.get("completed") else "Early elimination"
|
|
152
|
+
print(_cyan(f"{outcome} | Survived {ending_score['survival_months']} months"))
|
|
153
|
+
print(_green("Quantitative score: {quantitative_score:.1f}/100").format(**ending_score))
|
|
154
|
+
print(_yellow("Score breakdown: {competition_partial_score:.1f}/60").format(**ending_score))
|
|
155
|
+
for dimension in ending_score.get("dimensions", []):
|
|
156
|
+
print(
|
|
157
|
+
f" {dimension['label']}: {dimension['raw_value']:6.1f} → "
|
|
158
|
+
f"{dimension['normalized_score']:5.1f} (weight {dimension['weight']:.0%}, "
|
|
159
|
+
f"contribution {dimension['weighted_score']:4.1f})"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|