theref-sdk 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/README.md +326 -0
- package/README.md:Zone.Identifier +0 -0
- package/dist/index.d.mts +229 -0
- package/dist/index.d.ts +229 -0
- package/dist/index.js +634 -0
- package/dist/index.mjs +602 -0
- package/package.json +36 -0
- package/package.json:Zone.Identifier +0 -0
- package/src/adapters/normalizer.ts +252 -0
- package/src/adapters/normalizer.ts:Zone.Identifier +0 -0
- package/src/client/TheRefClient.ts +444 -0
- package/src/client/TheRefClient.ts:Zone.Identifier +0 -0
- package/src/index.ts +56 -0
- package/src/index.ts:Zone.Identifier +0 -0
- package/src/types/index.ts +149 -0
- package/src/types/index.ts:Zone.Identifier +0 -0
- package/src/utils/networks.ts +39 -0
- package/src/utils/networks.ts:Zone.Identifier +0 -0
- package/tsconfig.json +16 -0
- package/tsconfig.json:Zone.Identifier +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { AnyMove, NormalizedMove } from "../types";
|
|
2
|
+
|
|
3
|
+
// ── Universal Move Normalizer ─────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Converts ANY input format into a human-readable string the AI judge can understand.
|
|
6
|
+
// Handles: strings, numbers, objects, arrays, nested structures — everything.
|
|
7
|
+
|
|
8
|
+
// ── Game-specific adapters ────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/** Chess — handles algebraic, UCI, coordinate pairs, and move objects */
|
|
11
|
+
function adaptChess(move: AnyMove): string | null {
|
|
12
|
+
if (typeof move === "string") {
|
|
13
|
+
// Already algebraic notation — passthrough
|
|
14
|
+
if (/^[a-h][1-8]$|^[NBRQK][a-h]?[1-8]?x?[a-h][1-8][+#]?$|^O-O(-O)?[+#]?$/.test(move)) {
|
|
15
|
+
return move;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (typeof move === "object" && move !== null && !Array.isArray(move)) {
|
|
19
|
+
const m = move as Record<string, AnyMove>;
|
|
20
|
+
// { from: "e2", to: "e4" } or { from: "e2", to: "e4", promotion: "q" }
|
|
21
|
+
if (m.from && m.to) {
|
|
22
|
+
const promo = m.promotion ? `=${String(m.promotion).toUpperCase()}` : "";
|
|
23
|
+
const piece = m.piece ? `${String(m.piece).toUpperCase()} ` : "";
|
|
24
|
+
return `${piece}${m.from}${m.promotion ? "" : ""}${m.to}${promo}`.trim();
|
|
25
|
+
}
|
|
26
|
+
// { piece: "knight", from: "g1", to: "f3" }
|
|
27
|
+
if (m.piece && m.from && m.to) {
|
|
28
|
+
const pieceMap: Record<string, string> = {
|
|
29
|
+
knight: "N", bishop: "B", rook: "R", queen: "Q", king: "K", pawn: "",
|
|
30
|
+
};
|
|
31
|
+
const p = pieceMap[String(m.piece).toLowerCase()] ?? "";
|
|
32
|
+
return `${p}${m.to}`;
|
|
33
|
+
}
|
|
34
|
+
// UCI format { uci: "e2e4" }
|
|
35
|
+
if (m.uci && typeof m.uci === "string") {
|
|
36
|
+
return `${m.uci.slice(0, 2)}-${m.uci.slice(2, 4)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Coordinate array [fromFile, fromRank, toFile, toRank] e.g. [4,1,4,3]
|
|
40
|
+
if (Array.isArray(move) && move.length === 4 && move.every(n => typeof n === "number")) {
|
|
41
|
+
const files = "abcdefgh";
|
|
42
|
+
return `${files[move[0] as number]}${(move[1] as number) + 1}-${files[move[2] as number]}${(move[3] as number) + 1}`;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Rock Paper Scissors — normalizes any RPS variant */
|
|
48
|
+
function adaptRPS(move: AnyMove): string | null {
|
|
49
|
+
const choices: Record<string, string> = {
|
|
50
|
+
rock: "Rock", paper: "Paper", scissors: "Scissors",
|
|
51
|
+
r: "Rock", p: "Paper", s: "Scissors",
|
|
52
|
+
"0": "Rock", "1": "Paper", "2": "Scissors",
|
|
53
|
+
stone: "Rock", shears: "Scissors",
|
|
54
|
+
lizard: "Lizard", spock: "Spock", // RPSLS variant
|
|
55
|
+
};
|
|
56
|
+
if (typeof move === "string") {
|
|
57
|
+
const normalized = choices[move.toLowerCase().trim()];
|
|
58
|
+
if (normalized) return normalized;
|
|
59
|
+
}
|
|
60
|
+
if (typeof move === "number") {
|
|
61
|
+
const normalized = choices[String(move)];
|
|
62
|
+
if (normalized) return normalized;
|
|
63
|
+
}
|
|
64
|
+
if (typeof move === "object" && !Array.isArray(move) && move !== null) {
|
|
65
|
+
const m = move as Record<string, AnyMove>;
|
|
66
|
+
const choice = m.choice ?? m.action ?? m.move ?? m.pick;
|
|
67
|
+
if (choice) return adaptRPS(choice);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Combat games — RPG attacks, fighting games, card games */
|
|
73
|
+
function adaptCombat(move: AnyMove): string | null {
|
|
74
|
+
if (typeof move === "object" && !Array.isArray(move) && move !== null) {
|
|
75
|
+
const m = move as Record<string, AnyMove>;
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
|
|
78
|
+
// Who is doing what
|
|
79
|
+
if (m.player || m.character || m.unit) {
|
|
80
|
+
parts.push(String(m.player ?? m.character ?? m.unit));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Action
|
|
84
|
+
const action = m.action ?? m.move ?? m.skill ?? m.ability ?? m.attack ?? m.spell ?? m.card;
|
|
85
|
+
if (action) parts.push(`uses ${action}`);
|
|
86
|
+
|
|
87
|
+
// Target
|
|
88
|
+
const target = m.target ?? m.enemy ?? m.opponent;
|
|
89
|
+
if (target) parts.push(`on ${target}`);
|
|
90
|
+
|
|
91
|
+
// Modifiers
|
|
92
|
+
if (m.power !== undefined) parts.push(`(power: ${m.power})`);
|
|
93
|
+
if (m.damage !== undefined) parts.push(`(damage: ${m.damage})`);
|
|
94
|
+
if (m.mana_cost !== undefined) parts.push(`(mana: ${m.mana_cost})`);
|
|
95
|
+
if (m.combo) parts.push(`via combo: ${Array.isArray(m.combo) ? (m.combo as AnyMove[]).join("+") : m.combo}`);
|
|
96
|
+
if (m.type) parts.push(`[${m.type} type]`);
|
|
97
|
+
if (m.element) parts.push(`[${m.element}]`);
|
|
98
|
+
if (m.special) parts.push(`special: ${m.special}`);
|
|
99
|
+
if (m.position) parts.push(`at ${JSON.stringify(m.position)}`);
|
|
100
|
+
|
|
101
|
+
if (parts.length > 0) return parts.join(" ");
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Strategy/board game moves */
|
|
107
|
+
function adaptStrategy(move: AnyMove): string | null {
|
|
108
|
+
if (typeof move === "object" && !Array.isArray(move) && move !== null) {
|
|
109
|
+
const m = move as Record<string, AnyMove>;
|
|
110
|
+
const parts: string[] = [];
|
|
111
|
+
|
|
112
|
+
const action = m.action ?? m.command ?? m.order;
|
|
113
|
+
if (action) parts.push(String(action));
|
|
114
|
+
|
|
115
|
+
const unit = m.unit ?? m.piece ?? m.troop ?? m.building;
|
|
116
|
+
if (unit) parts.push(String(unit));
|
|
117
|
+
|
|
118
|
+
if (m.from && m.to) {
|
|
119
|
+
parts.push(`from ${JSON.stringify(m.from)} to ${JSON.stringify(m.to)}`);
|
|
120
|
+
} else if (m.position ?? m.target ?? m.location) {
|
|
121
|
+
parts.push(`at ${JSON.stringify(m.position ?? m.target ?? m.location)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (m.attack) parts.push(`attacking with ${m.attack}`);
|
|
125
|
+
if (m.defend) parts.push(`defending with ${m.defend}`);
|
|
126
|
+
if (m.resource !== undefined) parts.push(`(cost: ${m.resource})`);
|
|
127
|
+
|
|
128
|
+
if (parts.length > 0) return parts.join(" ");
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Trivia / Q&A — any answer format */
|
|
134
|
+
function adaptTrivia(move: AnyMove): string | null {
|
|
135
|
+
if (typeof move === "object" && !Array.isArray(move) && move !== null) {
|
|
136
|
+
const m = move as Record<string, AnyMove>;
|
|
137
|
+
const answer = m.answer ?? m.response ?? m.text ?? m.value;
|
|
138
|
+
if (answer !== undefined) return String(answer);
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Debate / argument */
|
|
144
|
+
function adaptDebate(move: AnyMove): string | null {
|
|
145
|
+
if (typeof move === "object" && !Array.isArray(move) && move !== null) {
|
|
146
|
+
const m = move as Record<string, AnyMove>;
|
|
147
|
+
const parts: string[] = [];
|
|
148
|
+
if (m.position) parts.push(`Position: ${m.position}`);
|
|
149
|
+
if (m.argument) parts.push(`Argument: ${m.argument}`);
|
|
150
|
+
if (m.evidence) parts.push(`Evidence: ${m.evidence}`);
|
|
151
|
+
if (m.rebuttal) parts.push(`Rebuttal: ${m.rebuttal}`);
|
|
152
|
+
if (parts.length > 0) return parts.join(". ");
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Generic deep object flattener ─────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function flattenObject(obj: Record<string, AnyMove>, prefix = ""): string {
|
|
160
|
+
return Object.entries(obj)
|
|
161
|
+
.map(([key, val]) => {
|
|
162
|
+
const label = prefix ? `${prefix}.${key}` : key;
|
|
163
|
+
if (val === null || val === undefined) return null;
|
|
164
|
+
if (typeof val === "object" && !Array.isArray(val)) {
|
|
165
|
+
return flattenObject(val as Record<string, AnyMove>, label);
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(val)) {
|
|
168
|
+
return `${label}: [${(val as AnyMove[]).map(v => String(v)).join(", ")}]`;
|
|
169
|
+
}
|
|
170
|
+
return `${label}: ${val}`;
|
|
171
|
+
})
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join(", ");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Main normalizer ───────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export function normalizeMove(move: AnyMove, gameHint?: string): NormalizedMove {
|
|
179
|
+
// 1. Already a string — use directly
|
|
180
|
+
if (typeof move === "string") {
|
|
181
|
+
return { raw: move, text: move.trim(), adapter: "passthrough" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. Number — convert to string
|
|
185
|
+
if (typeof move === "number") {
|
|
186
|
+
return { raw: move, text: String(move), adapter: "number" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 3. Boolean
|
|
190
|
+
if (typeof move === "boolean") {
|
|
191
|
+
return { raw: move, text: move ? "Yes" : "No", adapter: "boolean" };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 4. Null/undefined
|
|
195
|
+
if (move === null || move === undefined) {
|
|
196
|
+
return { raw: move as AnyMove, text: "No move", adapter: "null" };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hint = gameHint?.toLowerCase() ?? "";
|
|
200
|
+
|
|
201
|
+
// 5. Try game-specific adapters based on hint
|
|
202
|
+
if (hint.includes("chess")) {
|
|
203
|
+
const result = adaptChess(move);
|
|
204
|
+
if (result) return { raw: move, text: result, adapter: "chess" };
|
|
205
|
+
}
|
|
206
|
+
if (hint.includes("rock") || hint.includes("rps") || hint.includes("scissors")) {
|
|
207
|
+
const result = adaptRPS(move);
|
|
208
|
+
if (result) return { raw: move, text: result, adapter: "rps" };
|
|
209
|
+
}
|
|
210
|
+
if (hint.includes("trivia") || hint.includes("quiz") || hint.includes("question")) {
|
|
211
|
+
const result = adaptTrivia(move);
|
|
212
|
+
if (result) return { raw: move, text: result, adapter: "trivia" };
|
|
213
|
+
}
|
|
214
|
+
if (hint.includes("debate") || hint.includes("argument")) {
|
|
215
|
+
const result = adaptDebate(move);
|
|
216
|
+
if (result) return { raw: move, text: result, adapter: "debate" };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 6. Auto-detect from structure — try all adapters
|
|
220
|
+
if (typeof move === "object") {
|
|
221
|
+
const chess = adaptChess(move);
|
|
222
|
+
if (chess) return { raw: move, text: chess, adapter: "chess" };
|
|
223
|
+
|
|
224
|
+
const rps = adaptRPS(move);
|
|
225
|
+
if (rps) return { raw: move, text: rps, adapter: "rps" };
|
|
226
|
+
|
|
227
|
+
const combat = adaptCombat(move);
|
|
228
|
+
if (combat) return { raw: move, text: combat, adapter: "combat" };
|
|
229
|
+
|
|
230
|
+
const strategy = adaptStrategy(move);
|
|
231
|
+
if (strategy) return { raw: move, text: strategy, adapter: "strategy" };
|
|
232
|
+
|
|
233
|
+
const trivia = adaptTrivia(move);
|
|
234
|
+
if (trivia) return { raw: move, text: trivia, adapter: "trivia" };
|
|
235
|
+
|
|
236
|
+
const debate = adaptDebate(move);
|
|
237
|
+
if (debate) return { raw: move, text: debate, adapter: "debate" };
|
|
238
|
+
|
|
239
|
+
// 7. Array — join elements
|
|
240
|
+
if (Array.isArray(move)) {
|
|
241
|
+
const text = (move as AnyMove[]).map(m => normalizeMove(m).text).join(", ");
|
|
242
|
+
return { raw: move, text, adapter: "array" };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 8. Generic object — flatten to readable string
|
|
246
|
+
const flat = flattenObject(move as Record<string, AnyMove>);
|
|
247
|
+
return { raw: move, text: flat, adapter: "generic-object" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 9. Absolute fallback
|
|
251
|
+
return { raw: move, text: String(move), adapter: "fallback" };
|
|
252
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { createClient, createAccount, generatePrivateKey, chains } from "genlayer-js";
|
|
2
|
+
import { TransactionStatus } from "genlayer-js/types";
|
|
3
|
+
import {
|
|
4
|
+
TheRefConfig, NetworkConfig, GameState, GameStatus,
|
|
5
|
+
CreateGameOptions, JudgmentResult, RoundResult,
|
|
6
|
+
CreateTournamentOptions, Tournament, BracketMatch,
|
|
7
|
+
LeaderboardEntry, PlayerStats, PlayerType, AnyMove,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { resolveNetwork, gidToNum, DEFAULT_WS_URL } from "../utils/networks";
|
|
10
|
+
import { normalizeMove } from "../adapters/normalizer";
|
|
11
|
+
|
|
12
|
+
// ── Client ────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export class TheRefClient {
|
|
15
|
+
private network: NetworkConfig;
|
|
16
|
+
private glClient: ReturnType<typeof createClient>;
|
|
17
|
+
private retries: number;
|
|
18
|
+
private pollInterval: number;
|
|
19
|
+
private wsUrl: string;
|
|
20
|
+
|
|
21
|
+
constructor(config: TheRefConfig) {
|
|
22
|
+
this.network = resolveNetwork(config.network);
|
|
23
|
+
this.retries = config.retries ?? 300;
|
|
24
|
+
this.pollInterval = config.pollInterval ?? 5000;
|
|
25
|
+
this.wsUrl = config.wsUrl ?? DEFAULT_WS_URL;
|
|
26
|
+
|
|
27
|
+
// Select the correct built-in chain
|
|
28
|
+
const chain = this.network.chainId === 4221
|
|
29
|
+
? chains.testnetBradbury
|
|
30
|
+
: chains.studionet;
|
|
31
|
+
|
|
32
|
+
// Build account — private key for agents, wallet address for browser
|
|
33
|
+
let account: any;
|
|
34
|
+
if (config.privateKey) {
|
|
35
|
+
account = createAccount(config.privateKey);
|
|
36
|
+
} else if (config.walletAddress) {
|
|
37
|
+
account = config.walletAddress; // SDK routes through window.ethereum
|
|
38
|
+
} else {
|
|
39
|
+
// Dev mode — generate ephemeral key
|
|
40
|
+
account = createAccount(generatePrivateKey());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.glClient = createClient({
|
|
44
|
+
chain,
|
|
45
|
+
endpoint: this.network.rpc,
|
|
46
|
+
account,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Internal helpers ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
private async write(
|
|
53
|
+
address: string,
|
|
54
|
+
method: string,
|
|
55
|
+
args: any[],
|
|
56
|
+
value: bigint = 0n,
|
|
57
|
+
): Promise<{ payload: string; txHash: string }> {
|
|
58
|
+
const tx = await this.glClient.writeContract({
|
|
59
|
+
address: address as `0x${string}`,
|
|
60
|
+
functionName: method,
|
|
61
|
+
args: args as any,
|
|
62
|
+
value,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const receipt = await this.glClient.waitForTransactionReceipt({
|
|
66
|
+
hash: tx as any,
|
|
67
|
+
status: TransactionStatus.ACCEPTED,
|
|
68
|
+
retries: this.retries,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const leader = (receipt as any)?.consensus_data?.leader_receipt?.[0];
|
|
72
|
+
const payload = String(
|
|
73
|
+
leader?.result?.payload?.readable ??
|
|
74
|
+
(receipt as any)?.data?.result ?? ""
|
|
75
|
+
).replace(/^"|"$/g, "");
|
|
76
|
+
|
|
77
|
+
return { payload, txHash: String(tx) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async read(
|
|
81
|
+
address: string,
|
|
82
|
+
method: string,
|
|
83
|
+
args: any[] = [],
|
|
84
|
+
): Promise<unknown> {
|
|
85
|
+
return this.glClient.readContract({
|
|
86
|
+
address: address as `0x${string}`,
|
|
87
|
+
functionName: method,
|
|
88
|
+
args: args as any,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private get CORE() { return this.network.addresses.CORE; }
|
|
93
|
+
private get LB() { return this.network.addresses.LB; }
|
|
94
|
+
private get TRN() { return this.network.addresses.TRN; }
|
|
95
|
+
|
|
96
|
+
private mapGameState(raw: any): GameState {
|
|
97
|
+
return {
|
|
98
|
+
gameId: raw.game_id ?? "",
|
|
99
|
+
gameName: raw.game_name ?? "",
|
|
100
|
+
status: raw.status ?? "waiting",
|
|
101
|
+
player1: raw.player1 ?? "",
|
|
102
|
+
player2: raw.player2 ?? "",
|
|
103
|
+
agent1: raw.agent1 ?? "",
|
|
104
|
+
agent2: raw.agent2 ?? "",
|
|
105
|
+
maxRounds: Number(raw.max_rounds ?? 0),
|
|
106
|
+
roundCount: Number(raw.round_count ?? 0),
|
|
107
|
+
judgedThrough: Number(raw.judged_through ?? 0),
|
|
108
|
+
rules: raw.rules ?? "",
|
|
109
|
+
winner: raw.winner ?? "",
|
|
110
|
+
score: raw.score ?? {},
|
|
111
|
+
playerTypes: raw.player_types ?? {},
|
|
112
|
+
caller: raw.caller ?? "",
|
|
113
|
+
rounds: (raw.rounds ?? []).map((r: any) => ({
|
|
114
|
+
roundNumber: r.round_number,
|
|
115
|
+
movePlayer1: r.move_player1 ?? "",
|
|
116
|
+
movePlayer2: r.move_player2 ?? "",
|
|
117
|
+
result: r.result ?? "pending",
|
|
118
|
+
reasonType: r.reason_type ?? "normal",
|
|
119
|
+
invalidPlayer: r.invalid_player ?? "none",
|
|
120
|
+
reasoning: r.reasoning ?? "",
|
|
121
|
+
confidence: Number(r.confidence ?? 0),
|
|
122
|
+
status: r.status ?? "pending",
|
|
123
|
+
})),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Game API ────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a new game. Returns the game ID.
|
|
131
|
+
*/
|
|
132
|
+
async createGame(options: CreateGameOptions): Promise<string> {
|
|
133
|
+
const { payload } = await this.write(this.CORE, "start_game", [
|
|
134
|
+
options.name,
|
|
135
|
+
options.visibility ?? "public",
|
|
136
|
+
options.maxRounds ?? 3,
|
|
137
|
+
options.rules ?? "",
|
|
138
|
+
options.player1,
|
|
139
|
+
options.player2 ?? "",
|
|
140
|
+
options.agent1 ?? 0,
|
|
141
|
+
options.agent2 ?? 0,
|
|
142
|
+
]);
|
|
143
|
+
return payload;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Submit a move for a player. Accepts ANY move format — string, object, array, etc.
|
|
148
|
+
* The SDK automatically normalizes it to a string the AI judge can understand.
|
|
149
|
+
*/
|
|
150
|
+
async submitMove(
|
|
151
|
+
gameId: string,
|
|
152
|
+
playerName: string,
|
|
153
|
+
move: AnyMove,
|
|
154
|
+
gameHint?: string, // optional game name hint for better normalization
|
|
155
|
+
): Promise<{ txHash: string; normalizedMove: string }> {
|
|
156
|
+
const normalized = normalizeMove(move, gameHint);
|
|
157
|
+
const { txHash } = await this.write(this.CORE, "submit_move", [
|
|
158
|
+
gidToNum(gameId),
|
|
159
|
+
playerName,
|
|
160
|
+
normalized.text,
|
|
161
|
+
]);
|
|
162
|
+
return { txHash, normalizedMove: normalized.text };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Judge the game — triggers AI consensus on all pending rounds.
|
|
167
|
+
*/
|
|
168
|
+
async judgeGame(gameId: string): Promise<JudgmentResult> {
|
|
169
|
+
const { payload, txHash } = await this.write(
|
|
170
|
+
this.CORE, "judge_game", [gidToNum(gameId)]
|
|
171
|
+
);
|
|
172
|
+
const state = await this.getGameState(gameId);
|
|
173
|
+
return {
|
|
174
|
+
winner: state.winner,
|
|
175
|
+
isDraw: state.status === "draw",
|
|
176
|
+
score: state.score,
|
|
177
|
+
rounds: state.rounds,
|
|
178
|
+
txHash,
|
|
179
|
+
payload,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* End an open-ended game (max_rounds = 0). Only callable by the game creator.
|
|
185
|
+
*/
|
|
186
|
+
async endGame(gameId: string): Promise<JudgmentResult> {
|
|
187
|
+
const { payload, txHash } = await this.write(
|
|
188
|
+
this.CORE, "end_game", [gidToNum(gameId)]
|
|
189
|
+
);
|
|
190
|
+
const state = await this.getGameState(gameId);
|
|
191
|
+
return {
|
|
192
|
+
winner: state.winner,
|
|
193
|
+
isDraw: state.status === "draw",
|
|
194
|
+
score: state.score,
|
|
195
|
+
rounds: state.rounds,
|
|
196
|
+
txHash,
|
|
197
|
+
payload,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the full game state.
|
|
203
|
+
*/
|
|
204
|
+
async getGameState(gameId: string): Promise<GameState> {
|
|
205
|
+
const raw = await this.read(this.CORE, "get_game_state", [gameId]);
|
|
206
|
+
return this.mapGameState(raw);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get all active games.
|
|
211
|
+
*/
|
|
212
|
+
async getActiveGames(): Promise<GameState[]> {
|
|
213
|
+
const raw = await this.read(this.CORE, "get_active_games") as any[];
|
|
214
|
+
return (raw ?? []).map(this.mapGameState);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get total number of games.
|
|
219
|
+
*/
|
|
220
|
+
async getTotalGames(): Promise<number> {
|
|
221
|
+
const result = await this.read(this.CORE, "get_total_games");
|
|
222
|
+
return Number(result ?? 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Polling helpers ─────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Wait until both players have submitted their move for a given round.
|
|
229
|
+
* Polls every `pollInterval` ms.
|
|
230
|
+
*/
|
|
231
|
+
async waitForBothMoves(gameId: string, roundNumber: number): Promise<RoundResult> {
|
|
232
|
+
while (true) {
|
|
233
|
+
const state = await this.getGameState(gameId);
|
|
234
|
+
const round = state.rounds.find(r => r.roundNumber === roundNumber);
|
|
235
|
+
if (round?.movePlayer1 && round?.movePlayer2) return round;
|
|
236
|
+
await this.sleep(this.pollInterval);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Wait until the opponent has submitted their move for a round.
|
|
242
|
+
* isPlayer1: true if you are player1, false if player2.
|
|
243
|
+
*/
|
|
244
|
+
async waitForOpponentMove(
|
|
245
|
+
gameId: string,
|
|
246
|
+
roundNumber: number,
|
|
247
|
+
isPlayer1: boolean,
|
|
248
|
+
): Promise<string> {
|
|
249
|
+
while (true) {
|
|
250
|
+
const state = await this.getGameState(gameId);
|
|
251
|
+
const round = state.rounds.find(r => r.roundNumber === roundNumber);
|
|
252
|
+
const opponentMove = isPlayer1 ? round?.movePlayer2 : round?.movePlayer1;
|
|
253
|
+
if (opponentMove) return opponentMove;
|
|
254
|
+
await this.sleep(this.pollInterval);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Wait until the game reaches a specific status.
|
|
260
|
+
*/
|
|
261
|
+
async waitForStatus(gameId: string, status: GameStatus): Promise<GameState> {
|
|
262
|
+
while (true) {
|
|
263
|
+
const state = await this.getGameState(gameId);
|
|
264
|
+
if (state.status === status) return state;
|
|
265
|
+
if (state.status === "completed" || state.status === "draw") return state;
|
|
266
|
+
await this.sleep(this.pollInterval);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Play a full game automatically.
|
|
272
|
+
* moveFn is called for each round — return your move in any format.
|
|
273
|
+
*/
|
|
274
|
+
async playGame(
|
|
275
|
+
gameId: string,
|
|
276
|
+
playerName: string,
|
|
277
|
+
isPlayer1: boolean,
|
|
278
|
+
moveFn: (state: GameState, round: number) => Promise<AnyMove>,
|
|
279
|
+
gameHint?: string,
|
|
280
|
+
): Promise<JudgmentResult> {
|
|
281
|
+
let state = await this.getGameState(gameId);
|
|
282
|
+
const maxRounds = state.maxRounds || 999;
|
|
283
|
+
|
|
284
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
285
|
+
state = await this.getGameState(gameId);
|
|
286
|
+
if (state.status !== "active") break;
|
|
287
|
+
|
|
288
|
+
// Wait for opponent first if we're player2
|
|
289
|
+
if (!isPlayer1) {
|
|
290
|
+
await this.waitForOpponentMove(gameId, round, false);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Generate and submit our move
|
|
294
|
+
state = await this.getGameState(gameId);
|
|
295
|
+
const move = await moveFn(state, round);
|
|
296
|
+
await this.submitMove(gameId, playerName, move, gameHint);
|
|
297
|
+
|
|
298
|
+
// Wait for opponent if we're player1
|
|
299
|
+
if (isPlayer1) {
|
|
300
|
+
await this.waitForOpponentMove(gameId, round, true);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if max rounds reached
|
|
304
|
+
state = await this.getGameState(gameId);
|
|
305
|
+
if (state.roundCount >= maxRounds) break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return this.judgeGame(gameId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Leaderboard API ─────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
async getLeaderboard(
|
|
314
|
+
gameName: string,
|
|
315
|
+
playerType: PlayerType | "all" = "all",
|
|
316
|
+
): Promise<LeaderboardEntry[]> {
|
|
317
|
+
const raw = await this.read(this.LB, "get_leaderboard", [gameName, playerType]) as any[];
|
|
318
|
+
return (raw ?? []).map(e => ({
|
|
319
|
+
player: e.player ?? "",
|
|
320
|
+
wins: Number(e.wins ?? 0),
|
|
321
|
+
losses: Number(e.losses ?? 0),
|
|
322
|
+
draws: Number(e.draws ?? 0),
|
|
323
|
+
score: Number(e.score ?? 0),
|
|
324
|
+
playerType: (e.player_type ?? "human") as PlayerType,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async getTopPlayers(gameName: string, n = 10): Promise<LeaderboardEntry[]> {
|
|
329
|
+
const raw = await this.read(this.LB, "get_top_players", [gameName, n]) as any[];
|
|
330
|
+
return (raw ?? []).map(e => ({
|
|
331
|
+
player: e.player ?? "",
|
|
332
|
+
wins: Number(e.wins ?? 0),
|
|
333
|
+
losses: Number(e.losses ?? 0),
|
|
334
|
+
draws: Number(e.draws ?? 0),
|
|
335
|
+
score: Number(e.score ?? 0),
|
|
336
|
+
playerType: (e.player_type ?? "human") as PlayerType,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async getPlayerStats(
|
|
341
|
+
gameName: string,
|
|
342
|
+
playerName: string,
|
|
343
|
+
playerType: PlayerType | "all" = "all",
|
|
344
|
+
): Promise<PlayerStats | null> {
|
|
345
|
+
try {
|
|
346
|
+
const raw = await this.read(this.LB, "get_player_stats", [gameName, playerName, playerType]) as any;
|
|
347
|
+
if (!raw) return null;
|
|
348
|
+
return {
|
|
349
|
+
player: raw.player ?? playerName,
|
|
350
|
+
wins: Number(raw.wins ?? 0),
|
|
351
|
+
losses: Number(raw.losses ?? 0),
|
|
352
|
+
draws: Number(raw.draws ?? 0),
|
|
353
|
+
score: Number(raw.score ?? 0),
|
|
354
|
+
games: Number(raw.games ?? 0),
|
|
355
|
+
playerType: (raw.player_type ?? playerType) as PlayerType,
|
|
356
|
+
};
|
|
357
|
+
} catch { return null; }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Tournament API ──────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
async createTournament(options: CreateTournamentOptions): Promise<string> {
|
|
363
|
+
const { payload } = await this.write(this.TRN, "create_tournament", [
|
|
364
|
+
options.name,
|
|
365
|
+
options.gameName,
|
|
366
|
+
options.format,
|
|
367
|
+
options.maxPlayers,
|
|
368
|
+
0, // entry fee — always free
|
|
369
|
+
options.prizeSplit ?? [70, 30],
|
|
370
|
+
options.rules ?? "",
|
|
371
|
+
options.roundsPerMatch ?? 1,
|
|
372
|
+
]);
|
|
373
|
+
return payload;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async joinTournament(
|
|
377
|
+
tid: string,
|
|
378
|
+
playerName: string,
|
|
379
|
+
playerType: PlayerType = "human",
|
|
380
|
+
): Promise<void> {
|
|
381
|
+
await this.write(this.TRN, "join_tournament", [tid, playerName, playerType]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async startTournament(tid: string): Promise<void> {
|
|
385
|
+
await this.write(this.TRN, "start_tournament", [tid]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async getTournament(tid: string): Promise<Tournament | null> {
|
|
389
|
+
try {
|
|
390
|
+
const raw = await this.read(this.TRN, "get_tournament", [tid]) as any;
|
|
391
|
+
if (!raw) return null;
|
|
392
|
+
return {
|
|
393
|
+
tid: raw.tid ?? tid,
|
|
394
|
+
name: raw.name ?? "",
|
|
395
|
+
gameName: raw.game_name ?? "",
|
|
396
|
+
format: raw.format ?? "",
|
|
397
|
+
maxPlayers: Number(raw.max_players ?? 0),
|
|
398
|
+
status: raw.status ?? "",
|
|
399
|
+
players: raw.players ?? [],
|
|
400
|
+
bracket: (raw.bracket ?? []).map((m: any) => ({
|
|
401
|
+
matchId: m.match_id,
|
|
402
|
+
round: m.round,
|
|
403
|
+
player1: m.player1,
|
|
404
|
+
player2: m.player2,
|
|
405
|
+
gameId: m.game_id,
|
|
406
|
+
winner: m.winner,
|
|
407
|
+
status: m.status,
|
|
408
|
+
})),
|
|
409
|
+
winner: raw.winner ?? "",
|
|
410
|
+
};
|
|
411
|
+
} catch { return null; }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async listTournaments(): Promise<Tournament[]> {
|
|
415
|
+
const raw = await this.read(this.TRN, "list_tournaments") as any[];
|
|
416
|
+
return (raw ?? []).map(t => ({
|
|
417
|
+
tid: t.tid ?? "",
|
|
418
|
+
name: t.name ?? "",
|
|
419
|
+
gameName: t.game_name ?? "",
|
|
420
|
+
format: t.format ?? "",
|
|
421
|
+
maxPlayers: Number(t.max_players ?? 0),
|
|
422
|
+
status: t.status ?? "",
|
|
423
|
+
players: t.players ?? [],
|
|
424
|
+
bracket: [],
|
|
425
|
+
winner: t.winner ?? "",
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async recordMatchResult(tid: string, matchId: number, winner: string): Promise<void> {
|
|
430
|
+
await this.write(this.TRN, "record_match_result", [tid, matchId, winner]);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Utils ───────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
/** Normalize any move to a string without submitting */
|
|
436
|
+
normalizeMove(move: AnyMove, gameHint?: string) {
|
|
437
|
+
return normalizeMove(move, gameHint);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Sleep helper */
|
|
441
|
+
private sleep(ms: number) {
|
|
442
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
Binary file
|