@zktable/coup-lite 0.1.0
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/LICENSE +21 -0
- package/dist/chunk-U77RKQUV.js +56 -0
- package/dist/index.cjs +1236 -0
- package/dist/index.d.cts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +1113 -0
- package/dist/paths-RB454MTB.js +38 -0
- package/package.json +43 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/paths.ts
|
|
34
|
+
var paths_exports = {};
|
|
35
|
+
__export(paths_exports, {
|
|
36
|
+
CARD_MEMBERSHIP_CIRCUIT_DIR: () => CARD_MEMBERSHIP_CIRCUIT_DIR,
|
|
37
|
+
CARD_MEMBERSHIP_TARGET_DIR: () => CARD_MEMBERSHIP_TARGET_DIR,
|
|
38
|
+
CONTRACTS_DIR: () => CONTRACTS_DIR,
|
|
39
|
+
CONTRACTS_WASM_DIR: () => CONTRACTS_WASM_DIR,
|
|
40
|
+
DEFAULT_BB_BIN: () => DEFAULT_BB_BIN,
|
|
41
|
+
DEFAULT_BYTECODE_PATH: () => DEFAULT_BYTECODE_PATH,
|
|
42
|
+
DEFAULT_COUP_REFEREE_WASM: () => DEFAULT_COUP_REFEREE_WASM,
|
|
43
|
+
DEFAULT_NARGO_BIN: () => DEFAULT_NARGO_BIN,
|
|
44
|
+
DEFAULT_SHUFFLE_VK_PATH: () => DEFAULT_SHUFFLE_VK_PATH,
|
|
45
|
+
DEFAULT_STELLAR_BIN: () => DEFAULT_STELLAR_BIN,
|
|
46
|
+
DEFAULT_VERIFIER_WASM: () => DEFAULT_VERIFIER_WASM,
|
|
47
|
+
DEFAULT_VK_PATH: () => DEFAULT_VK_PATH,
|
|
48
|
+
GRAPH_TOOLS_BIN: () => GRAPH_TOOLS_BIN,
|
|
49
|
+
REPO_ROOT: () => REPO_ROOT,
|
|
50
|
+
VALID_SHUFFLE_CIRCUIT_DIR: () => VALID_SHUFFLE_CIRCUIT_DIR,
|
|
51
|
+
VALID_SHUFFLE_TARGET_DIR: () => VALID_SHUFFLE_TARGET_DIR,
|
|
52
|
+
toolEnv: () => toolEnv
|
|
53
|
+
});
|
|
54
|
+
function toolEnv() {
|
|
55
|
+
const home = import_node_os.default.homedir();
|
|
56
|
+
const extraDirs = [import_node_path.default.join(home, ".nargo", "bin"), import_node_path.default.join(home, ".bb", "bin")];
|
|
57
|
+
const currentPath = process.env.PATH ?? "";
|
|
58
|
+
return {
|
|
59
|
+
...process.env,
|
|
60
|
+
PATH: [...extraDirs, currentPath].filter(Boolean).join(import_node_path.default.delimiter)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
var import_node_os, import_node_path, import_node_url, import_meta, here, REPO_ROOT, CONTRACTS_WASM_DIR, DEFAULT_VERIFIER_WASM, DEFAULT_COUP_REFEREE_WASM, CONTRACTS_DIR, CARD_MEMBERSHIP_CIRCUIT_DIR, CARD_MEMBERSHIP_TARGET_DIR, DEFAULT_VK_PATH, DEFAULT_BYTECODE_PATH, VALID_SHUFFLE_CIRCUIT_DIR, VALID_SHUFFLE_TARGET_DIR, DEFAULT_SHUFFLE_VK_PATH, GRAPH_TOOLS_BIN, DEFAULT_STELLAR_BIN, DEFAULT_NARGO_BIN, DEFAULT_BB_BIN;
|
|
64
|
+
var init_paths = __esm({
|
|
65
|
+
"src/paths.ts"() {
|
|
66
|
+
"use strict";
|
|
67
|
+
import_node_os = __toESM(require("os"), 1);
|
|
68
|
+
import_node_path = __toESM(require("path"), 1);
|
|
69
|
+
import_node_url = require("url");
|
|
70
|
+
import_meta = {};
|
|
71
|
+
here = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
72
|
+
REPO_ROOT = import_node_path.default.resolve(here, "../../..");
|
|
73
|
+
CONTRACTS_WASM_DIR = import_node_path.default.join(
|
|
74
|
+
REPO_ROOT,
|
|
75
|
+
"packages/contracts/target/wasm32v1-none/release"
|
|
76
|
+
);
|
|
77
|
+
DEFAULT_VERIFIER_WASM = import_node_path.default.join(CONTRACTS_WASM_DIR, "zktable_verifier.wasm");
|
|
78
|
+
DEFAULT_COUP_REFEREE_WASM = import_node_path.default.join(CONTRACTS_WASM_DIR, "zktable_coup_referee.wasm");
|
|
79
|
+
CONTRACTS_DIR = import_node_path.default.join(REPO_ROOT, "packages/contracts");
|
|
80
|
+
CARD_MEMBERSHIP_CIRCUIT_DIR = import_node_path.default.join(REPO_ROOT, "packages/circuits/card_membership");
|
|
81
|
+
CARD_MEMBERSHIP_TARGET_DIR = import_node_path.default.join(CARD_MEMBERSHIP_CIRCUIT_DIR, "target");
|
|
82
|
+
DEFAULT_VK_PATH = import_node_path.default.join(CARD_MEMBERSHIP_TARGET_DIR, "vk");
|
|
83
|
+
DEFAULT_BYTECODE_PATH = import_node_path.default.join(CARD_MEMBERSHIP_TARGET_DIR, "card_membership.json");
|
|
84
|
+
VALID_SHUFFLE_CIRCUIT_DIR = import_node_path.default.join(REPO_ROOT, "packages/circuits/valid_shuffle");
|
|
85
|
+
VALID_SHUFFLE_TARGET_DIR = import_node_path.default.join(VALID_SHUFFLE_CIRCUIT_DIR, "target");
|
|
86
|
+
DEFAULT_SHUFFLE_VK_PATH = import_node_path.default.join(VALID_SHUFFLE_TARGET_DIR, "vk");
|
|
87
|
+
GRAPH_TOOLS_BIN = import_node_path.default.join(
|
|
88
|
+
REPO_ROOT,
|
|
89
|
+
"packages/contracts/tools/graph-tools/target/release/zktable-graph"
|
|
90
|
+
);
|
|
91
|
+
DEFAULT_STELLAR_BIN = "stellar";
|
|
92
|
+
DEFAULT_NARGO_BIN = "nargo";
|
|
93
|
+
DEFAULT_BB_BIN = "bb";
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// src/index.ts
|
|
98
|
+
var index_exports = {};
|
|
99
|
+
__export(index_exports, {
|
|
100
|
+
CHARACTERS: () => CHARACTERS,
|
|
101
|
+
CHARACTER_NAMES: () => CHARACTER_NAMES,
|
|
102
|
+
CardProver: () => CardProver,
|
|
103
|
+
CliRefereeClient: () => CliRefereeClient,
|
|
104
|
+
DEFAULT_COUP_REFEREE_WASM: () => DEFAULT_COUP_REFEREE_WASM,
|
|
105
|
+
DEFAULT_SHUFFLE_VK_PATH: () => DEFAULT_SHUFFLE_VK_PATH,
|
|
106
|
+
DEFAULT_VERIFIER_WASM: () => DEFAULT_VERIFIER_WASM,
|
|
107
|
+
DEFAULT_VK_PATH: () => DEFAULT_VK_PATH,
|
|
108
|
+
HAND_SIZE: () => HAND_SIZE,
|
|
109
|
+
MAX_PLAYERS: () => MAX_PLAYERS,
|
|
110
|
+
MIN_PLAYERS: () => MIN_PLAYERS,
|
|
111
|
+
N_PLAYERS: () => N_PLAYERS,
|
|
112
|
+
RefereeCliError: () => RefereeCliError,
|
|
113
|
+
START_INFLUENCE: () => START_INFLUENCE,
|
|
114
|
+
buildRoster: () => buildRoster,
|
|
115
|
+
chooseMove: () => chooseMove,
|
|
116
|
+
coupLite: () => coupLite,
|
|
117
|
+
createLocalMatch: () => createLocalMatch,
|
|
118
|
+
ensureContractWasms: () => ensureContractWasms,
|
|
119
|
+
pickSeeded: () => pickSeeded,
|
|
120
|
+
playCoupLite: () => playCoupLite,
|
|
121
|
+
playLocalMatch: () => playLocalMatch,
|
|
122
|
+
seededHand: () => seededHand,
|
|
123
|
+
stepLocalMatch: () => stepLocalMatch,
|
|
124
|
+
stripHexPrefix: () => stripHexPrefix,
|
|
125
|
+
toBe32Hex: () => toBe32Hex,
|
|
126
|
+
toolEnv: () => toolEnv
|
|
127
|
+
});
|
|
128
|
+
module.exports = __toCommonJS(index_exports);
|
|
129
|
+
|
|
130
|
+
// src/coup-lite.ts
|
|
131
|
+
var import_core = require("@zktable/core");
|
|
132
|
+
var CHARACTERS = [0, 1, 2, 3, 4];
|
|
133
|
+
var CHARACTER_NAMES = ["Duke", "Assassin", "Captain", "Contessa", "Ambassador"];
|
|
134
|
+
var HAND_SIZE = 2;
|
|
135
|
+
var START_INFLUENCE = 2;
|
|
136
|
+
var MIN_PLAYERS = 2;
|
|
137
|
+
var MAX_PLAYERS = 4;
|
|
138
|
+
var N_PLAYERS = 2;
|
|
139
|
+
function readConfig(raw) {
|
|
140
|
+
return raw.coupLite ?? {};
|
|
141
|
+
}
|
|
142
|
+
function hashSeed(seed) {
|
|
143
|
+
let h = 2166136261;
|
|
144
|
+
for (let i = 0; i < seed.length; i++) {
|
|
145
|
+
h ^= seed.charCodeAt(i);
|
|
146
|
+
h = Math.imul(h, 16777619);
|
|
147
|
+
}
|
|
148
|
+
return h >>> 0;
|
|
149
|
+
}
|
|
150
|
+
function mulberry32(seed) {
|
|
151
|
+
let a = seed;
|
|
152
|
+
return () => {
|
|
153
|
+
a = a + 1831565813 | 0;
|
|
154
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
155
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
156
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function seededHand(seed) {
|
|
160
|
+
const rnd = mulberry32(hashSeed(seed));
|
|
161
|
+
const pool = [...CHARACTERS];
|
|
162
|
+
const i0 = Math.floor(rnd() * pool.length);
|
|
163
|
+
const c0 = pool.splice(i0, 1)[0];
|
|
164
|
+
const i1 = Math.floor(rnd() * pool.length);
|
|
165
|
+
const c1 = pool.splice(i1, 1)[0];
|
|
166
|
+
return [c0, c1];
|
|
167
|
+
}
|
|
168
|
+
function handsByPlayer(state) {
|
|
169
|
+
return state.public.handsByPlayer ?? {};
|
|
170
|
+
}
|
|
171
|
+
function influenceByPlayer(state) {
|
|
172
|
+
return state.public.influenceByPlayer ?? {};
|
|
173
|
+
}
|
|
174
|
+
function deadByPlayer(state) {
|
|
175
|
+
return state.public.deadByPlayer ?? {};
|
|
176
|
+
}
|
|
177
|
+
function loseInfluence(state, player) {
|
|
178
|
+
const dead = deadByPlayer(state);
|
|
179
|
+
const influence = influenceByPlayer(state);
|
|
180
|
+
const playerDead = dead[player] ?? [false, false];
|
|
181
|
+
const slot = playerDead[0] ? 1 : 0;
|
|
182
|
+
const nextDead = slot === 0 ? [true, playerDead[1]] : [playerDead[0], true];
|
|
183
|
+
return {
|
|
184
|
+
influenceByPlayer: { ...influence, [player]: Math.max(0, (influence[player] ?? START_INFLUENCE) - 1) },
|
|
185
|
+
deadByPlayer: { ...dead, [player]: nextDead }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function alivePlayers(state) {
|
|
189
|
+
const influence = influenceByPlayer(state);
|
|
190
|
+
return state.players.map((p) => p.id).filter((id) => (influence[id] ?? START_INFLUENCE) > 0);
|
|
191
|
+
}
|
|
192
|
+
var coupLite = (0, import_core.defineGame)({
|
|
193
|
+
name: "coup-lite",
|
|
194
|
+
players: { min: MIN_PLAYERS, max: MAX_PLAYERS },
|
|
195
|
+
components: {
|
|
196
|
+
// Public shared-deck declaration (the character pool). The shuffle marker
|
|
197
|
+
// is REAL on-chain as of M8.3: a seed-forced `valid_shuffle` proof with
|
|
198
|
+
// position-assigned hands (see module doc); each hand stays hidden.
|
|
199
|
+
deck: import_core.zk.deck.of([...CHARACTER_NAMES]),
|
|
200
|
+
shuffle: import_core.zk.deck.shuffle(),
|
|
201
|
+
hand: import_core.zk.deck.deal(HAND_SIZE)
|
|
202
|
+
},
|
|
203
|
+
state: {
|
|
204
|
+
public: () => ({
|
|
205
|
+
// See the module doc: local-mirror-only visibility, not real hiding.
|
|
206
|
+
handsByPlayer: {},
|
|
207
|
+
influenceByPlayer: {},
|
|
208
|
+
deadByPlayer: {},
|
|
209
|
+
lastClaim: null,
|
|
210
|
+
claimHistory: [],
|
|
211
|
+
eliminatedIds: [],
|
|
212
|
+
winnerId: null
|
|
213
|
+
}),
|
|
214
|
+
// Real data (matches Blackout's `state.secret` / Liar's Dice's own
|
|
215
|
+
// `secret.dice` precedent — see the module doc): this player's actual
|
|
216
|
+
// hand, either the REAL `card_membership`-provable hand
|
|
217
|
+
// (`config.coupLite.handsByPlayer`, supplied by the on-chain runner) or a
|
|
218
|
+
// deterministic seeded fallback for chain-free local play.
|
|
219
|
+
secret: (ctx) => {
|
|
220
|
+
const cfg = readConfig(ctx.config);
|
|
221
|
+
const seed = ctx.config.seed ?? "coup-lite-default-seed";
|
|
222
|
+
const hand = cfg.handsByPlayer?.[ctx.id] ?? seededHand(`${seed}:${ctx.id}`);
|
|
223
|
+
return { hand };
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
setup: (state, ctx) => {
|
|
227
|
+
const cfg = readConfig(ctx.config);
|
|
228
|
+
const seed = ctx.config.seed ?? "coup-lite-default-seed";
|
|
229
|
+
const hands = {};
|
|
230
|
+
const influence = {};
|
|
231
|
+
const dead = {};
|
|
232
|
+
for (const p of ctx.players) {
|
|
233
|
+
hands[p.id] = cfg.handsByPlayer?.[p.id] ?? seededHand(`${seed}:${p.id}`);
|
|
234
|
+
influence[p.id] = START_INFLUENCE;
|
|
235
|
+
dead[p.id] = [false, false];
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
...state,
|
|
239
|
+
public: { ...state.public, handsByPlayer: hands, influenceByPlayer: influence, deadByPlayer: dead }
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
turn: {
|
|
243
|
+
order: "clockwise",
|
|
244
|
+
// A player at zero influence is out: the engine skips their turn and
|
|
245
|
+
// returns no legal moves for them (mirrors the referee's advance_turn).
|
|
246
|
+
eliminated: (state, playerId) => (influenceByPlayer(state)[playerId] ?? START_INFLUENCE) === 0,
|
|
247
|
+
moves: {
|
|
248
|
+
claim: {
|
|
249
|
+
// Public claim to hold `character` — no ZK binding by itself (the
|
|
250
|
+
// hidden fact is the hand's contents, proven only if challenged).
|
|
251
|
+
legal: (view) => CHARACTERS.map((character) => ({ type: "claim", character })),
|
|
252
|
+
apply: (state, move, ctx) => {
|
|
253
|
+
const entry = { player: ctx.playerId, character: move.character };
|
|
254
|
+
return {
|
|
255
|
+
...state,
|
|
256
|
+
public: {
|
|
257
|
+
...state.public,
|
|
258
|
+
lastClaim: entry,
|
|
259
|
+
claimHistory: [...state.public.claimHistory, entry]
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
challenge: {
|
|
265
|
+
// Binds this move to "the target proves in ZK that the claimed
|
|
266
|
+
// character is in their committed hand, or the challenge stands as a
|
|
267
|
+
// caught bluff" once ZK-secured — inert marker in the local engine
|
|
268
|
+
// (see the module doc), real on-chain via `prove_hold`/`reveal_card`
|
|
269
|
+
// in `runner.ts`.
|
|
270
|
+
zkp: import_core.zk.deck.proveHoldOrBluff(),
|
|
271
|
+
// See the module doc's turn-structure deviation note: only legal
|
|
272
|
+
// for the player whose turn falls immediately after a still-standing
|
|
273
|
+
// claim by someone else.
|
|
274
|
+
legal: (view) => {
|
|
275
|
+
const claim = view.public.lastClaim ?? null;
|
|
276
|
+
if (!claim || claim.player === view.self.id) return [];
|
|
277
|
+
return [{ type: "challenge" }];
|
|
278
|
+
},
|
|
279
|
+
apply: (state, _move, ctx) => {
|
|
280
|
+
const claim = state.public.lastClaim;
|
|
281
|
+
const hands = handsByPlayer(state);
|
|
282
|
+
const targetHand = hands[claim.player] ?? [0, 0];
|
|
283
|
+
const challenger = ctx.playerId;
|
|
284
|
+
const claimIsTrue = targetHand.includes(claim.character);
|
|
285
|
+
const loser = claimIsTrue ? challenger : claim.player;
|
|
286
|
+
const { influenceByPlayer: nextInfluence, deadByPlayer: nextDead } = loseInfluence(state, loser);
|
|
287
|
+
const priorEliminated = state.public.eliminatedIds ?? [];
|
|
288
|
+
const nextState = {
|
|
289
|
+
...state,
|
|
290
|
+
public: {
|
|
291
|
+
...state.public,
|
|
292
|
+
influenceByPlayer: nextInfluence,
|
|
293
|
+
deadByPlayer: nextDead,
|
|
294
|
+
lastClaim: null,
|
|
295
|
+
eliminatedIds: (nextInfluence[loser] ?? 0) === 0 ? [...priorEliminated, loser] : priorEliminated
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const survivors = alivePlayers(nextState);
|
|
299
|
+
const winnerId = survivors.length === 1 ? survivors[0] : null;
|
|
300
|
+
return { ...nextState, public: { ...nextState.public, winnerId } };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
end: (state) => {
|
|
306
|
+
const winnerId = state.public.winnerId;
|
|
307
|
+
return winnerId ? { winner: winnerId } : null;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// src/strategy.ts
|
|
312
|
+
function hashSeed2(seed) {
|
|
313
|
+
let h = 2166136261;
|
|
314
|
+
for (let i = 0; i < seed.length; i++) {
|
|
315
|
+
h ^= seed.charCodeAt(i);
|
|
316
|
+
h = Math.imul(h, 16777619);
|
|
317
|
+
}
|
|
318
|
+
return h >>> 0;
|
|
319
|
+
}
|
|
320
|
+
function mulberry322(seed) {
|
|
321
|
+
let a = seed;
|
|
322
|
+
return () => {
|
|
323
|
+
a = a + 1831565813 | 0;
|
|
324
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
325
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
326
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function pickSeeded(items, seed) {
|
|
330
|
+
if (items.length === 0) throw new Error("pickSeeded: items is empty");
|
|
331
|
+
const rnd = mulberry322(hashSeed2(seed));
|
|
332
|
+
const index = Math.min(items.length - 1, Math.floor(rnd() * items.length));
|
|
333
|
+
return items[index];
|
|
334
|
+
}
|
|
335
|
+
function ownHand(view) {
|
|
336
|
+
return view.self.secret.hand ?? [0, 0];
|
|
337
|
+
}
|
|
338
|
+
function chooseClaim(view, seed) {
|
|
339
|
+
const hand = ownHand(view);
|
|
340
|
+
const isBluff = pickSeeded([0, 1, 2, 3, 4], `${seed}:bluff`) === 0;
|
|
341
|
+
if (isBluff) {
|
|
342
|
+
const unheld = CHARACTERS.filter((c) => !hand.includes(c));
|
|
343
|
+
const character = unheld.length > 0 ? pickSeeded(unheld, `${seed}:bluffchar`) : pickSeeded([...CHARACTERS], `${seed}:fallback`);
|
|
344
|
+
return { type: "claim", character };
|
|
345
|
+
}
|
|
346
|
+
return { type: "claim", character: pickSeeded(hand, `${seed}:claimchar`) };
|
|
347
|
+
}
|
|
348
|
+
function chooseMove(view, seed) {
|
|
349
|
+
const canChallenge = view.legalMoves.some((m) => m.type === "challenge");
|
|
350
|
+
const claim = view.public.lastClaim ?? null;
|
|
351
|
+
if (canChallenge && claim) {
|
|
352
|
+
const hand = ownHand(view);
|
|
353
|
+
const holdsClaimed = hand.includes(claim.character);
|
|
354
|
+
const suspicionThreshold = holdsClaimed ? 0.15 : 0.55;
|
|
355
|
+
const roll = pickSeeded([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], `${seed}:challengeroll`) / 10;
|
|
356
|
+
if (roll < suspicionThreshold) {
|
|
357
|
+
return { type: "challenge" };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return chooseClaim(view, seed);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/card-prover.ts
|
|
364
|
+
var import_node_child_process = require("child_process");
|
|
365
|
+
var import_promises = require("fs/promises");
|
|
366
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
367
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
368
|
+
var import_node_util = require("util");
|
|
369
|
+
init_paths();
|
|
370
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
371
|
+
async function exists(p) {
|
|
372
|
+
try {
|
|
373
|
+
await (0, import_promises.access)(p);
|
|
374
|
+
return true;
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
var CardProver = class {
|
|
380
|
+
circuitDir;
|
|
381
|
+
shuffleCircuitDir;
|
|
382
|
+
graphToolsBin;
|
|
383
|
+
nargoBin;
|
|
384
|
+
bbBin;
|
|
385
|
+
workDir;
|
|
386
|
+
env;
|
|
387
|
+
constructor(opts = {}) {
|
|
388
|
+
this.circuitDir = opts.circuitDir ?? CARD_MEMBERSHIP_CIRCUIT_DIR;
|
|
389
|
+
this.shuffleCircuitDir = opts.shuffleCircuitDir ?? VALID_SHUFFLE_CIRCUIT_DIR;
|
|
390
|
+
this.graphToolsBin = opts.graphToolsBin ?? GRAPH_TOOLS_BIN;
|
|
391
|
+
this.nargoBin = opts.nargoBin ?? DEFAULT_NARGO_BIN;
|
|
392
|
+
this.bbBin = opts.bbBin ?? DEFAULT_BB_BIN;
|
|
393
|
+
this.workDir = opts.workDir ?? import_node_os2.default.tmpdir();
|
|
394
|
+
this.env = toolEnv();
|
|
395
|
+
}
|
|
396
|
+
get targetDir() {
|
|
397
|
+
return import_node_path2.default.join(this.circuitDir, "target");
|
|
398
|
+
}
|
|
399
|
+
get bytecodePath() {
|
|
400
|
+
return import_node_path2.default.join(this.targetDir, "card_membership.json");
|
|
401
|
+
}
|
|
402
|
+
get vkPath() {
|
|
403
|
+
return import_node_path2.default.join(this.targetDir, "vk");
|
|
404
|
+
}
|
|
405
|
+
get shuffleTargetDir() {
|
|
406
|
+
return import_node_path2.default.join(this.shuffleCircuitDir, "target");
|
|
407
|
+
}
|
|
408
|
+
get shuffleBytecodePath() {
|
|
409
|
+
return import_node_path2.default.join(this.shuffleTargetDir, "valid_shuffle.json");
|
|
410
|
+
}
|
|
411
|
+
get shuffleVkPath() {
|
|
412
|
+
return import_node_path2.default.join(this.shuffleTargetDir, "vk");
|
|
413
|
+
}
|
|
414
|
+
/** Compile the circuit once if `target/card_membership.json` is missing. */
|
|
415
|
+
async ensureCompiled() {
|
|
416
|
+
if (await exists(this.bytecodePath)) return;
|
|
417
|
+
await execFileAsync(this.nargoBin, ["compile"], { cwd: this.circuitDir, env: this.env });
|
|
418
|
+
}
|
|
419
|
+
/** Generate the verification key once if `target/vk` is missing. */
|
|
420
|
+
async ensureVk() {
|
|
421
|
+
await this.ensureCompiled();
|
|
422
|
+
if (await exists(this.vkPath)) return;
|
|
423
|
+
await this.writeVk(this.bytecodePath, this.targetDir);
|
|
424
|
+
}
|
|
425
|
+
/** Compile `valid_shuffle` once if its bytecode is missing. */
|
|
426
|
+
async ensureShuffleCompiled() {
|
|
427
|
+
if (await exists(this.shuffleBytecodePath)) return;
|
|
428
|
+
await execFileAsync(this.nargoBin, ["compile"], { cwd: this.shuffleCircuitDir, env: this.env });
|
|
429
|
+
}
|
|
430
|
+
/** Generate the `valid_shuffle` verification key once if missing. */
|
|
431
|
+
async ensureShuffleVk() {
|
|
432
|
+
await this.ensureShuffleCompiled();
|
|
433
|
+
if (await exists(this.shuffleVkPath)) return;
|
|
434
|
+
await this.writeVk(this.shuffleBytecodePath, this.shuffleTargetDir);
|
|
435
|
+
}
|
|
436
|
+
async writeVk(bytecodePath, outputDir) {
|
|
437
|
+
await execFileAsync(
|
|
438
|
+
this.bbBin,
|
|
439
|
+
[
|
|
440
|
+
"write_vk",
|
|
441
|
+
"--scheme",
|
|
442
|
+
"ultra_honk",
|
|
443
|
+
"--oracle_hash",
|
|
444
|
+
"keccak",
|
|
445
|
+
"--bytecode_path",
|
|
446
|
+
bytecodePath,
|
|
447
|
+
"--output_path",
|
|
448
|
+
outputDir,
|
|
449
|
+
"--output_format",
|
|
450
|
+
"bytes_and_fields"
|
|
451
|
+
],
|
|
452
|
+
{ env: this.env }
|
|
453
|
+
);
|
|
454
|
+
const vkPath = import_node_path2.default.join(outputDir, "vk");
|
|
455
|
+
const { stat, rename, rmdir } = await import("fs/promises");
|
|
456
|
+
const s = await stat(vkPath);
|
|
457
|
+
if (s.isDirectory()) {
|
|
458
|
+
await rename(import_node_path2.default.join(vkPath, "vk"), `${vkPath}.tmp`);
|
|
459
|
+
await rmdir(vkPath);
|
|
460
|
+
await rename(`${vkPath}.tmp`, vkPath);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Poseidon2(value, salt) via `zktable-graph commit` — used for the seed
|
|
465
|
+
* commit-reveal nonces (`hash2(nonce, 0)`), byte-identical to the referee.
|
|
466
|
+
*/
|
|
467
|
+
async commit(value, salt) {
|
|
468
|
+
const { stdout } = await execFileAsync(
|
|
469
|
+
this.graphToolsBin,
|
|
470
|
+
["commit", "--value", value.toString(), "--salt", salt.toString()],
|
|
471
|
+
{ env: this.env }
|
|
472
|
+
);
|
|
473
|
+
return stdout.trim();
|
|
474
|
+
}
|
|
475
|
+
/** The joint commit-reveal seed: left-fold of hash2 over the nonces (player-index order). */
|
|
476
|
+
async seed(nonces) {
|
|
477
|
+
const { stdout } = await execFileAsync(
|
|
478
|
+
this.graphToolsBin,
|
|
479
|
+
["seed", "--nonces", nonces.join(",")],
|
|
480
|
+
{ env: this.env }
|
|
481
|
+
);
|
|
482
|
+
return stdout.trim();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Full `valid_shuffle` pipeline (M8.3): derive the UNIQUE seed-forced
|
|
486
|
+
* permutation of the canonical 15-card deck via `zktable-graph
|
|
487
|
+
* shuffle-witness`, then `nargo execute` -> `bb prove`. The returned
|
|
488
|
+
* `cards`/`salts` are the dealer's private view (handed to each player
|
|
489
|
+
* off-chain); `leavesHex` + `proof` go on-chain via `submit_shuffle`.
|
|
490
|
+
*/
|
|
491
|
+
async proveShuffle(seedHex, salts) {
|
|
492
|
+
if (salts.length !== 15) throw new Error("proveShuffle: exactly 15 salts required");
|
|
493
|
+
await this.ensureShuffleCompiled();
|
|
494
|
+
const dir = await (0, import_promises.mkdtemp)(import_node_path2.default.join(this.workDir, "zktable-shuffle-prove-"));
|
|
495
|
+
try {
|
|
496
|
+
const proverTomlPath = import_node_path2.default.join(dir, "Prover");
|
|
497
|
+
const witnessJsonPath = import_node_path2.default.join(dir, "witness.json");
|
|
498
|
+
const witnessOutPath = import_node_path2.default.join(dir, "witness");
|
|
499
|
+
await execFileAsync(
|
|
500
|
+
this.graphToolsBin,
|
|
501
|
+
[
|
|
502
|
+
"shuffle-witness",
|
|
503
|
+
"--seed",
|
|
504
|
+
seedHex,
|
|
505
|
+
"--salts",
|
|
506
|
+
salts.join(","),
|
|
507
|
+
"--prover",
|
|
508
|
+
`${proverTomlPath}.toml`,
|
|
509
|
+
"--json",
|
|
510
|
+
witnessJsonPath
|
|
511
|
+
],
|
|
512
|
+
{ env: this.env }
|
|
513
|
+
);
|
|
514
|
+
const witness = JSON.parse(await (0, import_promises.readFile)(witnessJsonPath, "utf8"));
|
|
515
|
+
await execFileAsync(this.nargoBin, ["execute", "--prover-name", proverTomlPath, witnessOutPath], {
|
|
516
|
+
cwd: this.shuffleCircuitDir,
|
|
517
|
+
env: this.env
|
|
518
|
+
});
|
|
519
|
+
await execFileAsync(
|
|
520
|
+
this.bbBin,
|
|
521
|
+
[
|
|
522
|
+
"prove",
|
|
523
|
+
"--scheme",
|
|
524
|
+
"ultra_honk",
|
|
525
|
+
"--oracle_hash",
|
|
526
|
+
"keccak",
|
|
527
|
+
"--bytecode_path",
|
|
528
|
+
this.shuffleBytecodePath,
|
|
529
|
+
"--witness_path",
|
|
530
|
+
`${witnessOutPath}.gz`,
|
|
531
|
+
"--output_path",
|
|
532
|
+
dir,
|
|
533
|
+
"--output_format",
|
|
534
|
+
"bytes_and_fields"
|
|
535
|
+
],
|
|
536
|
+
{ env: this.env }
|
|
537
|
+
);
|
|
538
|
+
const proof = new Uint8Array(await (0, import_promises.readFile)(import_node_path2.default.join(dir, "proof")));
|
|
539
|
+
const publicInputs = new Uint8Array(await (0, import_promises.readFile)(import_node_path2.default.join(dir, "public_inputs")));
|
|
540
|
+
return {
|
|
541
|
+
proof,
|
|
542
|
+
publicInputs,
|
|
543
|
+
seedHex: witness.seed,
|
|
544
|
+
cards: witness.cards.map((c) => Number(c)),
|
|
545
|
+
salts: witness.salts,
|
|
546
|
+
leavesHex: witness.leaves
|
|
547
|
+
};
|
|
548
|
+
} finally {
|
|
549
|
+
await (0, import_promises.rm)(dir, { recursive: true, force: true });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Full pipeline for one "prove-hold" claim: `zktable-graph card-witness` ->
|
|
554
|
+
* `nargo execute` -> `bb prove`. Runs entirely inside an isolated temp dir;
|
|
555
|
+
* never touches `card_membership/Prover.toml` or `card_membership/target/`
|
|
556
|
+
* (besides the shared, idempotent compiled bytecode).
|
|
557
|
+
*/
|
|
558
|
+
async proveHold(claimed, cards, salts, heldIndex) {
|
|
559
|
+
await this.ensureCompiled();
|
|
560
|
+
const dir = await (0, import_promises.mkdtemp)(import_node_path2.default.join(this.workDir, "zktable-card-prove-"));
|
|
561
|
+
try {
|
|
562
|
+
const proverTomlPath = import_node_path2.default.join(dir, "Prover");
|
|
563
|
+
const witnessJsonPath = import_node_path2.default.join(dir, "witness.json");
|
|
564
|
+
const witnessOutPath = import_node_path2.default.join(dir, "witness");
|
|
565
|
+
await execFileAsync(
|
|
566
|
+
this.graphToolsBin,
|
|
567
|
+
[
|
|
568
|
+
"card-witness",
|
|
569
|
+
"--claimed",
|
|
570
|
+
claimed.toString(),
|
|
571
|
+
"--cards",
|
|
572
|
+
cards.join(","),
|
|
573
|
+
"--salts",
|
|
574
|
+
salts.join(","),
|
|
575
|
+
"--held",
|
|
576
|
+
String(heldIndex),
|
|
577
|
+
"--prover",
|
|
578
|
+
`${proverTomlPath}.toml`,
|
|
579
|
+
"--json",
|
|
580
|
+
witnessJsonPath
|
|
581
|
+
],
|
|
582
|
+
{ env: this.env }
|
|
583
|
+
);
|
|
584
|
+
const witness = JSON.parse(await (0, import_promises.readFile)(witnessJsonPath, "utf8"));
|
|
585
|
+
await execFileAsync(this.nargoBin, ["execute", "--prover-name", proverTomlPath, witnessOutPath], {
|
|
586
|
+
cwd: this.circuitDir,
|
|
587
|
+
env: this.env
|
|
588
|
+
});
|
|
589
|
+
await execFileAsync(
|
|
590
|
+
this.bbBin,
|
|
591
|
+
[
|
|
592
|
+
"prove",
|
|
593
|
+
"--scheme",
|
|
594
|
+
"ultra_honk",
|
|
595
|
+
"--oracle_hash",
|
|
596
|
+
"keccak",
|
|
597
|
+
"--bytecode_path",
|
|
598
|
+
this.bytecodePath,
|
|
599
|
+
"--witness_path",
|
|
600
|
+
`${witnessOutPath}.gz`,
|
|
601
|
+
"--output_path",
|
|
602
|
+
dir,
|
|
603
|
+
"--output_format",
|
|
604
|
+
"bytes_and_fields"
|
|
605
|
+
],
|
|
606
|
+
{ env: this.env }
|
|
607
|
+
);
|
|
608
|
+
const proof = new Uint8Array(await (0, import_promises.readFile)(import_node_path2.default.join(dir, "proof")));
|
|
609
|
+
const publicInputs = new Uint8Array(await (0, import_promises.readFile)(import_node_path2.default.join(dir, "public_inputs")));
|
|
610
|
+
const [c0, c1] = witness.commitments;
|
|
611
|
+
if (!c0 || !c1) throw new Error("proveHold: expected exactly 2 commitments");
|
|
612
|
+
return {
|
|
613
|
+
proof,
|
|
614
|
+
publicInputs,
|
|
615
|
+
claimed,
|
|
616
|
+
commitmentsHex: [c0, c1]
|
|
617
|
+
};
|
|
618
|
+
} finally {
|
|
619
|
+
await (0, import_promises.rm)(dir, { recursive: true, force: true });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/** Off-chain check via `bb verify`, useful for sanity-checking a proof before spending a testnet tx. */
|
|
623
|
+
async verifyLocally(proof, publicInputs) {
|
|
624
|
+
await this.ensureVk();
|
|
625
|
+
return this.bbVerify(proof, publicInputs, this.vkPath);
|
|
626
|
+
}
|
|
627
|
+
/** Off-chain `bb verify` of a `valid_shuffle` proof against ITS verification key. */
|
|
628
|
+
async shuffleLocalVerify(proof, publicInputs) {
|
|
629
|
+
await this.ensureShuffleVk();
|
|
630
|
+
return this.bbVerify(proof, publicInputs, this.shuffleVkPath);
|
|
631
|
+
}
|
|
632
|
+
async bbVerify(proof, publicInputs, vkPath) {
|
|
633
|
+
const dir = await (0, import_promises.mkdtemp)(import_node_path2.default.join(this.workDir, "zktable-card-verify-"));
|
|
634
|
+
try {
|
|
635
|
+
const { writeFile } = await import("fs/promises");
|
|
636
|
+
const proofPath = import_node_path2.default.join(dir, "proof");
|
|
637
|
+
const publicInputsPath = import_node_path2.default.join(dir, "public_inputs");
|
|
638
|
+
await writeFile(proofPath, proof);
|
|
639
|
+
await writeFile(publicInputsPath, publicInputs);
|
|
640
|
+
try {
|
|
641
|
+
await execFileAsync(
|
|
642
|
+
this.bbBin,
|
|
643
|
+
[
|
|
644
|
+
"verify",
|
|
645
|
+
"--scheme",
|
|
646
|
+
"ultra_honk",
|
|
647
|
+
"--oracle_hash",
|
|
648
|
+
"keccak",
|
|
649
|
+
"--proof_path",
|
|
650
|
+
proofPath,
|
|
651
|
+
"--vk_path",
|
|
652
|
+
vkPath,
|
|
653
|
+
"--public_inputs_path",
|
|
654
|
+
publicInputsPath
|
|
655
|
+
],
|
|
656
|
+
{ env: this.env }
|
|
657
|
+
);
|
|
658
|
+
return true;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
} finally {
|
|
663
|
+
await (0, import_promises.rm)(dir, { recursive: true, force: true });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// src/referee-client.ts
|
|
669
|
+
var import_node_child_process2 = require("child_process");
|
|
670
|
+
var import_node_util2 = require("util");
|
|
671
|
+
init_paths();
|
|
672
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
673
|
+
var REFEREE_ERROR_NAMES = {
|
|
674
|
+
1: "AlreadyInitialized",
|
|
675
|
+
2: "UnsupportedConfig",
|
|
676
|
+
3: "BadPlayerIndex",
|
|
677
|
+
4: "WrongPhase",
|
|
678
|
+
5: "AlreadyDealt",
|
|
679
|
+
6: "BadArrayLength",
|
|
680
|
+
7: "NotYourTurn",
|
|
681
|
+
8: "NotAlive",
|
|
682
|
+
9: "NoActiveClaim",
|
|
683
|
+
10: "ClaimTargetMismatch",
|
|
684
|
+
11: "CannotChallengeSelf",
|
|
685
|
+
12: "ClaimMismatch",
|
|
686
|
+
13: "VerificationFailed",
|
|
687
|
+
14: "ProofSizeMismatch",
|
|
688
|
+
15: "NotAuthorizedToReveal",
|
|
689
|
+
16: "SlotAlreadyRevealed",
|
|
690
|
+
17: "BadSlotIndex",
|
|
691
|
+
18: "CardRevealMismatch",
|
|
692
|
+
19: "AlreadyCommitted",
|
|
693
|
+
20: "NonceRevealMismatch",
|
|
694
|
+
21: "AlreadyNonceRevealed",
|
|
695
|
+
22: "NotFullyCommitted"
|
|
696
|
+
};
|
|
697
|
+
var RefereeCliError = class extends Error {
|
|
698
|
+
args;
|
|
699
|
+
stderr;
|
|
700
|
+
contractErrorCode;
|
|
701
|
+
contractErrorName;
|
|
702
|
+
constructor(args, stderr) {
|
|
703
|
+
const match = /Error\(Contract, #(\d+)\)/.exec(stderr);
|
|
704
|
+
const code = match ? Number(match[1]) : null;
|
|
705
|
+
const name = code !== null ? REFEREE_ERROR_NAMES[code] ?? null : null;
|
|
706
|
+
const suffix = code !== null ? ` -> Error(Contract, #${code})${name ? ` (${name})` : ""}` : "";
|
|
707
|
+
super(`stellar ${args.join(" ")} failed${suffix}
|
|
708
|
+
${stderr.trim()}`);
|
|
709
|
+
this.name = "RefereeCliError";
|
|
710
|
+
this.args = args;
|
|
711
|
+
this.stderr = stderr;
|
|
712
|
+
this.contractErrorCode = code;
|
|
713
|
+
this.contractErrorName = name;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
function stripHexPrefix(hex) {
|
|
717
|
+
return hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
718
|
+
}
|
|
719
|
+
function toBe32Hex(value) {
|
|
720
|
+
if (value < 0n) throw new Error("toBe32Hex: value must be non-negative");
|
|
721
|
+
const hex = value.toString(16);
|
|
722
|
+
if (hex.length > 64) throw new Error("toBe32Hex: value does not fit in 32 bytes");
|
|
723
|
+
return hex.padStart(64, "0");
|
|
724
|
+
}
|
|
725
|
+
function extractTxHash(stderr) {
|
|
726
|
+
const m = /explorer\/testnet\/tx\/([0-9a-f]{64})/i.exec(stderr);
|
|
727
|
+
return m ? m[1] : null;
|
|
728
|
+
}
|
|
729
|
+
var CliRefereeClient = class {
|
|
730
|
+
network;
|
|
731
|
+
source;
|
|
732
|
+
stellarBin;
|
|
733
|
+
retries;
|
|
734
|
+
retryDelayMs;
|
|
735
|
+
sourceForSeat;
|
|
736
|
+
constructor(opts = {}) {
|
|
737
|
+
this.network = opts.network ?? "testnet";
|
|
738
|
+
this.source = opts.source ?? "alice";
|
|
739
|
+
this.stellarBin = opts.stellarBin ?? DEFAULT_STELLAR_BIN;
|
|
740
|
+
this.retries = opts.retries ?? 3;
|
|
741
|
+
this.retryDelayMs = opts.retryDelayMs ?? 3e3;
|
|
742
|
+
this.sourceForSeat = opts.sourceForSeat ?? {};
|
|
743
|
+
}
|
|
744
|
+
/** `stellar contract deploy` for the `card_membership` verifier. Returns the deployed contract id. */
|
|
745
|
+
async deployVerifier(wasmPath, vkHex) {
|
|
746
|
+
const stdout = await this.runDeploy(wasmPath, ["--vk_bytes", stripHexPrefix(vkHex)]);
|
|
747
|
+
return stdout.trim();
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* `stellar contract deploy` for the coup referee. Returns the deployed
|
|
751
|
+
* contract id. `playerAddresses[i]` becomes seat i's owner — every seat-i
|
|
752
|
+
* action must then be signed by that address (require_auth). `verifier`
|
|
753
|
+
* holds the `card_membership` VK; `shuffleVerifier` the `valid_shuffle`
|
|
754
|
+
* VK (M8.3).
|
|
755
|
+
*/
|
|
756
|
+
async deployReferee(wasmPath, opts) {
|
|
757
|
+
const stdout = await this.runDeploy(wasmPath, [
|
|
758
|
+
"--verifier",
|
|
759
|
+
opts.verifier,
|
|
760
|
+
"--shuffle_verifier",
|
|
761
|
+
opts.shuffleVerifier,
|
|
762
|
+
"--players",
|
|
763
|
+
JSON.stringify(opts.playerAddresses)
|
|
764
|
+
]);
|
|
765
|
+
return stdout.trim();
|
|
766
|
+
}
|
|
767
|
+
async commitSeedNonce(refereeId, opts) {
|
|
768
|
+
const res = await this.invoke(
|
|
769
|
+
refereeId,
|
|
770
|
+
["commit_seed_nonce", "--player", String(opts.player), "--nonce_commitment", stripHexPrefix(opts.commitmentHex)],
|
|
771
|
+
opts.player
|
|
772
|
+
);
|
|
773
|
+
return res.txHash;
|
|
774
|
+
}
|
|
775
|
+
async revealSeedNonce(refereeId, opts) {
|
|
776
|
+
const res = await this.invoke(
|
|
777
|
+
refereeId,
|
|
778
|
+
["reveal_seed_nonce", "--player", String(opts.player), "--nonce", stripHexPrefix(opts.nonceHex)],
|
|
779
|
+
opts.player
|
|
780
|
+
);
|
|
781
|
+
return res.txHash;
|
|
782
|
+
}
|
|
783
|
+
/** Submits the 15 shuffle-proven deck leaves + the `valid_shuffle` proof (permissionless — the proof is the trust anchor). */
|
|
784
|
+
async submitShuffle(refereeId, opts) {
|
|
785
|
+
const res = await this.invoke(refereeId, [
|
|
786
|
+
"submit_shuffle",
|
|
787
|
+
"--leaves",
|
|
788
|
+
JSON.stringify(opts.leavesHex.map(stripHexPrefix)),
|
|
789
|
+
"--proof",
|
|
790
|
+
stripHexPrefix(opts.proofHex)
|
|
791
|
+
]);
|
|
792
|
+
return res.txHash;
|
|
793
|
+
}
|
|
794
|
+
async claim(refereeId, opts) {
|
|
795
|
+
const res = await this.invoke(
|
|
796
|
+
refereeId,
|
|
797
|
+
["claim", "--player", String(opts.player), "--character", String(opts.character)],
|
|
798
|
+
opts.player
|
|
799
|
+
);
|
|
800
|
+
return res.txHash;
|
|
801
|
+
}
|
|
802
|
+
async challenge(refereeId, opts) {
|
|
803
|
+
const res = await this.invoke(
|
|
804
|
+
refereeId,
|
|
805
|
+
["challenge", "--challenger", String(opts.challenger), "--target", String(opts.target)],
|
|
806
|
+
opts.challenger
|
|
807
|
+
);
|
|
808
|
+
return res.txHash;
|
|
809
|
+
}
|
|
810
|
+
async proveHold(refereeId, opts) {
|
|
811
|
+
const res = await this.invoke(
|
|
812
|
+
refereeId,
|
|
813
|
+
[
|
|
814
|
+
"prove_hold",
|
|
815
|
+
"--target",
|
|
816
|
+
String(opts.target),
|
|
817
|
+
"--claimed",
|
|
818
|
+
String(opts.claimed),
|
|
819
|
+
"--proof",
|
|
820
|
+
stripHexPrefix(opts.proofHex)
|
|
821
|
+
],
|
|
822
|
+
opts.target
|
|
823
|
+
);
|
|
824
|
+
return res.txHash;
|
|
825
|
+
}
|
|
826
|
+
async revealCard(refereeId, opts) {
|
|
827
|
+
const res = await this.invoke(
|
|
828
|
+
refereeId,
|
|
829
|
+
[
|
|
830
|
+
"reveal_card",
|
|
831
|
+
"--player",
|
|
832
|
+
String(opts.player),
|
|
833
|
+
"--slot",
|
|
834
|
+
String(opts.slot),
|
|
835
|
+
"--card",
|
|
836
|
+
String(opts.card),
|
|
837
|
+
"--salt",
|
|
838
|
+
stripHexPrefix(opts.saltHex)
|
|
839
|
+
],
|
|
840
|
+
opts.player
|
|
841
|
+
);
|
|
842
|
+
return res.txHash;
|
|
843
|
+
}
|
|
844
|
+
async gameState(refereeId) {
|
|
845
|
+
const stdout = await this.runReadOnly(refereeId, ["game_state"]);
|
|
846
|
+
return JSON.parse(stdout.trim());
|
|
847
|
+
}
|
|
848
|
+
// --- internals -----------------------------------------------------------
|
|
849
|
+
async runDeploy(wasmPath, ctorArgs) {
|
|
850
|
+
const args = [
|
|
851
|
+
"contract",
|
|
852
|
+
"deploy",
|
|
853
|
+
"--wasm",
|
|
854
|
+
wasmPath,
|
|
855
|
+
"--source",
|
|
856
|
+
this.source,
|
|
857
|
+
"--network",
|
|
858
|
+
this.network,
|
|
859
|
+
"--",
|
|
860
|
+
...ctorArgs
|
|
861
|
+
];
|
|
862
|
+
return (await this.execWithRetry(args)).stdout;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Mutating call (`--send=yes`). When `seat` is given, signs with that
|
|
866
|
+
* seat's identity (`sourceForSeat[seat]`, falling back to `source`) so
|
|
867
|
+
* the referee's per-seat `require_auth()` is satisfied.
|
|
868
|
+
*/
|
|
869
|
+
async invoke(contractId, methodArgs, seat) {
|
|
870
|
+
const source = seat !== void 0 ? this.sourceForSeat[seat] ?? this.source : this.source;
|
|
871
|
+
const args = [
|
|
872
|
+
"contract",
|
|
873
|
+
"invoke",
|
|
874
|
+
"--id",
|
|
875
|
+
contractId,
|
|
876
|
+
"--source",
|
|
877
|
+
source,
|
|
878
|
+
"--network",
|
|
879
|
+
this.network,
|
|
880
|
+
"--send=yes",
|
|
881
|
+
"--",
|
|
882
|
+
...methodArgs
|
|
883
|
+
];
|
|
884
|
+
return this.execWithRetry(args);
|
|
885
|
+
}
|
|
886
|
+
/** Read-only call (no `--send`). */
|
|
887
|
+
async runReadOnly(contractId, methodArgs) {
|
|
888
|
+
const args = [
|
|
889
|
+
"contract",
|
|
890
|
+
"invoke",
|
|
891
|
+
"--id",
|
|
892
|
+
contractId,
|
|
893
|
+
"--source",
|
|
894
|
+
this.source,
|
|
895
|
+
"--network",
|
|
896
|
+
this.network,
|
|
897
|
+
"--",
|
|
898
|
+
...methodArgs
|
|
899
|
+
];
|
|
900
|
+
return (await this.execWithRetry(args)).stdout;
|
|
901
|
+
}
|
|
902
|
+
async execWithRetry(args) {
|
|
903
|
+
let lastError;
|
|
904
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
905
|
+
try {
|
|
906
|
+
const { stdout, stderr } = await execFileAsync2(this.stellarBin, args, {
|
|
907
|
+
env: toolEnv(),
|
|
908
|
+
maxBuffer: 32 * 1024 * 1024
|
|
909
|
+
});
|
|
910
|
+
return { stdout, txHash: extractTxHash(stderr ?? "") };
|
|
911
|
+
} catch (err) {
|
|
912
|
+
const stderr = typeof err === "object" && err !== null && "stderr" in err ? String(err.stderr) : String(err);
|
|
913
|
+
const cliError = new RefereeCliError(args, stderr);
|
|
914
|
+
lastError = cliError;
|
|
915
|
+
if (cliError.contractErrorCode !== null || attempt === this.retries) {
|
|
916
|
+
throw cliError;
|
|
917
|
+
}
|
|
918
|
+
await sleep(this.retryDelayMs * (attempt + 1));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
throw lastError;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
function sleep(ms) {
|
|
925
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/runner.ts
|
|
929
|
+
var import_node_crypto = require("crypto");
|
|
930
|
+
var import_promises2 = require("fs/promises");
|
|
931
|
+
var import_core2 = require("@zktable/core");
|
|
932
|
+
init_paths();
|
|
933
|
+
|
|
934
|
+
// src/identities.ts
|
|
935
|
+
var import_node_child_process3 = require("child_process");
|
|
936
|
+
var import_node_util3 = require("util");
|
|
937
|
+
init_paths();
|
|
938
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
939
|
+
function seatIdentityNames(source, nSeats, multiSeat) {
|
|
940
|
+
if (!multiSeat) return Array.from({ length: nSeats }, () => source);
|
|
941
|
+
return Array.from({ length: nSeats }, (_, i) => i === 0 ? source : `${source}-seat${i}`);
|
|
942
|
+
}
|
|
943
|
+
async function ensureIdentities(names, stellarBin = DEFAULT_STELLAR_BIN) {
|
|
944
|
+
const out = {};
|
|
945
|
+
for (const name of [...new Set(names)]) {
|
|
946
|
+
try {
|
|
947
|
+
await execFileAsync3(stellarBin, ["keys", "address", name], { env: toolEnv() });
|
|
948
|
+
} catch {
|
|
949
|
+
await execFileAsync3(stellarBin, ["keys", "generate", name, "--network", "testnet", "--fund"], {
|
|
950
|
+
env: toolEnv()
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
const { stdout } = await execFileAsync3(stellarBin, ["keys", "address", name], { env: toolEnv() });
|
|
954
|
+
out[name] = stdout.trim();
|
|
955
|
+
}
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// src/runner.ts
|
|
960
|
+
function buildRoster(count = N_PLAYERS) {
|
|
961
|
+
const roster = [];
|
|
962
|
+
for (let i = 0; i < count; i++) roster.push({ id: `player${i + 1}` });
|
|
963
|
+
return roster;
|
|
964
|
+
}
|
|
965
|
+
function createLocalMatch(roster, config, seed) {
|
|
966
|
+
return (0, import_core2.createMatch)(coupLite, roster, { seed, config: { coupLite: config } });
|
|
967
|
+
}
|
|
968
|
+
function stepLocalMatch(match, seed) {
|
|
969
|
+
const playerId = match.state.turn.current;
|
|
970
|
+
const view = match.view(playerId);
|
|
971
|
+
const moveSeed = `${seed}:move${match.state.public.claimHistory.length}:${playerId}`;
|
|
972
|
+
const move = chooseMove(view, moveSeed);
|
|
973
|
+
match.submit(playerId, move);
|
|
974
|
+
if (move.type === "claim") {
|
|
975
|
+
return { playerId, move: { type: "claim", character: move.character } };
|
|
976
|
+
}
|
|
977
|
+
return { playerId, move: { type: "challenge" } };
|
|
978
|
+
}
|
|
979
|
+
function playLocalMatch(roster, config, seed, opts = {}) {
|
|
980
|
+
const match = createLocalMatch(roster, config, seed);
|
|
981
|
+
const steps = [];
|
|
982
|
+
const maxSteps = opts.maxSteps ?? 200;
|
|
983
|
+
while (match.state.status === "active" && steps.length < maxSteps) {
|
|
984
|
+
steps.push(stepLocalMatch(match, seed));
|
|
985
|
+
}
|
|
986
|
+
if (match.state.status === "active") {
|
|
987
|
+
throw new Error(`playLocalMatch: did not reach a natural end within ${maxSteps} steps`);
|
|
988
|
+
}
|
|
989
|
+
return { match, steps };
|
|
990
|
+
}
|
|
991
|
+
async function ensureContractWasms(log) {
|
|
992
|
+
const { access: access2 } = await import("fs/promises");
|
|
993
|
+
const missing = [];
|
|
994
|
+
for (const p of [DEFAULT_VERIFIER_WASM, DEFAULT_COUP_REFEREE_WASM]) {
|
|
995
|
+
try {
|
|
996
|
+
await access2(p);
|
|
997
|
+
} catch {
|
|
998
|
+
missing.push(p);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (missing.length === 0) return;
|
|
1002
|
+
log(`building contract wasms (missing: ${missing.join(", ")})\u2026`);
|
|
1003
|
+
const { execFile: execFile4 } = await import("child_process");
|
|
1004
|
+
const { promisify: promisify4 } = await import("util");
|
|
1005
|
+
const execFileAsync4 = promisify4(execFile4);
|
|
1006
|
+
const { CONTRACTS_DIR: CONTRACTS_DIR2, toolEnv: toolEnv2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
1007
|
+
await execFileAsync4(
|
|
1008
|
+
"cargo",
|
|
1009
|
+
[
|
|
1010
|
+
"+stable",
|
|
1011
|
+
"build",
|
|
1012
|
+
"--release",
|
|
1013
|
+
"--target",
|
|
1014
|
+
"wasm32v1-none",
|
|
1015
|
+
"-p",
|
|
1016
|
+
"zktable-verifier",
|
|
1017
|
+
"-p",
|
|
1018
|
+
"zktable-coup-referee"
|
|
1019
|
+
],
|
|
1020
|
+
{ cwd: CONTRACTS_DIR2, env: toolEnv2() }
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
function randomField() {
|
|
1024
|
+
return BigInt("0x" + (0, import_node_crypto.randomBytes)(16).toString("hex")) + 1n;
|
|
1025
|
+
}
|
|
1026
|
+
function nextDeadSlot(dead) {
|
|
1027
|
+
return dead[0] ? 1 : 0;
|
|
1028
|
+
}
|
|
1029
|
+
async function playCoupLite(opts = {}) {
|
|
1030
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
1031
|
+
const seed = opts.seed ?? `coup-lite-testnet-${Date.now()}`;
|
|
1032
|
+
const nPlayers = opts.players ?? N_PLAYERS;
|
|
1033
|
+
if (nPlayers < MIN_PLAYERS || nPlayers > MAX_PLAYERS) {
|
|
1034
|
+
throw new Error(`playCoupLite: players must be in [${MIN_PLAYERS}, ${MAX_PLAYERS}], got ${nPlayers}`);
|
|
1035
|
+
}
|
|
1036
|
+
const roster = buildRoster(nPlayers);
|
|
1037
|
+
await ensureContractWasms(log);
|
|
1038
|
+
const prover = new CardProver({ circuitDir: CARD_MEMBERSHIP_CIRCUIT_DIR });
|
|
1039
|
+
log("ensuring card_membership circuit is compiled + VK is built\u2026");
|
|
1040
|
+
await prover.ensureVk();
|
|
1041
|
+
log("ensuring valid_shuffle circuit is compiled + VK is built\u2026");
|
|
1042
|
+
await prover.ensureShuffleVk();
|
|
1043
|
+
const source = opts.source ?? "alice";
|
|
1044
|
+
const seatNames = seatIdentityNames(source, nPlayers, opts.multiSeat ?? false);
|
|
1045
|
+
log(`ensuring seat identities exist + are funded: ${[...new Set(seatNames)].join(", ")}\u2026`);
|
|
1046
|
+
const addressByName = await ensureIdentities(seatNames);
|
|
1047
|
+
const playerAddresses = seatNames.map((n) => addressByName[n]);
|
|
1048
|
+
const client = new CliRefereeClient({
|
|
1049
|
+
network: opts.network,
|
|
1050
|
+
source,
|
|
1051
|
+
sourceForSeat: Object.fromEntries(seatNames.map((n, i) => [i, n]))
|
|
1052
|
+
});
|
|
1053
|
+
log("reading card_membership verification key\u2026");
|
|
1054
|
+
const vkHex = (await (0, import_promises2.readFile)(opts.vkPath ?? DEFAULT_VK_PATH)).toString("hex");
|
|
1055
|
+
log("reading valid_shuffle verification key\u2026");
|
|
1056
|
+
const shuffleVkHex = (await (0, import_promises2.readFile)(opts.shuffleVkPath ?? DEFAULT_SHUFFLE_VK_PATH)).toString("hex");
|
|
1057
|
+
log("deploying card_membership verifier\u2026");
|
|
1058
|
+
const verifierContractId = await client.deployVerifier(opts.verifierWasmPath ?? DEFAULT_VERIFIER_WASM, vkHex);
|
|
1059
|
+
log(` verifier: ${verifierContractId}`);
|
|
1060
|
+
log("deploying valid_shuffle verifier\u2026");
|
|
1061
|
+
const shuffleVerifierContractId = await client.deployVerifier(
|
|
1062
|
+
opts.verifierWasmPath ?? DEFAULT_VERIFIER_WASM,
|
|
1063
|
+
shuffleVkHex
|
|
1064
|
+
);
|
|
1065
|
+
log(` shuffle verifier: ${shuffleVerifierContractId}`);
|
|
1066
|
+
log(`deploying coup-referee (seats=[${seatNames.join(", ")}])\u2026`);
|
|
1067
|
+
const refereeContractId = await client.deployReferee(opts.refereeWasmPath ?? DEFAULT_COUP_REFEREE_WASM, {
|
|
1068
|
+
verifier: verifierContractId,
|
|
1069
|
+
shuffleVerifier: shuffleVerifierContractId,
|
|
1070
|
+
playerAddresses
|
|
1071
|
+
});
|
|
1072
|
+
log(` referee: ${refereeContractId}`);
|
|
1073
|
+
const nonces = roster.map(() => randomField());
|
|
1074
|
+
for (let i = 0; i < roster.length; i++) {
|
|
1075
|
+
log(`player ${i}: committing seed nonce (hash2(nonce, 0))\u2026`);
|
|
1076
|
+
const commitmentHex = await prover.commit(nonces[i], 0n);
|
|
1077
|
+
await client.commitSeedNonce(refereeContractId, { player: i, commitmentHex });
|
|
1078
|
+
}
|
|
1079
|
+
for (let i = 0; i < roster.length; i++) {
|
|
1080
|
+
log(`player ${i}: revealing seed nonce\u2026`);
|
|
1081
|
+
await client.revealSeedNonce(refereeContractId, { player: i, nonceHex: toBe32Hex(nonces[i]) });
|
|
1082
|
+
}
|
|
1083
|
+
const seedHex = await prover.seed(nonces);
|
|
1084
|
+
const onChainSeed = (await client.gameState(refereeContractId)).seed;
|
|
1085
|
+
if (onChainSeed && onChainSeed.replace(/^0x/, "") !== seedHex.replace(/^0x/, "")) {
|
|
1086
|
+
throw new Error(`playCoupLite: locally-recomputed seed ${seedHex} != on-chain seed ${onChainSeed}`);
|
|
1087
|
+
}
|
|
1088
|
+
log(`joint seed: ${seedHex}`);
|
|
1089
|
+
const deckSalts = Array.from({ length: 15 }, () => randomField());
|
|
1090
|
+
log("proving valid_shuffle (real UltraHonk proof - the seed forces the order)\u2026");
|
|
1091
|
+
const shuffle = await prover.proveShuffle(seedHex, deckSalts);
|
|
1092
|
+
if (!await prover.shuffleLocalVerify(shuffle.proof, shuffle.publicInputs)) {
|
|
1093
|
+
throw new Error("playCoupLite: valid_shuffle proof failed local verification \u2014 aborting before the on-chain tx");
|
|
1094
|
+
}
|
|
1095
|
+
log("submitting submit_shuffle (ZK-verified on-chain; hands = deck positions)\u2026");
|
|
1096
|
+
await client.submitShuffle(refereeContractId, {
|
|
1097
|
+
leavesHex: shuffle.leavesHex,
|
|
1098
|
+
proofHex: Buffer.from(shuffle.proof).toString("hex")
|
|
1099
|
+
});
|
|
1100
|
+
const handsByPlayer2 = {};
|
|
1101
|
+
const saltsByPlayer = {};
|
|
1102
|
+
const deadByPlayer2 = {};
|
|
1103
|
+
for (let i = 0; i < roster.length; i++) {
|
|
1104
|
+
const playerId = roster[i].id;
|
|
1105
|
+
const hand = [shuffle.cards[2 * i], shuffle.cards[2 * i + 1]];
|
|
1106
|
+
handsByPlayer2[playerId] = hand;
|
|
1107
|
+
saltsByPlayer[playerId] = [BigInt(shuffle.salts[2 * i]), BigInt(shuffle.salts[2 * i + 1])];
|
|
1108
|
+
deadByPlayer2[playerId] = [false, false];
|
|
1109
|
+
log(`player ${i}: shuffled hand [${hand.join(", ")}] (${hand.map((c) => CHARACTER_NAMES[c]).join(", ")})`);
|
|
1110
|
+
}
|
|
1111
|
+
const localMatch = createLocalMatch(roster, { handsByPlayer: handsByPlayer2 }, seed);
|
|
1112
|
+
const moves = [];
|
|
1113
|
+
let state = await client.gameState(refereeContractId);
|
|
1114
|
+
let guard = 0;
|
|
1115
|
+
const maxSteps = 40;
|
|
1116
|
+
while (state.phase === "Playing" && guard < maxSteps) {
|
|
1117
|
+
guard += 1;
|
|
1118
|
+
const turnIdx = state.turn;
|
|
1119
|
+
const playerId = roster[turnIdx].id;
|
|
1120
|
+
const view = localMatch.view(playerId);
|
|
1121
|
+
const moveSeed = `${seed}:move${moves.length}:${playerId}`;
|
|
1122
|
+
const move = chooseMove(view, moveSeed);
|
|
1123
|
+
if (move.type === "claim") {
|
|
1124
|
+
const character = move.character;
|
|
1125
|
+
log(`${playerId} (index ${turnIdx}) claims: ${CHARACTER_NAMES[character]} (${character})`);
|
|
1126
|
+
await client.claim(refereeContractId, { player: turnIdx, character });
|
|
1127
|
+
localMatch.submit(playerId, move);
|
|
1128
|
+
moves.push({ kind: "claim", player: playerId, playerIndex: turnIdx, character });
|
|
1129
|
+
} else {
|
|
1130
|
+
const claim = localMatch.state.public.lastClaim;
|
|
1131
|
+
const targetIdx = roster.findIndex((p) => p.id === claim.player);
|
|
1132
|
+
const targetHand = handsByPlayer2[claim.player];
|
|
1133
|
+
const claimWasTrue = targetHand.includes(claim.character);
|
|
1134
|
+
log(
|
|
1135
|
+
`${playerId} (index ${turnIdx}) challenges ${claim.player} (index ${targetIdx})'s claim of ${CHARACTER_NAMES[claim.character]}\u2026`
|
|
1136
|
+
);
|
|
1137
|
+
await client.challenge(refereeContractId, { challenger: turnIdx, target: targetIdx });
|
|
1138
|
+
let loser;
|
|
1139
|
+
if (claimWasTrue) {
|
|
1140
|
+
const heldIndex = targetHand.indexOf(claim.character);
|
|
1141
|
+
log(` claim is TRUE - ${claim.player} proves it in ZK (real card_membership proof)\u2026`);
|
|
1142
|
+
const proof = await prover.proveHold(
|
|
1143
|
+
BigInt(claim.character),
|
|
1144
|
+
[BigInt(targetHand[0]), BigInt(targetHand[1])],
|
|
1145
|
+
saltsByPlayer[claim.player],
|
|
1146
|
+
heldIndex
|
|
1147
|
+
);
|
|
1148
|
+
await client.proveHold(refereeContractId, {
|
|
1149
|
+
target: targetIdx,
|
|
1150
|
+
claimed: claim.character,
|
|
1151
|
+
proofHex: Buffer.from(proof.proof).toString("hex")
|
|
1152
|
+
});
|
|
1153
|
+
loser = playerId;
|
|
1154
|
+
const loserDead = deadByPlayer2[loser];
|
|
1155
|
+
const slot = nextDeadSlot(loserDead);
|
|
1156
|
+
const loserHand = handsByPlayer2[loser];
|
|
1157
|
+
log(` challenger ${loser} loses influence - revealing real card ${loserHand[slot]}\u2026`);
|
|
1158
|
+
await client.revealCard(refereeContractId, {
|
|
1159
|
+
player: turnIdx,
|
|
1160
|
+
slot,
|
|
1161
|
+
card: loserHand[slot],
|
|
1162
|
+
saltHex: toBe32Hex(saltsByPlayer[loser][slot])
|
|
1163
|
+
});
|
|
1164
|
+
loserDead[slot] = true;
|
|
1165
|
+
} else {
|
|
1166
|
+
loser = claim.player;
|
|
1167
|
+
const loserDead = deadByPlayer2[loser];
|
|
1168
|
+
const slot = nextDeadSlot(loserDead);
|
|
1169
|
+
log(` claim is a BLUFF - ${loser} declines and reveals real card ${targetHand[slot]}\u2026`);
|
|
1170
|
+
await client.revealCard(refereeContractId, {
|
|
1171
|
+
player: targetIdx,
|
|
1172
|
+
slot,
|
|
1173
|
+
card: targetHand[slot],
|
|
1174
|
+
saltHex: toBe32Hex(saltsByPlayer[loser][slot])
|
|
1175
|
+
});
|
|
1176
|
+
loserDead[slot] = true;
|
|
1177
|
+
}
|
|
1178
|
+
localMatch.submit(playerId, { type: "challenge" });
|
|
1179
|
+
moves.push({
|
|
1180
|
+
kind: "challenge",
|
|
1181
|
+
challenger: playerId,
|
|
1182
|
+
challengerIndex: turnIdx,
|
|
1183
|
+
target: claim.player,
|
|
1184
|
+
targetIndex: targetIdx,
|
|
1185
|
+
claim,
|
|
1186
|
+
claimWasTrue,
|
|
1187
|
+
loser
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
state = await client.gameState(refereeContractId);
|
|
1191
|
+
}
|
|
1192
|
+
const outcome = state.outcome !== null ? roster[state.outcome]?.id ?? null : null;
|
|
1193
|
+
log(`game finished: outcome=${outcome}`);
|
|
1194
|
+
return {
|
|
1195
|
+
verifierContractId,
|
|
1196
|
+
refereeContractId,
|
|
1197
|
+
roster,
|
|
1198
|
+
handsByPlayer: handsByPlayer2,
|
|
1199
|
+
moves,
|
|
1200
|
+
finalState: state,
|
|
1201
|
+
outcome
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/index.ts
|
|
1206
|
+
init_paths();
|
|
1207
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1208
|
+
0 && (module.exports = {
|
|
1209
|
+
CHARACTERS,
|
|
1210
|
+
CHARACTER_NAMES,
|
|
1211
|
+
CardProver,
|
|
1212
|
+
CliRefereeClient,
|
|
1213
|
+
DEFAULT_COUP_REFEREE_WASM,
|
|
1214
|
+
DEFAULT_SHUFFLE_VK_PATH,
|
|
1215
|
+
DEFAULT_VERIFIER_WASM,
|
|
1216
|
+
DEFAULT_VK_PATH,
|
|
1217
|
+
HAND_SIZE,
|
|
1218
|
+
MAX_PLAYERS,
|
|
1219
|
+
MIN_PLAYERS,
|
|
1220
|
+
N_PLAYERS,
|
|
1221
|
+
RefereeCliError,
|
|
1222
|
+
START_INFLUENCE,
|
|
1223
|
+
buildRoster,
|
|
1224
|
+
chooseMove,
|
|
1225
|
+
coupLite,
|
|
1226
|
+
createLocalMatch,
|
|
1227
|
+
ensureContractWasms,
|
|
1228
|
+
pickSeeded,
|
|
1229
|
+
playCoupLite,
|
|
1230
|
+
playLocalMatch,
|
|
1231
|
+
seededHand,
|
|
1232
|
+
stepLocalMatch,
|
|
1233
|
+
stripHexPrefix,
|
|
1234
|
+
toBe32Hex,
|
|
1235
|
+
toolEnv
|
|
1236
|
+
});
|