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.
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()