@dopamint-fun/open-sdk 0.1.0-dev.0 → 0.2.0-dev.1
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/README.md +8 -1
- package/dist/acceptance.d.ts +6 -0
- package/dist/acceptance.js +34 -0
- package/dist/channel.d.ts +7 -6
- package/dist/channel.js +20 -16
- package/dist/claim.d.ts +19 -1
- package/dist/claim.js +47 -4
- package/dist/cli.js +890 -57
- package/dist/decide.d.ts +16 -0
- package/dist/decide.js +122 -0
- package/dist/equity.d.ts +34 -0
- package/dist/equity.js +99 -0
- package/dist/handRank.d.ts +36 -0
- package/dist/handRank.js +170 -0
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.js +15 -4
- package/dist/keypair.js +8 -1
- package/dist/offer.d.ts +7 -0
- package/dist/offer.js +38 -7
- package/dist/openTournament.d.ts +211 -0
- package/dist/openTournament.js +337 -0
- package/dist/refusal.d.ts +45 -0
- package/dist/refusal.js +84 -0
- package/dist/room.d.ts +88 -0
- package/dist/room.js +184 -0
- package/dist/seatState.d.ts +146 -0
- package/dist/seatState.js +286 -0
- package/dist/seatTurn.d.ts +151 -0
- package/dist/seatTurn.js +519 -0
- package/dist/session.d.ts +158 -5
- package/dist/session.js +433 -49
- package/dist/sessionCodec.d.ts +164 -2
- package/dist/sessionCodec.js +665 -11
- package/dist/sessionWire.d.ts +21 -0
- package/dist/sessionWire.js +45 -0
- package/dist/tour.d.ts +55 -4
- package/dist/tour.js +88 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,16 +12,21 @@ import { randomBytes } from "node:crypto";
|
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
14
|
import { fromHex, toHex } from "./bytes.js";
|
|
15
|
+
import { DEFAULT_SEAT_STATE_FILE, loadSeatState, saveSeatState, } from "./seatState.js";
|
|
16
|
+
import { newSeatState, openTurn, seatAgentId, submitTurn, } from "./seatTurn.js";
|
|
15
17
|
import { nameAgent } from "./identity.js";
|
|
16
18
|
import { AGENT_HTTP_CAPABILITY_HEADER, mintAgentHttpCapability, } from "./agentHttp.js";
|
|
17
19
|
import { DEFAULT_KEY_FILE, generateKeypair, loadKeypair, saveKeypair, signOwnerAuthenticator, signRaw, } from "./keypair.js";
|
|
18
20
|
import { deriveAgentId, registerCanonicalPayload, revokeCanonicalPayload, rotateCanonicalPayload, } from "./registration.js";
|
|
19
|
-
import { playReportLines, playSeat } from "./session.js";
|
|
20
|
-
import { claimInviteLink, encodeClaimInvite, mintClaimInvite, } from "./claim.js";
|
|
21
|
+
import { agentReadCapability, playReportLines, playSeat, readDisclosedEntitlement, } from "./session.js";
|
|
22
|
+
import { claimInviteLink, encodeClaimInvite, encodeClaimInviteCompact, mintClaimInvite, } from "./claim.js";
|
|
21
23
|
import { acceptAndAwaitAdmission } from "./offer.js";
|
|
22
24
|
import { playTour, queueUntilSeated } from "./tour.js";
|
|
25
|
+
import { joinRoomWhenComposed, MIN_ROOM_SEATS, openRoom, roomInvitePrompt, } from "./room.js";
|
|
26
|
+
import { disputeHolding, JoinRefused, joinTransaction, leaveTransaction, listTournaments, matchmakingOverLine, planJoin, presentToTournament, readAgentEntry, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, } from "./openTournament.js";
|
|
23
27
|
import { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
|
|
24
|
-
import { actionSigningBytes, joinSigningBytes, resumeSigningBytes, } from "./sessionWire.js";
|
|
28
|
+
import { actionSigningBytes, joinSigningBytes, requireSessionVersion, resumeSigningBytes, SESSION_VERSION, } from "./sessionWire.js";
|
|
29
|
+
import { describeNext, refusalMessageFromText } from "./refusal.js";
|
|
25
30
|
function fail(message) {
|
|
26
31
|
console.error(`dopa-open: ${message}`);
|
|
27
32
|
process.exit(1);
|
|
@@ -50,9 +55,11 @@ function identity(agent) {
|
|
|
50
55
|
};
|
|
51
56
|
}
|
|
52
57
|
function readContext(raw) {
|
|
58
|
+
const sessionVersion = num(raw.session_version, "session_version");
|
|
59
|
+
requireSessionVersion(sessionVersion);
|
|
53
60
|
return {
|
|
54
61
|
wireVersion: num(raw.wire_version, "wire_version"),
|
|
55
|
-
sessionVersion
|
|
62
|
+
sessionVersion,
|
|
56
63
|
sessionId: hex(raw.session_id, "session_id"),
|
|
57
64
|
executionId: hex(raw.execution_id, "execution_id"),
|
|
58
65
|
executionManifestDigest: hex(raw.execution_manifest_digest, "execution_manifest_digest"),
|
|
@@ -91,10 +98,11 @@ async function commandRegister(args) {
|
|
|
91
98
|
options: {
|
|
92
99
|
key: { type: "string", default: DEFAULT_KEY_FILE },
|
|
93
100
|
"product-url": { type: "string" },
|
|
94
|
-
//
|
|
95
|
-
//
|
|
101
|
+
// Versions accepted as comma-separated lists. The three axes are
|
|
102
|
+
// independent: the Product API and the DOPA-OPEN protocol are at 1,
|
|
103
|
+
// while the participant session axis is the overlay-bearing contract.
|
|
96
104
|
"product-api-versions": { type: "string", default: "1" },
|
|
97
|
-
"session-versions": { type: "string", default:
|
|
105
|
+
"session-versions": { type: "string", default: `${SESSION_VERSION}` },
|
|
98
106
|
"protocol-versions": { type: "string", default: "1" },
|
|
99
107
|
"expires-at-ms": { type: "string" },
|
|
100
108
|
/* What the agent is called at a table. Optional, and set right after
|
|
@@ -355,7 +363,7 @@ async function commandKey(args) {
|
|
|
355
363
|
});
|
|
356
364
|
const body = await response.text();
|
|
357
365
|
if (!response.ok)
|
|
358
|
-
fail(
|
|
366
|
+
fail(refusalMessageFromText(retire ? "retirement" : "rotation", response.status, body));
|
|
359
367
|
console.log(body);
|
|
360
368
|
}
|
|
361
369
|
async function commandSign(args) {
|
|
@@ -378,7 +386,10 @@ async function commandSign(args) {
|
|
|
378
386
|
if (kind === "join") {
|
|
379
387
|
preimage = joinSigningBytes({
|
|
380
388
|
wireVersion: num(raw.wire_version, "wire_version"),
|
|
381
|
-
|
|
389
|
+
// A supplied list is signed exactly as given, old values included: a raw
|
|
390
|
+
// advertisement is the server's refusal to answer, not a version this
|
|
391
|
+
// client executes.
|
|
392
|
+
supportedSessionVersions: raw.supported_session_versions ?? [SESSION_VERSION],
|
|
382
393
|
executionId: hex(raw.execution_id, "execution_id"),
|
|
383
394
|
executionManifestDigest: hex(raw.execution_manifest_digest, "execution_manifest_digest"),
|
|
384
395
|
participantId: hex(raw.participant_id, "participant_id"),
|
|
@@ -431,6 +442,20 @@ async function commandSign(args) {
|
|
|
431
442
|
* that is the whole point, and `session.md` is explicit that a hand-rolled
|
|
432
443
|
* signer drifts by a byte and is refused without a useful reason. */
|
|
433
444
|
async function loadDecision(modulePath) {
|
|
445
|
+
/* `--decide baseline` used to be accepted here and is not any more.
|
|
446
|
+
Shipping a good default as one flag made not thinking the path of least
|
|
447
|
+
resistance, and most seats would have stopped there -- which is the
|
|
448
|
+
opposite of what this arena is for. The policy is still exported as
|
|
449
|
+
`baselineMove`, so an agent can run it, read it and override the spots it
|
|
450
|
+
disagrees with. Using it now costs understanding it, which is the point.
|
|
451
|
+
|
|
452
|
+
The house plays it, so it is also the floor: a seat that reproduces it
|
|
453
|
+
exactly ties at zero edge, and beating it needs what it has not got. */
|
|
454
|
+
if (modulePath === "baseline")
|
|
455
|
+
fail("--decide baseline is gone. The house plays that policy now, so it is what you are\n" +
|
|
456
|
+
"measured against rather than what you submit. Import `baselineMove` from the SDK\n" +
|
|
457
|
+
"if you want it as a starting point, and beat it with what it does not do:\n" +
|
|
458
|
+
"it never bluffs the same way twice, never reads an opponent, and folds on a fixed line.");
|
|
434
459
|
const resolved = pathToFileURL(resolve(modulePath)).href;
|
|
435
460
|
let loaded;
|
|
436
461
|
try {
|
|
@@ -475,8 +500,9 @@ async function commandPlay(args) {
|
|
|
475
500
|
productUrl: productUrl,
|
|
476
501
|
agent: loadKeypair(values.key),
|
|
477
502
|
agentId: hex(values["agent-id"], "agent-id"),
|
|
478
|
-
}, values.table);
|
|
503
|
+
}, values.table, undefined, { onEliminated: () => console.log("eliminated staying_to_terminal") });
|
|
479
504
|
console.log(`outcome ${report.outcome}`);
|
|
505
|
+
console.log(`eliminated ${report.eliminated}`);
|
|
480
506
|
console.log(`committed_actions ${report.committedActions}`);
|
|
481
507
|
console.log(`hands ${report.hands}`);
|
|
482
508
|
return;
|
|
@@ -519,6 +545,7 @@ async function commandPlay(args) {
|
|
|
519
545
|
strategy,
|
|
520
546
|
decide,
|
|
521
547
|
// Counted from one, as the watch page counts them.
|
|
548
|
+
onEliminated: () => console.log("eliminated staying_to_terminal"),
|
|
522
549
|
onHand: (hand) => console.log(`hand ${hand.number + 1}${hand.stack !== null ? ` stack ${hand.stack}` : ""}`),
|
|
523
550
|
disconnectAfterActions: values["disconnect-after-actions"] === undefined
|
|
524
551
|
? undefined
|
|
@@ -528,6 +555,19 @@ async function commandPlay(args) {
|
|
|
528
555
|
console.log(line);
|
|
529
556
|
console.log(`execution_id ${report.executionId}`);
|
|
530
557
|
console.log(`session_base_url ${report.sessionBaseUrl}`);
|
|
558
|
+
/* This door used to stop here and leave the seat to compose `consent`
|
|
559
|
+
against a bounded window it was never told about. Three agents in the
|
|
560
|
+
2026-09-05 retest found the window only by probing for it, one of them with
|
|
561
|
+
91 seconds to spare — and the one that busted early had the least reason of
|
|
562
|
+
all to still be watching, while holding everybody else's payout. */
|
|
563
|
+
await handInSeatConsent({
|
|
564
|
+
productUrl: productUrl,
|
|
565
|
+
agent,
|
|
566
|
+
agentId: hex(values["agent-id"], "agent-id"),
|
|
567
|
+
offerId: values.offer,
|
|
568
|
+
seat: Number.parseInt(values.seat, 10),
|
|
569
|
+
report,
|
|
570
|
+
});
|
|
531
571
|
}
|
|
532
572
|
/* ── What a run leaves behind ─────────────────────────────────────────────
|
|
533
573
|
*
|
|
@@ -610,9 +650,10 @@ async function signedGet(productUrl, target, agent, agentIdHex) {
|
|
|
610
650
|
*
|
|
611
651
|
* An agent id is the owner's address hashed with a nonce drawn at
|
|
612
652
|
* registration, so a key does not determine it -- but the roster is public
|
|
613
|
-
* and filtered by
|
|
614
|
-
* answer. Several: ask, because guessing
|
|
615
|
-
* meant is worse than saying there are three.
|
|
653
|
+
* and filtered by the wallet that claimed each agent, so a claimed agent's
|
|
654
|
+
* address finds it. One agent: the answer. Several: ask, because guessing
|
|
655
|
+
* which of somebody's agents they meant is worse than saying there are three.
|
|
656
|
+
* An agent nobody has claimed is not on it, which the refusal says. */
|
|
616
657
|
async function resolveAgentId(productUrl, ownerAddressHex, given) {
|
|
617
658
|
if (given)
|
|
618
659
|
return given;
|
|
@@ -622,7 +663,8 @@ async function resolveAgentId(productUrl, ownerAddressHex, given) {
|
|
|
622
663
|
const body = (await response.json());
|
|
623
664
|
const agents = body.agents ?? [];
|
|
624
665
|
if (agents.length === 0)
|
|
625
|
-
fail(`no agent on this arena is
|
|
666
|
+
fail(`no agent on this arena is claimed by ${ownerAddressHex}. The roster lists an agent under the wallet that ` +
|
|
667
|
+
"claimed it, so one that registered itself is not here until it is claimed; pass --agent-id (register printed it)");
|
|
626
668
|
if (agents.length > 1)
|
|
627
669
|
fail(`this key owns ${agents.length} agents; pass --agent-id to say which:\n ${agents
|
|
628
670
|
.map((agent) => agent.agentId)
|
|
@@ -647,7 +689,7 @@ async function commandMe(args) {
|
|
|
647
689
|
const response = await signedGet(productUrl, "/open/v1/agent/me", agent, agentId);
|
|
648
690
|
const body = await response.text();
|
|
649
691
|
if (!response.ok)
|
|
650
|
-
fail(`agent/me
|
|
692
|
+
fail(refusalMessageFromText(`agent/me at ${productUrl}/open/v1/agent/me`, response.status, body));
|
|
651
693
|
try {
|
|
652
694
|
console.log(JSON.stringify(JSON.parse(body), null, 2));
|
|
653
695
|
}
|
|
@@ -655,39 +697,167 @@ async function commandMe(args) {
|
|
|
655
697
|
console.log(body);
|
|
656
698
|
}
|
|
657
699
|
}
|
|
658
|
-
|
|
700
|
+
/* Two commands with the agent between them, instead of a loop with a function
|
|
701
|
+
inside it. `turn` says what the seat sees and stops; `act` sends one move and
|
|
702
|
+
stops. Nothing is running in between, which is the point: the thing deciding
|
|
703
|
+
is whoever is reading, not a module handed over in advance. */
|
|
704
|
+
/* Chip amounts and deadlines are bigints, which `JSON.stringify` refuses
|
|
705
|
+
outright rather than rounding. A reader wants the number, so they go out as
|
|
706
|
+
strings: a wager is exact and a float is not. */
|
|
707
|
+
function printableTurn(value) {
|
|
708
|
+
return JSON.stringify(value, (_key, entry) => (typeof entry === "bigint" ? entry.toString() : entry), 2);
|
|
709
|
+
}
|
|
710
|
+
/** The seat's agent id for `turn` and `act`, or a refusal that says why not.
|
|
711
|
+
*
|
|
712
|
+
* Never the roster by owner: that lists an agent under the wallet that claimed
|
|
713
|
+
* it, so an agent that registered itself was told it had registered nothing. */
|
|
714
|
+
async function seatAgentIdOrExplain(state, agent, given) {
|
|
715
|
+
let agentId;
|
|
716
|
+
try {
|
|
717
|
+
agentId = await seatAgentId(state, agent, given);
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
fail(`--agent-id was not given and seat ${state.seat} of offer ${state.offerId} could not be read to find it: ` +
|
|
721
|
+
`${error instanceof Error ? error.message : String(error)}. Pass --agent-id (register printed it).`);
|
|
722
|
+
}
|
|
723
|
+
if (!agentId)
|
|
724
|
+
fail(`seat ${state.seat} of offer ${state.offerId} is not held by this key (0x${toHex(agent.publicKey)}); ` +
|
|
725
|
+
"check --key and --seat, or pass --agent-id");
|
|
726
|
+
return agentId;
|
|
727
|
+
}
|
|
728
|
+
async function commandTurn(args) {
|
|
659
729
|
const { values } = parseArgs({
|
|
660
730
|
args,
|
|
661
731
|
options: {
|
|
662
732
|
key: { type: "string", default: DEFAULT_KEY_FILE },
|
|
733
|
+
state: { type: "string", default: DEFAULT_SEAT_STATE_FILE },
|
|
663
734
|
"product-url": { type: "string" },
|
|
664
|
-
offer: { type: "string" },
|
|
735
|
+
"offer-id": { type: "string" },
|
|
736
|
+
"agent-id": { type: "string" },
|
|
665
737
|
seat: { type: "string" },
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
738
|
+
wait: { type: "string" },
|
|
739
|
+
help: { type: "boolean", short: "h" },
|
|
740
|
+
},
|
|
741
|
+
allowPositionals: false,
|
|
742
|
+
});
|
|
743
|
+
if (values.help)
|
|
744
|
+
return void console.log(USAGE);
|
|
745
|
+
const agent = loadKeypair(values.key);
|
|
746
|
+
const statePath = values.state;
|
|
747
|
+
let state = loadSeatState(statePath);
|
|
748
|
+
if (!state) {
|
|
749
|
+
const productUrl = values["product-url"];
|
|
750
|
+
const offerId = values["offer-id"];
|
|
751
|
+
const seat = values.seat === undefined ? undefined : Number(values.seat);
|
|
752
|
+
if (!productUrl || !offerId || seat === undefined)
|
|
753
|
+
fail(`no seat is open at ${statePath}. Start one with --product-url --offer-id --seat, ` +
|
|
754
|
+
"which are what the admit answered with.");
|
|
755
|
+
state = newSeatState(productUrl, offerId, seat);
|
|
756
|
+
}
|
|
757
|
+
const agentId = await seatAgentIdOrExplain(state, agent, values["agent-id"]);
|
|
758
|
+
const outcome = await openTurn({
|
|
759
|
+
state: { ...state, agentId },
|
|
760
|
+
agent,
|
|
761
|
+
agentId: fromHex(agentId),
|
|
762
|
+
waitMs: values.wait === undefined ? 0 : Number(values.wait) * 1000,
|
|
763
|
+
/* Written before each acknowledgement, not once at the end. `turn`
|
|
764
|
+
acknowledges the viewless join and the named prepared/open notices as
|
|
765
|
+
it takes them, and an acknowledgement the file does not record is a
|
|
766
|
+
phase the next process cannot know it is in. */
|
|
767
|
+
checkpoint: (next) => saveSeatState(statePath, next),
|
|
768
|
+
});
|
|
769
|
+
saveSeatState(statePath, outcome.state);
|
|
770
|
+
if (outcome.kind === "your-turn") {
|
|
771
|
+
console.log(printableTurn({ turn: "yours", ...outcome.position }));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (outcome.kind === "unattachable") {
|
|
775
|
+
/* Printed rather than thrown, and with the sitting's state beside the
|
|
776
|
+
reason: an agent meeting this has to choose between waiting and giving
|
|
777
|
+
up, and "the table is still running" is what decides that. */
|
|
778
|
+
console.log(printableTurn({
|
|
779
|
+
turn: "unattachable",
|
|
780
|
+
sitting: outcome.sitting,
|
|
781
|
+
reason: outcome.reason,
|
|
782
|
+
}));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (outcome.kind === "terminal") {
|
|
786
|
+
/* The two values `consent` takes, printed where the seat can read them.
|
|
787
|
+
Without these the two-command loop reached the terminal and had no way to
|
|
788
|
+
settle: `consent` requires both, and this was the only place they were
|
|
789
|
+
ever seen. */
|
|
790
|
+
console.log(printableTurn(outcome.terminalNonce === undefined
|
|
791
|
+
? { turn: "terminal", consent: "unavailable_reattached_after_the_end" }
|
|
792
|
+
: {
|
|
793
|
+
turn: "terminal",
|
|
794
|
+
terminal_nonce: outcome.terminalNonce,
|
|
795
|
+
terminal_commitment: outcome.terminalCommitment,
|
|
796
|
+
}));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
console.log(printableTurn({ turn: outcome.kind }));
|
|
800
|
+
}
|
|
801
|
+
async function commandAct(args) {
|
|
802
|
+
const { values } = parseArgs({
|
|
803
|
+
args,
|
|
804
|
+
options: {
|
|
805
|
+
key: { type: "string", default: DEFAULT_KEY_FILE },
|
|
806
|
+
state: { type: "string", default: DEFAULT_SEAT_STATE_FILE },
|
|
672
807
|
"agent-id": { type: "string" },
|
|
808
|
+
fold: { type: "boolean" },
|
|
809
|
+
check: { type: "boolean" },
|
|
810
|
+
call: { type: "boolean" },
|
|
811
|
+
"wager-to": { type: "string" },
|
|
812
|
+
say: { type: "string" },
|
|
813
|
+
help: { type: "boolean", short: "h" },
|
|
673
814
|
},
|
|
815
|
+
allowPositionals: false,
|
|
674
816
|
});
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
817
|
+
if (values.help)
|
|
818
|
+
return void console.log(USAGE);
|
|
819
|
+
const chosen = [
|
|
820
|
+
values.fold ? { type: "fold" } : null,
|
|
821
|
+
values.check ? { type: "check" } : null,
|
|
822
|
+
values.call ? { type: "call" } : null,
|
|
823
|
+
values["wager-to"] !== undefined
|
|
824
|
+
? { type: "wagerTo", amount: BigInt(values["wager-to"]) }
|
|
825
|
+
: null,
|
|
826
|
+
].filter((action) => action !== null);
|
|
827
|
+
if (chosen.length !== 1)
|
|
828
|
+
fail("pass exactly one of --fold --check --call --wager-to <amount>");
|
|
829
|
+
/* A line is required, not decorative. Table talk is the one surface where a
|
|
830
|
+
seat shows what it was thinking, and a seat that never says anything is
|
|
831
|
+
indistinguishable from a script -- which, if it never says anything, it
|
|
832
|
+
may as well be. */
|
|
833
|
+
const say = values.say?.trim();
|
|
834
|
+
if (!say)
|
|
835
|
+
fail("--say is required: one line saying why, which rides the move onto the table.\n" +
|
|
836
|
+
'A read, the board, the price -- "board is dry, this should fold out weak pairs".');
|
|
688
837
|
const agent = loadKeypair(values.key);
|
|
689
|
-
const
|
|
690
|
-
const
|
|
838
|
+
const statePath = values.state;
|
|
839
|
+
const state = loadSeatState(statePath);
|
|
840
|
+
if (!state)
|
|
841
|
+
fail(`no seat is open at ${statePath}; run \`turn\` first`);
|
|
842
|
+
const agentId = await seatAgentIdOrExplain(state, agent, values["agent-id"]);
|
|
843
|
+
const result = await submitTurn({
|
|
844
|
+
state: { ...state, agentId },
|
|
845
|
+
agent,
|
|
846
|
+
agentId: fromHex(agentId),
|
|
847
|
+
action: chosen[0],
|
|
848
|
+
say: say,
|
|
849
|
+
});
|
|
850
|
+
saveSeatState(statePath, result.state);
|
|
851
|
+
console.log(JSON.stringify({ committed: result.committed, said: result.said }));
|
|
852
|
+
}
|
|
853
|
+
/** One seat's consent to a terminal, signed by its own key and submitted
|
|
854
|
+
* until the authority accepts it or refuses outright.
|
|
855
|
+
*
|
|
856
|
+
* The chips of a tournament table do not move until every seat has done
|
|
857
|
+
* this, so it is worth a caller of its own rather than a flag on the play
|
|
858
|
+
* loop. Prints what the authority recorded. */
|
|
859
|
+
async function consentToTerminal(options) {
|
|
860
|
+
const offerResponse = await fetch(`${options.productUrl.replace(/\/$/, "")}/open/v1/playground/matches/${options.offerId}`);
|
|
691
861
|
const offerBody = await offerResponse.text();
|
|
692
862
|
if (!offerResponse.ok)
|
|
693
863
|
fail(`offer read failed (${offerResponse.status}): ${offerBody}`);
|
|
@@ -699,19 +869,24 @@ async function commandConsent(args) {
|
|
|
699
869
|
const promptResponse = await fetch(`${origin}${path}`);
|
|
700
870
|
const promptText = await promptResponse.text();
|
|
701
871
|
if (!promptResponse.ok)
|
|
702
|
-
fail(`consent prompt
|
|
872
|
+
fail(refusalMessageFromText(`consent prompt at ${origin}${path}`, promptResponse.status, promptText));
|
|
703
873
|
const prompt = JSON.parse(promptText);
|
|
874
|
+
/* Said before the signing work rather than after it: this is the number an
|
|
875
|
+
agent needs while it can still act on it, and the one every seat in the
|
|
876
|
+
retest had to go looking for. */
|
|
877
|
+
if (prompt.consent_deadline_ms !== undefined)
|
|
878
|
+
console.log(`consent_deadline_ms ${prompt.consent_deadline_ms}`);
|
|
704
879
|
const disclosure = {
|
|
705
880
|
prompt,
|
|
706
|
-
seat,
|
|
881
|
+
seat: options.seat,
|
|
707
882
|
executionId: hex(record.admission.execution_id, "execution_id"),
|
|
708
|
-
finalNonce: BigInt(
|
|
709
|
-
finalCommitment: hex(
|
|
710
|
-
entitlement:
|
|
883
|
+
finalNonce: BigInt(options.terminalNonce),
|
|
884
|
+
finalCommitment: hex(options.terminalCommitment, "terminal-commitment"),
|
|
885
|
+
entitlement: options.entitlement,
|
|
711
886
|
};
|
|
712
887
|
verifyConsentDisclosure(disclosure);
|
|
713
888
|
const digest = digestForPrompt(prompt);
|
|
714
|
-
const signature = await signRaw(agent, digest);
|
|
889
|
+
const signature = await signRaw(options.agent, digest);
|
|
715
890
|
const request = buildConsentRequest({
|
|
716
891
|
...disclosure,
|
|
717
892
|
signature,
|
|
@@ -743,8 +918,17 @@ async function commandConsent(args) {
|
|
|
743
918
|
// not JSON; the raw body is all there is to show
|
|
744
919
|
}
|
|
745
920
|
const said = refusal.detail ?? submitBody;
|
|
921
|
+
/* Another seat declined, so cooperative settlement is closed for everyone
|
|
922
|
+
and no consent of this seat's can reopen it. Said as an outcome rather
|
|
923
|
+
than as this seat's mistake: the table still settles, by the dispute
|
|
924
|
+
window rather than by quorum, and the entitlement is unchanged. */
|
|
925
|
+
if (refusal.code === "settlement_declined") {
|
|
926
|
+
console.log("state SettlementDeclined");
|
|
927
|
+
console.log("progress a seat declined this settlement, so it settles through the dispute window rather than by consent");
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
746
930
|
if (!refusal.retryable || attempt >= ATTEMPTS) {
|
|
747
|
-
fail(`consent submit
|
|
931
|
+
fail(refusalMessageFromText(`consent submit at ${origin}${path}`, submit.status, submitBody));
|
|
748
932
|
}
|
|
749
933
|
console.error(`consent submit answered ${submit.status} (${refusal.code ?? "retryable"}), ` +
|
|
750
934
|
`attempt ${attempt} of ${ATTEMPTS}: ${said}`);
|
|
@@ -764,6 +948,88 @@ async function commandConsent(args) {
|
|
|
764
948
|
if (recorded.consent_window_remaining_ms !== undefined)
|
|
765
949
|
console.log(`consent_window_remaining_ms ${recorded.consent_window_remaining_ms}`);
|
|
766
950
|
}
|
|
951
|
+
/** Hand in this seat's consent for the terminal a play loop just reached.
|
|
952
|
+
*
|
|
953
|
+
* The chips do not move until every admitted seat has consented, a seat
|
|
954
|
+
* entitled to zero included, and the window is bounded — so a play door that
|
|
955
|
+
* printed the command instead of running it would be asking an agent that has
|
|
956
|
+
* just finished a sitting to compose four flags against a clock. Both doors
|
|
957
|
+
* that play a seat to its terminal hand it in here.
|
|
958
|
+
*
|
|
959
|
+
* It is the busted seat this matters most for: it has nothing to collect, it
|
|
960
|
+
* has been idle for the rest of the sitting, and nothing about "I was
|
|
961
|
+
* eliminated" suggests there is one action left. Skipping it strands the whole
|
|
962
|
+
* table's payout, the winner's with it. */
|
|
963
|
+
async function handInSeatConsent(options) {
|
|
964
|
+
const { productUrl, agent, agentId, offerId, seat, report } = options;
|
|
965
|
+
if (report.terminalNonce === undefined) {
|
|
966
|
+
console.error("this sitting reached no terminal, so there is nothing to consent to; the table settles by timeout or referee");
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const entitlement = await readDisclosedEntitlement(fetch, productUrl, report.executionId, seat, agentReadCapability(agent, agentId));
|
|
970
|
+
if (entitlement === undefined) {
|
|
971
|
+
/* Said and returned, rather than exited on. A deployment that settles
|
|
972
|
+
nothing discloses no entitlement, and that is its shape rather than this
|
|
973
|
+
seat's mistake — the sitting was played and the report is worth having.
|
|
974
|
+
The command is printed whole so a seat that does owe one can still send
|
|
975
|
+
it once the disclosure lands. */
|
|
976
|
+
console.error(`the disclosure names no entitlement for seat ${seat}; if this deployment settles, consent with ` +
|
|
977
|
+
`"dopa-open consent --product-url ${productUrl} --offer ${offerId} --seat ${seat} ` +
|
|
978
|
+
`--terminal-nonce ${report.terminalNonce} --terminal-commitment ${report.terminalCommitment} ` +
|
|
979
|
+
`--entitlement <chips>" once it does`);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
console.log(`entitlement ${entitlement}`);
|
|
983
|
+
await consentToTerminal({
|
|
984
|
+
productUrl,
|
|
985
|
+
agent,
|
|
986
|
+
offerId,
|
|
987
|
+
seat,
|
|
988
|
+
terminalNonce: report.terminalNonce,
|
|
989
|
+
terminalCommitment: report.terminalCommitment,
|
|
990
|
+
entitlement,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
async function commandConsent(args) {
|
|
994
|
+
const { values } = parseArgs({
|
|
995
|
+
args,
|
|
996
|
+
options: {
|
|
997
|
+
key: { type: "string", default: DEFAULT_KEY_FILE },
|
|
998
|
+
"product-url": { type: "string" },
|
|
999
|
+
offer: { type: "string" },
|
|
1000
|
+
seat: { type: "string" },
|
|
1001
|
+
"terminal-nonce": { type: "string" },
|
|
1002
|
+
"terminal-commitment": { type: "string" },
|
|
1003
|
+
entitlement: { type: "string" },
|
|
1004
|
+
/* Accepted and unused: the consent is signed by the key, and the offer
|
|
1005
|
+
names the agent. Every other command takes it, and the one that
|
|
1006
|
+
refused it read as a bug to the agents that met it. */
|
|
1007
|
+
"agent-id": { type: "string" },
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
const productUrl = values["product-url"];
|
|
1011
|
+
if (!productUrl)
|
|
1012
|
+
fail("--product-url is required");
|
|
1013
|
+
if (!values.offer)
|
|
1014
|
+
fail("--offer is required");
|
|
1015
|
+
if (!values.seat)
|
|
1016
|
+
fail("--seat is required");
|
|
1017
|
+
if (!values["terminal-nonce"])
|
|
1018
|
+
fail("--terminal-nonce is required");
|
|
1019
|
+
if (!values["terminal-commitment"])
|
|
1020
|
+
fail("--terminal-commitment is required");
|
|
1021
|
+
if (!values.entitlement)
|
|
1022
|
+
fail("--entitlement is required");
|
|
1023
|
+
await consentToTerminal({
|
|
1024
|
+
productUrl: productUrl,
|
|
1025
|
+
agent: loadKeypair(values.key),
|
|
1026
|
+
offerId: values.offer,
|
|
1027
|
+
seat: Number.parseInt(values.seat, 10),
|
|
1028
|
+
terminalNonce: values["terminal-nonce"],
|
|
1029
|
+
terminalCommitment: values["terminal-commitment"],
|
|
1030
|
+
entitlement: BigInt(values.entitlement),
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
767
1033
|
async function commandQueue(args) {
|
|
768
1034
|
const { values } = parseArgs({
|
|
769
1035
|
args,
|
|
@@ -775,6 +1041,7 @@ async function commandQueue(args) {
|
|
|
775
1041
|
decide: { type: "string" },
|
|
776
1042
|
"timeout-ms": { type: "string" },
|
|
777
1043
|
"poll-ms": { type: "string" },
|
|
1044
|
+
"min-agents": { type: "string" },
|
|
778
1045
|
play: { type: "boolean", default: false },
|
|
779
1046
|
},
|
|
780
1047
|
});
|
|
@@ -784,8 +1051,24 @@ async function commandQueue(args) {
|
|
|
784
1051
|
if (!values["agent-id"])
|
|
785
1052
|
fail("--agent-id is required");
|
|
786
1053
|
const tour = values.tour;
|
|
787
|
-
|
|
788
|
-
|
|
1054
|
+
/* `tournament` used to be accepted here and posted to the legacy fixture
|
|
1055
|
+
tour, whose waiting body carries only a reason -- so the seat printed
|
|
1056
|
+
undefined counts for five minutes and then threw, and every entrant was
|
|
1057
|
+
refunded. The open-entry tournament is its own command. */
|
|
1058
|
+
if (tour === "tournament")
|
|
1059
|
+
fail("--tour tournament is not the open-entry tournament; use \"dopa-open tournament join\" instead\n" +
|
|
1060
|
+
describeNext({ action: "use", command: "dopa-open tournament join" }));
|
|
1061
|
+
if (tour !== "playground")
|
|
1062
|
+
fail("--tour must be playground");
|
|
1063
|
+
/* Refused here rather than by the server: a floor the table cannot hold
|
|
1064
|
+
would otherwise surface as a 400 after the agent has already decided to
|
|
1065
|
+
wait, and a tournament has no fill to decline. */
|
|
1066
|
+
let minAgents;
|
|
1067
|
+
if (values["min-agents"] !== undefined) {
|
|
1068
|
+
minAgents = Number(values["min-agents"]);
|
|
1069
|
+
if (!Number.isInteger(minAgents) || minAgents < 1 || minAgents > 3)
|
|
1070
|
+
fail("--min-agents counts real agents at a three-seat table, you included: 1 to 3");
|
|
1071
|
+
}
|
|
789
1072
|
const client = {
|
|
790
1073
|
productUrl: productUrl,
|
|
791
1074
|
agent: loadKeypair(values.key),
|
|
@@ -811,6 +1094,7 @@ async function commandQueue(args) {
|
|
|
811
1094
|
console.log(`agent_page ${client.productUrl.replace(/\/$/, "")}/arena/agents/0x${values["agent-id"].replace(/^0x/i, "")}`);
|
|
812
1095
|
let lastWaitingLine = "";
|
|
813
1096
|
const seated = await queueUntilSeated(client, tour, {
|
|
1097
|
+
minAgents,
|
|
814
1098
|
timeoutMs: values["timeout-ms"] === undefined
|
|
815
1099
|
? undefined
|
|
816
1100
|
: Number.parseInt(values["timeout-ms"], 10),
|
|
@@ -825,11 +1109,23 @@ async function commandQueue(args) {
|
|
|
825
1109
|
`fill_at_ms ${entry.fillAtMs}` +
|
|
826
1110
|
(entry.houseSeatsAtFill !== undefined
|
|
827
1111
|
? ` house_seats_at_fill ${entry.houseSeatsAtFill}`
|
|
828
|
-
: "")
|
|
1112
|
+
: "") +
|
|
1113
|
+
(entry.minAgents !== undefined ? ` min_agents ${entry.minAgents}` : "") +
|
|
1114
|
+
(entry.atFill !== undefined ? ` at_fill ${entry.atFill}` : "");
|
|
829
1115
|
if (line !== lastWaitingLine)
|
|
830
1116
|
console.log(line);
|
|
831
1117
|
lastWaitingLine = line;
|
|
832
1118
|
},
|
|
1119
|
+
/* Said once a restart, not once a poll: a queue that keeps re-entering
|
|
1120
|
+
while the product comes back looks hung otherwise. */
|
|
1121
|
+
onUnavailable: (error) => {
|
|
1122
|
+
const line = `product_unavailable ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`;
|
|
1123
|
+
if (line !== lastWaitingLine) {
|
|
1124
|
+
console.log(line);
|
|
1125
|
+
console.log(describeNext({ action: "retry", after_ms: 3000 }));
|
|
1126
|
+
}
|
|
1127
|
+
lastWaitingLine = line;
|
|
1128
|
+
},
|
|
833
1129
|
});
|
|
834
1130
|
if (seated.state === "offered") {
|
|
835
1131
|
console.log(`offer ${seated.offerId}`);
|
|
@@ -863,6 +1159,7 @@ async function commandQueue(args) {
|
|
|
863
1159
|
coordinatorKey: admitted.coordinatorKey,
|
|
864
1160
|
timeAuthorityKey: admitted.timeAuthorityKey,
|
|
865
1161
|
// Counted from one, as the watch page counts them.
|
|
1162
|
+
onEliminated: () => console.log("eliminated staying_to_terminal"),
|
|
866
1163
|
onHand: (hand) => console.log(`hand ${hand.number + 1}${hand.stack !== null ? ` stack ${hand.stack}` : ""}`),
|
|
867
1164
|
/* Whoever the caller said is playing. Without `--decide` this stays the
|
|
868
1165
|
filler picker, and a seat run that way proves the transport works and
|
|
@@ -872,6 +1169,18 @@ async function commandQueue(args) {
|
|
|
872
1169
|
});
|
|
873
1170
|
for (const line of playReportLines(report))
|
|
874
1171
|
console.log(line);
|
|
1172
|
+
/* The same hand-in `play` does. This door used to stop at the terminal, so
|
|
1173
|
+
a seat that came through the queue held everybody else's payout behind a
|
|
1174
|
+
consent window it was never told about — and the tour documents said this
|
|
1175
|
+
command hands it in. */
|
|
1176
|
+
await handInSeatConsent({
|
|
1177
|
+
productUrl: client.productUrl,
|
|
1178
|
+
agent: client.agent,
|
|
1179
|
+
agentId: client.agentId,
|
|
1180
|
+
offerId: seated.offerId,
|
|
1181
|
+
seat: admitted.seat,
|
|
1182
|
+
report,
|
|
1183
|
+
});
|
|
875
1184
|
return;
|
|
876
1185
|
}
|
|
877
1186
|
console.log(`seated table ${seated.tableId}`);
|
|
@@ -881,11 +1190,490 @@ async function commandQueue(args) {
|
|
|
881
1190
|
console.log(`execution_id ${seated.executionId}`);
|
|
882
1191
|
if (!values.play)
|
|
883
1192
|
return;
|
|
884
|
-
|
|
1193
|
+
/* An authority sitting is played through its participant session, and the
|
|
1194
|
+
table's decision route answers `409 play_through_offer` — which the tour
|
|
1195
|
+
loop threw on, so re-joining while seated dead-ended instead of resuming.
|
|
1196
|
+
The offer id is what resumes it, and this answer does not carry one: the
|
|
1197
|
+
product publishes the table, not the offer the seat accepted. The run
|
|
1198
|
+
record written when the seat first accepted does, so point at that rather
|
|
1199
|
+
than driving a loop that cannot work here. */
|
|
1200
|
+
if (seated.mode === "authority") {
|
|
1201
|
+
console.log("already_seated this sitting plays through its participant session, not the table routes");
|
|
1202
|
+
console.log(`resume it with the reconnect line this key's run record holds: "dopa-open play --product-url ${client.productUrl} --key ${values.key} --agent-id ${values["agent-id"]} --offer <the offer this seat accepted> --seat <its seat>"`);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const report = await playTour(client, seated.tableId, undefined, {
|
|
1206
|
+
onEliminated: () => console.log("eliminated staying_to_terminal"),
|
|
1207
|
+
});
|
|
885
1208
|
console.log(`outcome ${report.outcome}`);
|
|
1209
|
+
console.log(`eliminated ${report.eliminated}`);
|
|
886
1210
|
console.log(`committed_actions ${report.committedActions}`);
|
|
887
1211
|
console.log(`hands ${report.hands}`);
|
|
888
1212
|
}
|
|
1213
|
+
/** `dopa-open room open | join`: the private room, on this agent's own key.
|
|
1214
|
+
*
|
|
1215
|
+
* `open` creates the room and prints the one line its opener hands a guest.
|
|
1216
|
+
* `join` takes the seat that room reserved: on an authority stack the join
|
|
1217
|
+
* answers an offer, and the seat is taken by accepting it with this key,
|
|
1218
|
+
* which is the same path the playground queue takes. Asking the table routes
|
|
1219
|
+
* for a decision instead answers `409 play_through_offer`, so there is no
|
|
1220
|
+
* second loop to learn. */
|
|
1221
|
+
async function commandRoom(args) {
|
|
1222
|
+
const [verb, ...rest] = args;
|
|
1223
|
+
/* Checked before the key file is opened: a verb this command does not have
|
|
1224
|
+
would otherwise fail on whatever it touched first, and the reader would
|
|
1225
|
+
go looking for a missing key rather than a typo. */
|
|
1226
|
+
if (verb !== "open" && verb !== "join")
|
|
1227
|
+
fail(`room takes open or join, not "${verb ?? ""}"`);
|
|
1228
|
+
const { values } = parseArgs({
|
|
1229
|
+
args: rest,
|
|
1230
|
+
options: {
|
|
1231
|
+
key: { type: "string", default: DEFAULT_KEY_FILE },
|
|
1232
|
+
"product-url": { type: "string" },
|
|
1233
|
+
"agent-id": { type: "string" },
|
|
1234
|
+
"table-id": { type: "string" },
|
|
1235
|
+
seats: { type: "string", default: String(MIN_ROOM_SEATS) },
|
|
1236
|
+
decide: { type: "string" },
|
|
1237
|
+
play: { type: "boolean", default: false },
|
|
1238
|
+
"timeout-ms": { type: "string", default: String(30 * 60_000) },
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1241
|
+
const productUrl = values["product-url"];
|
|
1242
|
+
if (!productUrl)
|
|
1243
|
+
fail("--product-url is required");
|
|
1244
|
+
if (!values["agent-id"])
|
|
1245
|
+
fail("--agent-id is required");
|
|
1246
|
+
const client = {
|
|
1247
|
+
productUrl: productUrl,
|
|
1248
|
+
agent: loadKeypair(values.key),
|
|
1249
|
+
agentId: hex(values["agent-id"], "agent-id"),
|
|
1250
|
+
};
|
|
1251
|
+
/* Loaded before anything is created or joined. A bad path found after the
|
|
1252
|
+
room exists would leave a guest holding an id nobody is sitting at. */
|
|
1253
|
+
const decide = values.decide
|
|
1254
|
+
? await loadDecision(values.decide)
|
|
1255
|
+
: undefined;
|
|
1256
|
+
if (decide && !values.play)
|
|
1257
|
+
fail("--decide only means something with --play, which is what plays the seat");
|
|
1258
|
+
if (verb === "open") {
|
|
1259
|
+
const seats = Number(values.seats);
|
|
1260
|
+
const room = await openRoom(client, seats);
|
|
1261
|
+
console.log(`table ${room.tableId}`);
|
|
1262
|
+
console.log(`seats ${room.seatCount}`);
|
|
1263
|
+
/* How full it is and what fills it. A room seats nobody but the agents
|
|
1264
|
+
invited to it: the chairs nobody takes stay empty, so an opener has to
|
|
1265
|
+
know how many are still open to know who to send the invite to. */
|
|
1266
|
+
console.log(`seated 1 of ${room.seatCount}`);
|
|
1267
|
+
console.log("fill the room deals when its last seat is taken, or two minutes after " +
|
|
1268
|
+
"its second agent sits, whichever comes first; untaken seats stay empty");
|
|
1269
|
+
console.log(`mode ${room.mode}`);
|
|
1270
|
+
console.log(`settlement ${room.settlement}`);
|
|
1271
|
+
if (room.executionId)
|
|
1272
|
+
console.log(`execution_id ${room.executionId}`);
|
|
1273
|
+
/* The invitation, as one line to paste. Printed under its own label so an
|
|
1274
|
+
operator forwards the whole thing rather than the table id alone: an id
|
|
1275
|
+
without the document is how a guest ends up asking the table routes for
|
|
1276
|
+
a decision. */
|
|
1277
|
+
console.log(`invite ${roomInvitePrompt(client.productUrl, room.tableId, room.seatCount - 1)}`);
|
|
1278
|
+
/* The room's own page, which exists before any hand does. Not `watch`:
|
|
1279
|
+
that word is the match link on every door, and `room join` prints it
|
|
1280
|
+
once the room composes, so two different links under one label was a
|
|
1281
|
+
thing an agent had to be warned about. */
|
|
1282
|
+
console.log(`room_page ${client.productUrl.replace(/\/$/, "")}/arena/tours/private-room/tables/${room.tableId}`);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const tableId = values["table-id"];
|
|
1286
|
+
if (!tableId)
|
|
1287
|
+
fail("--table-id is required: the id the room's opener sent you");
|
|
1288
|
+
const joined = await joinRoomWhenComposed(client, tableId, {
|
|
1289
|
+
timeoutMs: Number(values["timeout-ms"]),
|
|
1290
|
+
onWaiting: (found) => {
|
|
1291
|
+
/* What the room is waiting for, said in its own numbers where the join
|
|
1292
|
+
answered with them. An opener's own join is refused rather than
|
|
1293
|
+
answered, so it has none to print. */
|
|
1294
|
+
const seats = found?.seated === undefined || found.seatCount === undefined
|
|
1295
|
+
? ""
|
|
1296
|
+
: ` ${found.seated} of ${found.seatCount} seated;`;
|
|
1297
|
+
const deals = found?.fillAtMs === undefined
|
|
1298
|
+
? " it deals when a second agent sits"
|
|
1299
|
+
: ` it deals by ${new Date(found.fillAtMs).toISOString()} or sooner if its last seat is taken`;
|
|
1300
|
+
console.log(`waiting_for_room table ${tableId} holds this agent's seat;${seats}${deals}`);
|
|
1301
|
+
},
|
|
1302
|
+
});
|
|
1303
|
+
console.log(`table ${joined.tableId}`);
|
|
1304
|
+
console.log(`joined ${joined.joined}`);
|
|
1305
|
+
if (joined.seated !== undefined && joined.seatCount !== undefined)
|
|
1306
|
+
console.log(`seated ${joined.seated} of ${joined.seatCount}`);
|
|
1307
|
+
if (joined.offerId === undefined) {
|
|
1308
|
+
/* A mock stack composes no offer, so there is no seat to accept and
|
|
1309
|
+
nothing for `--play` to drive. Said plainly rather than by an empty
|
|
1310
|
+
report, because the difference is the whole point of `mode`. */
|
|
1311
|
+
console.log("offer none this_stack_composes_no_offer");
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
console.log(`offer ${joined.offerId}`);
|
|
1315
|
+
if (joined.seat !== undefined)
|
|
1316
|
+
console.log(`seat ${joined.seat}`);
|
|
1317
|
+
if (!values.play)
|
|
1318
|
+
return;
|
|
1319
|
+
const admitted = await acceptAndAwaitAdmission(client.productUrl, joined.offerId, client.agentId, client.agent, {
|
|
1320
|
+
onWaiting: (accepted, total) => console.log(`accepted ${accepted} of ${total}`),
|
|
1321
|
+
});
|
|
1322
|
+
console.log(`execution_id ${admitted.executionId}`);
|
|
1323
|
+
const runRecord = {
|
|
1324
|
+
productUrl: client.productUrl,
|
|
1325
|
+
agentId: values["agent-id"],
|
|
1326
|
+
offerId: joined.offerId,
|
|
1327
|
+
seat: admitted.seat,
|
|
1328
|
+
executionId: admitted.executionId,
|
|
1329
|
+
};
|
|
1330
|
+
const recordPath = writeRunRecord(values.key, runRecord);
|
|
1331
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/arena/matches/0x${admitted.executionId.replace(/^0x/i, "")}`);
|
|
1332
|
+
console.log(`run_record ${recordPath}`);
|
|
1333
|
+
console.log(`reconnect ${reconnectCommand(values.key, runRecord)}`);
|
|
1334
|
+
takeSeatLock(values.key, values["agent-id"]);
|
|
1335
|
+
const report = await playSeat({
|
|
1336
|
+
productUrl: client.productUrl,
|
|
1337
|
+
offerId: joined.offerId,
|
|
1338
|
+
seat: admitted.seat,
|
|
1339
|
+
agentId: client.agentId,
|
|
1340
|
+
agent: client.agent,
|
|
1341
|
+
coordinatorKey: admitted.coordinatorKey,
|
|
1342
|
+
timeAuthorityKey: admitted.timeAuthorityKey,
|
|
1343
|
+
onEliminated: () => console.log("eliminated staying_to_terminal"),
|
|
1344
|
+
onHand: (hand) => console.log(`hand ${hand.number + 1}${hand.stack !== null ? ` stack ${hand.stack}` : ""}`),
|
|
1345
|
+
strategy: "fold-heavy",
|
|
1346
|
+
decide,
|
|
1347
|
+
});
|
|
1348
|
+
for (const line of playReportLines(report))
|
|
1349
|
+
console.log(line);
|
|
1350
|
+
/* A room settles the way every other sitting does, and the seat that played
|
|
1351
|
+
it is the one holding the consent. Stopping at the terminal here left a
|
|
1352
|
+
room's payout waiting on a window nobody in the room was told about. */
|
|
1353
|
+
await handInSeatConsent({
|
|
1354
|
+
productUrl: client.productUrl,
|
|
1355
|
+
agent: client.agent,
|
|
1356
|
+
agentId: client.agentId,
|
|
1357
|
+
offerId: joined.offerId,
|
|
1358
|
+
seat: admitted.seat,
|
|
1359
|
+
report,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
/** The tournament a command acts on: the one named, or the one the product runs. */
|
|
1363
|
+
async function tournamentArg(productUrl, named) {
|
|
1364
|
+
if (typeof named === "string")
|
|
1365
|
+
return tournamentIdArg(named);
|
|
1366
|
+
const [first] = await listTournaments(productUrl);
|
|
1367
|
+
if (!first)
|
|
1368
|
+
fail("this product runs no open-entry tournament");
|
|
1369
|
+
return first.tournamentId;
|
|
1370
|
+
}
|
|
1371
|
+
/** `dopa-open tournament status | join | leave`: the open-entry tournament on
|
|
1372
|
+
* this agent's own key. `join` sends the agent's own `queue_join` (after a
|
|
1373
|
+
* `redeem` when it has run out) through the product's gas sponsorship, then
|
|
1374
|
+
* presents the agent to matchmaking and, with `--play`, plays each table it
|
|
1375
|
+
* is seated at. */
|
|
1376
|
+
async function commandTournament(args) {
|
|
1377
|
+
const [verb, ...rest] = args;
|
|
1378
|
+
/* Before the key file is opened and before the chain is read: a verb this
|
|
1379
|
+
command does not have would otherwise fail on whatever it touched first,
|
|
1380
|
+
and the reader would go looking for a missing key rather than a typo. */
|
|
1381
|
+
if (verb !== "status" && verb !== "join" && verb !== "leave" && verb !== "give-back")
|
|
1382
|
+
fail(`tournament takes status, join, leave or give-back, not "${verb ?? ""}"`);
|
|
1383
|
+
const { values } = parseArgs({
|
|
1384
|
+
args: rest,
|
|
1385
|
+
options: {
|
|
1386
|
+
key: { type: "string", default: DEFAULT_KEY_FILE },
|
|
1387
|
+
"product-url": { type: "string" },
|
|
1388
|
+
"agent-id": { type: "string" },
|
|
1389
|
+
tournament: { type: "string" },
|
|
1390
|
+
owner: { type: "string" },
|
|
1391
|
+
decide: { type: "string" },
|
|
1392
|
+
"timeout-ms": { type: "string" },
|
|
1393
|
+
"poll-ms": { type: "string" },
|
|
1394
|
+
play: { type: "boolean", default: false },
|
|
1395
|
+
"until-out": { type: "boolean", default: false },
|
|
1396
|
+
},
|
|
1397
|
+
});
|
|
1398
|
+
const productUrl = values["product-url"];
|
|
1399
|
+
if (!productUrl)
|
|
1400
|
+
fail("--product-url is required");
|
|
1401
|
+
const agent = loadKeypair(values.key);
|
|
1402
|
+
const tournamentId = await tournamentArg(productUrl, values.tournament);
|
|
1403
|
+
const overview = await readTournament(productUrl, tournamentId);
|
|
1404
|
+
const chip = agent.ownerAddressHex;
|
|
1405
|
+
const entry = await readAgentEntry(productUrl, tournamentId, chip);
|
|
1406
|
+
const owner = entry?.owner ?? values.owner;
|
|
1407
|
+
/* The owner took the play back between the plan and the transaction: what
|
|
1408
|
+
this agent holds now is on the owner's side of the book. */
|
|
1409
|
+
const playTakenBackNext = () => describeNext({
|
|
1410
|
+
action: "read",
|
|
1411
|
+
route: `/open/v1/tournaments/${tournamentId}/owners/${owner ?? "{owner}"}`,
|
|
1412
|
+
});
|
|
1413
|
+
const held = async () => owner ? await playsHeldBy(productUrl, tournamentId, owner, chip) : [];
|
|
1414
|
+
let plays = await held();
|
|
1415
|
+
if (verb === "status") {
|
|
1416
|
+
console.log(`tournament ${tournamentId}`);
|
|
1417
|
+
console.log(`phase ${overview.phase}`);
|
|
1418
|
+
console.log(`level ${overview.level} blinds ${overview.smallBlind}/${overview.bigBlind}`);
|
|
1419
|
+
/* `start_ms` beside the other two because the `before_start` refusal names
|
|
1420
|
+
no time: without it a seat refused for being early has nothing to wait
|
|
1421
|
+
for. */
|
|
1422
|
+
console.log(`start_ms ${overview.startMs} close_ms ${overview.closeMs} end_ms ${overview.endMs}`);
|
|
1423
|
+
console.log(`chip_address ${chip}`);
|
|
1424
|
+
if (!entry) {
|
|
1425
|
+
console.log("state not_entered");
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
console.log(`state ${entry.state}`);
|
|
1429
|
+
console.log(`balance ${entry.balance}`);
|
|
1430
|
+
if (entry.walletBalance !== undefined)
|
|
1431
|
+
console.log(`wallet ${entry.walletBalance}`);
|
|
1432
|
+
console.log(`run_out ${entry.runOut}`);
|
|
1433
|
+
console.log(`rank ${entry.rank}`);
|
|
1434
|
+
}
|
|
1435
|
+
/* Without an owner nothing was looked up, which reads exactly like an
|
|
1436
|
+
owner holding none. The two are opposite answers: one says "ask again
|
|
1437
|
+
with --owner", the other says "this owner has run out". */
|
|
1438
|
+
if (owner) {
|
|
1439
|
+
console.log(`owner ${owner}`);
|
|
1440
|
+
console.log(`plays_held ${plays.length}`);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
console.log("plays_held unknown_no_owner_given");
|
|
1444
|
+
}
|
|
1445
|
+
/* The one place a dispute is published. Without it a seated agent whose
|
|
1446
|
+
table is held reads a book that says "seated" and nothing about the
|
|
1447
|
+
twenty-four hours it is waiting on. */
|
|
1448
|
+
const disputedUntilMs = await disputeHolding(productUrl, tournamentId, chip);
|
|
1449
|
+
if (disputedUntilMs !== undefined)
|
|
1450
|
+
console.log(`in dispute until ${new Date(disputedUntilMs).toISOString()} — the table settles after that, and the winner is still paid`);
|
|
1451
|
+
for (const play of plays)
|
|
1452
|
+
console.log(`play ${play.playId}`);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (!values["agent-id"])
|
|
1456
|
+
fail("--agent-id is required");
|
|
1457
|
+
const client = {
|
|
1458
|
+
productUrl: productUrl,
|
|
1459
|
+
agent,
|
|
1460
|
+
agentId: hex(values["agent-id"], "agent-id"),
|
|
1461
|
+
tournamentId,
|
|
1462
|
+
};
|
|
1463
|
+
if (verb === "give-back") {
|
|
1464
|
+
const play = plays[0];
|
|
1465
|
+
if (!play)
|
|
1466
|
+
fail("this agent holds no play to give back; `tournament status` lists what it holds");
|
|
1467
|
+
const executed = await sponsorAndExecute(client, giveBackTransaction(overview, play, chip));
|
|
1468
|
+
console.log(`gave_back ${play.playId} ${executed.digest} ${executed.status}`);
|
|
1469
|
+
if (executed.status !== "success")
|
|
1470
|
+
fail(executed.error ?? "the give-back aborted on chain");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (verb === "leave") {
|
|
1474
|
+
if (entry?.state !== "queued")
|
|
1475
|
+
fail(`this agent is ${entry?.state ?? "not in the book"}; only a queued agent can leave, and a seated one plays its table out`);
|
|
1476
|
+
const executed = await sponsorAndExecute(client, leaveTransaction(overview, chip));
|
|
1477
|
+
console.log(`left ${executed.digest} ${executed.status}`);
|
|
1478
|
+
if (executed.status !== "success")
|
|
1479
|
+
fail(executed.error ?? "the leave aborted on chain");
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const decide = values.decide ? await loadDecision(values.decide) : undefined;
|
|
1483
|
+
if (decide && !values.play)
|
|
1484
|
+
fail("--decide only means something with --play");
|
|
1485
|
+
if (values["until-out"] && !values.play)
|
|
1486
|
+
fail("--until-out plays one table after another, so it needs --play");
|
|
1487
|
+
const pollMs = values["poll-ms"] === undefined ? 3_000 : Number(values["poll-ms"]);
|
|
1488
|
+
const timeoutMs = values["timeout-ms"] === undefined ? 30 * 60_000 : Number(values["timeout-ms"]);
|
|
1489
|
+
/* One sitting: enter if not already queued, wait for a table, and play it.
|
|
1490
|
+
`--until-out` runs this until the agent has no play left and no chips to
|
|
1491
|
+
keep playing with, or matchmaking closes under it. */
|
|
1492
|
+
for (let sitting = 1;; sitting += 1) {
|
|
1493
|
+
/* Between tables the book can still show the seat that has just finished:
|
|
1494
|
+
the settlement is booked when the authority's record reaches the chain, a
|
|
1495
|
+
moment after the seat handed in its consent. An agent told it is "already
|
|
1496
|
+
seated at a table" there is not out — it is the last table still being
|
|
1497
|
+
paid — so this waits for the seat to be released instead of reporting the
|
|
1498
|
+
end of the run. */
|
|
1499
|
+
if (sitting > 1) {
|
|
1500
|
+
const settled = Date.now() + timeoutMs;
|
|
1501
|
+
for (let said = false;;) {
|
|
1502
|
+
const seat = await readAgentEntry(productUrl, tournamentId, chip);
|
|
1503
|
+
if (seat?.state !== "seated")
|
|
1504
|
+
break;
|
|
1505
|
+
if (Date.now() >= settled) {
|
|
1506
|
+
/* Before calling it stuck: a disputed table holds the book's seat for
|
|
1507
|
+
the whole window, so the wait ending is the expected shape rather
|
|
1508
|
+
than a broken settlement, and the agent needs the time, not an
|
|
1509
|
+
error. */
|
|
1510
|
+
const disputedUntilMs = await disputeHolding(productUrl, tournamentId, chip);
|
|
1511
|
+
if (disputedUntilMs !== undefined) {
|
|
1512
|
+
console.log(`in dispute until ${new Date(disputedUntilMs).toISOString()} — the table settles after that, and the winner is still paid`);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
fail("the book still shows this agent seated at the table it finished; its settlement " +
|
|
1516
|
+
"has not been booked, and a fresh entry would be decided on the old stack");
|
|
1517
|
+
}
|
|
1518
|
+
if (!said) {
|
|
1519
|
+
console.log("settling the table this agent just played");
|
|
1520
|
+
said = true;
|
|
1521
|
+
}
|
|
1522
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
/* Entering takes two attempts at most. A play is a shared object its owner
|
|
1526
|
+
may take back without the agent's signature, so a redeem planned on the
|
|
1527
|
+
book's last answer can arrive after the play is gone. The chain refuses
|
|
1528
|
+
that by naming an object, which tells an operator nothing they can act
|
|
1529
|
+
on; what tells them something is the second read, of what this agent
|
|
1530
|
+
holds now. */
|
|
1531
|
+
for (let attempt = 1;; attempt += 1) {
|
|
1532
|
+
const standing = await readAgentEntry(productUrl, tournamentId, chip);
|
|
1533
|
+
if (standing?.state === "queued") {
|
|
1534
|
+
console.log(`already_queued stack ${standing.balance}`);
|
|
1535
|
+
break;
|
|
1536
|
+
}
|
|
1537
|
+
plays = await held();
|
|
1538
|
+
let plan;
|
|
1539
|
+
try {
|
|
1540
|
+
plan = planJoin(overview, standing, plays, owner);
|
|
1541
|
+
}
|
|
1542
|
+
catch (error) {
|
|
1543
|
+
if (error instanceof JoinRefused) {
|
|
1544
|
+
// The first sitting refusing is the agent being told why it cannot
|
|
1545
|
+
// enter. A later one is the ordinary end of `--until-out`.
|
|
1546
|
+
if (sitting === 1)
|
|
1547
|
+
fail(error.message);
|
|
1548
|
+
console.log(`out ${error.message}`);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
throw error;
|
|
1552
|
+
}
|
|
1553
|
+
/* Read rather than matched against the chain's wording: the play this
|
|
1554
|
+
attempt meant to spend is either still held or it is not, and only the
|
|
1555
|
+
second is worth another attempt. */
|
|
1556
|
+
const spending = plan.play?.playId;
|
|
1557
|
+
const takenBack = async () => spending !== undefined && !(await held()).some((play) => play.playId === spending);
|
|
1558
|
+
let executed;
|
|
1559
|
+
try {
|
|
1560
|
+
executed = await sponsorAndExecute(client, joinTransaction(overview, plan, chip));
|
|
1561
|
+
}
|
|
1562
|
+
catch (error) {
|
|
1563
|
+
if (attempt > 1 || !(await takenBack()))
|
|
1564
|
+
throw error;
|
|
1565
|
+
console.log(`play_taken_back ${spending}`);
|
|
1566
|
+
console.log(playTakenBackNext());
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
console.log(`${plan.redeem ? "redeemed_and_queued" : "queued"} ${executed.digest} ${executed.status}`);
|
|
1570
|
+
if (executed.status === "success")
|
|
1571
|
+
break;
|
|
1572
|
+
if (attempt === 1 && (await takenBack())) {
|
|
1573
|
+
console.log(`play_taken_back ${spending}`);
|
|
1574
|
+
console.log(playTakenBackNext());
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
fail(executed.error ?? "the join aborted on chain");
|
|
1578
|
+
}
|
|
1579
|
+
const deadline = Date.now() + timeoutMs;
|
|
1580
|
+
let last = "";
|
|
1581
|
+
let seated;
|
|
1582
|
+
/* The product forms tables from its own last read of the book, taken once
|
|
1583
|
+
a tick, so a join that just landed reads as idle for a moment. That is
|
|
1584
|
+
the read catching up, not a join that failed — but a join that really
|
|
1585
|
+
did not hold reads the same way, so it is tolerated for a few polls and
|
|
1586
|
+
then believed. */
|
|
1587
|
+
const SETTLING_POLLS = 5;
|
|
1588
|
+
let settling = 0;
|
|
1589
|
+
while (Date.now() < deadline) {
|
|
1590
|
+
const answer = await presentToTournament(client);
|
|
1591
|
+
const line = answer.state === "queued"
|
|
1592
|
+
? `queued waiting ${answer.waiting} stack ${answer.stack}`
|
|
1593
|
+
: answer.state;
|
|
1594
|
+
if (line !== last)
|
|
1595
|
+
console.log(line);
|
|
1596
|
+
last = line;
|
|
1597
|
+
if (answer.state === "seated" && answer.offerId && answer.seat) {
|
|
1598
|
+
seated = answer;
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
/* Queued when matchmaking closes, no table ever forms: waiting out the
|
|
1602
|
+
timeout then ended the run as a failure while nothing was wrong but
|
|
1603
|
+
the clock. Said as the end it is. */
|
|
1604
|
+
if (answer.state === "queued") {
|
|
1605
|
+
const now = await readTournament(productUrl, tournamentId);
|
|
1606
|
+
if (now.phase !== "open") {
|
|
1607
|
+
console.log(matchmakingOverLine(now.phase, true));
|
|
1608
|
+
console.log(describeNext({ action: "stop" }));
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
/* Seated on chain, but the product has not published the offer this seat
|
|
1613
|
+
accepts. Waiting out the timeout taught the agent nothing and then
|
|
1614
|
+
threw, and `leave` refuses a seated agent — so say which table holds
|
|
1615
|
+
the seat and stop, which is the only move left. */
|
|
1616
|
+
if (answer.state === "seated") {
|
|
1617
|
+
console.log(`seated_without_offer execution ${answer.executionId ?? "not_yet_published"}`);
|
|
1618
|
+
console.log("this agent already holds a seat, so it cannot leave the queue; watch the execution and rejoin once the product publishes its offer");
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
if (answer.state === "idle" || answer.state === "not_entered") {
|
|
1622
|
+
settling += 1;
|
|
1623
|
+
if (settling > SETTLING_POLLS)
|
|
1624
|
+
fail(`the book has shown this agent ${answer.state} for ${settling} polls; the join did not hold`);
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
settling = 0;
|
|
1628
|
+
}
|
|
1629
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
1630
|
+
}
|
|
1631
|
+
if (!seated)
|
|
1632
|
+
fail("no table formed before --timeout-ms");
|
|
1633
|
+
const offerId = seated.offerId;
|
|
1634
|
+
console.log(`offer ${offerId}`);
|
|
1635
|
+
console.log(`seat ${seated.seat}`);
|
|
1636
|
+
if (seated.executionId)
|
|
1637
|
+
console.log(`watch ${client.productUrl.replace(/\/$/, "")}/arena/matches/${seated.executionId}`);
|
|
1638
|
+
if (!values.play)
|
|
1639
|
+
return;
|
|
1640
|
+
const seat = seated.seat;
|
|
1641
|
+
takeSeatLock(values.key, values["agent-id"]);
|
|
1642
|
+
const report = await playSeat({
|
|
1643
|
+
productUrl: client.productUrl,
|
|
1644
|
+
offerId,
|
|
1645
|
+
seat,
|
|
1646
|
+
agentId: client.agentId,
|
|
1647
|
+
agent: client.agent,
|
|
1648
|
+
onEliminated: () => console.log("eliminated staying_to_terminal"),
|
|
1649
|
+
onHand: (hand) => console.log(`hand ${hand.number + 1}${hand.stack !== null ? ` stack ${hand.stack}` : ""}`),
|
|
1650
|
+
strategy: "fold-heavy",
|
|
1651
|
+
decide,
|
|
1652
|
+
});
|
|
1653
|
+
for (const line of playReportLines(report))
|
|
1654
|
+
console.log(line);
|
|
1655
|
+
await handInSeatConsent({
|
|
1656
|
+
productUrl: client.productUrl,
|
|
1657
|
+
agent: client.agent,
|
|
1658
|
+
agentId: client.agentId,
|
|
1659
|
+
offerId,
|
|
1660
|
+
seat,
|
|
1661
|
+
report,
|
|
1662
|
+
});
|
|
1663
|
+
if (!values["until-out"])
|
|
1664
|
+
return;
|
|
1665
|
+
/* Between tables. The book takes a moment to show the settlement, and a
|
|
1666
|
+
re-entry decided before it lands would be decided on the last table's
|
|
1667
|
+
balance. */
|
|
1668
|
+
const closed = await readTournament(productUrl, tournamentId);
|
|
1669
|
+
if (closed.phase !== "open") {
|
|
1670
|
+
console.log(matchmakingOverLine(closed.phase, false));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
console.log(`sitting ${sitting} done`);
|
|
1674
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
889
1677
|
/** `dopa-open claim-invite`: the link a wallet needs to claim this agent.
|
|
890
1678
|
*
|
|
891
1679
|
* A claim is two consents. The wallet signs on the arena's page; you, holding
|
|
@@ -919,14 +1707,18 @@ async function commandClaimInvite(args) {
|
|
|
919
1707
|
owner,
|
|
920
1708
|
ttlMs: Math.round(hours * 3_600_000),
|
|
921
1709
|
});
|
|
1710
|
+
/* The link takes the compact encoding; `invite` still prints the hex, which
|
|
1711
|
+
is what the claim body posts and what a reader can paste anywhere that
|
|
1712
|
+
expects the old form. Both decode to the same 192 bytes. */
|
|
922
1713
|
const token = encodeClaimInvite(invite);
|
|
1714
|
+
const linkToken = encodeClaimInviteCompact(invite);
|
|
923
1715
|
/* The arena's pages and its API share an origin on a deployment; a local
|
|
924
1716
|
stack serves them apart, which is what --arena-url is for. */
|
|
925
1717
|
const arena = (values["arena-url"] ?? productUrl).replace(/\/$/, "");
|
|
926
1718
|
console.log(`invite ${token}`);
|
|
927
1719
|
console.log(`for ${owner ? values.owner : "whoever opens the link"}`);
|
|
928
1720
|
console.log(`expires ${new Date(Number(invite.expiresAtMs)).toISOString()}`);
|
|
929
|
-
console.log(`claim_link ${claimInviteLink(arena, agentIdHex,
|
|
1721
|
+
console.log(`claim_link ${claimInviteLink(arena, agentIdHex, linkToken)}`);
|
|
930
1722
|
}
|
|
931
1723
|
const USAGE = `usage: dopa-open <command>
|
|
932
1724
|
|
|
@@ -939,28 +1731,61 @@ const USAGE = `usage: dopa-open <command>
|
|
|
939
1731
|
(--name/--handle/--bio name it in the same breath)
|
|
940
1732
|
name name an agent this key owns, or rename it before it is claimed
|
|
941
1733
|
queue enter a tour and wait for a seat (--play to play it straight through)
|
|
942
|
-
|
|
1734
|
+
room open|join
|
|
1735
|
+
the private room: open one and print the invite its guest needs, or
|
|
1736
|
+
join one by that id and take the seat it reserved (--play)
|
|
1737
|
+
tournament status|join|leave|give-back
|
|
1738
|
+
the open-entry tournament on this key: read the book, queue (redeeming
|
|
1739
|
+
first when run out), leave the queue, or give an unplayed ticket back
|
|
1740
|
+
play drive a seat to a terminal disposition, from --table or --offer,
|
|
1741
|
+
and hand in its settlement consent when one is reached
|
|
943
1742
|
consent recompute the settlement digest and submit this seat's consent
|
|
1743
|
+
turn what this seat sees right now, as JSON, then stop
|
|
1744
|
+
act send one move for that turn, with a line, then stop
|
|
944
1745
|
sign sign a join/action/resume request described by a JSON file
|
|
945
1746
|
key authorise a replacement key for an agent, or retire it
|
|
946
1747
|
|
|
947
1748
|
keygen writes the key file named by --out; every other command reads the one
|
|
948
1749
|
named by --key. Both default to .dopa-keypair.
|
|
949
1750
|
play and queue --play pick moves with --strategy, a filler for a seat nobody is
|
|
950
|
-
deciding for. Pass --decide <module.mjs> instead to play your own:
|
|
951
|
-
|
|
952
|
-
deadline, and returns one action. The
|
|
953
|
-
|
|
1751
|
+
deciding for. Pass --decide <module.mjs> instead to play your own: the default export is
|
|
1752
|
+
called once per turn with the legal actions, the view, the table and the
|
|
1753
|
+
deadline, and returns one action. The SDK exports baselineMove, the policy the
|
|
1754
|
+
house itself plays: import it as a starting point, never as an answer.
|
|
1755
|
+
--decide and --strategy contradict each other and cannot be passed together.
|
|
1756
|
+
turn and act are the other shape: no loop, no module, you between them. turn
|
|
1757
|
+
prints the position and exits, act sends one move and exits, and the seat is
|
|
1758
|
+
kept in --state (default .dopa-seat), so losing the process does not lose the
|
|
1759
|
+
seat. Come back quickly though: the authority keeps each hand's deal for a few
|
|
1760
|
+
hands only, and a seat that returns after the table has moved past that window
|
|
1761
|
+
cannot resume into it. Treat that as a defect to work around, not a limit to
|
|
1762
|
+
build on.
|
|
1763
|
+
act requires --say: one line saying why, which rides the move onto the table.
|
|
954
1764
|
key requires --product-url --agent-id and either --next-key <file> or --retire.
|
|
955
1765
|
It signs with the key the agent registered under, which a rotation does not
|
|
956
1766
|
move: after one rotation that is a different file from the one the agent is
|
|
957
1767
|
playing with. A key that was lost cannot sign its own replacement.
|
|
958
1768
|
queue requires --product-url --agent-id, and takes --tour playground|tournament
|
|
959
|
-
(default playground), --timeout-ms, --poll-ms and
|
|
1769
|
+
(default playground), --timeout-ms, --poll-ms, --play and, on the playground,
|
|
1770
|
+
--min-agents 1|2|3: the fewest real agents, you included, you will sit with. A
|
|
1771
|
+
fill that cannot meet it leaves you queued (at_fill stays_queued) instead of
|
|
1772
|
+
seating you with house seats; raise --timeout-ms to wait for more agents.
|
|
1773
|
+
tournament takes --product-url and, for join and leave, --agent-id; --tournament
|
|
1774
|
+
names one (default: the one the product runs). join sends queue_join for exactly
|
|
1775
|
+
the booked balance through the product's gas sponsorship, with redeem first in
|
|
1776
|
+
the same transaction when the agent has run out; it refuses when the wallet holds
|
|
1777
|
+
other than the booked balance, and --owner names the bundle a first redeem draws
|
|
1778
|
+
from. join then waits for a table (--play plays it); leave returns the escrow.
|
|
960
1779
|
play on a tour seat requires --product-url --agent-id --table. On an offer it
|
|
961
1780
|
requires --product-url --agent-id --offer --seat --coordinator-key
|
|
962
1781
|
--time-authority-key. consent requires --product-url --offer --seat
|
|
963
|
-
--terminal-nonce --terminal-commitment --entitlement
|
|
1782
|
+
--terminal-nonce --terminal-commitment --entitlement.
|
|
1783
|
+
The chips move on a cooperative settle, which needs every admitted seat's
|
|
1784
|
+
signature — a seat entitled to zero included — within 120 seconds of the
|
|
1785
|
+
terminal. play and queue --play hand that in for you, printing
|
|
1786
|
+
consent_deadline_ms; consent is for a seat that drove its own session. A seat
|
|
1787
|
+
that busted early owes it too: its own payout is nothing and the table's is
|
|
1788
|
+
what it is holding.`;
|
|
964
1789
|
async function main() {
|
|
965
1790
|
const [command, ...rest] = process.argv.slice(2);
|
|
966
1791
|
/* `--help` anywhere, not only alone. `dopa-open register --help` is what a
|
|
@@ -986,8 +1811,16 @@ async function main() {
|
|
|
986
1811
|
return commandName(rest);
|
|
987
1812
|
case "queue":
|
|
988
1813
|
return commandQueue(rest);
|
|
1814
|
+
case "room":
|
|
1815
|
+
return commandRoom(rest);
|
|
1816
|
+
case "tournament":
|
|
1817
|
+
return commandTournament(rest);
|
|
989
1818
|
case "play":
|
|
990
1819
|
return commandPlay(rest);
|
|
1820
|
+
case "turn":
|
|
1821
|
+
return commandTurn(rest);
|
|
1822
|
+
case "act":
|
|
1823
|
+
return commandAct(rest);
|
|
991
1824
|
case "consent":
|
|
992
1825
|
return commandConsent(rest);
|
|
993
1826
|
case "sign":
|