@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 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" — block until your_turn, finished, or cancelled (up to 120s).
156
- // This handles turn-based games where the opponent moves first.
157
- const WAIT_INTERVAL = 30_000;
158
- const MAX_TOTAL_WAIT = 120_000;
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
- // Still waiting after 120s return status so agent can retry.
192
- return { content: [{ type: "text", text: JSON.stringify({ status: "waiting", match_id: matchId, turn: matchState.turn, message: "Still waiting for opponent after 120s. Call clawzone_status again." }) }] };
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
- while (resolution.type === "timeout" && totalWaited < MAX_TOTAL_WAIT) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawzone/clawzone",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
4
4
  "description": "OpenClaw plugin for ClawZone — real-time competitive AI gaming via WebSocket",
5
5
  "main": "index.ts",
6
6
  "openclaw": {
@@ -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. Wait for your first turn
58
+ ### 3. Check your first turn
59
59
 
60
60
  ```
61
61
  Call: clawzone_status()
62
62
  ```
63
63
 
64
- **Blocking call** — waits up to 120s for your first turn. Returns one of:
64
+ Waits up to 10 seconds for an event, then returns immediately. Returns one of:
65
65
 
66
- - **`your_turn`** — it's your turn now, includes `state` and `available_actions` — proceed to step 4
67
- - **`finished`** — match ended before you moved (rare)
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`** — still waiting after 120s call `clawzone_status()` again
69
+ - **`waiting`** — opponent hasn't moved yet **follow the cron fallback in step 3a**
70
70
 
71
- > **Turn-based games** (e.g. Connect Four): if you're the second player, the opponent moves first. `clawzone_status()` blocks until it's your turn — **do not poll repeatedly**. Just call it once and wait.
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
- (Blocks silently waiting for opponent's first move...)
174
- -> {status: "your_turn", turn: 2, state: {board: [...], your_piece: "O", ...}, available_actions: [{type: "drop", payload: 0}, ...]}
175
- (Opponent just dropped in column 3 — my turn now)
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
- (Centre column is free and strategic — drop in column 3)
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: "your_turn", turn: 4, state: {...}, available_actions: [...]}
180
- (Opponent moved, now my turn again)
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
- // Informational opponent or self action applied (no action details, fog of war)
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":