clarityxo-sdk 0.1.0 → 0.2.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
@@ -0,0 +1,348 @@
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 CONTRACT_FUNCTIONS = {
8
+ START_NEW_GAME: "start-new-game",
9
+ MAKE_MOVE: "make-move",
10
+ RESIGN_GAME: "resign-game",
11
+ GET_BOARD_STATE: "get-board-state",
12
+ GET_GAME_STATUS: "get-game-status",
13
+ GET_WINNER: "get-winner",
14
+ GET_CURRENT_TURN: "get-current-turn",
15
+ IS_VALID_MOVE: "is-valid-move"
16
+ };
17
+
18
+ // src/contract/read.ts
19
+ import { callReadOnlyFunction, uintCV } from "@stacks/transactions";
20
+
21
+ // src/utils/network.ts
22
+ import { StacksMainnet, StacksTestnet } from "@stacks/network";
23
+ function getStacksNetwork(network) {
24
+ return network === "mainnet" ? new StacksMainnet() : new StacksTestnet();
25
+ }
26
+
27
+ // src/utils/cv.ts
28
+ function parseBoardCV(cv) {
29
+ const rows = cv;
30
+ const board = [
31
+ [null, null, null],
32
+ [null, null, null],
33
+ [null, null, null]
34
+ ];
35
+ for (let i = 0; i < 3; i++) {
36
+ const row = rows.list[i].list;
37
+ for (let j = 0; j < 3; j++) {
38
+ const cell = row[j];
39
+ if (cell.type === "some") {
40
+ board[i][j] = cell.value.value;
41
+ } else {
42
+ board[i][j] = null;
43
+ }
44
+ }
45
+ }
46
+ return board;
47
+ }
48
+ function parseGameStatusCV(cv) {
49
+ const value = cv.value;
50
+ if (value === "active") return "active";
51
+ if (value === "finished") return "finished";
52
+ if (value === "not-started") return "not-started";
53
+ throw new Error(`Invalid game status: ${value}`);
54
+ }
55
+ function parseWinnerCV(cv) {
56
+ const value = cv.value;
57
+ if (value === "player") return "player";
58
+ if (value === "ai") return "ai";
59
+ if (value === "draw") return "draw";
60
+ return null;
61
+ }
62
+ function parseTurnCV(cv) {
63
+ const value = cv.value;
64
+ if (value === "player") return "player";
65
+ if (value === "ai") return "ai";
66
+ throw new Error(`Invalid turn: ${value}`);
67
+ }
68
+
69
+ // src/contract/read.ts
70
+ async function getBoardState(config) {
71
+ const network = getStacksNetwork(config.network);
72
+ const contractName = config.contractName || CONTRACT_NAME;
73
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
74
+ const cv = await callReadOnlyFunction({
75
+ network,
76
+ contractAddress,
77
+ contractName,
78
+ functionName: CONTRACT_FUNCTIONS.GET_BOARD_STATE,
79
+ functionArgs: [],
80
+ senderAddress: contractAddress
81
+ // arbitrary
82
+ });
83
+ return parseBoardCV(cv);
84
+ }
85
+ async function getGameStatus(config) {
86
+ const network = getStacksNetwork(config.network);
87
+ const contractName = config.contractName || CONTRACT_NAME;
88
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
89
+ const cv = await callReadOnlyFunction({
90
+ network,
91
+ contractAddress,
92
+ contractName,
93
+ functionName: CONTRACT_FUNCTIONS.GET_GAME_STATUS,
94
+ functionArgs: [],
95
+ senderAddress: contractAddress
96
+ });
97
+ return parseGameStatusCV(cv);
98
+ }
99
+ async function getWinner(config) {
100
+ const network = getStacksNetwork(config.network);
101
+ const contractName = config.contractName || CONTRACT_NAME;
102
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
103
+ const cv = await callReadOnlyFunction({
104
+ network,
105
+ contractAddress,
106
+ contractName,
107
+ functionName: CONTRACT_FUNCTIONS.GET_WINNER,
108
+ functionArgs: [],
109
+ senderAddress: contractAddress
110
+ });
111
+ return parseWinnerCV(cv);
112
+ }
113
+ async function getCurrentTurn(config) {
114
+ const network = getStacksNetwork(config.network);
115
+ const contractName = config.contractName || CONTRACT_NAME;
116
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
117
+ const cv = await callReadOnlyFunction({
118
+ network,
119
+ contractAddress,
120
+ contractName,
121
+ functionName: CONTRACT_FUNCTIONS.GET_CURRENT_TURN,
122
+ functionArgs: [],
123
+ senderAddress: contractAddress
124
+ });
125
+ return parseTurnCV(cv);
126
+ }
127
+ async function isValidMove(config, row, col) {
128
+ const network = getStacksNetwork(config.network);
129
+ const contractName = config.contractName || CONTRACT_NAME;
130
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
131
+ const cv = await callReadOnlyFunction({
132
+ network,
133
+ contractAddress,
134
+ contractName,
135
+ functionName: CONTRACT_FUNCTIONS.IS_VALID_MOVE,
136
+ functionArgs: [uintCV(row), uintCV(col)],
137
+ senderAddress: contractAddress
138
+ });
139
+ return cv.value;
140
+ }
141
+ async function getFullGameState(config) {
142
+ const [board, status, winner, currentTurn] = await Promise.all([
143
+ getBoardState(config),
144
+ getGameStatus(config),
145
+ getWinner(config),
146
+ getCurrentTurn(config)
147
+ ]);
148
+ return { board, status, winner, currentTurn };
149
+ }
150
+
151
+ // src/contract/write.ts
152
+ import { makeContractCall, broadcastTransaction, uintCV as uintCV2 } from "@stacks/transactions";
153
+ async function startNewGame(config) {
154
+ if (!config.senderKey || !config.senderAddress) {
155
+ throw new Error("senderKey and senderAddress are required for write operations");
156
+ }
157
+ const network = getStacksNetwork(config.network);
158
+ const contractName = config.contractName || CONTRACT_NAME;
159
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
160
+ const tx = await makeContractCall({
161
+ network,
162
+ contractAddress,
163
+ contractName,
164
+ functionName: CONTRACT_FUNCTIONS.START_NEW_GAME,
165
+ functionArgs: [],
166
+ senderKey: config.senderKey,
167
+ anchorMode: "any"
168
+ });
169
+ const broadcastResponse = await broadcastTransaction(tx, network);
170
+ return { txId: broadcastResponse.txid };
171
+ }
172
+ async function makeMove(config, row, col) {
173
+ if (!config.senderKey || !config.senderAddress) {
174
+ throw new Error("senderKey and senderAddress are required for write operations");
175
+ }
176
+ const network = getStacksNetwork(config.network);
177
+ const contractName = config.contractName || CONTRACT_NAME;
178
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
179
+ const tx = await makeContractCall({
180
+ network,
181
+ contractAddress,
182
+ contractName,
183
+ functionName: CONTRACT_FUNCTIONS.MAKE_MOVE,
184
+ functionArgs: [uintCV2(row), uintCV2(col)],
185
+ senderKey: config.senderKey,
186
+ anchorMode: "any"
187
+ });
188
+ const broadcastResponse = await broadcastTransaction(tx, network);
189
+ return { txId: broadcastResponse.txid };
190
+ }
191
+ async function resignGame(config) {
192
+ if (!config.senderKey || !config.senderAddress) {
193
+ throw new Error("senderKey and senderAddress are required for write operations");
194
+ }
195
+ const network = getStacksNetwork(config.network);
196
+ const contractName = config.contractName || CONTRACT_NAME;
197
+ const contractAddress = config.contractAddress || CONTRACT_ADDRESS;
198
+ const tx = await makeContractCall({
199
+ network,
200
+ contractAddress,
201
+ contractName,
202
+ functionName: CONTRACT_FUNCTIONS.RESIGN_GAME,
203
+ functionArgs: [],
204
+ senderKey: config.senderKey,
205
+ anchorMode: "any"
206
+ });
207
+ const broadcastResponse = await broadcastTransaction(tx, network);
208
+ return { txId: broadcastResponse.txid };
209
+ }
210
+
211
+ // src/leaderboard/api.ts
212
+ function getBaseUrl(config) {
213
+ return config.leaderboardApiUrl || DEFAULT_LEADERBOARD_API;
214
+ }
215
+ async function getLeaderboard(config, month) {
216
+ const baseUrl = getBaseUrl(config);
217
+ const response = await fetch(`${baseUrl}/api/leaderboard?month=${month}`);
218
+ if (!response.ok) {
219
+ throw new Error(`Failed to fetch leaderboard: ${response.statusText}`);
220
+ }
221
+ return response.json();
222
+ }
223
+ async function submitResult(config, result) {
224
+ const baseUrl = getBaseUrl(config);
225
+ const response = await fetch(`${baseUrl}/api/leaderboard/result`, {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/json" },
228
+ body: JSON.stringify(result)
229
+ });
230
+ if (!response.ok) {
231
+ throw new Error(`Failed to submit result: ${response.statusText}`);
232
+ }
233
+ }
234
+ async function syncLeaderboard(config) {
235
+ const baseUrl = getBaseUrl(config);
236
+ const response = await fetch(`${baseUrl}/api/sync`, {
237
+ method: "POST"
238
+ });
239
+ if (!response.ok) {
240
+ throw new Error(`Failed to sync leaderboard: ${response.statusText}`);
241
+ }
242
+ }
243
+ async function healthCheck(config) {
244
+ const baseUrl = getBaseUrl(config);
245
+ try {
246
+ const response = await fetch(`${baseUrl}/health`);
247
+ return response.ok;
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+ async function getPlayerStats(config, playerAddress, month) {
253
+ const currentMonth = month || (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
254
+ const leaderboard = await getLeaderboard(config, currentMonth);
255
+ const entry = leaderboard.entries.find((e) => e.player === playerAddress);
256
+ if (!entry) {
257
+ return null;
258
+ }
259
+ const totalGames = entry.wins + entry.losses + entry.draws;
260
+ const winRate = totalGames > 0 ? entry.wins / totalGames * 100 : 0;
261
+ return {
262
+ ...entry,
263
+ totalGames,
264
+ winRate: Math.round(winRate * 100) / 100
265
+ // Round to 2 decimal places
266
+ };
267
+ }
268
+
269
+ // src/client.ts
270
+ var ClarityXOClient = class {
271
+ constructor(config) {
272
+ this.config = config;
273
+ }
274
+ config;
275
+ // Game state
276
+ getBoardState() {
277
+ return getBoardState(this.config);
278
+ }
279
+ getGameStatus() {
280
+ return getGameStatus(this.config);
281
+ }
282
+ getWinner() {
283
+ return getWinner(this.config);
284
+ }
285
+ getCurrentTurn() {
286
+ return getCurrentTurn(this.config);
287
+ }
288
+ isValidMove(row, col) {
289
+ return isValidMove(this.config, row, col);
290
+ }
291
+ getFullGameState() {
292
+ return getFullGameState(this.config);
293
+ }
294
+ // Transactions
295
+ startNewGame() {
296
+ return startNewGame(this.config);
297
+ }
298
+ makeMove(row, col) {
299
+ return makeMove(this.config, row, col);
300
+ }
301
+ resignGame() {
302
+ return resignGame(this.config);
303
+ }
304
+ // Leaderboard
305
+ getLeaderboard(month) {
306
+ return getLeaderboard(this.config, month);
307
+ }
308
+ submitResult(result) {
309
+ return submitResult(this.config, result);
310
+ }
311
+ syncLeaderboard() {
312
+ return syncLeaderboard(this.config);
313
+ }
314
+ healthCheck() {
315
+ return healthCheck(this.config);
316
+ }
317
+ getPlayerStats(playerAddress, month) {
318
+ return getPlayerStats(this.config, playerAddress, month);
319
+ }
320
+ };
321
+ function createClient(config) {
322
+ return new ClarityXOClient(config);
323
+ }
324
+
325
+ export {
326
+ CONTRACT_NAME,
327
+ CONTRACT_ADDRESS,
328
+ MAINNET_API,
329
+ TESTNET_API,
330
+ DEFAULT_LEADERBOARD_API,
331
+ CONTRACT_FUNCTIONS,
332
+ getBoardState,
333
+ getGameStatus,
334
+ getWinner,
335
+ getCurrentTurn,
336
+ isValidMove,
337
+ getFullGameState,
338
+ startNewGame,
339
+ makeMove,
340
+ resignGame,
341
+ getLeaderboard,
342
+ submitResult,
343
+ syncLeaderboard,
344
+ healthCheck,
345
+ getPlayerStats,
346
+ ClarityXOClient,
347
+ createClient
348
+ };
package/dist/cli/index.js CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli/index.ts
27
- var import_commander5 = require("commander");
27
+ var import_commander8 = require("commander");
28
28
 
29
29
  // src/cli/commands/board.ts
30
30
  var import_commander = require("commander");
@@ -280,6 +280,22 @@ async function healthCheck(config) {
280
280
  return false;
281
281
  }
282
282
  }
283
+ async function getPlayerStats(config, playerAddress, month) {
284
+ const currentMonth = month || (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
285
+ const leaderboard = await getLeaderboard(config, currentMonth);
286
+ const entry = leaderboard.entries.find((e) => e.player === playerAddress);
287
+ if (!entry) {
288
+ return null;
289
+ }
290
+ const totalGames = entry.wins + entry.losses + entry.draws;
291
+ const winRate = totalGames > 0 ? entry.wins / totalGames * 100 : 0;
292
+ return {
293
+ ...entry,
294
+ totalGames,
295
+ winRate: Math.round(winRate * 100) / 100
296
+ // Round to 2 decimal places
297
+ };
298
+ }
283
299
 
284
300
  // src/client.ts
285
301
  var ClarityXOClient = class {
@@ -329,14 +345,53 @@ var ClarityXOClient = class {
329
345
  healthCheck() {
330
346
  return healthCheck(this.config);
331
347
  }
348
+ getPlayerStats(playerAddress, month) {
349
+ return getPlayerStats(this.config, playerAddress, month);
350
+ }
332
351
  };
333
352
  function createClient(config) {
334
353
  return new ClarityXOClient(config);
335
354
  }
336
355
 
356
+ // src/utils/validation.ts
357
+ function isValidStacksAddress(address) {
358
+ return /^S[TP][0-9A-Z]{38}$/.test(address);
359
+ }
360
+ function validateContractAddress(address) {
361
+ if (!address) {
362
+ throw new Error("Contract address is required");
363
+ }
364
+ if (!isValidStacksAddress(address)) {
365
+ throw new Error("Invalid Stacks address format");
366
+ }
367
+ }
368
+ function validateNetwork(network) {
369
+ if (network !== "mainnet" && network !== "testnet") {
370
+ throw new Error('Network must be either "mainnet" or "testnet"');
371
+ }
372
+ }
373
+ function validateWriteConfig(key, address) {
374
+ if (!key) {
375
+ throw new Error("--key (sender private key) is required for write operations");
376
+ }
377
+ if (!address) {
378
+ throw new Error("--address (sender address) is required for write operations");
379
+ }
380
+ if (!isValidStacksAddress(address)) {
381
+ throw new Error("Invalid sender address format");
382
+ }
383
+ }
384
+
337
385
  // src/cli/commands/board.ts
338
386
  var command = new import_commander.Command("board");
339
387
  command.description("Display the current game board").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
388
+ try {
389
+ validateNetwork(options.network);
390
+ validateContractAddress(options.contract);
391
+ } catch (error) {
392
+ console.error(import_chalk.default.red(`Error: ${error.message}`));
393
+ process.exit(1);
394
+ }
340
395
  const config = {
341
396
  network: options.network,
342
397
  contractAddress: options.contract,
@@ -384,6 +439,13 @@ var import_chalk2 = __toESM(require("chalk"));
384
439
  var import_ora2 = __toESM(require("ora"));
385
440
  var command2 = new import_commander2.Command("status");
386
441
  command2.description("Display the current game status and turn").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
442
+ try {
443
+ validateNetwork(options.network);
444
+ validateContractAddress(options.contract);
445
+ } catch (error) {
446
+ console.error(import_chalk2.default.red(`Error: ${error.message}`));
447
+ process.exit(1);
448
+ }
387
449
  const config = {
388
450
  network: options.network,
389
451
  contractAddress: options.contract,
@@ -413,6 +475,13 @@ var import_chalk3 = __toESM(require("chalk"));
413
475
  var import_ora3 = __toESM(require("ora"));
414
476
  var command3 = new import_commander3.Command("winner");
415
477
  command3.description("Display the game winner").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
478
+ try {
479
+ validateNetwork(options.network);
480
+ validateContractAddress(options.contract);
481
+ } catch (error) {
482
+ console.error(import_chalk3.default.red(`Error: ${error.message}`));
483
+ process.exit(1);
484
+ }
416
485
  const config = {
417
486
  network: options.network,
418
487
  contractAddress: options.contract,
@@ -443,6 +512,12 @@ var import_chalk4 = __toESM(require("chalk"));
443
512
  var import_ora4 = __toESM(require("ora"));
444
513
  var command4 = new import_commander4.Command("leaderboard");
445
514
  command4.description("Display the leaderboard for a given month").option("--month <yyyy-mm>", "Month (YYYY-MM)", (/* @__PURE__ */ new Date()).toISOString().slice(0, 7)).option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
515
+ try {
516
+ validateNetwork(options.network);
517
+ } catch (error) {
518
+ console.error(import_chalk4.default.red(`Error: ${error.message}`));
519
+ process.exit(1);
520
+ }
446
521
  const config = {
447
522
  network: options.network,
448
523
  contractAddress: options.contract,
@@ -470,13 +545,123 @@ command4.description("Display the leaderboard for a given month").option("--mont
470
545
  });
471
546
  var leaderboard_default = command4;
472
547
 
548
+ // src/cli/commands/move.ts
549
+ var import_commander5 = require("commander");
550
+ var import_chalk5 = __toESM(require("chalk"));
551
+ var import_ora5 = __toESM(require("ora"));
552
+ var command5 = new import_commander5.Command("move");
553
+ command5.description("Make a move on the board").argument("<row>", "Row (0-2)").argument("<col>", "Column (0-2)").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").option("--key <key>", "Sender private key (required)").option("--address <address>", "Sender address (required)").action(async (row, col, options) => {
554
+ try {
555
+ validateNetwork(options.network);
556
+ const rowNum2 = parseInt(row, 10);
557
+ const colNum2 = parseInt(col, 10);
558
+ if (isNaN(rowNum2) || isNaN(colNum2) || rowNum2 < 0 || rowNum2 > 2 || colNum2 < 0 || colNum2 > 2) {
559
+ throw new Error("Row and column must be integers between 0 and 2");
560
+ }
561
+ validateWriteConfig(options.key, options.address);
562
+ } catch (error) {
563
+ console.error(import_chalk5.default.red(`Error: ${error.message}`));
564
+ process.exit(1);
565
+ }
566
+ const config = {
567
+ network: options.network,
568
+ contractAddress: options.contract,
569
+ leaderboardApiUrl: options.api,
570
+ senderKey: options.key,
571
+ senderAddress: options.address
572
+ };
573
+ const spinner = (0, import_ora5.default)("Making move...").start();
574
+ try {
575
+ const client = createClient(config);
576
+ const tx = await client.makeMove(rowNum, colNum);
577
+ spinner.stop();
578
+ console.log(import_chalk5.default.green(`Move successful! Transaction ID: ${tx.txId}`));
579
+ } catch (error) {
580
+ spinner.stop();
581
+ console.error(import_chalk5.default.red(`Error: ${error.message}`));
582
+ process.exit(1);
583
+ }
584
+ });
585
+ var move_default = command5;
586
+
587
+ // src/cli/commands/start.ts
588
+ var import_commander6 = require("commander");
589
+ var import_chalk6 = __toESM(require("chalk"));
590
+ var import_ora6 = __toESM(require("ora"));
591
+ var command6 = new import_commander6.Command("start");
592
+ command6.description("Start a new game").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").option("--key <key>", "Sender private key (required)").option("--address <address>", "Sender address (required)").action(async (options) => {
593
+ try {
594
+ validateNetwork(options.network);
595
+ validateWriteConfig(options.key, options.address);
596
+ } catch (error) {
597
+ console.error(import_chalk6.default.red(`Error: ${error.message}`));
598
+ process.exit(1);
599
+ }
600
+ const config = {
601
+ network: options.network,
602
+ contractAddress: options.contract,
603
+ leaderboardApiUrl: options.api,
604
+ senderKey: options.key,
605
+ senderAddress: options.address
606
+ };
607
+ const spinner = (0, import_ora6.default)("Starting new game...").start();
608
+ try {
609
+ const client = createClient(config);
610
+ const tx = await client.startNewGame();
611
+ spinner.stop();
612
+ console.log(import_chalk6.default.green(`New game started! Transaction ID: ${tx.txId}`));
613
+ } catch (error) {
614
+ spinner.stop();
615
+ console.error(import_chalk6.default.red(`Error: ${error.message}`));
616
+ process.exit(1);
617
+ }
618
+ });
619
+ var start_default = command6;
620
+
621
+ // src/cli/commands/resign.ts
622
+ var import_commander7 = require("commander");
623
+ var import_chalk7 = __toESM(require("chalk"));
624
+ var import_ora7 = __toESM(require("ora"));
625
+ var command7 = new import_commander7.Command("resign");
626
+ command7.description("Resign the current game").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").option("--key <key>", "Sender private key (required)").option("--address <address>", "Sender address (required)").action(async (options) => {
627
+ try {
628
+ validateNetwork(options.network);
629
+ validateWriteConfig(options.key, options.address);
630
+ } catch (error) {
631
+ console.error(import_chalk7.default.red(`Error: ${error.message}`));
632
+ process.exit(1);
633
+ }
634
+ const config = {
635
+ network: options.network,
636
+ contractAddress: options.contract,
637
+ leaderboardApiUrl: options.api,
638
+ senderKey: options.key,
639
+ senderAddress: options.address
640
+ };
641
+ const spinner = (0, import_ora7.default)("Resigning game...").start();
642
+ try {
643
+ const client = createClient(config);
644
+ const tx = await client.resignGame();
645
+ spinner.stop();
646
+ console.log(import_chalk7.default.green(`Game resigned! Transaction ID: ${tx.txId}`));
647
+ } catch (error) {
648
+ spinner.stop();
649
+ console.error(import_chalk7.default.red(`Error: ${error.message}`));
650
+ process.exit(1);
651
+ }
652
+ });
653
+ var resign_default = command7;
654
+
473
655
  // src/cli/index.ts
474
- var packageJson = { version: "0.1.0" };
475
- var program = new import_commander5.Command();
656
+ var packageJson = { version: "0.2.0" };
657
+ var program = new import_commander8.Command();
476
658
  program.name("clarityxo").description("CLI for ClarityXO Tic-Tac-Toe on Stacks blockchain").version(packageJson.version);
477
659
  program.option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL");
478
660
  program.addCommand(board_default);
479
661
  program.addCommand(status_default);
480
662
  program.addCommand(winner_default);
481
663
  program.addCommand(leaderboard_default);
664
+ program.addCommand(move_default);
665
+ program.addCommand(start_default);
666
+ program.addCommand(resign_default);
482
667
  program.parse();
@@ -1,17 +1,55 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createClient
4
- } from "../chunk-Z5L3H7VV.mjs";
4
+ } from "../chunk-ETEDBK3G.mjs";
5
5
 
