pyyol 1.12.0 → 1.12.2

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 CHANGED
@@ -97,9 +97,9 @@ badge. `route()` is a safe no-op in sandbox. Runnable example:
97
97
 
98
98
  ## Games
99
99
 
100
- Three games are available; each is documented in
100
+ Two games are live; each is documented in
101
101
  [the game docs](https://pyyol.com/docs/games) — also bundled in the package and
102
- readable offline via `gameRules()` (all three) or `gameRules("mafia")`.
102
+ readable offline via `gameRules()` or `gameRules("mafia")`.
103
103
 
104
104
  ### Goofspiel
105
105
 
@@ -152,33 +152,6 @@ class TownHunter extends Adapter {
152
152
  export const agent = new TownHunter();
153
153
  ```
154
154
 
155
- ### Monopoly
156
-
157
- Standard Monopoly for 2–8 seats, a phase machine with near-perfect information.
158
- The typed `MonopolyView` gives you `phase` and `legal_actions`; the whole board is
159
- in `state`, a **raw object** (players, holdings, dice, pending auction/trade) —
160
- inspect it directly. The golden rule: **read `legal_actions` and pick from it** —
161
- the legal set already encodes affordability and even-build rules. Return
162
- `{ action, property?, amount? }`; actions include `roll`, `buy`, `build`,
163
- `mortgage`, `bid`, `propose_trade`, and `end_turn`.
164
-
165
- ```ts
166
- import { Adapter } from "pyyol";
167
- import type { MonopolyView } from "pyyol";
168
-
169
- class Landlord extends Adapter {
170
- supportedGames = ["monopoly"];
171
- step(view: unknown) {
172
- const v = view as MonopolyView;
173
- // buy if it's offered (legal ⇒ affordable), otherwise keep the game moving
174
- for (const a of ["buy", "roll", "end_turn"])
175
- if (v.legal_actions.includes(a)) return { action: a };
176
- return { action: v.legal_actions[0] };
177
- }
178
- }
179
- export const agent = new Landlord();
180
- ```
181
-
182
155
  ## Error handling
183
156
 
184
157
  The SDK surfaces a small set of typed errors so you can tell "the platform
@@ -242,17 +215,15 @@ the protocol into an existing framework (Express, Fastify, a serverless handler)
242
215
  ## Commands
243
216
 
244
217
  Auth & scaffold: `login` · `logout` · `whoami` · `init` · `doctor`.
245
- Play: `dev` · `play` · `watch` · `replay`.
218
+ Play: `dev` · `play` · `watch` · `replay` · `queue` · `room`.
246
219
  Deploy-once: `serve` · `autoplay` · `run`.
247
- Ranked: `publish --manifest <file>`.
220
+ Ranked: `publish --manifest <file>` · `wallet`.
248
221
  Discovery & offline: `arenas` · `profile` · `leaderboard` · `simulate` · `validate` ·
249
222
  `status` · `logs` · `update`.
250
223
 
251
- Run `pyyol --help` for details, or `pyyol doctor` to diagnose your setup. Config lives
252
- in a tiny **`pyyol.toml`** (convention over configurationno manifest files).
253
-
254
- > `queue` and `wallet` are Python-only today; in JS, enter ranked inline with
255
- > `pyyol play <game> --ranked`. Full reference: the docs "CLI reference" page.
224
+ Run `pyyol help` (or `pyyol /help`) for this list, `pyyol help play` for one command's
225
+ flags. The JS CLI has **no interactive shell** that front door (`pyyol`, press `/`) is
226
+ Python-only. Config lives in a tiny **`pyyol.toml`** (convention over configuration).
256
227
 
257
228
  ## Security
258
229
 
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ const BAD = "✗";
26
26
  const WARN = "•";
27
27
  // N-player games use the group matchmaking queue; Goofspiel (1v1) uses the 2-player
28
28
  // queue. Same enqueue request shape, different endpoint.
29
- const GROUP_GAMES = new Set(["mafia", "monopoly"]);
29
+ const GROUP_GAMES = new Set(["mafia"]);
30
30
  const queuePathFor = (game) => (GROUP_GAMES.has(game) ? "/v1/group-queue" : "/v1/queue");
31
31
  // Public platform defaults. `pyyol login` with no flags hits the live platform;
32
32
  // self-hosted/local users override via PYYOL_API / PYYOL_DASHBOARD (or --api /
@@ -45,12 +45,10 @@ const AGENT_KEY_PREFIX = "sk_arena_";
45
45
  const PLAY_PATH = {
46
46
  goofspiel: "/v1/sandbox/pushplay",
47
47
  mafia: "/v1/mafia/pushplay",
48
- monopoly: "/v1/monopoly/pushplay",
49
48
  };
50
49
  const REPLAY_PATH = {
51
50
  goofspiel: "/v1/match/{id}/replay",
52
51
  mafia: "/v1/mafia/{id}/replay",
53
- monopoly: "/v1/monopoly/{id}/replay",
54
52
  };
55
53
  /**
56
54
  * The `--watch` value: where to follow a match. "ask" (default) shows the pop-up when
@@ -751,13 +749,22 @@ async function cmdQueue(a) {
751
749
  console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
752
750
  return 0;
753
751
  }
754
- // Queuing needs a session auto-launch login on this device if absent.
755
- let token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
752
+ // Queuing is an AGENT action, so it needs the AGENT key.
753
+ //
754
+ // /v1/queue is registered with RequireScope(ScopeAgent). This sent the dashboard
755
+ // session token, so every ranked queue attempt came back `forbidden_scope` — for every
756
+ // developer, every time, on the command the scaffold prints as THE way to play ranked.
757
+ //
758
+ // It also read stored credentials BEFORE the explicit --token flag, so a caller passing
759
+ // a credential was ignored whenever anything happened to be logged in on the machine.
760
+ // connectionToken gets both right, and is what the play/dev commands already use to
761
+ // reach the same agent-scoped surface.
762
+ let { token } = connectionToken(a, c);
756
763
  if (!token) {
757
764
  const got = await ensureLogin(a);
758
765
  if (!got)
759
766
  return 2;
760
- token = got.accessToken || got.apiKey || "";
767
+ ({ token } = connectionToken(a, got));
761
768
  }
762
769
  const body = { game };
763
770
  if (str(a, "tier"))
@@ -807,13 +814,14 @@ async function cmdRoom(a) {
807
814
  console.error(` pyyol room join <room-id>`);
808
815
  return 2;
809
816
  }
810
- // A room is staked on both sides, so it needs a session exactly like `queue` does.
811
- let token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
817
+ // A room is an AGENT action exactly like `queue`: /v1/room/create and /v1/lobby/join
818
+ // are both agent-scoped, so the dashboard session token fails with `forbidden_scope`.
819
+ let { token } = connectionToken(a, c);
812
820
  if (!token) {
813
821
  const got = await ensureLogin(a);
814
822
  if (!got)
815
823
  return 2;
816
- token = got.accessToken || got.apiKey || "";
824
+ ({ token } = connectionToken(a, got));
817
825
  }
818
826
  if (action === "join") {
819
827
  const id = a.positionals[1] ?? "";
@@ -1466,13 +1474,6 @@ async function signedRequest(url, method, secret, payload, signPath) {
1466
1474
  }
1467
1475
  /** (view, legal, isLegalMove) for a probe turn — mirrors Python `_synthetic_turn`. */
1468
1476
  function syntheticTurn(game) {
1469
- if (game === "monopoly") {
1470
- const view = {
1471
- game: "monopoly", match_id: "validate", seat: 0, phase: "roll",
1472
- legal_actions: ["roll", "end_turn"], state: { players: [], phase: "roll" },
1473
- };
1474
- return [view, ["roll", "end_turn"], (m) => Boolean(m) && ["roll", "end_turn"].includes(m.action)];
1475
- }
1476
1477
  if (game === "mafia") {
1477
1478
  const view = {
1478
1479
  game: "mafia", match_id: "validate", your_seat: 1, your_role: "Villager", day: 1,
@@ -1762,13 +1763,23 @@ async function cmdServe(a) {
1762
1763
  });
1763
1764
  }
1764
1765
  const HELP = `pyyol — build, run, and rank autonomous AI agents.
1765
- Quickstart: pyyol login → pyyol init <dir> → pyyol dev
1766
+
1767
+ Start here:
1768
+ pyyol login
1769
+ pyyol init <dir> && cd <dir>
1770
+ pyyol dev # sandbox practice, no stakes
1771
+
1772
+ pyyol help # this list (also: pyyol /help, pyyol /)
1773
+ pyyol help play # flags for one command
1774
+
1775
+ The JS CLI has no interactive prompt — run commands directly.
1776
+ (The Python CLI's pyyol shell, with a / menu, is Python-only.)
1766
1777
 
1767
1778
  Commands:
1768
1779
  login [--with github|google|wallet] [--dashboard URL] [--token PAT]
1769
1780
  logout
1770
1781
  whoami
1771
- init <dir> [--arena goofspiel|mafia|monopoly] [--framework F] [--name N]
1782
+ init <dir> [--arena goofspiel|mafia] [--framework F] [--name N]
1772
1783
  dev [--matches N] local dev loop — SANDBOX, no stakes
1773
1784
  play <arena> [--ranked] [--tier] compete; --ranked = real stakes
1774
1785
  publish --manifest <file> certify your agent for ranked
@@ -1792,7 +1803,38 @@ Commands:
1792
1803
  doctor
1793
1804
  update
1794
1805
  `;
1806
+ function normalizeArgv(argv) {
1807
+ // Docs say "press `/`". People type `pyyol /help` and `pyyol /play …` from bash.
1808
+ // Without this those are "unknown command" — the help text advertising a syntax
1809
+ // it then refuses.
1810
+ if (!argv.length)
1811
+ return argv;
1812
+ const head = argv[0];
1813
+ if (!head.startsWith("/"))
1814
+ return argv;
1815
+ const rest = head.slice(1);
1816
+ return rest ? [rest, ...argv.slice(1)] : argv.slice(1);
1817
+ }
1818
+ function printHelp(topic) {
1819
+ if (!topic) {
1820
+ console.log(HELP);
1821
+ return 0;
1822
+ }
1823
+ const name = topic.replace(/^\//, "");
1824
+ const lines = HELP.split("\n").filter((line) => {
1825
+ const t = line.trim();
1826
+ return t === name || t.startsWith(name + " ") || t.startsWith(name + "\t");
1827
+ });
1828
+ if (!lines.length) {
1829
+ console.error(`${BAD} unknown command: ${topic}\n`);
1830
+ console.log(HELP);
1831
+ return 2;
1832
+ }
1833
+ console.log(lines.map((l) => l.trimEnd()).join("\n"));
1834
+ return 0;
1835
+ }
1795
1836
  export async function main(argv = process.argv.slice(2)) {
1837
+ argv = normalizeArgv(argv);
1796
1838
  const command = argv[0];
1797
1839
  const a = parse(argv.slice(1));
1798
1840
  // Anonymous, once-per-version, fire-and-forget adoption ping (opt out with
@@ -1857,8 +1899,9 @@ export async function main(argv = process.argv.slice(2)) {
1857
1899
  case "-h":
1858
1900
  case "--help":
1859
1901
  case "help":
1860
- console.log(HELP);
1861
- return 0;
1902
+ case "h":
1903
+ case "?":
1904
+ return printHelp(typeof a.positionals[0] === "string" ? a.positionals[0] : undefined);
1862
1905
  default:
1863
1906
  console.error(`${BAD} unknown command: ${command}\n`);
1864
1907
  console.log(HELP);
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const CONFIG_NAME = "pyyol.toml";
2
- export declare const KNOWN_ARENAS: readonly ["goofspiel", "mafia", "monopoly"];
2
+ export declare const KNOWN_ARENAS: readonly ["goofspiel", "mafia"];
3
3
  export declare const MODES: readonly ["sandbox", "ranked"];
4
4
  export interface Config {
5
5
  name: string;
package/dist/config.js CHANGED
@@ -15,7 +15,7 @@
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname, join, resolve } from "node:path";
17
17
  export const CONFIG_NAME = "pyyol.toml";
18
- export const KNOWN_ARENAS = ["goofspiel", "mafia", "monopoly"];
18
+ export const KNOWN_ARENAS = ["goofspiel", "mafia"];
19
19
  export const MODES = ["sandbox", "ranked"];
20
20
  const ORDER = [
21
21
  "name",
package/dist/index.d.ts CHANGED
@@ -27,6 +27,6 @@ export { instrument, uninstrument, recordResponse, extractUsage, patchPrototype
27
27
  export { route, enableGateway, disableGateway, gatewayBaseUrl, gatewayHeaders } from "./instrument.js";
28
28
  export type { ExtractedUsage } from "./instrument.js";
29
29
  export { estimateCost, rateFor, isKnown, canonical, cacheWriteRate, PRICING_VERSION, } from "./pricing.js";
30
- export { moveTool, moveToolChoice, moveToolName, moveFromResponse, boundMove, boundPlan, canonPlan, promptFor, PLAN_KEY, MAX_SPAN_ROUNDS, canonMove, canonGoofspiel, canonMafia, canonMonopoly, NO_TARGET, TOOL_GOOFSPIEL, TOOL_MAFIA, TOOL_MONOPOLY, GAME_GOOFSPIEL, GAME_MAFIA, GAME_MONOPOLY, } from "./movetools.js";
30
+ export { moveTool, moveToolChoice, moveToolName, moveFromResponse, boundMove, boundPlan, canonPlan, promptFor, PLAN_KEY, MAX_SPAN_ROUNDS, canonMove, canonGoofspiel, canonMafia, NO_TARGET, TOOL_GOOFSPIEL, TOOL_MAFIA, GAME_GOOFSPIEL, GAME_MAFIA, } from "./movetools.js";
31
31
  export type { Rate, CostArgs } from "./pricing.js";
32
32
  export { fingerprint as scaffoldFingerprint, fromRequest as scaffoldFromRequest, eligibleForPairing as scaffoldEligibleForPairing, SCAFFOLD_VERSION, } from "./scaffold.js";
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ export { moveTool, moveToolChoice, moveToolName, moveFromResponse, boundMove,
32
32
  // model made, not calls, so batching no longer costs an agent its verified share.
33
33
  boundPlan, canonPlan,
34
34
  // Renders a turn view as a prompt the move tools expect. Parity with Python's prompt_for.
35
- promptFor, PLAN_KEY, MAX_SPAN_ROUNDS, canonMove, canonGoofspiel, canonMafia, canonMonopoly, NO_TARGET, TOOL_GOOFSPIEL, TOOL_MAFIA, TOOL_MONOPOLY, GAME_GOOFSPIEL, GAME_MAFIA, GAME_MONOPOLY, } from "./movetools.js";
35
+ promptFor, PLAN_KEY, MAX_SPAN_ROUNDS, canonMove, canonGoofspiel, canonMafia, NO_TARGET, TOOL_GOOFSPIEL, TOOL_MAFIA, GAME_GOOFSPIEL, GAME_MAFIA, } from "./movetools.js";
36
36
  // Scaffold fingerprinting: the harness identity that makes a paired model comparison
37
37
  // possible (same scaffold, different model). Exported so a developer can print their own
38
38
  // fingerprint and confirm it is stable before relying on it.
package/dist/models.d.ts CHANGED
@@ -2,15 +2,14 @@
2
2
  * Typed models for the Pyyol push protocol.
3
3
  *
4
4
  * Lifecycle envelopes are fully typed. Turn views are typed for their common
5
- * fields; complex nested state (e.g. the Monopoly board) is left as an open
5
+ * fields; complex nested state is left as an open
6
6
  * object so the SDK stays thin and never drifts from the server's evolving state
7
7
  * shape. Nothing here contains game strategy — these are pure data shapes.
8
8
  */
9
9
  export declare const PROTOCOL_VERSION = "1.0";
10
10
  export declare const GOOFSPIEL = "goofspiel";
11
- export declare const MONOPOLY = "monopoly";
12
11
  export declare const MAFIA = "mafia";
13
- export declare const SUPPORTED_GAMES: readonly ["goofspiel", "monopoly", "mafia"];
12
+ export declare const SUPPORTED_GAMES: readonly ["goofspiel", "mafia"];
14
13
  export interface InitializeRequest {
15
14
  protocol: string;
16
15
  match_id: string;
@@ -71,24 +70,6 @@ export interface GoofspielView {
71
70
  warn_in_ms: number;
72
71
  raw: Record<string, unknown>;
73
72
  }
74
- export interface MonopolyView {
75
- game: "monopoly";
76
- match_id: string;
77
- seat: number;
78
- phase: string;
79
- legal_actions: string[];
80
- /** The raw board dict (players, holdings, phase, …) — inspect directly. */
81
- state: Record<string, unknown>;
82
- /** The engine's turn counter for this decision. The turn proof is bound to
83
- * (agent, match, ROUND), so a wrong number verifies against nothing and the decision
84
- * silently fails to earn Verified. The runtime reads it for you; it is typed here for
85
- * agents that call the gateway themselves. */
86
- round?: number;
87
- /** Proves a model call was made FOR THIS decision. Attach as X-Pyyol-Proof when calling
88
- * the gateway yourself; the SDK runtime does it automatically. */
89
- turn_proof?: string;
90
- raw: Record<string, unknown>;
91
- }
92
73
  export interface MafiaView {
93
74
  game: "mafia";
94
75
  match_id: string;
@@ -103,7 +84,7 @@ export interface MafiaView {
103
84
  private: Record<string, unknown>[];
104
85
  raw: Record<string, unknown>;
105
86
  }
106
- export type TurnView = GoofspielView | MonopolyView | MafiaView | Record<string, unknown>;
87
+ export type TurnView = GoofspielView | MafiaView | Record<string, unknown>;
107
88
  export interface GoofspielMove {
108
89
  round?: number;
109
90
  card: number;
@@ -129,45 +110,6 @@ export interface GoofspielMove {
129
110
  */
130
111
  rationale?: string;
131
112
  }
132
- /**
133
- * OPEN_TO_TABLE is the Monopoly trade target meaning "offer this to the whole table".
134
- *
135
- * -1, never 0: seat 0 is a real player, so a forgotten target is an offer to THEM, not to
136
- * everyone. Any seat that can satisfy an open offer may take it; they are asked in seat order
137
- * and the first yes wins, so a `reject_trade` from one seat only PASSES — the offer stays up
138
- * for the seats behind it (watch for `trade_declined` rather than `trade_rejected`).
139
- */
140
- export declare const OPEN_TO_TABLE = -1;
141
- /**
142
- * A proposed exchange. You give `give_*` and receive `want_*`.
143
- *
144
- * Houses and hotels cannot be traded (official rule) — sell them back to the bank first.
145
- */
146
- export interface MonopolyTrade {
147
- /** The seat you are offering to, or OPEN_TO_TABLE (-1) for the whole table. */
148
- target: number;
149
- give_props?: number[];
150
- give_cash?: number;
151
- /** Get-out-of-jail-free cards. */
152
- give_cards?: number;
153
- want_props?: number[];
154
- want_cash?: number;
155
- want_cards?: number;
156
- }
157
- export interface MonopolyMove {
158
- action: string;
159
- property?: number;
160
- amount?: number;
161
- /** REQUIRED to originate a `propose_trade` or `counter_trade`; ignored otherwise.
162
- * Without it the SDK could not express a Monopoly trade AT ALL — the negotiation half of
163
- * the game was unreachable from JavaScript and Python even though the engine had always
164
- * supported it. `accept_trade` / `reject_trade` need no payload: they answer the offer
165
- * already on the table. */
166
- trade?: MonopolyTrade;
167
- /** Published as table talk before the move lands, so the table watches you argue the deal
168
- * rather than a silent action appearing. Same one-call economics as Goofspiel's. */
169
- rationale?: string;
170
- }
171
113
  export interface MafiaMove {
172
114
  action: string;
173
115
  /** Seat to act on. Seat 0 is a real player, so for a night action
@@ -177,7 +119,7 @@ export interface MafiaMove {
177
119
  target?: number;
178
120
  tone?: string;
179
121
  /** Your PUBLIC in-game speech. Rides along with the action — one model call produces both
180
- * the decision and what the table hears. This is the house style; Goofspiel and Monopoly
122
+ * the decision and what the table hears. This is the house style; Goofspiel
181
123
  * do the same with `rationale`. */
182
124
  text?: string;
183
125
  /** PRIVATE reasoning, captured for observability only — deliberately NOT published. In
@@ -185,6 +127,6 @@ export interface MafiaMove {
185
127
  * plan to the town, so this never becomes table talk. Use `text` to speak. */
186
128
  rationale?: string;
187
129
  }
188
- export type Move = GoofspielMove | MonopolyMove | MafiaMove | Record<string, unknown>;
130
+ export type Move = GoofspielMove | MafiaMove | Record<string, unknown>;
189
131
  /** Parse a turn body into its typed view; unknown games return the raw object. */
190
132
  export declare function parseView(d: Record<string, any>): TurnView;
package/dist/models.js CHANGED
@@ -2,24 +2,14 @@
2
2
  * Typed models for the Pyyol push protocol.
3
3
  *
4
4
  * Lifecycle envelopes are fully typed. Turn views are typed for their common
5
- * fields; complex nested state (e.g. the Monopoly board) is left as an open
5
+ * fields; complex nested state is left as an open
6
6
  * object so the SDK stays thin and never drifts from the server's evolving state
7
7
  * shape. Nothing here contains game strategy — these are pure data shapes.
8
8
  */
9
9
  export const PROTOCOL_VERSION = "1.0";
10
10
  export const GOOFSPIEL = "goofspiel";
11
- export const MONOPOLY = "monopoly";
12
11
  export const MAFIA = "mafia";
13
- export const SUPPORTED_GAMES = [GOOFSPIEL, MONOPOLY, MAFIA];
14
- /**
15
- * OPEN_TO_TABLE is the Monopoly trade target meaning "offer this to the whole table".
16
- *
17
- * -1, never 0: seat 0 is a real player, so a forgotten target is an offer to THEM, not to
18
- * everyone. Any seat that can satisfy an open offer may take it; they are asked in seat order
19
- * and the first yes wins, so a `reject_trade` from one seat only PASSES — the offer stays up
20
- * for the seats behind it (watch for `trade_declined` rather than `trade_rejected`).
21
- */
22
- export const OPEN_TO_TABLE = -1;
12
+ export const SUPPORTED_GAMES = [GOOFSPIEL, MAFIA];
23
13
  const asNum = (v, d = 0) => (typeof v === "number" ? v : Number(v ?? d) || d);
24
14
  const asStr = (v, d = "") => (typeof v === "string" ? v : d);
25
15
  const asArr = (v) => (Array.isArray(v) ? v : []);
@@ -41,16 +31,6 @@ export function parseView(d) {
41
31
  warn_in_ms: Number(d.warn_in_ms ?? 0) || 0,
42
32
  raw: d,
43
33
  };
44
- case MONOPOLY:
45
- return {
46
- game: MONOPOLY,
47
- match_id: asStr(d.match_id),
48
- seat: asNum(d.seat),
49
- phase: asStr(d.phase),
50
- legal_actions: asArr(d.legal_actions),
51
- state: d.state ?? {},
52
- raw: d,
53
- };
54
34
  case MAFIA: {
55
35
  const aliveRaw = d.alive ?? {};
56
36
  const alive = {};
@@ -1,9 +1,7 @@
1
1
  export declare const TOOL_GOOFSPIEL = "play_card";
2
2
  export declare const TOOL_MAFIA = "mafia_action";
3
- export declare const TOOL_MONOPOLY = "monopoly_action";
4
3
  export declare const GAME_GOOFSPIEL = "goofspiel";
5
4
  export declare const GAME_MAFIA = "mafia";
6
- export declare const GAME_MONOPOLY = "monopoly";
7
5
  /**
8
6
  * NO_TARGET is the wire convention for "this action names no seat".
9
7
  *
@@ -73,14 +71,6 @@ export declare function canonGoofspiel(card: number): string;
73
71
  * substituted for doing nothing.
74
72
  */
75
73
  export declare function canonMafia(kind: string, target: number): string;
76
- /**
77
- * The bound form of a Monopoly action: verb, property, amount.
78
- *
79
- * All three are always rendered, including zeros. Omitting an absent field would let "mortgage
80
- * property 0 for 50" and "mortgage property 50 for 0" reduce to the same string, and two
81
- * different decisions sharing one canonical form is the one thing this mechanism cannot tolerate.
82
- */
83
- export declare function canonMonopoly(kind: string, property?: number, amount?: number): string;
84
74
  /**
85
75
  * Reduce move arguments to the canonical string a bound decision stores.
86
76
  *
package/dist/movetools.js CHANGED
@@ -41,10 +41,8 @@
41
41
  // its own name keeps passing.
42
42
  export const TOOL_GOOFSPIEL = "play_card";
43
43
  export const TOOL_MAFIA = "mafia_action";
44
- export const TOOL_MONOPOLY = "monopoly_action";
45
44
  export const GAME_GOOFSPIEL = "goofspiel";
46
45
  export const GAME_MAFIA = "mafia";
47
- export const GAME_MONOPOLY = "monopoly";
48
46
  /**
49
47
  * NO_TARGET is the wire convention for "this action names no seat".
50
48
  *
@@ -56,7 +54,6 @@ export const NO_TARGET = -1;
56
54
  const TOOL_BY_GAME = {
57
55
  [GAME_GOOFSPIEL]: TOOL_GOOFSPIEL,
58
56
  [GAME_MAFIA]: TOOL_MAFIA,
59
- [GAME_MONOPOLY]: TOOL_MONOPOLY,
60
57
  };
61
58
  // JSON Schema for each game's move arguments. Kept minimal on purpose: every field a model
62
59
  // must fill is a field it can fill wrongly, and a wrong field means an unbound turn.
@@ -83,49 +80,10 @@ const SCHEMAS = {
83
80
  },
84
81
  required: ["kind"],
85
82
  },
86
- [GAME_MONOPOLY]: {
87
- type: "object",
88
- properties: {
89
- kind: {
90
- type: "string",
91
- description: "The action verb, e.g. buy, pass, bid, mortgage, build.",
92
- },
93
- property: {
94
- type: "integer",
95
- description: "Board index of the property this action concerns, or 0. On a bid during a " +
96
- "HOUSING SHORTAGE auction this is the square you would put the piece on.",
97
- },
98
- amount: { type: "integer", description: "Coin amount this action carries, or 0." },
99
- // The trade payload. OPTIONAL and NOT part of the canonical bound form — a trade binds
100
- // on its verb alone (a nested structure re-rendered cosmetically differently would
101
- // reject an honest turn), so nothing here can cost a turn its binding. Without it a
102
- // bound agent could act but never DEAL, which is most of Monopoly.
103
- trade: {
104
- type: "object",
105
- description: "Required to propose or counter a trade. Ignored for other actions.",
106
- properties: {
107
- target: {
108
- type: "integer",
109
- description: "Seat to offer to, or -1 to offer to the WHOLE TABLE (any player who can " +
110
- "satisfy it may take it). Never 0 for 'everyone' — seat 0 is a real player.",
111
- },
112
- give_props: { type: "array", items: { type: "integer" }, description: "Squares you give." },
113
- give_cash: { type: "integer", description: "Cash you give." },
114
- give_cards: { type: "integer", description: "Get-out-of-jail-free cards you give." },
115
- want_props: { type: "array", items: { type: "integer" }, description: "Squares you want." },
116
- want_cash: { type: "integer", description: "Cash you want." },
117
- want_cards: { type: "integer", description: "Get-out-of-jail-free cards you want." },
118
- },
119
- required: ["target"],
120
- },
121
- },
122
- required: ["kind"],
123
- },
124
83
  };
125
84
  const DESCRIPTIONS = {
126
85
  [GAME_GOOFSPIEL]: "Play one card from your hand for this round. Call this to make your move.",
127
86
  [GAME_MAFIA]: "Take your action for this phase. Call this to make your move.",
128
- [GAME_MONOPOLY]: "Take your action for this turn. Call this to make your move.",
129
87
  };
130
88
  /** The tool name that carries a move for `game`, or "" if the game has no contract. */
131
89
  export function moveToolName(game) {
@@ -373,16 +331,6 @@ export function canonMafia(kind, target) {
373
331
  const t = Math.trunc(target) < 0 ? "none" : String(Math.trunc(target));
374
332
  return `${kind.trim().toLowerCase()}:${t}`;
375
333
  }
376
- /**
377
- * The bound form of a Monopoly action: verb, property, amount.
378
- *
379
- * All three are always rendered, including zeros. Omitting an absent field would let "mortgage
380
- * property 0 for 50" and "mortgage property 50 for 0" reduce to the same string, and two
381
- * different decisions sharing one canonical form is the one thing this mechanism cannot tolerate.
382
- */
383
- export function canonMonopoly(kind, property = 0, amount = 0) {
384
- return `${kind.trim().toLowerCase()}:${Math.trunc(property)}:${Math.trunc(amount)}`;
385
- }
386
334
  /**
387
335
  * Reduce move arguments to the canonical string a bound decision stores.
388
336
  *
@@ -403,14 +351,6 @@ export function canonMove(game, args) {
403
351
  const [target, has] = intArg(args, "target");
404
352
  return canonMafia(kind, has ? target : NO_TARGET);
405
353
  }
406
- if (game === GAME_MONOPOLY) {
407
- const kind = strArg(args, "kind").trim();
408
- if (!kind)
409
- return null;
410
- const [property] = intArg(args, "property");
411
- const [amount] = intArg(args, "amount");
412
- return canonMonopoly(kind, property, amount);
413
- }
414
354
  return null;
415
355
  }
416
356
  /**
package/dist/server.d.ts CHANGED
@@ -26,6 +26,8 @@ export interface AgentOptions {
26
26
  secret?: string;
27
27
  supportedGames?: string[];
28
28
  name?: string;
29
+ /** Public agent id echoed on /handshake so the platform can confirm identity. */
30
+ agentId?: string;
29
31
  skewSeconds?: number;
30
32
  /** Force verification on/off; defaults to on iff a secret is set. */
31
33
  verify?: boolean;
@@ -39,6 +41,7 @@ export declare class Agent {
39
41
  readonly secret: string;
40
42
  readonly supportedGames: string[];
41
43
  readonly name: string;
44
+ readonly agentId: string;
42
45
  private skew;
43
46
  private verify;
44
47
  private replay;
package/dist/server.js CHANGED
@@ -26,6 +26,7 @@ export class Agent {
26
26
  secret;
27
27
  supportedGames;
28
28
  name;
29
+ agentId;
29
30
  skew;
30
31
  verify;
31
32
  replay = new ReplayGuard();
@@ -38,6 +39,7 @@ export class Agent {
38
39
  this.secret = opts.secret ?? "";
39
40
  this.supportedGames = opts.supportedGames ?? [...SUPPORTED_GAMES];
40
41
  this.name = opts.name ?? "pyyol-agent";
42
+ this.agentId = opts.agentId ?? process.env.PYYOL_AGENT_ID ?? "";
41
43
  this.skew = opts.skewSeconds ?? 300;
42
44
  this.verify = opts.verify ?? Boolean(this.secret);
43
45
  }
@@ -76,7 +78,19 @@ export class Agent {
76
78
  }
77
79
  const data = loadJson(body);
78
80
  if (suffix === "handshake") {
79
- return { status: 200, body: { accepted: true, sdkVersion: SDK_VERSION, supportedGames: this.supportedGames } };
81
+ const body = {
82
+ accepted: true,
83
+ sdkVersion: SDK_VERSION,
84
+ supportedGames: this.supportedGames,
85
+ };
86
+ if (this.agentId) {
87
+ body.agent_id = this.agentId;
88
+ body.agentId = this.agentId;
89
+ }
90
+ const challenge = typeof data?.challenge === "string" ? data.challenge : "";
91
+ if (challenge)
92
+ body.challenge = challenge;
93
+ return { status: 200, body };
80
94
  }
81
95
  if (suffix === "initialize") {
82
96
  const ack = this.initHandler ? await this.initHandler(data) : undefined;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.12.0";
1
+ export declare const SDK_VERSION = "1.12.2";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // GENERATED by scripts/genversion.mjs — do not edit by hand.
2
2
  // Source of truth is the "version" field in package.json.
3
- export const SDK_VERSION = "1.12.0"; // x-release-please-version
3
+ export const SDK_VERSION = "1.12.2"; // x-release-please-version
package/dist/watch.js CHANGED
@@ -31,7 +31,6 @@ export const DEFAULT_DASHBOARD = (process.env.PYYOL_DASHBOARD || "").replace(/\/
31
31
  const WATCH_ROUTE = {
32
32
  goofspiel: "/goofspiel",
33
33
  mafia: "/arena/mafia",
34
- monopoly: "/monopoly",
35
34
  };
36
35
  /** Browser URL for a specific live match, or "" when it cannot be named exactly. */
37
36
  export function watchUrl(arena, matchId, dashboard = DEFAULT_DASHBOARD) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyyol",
3
- "version": "1.12.0",
3
+ "version": "1.12.2",
4
4
  "description": "Official JS/TS SDK for pyyol — run AI game-playing agents locally over a WebSocket (Beta)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,7 +40,6 @@
40
40
  "arena",
41
41
  "pyyol",
42
42
  "goofspiel",
43
- "monopoly",
44
43
  "mafia"
45
44
  ],
46
45
  "license": "MIT",