clarityxo-sdk 0.1.0 → 0.3.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 +40 -0
- package/dist/chunk-2P6NUM7A.mjs +350 -0
- package/dist/chunk-ETEDBK3G.mjs +348 -0
- package/dist/cli/index.js +196 -3
- package/dist/cli/index.mjs +180 -4
- package/dist/index.d.mts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +111 -0
- package/dist/index.mjs +74 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ These require `senderKey` and `senderAddress` in config.
|
|
|
57
57
|
- `submitResult(result: GameResult): Promise<void>` - Submit a game result
|
|
58
58
|
- `syncLeaderboard(): Promise<void>` - Sync leaderboard data
|
|
59
59
|
- `healthCheck(): Promise<boolean>` - Check if leaderboard API is healthy
|
|
60
|
+
- `getPlayerStats(playerAddress: string, month?: string): Promise<PlayerStats | null>` - Get statistics for a specific player
|
|
60
61
|
|
|
61
62
|
### Types
|
|
62
63
|
|
|
@@ -69,6 +70,7 @@ These require `senderKey` and `senderAddress` in config.
|
|
|
69
70
|
- `LeaderboardEntry`: { player, wins, losses, draws, points, rank }
|
|
70
71
|
- `LeaderboardMonth`: { month, entries }
|
|
71
72
|
- `GameResult`: { player, outcome, month }
|
|
73
|
+
- `PlayerStats`: { player, wins, losses, draws, points, rank, totalGames, winRate }
|
|
72
74
|
|
|
73
75
|
## CLI
|
|
74
76
|
|
|
@@ -129,6 +131,30 @@ Display the leaderboard for a month.
|
|
|
129
131
|
clarityxo leaderboard --month 2023-10 --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1
|
|
130
132
|
```
|
|
131
133
|
|
|
134
|
+
#### `move <row> <col>`
|
|
135
|
+
|
|
136
|
+
Make a move on the board. Requires sender key and address.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
clarityxo move 1 2 --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1 --key <private-key> --address <address>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### `start`
|
|
143
|
+
|
|
144
|
+
Start a new game. Requires sender key and address.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
clarityxo start --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1 --key <private-key> --address <address>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `resign`
|
|
151
|
+
|
|
152
|
+
Resign the current game. Requires sender key and address.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
clarityxo resign --contract ST1PQHQKVVA1MGKKKSQCXDTEYXSGHCV66VR89BS1 --key <private-key> --address <address>
|
|
156
|
+
```
|
|
157
|
+
|
|
132
158
|
## Network Configuration
|
|
133
159
|
|
|
134
160
|
- **Mainnet**: Use 'mainnet' network, mainnet contract addresses
|
|
@@ -149,6 +175,18 @@ try {
|
|
|
149
175
|
}
|
|
150
176
|
```
|
|
151
177
|
|
|
178
|
+
## Utility Functions
|
|
179
|
+
|
|
180
|
+
The SDK includes utility functions for board analysis:
|
|
181
|
+
|
|
182
|
+
- `checkWin(board, player)` - Check if a player has won
|
|
183
|
+
- `isBoardFull(board)` - Check if the board is full
|
|
184
|
+
- `isDraw(board)` - Check if the game is a draw
|
|
185
|
+
- `getAvailableMoves(board)` - Get list of available moves
|
|
186
|
+
- `getBoardString(board)` - Convert board to string representation
|
|
187
|
+
- `cloneBoard(board)` - Create a deep copy of the board
|
|
188
|
+
- `makeMoveOnBoard(board, row, col, player)` - Make a move and return new board
|
|
189
|
+
|
|
152
190
|
## Contributing
|
|
153
191
|
|
|
154
192
|
1. Fork the repository
|
|
@@ -159,6 +197,8 @@ try {
|
|
|
159
197
|
6. Format: `npm run format`
|
|
160
198
|
7. Submit a PR
|
|
161
199
|
|
|
200
|
+
If you'd like to contribute a bugfix or small improvement, open an issue first with a short description of the change. For larger features, open a discussion or draft PR so we can coordinate before significant design work.
|
|
201
|
+
|
|
162
202
|
## License
|
|
163
203
|
|
|
164
204
|
MIT
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var CONTRACT_NAME = "tictactoe";
|
|
3
|
+
var CONTRACT_ADDRESS = "SP30VGN68PSGVWGNMD0HH2WQMM5T486EK3YGP7Z3Y.clarity-xo-game";
|
|
4
|
+
var MAINNET_API = "https://api.mainnet.hiro.so";
|
|
5
|
+
var TESTNET_API = "https://api.testnet.hiro.so";
|
|
6
|
+
var DEFAULT_LEADERBOARD_API = "https://clarityxo.onrender.com";
|
|
7
|
+
var DEFAULT_NETWORK = "testnet";
|
|
8
|
+
var CONTRACT_FUNCTIONS = {
|
|
9
|
+
START_NEW_GAME: "start-new-game",
|
|
10
|
+
MAKE_MOVE: "make-move",
|
|
11
|
+
RESIGN_GAME: "resign-game",
|
|
12
|
+
GET_BOARD_STATE: "get-board-state",
|
|
13
|
+
GET_GAME_STATUS: "get-game-status",
|
|
14
|
+
GET_WINNER: "get-winner",
|
|
15
|
+
GET_CURRENT_TURN: "get-current-turn",
|
|
16
|
+
IS_VALID_MOVE: "is-valid-move"
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/contract/read.ts
|
|
20
|
+
import { callReadOnlyFunction, uintCV } from "@stacks/transactions";
|
|
21
|
+
|
|
22
|
+
// src/utils/network.ts
|
|
23
|
+
import { StacksMainnet, StacksTestnet } from "@stacks/network";
|
|
24
|
+
function getStacksNetwork(network) {
|
|
25
|
+
return network === "mainnet" ? new StacksMainnet() : new StacksTestnet();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/utils/cv.ts
|
|
29
|
+
function parseBoardCV(cv) {
|
|
30
|
+
const rows = cv;
|
|
31
|
+
const board = [
|
|
32
|
+
[null, null, null],
|
|
33
|
+
[null, null, null],
|
|
34
|
+
[null, null, null]
|
|
35
|
+
];
|
|
36
|
+
for (let i = 0; i < 3; i++) {
|
|
37
|
+
const row = rows.list[i].list;
|
|
38
|
+
for (let j = 0; j < 3; j++) {
|
|
39
|
+
const cell = row[j];
|
|
40
|
+
if (cell.type === "some") {
|
|
41
|
+
board[i][j] = cell.value.value;
|
|
42
|
+
} else {
|
|
43
|
+
board[i][j] = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return board;
|
|
48
|
+
}
|
|
49
|
+
function parseGameStatusCV(cv) {
|
|
50
|
+
const value = cv.value;
|
|
51
|
+
if (value === "active") return "active";
|
|
52
|
+
if (value === "finished") return "finished";
|
|
53
|
+
if (value === "not-started") return "not-started";
|
|
54
|
+
throw new Error(`Invalid game status: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
function parseWinnerCV(cv) {
|
|
57
|
+
const value = cv.value;
|
|
58
|
+
if (value === "player") return "player";
|
|
59
|
+
if (value === "ai") return "ai";
|
|
60
|
+
if (value === "draw") return "draw";
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function parseTurnCV(cv) {
|
|
64
|
+
const value = cv.value;
|
|
65
|
+
if (value === "player") return "player";
|
|
66
|
+
if (value === "ai") return "ai";
|
|
67
|
+
throw new Error(`Invalid turn: ${value}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/contract/read.ts
|
|
71
|
+
async function getBoardState(config) {
|
|
72
|
+
const network = getStacksNetwork(config.network);
|
|
73
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
74
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
75
|
+
const cv = await callReadOnlyFunction({
|
|
76
|
+
network,
|
|
77
|
+
contractAddress,
|
|
78
|
+
contractName,
|
|
79
|
+
functionName: CONTRACT_FUNCTIONS.GET_BOARD_STATE,
|
|
80
|
+
functionArgs: [],
|
|
81
|
+
senderAddress: contractAddress
|
|
82
|
+
// arbitrary
|
|
83
|
+
});
|
|
84
|
+
return parseBoardCV(cv);
|
|
85
|
+
}
|
|
86
|
+
async function getGameStatus(config) {
|
|
87
|
+
const network = getStacksNetwork(config.network);
|
|
88
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
89
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
90
|
+
const cv = await callReadOnlyFunction({
|
|
91
|
+
network,
|
|
92
|
+
contractAddress,
|
|
93
|
+
contractName,
|
|
94
|
+
functionName: CONTRACT_FUNCTIONS.GET_GAME_STATUS,
|
|
95
|
+
functionArgs: [],
|
|
96
|
+
senderAddress: contractAddress
|
|
97
|
+
});
|
|
98
|
+
return parseGameStatusCV(cv);
|
|
99
|
+
}
|
|
100
|
+
async function getWinner(config) {
|
|
101
|
+
const network = getStacksNetwork(config.network);
|
|
102
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
103
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
104
|
+
const cv = await callReadOnlyFunction({
|
|
105
|
+
network,
|
|
106
|
+
contractAddress,
|
|
107
|
+
contractName,
|
|
108
|
+
functionName: CONTRACT_FUNCTIONS.GET_WINNER,
|
|
109
|
+
functionArgs: [],
|
|
110
|
+
senderAddress: contractAddress
|
|
111
|
+
});
|
|
112
|
+
return parseWinnerCV(cv);
|
|
113
|
+
}
|
|
114
|
+
async function getCurrentTurn(config) {
|
|
115
|
+
const network = getStacksNetwork(config.network);
|
|
116
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
117
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
118
|
+
const cv = await callReadOnlyFunction({
|
|
119
|
+
network,
|
|
120
|
+
contractAddress,
|
|
121
|
+
contractName,
|
|
122
|
+
functionName: CONTRACT_FUNCTIONS.GET_CURRENT_TURN,
|
|
123
|
+
functionArgs: [],
|
|
124
|
+
senderAddress: contractAddress
|
|
125
|
+
});
|
|
126
|
+
return parseTurnCV(cv);
|
|
127
|
+
}
|
|
128
|
+
async function isValidMove(config, row, col) {
|
|
129
|
+
const network = getStacksNetwork(config.network);
|
|
130
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
131
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
132
|
+
const cv = await callReadOnlyFunction({
|
|
133
|
+
network,
|
|
134
|
+
contractAddress,
|
|
135
|
+
contractName,
|
|
136
|
+
functionName: CONTRACT_FUNCTIONS.IS_VALID_MOVE,
|
|
137
|
+
functionArgs: [uintCV(row), uintCV(col)],
|
|
138
|
+
senderAddress: contractAddress
|
|
139
|
+
});
|
|
140
|
+
return cv.value;
|
|
141
|
+
}
|
|
142
|
+
async function getFullGameState(config) {
|
|
143
|
+
const [board, status, winner, currentTurn] = await Promise.all([
|
|
144
|
+
getBoardState(config),
|
|
145
|
+
getGameStatus(config),
|
|
146
|
+
getWinner(config),
|
|
147
|
+
getCurrentTurn(config)
|
|
148
|
+
]);
|
|
149
|
+
return { board, status, winner, currentTurn };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/contract/write.ts
|
|
153
|
+
import { makeContractCall, broadcastTransaction, uintCV as uintCV2 } from "@stacks/transactions";
|
|
154
|
+
async function startNewGame(config) {
|
|
155
|
+
if (!config.senderKey || !config.senderAddress) {
|
|
156
|
+
throw new Error("senderKey and senderAddress are required for write operations");
|
|
157
|
+
}
|
|
158
|
+
const network = getStacksNetwork(config.network);
|
|
159
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
160
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
161
|
+
const tx = await makeContractCall({
|
|
162
|
+
network,
|
|
163
|
+
contractAddress,
|
|
164
|
+
contractName,
|
|
165
|
+
functionName: CONTRACT_FUNCTIONS.START_NEW_GAME,
|
|
166
|
+
functionArgs: [],
|
|
167
|
+
senderKey: config.senderKey,
|
|
168
|
+
anchorMode: "any"
|
|
169
|
+
});
|
|
170
|
+
const broadcastResponse = await broadcastTransaction(tx, network);
|
|
171
|
+
return { txId: broadcastResponse.txid };
|
|
172
|
+
}
|
|
173
|
+
async function makeMove(config, row, col) {
|
|
174
|
+
if (!config.senderKey || !config.senderAddress) {
|
|
175
|
+
throw new Error("senderKey and senderAddress are required for write operations");
|
|
176
|
+
}
|
|
177
|
+
const network = getStacksNetwork(config.network);
|
|
178
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
179
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
180
|
+
const tx = await makeContractCall({
|
|
181
|
+
network,
|
|
182
|
+
contractAddress,
|
|
183
|
+
contractName,
|
|
184
|
+
functionName: CONTRACT_FUNCTIONS.MAKE_MOVE,
|
|
185
|
+
functionArgs: [uintCV2(row), uintCV2(col)],
|
|
186
|
+
senderKey: config.senderKey,
|
|
187
|
+
anchorMode: "any"
|
|
188
|
+
});
|
|
189
|
+
const broadcastResponse = await broadcastTransaction(tx, network);
|
|
190
|
+
return { txId: broadcastResponse.txid };
|
|
191
|
+
}
|
|
192
|
+
async function resignGame(config) {
|
|
193
|
+
if (!config.senderKey || !config.senderAddress) {
|
|
194
|
+
throw new Error("senderKey and senderAddress are required for write operations");
|
|
195
|
+
}
|
|
196
|
+
const network = getStacksNetwork(config.network);
|
|
197
|
+
const contractName = config.contractName || CONTRACT_NAME;
|
|
198
|
+
const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
|
|
199
|
+
const tx = await makeContractCall({
|
|
200
|
+
network,
|
|
201
|
+
contractAddress,
|
|
202
|
+
contractName,
|
|
203
|
+
functionName: CONTRACT_FUNCTIONS.RESIGN_GAME,
|
|
204
|
+
functionArgs: [],
|
|
205
|
+
senderKey: config.senderKey,
|
|
206
|
+
anchorMode: "any"
|
|
207
|
+
});
|
|
208
|
+
const broadcastResponse = await broadcastTransaction(tx, network);
|
|
209
|
+
return { txId: broadcastResponse.txid };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/leaderboard/api.ts
|
|
213
|
+
function getBaseUrl(config) {
|
|
214
|
+
return config.leaderboardApiUrl || DEFAULT_LEADERBOARD_API;
|
|
215
|
+
}
|
|
216
|
+
async function getLeaderboard(config, month) {
|
|
217
|
+
const baseUrl = getBaseUrl(config);
|
|
218
|
+
const response = await fetch(`${baseUrl}/api/leaderboard?month=${month}`);
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
throw new Error(`Failed to fetch leaderboard: ${response.statusText}`);
|
|
221
|
+
}
|
|
222
|
+
return response.json();
|
|
223
|
+
}
|
|
224
|
+
async function submitResult(config, result) {
|
|
225
|
+
const baseUrl = getBaseUrl(config);
|
|
226
|
+
const response = await fetch(`${baseUrl}/api/leaderboard/result`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json" },
|
|
229
|
+
body: JSON.stringify(result)
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(`Failed to submit result: ${response.statusText}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function syncLeaderboard(config) {
|
|
236
|
+
const baseUrl = getBaseUrl(config);
|
|
237
|
+
const response = await fetch(`${baseUrl}/api/sync`, {
|
|
238
|
+
method: "POST"
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) {
|
|
241
|
+
throw new Error(`Failed to sync leaderboard: ${response.statusText}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function healthCheck(config) {
|
|
245
|
+
const baseUrl = getBaseUrl(config);
|
|
246
|
+
try {
|
|
247
|
+
const response = await fetch(`${baseUrl}/health`);
|
|
248
|
+
return response.ok;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function getPlayerStats(config, playerAddress, month) {
|
|
254
|
+
const currentMonth = month || (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
255
|
+
const leaderboard = await getLeaderboard(config, currentMonth);
|
|
256
|
+
const entry = leaderboard.entries.find((e) => e.player === playerAddress);
|
|
257
|
+
if (!entry) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const totalGames = entry.wins + entry.losses + entry.draws;
|
|
261
|
+
const winRate = totalGames > 0 ? entry.wins / totalGames * 100 : 0;
|
|
262
|
+
return {
|
|
263
|
+
...entry,
|
|
264
|
+
totalGames,
|
|
265
|
+
winRate: Math.round(winRate * 100) / 100
|
|
266
|
+
// Round to 2 decimal places
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/client.ts
|
|
271
|
+
var ClarityXOClient = class {
|
|
272
|
+
constructor(config) {
|
|
273
|
+
this.config = config;
|
|
274
|
+
}
|
|
275
|
+
config;
|
|
276
|
+
// Game state
|
|
277
|
+
getBoardState() {
|
|
278
|
+
return getBoardState(this.config);
|
|
279
|
+
}
|
|
280
|
+
getGameStatus() {
|
|
281
|
+
return getGameStatus(this.config);
|
|
282
|
+
}
|
|
283
|
+
getWinner() {
|
|
284
|
+
return getWinner(this.config);
|
|
285
|
+
}
|
|
286
|
+
getCurrentTurn() {
|
|
287
|
+
return getCurrentTurn(this.config);
|
|
288
|
+
}
|
|
289
|
+
isValidMove(row, col) {
|
|
290
|
+
return isValidMove(this.config, row, col);
|
|
291
|
+
}
|
|
292
|
+
getFullGameState() {
|
|
293
|
+
return getFullGameState(this.config);
|
|
294
|
+
}
|
|
295
|
+
// Transactions
|
|
296
|
+
startNewGame() {
|
|
297
|
+
return startNewGame(this.config);
|
|
298
|
+
}
|
|
299
|
+
makeMove(row, col) {
|
|
300
|
+
return makeMove(this.config, row, col);
|
|
301
|
+
}
|
|
302
|
+
resignGame() {
|
|
303
|
+
return resignGame(this.config);
|
|
304
|
+
}
|
|
305
|
+
// Leaderboard
|
|
306
|
+
getLeaderboard(month) {
|
|
307
|
+
return getLeaderboard(this.config, month);
|
|
308
|
+
}
|
|
309
|
+
submitResult(result) {
|
|
310
|
+
return submitResult(this.config, result);
|
|
311
|
+
}
|
|
312
|
+
syncLeaderboard() {
|
|
313
|
+
return syncLeaderboard(this.config);
|
|
314
|
+
}
|
|
315
|
+
healthCheck() {
|
|
316
|
+
return healthCheck(this.config);
|
|
317
|
+
}
|
|
318
|
+
getPlayerStats(playerAddress, month) {
|
|
319
|
+
return getPlayerStats(this.config, playerAddress, month);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
function createClient(config) {
|
|
323
|
+
return new ClarityXOClient(config);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export {
|
|
327
|
+
CONTRACT_NAME,
|
|
328
|
+
CONTRACT_ADDRESS,
|
|
329
|
+
MAINNET_API,
|
|
330
|
+
TESTNET_API,
|
|
331
|
+
DEFAULT_LEADERBOARD_API,
|
|
332
|
+
DEFAULT_NETWORK,
|
|
333
|
+
CONTRACT_FUNCTIONS,
|
|
334
|
+
getBoardState,
|
|
335
|
+
getGameStatus,
|
|
336
|
+
getWinner,
|
|
337
|
+
getCurrentTurn,
|
|
338
|
+
isValidMove,
|
|
339
|
+
getFullGameState,
|
|
340
|
+
startNewGame,
|
|
341
|
+
makeMove,
|
|
342
|
+
resignGame,
|
|
343
|
+
getLeaderboard,
|
|
344
|
+
submitResult,
|
|
345
|
+
syncLeaderboard,
|
|
346
|
+
healthCheck,
|
|
347
|
+
getPlayerStats,
|
|
348
|
+
ClarityXOClient,
|
|
349
|
+
createClient
|
|
350
|
+
};
|