crawlerverse 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.
- crawlerverse-0.1.0/.coverage +0 -0
- crawlerverse-0.1.0/.gitignore +59 -0
- crawlerverse-0.1.0/PKG-INFO +173 -0
- crawlerverse-0.1.0/README.md +143 -0
- crawlerverse-0.1.0/examples/anthropic_agent.py +227 -0
- crawlerverse-0.1.0/examples/local_llm_agent.py +276 -0
- crawlerverse-0.1.0/examples/openai_agent.py +231 -0
- crawlerverse-0.1.0/pyproject.toml +66 -0
- crawlerverse-0.1.0/src/crawlerverse/__init__.py +98 -0
- crawlerverse-0.1.0/src/crawlerverse/_base_client.py +99 -0
- crawlerverse-0.1.0/src/crawlerverse/actions.py +77 -0
- crawlerverse-0.1.0/src/crawlerverse/async_client.py +124 -0
- crawlerverse-0.1.0/src/crawlerverse/client.py +118 -0
- crawlerverse-0.1.0/src/crawlerverse/exceptions.py +73 -0
- crawlerverse-0.1.0/src/crawlerverse/models.py +231 -0
- crawlerverse-0.1.0/src/crawlerverse/py.typed +0 -0
- crawlerverse-0.1.0/src/crawlerverse/runner.py +175 -0
- crawlerverse-0.1.0/src/crawlerverse/types.py +35 -0
- crawlerverse-0.1.0/tests/__init__.py +0 -0
- crawlerverse-0.1.0/tests/test_actions.py +96 -0
- crawlerverse-0.1.0/tests/test_async_client.py +95 -0
- crawlerverse-0.1.0/tests/test_base_client.py +103 -0
- crawlerverse-0.1.0/tests/test_client.py +213 -0
- crawlerverse-0.1.0/tests/test_exceptions.py +51 -0
- crawlerverse-0.1.0/tests/test_models.py +252 -0
- crawlerverse-0.1.0/tests/test_openapi_sync.py +182 -0
- crawlerverse-0.1.0/tests/test_runner.py +244 -0
- crawlerverse-0.1.0/tests/test_types.py +24 -0
- crawlerverse-0.1.0/uv.lock +835 -0
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
|
|
5
|
+
# Build outputs
|
|
6
|
+
.next/
|
|
7
|
+
out/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
next-env.d.ts
|
|
11
|
+
|
|
12
|
+
# Turbo
|
|
13
|
+
.turbo/
|
|
14
|
+
|
|
15
|
+
# Environment
|
|
16
|
+
.env
|
|
17
|
+
.env.local
|
|
18
|
+
.env.*.local
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
|
|
30
|
+
# Logs
|
|
31
|
+
*.log
|
|
32
|
+
npm-debug.log*
|
|
33
|
+
pnpm-debug.log*
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
coverage/
|
|
37
|
+
.nyc_output/
|
|
38
|
+
|
|
39
|
+
# Headless runner traces
|
|
40
|
+
.traces/
|
|
41
|
+
|
|
42
|
+
# Playwright
|
|
43
|
+
playwright-report/
|
|
44
|
+
test-results/
|
|
45
|
+
.playwright-mcp/
|
|
46
|
+
|
|
47
|
+
# Worktrees
|
|
48
|
+
.worktrees/
|
|
49
|
+
|
|
50
|
+
# Generated assets (copied by postinstall)
|
|
51
|
+
packages/crawler-core/public/dice-box-assets/
|
|
52
|
+
apps/web/public/dice-box-assets/
|
|
53
|
+
|
|
54
|
+
# PWA service worker (generated at build time)
|
|
55
|
+
packages/crawler-core/public/sw.js
|
|
56
|
+
packages/crawler-core/public/workbox-*.js
|
|
57
|
+
|
|
58
|
+
# Misc
|
|
59
|
+
*.tsbuildinfo
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crawlerverse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Crawler Agent API
|
|
5
|
+
Project-URL: Homepage, https://crawlerver.se
|
|
6
|
+
Project-URL: Documentation, https://crawlerver.se/docs/agent-api
|
|
7
|
+
Project-URL: Repository, https://github.com/reluctantfuturist/crawlerverse
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: anthropic>=0.78.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: openai>=2.17.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-httpx>=0.34; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# crawlerverse
|
|
32
|
+
|
|
33
|
+
Python SDK for the [Crawler Agent API](https://crawlerver.se/docs/agent-api). Build AI agents that play the Crawler roguelike game.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install crawlerverse
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from crawlerverse import CrawlerClient, run_game, Move, Wait, Direction, Observation, Action
|
|
45
|
+
|
|
46
|
+
def my_agent(observation: Observation) -> Action:
|
|
47
|
+
# Attack any adjacent monster
|
|
48
|
+
monster = observation.nearest_monster()
|
|
49
|
+
if monster:
|
|
50
|
+
tile, _ = monster
|
|
51
|
+
dx = tile.x - observation.player.position[0]
|
|
52
|
+
dy = tile.y - observation.player.position[1]
|
|
53
|
+
if abs(dx) <= 1 and abs(dy) <= 1:
|
|
54
|
+
# Monster is adjacent, determine direction
|
|
55
|
+
for d in Direction:
|
|
56
|
+
if observation.can_move(d):
|
|
57
|
+
return Move(direction=d)
|
|
58
|
+
|
|
59
|
+
# Otherwise just wait
|
|
60
|
+
return Wait()
|
|
61
|
+
|
|
62
|
+
with CrawlerClient(api_key="cra_...") as client:
|
|
63
|
+
result = run_game(client, my_agent, model_id="my-bot-v1")
|
|
64
|
+
print(f"Game over! Floor {result.outcome.floor}, result: {result.outcome.status}")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Authentication
|
|
68
|
+
|
|
69
|
+
Set your API key via parameter or environment variable:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# Option 1: Pass directly
|
|
73
|
+
client = CrawlerClient(api_key="cra_...")
|
|
74
|
+
|
|
75
|
+
# Option 2: Environment variable
|
|
76
|
+
# export CRAWLERVERSE_API_KEY=cra_...
|
|
77
|
+
client = CrawlerClient()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Async Support
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from crawlerverse import AsyncCrawlerClient, async_run_game
|
|
84
|
+
|
|
85
|
+
async with AsyncCrawlerClient() as client:
|
|
86
|
+
result = await async_run_game(client, my_agent)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API Reference
|
|
90
|
+
|
|
91
|
+
### Client Methods
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
client.games.create(model_id="gpt-4o") # Start a new game
|
|
95
|
+
client.games.list(status="completed") # List your games
|
|
96
|
+
client.games.get(game_id) # Get game state
|
|
97
|
+
client.games.action(game_id, Move(...)) # Submit action
|
|
98
|
+
client.games.abandon(game_id) # Abandon game
|
|
99
|
+
client.health() # Health check
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Actions
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
Move(direction=Direction.NORTH)
|
|
106
|
+
Attack(direction=Direction.EAST)
|
|
107
|
+
Wait()
|
|
108
|
+
Pickup()
|
|
109
|
+
Drop(item_type="health-potion")
|
|
110
|
+
Use(item_type="health-potion")
|
|
111
|
+
Equip(item_type="iron-sword")
|
|
112
|
+
EnterPortal()
|
|
113
|
+
RangedAttack(direction=Direction.SOUTH, distance=5)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Observation Helpers
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
obs.tile_at(x, y) # Look up tile by coordinates
|
|
120
|
+
obs.monsters() # All visible monsters
|
|
121
|
+
obs.nearest_monster() # Closest monster
|
|
122
|
+
obs.items_at_feet() # Items at player's position
|
|
123
|
+
obs.has_item("sword") # Check inventory
|
|
124
|
+
obs.can_move(Direction.NORTH) # Check if direction is walkable
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
All examples default to a local API at `http://localhost:3000/api/agent`. Set `CRAWLERVERSE_BASE_URL` to point at production.
|
|
130
|
+
|
|
131
|
+
### OpenAI
|
|
132
|
+
|
|
133
|
+
See [`examples/openai_agent.py`](examples/openai_agent.py):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install openai
|
|
137
|
+
export CRAWLERVERSE_API_KEY=cra_...
|
|
138
|
+
export OPENAI_API_KEY=sk-...
|
|
139
|
+
python examples/openai_agent.py
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Works with any OpenAI-compatible provider (Ollama, LMStudio, Azure, etc.) via `OPENAI_BASE_URL`.
|
|
143
|
+
|
|
144
|
+
### Anthropic (Claude)
|
|
145
|
+
|
|
146
|
+
See [`examples/anthropic_agent.py`](examples/anthropic_agent.py):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install anthropic
|
|
150
|
+
export CRAWLERVERSE_API_KEY=cra_...
|
|
151
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
152
|
+
python examples/anthropic_agent.py
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Uses Claude Haiku 4.5 by default. Override with `ANTHROPIC_MODEL=claude-sonnet-4-5`.
|
|
156
|
+
|
|
157
|
+
### Local LLM (Ollama / LMStudio)
|
|
158
|
+
|
|
159
|
+
See [`examples/local_llm_agent.py`](examples/local_llm_agent.py) for a script with configurable turn limits and error recovery:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install openai
|
|
163
|
+
export CRAWLERVERSE_API_KEY=cra_...
|
|
164
|
+
export OPENAI_BASE_URL=http://localhost:11434/v1
|
|
165
|
+
export OPENAI_MODEL=llama3
|
|
166
|
+
python examples/local_llm_agent.py
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Supports `MAX_TURNS` (default 25) and `MODEL_ID` env vars.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# crawlerverse
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Crawler Agent API](https://crawlerver.se/docs/agent-api). Build AI agents that play the Crawler roguelike game.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install crawlerverse
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from crawlerverse import CrawlerClient, run_game, Move, Wait, Direction, Observation, Action
|
|
15
|
+
|
|
16
|
+
def my_agent(observation: Observation) -> Action:
|
|
17
|
+
# Attack any adjacent monster
|
|
18
|
+
monster = observation.nearest_monster()
|
|
19
|
+
if monster:
|
|
20
|
+
tile, _ = monster
|
|
21
|
+
dx = tile.x - observation.player.position[0]
|
|
22
|
+
dy = tile.y - observation.player.position[1]
|
|
23
|
+
if abs(dx) <= 1 and abs(dy) <= 1:
|
|
24
|
+
# Monster is adjacent, determine direction
|
|
25
|
+
for d in Direction:
|
|
26
|
+
if observation.can_move(d):
|
|
27
|
+
return Move(direction=d)
|
|
28
|
+
|
|
29
|
+
# Otherwise just wait
|
|
30
|
+
return Wait()
|
|
31
|
+
|
|
32
|
+
with CrawlerClient(api_key="cra_...") as client:
|
|
33
|
+
result = run_game(client, my_agent, model_id="my-bot-v1")
|
|
34
|
+
print(f"Game over! Floor {result.outcome.floor}, result: {result.outcome.status}")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
Set your API key via parameter or environment variable:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# Option 1: Pass directly
|
|
43
|
+
client = CrawlerClient(api_key="cra_...")
|
|
44
|
+
|
|
45
|
+
# Option 2: Environment variable
|
|
46
|
+
# export CRAWLERVERSE_API_KEY=cra_...
|
|
47
|
+
client = CrawlerClient()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Async Support
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from crawlerverse import AsyncCrawlerClient, async_run_game
|
|
54
|
+
|
|
55
|
+
async with AsyncCrawlerClient() as client:
|
|
56
|
+
result = await async_run_game(client, my_agent)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
### Client Methods
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
client.games.create(model_id="gpt-4o") # Start a new game
|
|
65
|
+
client.games.list(status="completed") # List your games
|
|
66
|
+
client.games.get(game_id) # Get game state
|
|
67
|
+
client.games.action(game_id, Move(...)) # Submit action
|
|
68
|
+
client.games.abandon(game_id) # Abandon game
|
|
69
|
+
client.health() # Health check
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Actions
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
Move(direction=Direction.NORTH)
|
|
76
|
+
Attack(direction=Direction.EAST)
|
|
77
|
+
Wait()
|
|
78
|
+
Pickup()
|
|
79
|
+
Drop(item_type="health-potion")
|
|
80
|
+
Use(item_type="health-potion")
|
|
81
|
+
Equip(item_type="iron-sword")
|
|
82
|
+
EnterPortal()
|
|
83
|
+
RangedAttack(direction=Direction.SOUTH, distance=5)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Observation Helpers
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
obs.tile_at(x, y) # Look up tile by coordinates
|
|
90
|
+
obs.monsters() # All visible monsters
|
|
91
|
+
obs.nearest_monster() # Closest monster
|
|
92
|
+
obs.items_at_feet() # Items at player's position
|
|
93
|
+
obs.has_item("sword") # Check inventory
|
|
94
|
+
obs.can_move(Direction.NORTH) # Check if direction is walkable
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Examples
|
|
98
|
+
|
|
99
|
+
All examples default to a local API at `http://localhost:3000/api/agent`. Set `CRAWLERVERSE_BASE_URL` to point at production.
|
|
100
|
+
|
|
101
|
+
### OpenAI
|
|
102
|
+
|
|
103
|
+
See [`examples/openai_agent.py`](examples/openai_agent.py):
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install openai
|
|
107
|
+
export CRAWLERVERSE_API_KEY=cra_...
|
|
108
|
+
export OPENAI_API_KEY=sk-...
|
|
109
|
+
python examples/openai_agent.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Works with any OpenAI-compatible provider (Ollama, LMStudio, Azure, etc.) via `OPENAI_BASE_URL`.
|
|
113
|
+
|
|
114
|
+
### Anthropic (Claude)
|
|
115
|
+
|
|
116
|
+
See [`examples/anthropic_agent.py`](examples/anthropic_agent.py):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install anthropic
|
|
120
|
+
export CRAWLERVERSE_API_KEY=cra_...
|
|
121
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
122
|
+
python examples/anthropic_agent.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Uses Claude Haiku 4.5 by default. Override with `ANTHROPIC_MODEL=claude-sonnet-4-5`.
|
|
126
|
+
|
|
127
|
+
### Local LLM (Ollama / LMStudio)
|
|
128
|
+
|
|
129
|
+
See [`examples/local_llm_agent.py`](examples/local_llm_agent.py) for a script with configurable turn limits and error recovery:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install openai
|
|
133
|
+
export CRAWLERVERSE_API_KEY=cra_...
|
|
134
|
+
export OPENAI_BASE_URL=http://localhost:11434/v1
|
|
135
|
+
export OPENAI_MODEL=llama3
|
|
136
|
+
python examples/local_llm_agent.py
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Supports `MAX_TURNS` (default 25) and `MODEL_ID` env vars.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Example: Use Anthropic Claude to play Crawler.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
export CRAWLERVERSE_API_KEY=cra_...
|
|
5
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
6
|
+
python examples/anthropic_agent.py
|
|
7
|
+
|
|
8
|
+
Requirements (not included in crawlerverse):
|
|
9
|
+
pip install anthropic
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from anthropic import Anthropic
|
|
19
|
+
|
|
20
|
+
from crawlerverse import (
|
|
21
|
+
Action,
|
|
22
|
+
Attack,
|
|
23
|
+
CrawlerClient,
|
|
24
|
+
Drop,
|
|
25
|
+
EnterPortal,
|
|
26
|
+
Equip,
|
|
27
|
+
Move,
|
|
28
|
+
Observation,
|
|
29
|
+
Pickup,
|
|
30
|
+
RangedAttack,
|
|
31
|
+
Use,
|
|
32
|
+
Wait,
|
|
33
|
+
run_game,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
37
|
+
log = logging.getLogger("crawlerverse")
|
|
38
|
+
log.setLevel(logging.DEBUG)
|
|
39
|
+
|
|
40
|
+
SYSTEM_PROMPT = """\
|
|
41
|
+
You are an AI agent playing Crawler, a roguelike dungeon game.
|
|
42
|
+
Each turn you receive an observation and must choose ONE action.
|
|
43
|
+
Respond with a JSON object (no markdown, no explanation).
|
|
44
|
+
|
|
45
|
+
## Actions
|
|
46
|
+
{"action": "move", "direction": "<dir>"}
|
|
47
|
+
{"action": "attack", "direction": "<dir>"}
|
|
48
|
+
{"action": "ranged_attack", "direction": "<dir>", "distance": <1-15>}
|
|
49
|
+
{"action": "pickup"}
|
|
50
|
+
{"action": "drop", "itemType": "<item>"}
|
|
51
|
+
{"action": "use", "itemType": "<item>"}
|
|
52
|
+
{"action": "equip", "itemType": "<item>"}
|
|
53
|
+
{"action": "wait"}
|
|
54
|
+
{"action": "enter_portal"}
|
|
55
|
+
|
|
56
|
+
Directions: north, south, east, west, northeast, northwest, southeast, southwest
|
|
57
|
+
|
|
58
|
+
## Strategy Tips
|
|
59
|
+
- Kill monsters to clear the path. Attack adjacent monsters.
|
|
60
|
+
- Pick up items (potions, weapons, armor) — they help you survive.
|
|
61
|
+
- Equip weapons and armor for better stats.
|
|
62
|
+
- Use health potions when HP is low.
|
|
63
|
+
- Find stairs down to descend to the next floor.
|
|
64
|
+
- Explore systematically; avoid getting surrounded.
|
|
65
|
+
|
|
66
|
+
Always include a "reasoning" field explaining your decision."""
|
|
67
|
+
|
|
68
|
+
ACTION_MAP: dict[str, type[Action]] = {
|
|
69
|
+
"move": Move,
|
|
70
|
+
"attack": Attack,
|
|
71
|
+
"wait": Wait,
|
|
72
|
+
"pickup": Pickup,
|
|
73
|
+
"drop": Drop,
|
|
74
|
+
"use": Use,
|
|
75
|
+
"equip": Equip,
|
|
76
|
+
"enter_portal": EnterPortal,
|
|
77
|
+
"ranged_attack": RangedAttack,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_observation(obs: Observation) -> str:
|
|
82
|
+
"""Format observation into a detailed prompt for Claude."""
|
|
83
|
+
p = obs.player
|
|
84
|
+
lines = [
|
|
85
|
+
f"Turn {obs.turn} | Floor {obs.floor}",
|
|
86
|
+
f"HP: {p.hp}/{p.max_hp} | ATK: {p.attack} | DEF: {p.defense}",
|
|
87
|
+
f"Position: ({p.position[0]}, {p.position[1]})",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
if p.equipped_weapon:
|
|
91
|
+
lines.append(f"Weapon: {p.equipped_weapon}")
|
|
92
|
+
if p.equipped_armor:
|
|
93
|
+
lines.append(f"Armor: {p.equipped_armor}")
|
|
94
|
+
|
|
95
|
+
if obs.inventory:
|
|
96
|
+
inv = ", ".join(f"{i.name} ({i.type})" for i in obs.inventory)
|
|
97
|
+
lines.append(f"Inventory: {inv}")
|
|
98
|
+
|
|
99
|
+
lines.append("")
|
|
100
|
+
lines.append("Visible tiles:")
|
|
101
|
+
for tile in obs.visible_tiles:
|
|
102
|
+
parts = [f" ({tile.x},{tile.y}) {tile.type}"]
|
|
103
|
+
if tile.monster:
|
|
104
|
+
m = tile.monster
|
|
105
|
+
parts.append(f"[MONSTER: {m.type} HP:{m.hp}/{m.max_hp}]")
|
|
106
|
+
if tile.items:
|
|
107
|
+
parts.append(f"[ITEMS: {', '.join(tile.items)}]")
|
|
108
|
+
lines.append(" ".join(parts))
|
|
109
|
+
|
|
110
|
+
if obs.messages:
|
|
111
|
+
lines.append("")
|
|
112
|
+
lines.append("Messages:")
|
|
113
|
+
for msg in obs.messages:
|
|
114
|
+
lines.append(f" {msg}")
|
|
115
|
+
|
|
116
|
+
return "\n".join(lines)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def parse_action(raw: str) -> Action:
|
|
120
|
+
"""Parse Claude's response into an Action, with fallback to Wait."""
|
|
121
|
+
text = raw.strip()
|
|
122
|
+
if text.startswith("```"):
|
|
123
|
+
text = text.split("\n", 1)[1] if "\n" in text else text[3:]
|
|
124
|
+
if text.endswith("```"):
|
|
125
|
+
text = text[:-3]
|
|
126
|
+
text = text.strip()
|
|
127
|
+
|
|
128
|
+
# Extract JSON if surrounded by other text
|
|
129
|
+
if not text.startswith("{"):
|
|
130
|
+
start = text.find("{")
|
|
131
|
+
if start >= 0:
|
|
132
|
+
end = text.rfind("}")
|
|
133
|
+
if end > start:
|
|
134
|
+
text = text[start : end + 1]
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
data = json.loads(text)
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
log.warning("Failed to parse Claude response as JSON: %s", text[:100])
|
|
140
|
+
return Wait(reasoning="Failed to parse response")
|
|
141
|
+
|
|
142
|
+
action_type = data.get("action", "wait")
|
|
143
|
+
cls = ACTION_MAP.get(action_type)
|
|
144
|
+
if cls is None:
|
|
145
|
+
log.warning("Unknown action type: %s", action_type)
|
|
146
|
+
return Wait(reasoning=f"Unknown action: {action_type}")
|
|
147
|
+
|
|
148
|
+
# Build kwargs from the response, filtering to known fields
|
|
149
|
+
valid_fields = set(cls.model_fields.keys())
|
|
150
|
+
kwargs = {}
|
|
151
|
+
for k, v in data.items():
|
|
152
|
+
if k != "action" and k in valid_fields:
|
|
153
|
+
kwargs[k] = v
|
|
154
|
+
|
|
155
|
+
# Handle camelCase -> snake_case for itemType
|
|
156
|
+
if "itemType" in data and "item_type" in valid_fields:
|
|
157
|
+
kwargs["item_type"] = data["itemType"]
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
return cls(**kwargs)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
log.warning("Failed to construct %s: %s", action_type, e)
|
|
163
|
+
return Wait(reasoning=f"Failed to construct {action_type}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def make_agent(model: str = "claude-haiku-4-5-20251001"):
|
|
167
|
+
"""Create an agent function that uses Anthropic Claude to decide actions."""
|
|
168
|
+
client = Anthropic()
|
|
169
|
+
messages: list[dict[str, str]] = []
|
|
170
|
+
|
|
171
|
+
def agent(obs: Observation) -> Action:
|
|
172
|
+
prompt = format_observation(obs)
|
|
173
|
+
messages.append({"role": "user", "content": prompt})
|
|
174
|
+
|
|
175
|
+
# Prefill assistant turn with "{" to force JSON output
|
|
176
|
+
prefill = {"role": "assistant", "content": "{"}
|
|
177
|
+
response = client.messages.create(
|
|
178
|
+
model=model,
|
|
179
|
+
system=SYSTEM_PROMPT,
|
|
180
|
+
messages=[*messages, prefill],
|
|
181
|
+
temperature=0.3,
|
|
182
|
+
max_tokens=200,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
reply = "{" + response.content[0].text
|
|
186
|
+
messages.append({"role": "assistant", "content": reply})
|
|
187
|
+
|
|
188
|
+
action = parse_action(reply)
|
|
189
|
+
log.info("Turn %d: %s", obs.turn, action.model_dump_json(by_alias=True))
|
|
190
|
+
return action
|
|
191
|
+
|
|
192
|
+
return agent
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main():
|
|
196
|
+
model = os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5-20251001")
|
|
197
|
+
model_id = os.environ.get("MODEL_ID", f"anthropic/{model}")
|
|
198
|
+
|
|
199
|
+
print(f"Starting game with model: {model} (leaderboard ID: {model_id})")
|
|
200
|
+
print()
|
|
201
|
+
|
|
202
|
+
agent = make_agent(model=model)
|
|
203
|
+
|
|
204
|
+
base_url = os.environ.get(
|
|
205
|
+
"CRAWLERVERSE_BASE_URL", "http://localhost:3000/api/agent"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
with CrawlerClient(base_url=base_url) as client:
|
|
209
|
+
result = run_game(
|
|
210
|
+
client,
|
|
211
|
+
agent,
|
|
212
|
+
model_id=model_id,
|
|
213
|
+
on_step=lambda obs, action: None,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
outcome = result.outcome
|
|
217
|
+
print()
|
|
218
|
+
print(f"Game over! {outcome.status}")
|
|
219
|
+
print(f" Floor reached: {outcome.floor}")
|
|
220
|
+
print(f" Total turns: {outcome.turns}")
|
|
221
|
+
if hasattr(outcome, "result"):
|
|
222
|
+
print(f" Result: {outcome.result}")
|
|
223
|
+
print(f" Watch replay: {result.spectator_url}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
main()
|