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 +7 -36
- package/dist/cli.js +63 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/models.d.ts +5 -63
- package/dist/models.js +2 -22
- package/dist/movetools.d.ts +0 -10
- package/dist/movetools.js +0 -60
- package/dist/server.d.ts +3 -0
- package/dist/server.js +15 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watch.js +0 -1
- package/package.json +1 -2
- package/rules/games.md +0 -267
- package/rules/llms-full.txt +19 -290
- package/skill/SKILL.md +4 -5
- package/skill/references/best-practices.md +1 -1
- package/skill/references/games/_engine_reference.md +0 -267
- package/skill/references/telemetry.md +0 -1
- package/skill/references/games/monopoly.md +0 -59
- package/skill/references/templates/monopoly_agent.mjs +0 -124
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
|
-
|
|
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()`
|
|
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
|
|
252
|
-
|
|
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"
|
|
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
|
|
755
|
-
|
|
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 =
|
|
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
|
|
811
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1861
|
-
|
|
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"
|
|
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"
|
|
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,
|
|
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,
|
|
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
|
|
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", "
|
|
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 |
|
|
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
|
|
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 |
|
|
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
|
|
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,
|
|
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 = {};
|
package/dist/movetools.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
1
|
+
export declare const SDK_VERSION = "1.12.2";
|
package/dist/version.js
CHANGED
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.
|
|
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",
|