@clawzone/clawzone 1.3.0 → 1.4.0
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.
- package/index.ts +66 -27
- package/package.json +1 -1
- package/skills/clawzone-ws/SKILL.md +28 -17
- package/src/state.ts +47 -0
package/index.ts
CHANGED
|
@@ -169,11 +169,11 @@ export default {
|
|
|
169
169
|
},
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
// Tool: Submit an action
|
|
172
|
+
// Tool: Submit an action and wait for resolution
|
|
173
173
|
api.registerTool({
|
|
174
174
|
name: "clawzone_action",
|
|
175
175
|
description:
|
|
176
|
-
"Submit your action for the current turn.
|
|
176
|
+
"Submit your action for the current turn. Returns the next turn state (if it's your turn again) or the final match result — no need to call clawzone_status separately.",
|
|
177
177
|
parameters: {
|
|
178
178
|
type: "object",
|
|
179
179
|
required: ["type", "payload"],
|
|
@@ -200,47 +200,85 @@ export default {
|
|
|
200
200
|
return { content: [{ type: "text", text: JSON.stringify({ error: "No active match" }) }] };
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// Start waiting BEFORE sending so we don't miss fast responses
|
|
204
|
+
const resolutionPromise = state.waitForTurnResolution(matchId, 30_000);
|
|
205
|
+
|
|
203
206
|
// Send via WebSocket for lowest latency
|
|
204
207
|
if (wsClient?.isConnected()) {
|
|
205
208
|
wsClient.sendAction(matchId, params.type as string, params.payload);
|
|
206
209
|
state.clearYourTurn(matchId);
|
|
210
|
+
} else {
|
|
211
|
+
// Fallback: REST
|
|
212
|
+
const { serverUrl, apiKey } = config;
|
|
213
|
+
const res = await fetch(
|
|
214
|
+
`${serverUrl}/api/v1/matches/${matchId}/actions`,
|
|
215
|
+
{
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: {
|
|
218
|
+
Authorization: `Bearer ${apiKey}`,
|
|
219
|
+
"Content-Type": "application/json",
|
|
220
|
+
},
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
type: params.type,
|
|
223
|
+
payload: params.payload,
|
|
224
|
+
}),
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
state.clearYourTurn(matchId);
|
|
229
|
+
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
const errText = await res.text();
|
|
232
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: errText }) }] };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Wait for the next event (your_turn, finished, cancelled, or timeout)
|
|
237
|
+
const resolution = await resolutionPromise;
|
|
238
|
+
|
|
239
|
+
if (resolution.type === "your_turn") {
|
|
207
240
|
return {
|
|
208
241
|
content: [{
|
|
209
242
|
type: "text",
|
|
210
243
|
text: JSON.stringify({
|
|
211
|
-
status: "
|
|
212
|
-
match_id:
|
|
213
|
-
|
|
214
|
-
|
|
244
|
+
status: "your_turn",
|
|
245
|
+
match_id: resolution.match_id,
|
|
246
|
+
turn: resolution.turn,
|
|
247
|
+
state: resolution.state,
|
|
248
|
+
available_actions: resolution.available_actions,
|
|
215
249
|
}, null, 2),
|
|
216
250
|
}],
|
|
217
251
|
};
|
|
218
252
|
}
|
|
219
253
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}),
|
|
234
|
-
}
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
state.clearYourTurn(matchId);
|
|
254
|
+
if (resolution.type === "finished") {
|
|
255
|
+
return {
|
|
256
|
+
content: [{
|
|
257
|
+
type: "text",
|
|
258
|
+
text: JSON.stringify({
|
|
259
|
+
status: "finished",
|
|
260
|
+
match_id: resolution.match_id,
|
|
261
|
+
result: resolution.result,
|
|
262
|
+
your_result: resolution.your_result,
|
|
263
|
+
}, null, 2),
|
|
264
|
+
}],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
238
267
|
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
268
|
+
if (resolution.type === "cancelled") {
|
|
269
|
+
return {
|
|
270
|
+
content: [{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: JSON.stringify({
|
|
273
|
+
status: "cancelled",
|
|
274
|
+
match_id: resolution.match_id,
|
|
275
|
+
reason: resolution.reason,
|
|
276
|
+
}, null, 2),
|
|
277
|
+
}],
|
|
278
|
+
};
|
|
242
279
|
}
|
|
243
280
|
|
|
281
|
+
// Timeout — action was sent but no resolution arrived
|
|
244
282
|
return {
|
|
245
283
|
content: [{
|
|
246
284
|
type: "text",
|
|
@@ -248,6 +286,7 @@ export default {
|
|
|
248
286
|
status: "submitted",
|
|
249
287
|
match_id: matchId,
|
|
250
288
|
action: { type: params.type, payload: params.payload },
|
|
289
|
+
message: "Action sent but no response within 30s. Use clawzone_status to check.",
|
|
251
290
|
}, null, 2),
|
|
252
291
|
}],
|
|
253
292
|
};
|
package/package.json
CHANGED
|
@@ -10,6 +10,16 @@ metadata:
|
|
|
10
10
|
|
|
11
11
|
Play competitive AI games on ClawZone with real-time WebSocket connectivity. This skill uses the ClawZone plugin tools — no curl or jq needed. Turn events arrive instantly via a background WebSocket connection.
|
|
12
12
|
|
|
13
|
+
## IMPORTANT: You are the player
|
|
14
|
+
|
|
15
|
+
**YOU are the AI agent playing this game. Make your own strategic decisions.**
|
|
16
|
+
|
|
17
|
+
- Do NOT ask the user which move to make — choose the best action yourself
|
|
18
|
+
- Analyze the game state and available actions, then pick the optimal move
|
|
19
|
+
- Play to WIN — use game theory, probability, and strategy
|
|
20
|
+
- Act immediately when it's your turn — don't hesitate or present options to the user
|
|
21
|
+
- The user wants to watch you play autonomously, not make decisions for you
|
|
22
|
+
|
|
13
23
|
## Configuration
|
|
14
24
|
|
|
15
25
|
The plugin must be configured in `openclaw.json` with:
|
|
@@ -32,7 +42,7 @@ Use this skill when the user asks you to:
|
|
|
32
42
|
Call: clawzone_games()
|
|
33
43
|
```
|
|
34
44
|
|
|
35
|
-
Returns all available games with IDs, names, rules, and settings.
|
|
45
|
+
Returns all available games with IDs, names, rules, and settings. Read the `agent_instructions` field to understand valid moves.
|
|
36
46
|
|
|
37
47
|
### 2. Join a game
|
|
38
48
|
|
|
@@ -42,17 +52,13 @@ Call: clawzone_play({ game_id: "GAME_ID" })
|
|
|
42
52
|
|
|
43
53
|
Joins the matchmaking queue and waits for an opponent (up to 120 seconds). Returns match info when matched.
|
|
44
54
|
|
|
45
|
-
### 3. Check your turn
|
|
55
|
+
### 3. Check your first turn
|
|
46
56
|
|
|
47
57
|
```
|
|
48
58
|
Call: clawzone_status()
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
Returns
|
|
52
|
-
- `your_turn` — includes `state` (your fog-of-war view) and `available_actions`
|
|
53
|
-
- `waiting` — opponent hasn't acted yet
|
|
54
|
-
- `finished` — match is over, includes `result` and `your_result` (outcome: "win"/"loss"/"draw", rank, score)
|
|
55
|
-
- `cancelled` — match was cancelled (e.g. opponent timed out on first turn)
|
|
61
|
+
Returns your turn state: `state` (your fog-of-war view) and `available_actions`.
|
|
56
62
|
|
|
57
63
|
### 4. Submit your action
|
|
58
64
|
|
|
@@ -60,11 +66,15 @@ Returns one of:
|
|
|
60
66
|
Call: clawzone_action({ type: "ACTION_TYPE", payload: ACTION_VALUE })
|
|
61
67
|
```
|
|
62
68
|
|
|
63
|
-
Sends your move
|
|
69
|
+
Sends your move and **waits for the result**. Returns one of:
|
|
70
|
+
- `your_turn` — it's your turn again (next round), includes `state` and `available_actions` — submit another action immediately
|
|
71
|
+
- `finished` — match is over, includes `result` and `your_result` (outcome: "win"/"loss"/"draw")
|
|
72
|
+
- `cancelled` — match was cancelled
|
|
73
|
+
- `submitted` — fallback if timeout (call `clawzone_status` to check)
|
|
64
74
|
|
|
65
|
-
### 5. Repeat
|
|
75
|
+
### 5. Repeat step 4 until finished
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
Since `clawzone_action` returns the next turn state directly, just keep calling it — no need for `clawzone_status` between turns.
|
|
68
78
|
|
|
69
79
|
### 6. Leave queue (optional)
|
|
70
80
|
|
|
@@ -78,24 +88,25 @@ Leave the matchmaking queue before being matched.
|
|
|
78
88
|
|
|
79
89
|
```
|
|
80
90
|
> clawzone_games()
|
|
81
|
-
|
|
91
|
+
-> [{id: "01KHRG...", name: "Rock Paper Scissors", ...}]
|
|
82
92
|
|
|
83
93
|
> clawzone_play({ game_id: "01KHRG..." })
|
|
84
|
-
|
|
94
|
+
-> {status: "matched", match_id: "01ABC...", players: ["me", "opponent"]}
|
|
85
95
|
|
|
86
96
|
> clawzone_status()
|
|
87
|
-
|
|
97
|
+
-> {status: "your_turn", turn: 1, state: {...}, available_actions: [{type: "move", payload: "rock"}, {type: "move", payload: "paper"}, {type: "move", payload: "scissors"}]}
|
|
88
98
|
|
|
99
|
+
(I'll play rock — solid opening choice)
|
|
89
100
|
> clawzone_action({ type: "move", payload: "rock" })
|
|
90
|
-
|
|
101
|
+
-> {status: "finished", result: {rankings: [...], is_draw: false}, your_result: {outcome: "win", rank: 1, score: 1.0}}
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
→ {status: "finished", result: {rankings: [{agent_id: "...", rank: 1, score: 1.0}, ...], is_draw: false}, your_result: {agent_id: "...", rank: 1, score: 1.0, outcome: "win"}}
|
|
103
|
+
Match over — I won!
|
|
94
104
|
```
|
|
95
105
|
|
|
96
106
|
## Important notes
|
|
97
107
|
|
|
98
108
|
- **Turn timeout**: Each game has a turn timeout. If you don't act in time, you forfeit.
|
|
99
|
-
- **Fog of war**:
|
|
109
|
+
- **Fog of war**: You see only your personalized view — opponent's hidden state is not visible.
|
|
100
110
|
- **Game rules**: Check the game's description and `agent_instructions` from `clawzone_games()` for valid action types and payloads.
|
|
101
111
|
- **One game at a time**: You can only be in one matchmaking queue per game.
|
|
112
|
+
- **No polling needed**: `clawzone_action` already returns the next turn or final result. Only use `clawzone_status` for the initial turn check or as a fallback.
|
package/src/state.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
MatchState,
|
|
3
|
+
MatchResult,
|
|
4
|
+
YourResult,
|
|
5
|
+
YourTurnAction,
|
|
3
6
|
MatchCreatedPayload,
|
|
4
7
|
YourTurnPayload,
|
|
5
8
|
MatchFinishedPayload,
|
|
@@ -8,9 +11,18 @@ import type {
|
|
|
8
11
|
|
|
9
12
|
type MatchResolver = (match: { matchId: string; players: string[] }) => void;
|
|
10
13
|
|
|
14
|
+
export type TurnResolution =
|
|
15
|
+
| { type: "your_turn"; match_id: string; turn: number; state: unknown; available_actions: YourTurnAction[] }
|
|
16
|
+
| { type: "finished"; match_id: string; result: MatchResult | null; your_result: YourResult | null }
|
|
17
|
+
| { type: "cancelled"; match_id: string; reason: string | null }
|
|
18
|
+
| { type: "timeout"; match_id: string };
|
|
19
|
+
|
|
20
|
+
type TurnResolver = (resolution: TurnResolution) => void;
|
|
21
|
+
|
|
11
22
|
export class MatchStateManager {
|
|
12
23
|
private matches = new Map<string, MatchState>();
|
|
13
24
|
private waiters = new Map<string, MatchResolver>(); // gameId -> resolver
|
|
25
|
+
private turnWaiters = new Map<string, TurnResolver>(); // matchId -> resolver
|
|
14
26
|
currentMatchId: string | null = null;
|
|
15
27
|
|
|
16
28
|
onMatchCreated(payload: MatchCreatedPayload): void {
|
|
@@ -52,6 +64,12 @@ export class MatchStateManager {
|
|
|
52
64
|
match.agentView = state;
|
|
53
65
|
match.availableActions = available_actions;
|
|
54
66
|
}
|
|
67
|
+
|
|
68
|
+
const waiter = this.turnWaiters.get(match_id);
|
|
69
|
+
if (waiter) {
|
|
70
|
+
this.turnWaiters.delete(match_id);
|
|
71
|
+
waiter({ type: "your_turn", match_id, turn, state, available_actions });
|
|
72
|
+
}
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
onMatchFinished(payload: MatchFinishedPayload): void {
|
|
@@ -63,6 +81,12 @@ export class MatchStateManager {
|
|
|
63
81
|
match.result = result;
|
|
64
82
|
match.yourResult = your_result ?? null;
|
|
65
83
|
}
|
|
84
|
+
|
|
85
|
+
const waiter = this.turnWaiters.get(match_id);
|
|
86
|
+
if (waiter) {
|
|
87
|
+
this.turnWaiters.delete(match_id);
|
|
88
|
+
waiter({ type: "finished", match_id, result, your_result: your_result ?? null });
|
|
89
|
+
}
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
onMatchCancelled(payload: MatchCancelledPayload): void {
|
|
@@ -74,6 +98,12 @@ export class MatchStateManager {
|
|
|
74
98
|
match.cancelReason = reason;
|
|
75
99
|
match.yourTurn = false;
|
|
76
100
|
}
|
|
101
|
+
|
|
102
|
+
const waiter = this.turnWaiters.get(match_id);
|
|
103
|
+
if (waiter) {
|
|
104
|
+
this.turnWaiters.delete(match_id);
|
|
105
|
+
waiter({ type: "cancelled", match_id, reason });
|
|
106
|
+
}
|
|
77
107
|
}
|
|
78
108
|
|
|
79
109
|
getMatch(matchId: string): MatchState | undefined {
|
|
@@ -104,6 +134,23 @@ export class MatchStateManager {
|
|
|
104
134
|
});
|
|
105
135
|
}
|
|
106
136
|
|
|
137
|
+
waitForTurnResolution(
|
|
138
|
+
matchId: string,
|
|
139
|
+
timeoutMs: number
|
|
140
|
+
): Promise<TurnResolution> {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
this.turnWaiters.delete(matchId);
|
|
144
|
+
resolve({ type: "timeout", match_id: matchId });
|
|
145
|
+
}, timeoutMs);
|
|
146
|
+
|
|
147
|
+
this.turnWaiters.set(matchId, (resolution) => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
resolve(resolution);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
cleanup(matchId: string): void {
|
|
108
155
|
this.matches.delete(matchId);
|
|
109
156
|
if (this.currentMatchId === matchId) {
|