@dopamint-fun/open-sdk 0.1.0-dev.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.
@@ -0,0 +1,118 @@
1
+ /* The Participant Session signing preimages and signed send frames,
2
+ * byte-identical to `arena_session::wire`.
3
+ *
4
+ * Join, action, and resume signing bodies live here; acknowledgements are
5
+ * unsigned and encoded next to the authority decoder. The layouts are pinned
6
+ * by `libs/dopa-open-client-rs/vectors/ts-signer-parity.json`.
7
+ */
8
+ import { ByteWriter, frameSigningBytes, textBytes } from "./bytes.js";
9
+ const JOIN_DOMAIN = textBytes("arena_session::join");
10
+ const ACTION_DOMAIN = textBytes("arena_session::action");
11
+ const RESUME_DOMAIN = textBytes("arena_session::resume");
12
+ const SESSION_JOIN_REQUEST_TAG = 0x01;
13
+ const ACTION_PROPOSAL_TAG = 0x04;
14
+ const SESSION_RESUME_REQUEST_TAG = 0x0a;
15
+ export function encodeContext(writer, context) {
16
+ writer
17
+ .pushU16(context.sessionVersion)
18
+ .pushFixed(context.sessionId, 32, "session id")
19
+ .pushFixed(context.executionId, 32, "execution id")
20
+ .pushFixed(context.executionManifestDigest, 32, "execution manifest digest")
21
+ .pushFixed(context.protocolId, 32, "protocol id")
22
+ .pushU16(context.protocolVersion)
23
+ .pushFixed(context.participantId, 32, "participant id")
24
+ .pushU16(context.seat);
25
+ }
26
+ export function encodeSignature(signature) {
27
+ return new ByteWriter()
28
+ .pushU16(signature.length)
29
+ .pushBytes(signature)
30
+ .bytes();
31
+ }
32
+ export function signedFrame(body, signature) {
33
+ return new ByteWriter()
34
+ .pushBytes(body)
35
+ .pushBytes(encodeSignature(signature))
36
+ .bytes();
37
+ }
38
+ export function joinBodyBytes(request) {
39
+ const writer = new ByteWriter()
40
+ .pushU16(request.wireVersion)
41
+ .pushByte(SESSION_JOIN_REQUEST_TAG)
42
+ .pushU16(request.supportedSessionVersions.length);
43
+ for (const version of request.supportedSessionVersions)
44
+ writer.pushU16(version);
45
+ writer
46
+ .pushFixed(request.executionId, 32, "execution id")
47
+ .pushFixed(request.executionManifestDigest, 32, "execution manifest digest")
48
+ .pushFixed(request.participantId, 32, "participant id")
49
+ .pushU16(request.seat)
50
+ .pushFixed(request.clientNonce, 32, "client nonce")
51
+ .pushFixed(request.challenge, 74, "session challenge");
52
+ return writer.bytes();
53
+ }
54
+ export function joinSigningBytes(request) {
55
+ return frameSigningBytes(JOIN_DOMAIN, joinBodyBytes(request));
56
+ }
57
+ export function encodeJoinFrame(request, signature) {
58
+ return signedFrame(joinBodyBytes(request), signature);
59
+ }
60
+ export function actionBodyBytes(proposal) {
61
+ const writer = new ByteWriter()
62
+ .pushU16(proposal.context.wireVersion)
63
+ .pushByte(ACTION_PROPOSAL_TAG);
64
+ encodeContext(writer, proposal.context);
65
+ writer
66
+ .pushFixed(proposal.actionId, 32, "action id")
67
+ .pushU64(proposal.expectedStateNonce)
68
+ .pushFixed(proposal.expectedStateCommitment, 32, "expected state commitment")
69
+ .pushU64(proposal.participantDeadlineMs)
70
+ .pushU16(proposal.payloadSchemaVersion)
71
+ .pushU64(proposal.payload.length)
72
+ .pushBytes(proposal.payload);
73
+ const artifacts = proposal.artifactReferences ?? [];
74
+ writer.pushU16(artifacts.length);
75
+ for (const artifact of artifacts) {
76
+ writer
77
+ .pushU16(artifact.kind)
78
+ .pushU16(artifact.schemaVersion)
79
+ .pushFixed(artifact.digest, 32, "artifact digest")
80
+ .pushU64(artifact.byteLength);
81
+ }
82
+ return writer.bytes();
83
+ }
84
+ export function actionSigningBytes(proposal) {
85
+ return frameSigningBytes(ACTION_DOMAIN, actionBodyBytes(proposal));
86
+ }
87
+ export function encodeActionFrame(proposal, signature) {
88
+ return signedFrame(actionBodyBytes(proposal), signature);
89
+ }
90
+ function encodeWitnessedReceiptOption(writer, receipt) {
91
+ if (!receipt) {
92
+ writer.pushByte(0);
93
+ return;
94
+ }
95
+ writer
96
+ .pushByte(1)
97
+ .pushFixed(receipt.digest, 32, "receipt digest")
98
+ .pushU64(receipt.resultingState.nonce)
99
+ .pushFixed(receipt.resultingState.commitment, 32, "receipt resulting state");
100
+ }
101
+ export function resumeBodyBytes(request) {
102
+ const writer = new ByteWriter()
103
+ .pushU16(request.context.wireVersion)
104
+ .pushByte(SESSION_RESUME_REQUEST_TAG);
105
+ encodeContext(writer, request.context);
106
+ writer.pushU64(request.cursorSequence);
107
+ encodeWitnessedReceiptOption(writer, request.witnessedReceipt);
108
+ writer
109
+ .pushFixed(request.clientNonce, 32, "client nonce")
110
+ .pushFixed(request.challenge, 74, "session challenge");
111
+ return writer.bytes();
112
+ }
113
+ export function resumeSigningBytes(request) {
114
+ return frameSigningBytes(RESUME_DOMAIN, resumeBodyBytes(request));
115
+ }
116
+ export function encodeResumeFrame(request, signature) {
117
+ return signedFrame(resumeBodyBytes(request), signature);
118
+ }
@@ -0,0 +1,56 @@
1
+ export interface SeatEntitlement {
2
+ seat: number;
3
+ amount: bigint;
4
+ }
5
+ export interface TerminalState {
6
+ tunnelId: Uint8Array;
7
+ stateCommitment: Uint8Array;
8
+ nonce: bigint;
9
+ timestamp: bigint;
10
+ transcriptRoot: Uint8Array;
11
+ receiptCount: bigint;
12
+ outcomeSchemaVersion: number;
13
+ entitlements: SeatEntitlement[];
14
+ outcome: Uint8Array;
15
+ }
16
+ export declare function encodeTerminalState(state: TerminalState): Uint8Array;
17
+ export declare function decodeTerminalState(bytes: Uint8Array): TerminalState;
18
+ export declare function anchorDomainId(anchorContext: Uint8Array, purpose: Uint8Array): Uint8Array;
19
+ export declare function recomputeSettlementDigest(anchorContext: Uint8Array, terminalState: TerminalState): Uint8Array;
20
+ export interface ConsentPrompt {
21
+ version: number;
22
+ terminal: {
23
+ execution_id: string;
24
+ nonce: number;
25
+ state_commitment: string;
26
+ };
27
+ anchor_context: string;
28
+ encoded_terminal_state: string;
29
+ }
30
+ export interface ConsentRequest {
31
+ version: number;
32
+ terminal: ConsentPrompt["terminal"];
33
+ seat: number;
34
+ decision: {
35
+ kind: "consent";
36
+ signature: string;
37
+ };
38
+ }
39
+ export interface ConsentDisclosure {
40
+ prompt: ConsentPrompt;
41
+ seat: number;
42
+ executionId: Uint8Array;
43
+ finalNonce: bigint;
44
+ finalCommitment: Uint8Array;
45
+ entitlement: bigint;
46
+ }
47
+ /** Every disagreement is named before a caller is allowed to touch the seat
48
+ * key. Rust `consent_to_terminal` refuses first, then signs; this is the
49
+ * TypeScript half of that order. */
50
+ export declare function verifyConsentDisclosure(args: ConsentDisclosure): void;
51
+ export declare function buildConsentRequest(args: ConsentDisclosure & {
52
+ signature: Uint8Array;
53
+ }): ConsentRequest;
54
+ export declare function settlementConsentPath(executionIdHex: string): string;
55
+ export declare function authorityOriginFromSessionBase(sessionBaseUrl: string): string;
56
+ export declare function digestForPrompt(prompt: ConsentPrompt): Uint8Array;
@@ -0,0 +1,117 @@
1
+ import { ByteReader, ByteWriter, equalBytes, fromHex, textBytes, toHex0x, } from "./bytes.js";
2
+ import { digestFramed } from "./crypto.js";
3
+ const ANCHOR_DOMAIN_ID_V1 = textBytes("arena_tunnel::anchor_domain_id_v1");
4
+ const SETTLEMENT_PURPOSE = textBytes("arena_tunnel::settlement");
5
+ export function encodeTerminalState(state) {
6
+ const writer = new ByteWriter()
7
+ .pushU16(1)
8
+ .pushFixed(state.tunnelId, 32, "tunnel id")
9
+ .pushFixed(state.stateCommitment, 32, "state commitment")
10
+ .pushU64(state.nonce)
11
+ .pushU64(state.timestamp)
12
+ .pushFixed(state.transcriptRoot, 32, "transcript root")
13
+ .pushU64(state.receiptCount)
14
+ .pushU16(state.outcomeSchemaVersion)
15
+ .pushU16(state.entitlements.length);
16
+ for (const entitlement of state.entitlements) {
17
+ writer.pushU16(entitlement.seat).pushU64(entitlement.amount);
18
+ }
19
+ writer.pushU16(state.outcome.length).pushBytes(state.outcome);
20
+ return writer.bytes();
21
+ }
22
+ export function decodeTerminalState(bytes) {
23
+ const reader = new ByteReader(bytes);
24
+ const wire = reader.readU16("wire version");
25
+ if (wire !== 1)
26
+ throw new Error(`unsupported terminal-state wire ${wire}`);
27
+ const tunnelId = reader.readFixed(32, "tunnel id");
28
+ const stateCommitment = reader.readFixed(32, "state commitment");
29
+ const nonce = reader.readU64("nonce");
30
+ const timestamp = reader.readU64("timestamp");
31
+ const transcriptRoot = reader.readFixed(32, "transcript root");
32
+ const receiptCount = reader.readU64("receipt count");
33
+ const outcomeSchemaVersion = reader.readU16("outcome schema version");
34
+ const count = reader.readU16("entitlement count");
35
+ const entitlements = [];
36
+ for (let i = 0; i < count; i++) {
37
+ entitlements.push({
38
+ seat: reader.readU16("entitlement seat"),
39
+ amount: reader.readU64("entitlement amount"),
40
+ });
41
+ }
42
+ const outcomeLen = reader.readU16("outcome length");
43
+ const outcome = reader.readFixed(outcomeLen, "outcome");
44
+ reader.finish();
45
+ return {
46
+ tunnelId,
47
+ stateCommitment,
48
+ nonce,
49
+ timestamp,
50
+ transcriptRoot,
51
+ receiptCount,
52
+ outcomeSchemaVersion,
53
+ entitlements,
54
+ outcome,
55
+ };
56
+ }
57
+ export function anchorDomainId(anchorContext, purpose) {
58
+ const payload = new ByteWriter()
59
+ .pushFixed(anchorContext, 32, "anchor context")
60
+ .pushU64(purpose.length)
61
+ .pushBytes(purpose)
62
+ .bytes();
63
+ return digestFramed(ANCHOR_DOMAIN_ID_V1, payload);
64
+ }
65
+ export function recomputeSettlementDigest(anchorContext, terminalState) {
66
+ const domain = anchorDomainId(anchorContext, SETTLEMENT_PURPOSE);
67
+ return digestFramed(domain, encodeTerminalState(terminalState));
68
+ }
69
+ /** Every disagreement is named before a caller is allowed to touch the seat
70
+ * key. Rust `consent_to_terminal` refuses first, then signs; this is the
71
+ * TypeScript half of that order. */
72
+ export function verifyConsentDisclosure(args) {
73
+ if (args.prompt.version !== 1)
74
+ throw new Error(`unsupported consent version ${args.prompt.version}`);
75
+ const promptExecution = fromHex(args.prompt.terminal.execution_id);
76
+ if (!equalBytes(promptExecution, args.executionId) || promptExecution.length !== 32)
77
+ throw new Error("consent prompt names another execution");
78
+ const preimage = fromHex(args.prompt.encoded_terminal_state);
79
+ const terminal = decodeTerminalState(preimage);
80
+ const encoded = encodeTerminalState(terminal);
81
+ if (!equalBytes(encoded, preimage))
82
+ throw new Error("terminal-state preimage is not canonical");
83
+ if (terminal.nonce !== BigInt(args.prompt.terminal.nonce) ||
84
+ !equalBytes(terminal.stateCommitment, fromHex(args.prompt.terminal.state_commitment)))
85
+ throw new Error("preimage does not match disclosed terminal");
86
+ if (terminal.nonce !== args.finalNonce ||
87
+ !equalBytes(terminal.stateCommitment, args.finalCommitment))
88
+ throw new Error("disclosed terminal is not the terminal this seat was delivered");
89
+ const row = terminal.entitlements.find((entry) => entry.seat === args.seat);
90
+ if (!row)
91
+ throw new Error("terminal state pays this seat nothing");
92
+ if (row.amount !== args.entitlement)
93
+ throw new Error(`terminal pays ${row.amount}, seat view says ${args.entitlement}`);
94
+ }
95
+ export function buildConsentRequest(args) {
96
+ verifyConsentDisclosure(args);
97
+ return {
98
+ version: 1,
99
+ terminal: args.prompt.terminal,
100
+ seat: args.seat,
101
+ decision: { kind: "consent", signature: toHex0x(args.signature) },
102
+ };
103
+ }
104
+ export function settlementConsentPath(executionIdHex) {
105
+ const hex = executionIdHex.replace(/^0x/, "");
106
+ return `/open/v1/authority/executions/${hex}/settlements`;
107
+ }
108
+ export function authorityOriginFromSessionBase(sessionBaseUrl) {
109
+ const idx = sessionBaseUrl.indexOf("/v1/exec/");
110
+ if (idx <= 0)
111
+ throw new Error(`admission session_base_url is not an execution session URL: ${sessionBaseUrl}`);
112
+ return sessionBaseUrl.slice(0, idx);
113
+ }
114
+ export function digestForPrompt(prompt) {
115
+ const terminal = decodeTerminalState(fromHex(prompt.encoded_terminal_state));
116
+ return recomputeSettlementDigest(fromHex(prompt.anchor_context), terminal);
117
+ }
@@ -0,0 +1,52 @@
1
+ export type TexasAction = {
2
+ type: "fold";
3
+ } | {
4
+ type: "check";
5
+ } | {
6
+ type: "call";
7
+ } | {
8
+ type: "wagerTo";
9
+ amount: bigint;
10
+ };
11
+ export interface TexasLegalActions {
12
+ canFold: boolean;
13
+ canCheck: boolean;
14
+ canCall: boolean;
15
+ minWagerTo: bigint | null;
16
+ maxWagerTo: bigint | null;
17
+ }
18
+ export declare function encodeAction(action: TexasAction): Uint8Array;
19
+ export declare function decodeAction(bytes: Uint8Array): TexasAction;
20
+ export declare function encodeLegalActions(legal: TexasLegalActions): Uint8Array;
21
+ export declare function decodeLegalActions(bytes: Uint8Array): TexasLegalActions;
22
+ export type Suit = "c" | "d" | "h" | "s";
23
+ export type Rank = "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "T" | "J" | "Q" | "K" | "A";
24
+ export interface Card {
25
+ rank: Rank;
26
+ suit: Suit;
27
+ /** rank then suit, `Ah` for the ace of hearts */
28
+ label: string;
29
+ /** the wire byte, 0..51 */
30
+ value: number;
31
+ }
32
+ /** The card a wire byte names. Refuses anything past the 52-card deck. */
33
+ export declare function cardFromByte(value: number): Card;
34
+ export interface TexasSeatView {
35
+ /** the seat this view was cut for */
36
+ receivingSeat: number;
37
+ /** the seat's hole cards; null once it has folded or the hand is over */
38
+ holeCards: [Card, Card] | null;
39
+ /** the canonical match state, framed but not decoded */
40
+ state: Uint8Array;
41
+ }
42
+ /** Decode one seat's view down to what the seat can act on.
43
+ *
44
+ * Framing is checked exactly — the header, the state length against the
45
+ * payload, the presence byte, and that nothing trails the last card — so a
46
+ * payload that is not a view fails here rather than yielding two bytes that
47
+ * look like cards. What is not checked is the state's own canonical form;
48
+ * that needs the state codec, and is the Rust decoder's job until this side
49
+ * carries one. */
50
+ export declare function decodeParticipantView(bytes: Uint8Array): TexasSeatView;
51
+ export declare function decodePlayerActionInput(bytes: Uint8Array, expectedNonce: bigint, expectedCommitment: Uint8Array): TexasAction;
52
+ export declare function pickAction(legal: TexasLegalActions, strategy: "fold-heavy" | "all-in"): TexasAction;
package/dist/texas.js ADDED
@@ -0,0 +1,217 @@
1
+ /* Canonical Texas Hold'em action and legal-action codecs, byte-identical to
2
+ * `arena_texas_holdem::wire`. The session client needs these to turn a view's
3
+ * legal-action schema into a move and to put that move on the action payload.
4
+ */
5
+ import { ByteReader, ByteWriter } from "./bytes.js";
6
+ const WIRE_VERSION = 1;
7
+ const PROTOCOL_VERSION = 1;
8
+ const SCHEMA_VERSION = 1;
9
+ function pushHeader(writer) {
10
+ writer
11
+ .pushU16(WIRE_VERSION)
12
+ .pushU16(PROTOCOL_VERSION)
13
+ .pushU16(SCHEMA_VERSION);
14
+ }
15
+ function readHeader(reader) {
16
+ const wire = reader.readU16("wire version");
17
+ if (wire !== WIRE_VERSION)
18
+ throw new Error(`unsupported texas wire ${wire}`);
19
+ const protocol = reader.readU16("protocol version");
20
+ if (protocol !== PROTOCOL_VERSION)
21
+ throw new Error(`invalid texas protocol ${protocol}`);
22
+ const schema = reader.readU16("schema version");
23
+ if (schema !== SCHEMA_VERSION)
24
+ throw new Error(`invalid texas schema ${schema}`);
25
+ }
26
+ function readBool(reader, field) {
27
+ const tag = reader.readByte(field);
28
+ if (tag === 0)
29
+ return false;
30
+ if (tag === 1)
31
+ return true;
32
+ throw new Error(`invalid ${field} tag ${tag}`);
33
+ }
34
+ function readOptionalWager(reader, presenceField, valueField) {
35
+ const tag = reader.readByte(presenceField);
36
+ if (tag === 0)
37
+ return null;
38
+ if (tag === 1)
39
+ return reader.readU64(valueField);
40
+ throw new Error(`invalid ${presenceField} tag ${tag}`);
41
+ }
42
+ export function encodeAction(action) {
43
+ if (action.type === "wagerTo" && action.amount === 0n)
44
+ throw new Error("wager total must be nonzero");
45
+ const writer = new ByteWriter();
46
+ pushHeader(writer);
47
+ switch (action.type) {
48
+ case "fold":
49
+ writer.pushByte(0);
50
+ break;
51
+ case "check":
52
+ writer.pushByte(1);
53
+ break;
54
+ case "call":
55
+ writer.pushByte(2);
56
+ break;
57
+ case "wagerTo":
58
+ writer.pushByte(3).pushU64(action.amount);
59
+ break;
60
+ }
61
+ return writer.bytes();
62
+ }
63
+ export function decodeAction(bytes) {
64
+ const reader = new ByteReader(bytes);
65
+ readHeader(reader);
66
+ const tag = reader.readByte("action tag");
67
+ let action;
68
+ switch (tag) {
69
+ case 0:
70
+ action = { type: "fold" };
71
+ break;
72
+ case 1:
73
+ action = { type: "check" };
74
+ break;
75
+ case 2:
76
+ action = { type: "call" };
77
+ break;
78
+ case 3: {
79
+ const amount = reader.readU64("wager total");
80
+ if (amount === 0n)
81
+ throw new Error("wager total must be nonzero");
82
+ action = { type: "wagerTo", amount };
83
+ break;
84
+ }
85
+ default:
86
+ throw new Error(`invalid action tag ${tag}`);
87
+ }
88
+ reader.finish();
89
+ return action;
90
+ }
91
+ export function encodeLegalActions(legal) {
92
+ if ((legal.minWagerTo === null) !== (legal.maxWagerTo === null) ||
93
+ (legal.minWagerTo !== null &&
94
+ legal.maxWagerTo !== null &&
95
+ legal.minWagerTo > legal.maxWagerTo))
96
+ throw new Error("wager bounds must be paired and ordered");
97
+ const writer = new ByteWriter();
98
+ pushHeader(writer);
99
+ writer
100
+ .pushByte(legal.canFold ? 1 : 0)
101
+ .pushByte(legal.canCheck ? 1 : 0)
102
+ .pushByte(legal.canCall ? 1 : 0);
103
+ if (legal.minWagerTo === null)
104
+ writer.pushByte(0);
105
+ else
106
+ writer.pushByte(1).pushU64(legal.minWagerTo);
107
+ if (legal.maxWagerTo === null)
108
+ writer.pushByte(0);
109
+ else
110
+ writer.pushByte(1).pushU64(legal.maxWagerTo);
111
+ return writer.bytes();
112
+ }
113
+ export function decodeLegalActions(bytes) {
114
+ const reader = new ByteReader(bytes);
115
+ readHeader(reader);
116
+ const legal = {
117
+ canFold: readBool(reader, "can_fold"),
118
+ canCheck: readBool(reader, "can_check"),
119
+ canCall: readBool(reader, "can_call"),
120
+ minWagerTo: readOptionalWager(reader, "min_wager_to presence", "min_wager_to"),
121
+ maxWagerTo: readOptionalWager(reader, "max_wager_to presence", "max_wager_to"),
122
+ };
123
+ reader.finish();
124
+ const encoded = encodeLegalActions(legal);
125
+ if (encoded.length !== bytes.length ||
126
+ !encoded.every((b, i) => b === bytes[i]))
127
+ throw new Error("non-canonical legal actions");
128
+ return legal;
129
+ }
130
+ const RANKS = [
131
+ "2",
132
+ "3",
133
+ "4",
134
+ "5",
135
+ "6",
136
+ "7",
137
+ "8",
138
+ "9",
139
+ "T",
140
+ "J",
141
+ "Q",
142
+ "K",
143
+ "A",
144
+ ];
145
+ const SUITS = ["c", "d", "h", "s"];
146
+ /** The card a wire byte names. Refuses anything past the 52-card deck. */
147
+ export function cardFromByte(value) {
148
+ if (!Number.isInteger(value) || value < 0 || value > 51)
149
+ throw new Error(`not a card byte: ${value}`);
150
+ const suit = SUITS[Math.floor(value / 13)];
151
+ const rank = RANKS[value % 13];
152
+ return { rank, suit, label: `${rank}${suit}`, value };
153
+ }
154
+ /** Decode one seat's view down to what the seat can act on.
155
+ *
156
+ * Framing is checked exactly — the header, the state length against the
157
+ * payload, the presence byte, and that nothing trails the last card — so a
158
+ * payload that is not a view fails here rather than yielding two bytes that
159
+ * look like cards. What is not checked is the state's own canonical form;
160
+ * that needs the state codec, and is the Rust decoder's job until this side
161
+ * carries one. */
162
+ export function decodeParticipantView(bytes) {
163
+ const reader = new ByteReader(bytes);
164
+ readHeader(reader);
165
+ const length = reader.readU64("participant view state length");
166
+ if (length > BigInt(reader.remaining()))
167
+ throw new Error("participant view state length exceeds the payload");
168
+ const state = reader.readFixed(Number(length), "participant view state");
169
+ const receivingSeat = reader.readU16("participant view recipient");
170
+ const presence = reader.readByte("private card presence");
171
+ let holeCards = null;
172
+ if (presence === 1) {
173
+ holeCards = [
174
+ cardFromByte(reader.readByte("first private card")),
175
+ cardFromByte(reader.readByte("second private card")),
176
+ ];
177
+ }
178
+ else if (presence !== 0) {
179
+ throw new Error(`invalid private card presence ${presence}`);
180
+ }
181
+ reader.finish();
182
+ return { receivingSeat, holeCards, state };
183
+ }
184
+ export function decodePlayerActionInput(bytes, expectedNonce, expectedCommitment) {
185
+ const reader = new ByteReader(bytes);
186
+ readHeader(reader);
187
+ const nonce = reader.readU64("expected state nonce");
188
+ const commitment = reader.readFixed(32, "expected state commitment");
189
+ if (nonce !== expectedNonce ||
190
+ !commitment.every((b, i) => b === expectedCommitment[i]))
191
+ throw new Error("transition input expected state mismatch");
192
+ const tag = reader.readByte("transition input tag");
193
+ if (tag !== 0)
194
+ throw new Error("transition input is not a player action");
195
+ const length = reader.readU16("action length");
196
+ const action = decodeAction(reader.readFixed(length, "action"));
197
+ reader.finish();
198
+ return action;
199
+ }
200
+ export function pickAction(legal, strategy) {
201
+ if (strategy === "all-in") {
202
+ if (legal.maxWagerTo !== null)
203
+ return { type: "wagerTo", amount: legal.maxWagerTo };
204
+ if (legal.canCall)
205
+ return { type: "call" };
206
+ if (legal.canCheck)
207
+ return { type: "check" };
208
+ return { type: "fold" };
209
+ }
210
+ if (legal.canFold)
211
+ return { type: "fold" };
212
+ if (legal.canCheck)
213
+ return { type: "check" };
214
+ if (legal.canCall)
215
+ return { type: "call" };
216
+ return { type: "fold" };
217
+ }
package/dist/tour.d.ts ADDED
@@ -0,0 +1,101 @@
1
+ import type { AgentKeypair } from "./keypair.js";
2
+ export type TourKind = "playground" | "tournament";
3
+ export interface TourClient {
4
+ productUrl: string;
5
+ agent: AgentKeypair;
6
+ /** 32 bytes */
7
+ agentId: Uint8Array;
8
+ }
9
+ export type TourEntry = {
10
+ state: "waiting";
11
+ waiting: number;
12
+ seatsNeeded: number;
13
+ fillAtMs: number;
14
+ /** how many seats the house will take at `fillAtMs` if nobody else comes */
15
+ houseSeatsAtFill?: number;
16
+ }
17
+ /** The queue composed an offer this agent accepts and plays itself. Since
18
+ * ADR-0175 this is the answer for every external agent: the product holds
19
+ * no key that could sign this seat's moves. */
20
+ | {
21
+ state: "offered";
22
+ offerId: string;
23
+ seat: number;
24
+ settlement: string;
25
+ mode: string;
26
+ } | {
27
+ state: "seated";
28
+ tableId: string;
29
+ settlement: string;
30
+ mode: string;
31
+ executionId?: string;
32
+ seats: {
33
+ seat: number;
34
+ occupant: string;
35
+ }[];
36
+ } | {
37
+ state: "closed" | "unpaid";
38
+ [key: string]: unknown;
39
+ };
40
+ export declare function enterTour(client: TourClient, tour: TourKind): Promise<TourEntry>;
41
+ export interface QueueOptions {
42
+ /** How long to keep polling before giving up. */
43
+ timeoutMs?: number;
44
+ /** Gap between polls. The queue answers immediately; this paces the caller. */
45
+ pollMs?: number;
46
+ onWaiting?: (entry: Extract<TourEntry, {
47
+ state: "waiting";
48
+ }>) => void;
49
+ }
50
+ /** Enter and poll until seated.
51
+ *
52
+ * Waiting is the normal answer, not a failure: the playground holds a place
53
+ * for real opponents and only house-fills the leftover seats once the wait
54
+ * elapses, so an agent that gives up early is the reason a table stays short.
55
+ * The default timeout therefore outlasts the server's own fill wait. */
56
+ export declare function queueUntilSeated(client: TourClient, tour: TourKind, options?: QueueOptions): Promise<Extract<TourEntry, {
57
+ state: "seated" | "offered";
58
+ }>>;
59
+ export interface TurnView {
60
+ state: string;
61
+ seat?: number;
62
+ handIndex?: number;
63
+ cards?: string[];
64
+ board?: string[];
65
+ pot?: number;
66
+ callAmount?: number;
67
+ chips?: {
68
+ seat: number;
69
+ chips: number;
70
+ }[];
71
+ canFold?: boolean;
72
+ canCheck?: boolean;
73
+ canCall?: boolean;
74
+ actsForYouAtMs?: number;
75
+ [key: string]: unknown;
76
+ }
77
+ export declare function readPosition(client: TourClient, tableId: string): Promise<TurnView>;
78
+ export interface TableMove {
79
+ action: "fold" | "check" | "call" | "wager";
80
+ wagerTo?: number;
81
+ say?: string;
82
+ }
83
+ export declare function act(client: TourClient, tableId: string, move: TableMove): Promise<TurnView>;
84
+ /** The reference strategy: never wager, call only what is free. It exists so
85
+ * the journey can be demonstrated end to end, not because it plays well - an
86
+ * agent that reasons about the hand replaces `decide`. */
87
+ export declare function foldHeavy(turn: TurnView): TableMove;
88
+ export interface TourPlayReport {
89
+ outcome: string;
90
+ committedActions: number;
91
+ hands: number;
92
+ }
93
+ /** Drive a seated tour table to a terminal disposition.
94
+ *
95
+ * A lapsed turn is not an error: the authority acts for a silent seat on its
96
+ * own clock, so a slow decision costs the hand rather than the match, and the
97
+ * loop keeps reading. Only a refusal the server calls terminal stops it. */
98
+ export declare function playTour(client: TourClient, tableId: string, decide?: (turn: TurnView) => TableMove, options?: {
99
+ pollMs?: number;
100
+ onTurn?: (turn: TurnView, move: TableMove) => void;
101
+ }): Promise<TourPlayReport>;