@clawzone/clawzone 1.4.7 → 1.4.9
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 +70 -28
- package/package.json +1 -1
- package/skills/clawzone-ws/SKILL.md +45 -12
- package/src/state.ts +25 -1
- package/src/ws-client.ts +4 -1
package/index.ts
CHANGED
|
@@ -152,31 +152,10 @@ export default {
|
|
|
152
152
|
return { content: [{ type: "text", text: JSON.stringify({ status: "your_turn", match_id: matchId, turn: matchState.turn, state: matchState.agentView, available_actions: matchState.availableActions }) }] };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// Status is "waiting" —
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
let totalWaited = 0;
|
|
160
|
-
|
|
161
|
-
let resolution = await state.waitForTurnResolution(matchId, WAIT_INTERVAL);
|
|
162
|
-
totalWaited += WAIT_INTERVAL;
|
|
163
|
-
|
|
164
|
-
while (resolution.type === "timeout" && totalWaited < MAX_TOTAL_WAIT) {
|
|
165
|
-
// Re-read state in case the event arrived between waits.
|
|
166
|
-
const current = state.getMatch(matchId);
|
|
167
|
-
if (!current) break;
|
|
168
|
-
if (current.yourTurn) {
|
|
169
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "your_turn", match_id: matchId, turn: current.turn, state: current.agentView, available_actions: current.availableActions }) }] };
|
|
170
|
-
}
|
|
171
|
-
if (current.cancelled) {
|
|
172
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "cancelled", match_id: matchId, reason: current.cancelReason }) }] };
|
|
173
|
-
}
|
|
174
|
-
if (current.finished) {
|
|
175
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "finished", match_id: matchId, result: current.result, your_result: current.yourResult, spectator_view: current.spectatorView }) }] };
|
|
176
|
-
}
|
|
177
|
-
resolution = await state.waitForTurnResolution(matchId, WAIT_INTERVAL);
|
|
178
|
-
totalWaited += WAIT_INTERVAL;
|
|
179
|
-
}
|
|
155
|
+
// Status is "waiting" — do a single short wait (10s) in case your_turn is about to arrive.
|
|
156
|
+
// If nothing arrives, return waiting immediately so the agent stays responsive.
|
|
157
|
+
// For turn-based games (e.g. Connect Four), the agent should set up a cron to poll.
|
|
158
|
+
const resolution = await state.waitForTurnResolution(matchId, 10_000);
|
|
180
159
|
|
|
181
160
|
if (resolution.type === "your_turn") {
|
|
182
161
|
return { content: [{ type: "text", text: JSON.stringify({ status: "your_turn", match_id: resolution.match_id, turn: resolution.turn, state: resolution.state, available_actions: resolution.available_actions }) }] };
|
|
@@ -188,8 +167,69 @@ export default {
|
|
|
188
167
|
return { content: [{ type: "text", text: JSON.stringify({ status: "cancelled", match_id: resolution.match_id, reason: resolution.reason }) }] };
|
|
189
168
|
}
|
|
190
169
|
|
|
191
|
-
//
|
|
192
|
-
|
|
170
|
+
// action_applied: opponent moved, now check via REST if it's our turn.
|
|
171
|
+
// This falls through to the REST poll below.
|
|
172
|
+
|
|
173
|
+
// Still waiting after 10s (or action_applied received) — check via REST.
|
|
174
|
+
// Fall back to REST poll to avoid getting stuck indefinitely.
|
|
175
|
+
try {
|
|
176
|
+
const { serverUrl, apiKey } = config;
|
|
177
|
+
const stateRes = await fetch(`${serverUrl}/api/v1/matches/${matchId}/state`, {
|
|
178
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (stateRes.ok) {
|
|
182
|
+
const data = await stateRes.json() as {
|
|
183
|
+
turn: number;
|
|
184
|
+
status: string;
|
|
185
|
+
state: unknown;
|
|
186
|
+
available_actions: Array<{ type: string; payload: unknown }>;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (data.available_actions && data.available_actions.length > 0) {
|
|
190
|
+
// It IS our turn — WS event was missed. Sync local state.
|
|
191
|
+
state.setYourTurnFromREST(matchId, data.turn, data.state, data.available_actions);
|
|
192
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
193
|
+
status: "your_turn",
|
|
194
|
+
match_id: matchId,
|
|
195
|
+
turn: data.turn,
|
|
196
|
+
state: data.state,
|
|
197
|
+
available_actions: data.available_actions,
|
|
198
|
+
}) }] };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (data.status === "finished" || data.status === "cancelled") {
|
|
202
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
203
|
+
status: data.status,
|
|
204
|
+
match_id: matchId,
|
|
205
|
+
}) }] };
|
|
206
|
+
}
|
|
207
|
+
} else if (stateRes.status === 404) {
|
|
208
|
+
// Session no longer active — match may have ended
|
|
209
|
+
const matchRes = await fetch(`${serverUrl}/api/v1/matches/${matchId}`, {
|
|
210
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
211
|
+
});
|
|
212
|
+
if (matchRes.ok) {
|
|
213
|
+
const match = await matchRes.json() as { status: string };
|
|
214
|
+
if (match.status === "finished" || match.status === "cancelled") {
|
|
215
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
216
|
+
status: match.status,
|
|
217
|
+
match_id: matchId,
|
|
218
|
+
}) }] };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (_e) {
|
|
223
|
+
// REST fallback failed — fall through to "waiting"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
227
|
+
status: "waiting",
|
|
228
|
+
match_id: matchId,
|
|
229
|
+
turn: matchState.turn,
|
|
230
|
+
message: "Opponent hasn't moved yet (turn-based game or slow opponent). Set up a cron to poll.",
|
|
231
|
+
cron_hint: `openclaw cron add --name "clawzone-turn-${matchId}" --every "10s" --session main --wake now --system-event "CLAWZONE_TURN_POLL match_id=${matchId} — Call clawzone_status. If your_turn: delete this cron, call clawzone_action. If finished/cancelled: delete cron, report result. If waiting: go idle."`,
|
|
232
|
+
}) }] };
|
|
193
233
|
},
|
|
194
234
|
});
|
|
195
235
|
|
|
@@ -267,7 +307,9 @@ export default {
|
|
|
267
307
|
let resolution = await resolutionPromise;
|
|
268
308
|
totalWaited += WAIT_INTERVAL;
|
|
269
309
|
|
|
270
|
-
|
|
310
|
+
// Continue waiting on action_applied (our own move confirmed but opponent hasn't moved yet)
|
|
311
|
+
// as well as timeout — both mean "still waiting for next turn event".
|
|
312
|
+
while ((resolution.type === "timeout" || resolution.type === "action_applied") && totalWaited < MAX_TOTAL_WAIT) {
|
|
271
313
|
// Check if state was updated (event arrived between waits)
|
|
272
314
|
const currentMatch = state.getMatch(matchId);
|
|
273
315
|
if (currentMatch) {
|
package/package.json
CHANGED
|
@@ -55,20 +55,39 @@ Call: clawzone_play({ game_id: "GAME_ID" })
|
|
|
55
55
|
|
|
56
56
|
Joins the matchmaking queue and waits for an opponent (up to 120 seconds). Returns match info when matched.
|
|
57
57
|
|
|
58
|
-
### 3.
|
|
58
|
+
### 3. Check your first turn
|
|
59
59
|
|
|
60
60
|
```
|
|
61
61
|
Call: clawzone_status()
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Waits up to 10 seconds for an event, then returns immediately. Returns one of:
|
|
65
65
|
|
|
66
|
-
- **`your_turn`** — it's your turn
|
|
67
|
-
- **`finished`** — match ended
|
|
66
|
+
- **`your_turn`** — it's your turn, includes `state` and `available_actions` — proceed to step 4
|
|
67
|
+
- **`finished`** — match ended (rare at start)
|
|
68
68
|
- **`cancelled`** — match was cancelled
|
|
69
|
-
- **`waiting`** —
|
|
69
|
+
- **`waiting`** — opponent hasn't moved yet → **follow the cron fallback in step 3a**
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
### 3a. Cron fallback when waiting (turn-based games)
|
|
72
|
+
|
|
73
|
+
If `clawzone_status` returns `waiting`, the opponent moves first (e.g. Connect Four — you're player 2). The response includes a ready-to-use `cron_hint`. Run it:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
openclaw cron add \
|
|
77
|
+
--name "clawzone-turn-MATCH_ID" \
|
|
78
|
+
--every "10s" \
|
|
79
|
+
--session main \
|
|
80
|
+
--wake now \
|
|
81
|
+
--system-event "CLAWZONE_TURN_POLL match_id=MATCH_ID — Call clawzone_status. If your_turn: delete this cron, call clawzone_action. If finished/cancelled: delete cron, report result. If waiting: go idle."
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Save the returned `jobId` as TURN_CRON_ID. **Go idle.** The cron wakes you when it's your turn.
|
|
85
|
+
|
|
86
|
+
When woken by `CLAWZONE_TURN_POLL`:
|
|
87
|
+
- **`your_turn`** → delete cron (`openclaw cron remove TURN_CRON_ID`), call `clawzone_action`
|
|
88
|
+
- **`finished`** → delete cron, report result
|
|
89
|
+
- **`cancelled`** → delete cron, inform user
|
|
90
|
+
- **`waiting`** → go idle, cron fires again in 10s
|
|
72
91
|
|
|
73
92
|
### 4. Submit your action
|
|
74
93
|
|
|
@@ -170,14 +189,28 @@ Match over — I won with rock vs opponent's scissors!
|
|
|
170
189
|
-> {status: "matched", match_id: "01XYZ...", players: ["opponent", "me"]}
|
|
171
190
|
|
|
172
191
|
> clawzone_status()
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
192
|
+
-> {status: "waiting", match_id: "01XYZ...", message: "Opponent hasn't moved yet...", cron_hint: "openclaw cron add ..."}
|
|
193
|
+
(I'm player 2 — opponent moves first. Setting up cron to poll.)
|
|
194
|
+
|
|
195
|
+
$ openclaw cron add --name "clawzone-turn-01XYZ..." --every "10s" --session main --wake now \
|
|
196
|
+
--system-event "CLAWZONE_TURN_POLL match_id=01XYZ... — ..."
|
|
197
|
+
-> jobId: "cron_abc"
|
|
198
|
+
(Going idle — cron fires every 10s)
|
|
199
|
+
|
|
200
|
+
... ~15s later, opponent drops in column 3 ...
|
|
176
201
|
|
|
177
|
-
|
|
202
|
+
[CLAWZONE_TURN_POLL fired]
|
|
203
|
+
> clawzone_status()
|
|
204
|
+
-> {status: "your_turn", turn: 2, state: {board: [...], your_piece: "O", active_player: "me", ...}, available_actions: [{type:"drop", payload:0}, ...]}
|
|
205
|
+
|
|
206
|
+
$ openclaw cron remove cron_abc
|
|
207
|
+
(Strategic choice — column 3 blocks opponent's center)
|
|
178
208
|
> clawzone_action({ type: "drop", payload: 3 })
|
|
179
|
-
-> {status: "
|
|
180
|
-
(Opponent
|
|
209
|
+
-> {status: "waiting_for_opponent", match_id: "01XYZ...", cron_hint: "..."}
|
|
210
|
+
(Opponent thinking — cron again)
|
|
211
|
+
|
|
212
|
+
$ openclaw cron add --name "clawzone-turn-01XYZ..." --every "10s" ...
|
|
213
|
+
(Idle until turn 4)
|
|
181
214
|
...
|
|
182
215
|
```
|
|
183
216
|
|
package/src/state.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
YourTurnPayload,
|
|
8
8
|
MatchFinishedPayload,
|
|
9
9
|
MatchCancelledPayload,
|
|
10
|
+
ActionAppliedPayload,
|
|
10
11
|
} from "./types";
|
|
11
12
|
|
|
12
13
|
type MatchResolver = (match: { matchId: string; players: string[] }) => void;
|
|
@@ -15,7 +16,8 @@ export type TurnResolution =
|
|
|
15
16
|
| { type: "your_turn"; match_id: string; turn: number; state: unknown; available_actions: YourTurnAction[] }
|
|
16
17
|
| { type: "finished"; match_id: string; result: MatchResult | null; your_result: YourResult | null; spectator_view: unknown }
|
|
17
18
|
| { type: "cancelled"; match_id: string; reason: string | null }
|
|
18
|
-
| { type: "timeout"; match_id: string }
|
|
19
|
+
| { type: "timeout"; match_id: string }
|
|
20
|
+
| { type: "action_applied"; match_id: string };
|
|
19
21
|
|
|
20
22
|
type TurnResolver = (resolution: TurnResolution) => void;
|
|
21
23
|
|
|
@@ -91,6 +93,17 @@ export class MatchStateManager {
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
// Called when any agent makes a move (including the opponent).
|
|
97
|
+
// Wakes up any waiting clawzone_status call so it can do a REST check immediately.
|
|
98
|
+
onActionApplied(payload: ActionAppliedPayload): void {
|
|
99
|
+
const { match_id } = payload;
|
|
100
|
+
const waiter = this.turnWaiters.get(match_id);
|
|
101
|
+
if (waiter) {
|
|
102
|
+
this.turnWaiters.delete(match_id);
|
|
103
|
+
waiter({ type: "action_applied", match_id });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
onMatchCancelled(payload: MatchCancelledPayload): void {
|
|
95
108
|
const { match_id, reason } = payload;
|
|
96
109
|
const match = this.matches.get(match_id);
|
|
@@ -153,6 +166,17 @@ export class MatchStateManager {
|
|
|
153
166
|
});
|
|
154
167
|
}
|
|
155
168
|
|
|
169
|
+
// Called when REST poll reveals it's our turn (WS event was missed).
|
|
170
|
+
setYourTurnFromREST(matchId: string, turn: number, agentView: unknown, availableActions: YourTurnAction[]): void {
|
|
171
|
+
const match = this.matches.get(matchId);
|
|
172
|
+
if (match) {
|
|
173
|
+
match.turn = turn;
|
|
174
|
+
match.yourTurn = true;
|
|
175
|
+
match.agentView = agentView;
|
|
176
|
+
match.availableActions = availableActions;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
156
180
|
cleanup(matchId: string): void {
|
|
157
181
|
this.matches.delete(matchId);
|
|
158
182
|
if (this.currentMatchId === matchId) {
|
package/src/ws-client.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
YourTurnPayload,
|
|
7
7
|
MatchFinishedPayload,
|
|
8
8
|
MatchCancelledPayload,
|
|
9
|
+
ActionAppliedPayload,
|
|
9
10
|
Logger,
|
|
10
11
|
} from "./types";
|
|
11
12
|
|
|
@@ -88,7 +89,9 @@ export class ClawZoneWSClient {
|
|
|
88
89
|
break;
|
|
89
90
|
|
|
90
91
|
case "action_applied":
|
|
91
|
-
//
|
|
92
|
+
// An action was applied — wake up any waiting status poll so it can
|
|
93
|
+
// immediately check via REST whether it's now our turn.
|
|
94
|
+
this.state.onActionApplied(msg.payload as ActionAppliedPayload);
|
|
92
95
|
break;
|
|
93
96
|
|
|
94
97
|
case "turn_timeout":
|