6
6
  // src/cli/index.ts
7
- import { Command as Command5 } from "commander";
7
+ import { Command as Command8 } from "commander";
8
8
 
9
9
  // src/cli/commands/board.ts
10
10
  import { Command } from "commander";
11
11
  import chalk from "chalk";
12
12
  import ora from "ora";
13
+
14
+ // src/utils/validation.ts
15
+ function isValidStacksAddress(address) {
16
+ return /^S[TP][0-9A-Z]{38}$/.test(address);
17
+ }
18
+ function validateContractAddress(address) {
19
+ if (!address) {
20
+ throw new Error("Contract address is required");
21
+ }
22
+ if (!isValidStacksAddress(address)) {
23
+ throw new Error("Invalid Stacks address format");
24
+ }
25
+ }
26
+ function validateNetwork(network) {
27
+ if (network !== "mainnet" && network !== "testnet") {
28
+ throw new Error('Network must be either "mainnet" or "testnet"');
29
+ }
30
+ }
31
+ function validateWriteConfig(key, address) {
32
+ if (!key) {
33
+ throw new Error("--key (sender private key) is required for write operations");
34
+ }
35
+ if (!address) {
36
+ throw new Error("--address (sender address) is required for write operations");
37
+ }
38
+ if (!isValidStacksAddress(address)) {
39
+ throw new Error("Invalid sender address format");
40
+ }
41
+ }
42
+
43
+ // src/cli/commands/board.ts
13
44
  var command = new Command("board");
