histrategy-sdk 0.1.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.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: histrategy-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for 三國志略 (Histrategy) — AI-powered Three Kingdoms strategy game engine
5
+ Author: Emergence Science
6
+ License-Expression: MIT
7
+ Keywords: three-kingdoms,strategy-game,ai-game,llm,deepseek
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Games/Entertainment :: Turn Based Strategy
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: httpx>=0.27
16
+ Provides-Extra: engine
17
+ Requires-Dist: histrategy-engine>=0.1.0; extra == "engine"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: pytest-cov>=5; extra == "dev"
21
+
22
+ # histrategy-sdk
23
+
24
+ Python SDK for [三國志略 (Histrategy)](https://emergence.science/play/histrategy) — AI-powered Three Kingdoms strategy game.
25
+
26
+ ```bash
27
+ pip install histrategy-sdk
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### Server Client (HTTP)
33
+
34
+ ```python
35
+ from histrategy_sdk import ServerClient
36
+
37
+ client = ServerClient()
38
+
39
+ # Create a new game as Shu Han (刘备)
40
+ game = client.create_game(faction="shu")
41
+ print(game["narrative"])
42
+ # → "建安十二年冬,刘备屯兵新野,寄居刘表麾下..."
43
+
44
+ # Get strategic suggestions
45
+ plan = client.get_plan(game["game_id"])
46
+ for s in plan["suggestions"]:
47
+ print(f" • {s}")
48
+
49
+ # Execute a command
50
+ result = client.execute_command(game["game_id"], "联吴抗曹,攻打襄阳")
51
+ print(result["narrative"])
52
+ print(f"Token usage: {result['token_usage']}")
53
+ ```
54
+
55
+ ### Direct Engine (In-Process)
56
+
57
+ ```bash
58
+ pip install histrategy-sdk[engine]
59
+ ```
60
+
61
+ ```python
62
+ from histrategy_sdk import DirectEngine
63
+
64
+ engine = DirectEngine(faction="cao") # Play as 曹操
65
+ intro = engine.get_intro()
66
+ result = engine.execute("南征刘备,先取新野")
67
+
68
+ # Save game state
69
+ save = engine.to_dict()
70
+
71
+ # Restore later
72
+ engine2 = DirectEngine.from_dict(save)
73
+ ```
74
+
75
+ ## API Reference
76
+
77
+ ### `ServerClient`
78
+
79
+ | Method | Description |
80
+ |--------|-------------|
81
+ | `create_game(faction, scenario)` | Create new game → `GameIntro` |
82
+ | `get_plan(game_id)` | Get advisor suggestions → `PlanData` |
83
+ | `execute_command(game_id, decision)` | Process turn → `TurnResult` |
84
+ | `get_status(game_id)` | Get faction resources → `FactionStatus` |
85
+ | `restore_game(world_state)` | Restore from save → `RestoreResult` |
86
+ | `health()` | Check server status |
87
+
88
+ ### `DirectEngine`
89
+
90
+ | Method | Description |
91
+ |--------|-------------|
92
+ | `DirectEngine(faction, llm_api_key)` | Create new in-process engine |
93
+ | `get_intro()` | Get intro scene → `GameIntro` |
94
+ | `get_plan()` | Get suggestions → `PlanData` |
95
+ | `execute(decision)` | Process turn → `TurnResult` |
96
+ | `get_status()` | Get faction status → `FactionStatus` |
97
+ | `to_dict()` | Serialize game state |
98
+ | `DirectEngine.from_dict(data)` | Restore from saved state |
99
+
100
+ ### `TurnResult`
101
+
102
+ | Field | Type | Description |
103
+ |-------|------|-------------|
104
+ | `narrative` | `str` | AI-generated historical chronicle |
105
+ | `aftermath` | `str` | Resource changes summary |
106
+ | `state_changes` | `dict` | Numerical state deltas |
107
+ | `new_suggestions` | `list[str]` | Next-turn strategy suggestions |
108
+ | `token_usage` | `TokenUsage` | LLM token consumption |
109
+ | `game_over` | `dict \| None` | Victory/defeat message |
110
+ | `faction_status` | `FactionStatus` | Current resources and territories |
111
+
112
+ ## License
113
+
114
+ MIT — Emergence Science
@@ -0,0 +1,93 @@
1
+ # histrategy-sdk
2
+
3
+ Python SDK for [三國志略 (Histrategy)](https://emergence.science/play/histrategy) — AI-powered Three Kingdoms strategy game.
4
+
5
+ ```bash
6
+ pip install histrategy-sdk
7
+ ```
8
+
9
+ ## Quick Start
10
+
11
+ ### Server Client (HTTP)
12
+
13
+ ```python
14
+ from histrategy_sdk import ServerClient
15
+
16
+ client = ServerClient()
17
+
18
+ # Create a new game as Shu Han (刘备)
19
+ game = client.create_game(faction="shu")
20
+ print(game["narrative"])
21
+ # → "建安十二年冬,刘备屯兵新野,寄居刘表麾下..."
22
+
23
+ # Get strategic suggestions
24
+ plan = client.get_plan(game["game_id"])
25
+ for s in plan["suggestions"]:
26
+ print(f" • {s}")
27
+
28
+ # Execute a command
29
+ result = client.execute_command(game["game_id"], "联吴抗曹,攻打襄阳")
30
+ print(result["narrative"])
31
+ print(f"Token usage: {result['token_usage']}")
32
+ ```
33
+
34
+ ### Direct Engine (In-Process)
35
+
36
+ ```bash
37
+ pip install histrategy-sdk[engine]
38
+ ```
39
+
40
+ ```python
41
+ from histrategy_sdk import DirectEngine
42
+
43
+ engine = DirectEngine(faction="cao") # Play as 曹操
44
+ intro = engine.get_intro()
45
+ result = engine.execute("南征刘备,先取新野")
46
+
47
+ # Save game state
48
+ save = engine.to_dict()
49
+
50
+ # Restore later
51
+ engine2 = DirectEngine.from_dict(save)
52
+ ```
53
+
54
+ ## API Reference
55
+
56
+ ### `ServerClient`
57
+
58
+ | Method | Description |
59
+ |--------|-------------|
60
+ | `create_game(faction, scenario)` | Create new game → `GameIntro` |
61
+ | `get_plan(game_id)` | Get advisor suggestions → `PlanData` |
62
+ | `execute_command(game_id, decision)` | Process turn → `TurnResult` |
63
+ | `get_status(game_id)` | Get faction resources → `FactionStatus` |
64
+ | `restore_game(world_state)` | Restore from save → `RestoreResult` |
65
+ | `health()` | Check server status |
66
+
67
+ ### `DirectEngine`
68
+
69
+ | Method | Description |
70
+ |--------|-------------|
71
+ | `DirectEngine(faction, llm_api_key)` | Create new in-process engine |
72
+ | `get_intro()` | Get intro scene → `GameIntro` |
73
+ | `get_plan()` | Get suggestions → `PlanData` |
74
+ | `execute(decision)` | Process turn → `TurnResult` |
75
+ | `get_status()` | Get faction status → `FactionStatus` |
76
+ | `to_dict()` | Serialize game state |
77
+ | `DirectEngine.from_dict(data)` | Restore from saved state |
78
+
79
+ ### `TurnResult`
80
+
81
+ | Field | Type | Description |
82
+ |-------|------|-------------|
83
+ | `narrative` | `str` | AI-generated historical chronicle |
84
+ | `aftermath` | `str` | Resource changes summary |
85
+ | `state_changes` | `dict` | Numerical state deltas |
86
+ | `new_suggestions` | `list[str]` | Next-turn strategy suggestions |
87
+ | `token_usage` | `TokenUsage` | LLM token consumption |
88
+ | `game_over` | `dict \| None` | Victory/defeat message |
89
+ | `faction_status` | `FactionStatus` | Current resources and territories |
90
+
91
+ ## License
92
+
93
+ MIT — Emergence Science
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "histrategy-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for 三國志略 (Histrategy) — AI-powered Three Kingdoms strategy game engine"
9
+ requires-python = ">=3.11"
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ authors = [{name = "Emergence Science"}]
13
+ keywords = ["three-kingdoms", "strategy-game", "ai-game", "llm", "deepseek"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Games/Entertainment :: Turn Based Strategy",
20
+ ]
21
+
22
+ dependencies = [
23
+ "httpx>=0.27",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ engine = ["histrategy-engine>=0.1.0"]
28
+ dev = ["pytest>=8", "pytest-cov>=5"]
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ addopts = ["-v", "--tb=short"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,79 @@
1
+ """histrategy-sdk — Python SDK for 三國志略 (Histrategy).
2
+
3
+ 三國志略 is an AI-powered Three Kingdoms strategy game where the LLM acts
4
+ as the game engine — generating advisor speeches, strategic suggestions,
5
+ consequences, and NPC actions based on actual world state.
6
+
7
+ Quick Start
8
+ -----------
9
+
10
+ **Option A: Remote Server (lightweight, no engine deps)**
11
+
12
+ from histrategy_sdk import ServerClient
13
+
14
+ client = ServerClient()
15
+ game = client.create_game(faction="shu")
16
+ result = client.execute_command(game["game_id"], "联吴抗曹,攻打襄阳")
17
+ print(result["narrative"])
18
+
19
+ **Option B: Direct Engine (in-process, needs histrategy-engine)**
20
+
21
+ pip install histrategy-sdk[engine]
22
+
23
+ from histrategy_sdk import DirectEngine
24
+
25
+ engine = DirectEngine(faction="shu")
26
+ intro = engine.get_intro()
27
+ result = engine.execute("联吴抗曹")
28
+ print(result["narrative"])
29
+
30
+ # Save and restore
31
+ data = engine.to_dict()
32
+ engine2 = DirectEngine.from_dict(data)
33
+ """
34
+
35
+ from ._client import ServerClient
36
+ from .exceptions import (
37
+ APIError,
38
+ ConnectionError,
39
+ EngineNotAvailableError,
40
+ GameNotFoundError,
41
+ HistrategyError,
42
+ TurnExecutionError,
43
+ )
44
+ from .types import (
45
+ FactionStatus,
46
+ GameIntro,
47
+ PlanData,
48
+ RestoreResult,
49
+ TokenUsage,
50
+ TurnResult,
51
+ )
52
+
53
+ # DirectEngine is optional — import fails gracefully if histrategy not installed
54
+ try:
55
+ from ._engine import DirectEngine
56
+ except ImportError:
57
+ DirectEngine = None # type: ignore[assignment]
58
+
59
+ __all__ = [
60
+ # Client
61
+ "ServerClient",
62
+ # Engine (optional)
63
+ "DirectEngine",
64
+ # Types
65
+ "FactionStatus",
66
+ "GameIntro",
67
+ "PlanData",
68
+ "RestoreResult",
69
+ "TokenUsage",
70
+ "TurnResult",
71
+ # Exceptions
72
+ "HistrategyError",
73
+ "GameNotFoundError",
74
+ "ConnectionError",
75
+ "APIError",
76
+ "EngineNotAvailableError",
77
+ "TurnExecutionError",
78
+ ]
79
+ __version__ = "0.1.0"
@@ -0,0 +1,230 @@
1
+ """ServerClient — HTTP client for remote histrategy server.
2
+
3
+ Use this when the game engine runs on a separate server (Railway, etc.).
4
+ Lightweight: only depends on httpx.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ from .exceptions import APIError, ConnectionError, GameNotFoundError
14
+ from .types import (
15
+ FactionStatus,
16
+ GameIntro,
17
+ PlanData,
18
+ RestoreResult,
19
+ TurnResult,
20
+ )
21
+
22
+
23
+ class ServerClient:
24
+ """HTTP client for a remote histrategy game server.
25
+
26
+ Usage:
27
+ client = ServerClient(base_url="https://histrategy.example.com")
28
+ game = client.create_game(faction="shu")
29
+ result = client.execute_command(game["game_id"], "联吴抗曹")
30
+ print(result["narrative"])
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ base_url: str = "https://histrategy-emergence.railway.app",
36
+ timeout: float = 120.0,
37
+ ):
38
+ self.base_url = base_url.rstrip("/")
39
+ self._client = httpx.Client(timeout=timeout)
40
+
41
+ def close(self) -> None:
42
+ """Close the underlying HTTP client."""
43
+ self._client.close()
44
+
45
+ def __enter__(self) -> ServerClient:
46
+ return self
47
+
48
+ def __exit__(self, *args: Any) -> None:
49
+ self.close()
50
+
51
+ # ── HTTP helpers ──────────────────────────────────────
52
+
53
+ def _get(self, path: str) -> dict:
54
+ try:
55
+ r = self._client.get(f"{self.base_url}{path}")
56
+ except httpx.RequestError as e:
57
+ raise ConnectionError(f"Could not reach server: {e}") from e
58
+ if r.status_code == 404:
59
+ raise GameNotFoundError(f"Game not found at {path}")
60
+ if not r.is_success:
61
+ detail = ""
62
+ try:
63
+ detail = r.json().get("detail", r.text)
64
+ except Exception:
65
+ detail = r.text
66
+ raise APIError(r.status_code, detail)
67
+ return r.json()
68
+
69
+ def _post(self, path: str, json: dict | None = None) -> dict:
70
+ try:
71
+ r = self._client.post(f"{self.base_url}{path}", json=json)
72
+ except httpx.RequestError as e:
73
+ raise ConnectionError(f"Could not reach server: {e}") from e
74
+ if r.status_code == 404:
75
+ raise GameNotFoundError(f"Resource not found at {path}")
76
+ if not r.is_success:
77
+ detail = ""
78
+ try:
79
+ detail = r.json().get("detail", r.text)
80
+ except Exception:
81
+ detail = r.text
82
+ raise APIError(r.status_code, detail)
83
+ return r.json()
84
+
85
+ # ── Game API ──────────────────────────────────────────
86
+
87
+ def create_game(
88
+ self,
89
+ faction: str = "shu",
90
+ scenario: str = "207",
91
+ llm_api_key: str | None = None,
92
+ session_id: str | None = None,
93
+ language_style: str | None = None,
94
+ ) -> GameIntro:
95
+ """Create a new game and return the intro scene.
96
+
97
+ Args:
98
+ faction: "shu" (刘备), "cao" (曹操), or "wu" (孙权)
99
+ scenario: Scenario ID, currently only "207"
100
+ llm_api_key: User's own DeepSeek API key (not persisted)
101
+ session_id: Orchestrator session ID for persistence
102
+ language_style: "classical" (古文风) or "vernacular" (白话文)
103
+
104
+ Returns:
105
+ GameIntro with game_id, intro narrative, suggestions, faction status
106
+ """
107
+ body: dict[str, Any] = {"faction": faction, "scenario": scenario}
108
+ if llm_api_key:
109
+ body["llm_api_key"] = llm_api_key
110
+ if session_id:
111
+ body["session_id"] = session_id
112
+ if language_style:
113
+ body["language_style"] = language_style
114
+
115
+ data = self._post("/api/games", body)
116
+ intro = data.get("intro", {})
117
+ if isinstance(intro, dict):
118
+ narrative = intro.get("narrative", "")
119
+ suggestions = intro.get("new_choices", [])
120
+ else:
121
+ narrative = str(intro)
122
+ suggestions = []
123
+
124
+ return GameIntro(
125
+ game_id=data["game_id"],
126
+ scenario=data.get("scenario", scenario),
127
+ faction=data.get("faction", faction),
128
+ narrative=narrative,
129
+ suggestions=suggestions,
130
+ faction_status=FactionStatus(**data.get("faction_status", {})),
131
+ )
132
+
133
+ def get_status(self, game_id: str) -> FactionStatus:
134
+ """Get current faction status for a game.
135
+
136
+ Returns:
137
+ FactionStatus with strength, food, treasury, territories, etc.
138
+ """
139
+ data = self._get(f"/api/games/{game_id}")
140
+ return FactionStatus(**data.get("faction_status", {}))
141
+
142
+ def get_plan(self, game_id: str) -> PlanData:
143
+ """Get Plan Mode: advisor court dialogue + strategic suggestions.
144
+
145
+ Returns:
146
+ PlanData with court_dialogue, suggestions, faction_status
147
+ """
148
+ data = self._post(f"/api/games/{game_id}/plan")
149
+ return PlanData(
150
+ game_id=data["game_id"],
151
+ court_dialogue=data.get("court_dialogue", ""),
152
+ suggestions=data.get("suggestions", []),
153
+ season_summary=data.get("season_summary", ""),
154
+ year=data.get("year", 207),
155
+ season=data.get("season", "春"),
156
+ turn=data.get("turn", 1),
157
+ faction_status=FactionStatus(**data.get("faction_status", {})),
158
+ )
159
+
160
+ def execute_command(self, game_id: str, decision: str) -> TurnResult:
161
+ """Submit a player decision and process the turn.
162
+
163
+ Args:
164
+ game_id: Game ID from create_game / restore_game
165
+ decision: Free-text player decision (e.g. "联吴抗曹,攻打襄阳")
166
+
167
+ Returns:
168
+ TurnResult with narrative, aftermath, state_changes, suggestions, etc.
169
+ """
170
+ data = self._post(f"/api/games/{game_id}/command", {"decision": decision})
171
+
172
+ # Build token usage from response (may be empty if not tracked)
173
+ token_usage: dict[str, int] = {}
174
+ for key in ("command_tokens", "plan_tokens", "npc_tokens", "sim_tokens"):
175
+ token_usage[key] = data.get(key, 0)
176
+
177
+ from .types import TokenUsage as _TU
178
+
179
+ return TurnResult(
180
+ game_id=data["game_id"],
181
+ narrative=data.get("narrative", ""),
182
+ aftermath=data.get("aftermath", ""),
183
+ state_changes=data.get("state_changes", {}),
184
+ events_occurred=data.get("events_occurred", []),
185
+ npc_actions=data.get("npc_actions", []),
186
+ new_suggestions=data.get("new_suggestions", []),
187
+ game_over=data.get("game_over"),
188
+ faction_status=FactionStatus(**data.get("faction_status", {})),
189
+ year=data.get("year", 207),
190
+ season=data.get("season", "春"),
191
+ turn=data.get("turn", 1),
192
+ token_usage=_TU(**token_usage),
193
+ )
194
+
195
+ def restore_game(
196
+ self,
197
+ world_state: dict,
198
+ session_id: str | None = None,
199
+ llm_api_key: str | None = None,
200
+ ) -> RestoreResult:
201
+ """Restore a game from a previously saved world_state dict.
202
+
203
+ Args:
204
+ world_state: Full world_state dict (from engine.to_dict() or previous save)
205
+ session_id: Orchestrator session ID for persistence
206
+ llm_api_key: User's own DeepSeek API key
207
+
208
+ Returns:
209
+ RestoreResult with game_id and restored faction status
210
+ """
211
+ body: dict[str, Any] = {"world_state": world_state}
212
+ if session_id:
213
+ body["session_id"] = session_id
214
+ if llm_api_key:
215
+ body["llm_api_key"] = llm_api_key
216
+
217
+ data = self._post("/api/games/restore", body)
218
+ return RestoreResult(
219
+ game_id=data["game_id"],
220
+ scenario=data.get("scenario", "207"),
221
+ faction=data.get("faction", "?"),
222
+ faction_status=FactionStatus(**data.get("faction_status", {})),
223
+ restored=data.get("restored", False),
224
+ restored_turn=data.get("restored_turn", 1),
225
+ restored_year=data.get("restored_year", 207),
226
+ )
227
+
228
+ def health(self) -> dict:
229
+ """Check server health and LLM availability."""
230
+ return self._get("/api/health")
@@ -0,0 +1,270 @@
1
+ """DirectEngine — in-process game engine wrapper.
2
+
3
+ Use this when you have histrategy-engine installed locally.
4
+ No server needed — the game runs in your Python process.
5
+
6
+ Requires: pip install histrategy-sdk[engine]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .exceptions import EngineNotAvailableError, TurnExecutionError
12
+ from .types import FactionStatus, GameIntro, PlanData, TurnResult
13
+
14
+
15
+ class DirectEngine:
16
+ """In-process game engine for 三國志略.
17
+
18
+ Usage:
19
+ engine = DirectEngine(faction="shu")
20
+ intro = engine.get_intro()
21
+ plan = engine.get_plan()
22
+ result = engine.execute("联吴抗曹,攻打襄阳")
23
+ print(result["narrative"])
24
+
25
+ # Save and restore
26
+ data = engine.to_dict()
27
+ engine2 = DirectEngine.from_dict(data)
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ scenario: str = "207",
33
+ faction: str = "shu",
34
+ llm_api_key: str | None = None,
35
+ llm_provider: str | None = None,
36
+ ):
37
+ """Create a new game engine.
38
+
39
+ Args:
40
+ scenario: Scenario ID ("207")
41
+ faction: Player faction ("shu", "cao", "wu")
42
+ llm_api_key: API key for LLM provider (auto-detected if unset)
43
+ llm_provider: Override provider detection ("deepseek", "openai", "tongyi")
44
+ """
45
+ self._ensure_engine_available()
46
+
47
+ import os as _os
48
+
49
+ if llm_api_key:
50
+ _os.environ["DEEPSEEK_API_KEY"] = llm_api_key
51
+
52
+ from histrategy.engine.game import GameEngine
53
+ from histrategy.llm.adapter import LLMAdapter
54
+
55
+ try:
56
+ llm = LLMAdapter(provider=llm_provider or None)
57
+ except Exception:
58
+ llm = None
59
+
60
+ self._engine = GameEngine(scenario=scenario, new_game=True, llm=llm)
61
+ self._engine.set_player_faction(faction)
62
+ self._game_id = self._engine.world_state_v2.player_faction_id if self._engine._use_v2 else "local"
63
+
64
+ @staticmethod
65
+ def _ensure_engine_available() -> None:
66
+ try:
67
+ import histrategy.engine.game # noqa: F401
68
+ except ImportError:
69
+ raise EngineNotAvailableError(
70
+ "DirectEngine requires the full histrategy package.\n"
71
+ "Install with: pip install histrategy-sdk[engine]\n"
72
+ "Or use ServerClient for remote games."
73
+ ) from None
74
+
75
+ # ── Properties ────────────────────────────────────────
76
+
77
+ @property
78
+ def game_id(self) -> str:
79
+ return self._game_id
80
+
81
+ # ── Game API ──────────────────────────────────────────
82
+
83
+ def get_intro(self) -> GameIntro:
84
+ """Get the game intro scene.
85
+
86
+ Returns:
87
+ GameIntro with narrative, suggestions, faction_status.
88
+ """
89
+ from histrategy.engine.game import _suppress_stderr
90
+
91
+ with _suppress_stderr():
92
+ intro = self._engine.get_intro_scene()
93
+
94
+ ws = self._engine.world_state_v2
95
+
96
+ narrative = ""
97
+ suggestions: list[str] = []
98
+ if isinstance(intro, dict):
99
+ narrative = intro.get("narrative", "")
100
+ suggestions = intro.get("new_choices", [])
101
+ elif isinstance(intro, str):
102
+ narrative = intro
103
+
104
+ return GameIntro(
105
+ game_id=self._game_id,
106
+ scenario=self._engine.scenario,
107
+ faction=ws.player_faction_id,
108
+ narrative=narrative,
109
+ suggestions=suggestions,
110
+ faction_status=self._get_status_inner(),
111
+ )
112
+
113
+ def get_plan(self) -> PlanData:
114
+ """Get Plan Mode: advisor court dialogue + strategic suggestions.
115
+
116
+ Returns:
117
+ PlanData with court_dialogue, suggestions, faction_status.
118
+ """
119
+ from histrategy.engine.game import _suppress_stderr
120
+
121
+ with _suppress_stderr():
122
+ plan = self._engine.get_plan_data()
123
+
124
+ status = self._get_status_inner()
125
+
126
+ return PlanData(
127
+ game_id=self._game_id,
128
+ court_dialogue=plan.get("court_dialogue", ""),
129
+ suggestions=plan.get("suggestions", []),
130
+ season_summary=plan.get("season_summary", ""),
131
+ year=status.get("year", 207),
132
+ season=status.get("season", "春"),
133
+ turn=status.get("turn", 1),
134
+ faction_status=status,
135
+ )
136
+
137
+ def execute(self, decision: str) -> TurnResult:
138
+ """Submit a player decision and process the turn.
139
+
140
+ Args:
141
+ decision: Free-text player decision (e.g. "联吴抗曹,攻打襄阳")
142
+
143
+ Returns:
144
+ TurnResult with narrative, aftermath, state_changes, suggestions.
145
+ """
146
+ from histrategy.engine.game import _suppress_stderr
147
+
148
+ try:
149
+ with _suppress_stderr():
150
+ result = self._engine.process_turn(decision)
151
+ except Exception as e:
152
+ raise TurnExecutionError(f"Turn execution failed: {e}") from e
153
+
154
+ status = self._get_status_inner()
155
+
156
+ usage = result.get("_usage", {})
157
+ from .types import TokenUsage
158
+
159
+ return TurnResult(
160
+ game_id=self._game_id,
161
+ narrative=result.get("narrative", ""),
162
+ aftermath=result.get("aftermath", ""),
163
+ state_changes=result.get("state_changes", {}),
164
+ events_occurred=result.get("events_occurred", []),
165
+ npc_actions=result.get("npc_actions", result.get("npc_reactions", [])),
166
+ new_suggestions=result.get("new_choices", []),
167
+ game_over=result.get("game_over"),
168
+ faction_status=status,
169
+ year=status.get("year", 207),
170
+ season=status.get("season", "春"),
171
+ turn=status.get("turn", 1),
172
+ token_usage=TokenUsage(
173
+ command_tokens=usage.get("command_tokens", 0),
174
+ plan_tokens=usage.get("plan_tokens", 0),
175
+ npc_tokens=usage.get("npc_tokens", 0),
176
+ sim_tokens=usage.get("sim_tokens", 0),
177
+ ),
178
+ )
179
+
180
+ def get_status(self) -> FactionStatus:
181
+ """Get current faction status."""
182
+ return self._get_status_inner()
183
+
184
+ def to_dict(self) -> dict:
185
+ """Serialize the full game state to a JSON-safe dict.
186
+
187
+ Use with from_dict() to save and restore games.
188
+ """
189
+ return self._engine.to_dict()
190
+
191
+ @classmethod
192
+ def from_dict(
193
+ cls,
194
+ data: dict,
195
+ llm_api_key: str | None = None,
196
+ llm_provider: str | None = None,
197
+ ) -> DirectEngine:
198
+ """Restore a game from a previously saved world_state dict.
199
+
200
+ Args:
201
+ data: Full world_state dict (from to_dict())
202
+ llm_api_key: API key for LLM provider
203
+ llm_provider: Override provider detection
204
+
205
+ Returns:
206
+ DirectEngine with restored game state.
207
+ """
208
+ cls._ensure_engine_available()
209
+
210
+ import os as _os
211
+
212
+ if llm_api_key:
213
+ _os.environ["DEEPSEEK_API_KEY"] = llm_api_key
214
+
215
+ from histrategy.engine.game import GameEngine
216
+ from histrategy.llm.adapter import LLMAdapter
217
+
218
+ try:
219
+ llm = LLMAdapter(provider=llm_provider or None)
220
+ except Exception:
221
+ llm = None
222
+
223
+ engine = GameEngine.from_dict(data, llm=llm)
224
+ instance = cls.__new__(cls)
225
+ instance._engine = engine
226
+ instance._game_id = engine.world_state_v2.player_faction_id
227
+ return instance
228
+
229
+ # ── Internal ──────────────────────────────────────────
230
+
231
+ def _get_status_inner(self) -> FactionStatus:
232
+ """Extract faction status from the engine (mirrors server api.py)."""
233
+ ws = self._engine.world_state_v2
234
+ player = ws.factions.get(ws.player_faction_id)
235
+ if not player:
236
+ return FactionStatus(
237
+ name="?",
238
+ faction_id="?",
239
+ strength=0,
240
+ food=0,
241
+ treasury=0,
242
+ territories=[],
243
+ territory_names=[],
244
+ morale=0,
245
+ is_active=False,
246
+ year=ws.year,
247
+ season=ws.season.cn if hasattr(ws.season, "cn") else ws.season.value,
248
+ turn=ws.turn_number,
249
+ )
250
+
251
+ # Resolve territory names
252
+ territory_names = []
253
+ for tid in player.territories:
254
+ t = ws.territories.get(tid)
255
+ territory_names.append(t.name if t else tid)
256
+
257
+ return FactionStatus(
258
+ name=player.name,
259
+ faction_id=ws.player_faction_id,
260
+ strength=player.strength_actual,
261
+ food=player.food,
262
+ treasury=player.treasury,
263
+ territories=list(player.territories),
264
+ territory_names=territory_names,
265
+ morale=player.morale_actual,
266
+ is_active=player.is_active,
267
+ year=ws.year,
268
+ season=ws.season.cn if hasattr(ws.season, "cn") else ws.season.value,
269
+ turn=ws.turn_number,
270
+ )
@@ -0,0 +1,33 @@
1
+ """SDK-specific exceptions."""
2
+
3
+
4
+ class HistrategyError(Exception):
5
+ """Base exception for all SDK errors."""
6
+
7
+
8
+ class GameNotFoundError(HistrategyError):
9
+ """The requested game was not found (expired or never created)."""
10
+
11
+
12
+ class ConnectionError(HistrategyError):
13
+ """Could not connect to the histrategy server."""
14
+
15
+
16
+ class APIError(HistrategyError):
17
+ """Server returned an error response."""
18
+
19
+ def __init__(self, status_code: int, detail: str):
20
+ self.status_code = status_code
21
+ self.detail = detail
22
+ super().__init__(f"HTTP {status_code}: {detail}")
23
+
24
+
25
+ class EngineNotAvailableError(HistrategyError):
26
+ """DirectEngine requires histrategy-engine to be installed.
27
+
28
+ Install with: pip install histrategy-sdk[engine]
29
+ """
30
+
31
+
32
+ class TurnExecutionError(HistrategyError):
33
+ """Failed to process a game turn."""
@@ -0,0 +1,85 @@
1
+ """Type definitions for histrategy-sdk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TypedDict
6
+
7
+
8
+ class FactionStatus(TypedDict, total=False):
9
+ """Current status of the player's faction."""
10
+
11
+ name: str
12
+ faction_id: str
13
+ strength: int
14
+ food: int
15
+ treasury: int
16
+ territories: list[str]
17
+ territory_names: list[str]
18
+ morale: int
19
+ is_active: bool
20
+ year: int
21
+ season: str
22
+ turn: int
23
+
24
+
25
+ class GameIntro(TypedDict):
26
+ """Response from create_game / restore_game."""
27
+
28
+ game_id: str
29
+ scenario: str
30
+ faction: str
31
+ narrative: str
32
+ suggestions: list[str]
33
+ faction_status: FactionStatus
34
+
35
+
36
+ class PlanData(TypedDict):
37
+ """Response from get_plan."""
38
+
39
+ game_id: str
40
+ court_dialogue: str
41
+ suggestions: list[str]
42
+ season_summary: str
43
+ year: int
44
+ season: str
45
+ turn: int
46
+ faction_status: FactionStatus
47
+
48
+
49
+ class TurnResult(TypedDict):
50
+ """Response from execute_command."""
51
+
52
+ game_id: str
53
+ narrative: str
54
+ aftermath: str
55
+ state_changes: dict[str, int]
56
+ events_occurred: list[str]
57
+ npc_actions: list[str]
58
+ new_suggestions: list[str]
59
+ game_over: dict | None
60
+ faction_status: FactionStatus
61
+ year: int
62
+ season: str
63
+ turn: int
64
+ token_usage: TokenUsage
65
+
66
+
67
+ class TokenUsage(TypedDict, total=False):
68
+ """LLM token consumption for a turn."""
69
+
70
+ command_tokens: int
71
+ plan_tokens: int
72
+ npc_tokens: int
73
+ sim_tokens: int
74
+
75
+
76
+ class RestoreResult(TypedDict):
77
+ """Response from restore_game."""
78
+
79
+ game_id: str
80
+ scenario: str
81
+ faction: str
82
+ faction_status: FactionStatus
83
+ restored: bool
84
+ restored_turn: int
85
+ restored_year: int
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: histrategy-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for 三國志略 (Histrategy) — AI-powered Three Kingdoms strategy game engine
5
+ Author: Emergence Science
6
+ License-Expression: MIT
7
+ Keywords: three-kingdoms,strategy-game,ai-game,llm,deepseek
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Games/Entertainment :: Turn Based Strategy
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: httpx>=0.27
16
+ Provides-Extra: engine
17
+ Requires-Dist: histrategy-engine>=0.1.0; extra == "engine"
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: pytest-cov>=5; extra == "dev"
21
+
22
+ # histrategy-sdk
23
+
24
+ Python SDK for [三國志略 (Histrategy)](https://emergence.science/play/histrategy) — AI-powered Three Kingdoms strategy game.
25
+
26
+ ```bash
27
+ pip install histrategy-sdk
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### Server Client (HTTP)
33
+
34
+ ```python
35
+ from histrategy_sdk import ServerClient
36
+
37
+ client = ServerClient()
38
+
39
+ # Create a new game as Shu Han (刘备)
40
+ game = client.create_game(faction="shu")
41
+ print(game["narrative"])
42
+ # → "建安十二年冬,刘备屯兵新野,寄居刘表麾下..."
43
+
44
+ # Get strategic suggestions
45
+ plan = client.get_plan(game["game_id"])
46
+ for s in plan["suggestions"]:
47
+ print(f" • {s}")
48
+
49
+ # Execute a command
50
+ result = client.execute_command(game["game_id"], "联吴抗曹,攻打襄阳")
51
+ print(result["narrative"])
52
+ print(f"Token usage: {result['token_usage']}")
53
+ ```
54
+
55
+ ### Direct Engine (In-Process)
56
+
57
+ ```bash
58
+ pip install histrategy-sdk[engine]
59
+ ```
60
+
61
+ ```python
62
+ from histrategy_sdk import DirectEngine
63
+
64
+ engine = DirectEngine(faction="cao") # Play as 曹操
65
+ intro = engine.get_intro()
66
+ result = engine.execute("南征刘备,先取新野")
67
+
68
+ # Save game state
69
+ save = engine.to_dict()
70
+
71
+ # Restore later
72
+ engine2 = DirectEngine.from_dict(save)
73
+ ```
74
+
75
+ ## API Reference
76
+
77
+ ### `ServerClient`
78
+
79
+ | Method | Description |
80
+ |--------|-------------|
81
+ | `create_game(faction, scenario)` | Create new game → `GameIntro` |
82
+ | `get_plan(game_id)` | Get advisor suggestions → `PlanData` |
83
+ | `execute_command(game_id, decision)` | Process turn → `TurnResult` |
84
+ | `get_status(game_id)` | Get faction resources → `FactionStatus` |
85
+ | `restore_game(world_state)` | Restore from save → `RestoreResult` |
86
+ | `health()` | Check server status |
87
+
88
+ ### `DirectEngine`
89
+
90
+ | Method | Description |
91
+ |--------|-------------|
92
+ | `DirectEngine(faction, llm_api_key)` | Create new in-process engine |
93
+ | `get_intro()` | Get intro scene → `GameIntro` |
94
+ | `get_plan()` | Get suggestions → `PlanData` |
95
+ | `execute(decision)` | Process turn → `TurnResult` |
96
+ | `get_status()` | Get faction status → `FactionStatus` |
97
+ | `to_dict()` | Serialize game state |
98
+ | `DirectEngine.from_dict(data)` | Restore from saved state |
99
+
100
+ ### `TurnResult`
101
+
102
+ | Field | Type | Description |
103
+ |-------|------|-------------|
104
+ | `narrative` | `str` | AI-generated historical chronicle |
105
+ | `aftermath` | `str` | Resource changes summary |
106
+ | `state_changes` | `dict` | Numerical state deltas |
107
+ | `new_suggestions` | `list[str]` | Next-turn strategy suggestions |
108
+ | `token_usage` | `TokenUsage` | LLM token consumption |
109
+ | `game_over` | `dict \| None` | Victory/defeat message |
110
+ | `faction_status` | `FactionStatus` | Current resources and territories |
111
+
112
+ ## License
113
+
114
+ MIT — Emergence Science
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/histrategy_sdk/__init__.py
4
+ src/histrategy_sdk/_client.py
5
+ src/histrategy_sdk/_engine.py
6
+ src/histrategy_sdk/exceptions.py
7
+ src/histrategy_sdk/types.py
8
+ src/histrategy_sdk.egg-info/PKG-INFO
9
+ src/histrategy_sdk.egg-info/SOURCES.txt
10
+ src/histrategy_sdk.egg-info/dependency_links.txt
11
+ src/histrategy_sdk.egg-info/requires.txt
12
+ src/histrategy_sdk.egg-info/top_level.txt
@@ -0,0 +1,8 @@
1
+ httpx>=0.27
2
+
3
+ [dev]
4
+ pytest>=8
5
+ pytest-cov>=5
6
+
7
+ [engine]
8
+ histrategy-engine>=0.1.0
@@ -0,0 +1 @@
1
+ histrategy_sdk