histrategy-agent 0.2.0__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.
- histrategy_agent-0.2.0/PKG-INFO +12 -0
- histrategy_agent-0.2.0/pyproject.toml +22 -0
- histrategy_agent-0.2.0/setup.cfg +4 -0
- histrategy_agent-0.2.0/src/histrategy_agent/__init__.py +25 -0
- histrategy_agent-0.2.0/src/histrategy_agent/format_engine.py +258 -0
- histrategy_agent-0.2.0/src/histrategy_agent/im_adapters/__init__.py +6 -0
- histrategy_agent-0.2.0/src/histrategy_agent/im_adapters/base.py +42 -0
- histrategy_agent-0.2.0/src/histrategy_agent/im_adapters/feishu.py +445 -0
- histrategy_agent-0.2.0/src/histrategy_agent/llm_adapter.py +188 -0
- histrategy_agent-0.2.0/src/histrategy_agent/multiplayer.py +262 -0
- histrategy_agent-0.2.0/src/histrategy_agent/session.py +418 -0
- histrategy_agent-0.2.0/src/histrategy_agent/state_bridge.py +560 -0
- histrategy_agent-0.2.0/src/histrategy_agent/turn_processor.py +543 -0
- histrategy_agent-0.2.0/src/histrategy_agent.egg-info/PKG-INFO +12 -0
- histrategy_agent-0.2.0/src/histrategy_agent.egg-info/SOURCES.txt +22 -0
- histrategy_agent-0.2.0/src/histrategy_agent.egg-info/dependency_links.txt +1 -0
- histrategy_agent-0.2.0/src/histrategy_agent.egg-info/requires.txt +8 -0
- histrategy_agent-0.2.0/src/histrategy_agent.egg-info/top_level.txt +1 -0
- histrategy_agent-0.2.0/tests/test_feishu_adapter.py +235 -0
- histrategy_agent-0.2.0/tests/test_format_engine.py +285 -0
- histrategy_agent-0.2.0/tests/test_multiplayer.py +302 -0
- histrategy_agent-0.2.0/tests/test_session.py +196 -0
- histrategy_agent-0.2.0/tests/test_state_bridge.py +187 -0
- histrategy_agent-0.2.0/tests/test_turn_processor.py +236 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: histrategy-agent
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Shared core for OpenClaw and Hermes Agent game skills — IM-native Three Kingdoms strategy
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: histrategy-engine>=0.1.0
|
|
8
|
+
Provides-Extra: llm
|
|
9
|
+
Requires-Dist: httpx>=0.27; extra == "llm"
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=75"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "histrategy-agent"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Shared core for OpenClaw and Hermes Agent game skills — IM-native Three Kingdoms strategy"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
dependencies = ["histrategy-engine>=0.1.0"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
llm = ["httpx>=0.27"]
|
|
15
|
+
dev = ["pytest>=8", "pytest-cov>=5"]
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
addopts = ["-v", "--tb=short"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
histrategy-agent — Shared core for OpenClaw and Hermes Agent game skills.
|
|
3
|
+
|
|
4
|
+
Bridges IM chat messages to the histrategy-engine deterministic engines.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .format_engine import FormatEngine
|
|
8
|
+
from .multiplayer import GamePhase, MultiplayerSession, PlayerSlot
|
|
9
|
+
from .session import GameSession, GameSessionManager
|
|
10
|
+
from .state_bridge import StateBridge
|
|
11
|
+
from .turn_processor import TurnProcessor
|
|
12
|
+
from .turn_processor import TurnResult as ProcessorTurnResult
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GameSession",
|
|
16
|
+
"GameSessionManager",
|
|
17
|
+
"TurnProcessor",
|
|
18
|
+
"ProcessorTurnResult",
|
|
19
|
+
"StateBridge",
|
|
20
|
+
"FormatEngine",
|
|
21
|
+
"GamePhase",
|
|
22
|
+
"MultiplayerSession",
|
|
23
|
+
"PlayerSlot",
|
|
24
|
+
]
|
|
25
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FormatEngine — renders game output as platform-specific text/cards.
|
|
3
|
+
|
|
4
|
+
All user-facing text is in Chinese (Simplified) by default.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from histrategy_engine import CombatResult, WorldState
|
|
13
|
+
|
|
14
|
+
from .session import GameSession
|
|
15
|
+
from .turn_processor import TurnResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FormatEngine:
|
|
19
|
+
"""Formats game output for different IM platforms."""
|
|
20
|
+
|
|
21
|
+
def render_turn_result(self, result: TurnResult, platform: str = "feishu") -> str:
|
|
22
|
+
"""Render complete turn output as markdown text for the platform."""
|
|
23
|
+
ws = result.world_snapshot
|
|
24
|
+
year = ws.get("year", 207)
|
|
25
|
+
season = ws.get("season", "春")
|
|
26
|
+
turn = ws.get("turn", 1)
|
|
27
|
+
|
|
28
|
+
lines = []
|
|
29
|
+
lines.append(f"🎌 **建安{year}年 · {season}** | 回合 #{turn}")
|
|
30
|
+
lines.append("")
|
|
31
|
+
lines.append(f"> {result.narrative}")
|
|
32
|
+
lines.append("")
|
|
33
|
+
|
|
34
|
+
if result.map_ascii:
|
|
35
|
+
lines.append("🗺️ **天下大势**")
|
|
36
|
+
lines.append("```")
|
|
37
|
+
lines.append(result.map_ascii)
|
|
38
|
+
lines.append("```")
|
|
39
|
+
lines.append("")
|
|
40
|
+
|
|
41
|
+
lines.append("⚔️ **我军态势**")
|
|
42
|
+
territories = "、".join(t.get("name", "") for t in ws.get("territories", [])) or "无"
|
|
43
|
+
lines.append(f"| 领地 | {territories} |")
|
|
44
|
+
lines.append(f"| 兵力 | {ws.get('total_troops', 0):,} |")
|
|
45
|
+
lines.append(f"| 粮草 | {ws.get('food', 0):,} |")
|
|
46
|
+
lines.append(f"| 声望 | {ws.get('prestige', 0)} |")
|
|
47
|
+
lines.append(f"| 金库 | {ws.get('treasury', 0):,} |")
|
|
48
|
+
lines.append("")
|
|
49
|
+
|
|
50
|
+
if result.events:
|
|
51
|
+
lines.append("📜 **本回合事件**")
|
|
52
|
+
for event in result.events[:10]:
|
|
53
|
+
lines.append(f"- {event}")
|
|
54
|
+
lines.append("")
|
|
55
|
+
|
|
56
|
+
if result.suggestions:
|
|
57
|
+
lines.append("📋 **可选行动**")
|
|
58
|
+
for i, s in enumerate(result.suggestions, 1):
|
|
59
|
+
lines.append(f"{i}. {s}")
|
|
60
|
+
lines.append("")
|
|
61
|
+
|
|
62
|
+
return "\n".join(lines).strip()
|
|
63
|
+
|
|
64
|
+
def render_state_summary(self, session: GameSession) -> str:
|
|
65
|
+
"""Render faction overview + territory list + military strength."""
|
|
66
|
+
ws = session.world_state
|
|
67
|
+
faction = ws.factions.get(session.player_faction_id)
|
|
68
|
+
if not faction:
|
|
69
|
+
return "无存档数据"
|
|
70
|
+
|
|
71
|
+
lines = []
|
|
72
|
+
lines.append(f"🎌 **{faction.name}** | 建安{ws.year}年 · {ws.season.cn} | 回合 #{ws.turn_number}")
|
|
73
|
+
lines.append("")
|
|
74
|
+
|
|
75
|
+
# Territories
|
|
76
|
+
lines.append("🏰 **领地**")
|
|
77
|
+
for tid in faction.territories:
|
|
78
|
+
territory = ws.territories.get(tid)
|
|
79
|
+
if territory:
|
|
80
|
+
lines.append(
|
|
81
|
+
f"- {territory.name} | 人口: {territory.population:,} | "
|
|
82
|
+
f"发展: {territory.development} | 守军: {territory.garrison:,}"
|
|
83
|
+
)
|
|
84
|
+
lines.append("")
|
|
85
|
+
|
|
86
|
+
# Military
|
|
87
|
+
lines.append("⚔️ **军事**")
|
|
88
|
+
total_troops = 0
|
|
89
|
+
for army in ws.armies.values():
|
|
90
|
+
if army.faction_id == session.player_faction_id:
|
|
91
|
+
unit_desc = "、".join(f"{ut.value}{c}" for ut, c in army.units.items() if c > 0)
|
|
92
|
+
lines.append(
|
|
93
|
+
f"- {army.id} 位置: {army.location} | "
|
|
94
|
+
f"兵力: {army.total_troops:,} ({unit_desc}) | "
|
|
95
|
+
f"士气: {army.morale}"
|
|
96
|
+
)
|
|
97
|
+
total_troops += army.total_troops
|
|
98
|
+
if total_troops == 0:
|
|
99
|
+
lines.append("- 暂无军队")
|
|
100
|
+
lines.append(f"**总兵力: {total_troops:,}**")
|
|
101
|
+
lines.append("")
|
|
102
|
+
|
|
103
|
+
# Resources
|
|
104
|
+
lines.append("💰 **资源**")
|
|
105
|
+
lines.append(f"- 金库: {faction.treasury:,}")
|
|
106
|
+
lines.append(f"- 粮草: {faction.food:,}")
|
|
107
|
+
lines.append(f"- 声望: {faction.prestige}")
|
|
108
|
+
lines.append(f"- 税率: {faction.tax_rate:.0%}")
|
|
109
|
+
lines.append("")
|
|
110
|
+
|
|
111
|
+
# Diplomacy
|
|
112
|
+
lines.append("🤝 **外交**")
|
|
113
|
+
if faction.allies:
|
|
114
|
+
for aid in faction.allies:
|
|
115
|
+
ally = ws.factions.get(aid)
|
|
116
|
+
lines.append(f"- 盟友: {ally.name if ally else aid}")
|
|
117
|
+
else:
|
|
118
|
+
lines.append("- 暂无盟友")
|
|
119
|
+
if faction.enemies:
|
|
120
|
+
for eid in faction.enemies:
|
|
121
|
+
enemy = ws.factions.get(eid)
|
|
122
|
+
lines.append(f"- 敌对: {enemy.name if enemy else eid}")
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
return "\n".join(lines).strip()
|
|
126
|
+
|
|
127
|
+
def render_map_ascii(self, world_state: WorldState, faction_id: str) -> str:
|
|
128
|
+
"""Render a simplified ASCII map showing controlled territories."""
|
|
129
|
+
faction_symbols = {
|
|
130
|
+
"shu": "蜀",
|
|
131
|
+
"cao": "魏",
|
|
132
|
+
"wu": "吴",
|
|
133
|
+
"liubiao": "荆",
|
|
134
|
+
"liuzhang": "益",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
world_state.factions.get(faction_id)
|
|
138
|
+
|
|
139
|
+
lines = []
|
|
140
|
+
lines.append("天下大势图")
|
|
141
|
+
lines.append("─" * 40)
|
|
142
|
+
|
|
143
|
+
for _tid, territory in sorted(world_state.territories.items()):
|
|
144
|
+
owner = territory.owner_id
|
|
145
|
+
symbol = faction_symbols.get(owner, "·")
|
|
146
|
+
name = territory.name
|
|
147
|
+
neighbors = " → ".join(
|
|
148
|
+
world_state.territories[n].name if n in world_state.territories else n for n in territory.neighbors[:2]
|
|
149
|
+
)
|
|
150
|
+
lines.append(f"[{symbol}] {name:<6} → {neighbors}")
|
|
151
|
+
|
|
152
|
+
lines.append("─" * 40)
|
|
153
|
+
lines.append("蜀=刘备 魏=曹操 吴=孙权 荆=刘表 益=刘璋")
|
|
154
|
+
return "\n".join(lines)
|
|
155
|
+
|
|
156
|
+
def render_battle_card(self, battle: CombatResult) -> str:
|
|
157
|
+
"""Render a battle report as markdown."""
|
|
158
|
+
result_cn = {
|
|
159
|
+
"decisive_victory": "大胜",
|
|
160
|
+
"victory": "胜利",
|
|
161
|
+
"draw": "平局",
|
|
162
|
+
"defeat": "败北",
|
|
163
|
+
"decisive_defeat": "大败",
|
|
164
|
+
}
|
|
165
|
+
result_text = result_cn.get(
|
|
166
|
+
battle.result.value if hasattr(battle.result, "value") else str(battle.result),
|
|
167
|
+
str(battle.result),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
lines = []
|
|
171
|
+
lines.append(f"⚔️ **战斗报告** — {battle.location}")
|
|
172
|
+
lines.append("")
|
|
173
|
+
lines.append("| 内容 | 详情 |")
|
|
174
|
+
lines.append("|------|------|")
|
|
175
|
+
lines.append(f"| 攻击方 | {battle.attacker_id} |")
|
|
176
|
+
lines.append(f"| 防守方 | {battle.defender_id} |")
|
|
177
|
+
lines.append(f"| 结果 | **{result_text}** |")
|
|
178
|
+
|
|
179
|
+
if battle.attacker_casualties:
|
|
180
|
+
atk_loss = sum(battle.attacker_casualties.values())
|
|
181
|
+
lines.append(f"| 攻击方损失 | {atk_loss:,} |")
|
|
182
|
+
|
|
183
|
+
if battle.defender_casualties:
|
|
184
|
+
def_loss = sum(battle.defender_casualties.values())
|
|
185
|
+
lines.append(f"| 防守方损失 | {def_loss:,} |")
|
|
186
|
+
|
|
187
|
+
if battle.territory_captured:
|
|
188
|
+
lines.append(f"| 领土占领 | 成功占领{battle.location} |")
|
|
189
|
+
|
|
190
|
+
lines.append("")
|
|
191
|
+
return "\n".join(lines)
|
|
192
|
+
|
|
193
|
+
def render_onboarding(self, session: GameSession) -> str:
|
|
194
|
+
"""Render new game welcome message with faction intro and starting state."""
|
|
195
|
+
faction = session.world_state.factions.get(session.player_faction_id)
|
|
196
|
+
if not faction:
|
|
197
|
+
return "游戏初始化失败"
|
|
198
|
+
|
|
199
|
+
faction_intros = {
|
|
200
|
+
"shu": (
|
|
201
|
+
"汉室宗亲刘备,以仁义立世,心怀匡扶汉室之志。"
|
|
202
|
+
"目前寄居新野,兵微将寡,急需卧龙出山,谋定天下。"
|
|
203
|
+
"\n\n**初始领土**: 新野\n**大将**: 关羽、张飞、赵云"
|
|
204
|
+
),
|
|
205
|
+
"cao": (
|
|
206
|
+
"汉丞相曹操,雄才大略,挟天子以令诸侯。"
|
|
207
|
+
"拥兖豫之地,兵精粮足,虎视天下。"
|
|
208
|
+
"\n\n**初始领土**: 许昌、宛城、洛阳、邺城等\n**大将**: 夏侯渊、张郃、张辽、司马懿"
|
|
209
|
+
),
|
|
210
|
+
"wu": (
|
|
211
|
+
"江东孙权,继承父兄基业,坐断东南。"
|
|
212
|
+
"水军强盛,人才济济,伺机北伐中原。"
|
|
213
|
+
"\n\n**初始领土**: 建业、柴桑、吴郡\n**大将**: 周瑜、鲁肃、吕蒙、陆逊"
|
|
214
|
+
),
|
|
215
|
+
"liubiao": (
|
|
216
|
+
"汉室宗亲刘表,荆州牧,据守荆襄九郡。"
|
|
217
|
+
"坐拥江汉富庶之地,兵精粮足,然年事已高,"
|
|
218
|
+
"外有曹操虎视,内有蔡氏弄权,需谨慎周旋。"
|
|
219
|
+
"\n\n**初始领土**: 襄阳、江陵\n**大将**: 蒯越、蔡瑁、黄祖"
|
|
220
|
+
),
|
|
221
|
+
"liuzhang": (
|
|
222
|
+
"汉室宗亲刘璋,益州牧,据守天府之国。"
|
|
223
|
+
"蜀地险要,易守难攻,然刘璋暗弱,"
|
|
224
|
+
"内有张鲁之患,外不知天下之势。"
|
|
225
|
+
"\n\n**初始领土**: 成都、汉中\n**大将**: 张任、严颜、法正"
|
|
226
|
+
),
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
intro = faction_intros.get(
|
|
230
|
+
session.player_faction_id,
|
|
231
|
+
f"欢迎来到三國志略!您将扮演{faction.name},在乱世中争霸天下。",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
lines = []
|
|
235
|
+
lines.append("🎌 **三國志略** — 新游戏开始!")
|
|
236
|
+
lines.append("")
|
|
237
|
+
lines.append(f"**{faction.name}势力**")
|
|
238
|
+
lines.append("")
|
|
239
|
+
lines.append(intro)
|
|
240
|
+
lines.append("")
|
|
241
|
+
lines.append("📅 **建安207年 · 冬**")
|
|
242
|
+
lines.append(f"💰 金库: {faction.treasury:,} | 🌾 粮草: {faction.food:,} | ⭐ 声望: {faction.prestige}")
|
|
243
|
+
lines.append("")
|
|
244
|
+
lines.append("💡 **玩法提示**")
|
|
245
|
+
lines.append("- 用自然语言下达指令,如「进攻洛阳」「招募步兵」")
|
|
246
|
+
lines.append("- 输入「状态」查看当前局势")
|
|
247
|
+
lines.append("- 每回合都有建议行动供你选择")
|
|
248
|
+
lines.append("")
|
|
249
|
+
lines.append("─── 汉室倾颓,奸臣当道。将军可愿匡扶天下?───")
|
|
250
|
+
|
|
251
|
+
return "\n".join(lines)
|
|
252
|
+
|
|
253
|
+
def render_suggestions(self, suggestions: list[str]) -> str:
|
|
254
|
+
"""Format suggestions as a numbered list."""
|
|
255
|
+
lines = ["📋 **可选行动**"]
|
|
256
|
+
for i, s in enumerate(suggestions, 1):
|
|
257
|
+
lines.append(f"{i}. {s}")
|
|
258
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IMAdapter — abstract base class for platform-specific message formatting.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IMAdapter(ABC):
|
|
11
|
+
"""Abstract base for platform-specific message formatting."""
|
|
12
|
+
|
|
13
|
+
MAX_MESSAGE_LENGTH: int = 15000
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def format_message(self, content: str) -> dict:
|
|
17
|
+
"""Format content for the platform's message format."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def format_error(self, error_message: str) -> dict:
|
|
21
|
+
"""Format an error message."""
|
|
22
|
+
|
|
23
|
+
def split_long_message(self, content: str) -> list[str]:
|
|
24
|
+
"""Split content that exceeds MAX_MESSAGE_LENGTH."""
|
|
25
|
+
if len(content) <= self.MAX_MESSAGE_LENGTH:
|
|
26
|
+
return [content]
|
|
27
|
+
|
|
28
|
+
chunks = []
|
|
29
|
+
start = 0
|
|
30
|
+
while start < len(content):
|
|
31
|
+
end = start + self.MAX_MESSAGE_LENGTH
|
|
32
|
+
# Try to split at a natural boundary
|
|
33
|
+
if end < len(content):
|
|
34
|
+
# Look for last newline or period near the limit
|
|
35
|
+
for ch in ["\n\n", "\n", "。", ",", " "]:
|
|
36
|
+
last = content.rfind(ch, start, end)
|
|
37
|
+
if last > start + self.MAX_MESSAGE_LENGTH // 2:
|
|
38
|
+
end = last + len(ch)
|
|
39
|
+
break
|
|
40
|
+
chunks.append(content[start:end])
|
|
41
|
+
start = end
|
|
42
|
+
return chunks
|