14
45
  command.description("Display the current game board").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
46
+ try {
47
+ validateNetwork(options.network);
48
+ validateContractAddress(options.contract);
49
+ } catch (error) {
50
+ console.error(chalk.red(`Error: ${error.message}`));
51
+ process.exit(1);
52
+ }
15
53
  const config = {
16
54
  network: options.network,
17
55
  contractAddress: options.contract,
@@ -59,6 +97,13 @@ import chalk2 from "chalk";
59
97
  import ora2 from "ora";
60
98
  var command2 = new Command2("status");
61
99
  command2.description("Display the current game status and turn").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
100
+ try {
101
+ validateNetwork(options.network);
102
+ validateContractAddress(options.contract);
103
+ } catch (error) {
104
+ console.error(chalk2.red(`Error: ${error.message}`));
105
+ process.exit(1);
106
+ }
62
107
  const config = {
63
108
  network: options.network,
64
109
  contractAddress: options.contract,
@@ -88,6 +133,13 @@ import chalk3 from "chalk";
88
133
  import ora3 from "ora";
89
134
  var command3 = new Command3("winner");
90
135
  command3.description("Display the game winner").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
136
+ try {
137
+ validateNetwork(options.network);
138
+ validateContractAddress(options.contract);
139
+ } catch (error) {
140
+ console.error(chalk3.red(`Error: ${error.message}`));
141
+ process.exit(1);
142
+ }
91
143
  const config = {
92
144
  network: options.network,
93
145
  contractAddress: options.contract,
@@ -118,6 +170,12 @@ import chalk4 from "chalk";
118
170
  import ora4 from "ora";
119
171
  var command4 = new Command4("leaderboard");
120
172
  command4.description("Display the leaderboard for a given month").option("--month <yyyy-mm>", "Month (YYYY-MM)", (/* @__PURE__ */ new Date()).toISOString().slice(0, 7)).option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").action(async (options) => {
173
+ try {
174
+ validateNetwork(options.network);
175
+ } catch (error) {
176
+ console.error(chalk4.red(`Error: ${error.message}`));
177
+ process.exit(1);
178
+ }
121
179
  const config = {
122
180
  network: options.network,
123
181
  contractAddress: options.contract,
@@ -145,13 +203,123 @@ command4.description("Display the leaderboard for a given month").option("--mont
145
203
  });
146
204
  var leaderboard_default = command4;
147
205
 
206
+ // src/cli/commands/move.ts
207
+ import { Command as Command5 } from "commander";
208
+ import chalk5 from "chalk";
209
+ import ora5 from "ora";
210
+ var command5 = new Command5("move");
211
+ command5.description("Make a move on the board").argument("<row>", "Row (0-2)").argument("<col>", "Column (0-2)").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").option("--key <key>", "Sender private key (required)").option("--address <address>", "Sender address (required)").action(async (row, col, options) => {
212
+ try {
213
+ validateNetwork(options.network);
214
+ const rowNum2 = parseInt(row, 10);
215
+ const colNum2 = parseInt(col, 10);
216
+ if (isNaN(rowNum2) || isNaN(colNum2) || rowNum2 < 0 || rowNum2 > 2 || colNum2 < 0 || colNum2 > 2) {
217
+ throw new Error("Row and column must be integers between 0 and 2");
218
+ }
219
+ validateWriteConfig(options.key, options.address);
220
+ } catch (error) {
221
+ console.error(chalk5.red(`Error: ${error.message}`));
222
+ process.exit(1);
223
+ }
224
+ const config = {
225
+ network: options.network,
226
+ contractAddress: options.contract,
227
+ leaderboardApiUrl: options.api,
228
+ senderKey: options.key,
229
+ senderAddress: options.address
230
+ };
231
+ const spinner = ora5("Making move...").start();
232
+ try {
233
+ const client = createClient(config);
234
+ const tx = await client.makeMove(rowNum, colNum);
235
+ spinner.stop();
236
+ console.log(chalk5.green(`Move successful! Transaction ID: ${tx.txId}`));
237
+ } catch (error) {
238
+ spinner.stop();
239
+ console.error(chalk5.red(`Error: ${error.message}`));
240
+ process.exit(1);
241
+ }
242
+ });
243
+ var move_default = command5;
244
+
245
+ // src/cli/commands/start.ts
246
+ import { Command as Command6 } from "commander";
247
+ import chalk6 from "chalk";
248
+ import ora6 from "ora";
249
+ var command6 = new Command6("start");
250
+ command6.description("Start a new game").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").option("--key <key>", "Sender private key (required)").option("--address <address>", "Sender address (required)").action(async (options) => {
251
+ try {
252
+ validateNetwork(options.network);
253
+ validateWriteConfig(options.key, options.address);
254
+ } catch (error) {
255
+ console.error(chalk6.red(`Error: ${error.message}`));
256
+ process.exit(1);
257
+ }
258
+ const config = {
259
+ network: options.network,
260
+ contractAddress: options.contract,
261
+ leaderboardApiUrl: options.api,
262
+ senderKey: options.key,
263
+ senderAddress: options.address
264
+ };
265
+ const spinner = ora6("Starting new game...").start();
266
+ try {
267
+ const client = createClient(config);
268
+ const tx = await client.startNewGame();
269
+ spinner.stop();
270
+ console.log(chalk6.green(`New game started! Transaction ID: ${tx.txId}`));
271
+ } catch (error) {
272
+ spinner.stop();
273
+ console.error(chalk6.red(`Error: ${error.message}`));
274
+ process.exit(1);
275
+ }
276
+ });
277
+ var start_default = command6;
278
+
279
+ // src/cli/commands/resign.ts
280
+ import { Command as Command7 } from "commander";
281
+ import chalk7 from "chalk";
282
+ import ora7 from "ora";
283
+ var command7 = new Command7("resign");
284
+ command7.description("Resign the current game").option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL").option("--key <key>", "Sender private key (required)").option("--address <address>", "Sender address (required)").action(async (options) => {
285
+ try {
286
+ validateNetwork(options.network);
287
+ validateWriteConfig(options.key, options.address);
288
+ } catch (error) {
289
+ console.error(chalk7.red(`Error: ${error.message}`));
290
+ process.exit(1);
291
+ }
292
+ const config = {
293
+ network: options.network,
294
+ contractAddress: options.contract,
295
+ leaderboardApiUrl: options.api,
296
+ senderKey: options.key,
297
+ senderAddress: options.address
298
+ };
299
+ const spinner = ora7("Resigning game...").start();
300
+ try {
301
+ const client = createClient(config);
302
+ const tx = await client.resignGame();
303
+ spinner.stop();
304
+ console.log(chalk7.green(`Game resigned! Transaction ID: ${tx.txId}`));
305
+ } catch (error) {
306
+ spinner.stop();
307
+ console.error(chalk7.red(`Error: ${error.message}`));
308
+ process.exit(1);
309
+ }
310
+ });
311
+ var resign_default = command7;
312
+
148
313
  // src/cli/index.ts
