pyyol 1.11.0 → 1.12.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 +121 -14
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watch.js +0 -1
- package/package.json +1 -1
- package/rules/games.md +0 -267
- package/rules/llms-full.txt +19 -290
- package/skill/references/games/_engine_reference.md +0 -267
- package/skill/references/games/monopoly.md +0 -59
- package/skill/references/templates/monopoly_agent.mjs +0 -124
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"))
|
|
@@ -784,6 +791,109 @@ async function cmdQueue(a) {
|
|
|
784
791
|
console.log(` ${OK} matched → ${resp.match_id}\n watch it: pyyol watch ${resp.match_id}`);
|
|
785
792
|
return 0;
|
|
786
793
|
}
|
|
794
|
+
/** `pyyol room create|join [id] [--tier low|mid|high | --bid N]` — a PRIVATE staked table.
|
|
795
|
+
*
|
|
796
|
+
* The queue supplies whoever is waiting. A room is for the other case: two developers who
|
|
797
|
+
* want THEIR two agents to play each other. One creates it, sends the id, the other joins.
|
|
798
|
+
*
|
|
799
|
+
* Deliberately the same match as everywhere else: same stake path, same escrow, same
|
|
800
|
+
* certification gate, same refusal to seat both sides on one account. The only thing a room
|
|
801
|
+
* changes is that it is not listed in the open lobby, so the seat cannot be taken by a
|
|
802
|
+
* stranger between the moment the id is shared and the moment it is used.
|
|
803
|
+
*/
|
|
804
|
+
async function cmdRoom(a) {
|
|
805
|
+
const c = creds.load();
|
|
806
|
+
const base = httpBase(a, c);
|
|
807
|
+
if (!base) {
|
|
808
|
+
console.error(`${BAD} no arena to talk to — run \`pyyol login\`, or pass --api.`);
|
|
809
|
+
return 2;
|
|
810
|
+
}
|
|
811
|
+
const action = a.positionals[0] ?? "";
|
|
812
|
+
if (action !== "create" && action !== "join") {
|
|
813
|
+
console.error(`${BAD} usage: pyyol room create [--tier low|mid|high | --bid N]`);
|
|
814
|
+
console.error(` pyyol room join <room-id>`);
|
|
815
|
+
return 2;
|
|
816
|
+
}
|
|
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);
|
|
820
|
+
if (!token) {
|
|
821
|
+
const got = await ensureLogin(a);
|
|
822
|
+
if (!got)
|
|
823
|
+
return 2;
|
|
824
|
+
({ token } = connectionToken(a, got));
|
|
825
|
+
}
|
|
826
|
+
if (action === "join") {
|
|
827
|
+
const id = a.positionals[1] ?? "";
|
|
828
|
+
if (!id) {
|
|
829
|
+
console.error(`${BAD} which room? \`pyyol room join <room-id>\``);
|
|
830
|
+
return 2;
|
|
831
|
+
}
|
|
832
|
+
const [st, resp] = await apiPost(`${base}/v1/lobby/join`, token, { match_id: id });
|
|
833
|
+
if (st !== 200)
|
|
834
|
+
return roomError(st, resp, "join");
|
|
835
|
+
console.log(`${OK} joined room ${id}`);
|
|
836
|
+
console.log(" keep your agent connected (`pyyol run`) — it plays automatically.");
|
|
837
|
+
console.log(` watch it: pyyol watch ${id}`);
|
|
838
|
+
return 0;
|
|
839
|
+
}
|
|
840
|
+
const body = {};
|
|
841
|
+
if (str(a, "tier"))
|
|
842
|
+
body.tier = str(a, "tier");
|
|
843
|
+
else if (num(a, "bid", 0) > 0)
|
|
844
|
+
body.bid = num(a, "bid", 0);
|
|
845
|
+
else {
|
|
846
|
+
console.error(`${BAD} a room is staked: pass --tier <low|mid|high> ` +
|
|
847
|
+
`(see \`pyyol queue goofspiel --list\`) or --bid <coins>.`);
|
|
848
|
+
return 2;
|
|
849
|
+
}
|
|
850
|
+
const [st, resp] = await apiPost(`${base}/v1/room/create`, token, body);
|
|
851
|
+
if (st !== 200 && st !== 201)
|
|
852
|
+
return roomError(st, resp, "create");
|
|
853
|
+
const roomId = String(resp.room_id ?? resp.match_id ?? "");
|
|
854
|
+
console.log(`${OK} room created`);
|
|
855
|
+
if (resp.bid)
|
|
856
|
+
console.log(` stake: ${resp.bid} coins each`);
|
|
857
|
+
// The id gets its own line with nothing around it, because the next thing anyone does is
|
|
858
|
+
// drag-select it to paste into a chat, and a line with prose on it selects badly.
|
|
859
|
+
console.log();
|
|
860
|
+
console.log(` ${roomId}`);
|
|
861
|
+
console.log();
|
|
862
|
+
console.log(" send that to the other player. they run:");
|
|
863
|
+
console.log(` pyyol room join ${roomId}`);
|
|
864
|
+
console.log(" keep your agent connected (`pyyol run`) — it plays as soon as they join.");
|
|
865
|
+
return 0;
|
|
866
|
+
}
|
|
867
|
+
/** Turn the arena's refusal codes into something a developer can act on.
|
|
868
|
+
*
|
|
869
|
+
* Every branch here is a real first-try failure. The raw JSON says what was refused and
|
|
870
|
+
* never what to do about it, which on a staked action is the difference between a retry and
|
|
871
|
+
* giving up.
|
|
872
|
+
*/
|
|
873
|
+
function roomError(st, resp, what) {
|
|
874
|
+
const code = String(resp.code ?? resp.error ?? "");
|
|
875
|
+
const msg = resp.message ?? "";
|
|
876
|
+
if (code.includes("same_owner")) {
|
|
877
|
+
console.error(`${BAD} that is your own room — a match needs two different accounts. ` +
|
|
878
|
+
`Send the id to the other player.`);
|
|
879
|
+
}
|
|
880
|
+
else if (code.includes("certified")) {
|
|
881
|
+
console.error(`${BAD} agent not certified — run \`pyyol publish\` to verify your endpoint first.`);
|
|
882
|
+
}
|
|
883
|
+
else if (code.includes("balance") || code.includes("insufficient")) {
|
|
884
|
+
console.error(`${BAD} not enough coins to stake this room.`);
|
|
885
|
+
}
|
|
886
|
+
else if (code.includes("not_found")) {
|
|
887
|
+
console.error(`${BAD} no such room — check the id, or it may have been cancelled.`);
|
|
888
|
+
}
|
|
889
|
+
else if (code.includes("not_waiting")) {
|
|
890
|
+
console.error(`${BAD} that room is no longer open (already started or cancelled).`);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
console.error(`${BAD} could not ${what} room (${st}): ${msg || JSON.stringify(resp)}`);
|
|
894
|
+
}
|
|
895
|
+
return 1;
|
|
896
|
+
}
|
|
787
897
|
async function cmdLeaderboard(a) {
|
|
788
898
|
const base = httpBase(a, creds.load());
|
|
789
899
|
if (!base) {
|
|
@@ -1364,13 +1474,6 @@ async function signedRequest(url, method, secret, payload, signPath) {
|
|
|
1364
1474
|
}
|
|
1365
1475
|
/** (view, legal, isLegalMove) for a probe turn — mirrors Python `_synthetic_turn`. */
|
|
1366
1476
|
function syntheticTurn(game) {
|
|
1367
|
-
if (game === "monopoly") {
|
|
1368
|
-
const view = {
|
|
1369
|
-
game: "monopoly", match_id: "validate", seat: 0, phase: "roll",
|
|
1370
|
-
legal_actions: ["roll", "end_turn"], state: { players: [], phase: "roll" },
|
|
1371
|
-
};
|
|
1372
|
-
return [view, ["roll", "end_turn"], (m) => Boolean(m) && ["roll", "end_turn"].includes(m.action)];
|
|
1373
|
-
}
|
|
1374
1477
|
if (game === "mafia") {
|
|
1375
1478
|
const view = {
|
|
1376
1479
|
game: "mafia", match_id: "validate", your_seat: 1, your_role: "Villager", day: 1,
|
|
@@ -1666,11 +1769,13 @@ Commands:
|
|
|
1666
1769
|
login [--with github|google|wallet] [--dashboard URL] [--token PAT]
|
|
1667
1770
|
logout
|
|
1668
1771
|
whoami
|
|
1669
|
-
init <dir> [--arena goofspiel|mafia
|
|
1772
|
+
init <dir> [--arena goofspiel|mafia] [--framework F] [--name N]
|
|
1670
1773
|
dev [--matches N] local dev loop — SANDBOX, no stakes
|
|
1671
1774
|
play <arena> [--ranked] [--tier] compete; --ranked = real stakes
|
|
1672
1775
|
publish --manifest <file> certify your agent for ranked
|
|
1673
1776
|
queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
|
|
1777
|
+
room create [--tier low|mid|high | --bid N] open a PRIVATE staked table
|
|
1778
|
+
room join <room-id> play a specific opponent by their room id
|
|
1674
1779
|
wallet [--json] your coin balance + per-agent wallets
|
|
1675
1780
|
replay <match_id> [--game] [--json]
|
|
1676
1781
|
profile [handle]
|
|
@@ -1721,6 +1826,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1721
1826
|
return cmdWallet(a);
|
|
1722
1827
|
case "queue":
|
|
1723
1828
|
return cmdQueue(a);
|
|
1829
|
+
case "room":
|
|
1830
|
+
return cmdRoom(a);
|
|
1724
1831
|
case "replay":
|
|
1725
1832
|
return cmdReplay(a);
|
|
1726
1833
|
case "status":
|
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/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.12.1";
|
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) {
|