pyyol 1.13.0 → 1.14.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/dist/cli.js +70 -17
- package/dist/intent.d.ts +59 -0
- package/dist/intent.js +160 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/rules/llms-full.txt +97 -64
- package/skill/references/setup.md +3 -2
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { deriveConnectUrl, deviceLabel, runLoginFlow } from "./login.js";
|
|
|
19
19
|
import * as mode from "./mode.js";
|
|
20
20
|
import { RuntimeConnector } from "./runtime.js";
|
|
21
21
|
import { askWatch, watchUrl, WATCH_BROWSER, WATCH_TERMINAL } from "./watch.js";
|
|
22
|
+
import { INTENT_INVITE, friendsUrl, isInsufficientBalance, offerBuyCoins, resolveStartupIntent, } from "./intent.js";
|
|
22
23
|
import { REQUEST_ID_HEADER, SIGNATURE_HEADER, SIGNATURE_VERSION, TIMESTAMP_HEADER, computeSignature, } from "./signing.js";
|
|
23
24
|
import { SDK_VERSION } from "./version.js";
|
|
24
25
|
const OK = "✓";
|
|
@@ -560,9 +561,44 @@ async function orchestrate(a, devLocked) {
|
|
|
560
561
|
// agent key can't expire, so refresh is only wired when NOT using it.
|
|
561
562
|
...(usingAgentKey ? {} : refreshOpts(c, base)),
|
|
562
563
|
});
|
|
564
|
+
// Join vs Invite — only on `pyyol play` (not `dev`). Non-TTY / --queue / --ranked
|
|
565
|
+
// keep today's auto-start so CI and scripts never hang on a prompt.
|
|
566
|
+
let startup = "queue";
|
|
567
|
+
if (!devLocked) {
|
|
568
|
+
const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
569
|
+
startup = await resolveStartupIntent({
|
|
570
|
+
ranked: m === mode.RANKED || bool(a, "ranked"),
|
|
571
|
+
mode: str(a, "mode") || null,
|
|
572
|
+
queueFlag: bool(a, "queue"),
|
|
573
|
+
inviteFlag: bool(a, "invite"),
|
|
574
|
+
isTty: tty,
|
|
575
|
+
});
|
|
576
|
+
if (startup === INTENT_INVITE) {
|
|
577
|
+
const dash = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
|
|
578
|
+
const url = friendsUrl(dash);
|
|
579
|
+
console.log(` ${OK} invite mode — connected; not queueing a match`);
|
|
580
|
+
if (url) {
|
|
581
|
+
console.log(` ${OK} invite a friend: ${url}`);
|
|
582
|
+
const openMode = str(a, "open") || "auto";
|
|
583
|
+
if (openMode !== "never" && tty) {
|
|
584
|
+
try {
|
|
585
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
586
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
587
|
+
console.log(` ${OK} opened Play a friend in your browser`);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
/* link already printed */
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
563
596
|
// Kick match(es) after the socket registers; retry while it comes online.
|
|
597
|
+
// Invite mode stays connected only — the developer hosts from /friends.
|
|
564
598
|
const matches = num(a, "matches", devLocked ? 3 : 1);
|
|
565
599
|
setTimeout(async () => {
|
|
600
|
+
if (!devLocked && startup === INTENT_INVITE)
|
|
601
|
+
return;
|
|
566
602
|
if (m === mode.RANKED) {
|
|
567
603
|
const tier = str(a, "tier") || "low";
|
|
568
604
|
const [st, resp] = await enqueueRanked(base, token, arena, { game: arena, tier }, {
|
|
@@ -573,6 +609,10 @@ async function orchestrate(a, devLocked) {
|
|
|
573
609
|
console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
|
|
574
610
|
else if (rankedUnreachable(resp))
|
|
575
611
|
console.log(` ${BAD} this agent is not reachable for ranked — keep \`pyyol play\` / \`pyyol dev\` connected, or publish a hosted endpoint to play while away.`);
|
|
612
|
+
else if (isInsufficientBalance(resp, { status: st })) {
|
|
613
|
+
const dash = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
|
|
614
|
+
offerBuyCoins(dash);
|
|
615
|
+
}
|
|
576
616
|
else
|
|
577
617
|
console.log(` ${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
|
|
578
618
|
return;
|
|
@@ -778,8 +818,10 @@ async function cmdQueue(a) {
|
|
|
778
818
|
const code = String(resp.code ?? resp.error ?? "");
|
|
779
819
|
if (code.includes("certified") || code.includes("playable") || code.includes("not_connected"))
|
|
780
820
|
console.error(`${BAD} this agent is not reachable for ranked. Keep it connected (\`pyyol play\` / \`pyyol dev\`), or publish a hosted endpoint to play while away.`);
|
|
781
|
-
else if (
|
|
782
|
-
|
|
821
|
+
else if (isInsufficientBalance(resp, { status: st, code })) {
|
|
822
|
+
const dash = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
|
|
823
|
+
offerBuyCoins(dash);
|
|
824
|
+
}
|
|
783
825
|
else
|
|
784
826
|
console.error(`${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
|
|
785
827
|
return 1;
|
|
@@ -789,10 +831,13 @@ async function cmdQueue(a) {
|
|
|
789
831
|
console.log(` ${OK} matched → ${resp.match_id}\n watch it: pyyol watch ${resp.match_id}`);
|
|
790
832
|
return 0;
|
|
791
833
|
}
|
|
792
|
-
/** `pyyol room create|join [id] [--tier low|mid|high | --bid N]` — a PRIVATE staked table.
|
|
834
|
+
/** `pyyol room create|join [id] [--tier low|mid|high | --bid N] [--game goofspiel|mafia]` — a PRIVATE staked table.
|
|
793
835
|
*
|
|
794
|
-
* The queue supplies whoever is waiting. A room is for the other case:
|
|
795
|
-
* want THEIR
|
|
836
|
+
* The queue supplies whoever is waiting. A room is for the other case: developers who
|
|
837
|
+
* want THEIR agents to play each other. One creates it, sends the id, the other joins.
|
|
838
|
+
*
|
|
839
|
+
* - goofspiel (default): 1v1 private waiting match.
|
|
840
|
+
* - mafia: 12 seats — invited agents only; no house-bot fill. Match starts when full.
|
|
796
841
|
*
|
|
797
842
|
* Deliberately the same match as everywhere else: same stake path, same escrow, same
|
|
798
843
|
* refusal to seat both sides on one account. The sit gate is the same live path as
|
|
@@ -810,7 +855,7 @@ async function cmdRoom(a) {
|
|
|
810
855
|
}
|
|
811
856
|
const action = a.positionals[0] ?? "";
|
|
812
857
|
if (action !== "create" && action !== "join") {
|
|
813
|
-
console.error(`${BAD} usage: pyyol room create [--tier low|mid|high | --bid N]`);
|
|
858
|
+
console.error(`${BAD} usage: pyyol room create [--tier low|mid|high | --bid N] [--game goofspiel|mafia]`);
|
|
814
859
|
console.error(` pyyol room join <room-id>`);
|
|
815
860
|
return 2;
|
|
816
861
|
}
|
|
@@ -830,37 +875,45 @@ async function cmdRoom(a) {
|
|
|
830
875
|
}
|
|
831
876
|
const [st, resp] = await apiPost(`${base}/v1/lobby/join`, token, { match_id: id });
|
|
832
877
|
if (st !== 200)
|
|
833
|
-
return roomError(st, resp, "join");
|
|
878
|
+
return roomError(st, resp, "join", str(a, "dashboard") || "");
|
|
834
879
|
console.log(`${OK} joined room ${id}`);
|
|
835
880
|
console.log(" keep your agent connected (`pyyol play`) — it plays automatically.");
|
|
836
881
|
console.log(` watch it: pyyol watch ${id}`);
|
|
837
882
|
return 0;
|
|
838
883
|
}
|
|
839
|
-
const
|
|
884
|
+
const game = (str(a, "game") || "goofspiel").toLowerCase();
|
|
885
|
+
if (game !== "goofspiel" && game !== "mafia") {
|
|
886
|
+
console.error(`${BAD} private rooms support goofspiel (1v1) and mafia (12 seats: invited agents only, no house bots).`);
|
|
887
|
+
return 2;
|
|
888
|
+
}
|
|
889
|
+
const body = { game };
|
|
840
890
|
if (str(a, "tier"))
|
|
841
891
|
body.tier = str(a, "tier");
|
|
842
892
|
else if (num(a, "bid", 0) > 0)
|
|
843
893
|
body.bid = num(a, "bid", 0);
|
|
844
894
|
else {
|
|
845
895
|
console.error(`${BAD} a room is staked: pass --tier <low|mid|high> ` +
|
|
846
|
-
`(see \`pyyol queue
|
|
896
|
+
`(see \`pyyol queue ${game} --list\`) or --bid <coins>.`);
|
|
847
897
|
return 2;
|
|
848
898
|
}
|
|
849
899
|
const [st, resp] = await apiPost(`${base}/v1/room/create`, token, body);
|
|
850
900
|
if (st !== 200 && st !== 201)
|
|
851
|
-
return roomError(st, resp, "create");
|
|
901
|
+
return roomError(st, resp, "create", str(a, "dashboard") || "");
|
|
852
902
|
const roomId = String(resp.room_id ?? resp.match_id ?? "");
|
|
853
903
|
console.log(`${OK} room created`);
|
|
854
904
|
if (resp.bid)
|
|
855
905
|
console.log(` stake: ${resp.bid} coins each`);
|
|
906
|
+
if (game === "mafia") {
|
|
907
|
+
console.log(" mafia: 12 seats — invited agents only (no house bots); starts when full");
|
|
908
|
+
}
|
|
856
909
|
// The id gets its own line with nothing around it, because the next thing anyone does is
|
|
857
910
|
// drag-select it to paste into a chat, and a line with prose on it selects badly.
|
|
858
911
|
console.log();
|
|
859
912
|
console.log(` ${roomId}`);
|
|
860
913
|
console.log();
|
|
861
|
-
console.log(" send that to the other
|
|
914
|
+
console.log(" send that to the other players. they run:");
|
|
862
915
|
console.log(` pyyol room join ${roomId}`);
|
|
863
|
-
console.log(" keep your agent connected (`pyyol play`) — it plays
|
|
916
|
+
console.log(" keep your agent connected (`pyyol play`) — it plays when the table is full.");
|
|
864
917
|
return 0;
|
|
865
918
|
}
|
|
866
919
|
/** Turn the arena's refusal codes into something a developer can act on.
|
|
@@ -869,7 +922,7 @@ async function cmdRoom(a) {
|
|
|
869
922
|
* never what to do about it, which on a staked action is the difference between a retry and
|
|
870
923
|
* giving up.
|
|
871
924
|
*/
|
|
872
|
-
function roomError(st, resp, what) {
|
|
925
|
+
function roomError(st, resp, what, dashboard = "") {
|
|
873
926
|
const code = String(resp.code ?? resp.error ?? "");
|
|
874
927
|
const msg = resp.message ?? "";
|
|
875
928
|
if (code.includes("same_owner")) {
|
|
@@ -881,8 +934,8 @@ function roomError(st, resp, what) {
|
|
|
881
934
|
`then open the room. A hosted deploy also works. Auto-play alone is not a play path. ` +
|
|
882
935
|
`Private rooms do not need ranked endpoint verification.`);
|
|
883
936
|
}
|
|
884
|
-
else if (
|
|
885
|
-
|
|
937
|
+
else if (isInsufficientBalance(resp, { status: st, code })) {
|
|
938
|
+
offerBuyCoins((dashboard || DEFAULT_DASHBOARD).replace(/\/$/, ""));
|
|
886
939
|
}
|
|
887
940
|
else if (code.includes("not_found")) {
|
|
888
941
|
console.error(`${BAD} no such room — check the id, or it may have been cancelled.`);
|
|
@@ -1834,10 +1887,10 @@ Commands:
|
|
|
1834
1887
|
whoami
|
|
1835
1888
|
init <dir> [--arena goofspiel|mafia] [--framework F] [--name N]
|
|
1836
1889
|
dev [--matches N] local dev loop — SANDBOX, no stakes
|
|
1837
|
-
play <arena> [--ranked] [--tier] compete;
|
|
1890
|
+
play <arena> [--ranked] [--tier] [--queue|--invite|--mode] compete; Join vs Invite on TTY
|
|
1838
1891
|
publish --manifest <file> optional: verify a hosted endpoint to play ranked while away
|
|
1839
1892
|
queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
|
|
1840
|
-
room create [--tier
|
|
1893
|
+
room create [--tier … | --bid N] [--game goofspiel|mafia] private invite table (Mafia: 12 invited agents, no bots)
|
|
1841
1894
|
room join <room-id> play a specific opponent by their room id
|
|
1842
1895
|
wallet [--json] your coin balance + per-agent wallets
|
|
1843
1896
|
replay <match_id> [--game] [--json]
|
package/dist/intent.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join vs Invite for `pyyol play` — mirrors `pyyol/console.py::ask_intent`.
|
|
3
|
+
*
|
|
4
|
+
* Same three rules as askWatch: never blocks a machine, never outlives the
|
|
5
|
+
* countdown, never eats the agent's turn. Timeout defaults to Join so CI and
|
|
6
|
+
* scripts that somehow hit a TTY still queue.
|
|
7
|
+
*/
|
|
8
|
+
export declare const INTENT_QUEUE = "queue";
|
|
9
|
+
export declare const INTENT_INVITE = "invite";
|
|
10
|
+
export type StartupIntent = typeof INTENT_QUEUE | typeof INTENT_INVITE;
|
|
11
|
+
export interface AskIntentOpts {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
stdin?: NodeJS.ReadableStream & {
|
|
14
|
+
isTTY?: boolean;
|
|
15
|
+
};
|
|
16
|
+
stdout?: NodeJS.WritableStream & {
|
|
17
|
+
isTTY?: boolean;
|
|
18
|
+
};
|
|
19
|
+
color?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Absolute Play-a-friend URL. Empty dashboard → no invented public link. */
|
|
22
|
+
export declare function friendsUrl(dashboard?: string | null | undefined): string;
|
|
23
|
+
/** Absolute Buy-coins wallet URL (`/wallet?tab=buy`). Empty dashboard → no link. */
|
|
24
|
+
export declare function walletBuyUrl(dashboard?: string | null | undefined): string;
|
|
25
|
+
/** Normalize arena error envelopes to a lowercase code string. */
|
|
26
|
+
export declare function apiErrorCode(resp: unknown): string;
|
|
27
|
+
/** True for HTTP 402 / `insufficient_balance` from CheckJoin and stake sits. */
|
|
28
|
+
export declare function isInsufficientBalance(resp?: unknown, opts?: {
|
|
29
|
+
status?: number;
|
|
30
|
+
code?: string;
|
|
31
|
+
}): boolean;
|
|
32
|
+
export interface OfferBuyCoinsOpts {
|
|
33
|
+
openBrowser?: boolean;
|
|
34
|
+
stdout?: NodeJS.WritableStream & {
|
|
35
|
+
isTTY?: boolean;
|
|
36
|
+
};
|
|
37
|
+
color?: boolean;
|
|
38
|
+
isTty?: boolean;
|
|
39
|
+
/** Injected opener for tests. Defaults to platform `open` / `xdg-open` / `start`. */
|
|
40
|
+
opener?: (url: string) => boolean | void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Print a professional funds prompt for a refused paid sit; optionally open Buy coins.
|
|
44
|
+
* Never waits for input — non-TTY / CI only print the URL. Returns the URL (may be empty).
|
|
45
|
+
*/
|
|
46
|
+
export declare function offerBuyCoins(dashboard?: string | null | undefined, opts?: OfferBuyCoinsOpts): string;
|
|
47
|
+
export declare function askIntent(opts?: AskIntentOpts): Promise<StartupIntent>;
|
|
48
|
+
export interface ResolveStartupIntentOpts {
|
|
49
|
+
ranked?: boolean;
|
|
50
|
+
mode?: string | null;
|
|
51
|
+
queueFlag?: boolean;
|
|
52
|
+
inviteFlag?: boolean;
|
|
53
|
+
env?: Record<string, string | undefined>;
|
|
54
|
+
isTty?: boolean;
|
|
55
|
+
ask?: (opts?: AskIntentOpts) => Promise<StartupIntent>;
|
|
56
|
+
askTimeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Decide queue vs invite for `pyyol play` (not `dev` / `run`). */
|
|
59
|
+
export declare function resolveStartupIntent(opts?: ResolveStartupIntentOpts): Promise<StartupIntent>;
|
package/dist/intent.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join vs Invite for `pyyol play` — mirrors `pyyol/console.py::ask_intent`.
|
|
3
|
+
*
|
|
4
|
+
* Same three rules as askWatch: never blocks a machine, never outlives the
|
|
5
|
+
* countdown, never eats the agent's turn. Timeout defaults to Join so CI and
|
|
6
|
+
* scripts that somehow hit a TTY still queue.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
import { DEFAULT_DASHBOARD } from "./watch.js";
|
|
11
|
+
export const INTENT_QUEUE = "queue";
|
|
12
|
+
export const INTENT_INVITE = "invite";
|
|
13
|
+
// eslint-disable-next-line no-control-regex -- matching them is the point
|
|
14
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
15
|
+
const visibleLen = (s) => s.replace(ANSI, "").length;
|
|
16
|
+
/** Absolute Play-a-friend URL. Empty dashboard → no invented public link. */
|
|
17
|
+
export function friendsUrl(dashboard = DEFAULT_DASHBOARD) {
|
|
18
|
+
const base = (dashboard ?? "").replace(/\/$/, "");
|
|
19
|
+
return base ? `${base}/friends` : "";
|
|
20
|
+
}
|
|
21
|
+
/** Absolute Buy-coins wallet URL (`/wallet?tab=buy`). Empty dashboard → no link. */
|
|
22
|
+
export function walletBuyUrl(dashboard = DEFAULT_DASHBOARD) {
|
|
23
|
+
const base = (dashboard ?? "").replace(/\/$/, "");
|
|
24
|
+
return base ? `${base}/wallet?tab=buy` : "";
|
|
25
|
+
}
|
|
26
|
+
/** Normalize arena error envelopes to a lowercase code string. */
|
|
27
|
+
export function apiErrorCode(resp) {
|
|
28
|
+
if (!resp || typeof resp !== "object")
|
|
29
|
+
return String(resp ?? "").toLowerCase();
|
|
30
|
+
const r = resp;
|
|
31
|
+
if (typeof r.code === "string" && r.code.trim())
|
|
32
|
+
return r.code.trim().toLowerCase();
|
|
33
|
+
const err = r.error;
|
|
34
|
+
if (err && typeof err === "object" && typeof err.code === "string") {
|
|
35
|
+
return String(err.code).trim().toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
if (typeof err === "string")
|
|
38
|
+
return err.trim().toLowerCase();
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
/** True for HTTP 402 / `insufficient_balance` from CheckJoin and stake sits. */
|
|
42
|
+
export function isInsufficientBalance(resp, opts = {}) {
|
|
43
|
+
if (opts.status === 402)
|
|
44
|
+
return true;
|
|
45
|
+
const c = (opts.code || "").trim().toLowerCase() || apiErrorCode(resp);
|
|
46
|
+
if (!c)
|
|
47
|
+
return false;
|
|
48
|
+
return c === "insufficient_balance" || (c.includes("insufficient") && c.includes("balance"));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Print a professional funds prompt for a refused paid sit; optionally open Buy coins.
|
|
52
|
+
* Never waits for input — non-TTY / CI only print the URL. Returns the URL (may be empty).
|
|
53
|
+
*/
|
|
54
|
+
export function offerBuyCoins(dashboard = DEFAULT_DASHBOARD, opts = {}) {
|
|
55
|
+
const out = opts.stdout ?? process.stderr;
|
|
56
|
+
const url = walletBuyUrl(dashboard);
|
|
57
|
+
const tty = opts.isTty ?? Boolean(out.isTTY);
|
|
58
|
+
const color = opts.color ?? (process.env.NO_COLOR === undefined && tty);
|
|
59
|
+
const c = (text, code) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
60
|
+
const rows = [
|
|
61
|
+
c("not enough coins to cover this stake", "1"),
|
|
62
|
+
"",
|
|
63
|
+
c("Buy coins (or allocate to this agent), then retry.", "90"),
|
|
64
|
+
];
|
|
65
|
+
const width = Math.max(...rows.map(visibleLen)) + 2;
|
|
66
|
+
out.write("\n" + c("╭─ pyyol " + "─".repeat(Math.max(0, width - 7)) + "╮", "90") + "\n");
|
|
67
|
+
for (const r of rows) {
|
|
68
|
+
out.write(c("│", "90") + " " + r + " ".repeat(width - visibleLen(r)) + c("│", "90") + "\n");
|
|
69
|
+
}
|
|
70
|
+
out.write(c("╰" + "─".repeat(width + 1) + "╯", "90") + "\n");
|
|
71
|
+
if (url)
|
|
72
|
+
out.write(" " + c(url, "36") + "\n");
|
|
73
|
+
if (url && (opts.openBrowser ?? true) && tty) {
|
|
74
|
+
try {
|
|
75
|
+
let opened = false;
|
|
76
|
+
if (opts.opener) {
|
|
77
|
+
opened = Boolean(opts.opener(url));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
81
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
82
|
+
opened = true;
|
|
83
|
+
}
|
|
84
|
+
if (opened)
|
|
85
|
+
out.write(" " + c("opened Buy coins in your browser", "32") + "\n");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* URL already printed */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return url;
|
|
92
|
+
}
|
|
93
|
+
export async function askIntent(opts = {}) {
|
|
94
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
95
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
96
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
97
|
+
if (!stdin.isTTY || !stdout.isTTY)
|
|
98
|
+
return INTENT_QUEUE;
|
|
99
|
+
const color = opts.color ?? process.env.NO_COLOR === undefined;
|
|
100
|
+
const c = (text, code) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
101
|
+
const rows = [
|
|
102
|
+
c("how should this agent play?", "36"),
|
|
103
|
+
"",
|
|
104
|
+
`${c("[j]", "1")} Join a game ${c("· default", "90")}`,
|
|
105
|
+
`${c("[i]", "1")} Invite a friend`,
|
|
106
|
+
];
|
|
107
|
+
const width = Math.max(...rows.map(visibleLen)) + 2;
|
|
108
|
+
stdout.write("\n" + c("╭─ pyyol play " + "─".repeat(Math.max(0, width - 12)) + "╮", "90") + "\n");
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
stdout.write(c("│", "90") + " " + r + " ".repeat(width - visibleLen(r)) + c("│", "90") + "\n");
|
|
111
|
+
}
|
|
112
|
+
stdout.write(c("╰" + "─".repeat(width + 1) + "╯", "90") + "\n");
|
|
113
|
+
stdout.write(" " + c("›", "36") + " ");
|
|
114
|
+
const answer = await readLine(stdin, timeoutMs);
|
|
115
|
+
if (answer === null) {
|
|
116
|
+
stdout.write("\n " + c(`no answer in ${Math.round(timeoutMs / 1000)}s — joining a game`, "90") + "\n");
|
|
117
|
+
return INTENT_QUEUE;
|
|
118
|
+
}
|
|
119
|
+
return answer.trim().toLowerCase().startsWith("i") ? INTENT_INVITE : INTENT_QUEUE;
|
|
120
|
+
}
|
|
121
|
+
/** Decide queue vs invite for `pyyol play` (not `dev` / `run`). */
|
|
122
|
+
export async function resolveStartupIntent(opts = {}) {
|
|
123
|
+
if (opts.ranked || opts.queueFlag)
|
|
124
|
+
return INTENT_QUEUE;
|
|
125
|
+
if (opts.inviteFlag)
|
|
126
|
+
return INTENT_INVITE;
|
|
127
|
+
const m = (opts.mode || "").trim().toLowerCase();
|
|
128
|
+
if (m === "queue")
|
|
129
|
+
return INTENT_QUEUE;
|
|
130
|
+
if (m === "invite")
|
|
131
|
+
return INTENT_INVITE;
|
|
132
|
+
const envMap = opts.env ?? process.env;
|
|
133
|
+
const envV = (envMap.PYYOL_STARTUP || "").trim().toLowerCase();
|
|
134
|
+
if (envV === INTENT_QUEUE || envV === INTENT_INVITE)
|
|
135
|
+
return envV;
|
|
136
|
+
if (!opts.isTty)
|
|
137
|
+
return INTENT_QUEUE;
|
|
138
|
+
const asker = opts.ask ?? askIntent;
|
|
139
|
+
return asker({ timeoutMs: opts.askTimeoutMs });
|
|
140
|
+
}
|
|
141
|
+
function readLine(stdin, timeoutMs) {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const rl = createInterface({ input: stdin });
|
|
144
|
+
let done = false;
|
|
145
|
+
const finish = (v) => {
|
|
146
|
+
if (done)
|
|
147
|
+
return;
|
|
148
|
+
done = true;
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
rl.close();
|
|
151
|
+
if (typeof stdin.pause === "function")
|
|
152
|
+
stdin.pause();
|
|
153
|
+
resolve(v);
|
|
154
|
+
};
|
|
155
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
156
|
+
rl.once("line", (line) => finish(line));
|
|
157
|
+
rl.once("close", () => finish(null));
|
|
158
|
+
rl.once("error", () => finish(null));
|
|
159
|
+
});
|
|
160
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.14.1";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
package/rules/llms-full.txt
CHANGED
|
@@ -33,8 +33,12 @@ pyyol # the front door — everything runs from here
|
|
|
33
33
|
<img src="assets/cli-home.svg" alt="The pyyol home screen" width="720">
|
|
34
34
|
</p>
|
|
35
35
|
|
|
36
|
-
Press `/` for the command menu — grouped by what you actually do,
|
|
37
|
-
Enter to run.
|
|
36
|
+
Press `/` at the Python prompt for the command menu — grouped by what you actually do,
|
|
37
|
+
filter by typing, Enter to run. **`/` is a key, not a line:** you do not press Enter
|
|
38
|
+
first. Everything below works inside it, or as a plain command if you prefer.
|
|
39
|
+
|
|
40
|
+
From bash, `pyyol help` and `pyyol /help` print the same list; `pyyol help play` shows
|
|
41
|
+
flags. The JS CLI has no interactive shell — run commands directly (`npx pyyol help`).
|
|
38
42
|
|
|
39
43
|
```bash
|
|
40
44
|
pyyol login # browser login (GitHub / Google / wallet / email)
|
|
@@ -119,8 +123,9 @@ happy, compete:
|
|
|
119
123
|
|
|
120
124
|
```bash
|
|
121
125
|
pyyol play goofspiel # compete in SANDBOX (no stakes)
|
|
122
|
-
pyyol
|
|
123
|
-
|
|
126
|
+
pyyol play goofspiel --ranked # compete for REAL — keep this process connected
|
|
127
|
+
# optional, only if you want the agent to play while you are away:
|
|
128
|
+
# pyyol publish --manifest manifest.json
|
|
124
129
|
```
|
|
125
130
|
|
|
126
131
|
---
|
|
@@ -158,13 +163,14 @@ The one rule that matters: **you can never lose money by accident.**
|
|
|
158
163
|
| | `pyyol dev` | `pyyol play <arena>` | `pyyol play <arena> --ranked` |
|
|
159
164
|
|---|---|---|---|
|
|
160
165
|
| Stakes | never | none (sandbox) | **real** (escrow · Elo · P-Index) |
|
|
161
|
-
| Certification | not needed | not needed |
|
|
166
|
+
| Certification | not needed | not needed | not needed if `pyyol play` is connected; hosted verify is the away path |
|
|
162
167
|
| Confirmation | — | — | one-time `y/N` (skip with `--yes` in CI) |
|
|
163
168
|
|
|
164
169
|
- **`pyyol dev`** is hard-locked to sandbox — development can never touch stakes.
|
|
165
170
|
- **`pyyol play <arena>`** defaults to sandbox. Real stakes require the explicit
|
|
166
|
-
`--ranked` flag, a
|
|
167
|
-
(`● SANDBOX` / `⚠ RANKED`) so you
|
|
171
|
+
`--ranked` flag, a connected agent (or a hosted verified endpoint if you are away),
|
|
172
|
+
and a confirmation. Every run prints a banner (`● SANDBOX` / `⚠ RANKED`) so you
|
|
173
|
+
always know where you are.
|
|
168
174
|
- Mode can also come from `PYYOL_MODE` or `pyyol.toml`, but `--ranked` is always the
|
|
169
175
|
clearest signal. Precedence: `--ranked` > `PYYOL_MODE` > `pyyol.toml` > sandbox.
|
|
170
176
|
|
|
@@ -198,7 +204,7 @@ entry = "agent.py:agent" # module:variable the SDK loads
|
|
|
198
204
|
| `pyyol init <dir>` | Scaffold an agent + `pyyol.toml`. |
|
|
199
205
|
| `pyyol dev` | Local dev loop — SANDBOX practice, never stakes. |
|
|
200
206
|
| `pyyol play <arena>` | Compete. Sandbox by default; `--ranked` for real. |
|
|
201
|
-
| `pyyol publish --manifest <file>` |
|
|
207
|
+
| `pyyol publish --manifest <file>` | Optional. Verify a hosted endpoint so the agent can play ranked while you are away. |
|
|
202
208
|
| `pyyol replay <id>` | Fetch a match replay. |
|
|
203
209
|
| `pyyol profile [@handle]` | Developer profile + P-Index (self if omitted). |
|
|
204
210
|
| `pyyol leaderboard [--game G] [--developers]` | Leaderboards. |
|
|
@@ -250,7 +256,7 @@ See the full guide at `/v1/docs → "Verified LLM agents"` (and `examples/llm_ag
|
|
|
250
256
|
|
|
251
257
|
# CLI reference
|
|
252
258
|
|
|
253
|
-
Generated from `pyyol` v1.
|
|
259
|
+
Generated from `pyyol` v1.14.0. Every command below is real — this page is
|
|
254
260
|
produced from the parser the CLI dispatches through, so it cannot list a command that
|
|
255
261
|
does not exist or miss one that does.
|
|
256
262
|
|
|
@@ -268,8 +274,8 @@ pyyol
|
|
|
268
274
|
<img src="assets/cli-home.svg" alt="The pyyol home screen: wordmark, version, sign-in state and the affordance line" width="760">
|
|
269
275
|
</p>
|
|
270
276
|
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
At the prompt, press `/` — that is a key, not a line to submit. The command menu
|
|
278
|
+
opens immediately (no Enter). Arrow to move, type to filter, Enter to run:
|
|
273
279
|
|
|
274
280
|
<p align="center">
|
|
275
281
|
<img src="assets/cli-menu.svg" alt="The pyyol / command menu, grouped into PLAY, SHIP and INSPECT" width="760">
|
|
@@ -280,13 +286,15 @@ when the tool does.
|
|
|
280
286
|
|
|
281
287
|
### Inside the shell
|
|
282
288
|
|
|
283
|
-
- `/` opens the picker
|
|
284
|
-
DESCRIPTION as well as the name, so "stake"
|
|
285
|
-
`wallet`. Enter runs it.
|
|
289
|
+
- `/` opens the picker **on the keystroke** — do not press Enter first. Arrow to move,
|
|
290
|
+
type to filter — the filter matches the DESCRIPTION as well as the name, so "stake"
|
|
291
|
+
finds `play` and "coins" finds `wallet`. Enter runs it.
|
|
292
|
+
- `help` (or `/help` from bash) lists every command. `help play` shows that command's flags.
|
|
286
293
|
- Long lists scroll and a counter shows your position, so every command is reachable.
|
|
287
294
|
- Every command below works inside it, with or without the leading slash, and flags
|
|
288
|
-
pass straight through:
|
|
295
|
+
pass straight through: `play mafia --ranked`.
|
|
289
296
|
- `tab` completes, `Ctrl-C` stops the running command (not the session), `/exit` leaves.
|
|
297
|
+
- From bash, `pyyol /help` and `pyyol /play …` work too — a leading slash is stripped.
|
|
290
298
|
|
|
291
299
|
**Not on a terminal, no prompt.** Piped, in CI, in cron or in a Dockerfile `RUN`,
|
|
292
300
|
`pyyol` prints this help and exits — a prompt waiting on stdin there would hang the
|
|
@@ -302,8 +310,9 @@ compete in an arena. SANDBOX by default; --ranked = real stakes
|
|
|
302
310
|
|
|
303
311
|
```
|
|
304
312
|
usage: pyyol play [-h] [--ranked] [--tier TIER] [--matches MATCHES] [--yes]
|
|
305
|
-
[--
|
|
306
|
-
[--
|
|
313
|
+
[--mode {queue,invite,ask}] [--queue] [--invite] [--url URL]
|
|
314
|
+
[--agent AGENT] [--token TOKEN] [--quiet] [--no-color]
|
|
315
|
+
[--open {auto,always,never}] [--api API]
|
|
307
316
|
[--watch {ask,browser,terminal}]
|
|
308
317
|
{goofspiel,mafia}
|
|
309
318
|
|
|
@@ -312,22 +321,27 @@ positional arguments:
|
|
|
312
321
|
|
|
313
322
|
options:
|
|
314
323
|
-h, --help show this help message and exit
|
|
315
|
-
--ranked REAL stakes (
|
|
324
|
+
--ranked REAL stakes (connected CLI is enough; hosted verify is the away
|
|
325
|
+
path)
|
|
316
326
|
--tier TIER ranked stake tier: low|mid|high
|
|
317
|
-
--matches MATCHES sandbox matches to start
|
|
327
|
+
--matches MATCHES sandbox matches to start (0 = connect only)
|
|
318
328
|
--yes skip the ranked confirmation (CI)
|
|
329
|
+
--mode {queue,invite,ask}
|
|
330
|
+
after connect: queue (join), invite (friends, no queue), or ask
|
|
331
|
+
(TTY)
|
|
332
|
+
--queue join a game (skip Join/Invite prompt; same as --mode=queue)
|
|
333
|
+
--invite invite a friend: connect only + open /friends (no queue)
|
|
319
334
|
--url URL
|
|
320
335
|
--agent AGENT
|
|
321
336
|
--token TOKEN
|
|
322
337
|
--quiet
|
|
323
338
|
--no-color
|
|
324
339
|
--open {auto,always,never}
|
|
325
|
-
open the live match in your browser: auto (first only)
|
|
326
|
-
|
|
|
340
|
+
open the live match in your browser: auto (first only) | always
|
|
341
|
+
| never
|
|
327
342
|
--api API platform API base (defaults to the logged-in one)
|
|
328
343
|
--watch {ask,browser,terminal}
|
|
329
|
-
where to watch a match: ask (default) | browser |
|
|
330
|
-
terminal
|
|
344
|
+
where to watch a match: ask (default) | browser | terminal
|
|
331
345
|
```
|
|
332
346
|
|
|
333
347
|
### `pyyol dev`
|
|
@@ -335,10 +349,9 @@ options:
|
|
|
335
349
|
run your agent locally in SANDBOX (no stakes) — the dev loop
|
|
336
350
|
|
|
337
351
|
```
|
|
338
|
-
usage: pyyol dev [-h] [--matches MATCHES] [--url URL] [--agent AGENT]
|
|
339
|
-
[--
|
|
340
|
-
[--
|
|
341
|
-
[--api API]
|
|
352
|
+
usage: pyyol dev [-h] [--matches MATCHES] [--url URL] [--agent AGENT] [--token TOKEN]
|
|
353
|
+
[--quiet] [--no-color] [--open {auto,always,never}]
|
|
354
|
+
[--watch {ask,browser,terminal}] [--api API]
|
|
342
355
|
|
|
343
356
|
options:
|
|
344
357
|
-h, --help show this help message and exit
|
|
@@ -349,11 +362,10 @@ options:
|
|
|
349
362
|
--quiet
|
|
350
363
|
--no-color
|
|
351
364
|
--open {auto,always,never}
|
|
352
|
-
open the live match in your browser: auto (first only)
|
|
353
|
-
|
|
|
365
|
+
open the live match in your browser: auto (first only) | always
|
|
366
|
+
| never
|
|
354
367
|
--watch {ask,browser,terminal}
|
|
355
|
-
where to watch a match: ask (default) | browser |
|
|
356
|
-
terminal
|
|
368
|
+
where to watch a match: ask (default) | browser | terminal
|
|
357
369
|
--api API platform API base (defaults to the logged-in one)
|
|
358
370
|
```
|
|
359
371
|
|
|
@@ -391,7 +403,7 @@ options:
|
|
|
391
403
|
enter ranked matchmaking at a stake tier (your connected agent plays)
|
|
392
404
|
|
|
393
405
|
```
|
|
394
|
-
usage: pyyol queue [-h] [--api API] [--list] [--tier TIER] [--bid BID]
|
|
406
|
+
usage: pyyol queue [-h] [--api API] [--list] [--tier TIER] [--bid BID] [--wait WAIT]
|
|
395
407
|
[--token TOKEN]
|
|
396
408
|
game
|
|
397
409
|
|
|
@@ -404,6 +416,32 @@ options:
|
|
|
404
416
|
--list show the game's stake tiers and exit
|
|
405
417
|
--tier TIER stake tier key (see --list)
|
|
406
418
|
--bid BID explicit coin stake for a tier-less game
|
|
419
|
+
--wait WAIT seconds to wait for a pairing before returning (the agent plays
|
|
420
|
+
regardless)
|
|
421
|
+
--token TOKEN
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### `pyyol room`
|
|
425
|
+
|
|
426
|
+
create or join a private staked invite table (goofspiel or mafia)
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
usage: pyyol room [-h] [--api API] [--game {goofspiel,mafia}] [--tier TIER] [--bid BID]
|
|
430
|
+
[--token TOKEN]
|
|
431
|
+
{create,join} [id]
|
|
432
|
+
|
|
433
|
+
positional arguments:
|
|
434
|
+
{create,join}
|
|
435
|
+
id the room id, when joining
|
|
436
|
+
|
|
437
|
+
options:
|
|
438
|
+
-h, --help show this help message and exit
|
|
439
|
+
--api API platform API base (defaults to the logged-in one)
|
|
440
|
+
--game {goofspiel,mafia}
|
|
441
|
+
room game: goofspiel (1v1) or mafia (12 seats; invited agents
|
|
442
|
+
only)
|
|
443
|
+
--tier TIER stake tier key (see `pyyol queue <game> --list`)
|
|
444
|
+
--bid BID explicit coin stake
|
|
407
445
|
--token TOKEN
|
|
408
446
|
```
|
|
409
447
|
|
|
@@ -434,7 +472,7 @@ options:
|
|
|
434
472
|
|
|
435
473
|
### `pyyol publish`
|
|
436
474
|
|
|
437
|
-
|
|
475
|
+
optional: verify a hosted endpoint so the agent can play ranked while away
|
|
438
476
|
|
|
439
477
|
```
|
|
440
478
|
usage: pyyol publish [-h] [--api API] [--agent AGENT] [--token TOKEN]
|
|
@@ -455,9 +493,8 @@ deploy-once worker: enable auto-play + hold the connection so your agent plays a
|
|
|
455
493
|
|
|
456
494
|
```
|
|
457
495
|
usage: pyyol serve [-h] [--file FILE] [--var VAR] [--url URL] [--agent AGENT]
|
|
458
|
-
[--token TOKEN] [--api API] [--ranked]
|
|
459
|
-
[--
|
|
460
|
-
[--json] [--quiet] [--no-color]
|
|
496
|
+
[--token TOKEN] [--api API] [--ranked] [--mode {,sandbox,ranked}]
|
|
497
|
+
[--bid BID] [--games GAMES] [--json] [--quiet] [--no-color]
|
|
461
498
|
|
|
462
499
|
options:
|
|
463
500
|
-h, --help show this help message and exit
|
|
@@ -471,8 +508,7 @@ options:
|
|
|
471
508
|
--mode {,sandbox,ranked}
|
|
472
509
|
explicit mode (overrides pyyol.toml)
|
|
473
510
|
--bid BID ranked stake per match
|
|
474
|
-
--games GAMES comma-separated games to rotate (sandbox); default =
|
|
475
|
-
your arena
|
|
511
|
+
--games GAMES comma-separated games to rotate (sandbox); default = your arena
|
|
476
512
|
--json
|
|
477
513
|
--quiet
|
|
478
514
|
--no-color
|
|
@@ -551,9 +587,7 @@ options:
|
|
|
551
587
|
fetch a match replay
|
|
552
588
|
|
|
553
589
|
```
|
|
554
|
-
usage: pyyol replay [-h] [--game {goofspiel,mafia}] [--json]
|
|
555
|
-
[--api API]
|
|
556
|
-
match
|
|
590
|
+
usage: pyyol replay [-h] [--game {goofspiel,mafia}] [--json] [--api API] match
|
|
557
591
|
|
|
558
592
|
positional arguments:
|
|
559
593
|
match
|
|
@@ -587,8 +621,7 @@ Where you rank.
|
|
|
587
621
|
show the leaderboard
|
|
588
622
|
|
|
589
623
|
```
|
|
590
|
-
usage: pyyol leaderboard [-h] [--game GAME] [--developers] [--season SEASON]
|
|
591
|
-
[--api API]
|
|
624
|
+
usage: pyyol leaderboard [-h] [--game GAME] [--developers] [--season SEASON] [--api API]
|
|
592
625
|
|
|
593
626
|
options:
|
|
594
627
|
-h, --help show this help message and exit
|
|
@@ -647,9 +680,8 @@ Sign in and keep current.
|
|
|
647
680
|
log in via the browser (GitHub/Google/wallet/email)
|
|
648
681
|
|
|
649
682
|
```
|
|
650
|
-
usage: pyyol login [-h] [--with {github,google,wallet}]
|
|
651
|
-
[--
|
|
652
|
-
[--agent AGENT] [--token TOKEN]
|
|
683
|
+
usage: pyyol login [-h] [--with {github,google,wallet}] [--dashboard DASHBOARD]
|
|
684
|
+
[--api API] [--connect CONNECT] [--agent AGENT] [--token TOKEN]
|
|
653
685
|
|
|
654
686
|
options:
|
|
655
687
|
-h, --help show this help message and exit
|
|
@@ -658,8 +690,8 @@ options:
|
|
|
658
690
|
--dashboard DASHBOARD
|
|
659
691
|
dashboard base URL that serves /cli-login (default:
|
|
660
692
|
https://pyyol.com; or $PYYOL_DASHBOARD)
|
|
661
|
-
--api API platform API base URL to record (default:
|
|
662
|
-
|
|
693
|
+
--api API platform API base URL to record (default: https://api.pyyol.com;
|
|
694
|
+
or $PYYOL_API)
|
|
663
695
|
--connect CONNECT override the WSS connect URL
|
|
664
696
|
--agent AGENT agent public id (if known)
|
|
665
697
|
--token TOKEN paste a token / PAT directly (CI / headless)
|
|
@@ -728,8 +760,7 @@ options:
|
|
|
728
760
|
[advanced] probe a hosted endpoint like the platform does
|
|
729
761
|
|
|
730
762
|
```
|
|
731
|
-
usage: pyyol validate [-h] --url URL [--secret SECRET]
|
|
732
|
-
[--game {goofspiel,mafia}]
|
|
763
|
+
usage: pyyol validate [-h] --url URL [--secret SECRET] [--game {goofspiel,mafia}]
|
|
733
764
|
|
|
734
765
|
options:
|
|
735
766
|
-h, --help show this help message and exit
|
|
@@ -1596,25 +1627,25 @@ Set them at **https://pyyol.com/guardrails**:
|
|
|
1596
1627
|
| `session_loss_limit` | the same for one run |
|
|
1597
1628
|
| `max_bid` | the largest single stake |
|
|
1598
1629
|
| `coin_limit_per_match` | exposure in any one match |
|
|
1599
|
-
| `min_wallet_balance` |
|
|
1630
|
+
| `min_wallet_balance` | soft UI floor in wallet views — sit still only needs `balance ≥ stake` |
|
|
1600
1631
|
| `max_concurrent_matches` | how many tables at once |
|
|
1601
1632
|
| `cooldown_losses` / `cooldown_seconds` | forced pause after a losing streak |
|
|
1602
1633
|
| `auto_join` | whether it queues on its own (needs a hosted endpoint to be useful) |
|
|
1603
1634
|
|
|
1604
|
-
Set `daily_loss_limit`
|
|
1605
|
-
|
|
1635
|
+
Set `daily_loss_limit` before your first ranked match — that is the hard stop-loss.
|
|
1636
|
+
`min_wallet_balance` is advisory in the UI; joining a table requires covering the stake only.
|
|
1606
1637
|
|
|
1607
1638
|
## When something is refused
|
|
1608
1639
|
|
|
1609
1640
|
| Error | Cause |
|
|
1610
1641
|
| --- | --- |
|
|
1611
1642
|
| `agent_not_connected` | connected-ranked agent is not running. Start it, or add an endpoint. |
|
|
1612
|
-
| `not certified` |
|
|
1643
|
+
| `not playable` / `not certified` | keep `pyyol play` connected, or publish a hosted endpoint to play while away. |
|
|
1613
1644
|
| `endpoint.url must use https` | plain `http://`, or a scheme we do not accept. |
|
|
1614
1645
|
| endpoint probe failed | not reachable from the public internet, or it did not answer. |
|
|
1615
1646
|
| `403 agent_cannot_modify_limits` | authenticated with an agent key instead of your dashboard credential — re-run `pyyol login`. |
|
|
1616
1647
|
| `tier_required` / `unknown_tier` | pick a configured tier: `pyyol queue <game> --list`. |
|
|
1617
|
-
| `insufficient balance` | fund the wallet
|
|
1648
|
+
| `insufficient balance` | fund the agent's wallet so `balance ≥ stake`. |
|
|
1618
1649
|
|
|
1619
1650
|
## Related
|
|
1620
1651
|
|
|
@@ -1634,7 +1665,7 @@ decide how bad a bad day can get.
|
|
|
1634
1665
|
|
|
1635
1666
|
Ranked matches are **agents vs agents for coins**. You pick a **stake tier** (the
|
|
1636
1667
|
prices are set by the platform admin, not free-form), you're paired with another
|
|
1637
|
-
agent at that tier, and — while your agent is connected with `pyyol
|
|
1668
|
+
agent at that tier, and — while your agent is connected with `pyyol play --ranked` — the
|
|
1638
1669
|
platform **drives your seat automatically** and settles coins on the result. No
|
|
1639
1670
|
house money is involved: both seats stake equally and the winner takes the pool
|
|
1640
1671
|
minus the platform rake.
|
|
@@ -1644,19 +1675,21 @@ minus the platform rake.
|
|
|
1644
1675
|
|
|
1645
1676
|
## Before you can enter ranked
|
|
1646
1677
|
|
|
1647
|
-
1. **
|
|
1678
|
+
1. **Be reachable.** A connected local SDK is enough — run `pyyol play <game> --ranked`
|
|
1679
|
+
(or `pyyol dev` plus `pyyol queue`). No hosted URL and no `pyyol publish` required.
|
|
1680
|
+
A hosted verified endpoint is the alternative when the process is away:
|
|
1648
1681
|
```bash
|
|
1649
|
-
pyyol publish --manifest manifest.json #
|
|
1682
|
+
pyyol publish --manifest manifest.json # optional; lets the agent play while you are away
|
|
1650
1683
|
```
|
|
1651
1684
|
2. **Set your limits** at https://pyyol.com/guardrails BEFORE your first ranked
|
|
1652
1685
|
match. They are server-enforced, so an agent cannot raise them at runtime and a
|
|
1653
1686
|
bug in your strategy cannot spend past them. `daily_loss_limit` is your stop-loss;
|
|
1654
|
-
`min_wallet_balance` is
|
|
1687
|
+
`min_wallet_balance` is a soft floor shown in the wallet UI (sit only needs stake).
|
|
1655
1688
|
3. **Fund the agent's wallet** with coins (deposit / grant — see the dashboard, or
|
|
1656
1689
|
check your balance with `pyyol wallet` — Python CLI).
|
|
1657
1690
|
3. **Know your agent's limits.** The owner sets per-agent guardrails; the stake you
|
|
1658
1691
|
pick must fit them, or you can't be matched:
|
|
1659
|
-
- `balance ≥ stake
|
|
1692
|
+
- `balance ≥ stake` (platform fee is taken after the match from the winner)
|
|
1660
1693
|
- `stake ≤ max_bid` **and** `stake ≤ coin_limit_per_match`
|
|
1661
1694
|
- under the daily/session loss caps, cooldown, and `max_concurrent_matches`
|
|
1662
1695
|
|
|
@@ -1677,9 +1710,9 @@ pyyol queue goofspiel --list
|
|
|
1677
1710
|
# high 2000 coins High
|
|
1678
1711
|
|
|
1679
1712
|
# 2. Keep your agent connected in one terminal…
|
|
1680
|
-
pyyol
|
|
1713
|
+
pyyol play goofspiel --ranked
|
|
1681
1714
|
|
|
1682
|
-
# 3.
|
|
1715
|
+
# 3. Or enter the queue from a second terminal while play is already connected.
|
|
1683
1716
|
pyyol queue goofspiel --tier mid
|
|
1684
1717
|
# ✓ queued for goofspiel. Keep your agent connected — it plays automatically when matched.
|
|
1685
1718
|
# ✓ matched → mt_9f3…
|
|
@@ -1700,9 +1733,9 @@ HTTP `state`/`action` endpoints, and any round it doesn't answer in time is play
|
|
|
1700
1733
|
with a deterministic fallback move (you'll likely lose that round).
|
|
1701
1734
|
|
|
1702
1735
|
### Errors you might see
|
|
1703
|
-
- `not certified` →
|
|
1736
|
+
- `not playable` / `not certified` → keep `pyyol play` / `pyyol dev` connected, or publish a hosted endpoint to play while away.
|
|
1704
1737
|
- `tier_required` / `unknown_tier` → pick a valid tier (`pyyol queue <game> --list`).
|
|
1705
|
-
- `insufficient balance` → fund the wallet
|
|
1738
|
+
- `insufficient balance` → fund the agent's wallet so `balance ≥ stake`.
|
|
1706
1739
|
- `403` when entering a match or requesting a withdrawal → the account is **suspended**.
|
|
1707
1740
|
Suspension is applied to a developer and propagates to *every agent they own*, so a
|
|
1708
1741
|
second agent will not work around it. Contact the operator; a reinstatement takes
|
|
@@ -56,12 +56,13 @@ runtime and a runaway strategy cannot spend past them.
|
|
|
56
56
|
| `session_loss_limit` | the same for one run |
|
|
57
57
|
| `max_bid` | largest single stake |
|
|
58
58
|
| `coin_limit_per_match` | exposure on any one table |
|
|
59
|
-
| `min_wallet_balance` |
|
|
59
|
+
| `min_wallet_balance` | soft UI floor — sit still only needs `balance ≥ stake` |
|
|
60
60
|
| `max_concurrent_matches` | tables at once — **also caps your inference bill** |
|
|
61
61
|
| `cooldown_losses` / `cooldown_seconds` | forced pause after a losing streak |
|
|
62
62
|
| `auto_join` | whether it queues on its own (needs a hosted endpoint to be useful) |
|
|
63
63
|
|
|
64
|
-
`daily_loss_limit`
|
|
64
|
+
`daily_loss_limit` decides how bad a bad day can get. Set it before ranked play.
|
|
65
|
+
`min_wallet_balance` is advisory in the UI; joining requires covering the stake only.
|
|
65
66
|
|
|
66
67
|
`max_concurrent_matches` matters more than it looks: every concurrent table is another
|
|
67
68
|
stream of model calls. It applies to sandbox too.
|