149
- var packageJson = { version: "0.1.0" };
150
- var program = new Command5();
314
+ var packageJson = { version: "0.2.0" };
315
+ var program = new Command8();
151
316
  program.name("clarityxo").description("CLI for ClarityXO Tic-Tac-Toe on Stacks blockchain").version(packageJson.version);
152
317
  program.option("--contract [address]", "Contract address").option("--network <network>", "Network (mainnet or testnet)", "testnet").option("--api <url>", "Leaderboard API URL");
153
318
  program.addCommand(board_default);
154
319
  program.addCommand(status_default);
155
320
  program.addCommand(winner_default);
156
321
  program.addCommand(leaderboard_default);
322
+ program.addCommand(move_default);
323
+ program.addCommand(start_default);
324
+ program.addCommand(resign_default);
157
325
  program.parse();
package/dist/index.d.mts CHANGED
@@ -33,6 +33,16 @@ interface LeaderboardEntry {
33
33
  points: number;
34
34
  rank: number;
35
35
  }
36
+ interface PlayerStats {
37
+ player: string;
38
+ wins: number;
39
+ losses: number;
40
+ draws: number;
41
+ points: number;
42
+ rank: number;
43
+ totalGames: number;
44
+ winRate: number;
45
+ }
36
46
  interface LeaderboardMonth {
37
47
  month: string;
38
48
  entries: LeaderboardEntry[];
@@ -73,6 +83,7 @@ declare class ClarityXOClient {
73
83
  submitResult(result: GameResult): Promise<void>;
74
84
  syncLeaderboard(): Promise<void>;
75
85
  healthCheck(): Promise<boolean>;
86
+ getPlayerStats(playerAddress: string, month?: string): Promise<PlayerStats | null>;
76
87
  }
77
88
  declare function createClient(config: ClarityXOConfig): ClarityXOClient;
78
89
 
@@ -113,5 +124,14 @@ declare function getLeaderboard(config: ClarityXOConfig, month: string): Promise
113
124
  declare function submitResult(config: ClarityXOConfig, result: GameResult): Promise<void>;
114
125
  declare function syncLeaderboard(config: ClarityXOConfig): Promise<void>;
115
126
  declare function healthCheck(config: ClarityXOConfig): Promise<boolean>;
127
+ declare function getPlayerStats(config: ClarityXOConfig, playerAddress: string, month?: string): Promise<PlayerStats | null>;
128
+
129
+ declare function checkWin(board: Board, player: CellValue): boolean;
130
+ declare function isBoardFull(board: Board): boolean;
131
+ declare function isDraw(board: Board): boolean;
132
+ declare function getAvailableMoves(board: Board): Array<[number, number]>;
133
+ declare function getBoardString(board: Board): string;
134
+ declare function cloneBoard(board: Board): Board;
135
+ declare function makeMoveOnBoard(board: Board, row: number, col: number, player: CellValue): Board;
116
136
 
117
- export { type Board, CONTRACT_ADDRESS, CONTRACT_FUNCTIONS, CONTRACT_NAME, type CellValue, ClarityXOClient, type ClarityXOConfig, DEFAULT_LEADERBOARD_API, type GameResult, type GameState, type GameStatus, type LeaderboardEntry, type LeaderboardMonth, MAINNET_API, type Network, TESTNET_API, type Turn, createClient, getBoardState, getCurrentTurn, getFullGameState, getGameStatus, getLeaderboard, getWinner, healthCheck, isValidMove, makeMove, resignGame, startNewGame, submitResult, syncLeaderboard };
137
+ export { type Board, CONTRACT_ADDRESS, CONTRACT_FUNCTIONS, CONTRACT_NAME, type CellValue, ClarityXOClient, type ClarityXOConfig, DEFAULT_LEADERBOARD_API, type GameResult, type GameState, type GameStatus, type LeaderboardEntry, type LeaderboardMonth, MAINNET_API, type Network, type PlayerStats, TESTNET_API, type Turn, checkWin, cloneBoard, createClient, getAvailableMoves, getBoardState, getBoardString, getCurrentTurn, getFullGameState, getGameStatus, getLeaderboard, getPlayerStats, getWinner, healthCheck, isBoardFull, isDraw, isValidMove, makeMove, makeMoveOnBoard, resignGame, startNewGame, submitResult, syncLeaderboard };
package/dist/index.d.ts CHANGED
@@ -33,6 +33,16 @@ interface LeaderboardEntry {
33
33
  points: number;
34
34
  rank: number;
35
35
  }
36
+ interface PlayerStats {
37
+ player: string;
38
+ wins: number;
39
+ losses: number;
40
+ draws: number;
41
+ points: number;
42
+ rank: number;
43
+ totalGames: number;
44
+ winRate: number;
45
+ }
36
46
  interface LeaderboardMonth {
37
47
  month: string;
38
48
  entries: LeaderboardEntry[];
@@ -73,6 +83,7 @@ declare class ClarityXOClient {
73
83
  submitResult(result: GameResult): Promise<void>;
74
84
  syncLeaderboard(): Promise<void>;
75
85
  healthCheck(): Promise<boolean>;
86
+ getPlayerStats(playerAddress: string, month?: string): Promise<PlayerStats | null>;
76
87
  }
77
88
  declare function createClient(config: ClarityXOConfig): ClarityXOClient;
78
89
 
@@ -113,5 +124,14 @@ declare function getLeaderboard(config: ClarityXOConfig, month: string): Promise
113
124
  declare function submitResult(config: ClarityXOConfig, result: GameResult): Promise<void>;
114
125
  declare function syncLeaderboard(config: ClarityXOConfig): Promise<void>;
115
126
  declare function healthCheck(config: ClarityXOConfig): Promise<boolean>;
127
+ declare function getPlayerStats(config: ClarityXOConfig, playerAddress: string, month?: string): Promise<PlayerStats | null>;
128
+
129
+ declare function checkWin(board: Board, player: CellValue): boolean;
130
+ declare function isBoardFull(board: Board): boolean;
131
+ declare function isDraw(board: Board): boolean;
132
+ declare function getAvailableMoves(board: Board): Array<[number, number]>;
133
+ declare function getBoardString(board: Board): string;
134
+ declare function cloneBoard(board: Board): Board;
135
+ declare function makeMoveOnBoard(board: Board, row: number, col: number, player: CellValue): Board;
116
136
 
117
- export { type Board, CONTRACT_ADDRESS, CONTRACT_FUNCTIONS, CONTRACT_NAME, type CellValue, ClarityXOClient, type ClarityXOConfig, DEFAULT_LEADERBOARD_API, type GameResult, type GameState, type GameStatus, type LeaderboardEntry, type LeaderboardMonth, MAINNET_API, type Network, TESTNET_API, type Turn, createClient, getBoardState, getCurrentTurn, getFullGameState, getGameStatus, getLeaderboard, getWinner, healthCheck, isValidMove, makeMove, resignGame, startNewGame, submitResult, syncLeaderboard };
137
+ export { type Board, CONTRACT_ADDRESS, CONTRACT_FUNCTIONS, CONTRACT_NAME, type CellValue, ClarityXOClient, type ClarityXOConfig, DEFAULT_LEADERBOARD_API, type GameResult, type GameState, type GameStatus, type LeaderboardEntry, type LeaderboardMonth, MAINNET_API, type Network, type PlayerStats, TESTNET_API, type Turn, checkWin, cloneBoard, createClient, getAvailableMoves, getBoardState, getBoardString, getCurrentTurn, getFullGameState, getGameStatus, getLeaderboard, getPlayerStats, getWinner, healthCheck, isBoardFull, isDraw, isValidMove, makeMove, makeMoveOnBoard, resignGame, startNewGame, submitResult, syncLeaderboard };
package/dist/index.js CHANGED
@@ -27,16 +27,24 @@ __export(index_exports, {
27
27
  DEFAULT_LEADERBOARD_API: () => DEFAULT_LEADERBOARD_API,
28
28
  MAINNET_API: () => MAINNET_API,
29
29
  TESTNET_API: () => TESTNET_API,
30
+ checkWin: () => checkWin,
31
+ cloneBoard: () => cloneBoard,
30
32
  createClient: () => createClient,
33
+ getAvailableMoves: () => getAvailableMoves,
31
34
  getBoardState: () => getBoardState,
35
+ getBoardString: () => getBoardString,
32
36
  getCurrentTurn: () => getCurrentTurn,
33
37
  getFullGameState: () => getFullGameState,
34
38
  getGameStatus: () => getGameStatus,
35
39
  getLeaderboard: () => getLeaderboard,
40
+ getPlayerStats: () => getPlayerStats,
36
41
  getWinner: () => getWinner,
37
42
  healthCheck: () => healthCheck,
43
+ isBoardFull: () => isBoardFull,
44
+ isDraw: () => isDraw,
38
45
  isValidMove: () => isValidMove,
39
46
  makeMove: () => makeMove,
47
+ makeMoveOnBoard: () => makeMoveOnBoard,
40
48
  resignGame: () => resignGame,
41
49
  startNewGame: () => startNewGame,
42
50
  submitResult: () => submitResult,
@@ -295,6 +303,22 @@ async function healthCheck(config) {
295
303
  return false;
296
304
  }
297
305
  }
306
+ async function getPlayerStats(config, playerAddress, month) {
307
+ const currentMonth = month || (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
308
+ const leaderboard = await getLeaderboard(config, currentMonth);
309
+ const entry = leaderboard.entries.find((e) => e.player === playerAddress);
310
+ if (!entry) {
311
+ return null;
312
+ }
313
+ const totalGames = entry.wins + entry.losses + entry.draws;
314
+ const winRate = totalGames > 0 ? entry.wins / totalGames * 100 : 0;
315
+ return {
316
+ ...entry,
317
+ totalGames,
318
+ winRate: Math.round(winRate * 100) / 100
319
+ // Round to 2 decimal places
320
+ };
321
+ }
298
322
 
299
323
  // src/client.ts
300
324
  var ClarityXOClient = class {
@@ -344,10 +368,66 @@ var ClarityXOClient = class {
344
368
  healthCheck() {
345
369
  return healthCheck(this.config);
346
370
  }
371
+ getPlayerStats(playerAddress, month) {
372
+ return getPlayerStats(this.config, playerAddress, month);
373
+ }
347
374
  };
348
375
  function createClient(config) {
349
376
  return new ClarityXOClient(config);
350
377
  }
378
+
379
+ // src/utils/board.ts
380
+ function checkWin(board, player) {
381
+ if (player === null) return false;
382
+ for (let row = 0; row < 3; row++) {
383
+ if (board[row][0] === player && board[row][1] === player && board[row][2] === player) {
384
+ return true;
385
+ }
386
+ }
387
+ for (let col = 0; col < 3; col++) {
388
+ if (board[0][col] === player && board[1][col] === player && board[2][col] === player) {
389
+ return true;
390
+ }
391
+ }
392
+ if (board[0][0] === player && board[1][1] === player && board[2][2] === player) {
393
+ return true;
394
+ }
395
+ if (board[0][2] === player && board[1][1] === player && board[2][0] === player) {
396
+ return true;
397
+ }
398
+ return false;
399
+ }
400
+ function isBoardFull(board) {
401
+ return board.every((row) => row.every((cell) => cell !== null));
402
+ }
403
+ function isDraw(board) {
404
+ return isBoardFull(board) && !checkWin(board, "X") && !checkWin(board, "O");
405
+ }
406
+ function getAvailableMoves(board) {
407
+ const moves = [];
408
+ for (let row = 0; row < 3; row++) {
409
+ for (let col = 0; col < 3; col++) {
410
+ if (board[row][col] === null) {
411
+ moves.push([row, col]);
412
+ }
413
+ }
414
+ }
415
+ return moves;
416
+ }
417
+ function getBoardString(board) {
418
+ return board.map((row) => row.map((cell) => cell || " ").join("")).join("\n");
419
+ }
420
+ function cloneBoard(board) {
421
+ return board.map((row) => [...row]);
422
+ }
423
+ function makeMoveOnBoard(board, row, col, player) {
424
+ if (board[row][col] !== null) {
425
+ throw new Error("Cell is already occupied");
426
+ }
427
+ const newBoard = cloneBoard(board);
428
+ newBoard[row][col] = player;
429
+ return newBoard;
430
+ }
351
431
  // Annotate the CommonJS export names for ESM import in node:
352
432
  0 && (module.exports = {
353
433
  CONTRACT_ADDRESS,
@@ -357,16 +437,24 @@ function createClient(config) {
357
437
  DEFAULT_LEADERBOARD_API,
358
438
  MAINNET_API,
359
439
  TESTNET_API,
440
+ checkWin,
441
+ cloneBoard,
360
442
  createClient,
443
+ getAvailableMoves,
361
444
  getBoardState,
445
+ getBoardString,
362
446
  getCurrentTurn,
363
447
  getFullGameState,
364
448
  getGameStatus,
365
449
  getLeaderboard,
450
+ getPlayerStats,
366
451
  getWinner,
367
452
  healthCheck,
453
+ isBoardFull,
454
+ isDraw,
368
455
  isValidMove,
369
456
  makeMove,
457
+ makeMoveOnBoard,
370
458
  resignGame,
371
459
  startNewGame,
372
460
  submitResult,
package/dist/index.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  getFullGameState,
13
13
  getGameStatus,
14
14
  getLeaderboard,
15
+ getPlayerStats,
15
16
  getWinner,
16
17
  healthCheck,
17
18
  isValidMove,
@@ -20,7 +21,60 @@ import {
20
21
  startNewGame,
21
22
  submitResult,
22
23
  syncLeaderboard
23
- } from "./chunk-Z5L3H7VV.mjs";
24
+ } from "./chunk-ETEDBK3G.mjs";
25
+
26
+ // src/utils/board.ts
27
+ function checkWin(board, player) {
28
+ if (player === null) return false;
29
+ for (let row = 0; row < 3; row++) {
30
+ if (board[row][0] === player && board[row][1] === player && board[row][2] === player) {
31
+ return true;
32
+ }
33
+ }
34
+ for (let col = 0; col < 3; col++) {
35
+ if (board[0][col] === player && board[1][col] === player && board[2][col] === player) {
36
+ return true;
37
+ }
38
+ }
39
+ if (board[0][0] === player && board[1][1] === player && board[2][2] === player) {
40
+ return true;
41
+ }
42
+ if (board[0][2] === player && board[1][1] === player && board[2][0] === player) {
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ function isBoardFull(board) {
48
+ return board.every((row) => row.every((cell) => cell !== null));
49
+ }
50
+ function isDraw(board) {
51
+ return isBoardFull(board) && !checkWin(board, "X") && !checkWin(board, "O");
52
+ }
53
+ function getAvailableMoves(board) {
54
+ const moves = [];
55
+ for (let row = 0; row < 3; row++) {
56
+ for (let col = 0; col < 3; col++) {
57
+ if (board[row][col] === null) {
58
+ moves.push([row, col]);
59
+ }
60
+ }
61
+ }
62
+ return moves;
63
+ }
64
+ function getBoardString(board) {
65
+ return board.map((row) => row.map((cell) => cell || " ").join("")).join("\n");
66
+ }
67
+ function cloneBoard(board) {
68
+ return board.map((row) => [...row]);
69
+ }
70
+ function makeMoveOnBoard(board, row, col, player) {
71
+ if (board[row][col] !== null) {
72
+ throw new Error("Cell is already occupied");
73
+ }
74
+ const newBoard = cloneBoard(board);
75
+ newBoard[row][col] = player;
76
+ return newBoard;
77
+ }
24
78
  export {
25
79
  CONTRACT_ADDRESS,
26
80
  CONTRACT_FUNCTIONS,
@@ -29,16 +83,24 @@ export {
29
83
  DEFAULT_LEADERBOARD_API,
30
84
  MAINNET_API,
31
85
  TESTNET_API,
86
+ checkWin,
87
+ cloneBoard,
32
88
  createClient,
89
+ getAvailableMoves,
33
90
  getBoardState,
91
+ getBoardString,
34
92
  getCurrentTurn,
35
93
  getFullGameState,
36
94
  getGameStatus,
37
95
  getLeaderboard,
96
+ getPlayerStats,
38
97
  getWinner,
39
98
  healthCheck,
99
+ isBoardFull,
100
+ isDraw,
40
101
  isValidMove,
41
102
  makeMove,
103
+ makeMoveOnBoard,
42
104
  resignGame,
43
105
  startNewGame,
44
106
  submitResult,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarityxo-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript SDK and CLI for the ClarityXO on-chain Tic-Tac-Toe game on Stacks",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -31,7 +31,6 @@
31
31
  "web3"
32
32
  ],
33
33
  "author": "Dominion <limbotech116@gmail.com>",
34
- "author": "Dominion <limbotech116@gmail.com>",
35
34
  "license": "MIT",
36
35
  "repository": {
37
36
  "type": "git",