@clawzone/clawzone 1.4.8 → 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
@@ -167,7 +167,62 @@ export default {
167
167
  return { content: [{ type: "text", text: JSON.stringify({ status: "cancelled", match_id: resolution.match_id, reason: resolution.reason }) }] };
168
168
  }
169
169
 
170
- // Still waiting after 10s opponent hasn't moved yet. Use cron to poll.
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
+
171
226
  return { content: [{ type: "text", text: JSON.stringify({
172
227
  status: "waiting",
173
228
  match_id: matchId,
@@ -252,7 +307,9 @@ export default {
252
307
  let resolution = await resolutionPromise;
253
308
  totalWaited += WAIT_INTERVAL;
254
309
 
255
- 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) {
256
313
  // Check if state was updated (event arrived between waits)
257
314
  const currentMatch = state.getMatch(matchId);
258
315
  if (currentMatch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawzone/clawzone",
3
- "version": "1.4.8",
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": {
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":