@chipzen-ai/bot 0.3.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/CHANGELOG.md +220 -0
- package/README.md +96 -0
- package/dist/bin.js +2545 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.cjs +2310 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +746 -0
- package/dist/index.d.ts +746 -0
- package/dist/index.js +2244 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core data models — Action, GameState, Card.
|
|
3
|
+
*
|
|
4
|
+
* Field naming uses idiomatic camelCase for the SDK's user-facing
|
|
5
|
+
* surface. The on-the-wire JSON the protocol uses is snake_case
|
|
6
|
+
* (Layer 1 / Layer 2 spec); the parsers in this module translate.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A standard playing card. `rank` is one of `2`-`9`, `T`, `J`, `Q`,
|
|
10
|
+
* `K`, `A`. `suit` is one of `h` (hearts), `d` (diamonds), `c`
|
|
11
|
+
* (clubs), `s` (spades).
|
|
12
|
+
*/
|
|
13
|
+
interface Card {
|
|
14
|
+
readonly rank: string;
|
|
15
|
+
readonly suit: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse a card from its 2-character wire representation, e.g. `"Ah"`.
|
|
19
|
+
*
|
|
20
|
+
* Throws `Error` on malformed input; the wire format is
|
|
21
|
+
* always exactly 2 characters with a valid rank + suit.
|
|
22
|
+
*/
|
|
23
|
+
declare function cardFromString(s: string): Card;
|
|
24
|
+
/** Render a card back to its 2-character wire form. */
|
|
25
|
+
declare function cardToString(c: Card): string;
|
|
26
|
+
type ActionKind = "fold" | "check" | "call" | "raise" | "all_in";
|
|
27
|
+
/**
|
|
28
|
+
* The action a bot returns from `decide()`.
|
|
29
|
+
*
|
|
30
|
+
* Construct via the static factories (`Action.fold()` etc.) — they
|
|
31
|
+
* validate the action vs. amount invariants for you.
|
|
32
|
+
*/
|
|
33
|
+
declare class Action {
|
|
34
|
+
readonly action: ActionKind;
|
|
35
|
+
readonly amount?: number | undefined;
|
|
36
|
+
private constructor();
|
|
37
|
+
static fold(): Action;
|
|
38
|
+
static check(): Action;
|
|
39
|
+
static call(): Action;
|
|
40
|
+
static raiseTo(amount: number): Action;
|
|
41
|
+
static allIn(): Action;
|
|
42
|
+
/**
|
|
43
|
+
* Serialize to the two-layer `turn_action` payload shape the server expects.
|
|
44
|
+
*
|
|
45
|
+
* Returns `{action, params}` where `params` carries the raise amount
|
|
46
|
+
* for `raise` and is empty for everything else.
|
|
47
|
+
*/
|
|
48
|
+
toWire(): {
|
|
49
|
+
action: ActionKind;
|
|
50
|
+
params: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A single entry from `state.actionHistory`. Synthetic blind/ante
|
|
55
|
+
* entries (`post_small_blind`, `post_big_blind`, `post_ante`) appear
|
|
56
|
+
* here too — the server generates them; bots do not submit them.
|
|
57
|
+
*/
|
|
58
|
+
interface ActionHistoryEntry {
|
|
59
|
+
readonly seat: number;
|
|
60
|
+
readonly action: string;
|
|
61
|
+
readonly amount?: number;
|
|
62
|
+
readonly isTimeout?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Built from the server's `turn_request` message. The parser in
|
|
66
|
+
* `parseGameState` converts the wire-format snake_case to the
|
|
67
|
+
* camelCase fields below.
|
|
68
|
+
*/
|
|
69
|
+
interface GameState {
|
|
70
|
+
readonly handNumber: number;
|
|
71
|
+
readonly phase: "preflop" | "flop" | "turn" | "river";
|
|
72
|
+
readonly holeCards: readonly Card[];
|
|
73
|
+
readonly board: readonly Card[];
|
|
74
|
+
readonly pot: number;
|
|
75
|
+
readonly yourStack: number;
|
|
76
|
+
readonly opponentStacks: readonly number[];
|
|
77
|
+
readonly yourSeat: number;
|
|
78
|
+
readonly dealerSeat: number;
|
|
79
|
+
readonly toCall: number;
|
|
80
|
+
readonly minRaise: number;
|
|
81
|
+
readonly maxRaise: number;
|
|
82
|
+
readonly validActions: readonly string[];
|
|
83
|
+
readonly actionHistory: readonly ActionHistoryEntry[];
|
|
84
|
+
readonly roundId: string;
|
|
85
|
+
readonly requestId: string;
|
|
86
|
+
}
|
|
87
|
+
interface RawTurnRequest {
|
|
88
|
+
request_id?: string;
|
|
89
|
+
round_id?: string;
|
|
90
|
+
valid_actions?: string[];
|
|
91
|
+
state?: Record<string, unknown> | null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parse a `turn_request` envelope into a `GameState`.
|
|
95
|
+
*
|
|
96
|
+
* The wire shape is documented in
|
|
97
|
+
* `docs/protocol/POKER-GAME-STATE-PROTOCOL.md`. All fields default
|
|
98
|
+
* to safe values when absent — but a real server always sends them.
|
|
99
|
+
*/
|
|
100
|
+
declare function parseGameState(message: RawTurnRequest): GameState;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `Bot` abstract base class.
|
|
104
|
+
*
|
|
105
|
+
* Subclass and override `decide()`. Lifecycle hooks
|
|
106
|
+
* (`onMatchStart` / `onRoundStart` / `onPhaseChange` / `onTurnResult` /
|
|
107
|
+
* `onRoundResult` / `onMatchEnd`) are optional — defaults are no-ops.
|
|
108
|
+
* Override the ones you need; the SDK's session loop will call them
|
|
109
|
+
* at the right point.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
declare abstract class Bot {
|
|
113
|
+
/**
|
|
114
|
+
* The only required override. Called every time the server sends a
|
|
115
|
+
* `turn_request` for your seat. Return one of:
|
|
116
|
+
* `Action.fold()`, `Action.check()`, `Action.call()`,
|
|
117
|
+
* `Action.raiseTo(amount)`, `Action.allIn()`.
|
|
118
|
+
*
|
|
119
|
+
* The returned action's wire-form `action` string MUST be in
|
|
120
|
+
* `state.validActions`; raise amounts MUST satisfy
|
|
121
|
+
* `state.minRaise <= amount <= state.maxRaise`. If the bot returns
|
|
122
|
+
* an illegal action, the SDK's safe-fallback substitutes `check`
|
|
123
|
+
* (or `fold` if check is illegal) and the platform sends a
|
|
124
|
+
* `bot_error` event to the human's UI.
|
|
125
|
+
*/
|
|
126
|
+
abstract decide(state: GameState): Action;
|
|
127
|
+
/** Called once when the `match_start` message arrives. */
|
|
128
|
+
onMatchStart(_matchInfo: Record<string, unknown>): void;
|
|
129
|
+
/**
|
|
130
|
+
* Called at the start of every hand with the raw `round_start` message.
|
|
131
|
+
*
|
|
132
|
+
* Override if you need the Layer 1 envelope (`round_id`,
|
|
133
|
+
* `round_number`) or the full Layer 2 `state` payload. For most bots,
|
|
134
|
+
* `onHandStart` is the simpler hook.
|
|
135
|
+
*/
|
|
136
|
+
onRoundStart(_message: Record<string, unknown>): void;
|
|
137
|
+
/**
|
|
138
|
+
* Called when the flop, turn, or river is dealt. Useful for triggering
|
|
139
|
+
* postflop planning *between* your turns rather than inside `decide`.
|
|
140
|
+
*/
|
|
141
|
+
onPhaseChange(_message: Record<string, unknown>): void;
|
|
142
|
+
/**
|
|
143
|
+
* Called after every participant's action is broadcast — yours and
|
|
144
|
+
* every opponent's. Use for opponent modeling, timing analysis, or
|
|
145
|
+
* stack tracking.
|
|
146
|
+
*
|
|
147
|
+
* This hook runs *before* the next `turn_request` is dispatched, and
|
|
148
|
+
* runs serially. Slow work here eats into your decide budget — see
|
|
149
|
+
* the DEV-MANUAL §6 for the "queue drain" failure mode.
|
|
150
|
+
*/
|
|
151
|
+
onTurnResult(_message: Record<string, unknown>): void;
|
|
152
|
+
/** Called when a hand ends (`round_result` message). */
|
|
153
|
+
onRoundResult(_message: Record<string, unknown>): void;
|
|
154
|
+
/** Called once when the match ends. */
|
|
155
|
+
onMatchEnd(_results: Record<string, unknown>): void;
|
|
156
|
+
/**
|
|
157
|
+
* Called after each `turn_action` is sent, with the wall-clock time
|
|
158
|
+
* `decide()` took in milliseconds.
|
|
159
|
+
*
|
|
160
|
+
* Default is a no-op. Override to track decision latency — useful for
|
|
161
|
+
* spotting when your bot is drifting toward the platform's turn-timeout
|
|
162
|
+
* budget. See chipzen-ai/chipzen-sdk#46.
|
|
163
|
+
*/
|
|
164
|
+
onDecisionLatency(_latencyMs: number): void;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Retry / backoff policy for the WebSocket client.
|
|
169
|
+
*
|
|
170
|
+
* When the connection to the Chipzen server drops (TCP reset, heartbeat
|
|
171
|
+
* miss, transient network failure, etc.) the SDK reconnects within the
|
|
172
|
+
* server's reconnect grace window. The pacing of those reconnect attempts
|
|
173
|
+
* is configurable via {@link RetryPolicy}, accepted by both `runBot` and
|
|
174
|
+
* `runExternalBot`.
|
|
175
|
+
*
|
|
176
|
+
* The default policy mirrors the Python SDK's spec (External-API Issue 26):
|
|
177
|
+
* 5 attempts, 500ms initial backoff, doubling each attempt, capped at
|
|
178
|
+
* 30 seconds. The defaults are sensible for the typical home-network
|
|
179
|
+
* deployment; devs on noisy connections may want a longer backoff or
|
|
180
|
+
* more attempts.
|
|
181
|
+
*
|
|
182
|
+
* Note: this policy controls **only** how reconnect attempts are paced.
|
|
183
|
+
* The 30-second server-side grace window itself is unchanged; if the
|
|
184
|
+
* reconnects burn through the window the session is considered lost and
|
|
185
|
+
* the server terminates the match-side state.
|
|
186
|
+
*/
|
|
187
|
+
/** Backoff knobs accepted by {@link RetryPolicy}. All fields are optional. */
|
|
188
|
+
interface RetryPolicyOptions {
|
|
189
|
+
/**
|
|
190
|
+
* Maximum number of reconnection attempts after a connection drop or
|
|
191
|
+
* heartbeat miss. Must be `>= 0`. `0` disables reconnection entirely
|
|
192
|
+
* (the first connect failure raises). Default `5`.
|
|
193
|
+
*/
|
|
194
|
+
maxReconnectAttempts?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Delay before the **first** reconnect attempt, in milliseconds. Must
|
|
197
|
+
* be `>= 0`. Default `500`.
|
|
198
|
+
*/
|
|
199
|
+
initialBackoffMs?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Upper bound for any single backoff delay, in milliseconds. Must be
|
|
202
|
+
* `>= initialBackoffMs`. Default `30000` (matches the server-side grace
|
|
203
|
+
* window so a single backoff never exceeds the window itself).
|
|
204
|
+
*/
|
|
205
|
+
maxBackoffMs?: number;
|
|
206
|
+
/**
|
|
207
|
+
* Exponential factor applied between attempts. `2.0` doubles the delay
|
|
208
|
+
* each attempt. Must be `>= 1.0`; `1.0` produces constant backoff.
|
|
209
|
+
* Default `2.0`.
|
|
210
|
+
*/
|
|
211
|
+
backoffMultiplier?: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Backoff knobs applied to reconnect attempts.
|
|
215
|
+
*
|
|
216
|
+
* Backoff progression for attempt `n` (1-indexed) is:
|
|
217
|
+
*
|
|
218
|
+
* min(initialBackoffMs * backoffMultiplier ** (n - 1), maxBackoffMs)
|
|
219
|
+
*
|
|
220
|
+
* Examples (defaults):
|
|
221
|
+
*
|
|
222
|
+
* attempt 1: 500 ms
|
|
223
|
+
* attempt 2: 1000 ms
|
|
224
|
+
* attempt 3: 2000 ms
|
|
225
|
+
* attempt 4: 4000 ms
|
|
226
|
+
* attempt 5: 8000 ms
|
|
227
|
+
* attempt 6: 16000 ms (would be next, but capped by attempts=5)
|
|
228
|
+
*/
|
|
229
|
+
declare class RetryPolicy {
|
|
230
|
+
readonly maxReconnectAttempts: number;
|
|
231
|
+
readonly initialBackoffMs: number;
|
|
232
|
+
readonly maxBackoffMs: number;
|
|
233
|
+
readonly backoffMultiplier: number;
|
|
234
|
+
constructor(options?: RetryPolicyOptions);
|
|
235
|
+
/**
|
|
236
|
+
* Return the delay (in ms) to wait **before** the given attempt.
|
|
237
|
+
*
|
|
238
|
+
* @param attempt 1-indexed attempt number. `attempt=1` is the first
|
|
239
|
+
* reconnect after a drop, `attempt=2` the second, etc.
|
|
240
|
+
* @returns The delay in milliseconds, capped at `maxBackoffMs`.
|
|
241
|
+
* @throws Error if `attempt < 1`.
|
|
242
|
+
*/
|
|
243
|
+
backoffMs(attempt: number): number;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The default {@link RetryPolicy} used when `runBot` / `runExternalBot`
|
|
247
|
+
* is called without an explicit `retryPolicy` argument.
|
|
248
|
+
*/
|
|
249
|
+
declare const DEFAULT_RETRY_POLICY: RetryPolicy;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* WebSocket client for the Chipzen two-layer protocol.
|
|
253
|
+
*
|
|
254
|
+
* The user-facing surface is `runBot(url, bot, options)`. Internals
|
|
255
|
+
* (session loop, mock-friendly helpers) are exported with `_` prefix
|
|
256
|
+
* for the conformance harness and tests; they are not part of the
|
|
257
|
+
* supported API.
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Raised when `bot.decide()` errors and `safeMode` is `false`.
|
|
262
|
+
*
|
|
263
|
+
* Distinguished from transport/connection errors so the caller treats it
|
|
264
|
+
* as terminal (a deterministic bot bug, not a transient disconnect) and
|
|
265
|
+
* does NOT reconnect-retry it. See chipzen-ai/chipzen-sdk#52.
|
|
266
|
+
*/
|
|
267
|
+
declare class BotDecisionError extends Error {
|
|
268
|
+
constructor(message: string);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Optional knobs for `runBot`. All fields default to sensible values
|
|
272
|
+
* matching the platform's expectations.
|
|
273
|
+
*/
|
|
274
|
+
interface RunBotOptions {
|
|
275
|
+
/** Bot API token. Required for the `/bot` endpoint; empty is fine for local dev. */
|
|
276
|
+
token?: string | null;
|
|
277
|
+
/** Single-use ticket alternative to `token` (competitive endpoints). */
|
|
278
|
+
ticket?: string | null;
|
|
279
|
+
/** Match UUID. Auto-extracted from the URL if not supplied. */
|
|
280
|
+
matchId?: string;
|
|
281
|
+
/** Client software name sent in the `hello` handshake. */
|
|
282
|
+
clientName?: string;
|
|
283
|
+
/** Client software version sent in the `hello` handshake. Defaults to the SDK version. */
|
|
284
|
+
clientVersion?: string;
|
|
285
|
+
/**
|
|
286
|
+
* Reconnect attempt **cap**. When given, overrides the attempt count
|
|
287
|
+
* from `retryPolicy` (the policy's backoff knobs still apply). When
|
|
288
|
+
* omitted, the policy's `maxReconnectAttempts` is used.
|
|
289
|
+
*/
|
|
290
|
+
maxRetries?: number;
|
|
291
|
+
/**
|
|
292
|
+
* {@link RetryPolicy} controlling reconnect attempts + exponential
|
|
293
|
+
* backoff. Defaults to {@link DEFAULT_RETRY_POLICY}.
|
|
294
|
+
*/
|
|
295
|
+
retryPolicy?: RetryPolicy;
|
|
296
|
+
/**
|
|
297
|
+
* When `true` (default), an exception thrown by `bot.decide()` is
|
|
298
|
+
* folded (a safe fallback action is sent). Set `false` for dev/eval so
|
|
299
|
+
* the first exception propagates as a {@link BotDecisionError} and the
|
|
300
|
+
* process exits non-zero (chipzen-ai/chipzen-sdk#52).
|
|
301
|
+
*/
|
|
302
|
+
safeMode?: boolean;
|
|
303
|
+
/**
|
|
304
|
+
* Override the WS `User-Agent` header. Defaults to
|
|
305
|
+
* `chipzen-sdk-js/<version>` (a non-default UA also clears the
|
|
306
|
+
* platform's Cloudflare bot-fight rule; chipzen-ai/chipzen-sdk#46).
|
|
307
|
+
*/
|
|
308
|
+
userAgent?: string;
|
|
309
|
+
}
|
|
310
|
+
/** Protocol versions this client claims to support in the handshake. */
|
|
311
|
+
declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ["1.0"];
|
|
312
|
+
/**
|
|
313
|
+
* Connect a bot to the Chipzen server and play until the match ends.
|
|
314
|
+
*
|
|
315
|
+
* Resolves on `match_end` (with the `match_end` payload, ignored by most
|
|
316
|
+
* callers). Rejects if the connection cannot be established (after the
|
|
317
|
+
* reconnect budget is exhausted) or if `safeMode` is `false` and
|
|
318
|
+
* `bot.decide()` throws (a terminal {@link BotDecisionError}).
|
|
319
|
+
*/
|
|
320
|
+
declare function runBot(url: string, bot: Bot, options?: RunBotOptions): Promise<Record<string, unknown> | null>;
|
|
321
|
+
interface AsyncMessageReader {
|
|
322
|
+
next(): Promise<string | null>;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* `chipzen.toml` discovery and parsing for the SDK.
|
|
327
|
+
*
|
|
328
|
+
* Devs running an external-API bot should be able to drop their long-lived
|
|
329
|
+
* API token into a config file once and forget about it, instead of
|
|
330
|
+
* hard-coding `token="cz_extbot_..."` into source. This module implements
|
|
331
|
+
* the discovery + parsing half of that convention; `runExternalBot` (and
|
|
332
|
+
* the `chipzen-sdk run-external` CLI) consume the result and prefer
|
|
333
|
+
* explicit kwargs over config-file values.
|
|
334
|
+
*
|
|
335
|
+
* Mirrors the Python SDK's `chipzen.config` (External-API Issue 23,
|
|
336
|
+
* chipzen-ai/chipzen-sdk#42).
|
|
337
|
+
*
|
|
338
|
+
* Discovery
|
|
339
|
+
* ---------
|
|
340
|
+
*
|
|
341
|
+
* Search order, first match wins:
|
|
342
|
+
*
|
|
343
|
+
* 1. `./chipzen.toml` (current working directory)
|
|
344
|
+
* 2. `~/.chipzen/chipzen.toml` (user-home config)
|
|
345
|
+
* 3. `/etc/chipzen/chipzen.toml` (system config, POSIX only — silently
|
|
346
|
+
* skipped on Windows where `/etc` does not exist)
|
|
347
|
+
*
|
|
348
|
+
* If no file is found, `loadChipzenConfig` returns `null` and the caller
|
|
349
|
+
* falls back to whatever explicit arguments were passed. A clear error is
|
|
350
|
+
* only raised when a file IS found but is malformed or missing the
|
|
351
|
+
* expected section.
|
|
352
|
+
*
|
|
353
|
+
* File format
|
|
354
|
+
* -----------
|
|
355
|
+
*
|
|
356
|
+
* [external_api]
|
|
357
|
+
* token = "cz_extbot_<32-char-base62-random>"
|
|
358
|
+
* url = "wss://chipzen.ai/ws/external/bot/<bot_id>" # optional
|
|
359
|
+
* bot_id = "<bot-uuid>" # optional
|
|
360
|
+
*
|
|
361
|
+
* All three fields are optional and must be quoted strings. We parse the
|
|
362
|
+
* single `[external_api]` table with a minimal inline reader rather than
|
|
363
|
+
* pulling in a TOML dependency — keeping the package's single runtime dep
|
|
364
|
+
* (`ws`).
|
|
365
|
+
*/
|
|
366
|
+
declare const CONFIG_FILENAME = "chipzen.toml";
|
|
367
|
+
declare const SECTION_NAME = "external_api";
|
|
368
|
+
/**
|
|
369
|
+
* Parsed contents of a `chipzen.toml` file. All `[external_api]` fields
|
|
370
|
+
* are optional; absence yields `null`.
|
|
371
|
+
*/
|
|
372
|
+
interface ChipzenConfig {
|
|
373
|
+
/** Filesystem path the config was loaded from (for error messages). */
|
|
374
|
+
readonly path: string;
|
|
375
|
+
/** Value of `[external_api] token` if present, else `null`. */
|
|
376
|
+
readonly token: string | null;
|
|
377
|
+
/** Value of `[external_api] url` if present, else `null`. */
|
|
378
|
+
readonly url: string | null;
|
|
379
|
+
/** Value of `[external_api] bot_id` if present, else `null`. */
|
|
380
|
+
readonly botId: string | null;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Raised when a `chipzen.toml` is found but cannot be used (malformed,
|
|
384
|
+
* missing the `[external_api]` section, or a field is the wrong type).
|
|
385
|
+
*
|
|
386
|
+
* A "found but unusable" file is always a hard error — silent fallback
|
|
387
|
+
* would mask typos that would otherwise be obvious.
|
|
388
|
+
*/
|
|
389
|
+
declare class ChipzenConfigError extends Error {
|
|
390
|
+
constructor(message: string);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Discover and parse a `chipzen.toml` from the search path.
|
|
394
|
+
*
|
|
395
|
+
* @param paths Override the default search order. When omitted, uses
|
|
396
|
+
* cwd → `~/.chipzen/` → `/etc/chipzen/` (POSIX only).
|
|
397
|
+
* @returns A {@link ChipzenConfig} if a file was found and parsed; `null`
|
|
398
|
+
* if no file exists on the search path. The "no file" case is NOT an
|
|
399
|
+
* error — the SDK falls back to explicit kwargs in that case.
|
|
400
|
+
* @throws ChipzenConfigError if a file is found but is malformed, lacks
|
|
401
|
+
* the `[external_api]` section, or has a wrong-typed token / url / bot_id.
|
|
402
|
+
*/
|
|
403
|
+
declare function loadChipzenConfig(paths?: string[]): ChipzenConfig | null;
|
|
404
|
+
/**
|
|
405
|
+
* Return the token to use, honoring the precedence rules.
|
|
406
|
+
*
|
|
407
|
+
* 1. If `explicitToken` is non-`undefined`/non-`null`, return it. Even an
|
|
408
|
+
* empty string wins — the dev was explicit.
|
|
409
|
+
* 2. If `explicitTicket` is set, return `null` (ticket-auth; no token).
|
|
410
|
+
* 3. Otherwise, if `config` carries a token, return it.
|
|
411
|
+
* 4. Otherwise, return `null`.
|
|
412
|
+
*/
|
|
413
|
+
declare function resolveToken(opts: {
|
|
414
|
+
explicitToken?: string | null;
|
|
415
|
+
explicitTicket?: string | null;
|
|
416
|
+
config?: ChipzenConfig | null;
|
|
417
|
+
}): string | null;
|
|
418
|
+
/**
|
|
419
|
+
* Return the URL override to use, honoring the precedence rules.
|
|
420
|
+
*
|
|
421
|
+
* 1. If `explicitUrl` is set, return it.
|
|
422
|
+
* 2. Otherwise, if `config` carries a `url`, return it.
|
|
423
|
+
* 3. Otherwise, return `null`.
|
|
424
|
+
*/
|
|
425
|
+
declare function resolveUrl(opts: {
|
|
426
|
+
explicitUrl?: string | null;
|
|
427
|
+
config?: ChipzenConfig | null;
|
|
428
|
+
}): string | null;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Environment-aware connection helper for the external-API lobby.
|
|
432
|
+
*
|
|
433
|
+
* Devs running an external-API bot against Chipzen shouldn't need to
|
|
434
|
+
* remember the exact lobby WebSocket URL per environment. This module
|
|
435
|
+
* exposes {@link connectToChipzen} — a small one-liner that returns a
|
|
436
|
+
* fully-populated {@link ConnectionConfig} containing the resolved `url`,
|
|
437
|
+
* `token`, and `retryPolicy`, ready to hand off to `runExternalBot`.
|
|
438
|
+
*
|
|
439
|
+
* Mirrors the Python SDK's `chipzen.connect` (External-API Issue 24,
|
|
440
|
+
* chipzen-ai/chipzen-sdk#43).
|
|
441
|
+
*
|
|
442
|
+
* Environment → URL mapping:
|
|
443
|
+
*
|
|
444
|
+
* prod -> wss://chipzen.ai/ws/external/bot/{botId}
|
|
445
|
+
* staging -> wss://staging.chipzen.ai/ws/external/bot/{botId}
|
|
446
|
+
* local -> ws://localhost:8001/ws/external/bot/{botId}
|
|
447
|
+
*
|
|
448
|
+
* Precedence (highest first) for the final WebSocket URL:
|
|
449
|
+
*
|
|
450
|
+
* 1. `[external_api].url` from a discovered `chipzen.toml` — a config-file
|
|
451
|
+
* URL ALWAYS wins (most explicit, user-managed override).
|
|
452
|
+
* 2. An **explicitly passed** `env` argument.
|
|
453
|
+
* 3. The `CHIPZEN_ENV` environment variable, if set to a recognized value.
|
|
454
|
+
* 4. The default of `"prod"`.
|
|
455
|
+
*/
|
|
456
|
+
|
|
457
|
+
/** Recognized target environments. */
|
|
458
|
+
type EnvName = "prod" | "staging" | "local";
|
|
459
|
+
/** The canonical env names, in order, for error messages + validation. */
|
|
460
|
+
declare const ENV_NAMES: readonly EnvName[];
|
|
461
|
+
/**
|
|
462
|
+
* Name of the environment variable consulted when `env` is not explicitly
|
|
463
|
+
* passed.
|
|
464
|
+
*/
|
|
465
|
+
declare const ENV_VAR_NAME = "CHIPZEN_ENV";
|
|
466
|
+
/**
|
|
467
|
+
* Fully-resolved connection parameters ready for `runExternalBot`.
|
|
468
|
+
* Returned by {@link connectToChipzen}.
|
|
469
|
+
*/
|
|
470
|
+
interface ConnectionConfig {
|
|
471
|
+
/** WebSocket URL the bot should connect to. */
|
|
472
|
+
readonly url: string;
|
|
473
|
+
/** Long-lived API token from a discovered `chipzen.toml`, or `null`. */
|
|
474
|
+
readonly token: string | null;
|
|
475
|
+
/** {@link RetryPolicy} controlling reconnect pacing. */
|
|
476
|
+
readonly retryPolicy: RetryPolicy;
|
|
477
|
+
/**
|
|
478
|
+
* The resolved environment name, or `null` if the URL was supplied
|
|
479
|
+
* verbatim via a config file (no env mapping applied). Mostly for logs.
|
|
480
|
+
*/
|
|
481
|
+
readonly env: EnvName | null;
|
|
482
|
+
/**
|
|
483
|
+
* The {@link ChipzenConfig} discovered during resolution (if any), or
|
|
484
|
+
* `null`. Exposed so callers can pass it through to `runExternalBot`
|
|
485
|
+
* and avoid a second filesystem stat.
|
|
486
|
+
*/
|
|
487
|
+
readonly config: ChipzenConfig | null;
|
|
488
|
+
}
|
|
489
|
+
/** Options for {@link connectToChipzen}. */
|
|
490
|
+
interface ConnectToChipzenOptions {
|
|
491
|
+
/** Override the reconnect-pacing policy. Defaults to {@link DEFAULT_RETRY_POLICY}. */
|
|
492
|
+
retryPolicy?: RetryPolicy;
|
|
493
|
+
/** Pre-loaded config to avoid a second filesystem stat. `undefined` triggers discovery. */
|
|
494
|
+
config?: ChipzenConfig | null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Resolve a {@link ConnectionConfig} for the external-API lobby.
|
|
498
|
+
*
|
|
499
|
+
* Maps `env` to a canonical lobby URL and combines it with whatever
|
|
500
|
+
* config-file token / URL / retry policy the dev has set up.
|
|
501
|
+
*
|
|
502
|
+
* @param botId External-API bot UUID. Required, non-empty.
|
|
503
|
+
* @param env Target environment. `undefined`/`null` means "look at
|
|
504
|
+
* `$CHIPZEN_ENV` first, then fall back to `prod`".
|
|
505
|
+
* @param options Optional retry policy + pre-loaded config.
|
|
506
|
+
* @throws Error if `botId` is empty, `env` is unrecognized, or
|
|
507
|
+
* `$CHIPZEN_ENV` is set to an unrecognized value.
|
|
508
|
+
* @throws ChipzenConfigError if a discovered `chipzen.toml` is malformed.
|
|
509
|
+
*/
|
|
510
|
+
declare function connectToChipzen(botId: string, env?: EnvName | null, options?: ConnectToChipzenOptions): ConnectionConfig;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* External-API remote-play entry point: {@link runExternalBot}.
|
|
514
|
+
*
|
|
515
|
+
* Where `runBot` connects a bot to a *known* match URL (the
|
|
516
|
+
* containerized/upload path — the platform's executor hands the container
|
|
517
|
+
* its `/ws/match/{matchId}/{participantId}` URL), this module implements
|
|
518
|
+
* the **external-API remote-play** path: a developer runs their bot on
|
|
519
|
+
* their own machine, authenticates with a long-lived `cz_extbot_` token,
|
|
520
|
+
* and the platform matches and dispatches them like any other competitor.
|
|
521
|
+
*
|
|
522
|
+
* The flow:
|
|
523
|
+
*
|
|
524
|
+
* lobby WS /ws/external/bot/{botId} (token in authenticate frame)
|
|
525
|
+
* -> "matched" notify (carries matchId + gatewayWsUrl)
|
|
526
|
+
* -> per-match gateway WS /ws/external/match/{mid}/{pid}
|
|
527
|
+
* (token in Sec-WebSocket-Protocol header)
|
|
528
|
+
* -> two-layer bot handshake + game loop to match_end
|
|
529
|
+
*
|
|
530
|
+
* The **match data plane is identical** to the containerized path, so the
|
|
531
|
+
* game loop here reuses `_runSession` from `client.ts` verbatim — the only
|
|
532
|
+
* external-API-specific code is the lobby connection and the per-match
|
|
533
|
+
* gateway handshake. A developer writes ONE `Bot` subclass and it works on
|
|
534
|
+
* both paths.
|
|
535
|
+
*
|
|
536
|
+
* The lobby is held open for the bot's whole session and each `matched`
|
|
537
|
+
* plays in its own task (Promise), so the 15-second lobby heartbeat is
|
|
538
|
+
* answered even while a multi-minute match is in flight. This is what lets
|
|
539
|
+
* a single connection serve a whole tournament (the bot is "checked in"
|
|
540
|
+
* via lobby presence and matched once per round).
|
|
541
|
+
*
|
|
542
|
+
* Mirrors the Python SDK's `chipzen.external`, including the reconnect
|
|
543
|
+
* fix: a dropped gateway socket RECONNECTS and resumes; match-task
|
|
544
|
+
* ownership is hoisted to the top level so a lobby reconnect doesn't kill
|
|
545
|
+
* in-flight matches; teardown drains-then-cancels, never orphaning a task.
|
|
546
|
+
*/
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Sentinel subprotocol that marks the `cz_extbot_` token in the
|
|
550
|
+
* `Sec-WebSocket-Protocol` header (CZ issue 2932 moved the token off the
|
|
551
|
+
* query string, where it leaked into proxy access logs). Must match the
|
|
552
|
+
* value the platform's api gateway expects.
|
|
553
|
+
*/
|
|
554
|
+
declare const BOT_TOKEN_SUBPROTOCOL = "chipzen-bot-token";
|
|
555
|
+
/**
|
|
556
|
+
* Build the `Sec-WebSocket-Protocol` offer that carries the bot token.
|
|
557
|
+
*
|
|
558
|
+
* Returns `[sentinel, token]` — the sentinel marks "the next value is my
|
|
559
|
+
* bot token". The api gateway extracts the token from this header (so it
|
|
560
|
+
* never appears in any access log / URL) and echoes the sentinel back on
|
|
561
|
+
* accept.
|
|
562
|
+
*/
|
|
563
|
+
declare function botTokenSubprotocols(token: string): [string, string];
|
|
564
|
+
/**
|
|
565
|
+
* Resolve the `matched.gateway_ws_url` path against the lobby origin.
|
|
566
|
+
*
|
|
567
|
+
* The `matched` notification carries `gateway_ws_url` as a *path*
|
|
568
|
+
* (`/ws/external/match/{mid}/{pid}`). The `cz_extbot_` token is NOT on the
|
|
569
|
+
* query string — it travels in the `Sec-WebSocket-Protocol` header. A
|
|
570
|
+
* future server that returns a full URL is passed through unchanged.
|
|
571
|
+
*/
|
|
572
|
+
declare function resolveGatewayUrl(lobbyUrl: string, gatewayWsPath: string): string;
|
|
573
|
+
/** A factory that produces a fresh {@link Bot} per match (or returns the same one). */
|
|
574
|
+
type BotFactory = () => Bot;
|
|
575
|
+
/** A connected WS-shaped object the lobby loop / `_runSession` drive. */
|
|
576
|
+
interface ExternalConnection {
|
|
577
|
+
/** Send a string frame. */
|
|
578
|
+
send(data: string): void | Promise<void>;
|
|
579
|
+
/** Pull-based message reader; resolves `null` when the socket closes. */
|
|
580
|
+
reader: AsyncMessageReader;
|
|
581
|
+
/** Close the underlying socket. */
|
|
582
|
+
close(): void;
|
|
583
|
+
}
|
|
584
|
+
/** Options handed to a {@link Transport} on connect. */
|
|
585
|
+
interface TransportConnectOptions {
|
|
586
|
+
/** WS `User-Agent` header value. */
|
|
587
|
+
userAgent: string;
|
|
588
|
+
/**
|
|
589
|
+
* Sec-WebSocket-Protocol offer (gateway leg carries `[sentinel, token]`).
|
|
590
|
+
* Omitted for the lobby leg.
|
|
591
|
+
*/
|
|
592
|
+
subprotocols?: string[];
|
|
593
|
+
}
|
|
594
|
+
/** Opens WS connections for the external path. Injectable for tests. */
|
|
595
|
+
type Transport = (url: string, options: TransportConnectOptions) => Promise<ExternalConnection>;
|
|
596
|
+
/** Per-match result recorded by the session. */
|
|
597
|
+
interface MatchResult {
|
|
598
|
+
matchId: string | null;
|
|
599
|
+
end: Record<string, unknown> | null;
|
|
600
|
+
}
|
|
601
|
+
/** Options for {@link runExternalBot}. */
|
|
602
|
+
interface RunExternalBotOptions {
|
|
603
|
+
/** External-API bot UUID. Used to build the lobby URL when `url` is absent. */
|
|
604
|
+
botId?: string;
|
|
605
|
+
/** Target environment (`prod` / `staging` / `local`). `undefined` consults `$CHIPZEN_ENV`. */
|
|
606
|
+
env?: EnvName | null;
|
|
607
|
+
/** Explicit full lobby URL. Overrides `botId` / `env` derivation. */
|
|
608
|
+
url?: string;
|
|
609
|
+
/** Long-lived `cz_extbot_` API token. Falls back to `[external_api].token`. Required. */
|
|
610
|
+
token?: string | null;
|
|
611
|
+
/** Pre-loaded config, to avoid a second filesystem stat. `undefined` triggers discovery. */
|
|
612
|
+
config?: ChipzenConfig | null;
|
|
613
|
+
/** Reconnect-pacing policy. Defaults to {@link DEFAULT_RETRY_POLICY}. */
|
|
614
|
+
retryPolicy?: RetryPolicy;
|
|
615
|
+
/** Client software name sent in the per-match `hello`. */
|
|
616
|
+
clientName?: string;
|
|
617
|
+
/** Client software version. Defaults to the SDK version. */
|
|
618
|
+
clientVersion?: string;
|
|
619
|
+
/** When `true` (default), a `decide()` throw is folded; `false` propagates a {@link BotDecisionError}. */
|
|
620
|
+
safeMode?: boolean;
|
|
621
|
+
/** Stop after this many matches complete. `undefined` runs until the lobby closes / evict. */
|
|
622
|
+
maxMatches?: number | null;
|
|
623
|
+
/** Override the WS `User-Agent`. Defaults to `chipzen-sdk-js/<version>`. */
|
|
624
|
+
userAgent?: string;
|
|
625
|
+
/** Injectable transport (tests). Defaults to a real `ws.WebSocket`. */
|
|
626
|
+
transport?: Transport;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Run a bot on the Chipzen external-API remote-play path.
|
|
630
|
+
*
|
|
631
|
+
* Connects to the lobby, then plays every match the platform dispatches to
|
|
632
|
+
* this bot (a single challenge, or every round of a tournament) until the
|
|
633
|
+
* lobby closes, the bot is evicted, or `maxMatches` matches complete.
|
|
634
|
+
*
|
|
635
|
+
* @returns A list of per-match result objects (`{matchId, end}`), one per
|
|
636
|
+
* match played this session.
|
|
637
|
+
* @throws Error if no token can be resolved, or neither `url` nor a
|
|
638
|
+
* `botId` is available to build the lobby URL.
|
|
639
|
+
* @throws BotDecisionError if `safeMode` is `false` and `bot.decide()` throws.
|
|
640
|
+
*/
|
|
641
|
+
declare function runExternalBot(bot: Bot | BotFactory, options?: RunExternalBotOptions): Promise<MatchResult[]>;
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Single source of truth for the SDK version string.
|
|
645
|
+
*
|
|
646
|
+
* Reads `version` from the package's own `package.json` so the value the
|
|
647
|
+
* handshake reports always tracks the published package (chipzen-ai/chipzen-sdk#41)
|
|
648
|
+
* — no hardcoded literal to drift out of sync on a release bump.
|
|
649
|
+
*
|
|
650
|
+
* The import is resolved against `../package.json` (one level up from
|
|
651
|
+
* `src/`). `tsup` inlines the JSON at build time, so the bundled output
|
|
652
|
+
* carries the literal value with no runtime filesystem read.
|
|
653
|
+
*/
|
|
654
|
+
/** The installed SDK version, e.g. `"0.3.0"`. */
|
|
655
|
+
declare const VERSION: string;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* `chipzen-sdk init <name>` — scaffold a new Chipzen bot project.
|
|
659
|
+
*
|
|
660
|
+
* Mirrors the Python `chipzen.scaffold` shape so cross-language
|
|
661
|
+
* developers see the same outputs. The scaffolded project is plain
|
|
662
|
+
* ESM JavaScript (no TypeScript dep required to run); convert to TS
|
|
663
|
+
* if you want by renaming bot.js -> bot.ts and adding a tsconfig.
|
|
664
|
+
*/
|
|
665
|
+
interface ScaffoldOptions {
|
|
666
|
+
/** Where to create the new project. Defaults to cwd. */
|
|
667
|
+
parentDir?: string;
|
|
668
|
+
}
|
|
669
|
+
declare function scaffoldBot(name: string, options?: ScaffoldOptions): Promise<string>;
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* `chipzen-sdk validate <path>` — pre-upload conformance checks.
|
|
673
|
+
*
|
|
674
|
+
* Mirrors the Python validator's check shape and severity model so a
|
|
675
|
+
* `(severity, name, message)` tuple from either language renders the
|
|
676
|
+
* same way in client tooling.
|
|
677
|
+
*/
|
|
678
|
+
type Severity$1 = "pass" | "warn" | "fail";
|
|
679
|
+
interface ValidationResult {
|
|
680
|
+
severity: Severity$1;
|
|
681
|
+
name: string;
|
|
682
|
+
message: string;
|
|
683
|
+
}
|
|
684
|
+
interface ValidateOptions {
|
|
685
|
+
/** Override entry point filename (default: auto-detect bot.js / bot.mjs / bot.cjs). */
|
|
686
|
+
entryPoint?: string;
|
|
687
|
+
/** Hard-fail upload size threshold, in bytes. Defaults to 500 MB (platform cap). */
|
|
688
|
+
maxUploadBytes?: number;
|
|
689
|
+
/** Warn if `decide()` takes longer than this (ms). */
|
|
690
|
+
timeoutWarnMs?: number;
|
|
691
|
+
/**
|
|
692
|
+
* If true, run the protocol-conformance harness after the smoke test
|
|
693
|
+
* passes — drives the bot through one full canned match (handshake +
|
|
694
|
+
* 1 hand + match_end) against an in-process mock WebSocket.
|
|
695
|
+
*/
|
|
696
|
+
checkConnectivity?: boolean;
|
|
697
|
+
/** Per-conformance-scenario timeout (ms). Default 10s. */
|
|
698
|
+
conformanceTimeoutMs?: number;
|
|
699
|
+
}
|
|
700
|
+
declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
701
|
+
declare const DEFAULT_TIMEOUT_WARN_MS = 100;
|
|
702
|
+
declare const PLATFORM_TIMEOUT_MS = 500;
|
|
703
|
+
declare function validateBot(botPath: string, options?: ValidateOptions): Promise<ValidationResult[]>;
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Protocol-conformance harness used by `chipzen-sdk validate --check-connectivity`.
|
|
707
|
+
*
|
|
708
|
+
* Mirrors the Python harness in `packages/python/src/chipzen/conformance.py`
|
|
709
|
+
* — same scenario shape, same severity model, same canned protocol
|
|
710
|
+
* exchange (handshake + one full hand + match_end). A clean run means
|
|
711
|
+
* the upload pipeline will accept the bot on protocol grounds. It does
|
|
712
|
+
* NOT mean the bot is good.
|
|
713
|
+
*
|
|
714
|
+
* Implementation: drives `_runSession` from `client.ts` against an
|
|
715
|
+
* in-process mock WebSocket rather than spinning up a real server. The
|
|
716
|
+
* `ws` transport is well-tested upstream; what we verify here is the
|
|
717
|
+
* bot's own protocol handling, which is the only part the user's code
|
|
718
|
+
* influences.
|
|
719
|
+
*/
|
|
720
|
+
|
|
721
|
+
type Severity = "pass" | "warn" | "fail";
|
|
722
|
+
/** Single conformance scenario result. Same shape as `ValidationResult`. */
|
|
723
|
+
interface ConformanceCheck {
|
|
724
|
+
severity: Severity;
|
|
725
|
+
name: string;
|
|
726
|
+
message: string;
|
|
727
|
+
}
|
|
728
|
+
interface RunConformanceOptions {
|
|
729
|
+
/** Per-scenario timeout in milliseconds. Default 10s. */
|
|
730
|
+
timeoutMs?: number;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Run every conformance scenario against `bot` and return per-check
|
|
734
|
+
* results. The same bot instance is reused across scenarios — matches
|
|
735
|
+
* the user's production usage shape.
|
|
736
|
+
*
|
|
737
|
+
* Note: the JavaScript harness does not currently include a hard
|
|
738
|
+
* wall-clock watchdog. A bot whose `decide()` blocks the event loop
|
|
739
|
+
* (busy-loop, sync `Atomics.wait`) will hang the harness because
|
|
740
|
+
* `setTimeout` cannot fire while the loop is starved. The Python SDK
|
|
741
|
+
* uses a daemon thread for this; the JS equivalent is a Worker and is
|
|
742
|
+
* deferred to a follow-up.
|
|
743
|
+
*/
|
|
744
|
+
declare function runConformanceChecks(bot: Bot, options?: RunConformanceOptions): Promise<ConformanceCheck[]>;
|
|
745
|
+
|
|
746
|
+
export { Action, type ActionHistoryEntry, type ActionKind, BOT_TOKEN_SUBPROTOCOL, Bot, BotDecisionError, type BotFactory, CONFIG_FILENAME, type Card, type ChipzenConfig, ChipzenConfigError, type ConformanceCheck, type ConnectToChipzenOptions, type ConnectionConfig, DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_WARN_MS, ENV_NAMES, ENV_VAR_NAME, type EnvName, type GameState, type MatchResult, PLATFORM_TIMEOUT_MS, RetryPolicy, type RetryPolicyOptions, type RunBotOptions, type RunConformanceOptions, type RunExternalBotOptions, SECTION_NAME, SUPPORTED_PROTOCOL_VERSIONS, type ScaffoldOptions, type Severity$1 as Severity, VERSION, type ValidateOptions, type ValidationResult, botTokenSubprotocols, cardFromString, cardToString, connectToChipzen, loadChipzenConfig, parseGameState, resolveGatewayUrl, resolveToken, resolveUrl, runBot, runConformanceChecks, runExternalBot, scaffoldBot, validateBot };
|