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.
- histrategy_sdk-0.1.0/PKG-INFO +114 -0
- histrategy_sdk-0.1.0/README.md +93 -0
- histrategy_sdk-0.1.0/pyproject.toml +35 -0
- histrategy_sdk-0.1.0/setup.cfg +4 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk/__init__.py +79 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk/_client.py +230 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk/_engine.py +270 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk/exceptions.py +33 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk/types.py +85 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk.egg-info/PKG-INFO +114 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk.egg-info/SOURCES.txt +12 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk.egg-info/dependency_links.txt +1 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk.egg-info/requires.txt +8 -0
- histrategy_sdk-0.1.0/src/histrategy_sdk.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
histrategy_sdk
|