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 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
+ };