diff --git a/contracts/tic-tac-toe.clar b/contracts/tic-tac-toe.clar index 4bc01e3..0b5d524 100644 --- a/contracts/tic-tac-toe.clar +++ b/contracts/tic-tac-toe.clar @@ -18,7 +18,22 @@ bet-amount: uint, board: (list 9 uint), - winner: (optional principal) + winner: (optional principal), + + ;; NEW: Track total number of moves made in this game + move-count: uint + } +) + +;; NEW: Map to store individual move history +;; Key: {game-id, move-number}, Value: {player, move-index, move-value, block-height} +(define-map move-history + { game-id: uint, move-number: uint } + { + player: principal, + move-index: uint, + move-value: uint, + block-height: uint } ) @@ -37,7 +52,8 @@ is-player-one-turn: false, bet-amount: bet-amount, board: game-board, - winner: none + winner: none, + move-count: u1 ;; NEW: Initialize with 1 move (the creation move) }) ) @@ -55,6 +71,17 @@ ;; Increment the Game ID counter (var-set latest-game-id (+ game-id u1)) + ;; NEW: Record the first move in history + (map-set move-history + { game-id: game-id, move-number: u0 } + { + player: contract-caller, + move-index: move-index, + move-value: move, + block-height: stacks-block-height + } + ) + ;; Log the creation of the new game (print { action: "create-game", data: game-data}) ;; Return the Game ID of the new game @@ -67,6 +94,8 @@ (original-game-data (unwrap! (map-get? games game-id) (err ERR_GAME_NOT_FOUND))) ;; Get the original board from the game data (original-board (get board original-game-data)) + ;; Get current move count + (current-move-count (get move-count original-game-data)) ;; Update the game board by placing the player's move at the specified index (game-board (unwrap! (replace-at? original-board move-index move) (err ERR_INVALID_MOVE))) @@ -74,7 +103,8 @@ (game-data (merge original-game-data { board: game-board, player-two: (some contract-caller), - is-player-one-turn: true + is-player-one-turn: true, + move-count: (+ current-move-count u1) ;; NEW: Increment move count })) ) @@ -91,6 +121,17 @@ ;; Update the games map with the new game data (map-set games game-id game-data) + ;; NEW: Record this move in history + (map-set move-history + { game-id: game-id, move-number: current-move-count } + { + player: contract-caller, + move-index: move-index, + move-value: move, + block-height: stacks-block-height + } + ) + ;; Log the joining of the game (print { action: "join-game", data: game-data}) ;; Return the Game ID of the game @@ -103,6 +144,8 @@ (original-game-data (unwrap! (map-get? games game-id) (err ERR_GAME_NOT_FOUND))) ;; Get the original board from the game data (original-board (get board original-game-data)) + ;; Get current move count + (current-move-count (get move-count original-game-data)) ;; Is it player one's turn? (is-player-one-turn (get is-player-one-turn original-game-data)) @@ -120,7 +163,8 @@ (game-data (merge original-game-data { board: game-board, is-player-one-turn: (not is-player-one-turn), - winner: (if is-now-winner (some player-turn) none) + winner: (if is-now-winner (some player-turn) none), + move-count: (+ current-move-count u1) ;; NEW: Increment move count })) ) @@ -137,6 +181,17 @@ ;; Update the games map with the new game data (map-set games game-id game-data) + ;; NEW: Record this move in history + (map-set move-history + { game-id: game-id, move-number: current-move-count } + { + player: contract-caller, + move-index: move-index, + move-value: move, + block-height: stacks-block-height + } + ) + ;; Log the action of a move being made (print {action: "play", data: game-data}) ;; Return the Game ID of the game @@ -151,6 +206,21 @@ (var-get latest-game-id) ) +;; NEW: Get a specific move from game history +;; Returns the move details or none if move doesn't exist +(define-read-only (get-move (game-id uint) (move-number uint)) + (map-get? move-history { game-id: game-id, move-number: move-number }) +) + +;; NEW: Get the total number of moves made in a game +;; Returns the move count or 0 if game doesn't exist +(define-read-only (get-move-count (game-id uint)) + (match (map-get? games game-id) + game-data (ok (get move-count game-data)) + (ok u0) + ) +) + (define-private (validate-move (board (list 9 uint)) (move-index uint) (move uint)) (let ( ;; Validate that the move is being played within range of the board diff --git a/frontend/app/game/[gameId]/page.tsx b/frontend/app/game/[gameId]/page.tsx index bcaf380..844f8ef 100644 --- a/frontend/app/game/[gameId]/page.tsx +++ b/frontend/app/game/[gameId]/page.tsx @@ -1,23 +1,58 @@ import { PlayGame } from "@/components/play-game"; -import { getGame } from "@/lib/contract"; +import { GameHistory } from "@/components/game-history"; +import { MoveReplay } from "@/components/move-replay"; +import { getGame, getGameHistory } from "@/lib/contract"; type Params = Promise<{ gameId: string }>; export default async function GamePage({ params }: { params: Params }) { const gameId = (await params).gameId; const game = await getGame(parseInt(gameId)); - if (!game) return
Game not found
; + + if (!game) { + return ( +
+

Game not found

+

The game ID {gameId} does not exist.

+
+ ); + } + + // Fetch game history + const history = await getGameHistory(parseInt(gameId)); return ( -
-
+
+

Game #{gameId}

- Play the game with your opponent + Play the game or review the move history
+ {/* Main game board and controls */} + + {/* Move History Section */} + {history.length > 0 && ( + <> +
+ + {/* Move Replay Animation */} +
+ +
+ +
+ + {/* Detailed Move History Table */} + + + )}
); -} +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index f370348..b9746e9 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,7 +1,8 @@ import { GamesList } from "@/components/games-list"; import { getAllGames } from "@/lib/contract"; -export const dynamic = "force-dynamic"; +// Cache the page for 30 seconds to avoid repeated API calls +export const revalidate = 30; export default async function Home() { const games = await getAllGames(); @@ -18,4 +19,4 @@ export default async function Home() {
); -} +} \ No newline at end of file diff --git a/frontend/components/game-history.tsx b/frontend/components/game-history.tsx new file mode 100644 index 0000000..ca01b82 --- /dev/null +++ b/frontend/components/game-history.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { MoveHistoryEntry } from "@/lib/contract"; +import { abbreviateAddress } from "@/lib/stx-utils"; +import Link from "next/link"; + +interface GameHistoryProps { + history: MoveHistoryEntry[]; + playerOne: string; + playerTwo: string | null; +} + +export function GameHistory({ history, playerOne, playerTwo }: GameHistoryProps) { + if (history.length === 0) { + return ( +
+ No moves recorded yet +
+ ); + } + + return ( +
+

Game History

+ +
+
+
Move #
+
Player
+
Symbol
+
Position
+
Block
+
+ + {history.map((move) => { + const isPlayerOne = move.player === playerOne; + const playerName = isPlayerOne ? "Player 1" : "Player 2"; + const symbol = move.moveValue === 1 ? "X" : "O"; + const symbolColor = move.moveValue === 1 ? "text-blue-400" : "text-red-400"; + + // Convert move index to board position (e.g., 0 = "Top-Left", 4 = "Center") + const positions = [ + "Top-Left", "Top-Center", "Top-Right", + "Mid-Left", "Center", "Mid-Right", + "Bottom-Left", "Bottom-Center", "Bottom-Right" + ]; + const position = positions[move.moveIndex]; + + return ( +
+
{move.moveNumber + 1}
+ +
+ + {abbreviateAddress(move.player)} + +
{playerName}
+
+ +
+ {symbol} +
+ +
+ {position} +
Cell {move.moveIndex}
+
+ + + {move.blockHeight} + +
+ ); + })} +
+ +
+ Total moves: {history.length} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/components/games-list.tsx b/frontend/components/games-list.tsx index 3817658..72405de 100644 --- a/frontend/components/games-list.tsx +++ b/frontend/components/games-list.tsx @@ -45,6 +45,7 @@ export function GamesList({ games }: { games: Game[] }) { return (
+ {/* Active Games Section */} {userData ? (

Active Games

@@ -66,7 +67,7 @@ export function GamesList({ games }: { games: Game[] }) { Next Turn: {game["is-player-one-turn"] ? "X" : "O"}
+ {/* NEW: Show move count */} +
+ 📜 {game["move-count"]} {game["move-count"] === 1 ? "move" : "moves"} +
))}
@@ -86,6 +91,7 @@ export function GamesList({ games }: { games: Game[] }) { ) : null} + {/* Joinable Games Section */}

Joinable Games

{joinableGames.length === 0 ? ( @@ -106,7 +112,7 @@ export function GamesList({ games }: { games: Game[] }) { {formatStx(game["bet-amount"])} STX
-
- Next Turn: {game["is-player-one-turn"] ? "X" : "O"} +
+ Join Now! +
+ {/* NEW: Show move count (always 1 for joinable games) */} +
+ 📜 {game["move-count"]} move
))} @@ -125,6 +135,7 @@ export function GamesList({ games }: { games: Game[] }) { )}
+ {/* Ended Games Section */}

Ended Games

{endedGames.length === 0 ? ( @@ -145,7 +156,7 @@ export function GamesList({ games }: { games: Game[] }) { {formatStx(game["bet-amount"])} STX
-
+
Winner: {game["is-player-one-turn"] ? "O" : "X"}
+ {/* NEW: Show move count with view history hint */} +
+ 📜 {game["move-count"]} {game["move-count"] === 1 ? "move" : "moves"} · View History +
))}
@@ -165,4 +180,4 @@ export function GamesList({ games }: { games: Game[] }) { ); -} +} \ No newline at end of file diff --git a/frontend/components/move-replay.tsx b/frontend/components/move-replay.tsx new file mode 100644 index 0000000..fd28d1c --- /dev/null +++ b/frontend/components/move-replay.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { MoveHistoryEntry, Move } from "@/lib/contract"; +import { GameBoard } from "./game-board"; + +interface MoveReplayProps { + history: MoveHistoryEntry[]; +} + +export function MoveReplay({ history }: MoveReplayProps) { + const [currentMoveIndex, setCurrentMoveIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [board, setBoard] = useState(Array(9).fill(Move.EMPTY)); + + // Reconstruct board up to current move + useEffect(() => { + const newBoard = Array(9).fill(Move.EMPTY); + + for (let i = 0; i <= currentMoveIndex && i < history.length; i++) { + const move = history[i]; + newBoard[move.moveIndex] = move.moveValue; + } + + setBoard(newBoard); + }, [currentMoveIndex, history]); + + // Auto-play functionality + useEffect(() => { + if (!isPlaying) return; + + if (currentMoveIndex >= history.length - 1) { + setIsPlaying(false); + return; + } + + const timer = setTimeout(() => { + setCurrentMoveIndex((prev) => prev + 1); + }, 1000); // 1 second per move + + return () => clearTimeout(timer); + }, [isPlaying, currentMoveIndex, history.length]); + + const handlePrevious = () => { + setIsPlaying(false); + setCurrentMoveIndex((prev) => Math.max(0, prev - 1)); + }; + + const handleNext = () => { + setIsPlaying(false); + setCurrentMoveIndex((prev) => Math.min(history.length - 1, prev + 1)); + }; + + const handlePlayPause = () => { + setIsPlaying(!isPlaying); + }; + + const handleReset = () => { + setIsPlaying(false); + setCurrentMoveIndex(0); + }; + + if (history.length === 0) { + return ( +
+ No moves to replay +
+ ); + } + + const currentMove = history[currentMoveIndex]; + const symbol = currentMove?.moveValue === 1 ? "X" : "O"; + + return ( +
+

Move Replay

+ +
+
+ Move {currentMoveIndex + 1} of {history.length} +
+ {currentMove && ( +
+ + {symbol} + {" "} + played at position {currentMove.moveIndex} +
+ )} +
+ + + +
+ + + + + + + +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/lib/contract.ts b/frontend/lib/contract.ts index ab626ad..967b829 100644 --- a/frontend/lib/contract.ts +++ b/frontend/lib/contract.ts @@ -21,6 +21,7 @@ type GameCV = { "bet-amount": UIntCV; board: ListCV; winner: OptionalCV; + "move-count": UIntCV; }; export type Game = { @@ -31,6 +32,16 @@ export type Game = { "bet-amount": number; board: number[]; winner: string | null; + "move-count": number; +}; + +// NEW: TypeScript types for move history +export type MoveHistoryEntry = { + player: string; + moveIndex: number; + moveValue: number; // 1 for X, 2 for O + blockHeight: number; + moveNumber: number; }; export enum Move { @@ -51,6 +62,9 @@ export const EMPTY_BOARD = [ Move.EMPTY, ]; +// Helper function to add delay between API calls +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + export async function getAllGames() { // Fetch the latest-game-id from the contract const latestGameIdCV = (await fetchCallReadOnlyFunction({ @@ -65,11 +79,26 @@ export async function getAllGames() { // Convert the uintCV to a JS/TS number type const latestGameId = parseInt(latestGameIdCV.value.toString()); - // Loop from 0 to latestGameId-1 and fetch the game details for each game + // IMPORTANT: Limit to last 20 games to avoid rate limiting + // For more games, users should upgrade their Hiro API plan + const startId = Math.max(0, latestGameId - 20); + + // Loop and fetch the game details for recent games only + // Add 500ms delay to avoid rate limiting (increased from 200ms) const games: Game[] = []; - for (let i = 0; i < latestGameId; i++) { - const game = await getGame(i); - if (game) games.push(game); + for (let i = startId; i < latestGameId; i++) { + try { + const game = await getGame(i); + if (game) games.push(game); + + // Add 500ms delay between requests to avoid rate limiting + if (i < latestGameId - 1) { + await delay(500); + } + } catch (error) { + console.error(`Failed to fetch game ${i}:`, error); + // Continue fetching other games even if one fails + } } return games; } @@ -106,6 +135,10 @@ export async function getGame(gameId: number) { board: gameCV["board"].value.map((cell) => parseInt(cell.value.toString())), winner: gameCV["winner"].type === "some" ? gameCV["winner"].value.value : null, + // Handle backward compatibility: old games won't have move-count + "move-count": gameCV["move-count"] + ? parseInt(gameCV["move-count"].value.toString()) + : 0, }; return game; } @@ -146,3 +179,73 @@ export async function play(gameId: number, moveIndex: number, move: Move) { return txOptions; } + +// NEW: Get a specific move from history +export async function getMove(gameId: number, moveNumber: number) { + const moveData = await fetchCallReadOnlyFunction({ + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-move", + functionArgs: [uintCV(gameId), uintCV(moveNumber)], + senderAddress: CONTRACT_ADDRESS, + network: STACKS_TESTNET, + }); + + const responseCV = moveData as OptionalCV; + + // If we get back a none, the move doesn't exist + if (responseCV.type === "none") return null; + // If we get back a value that is not a tuple, something went wrong + if (responseCV.value.type !== "tuple") return null; + + const moveCV = responseCV.value.value as any; + + return { + player: moveCV.player.value, + moveIndex: parseInt(moveCV["move-index"].value.toString()), + moveValue: parseInt(moveCV["move-value"].value.toString()), + blockHeight: parseInt(moveCV["block-height"].value.toString()), + }; +} + +// NEW: Get total move count for a game +export async function getMoveCount(gameId: number): Promise { + try { + const result = await fetchCallReadOnlyFunction({ + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: "get-move-count", + functionArgs: [uintCV(gameId)], + senderAddress: CONTRACT_ADDRESS, + network: STACKS_TESTNET, + }); + + // The result is wrapped in a response CV (ok/err) + if (result.type !== "ok") return 0; + if (result.value.type !== "uint") return 0; + + return parseInt(result.value.value.toString()); + } catch (error) { + // Function doesn't exist in old contract - return 0 + console.warn("get-move-count not available in deployed contract"); + return 0; + } +} + +// NEW: Get complete game history (all moves) +export async function getGameHistory(gameId: number): Promise { + const moveCount = await getMoveCount(gameId); + const history: MoveHistoryEntry[] = []; + + for (let i = 0; i < moveCount; i++) { + const move = await getMove(gameId, i); + if (move) { + history.push({ + ...move, + moveNumber: i, + }); + } + } + + return history; +} \ No newline at end of file diff --git a/tests/tic-tac-toe.test.ts b/tests/tic-tac-toe.test.ts index 8de6105..4c41fc5 100644 --- a/tests/tic-tac-toe.test.ts +++ b/tests/tic-tac-toe.test.ts @@ -132,6 +132,7 @@ describe("Tic Tac Toe Tests", () => { Cl.uint(0), ]), winner: Cl.some(Cl.principal(alice)), + "move-count": Cl.uint(5), // NEW: Verify move count is tracked }) ); }); @@ -166,7 +167,274 @@ describe("Tic Tac Toe Tests", () => { Cl.uint(1), ]), winner: Cl.some(Cl.principal(bob)), + "move-count": Cl.uint(6), // NEW: Verify move count is tracked }) ); }); }); + +// NEW: Game History Feature Tests +describe("Game History Feature", () => { + it("records move history when game is created", () => { + createGame(100, 0, 1, alice); + + // Check that first move was recorded + const moveData = simnet.getMapEntry( + "tic-tac-toe", + "move-history", + Cl.tuple({ "game-id": Cl.uint(0), "move-number": Cl.uint(0) }) + ); + + // Verify the move data exists and has correct structure + expect(moveData).not.toBeNone(); + + // Extract and verify individual fields + const move = (moveData as any).value.data; + + expect(move.player).toEqual(Cl.principal(alice)); + expect(move["move-index"]).toEqual(Cl.uint(0)); + expect(move["move-value"]).toEqual(Cl.uint(1)); + expect(move["block-height"].type).toBe(Cl.uint(1).type); + expect(Number(move["block-height"].value)).toBeGreaterThan(0); + }); + + it("records move history when player joins", () => { + createGame(100, 0, 1, alice); + joinGame(1, 2, bob); + + // Check that second move was recorded + const moveData = simnet.getMapEntry( + "tic-tac-toe", + "move-history", + Cl.tuple({ "game-id": Cl.uint(0), "move-number": Cl.uint(1) }) + ); + + expect(moveData).not.toBeNone(); + + const move = (moveData as any).value.data; + + expect(move.player).toEqual(Cl.principal(bob)); + expect(move["move-index"]).toEqual(Cl.uint(1)); + expect(move["move-value"]).toEqual(Cl.uint(2)); + expect(move["block-height"].type).toBe(Cl.uint(1).type); + expect(Number(move["block-height"].value)).toBeGreaterThan(0); + }); + + it("records move history during gameplay", () => { + createGame(100, 0, 1, alice); + joinGame(1, 2, bob); + play(2, 1, alice); + + // Check that third move was recorded + const moveData = simnet.getMapEntry( + "tic-tac-toe", + "move-history", + Cl.tuple({ "game-id": Cl.uint(0), "move-number": Cl.uint(2) }) + ); + + expect(moveData).not.toBeNone(); + + const move = (moveData as any).value.data; + + expect(move.player).toEqual(Cl.principal(alice)); + expect(move["move-index"]).toEqual(Cl.uint(2)); + expect(move["move-value"]).toEqual(Cl.uint(1)); + expect(move["block-height"].type).toBe(Cl.uint(1).type); + expect(Number(move["block-height"].value)).toBeGreaterThan(0); + }); + + it("tracks complete game history", () => { + // Play a complete game + createGame(100, 0, 1, alice); // Move 0: Alice at position 0 + joinGame(3, 2, bob); // Move 1: Bob at position 3 + play(1, 1, alice); // Move 2: Alice at position 1 + play(4, 2, bob); // Move 3: Bob at position 4 + play(2, 1, alice); // Move 4: Alice at position 2 (Alice wins: 0,1,2) + + // Verify all moves were recorded using the read-only function + const move0Result = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(0)], + alice + ); + expect(move0Result.result).not.toBeNone(); + + const move1Result = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(1)], + alice + ); + expect(move1Result.result).not.toBeNone(); + + const move2Result = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(2)], + alice + ); + expect(move2Result.result).not.toBeNone(); + + const move3Result = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(3)], + alice + ); + expect(move3Result.result).not.toBeNone(); + + const move4Result = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(4)], + alice + ); + expect(move4Result.result).not.toBeNone(); + + // Verify move 5 doesn't exist (game ended at move 4) + const move5Result = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(5)], + alice + ); + expect(move5Result.result).toBeNone(); + }); + + it("correctly retrieves move via get-move function", () => { + createGame(100, 4, 1, alice); // Alice plays center (index 4) + + const { result } = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(0)], + alice + ); + + expect(result).not.toBeNone(); + + const move = (result as any).value.data; + + expect(move.player).toEqual(Cl.principal(alice)); + expect(move["move-index"]).toEqual(Cl.uint(4)); + expect(move["move-value"]).toEqual(Cl.uint(1)); + expect(move["block-height"].type).toBe(Cl.uint(1).type); + expect(Number(move["block-height"].value)).toBeGreaterThan(0); + }); + + it("returns none for non-existent move", () => { + createGame(100, 0, 1, alice); + + // Try to get move 10 (doesn't exist) + const { result } = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move", + [Cl.uint(0), Cl.uint(10)], + alice + ); + + expect(result).toBeNone(); + }); + + it("correctly reports move count via get-move-count", () => { + createGame(100, 0, 1, alice); + joinGame(1, 2, bob); + play(2, 1, alice); + + const { result } = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move-count", + [Cl.uint(0)], + alice + ); + + expect(result).toBeOk(Cl.uint(3)); // 3 moves made + }); + + it("returns 0 move count for non-existent game", () => { + const { result } = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move-count", + [Cl.uint(999)], // Game doesn't exist + alice + ); + + expect(result).toBeOk(Cl.uint(0)); + }); + + it("tracks moves across multiple games independently", () => { + // Create and play two games + createGame(100, 0, 1, alice); // Game 0 + createGame(100, 4, 1, bob); // Game 1 + + // Check game 0 move 0 + const game0Move = simnet.getMapEntry( + "tic-tac-toe", + "move-history", + Cl.tuple({ "game-id": Cl.uint(0), "move-number": Cl.uint(0) }) + ); + + expect(game0Move).not.toBeNone(); + + const move0 = (game0Move as any).value.data; + + expect(move0.player).toEqual(Cl.principal(alice)); + expect(move0["move-index"]).toEqual(Cl.uint(0)); + expect(move0["move-value"]).toEqual(Cl.uint(1)); + expect(move0["block-height"].type).toBe(Cl.uint(1).type); + expect(Number(move0["block-height"].value)).toBeGreaterThan(0); + + // Check game 1 move 0 + const game1Move = simnet.getMapEntry( + "tic-tac-toe", + "move-history", + Cl.tuple({ "game-id": Cl.uint(1), "move-number": Cl.uint(0) }) + ); + + expect(game1Move).not.toBeNone(); + + const move1 = (game1Move as any).value.data; + + expect(move1.player).toEqual(Cl.principal(bob)); + expect(move1["move-index"]).toEqual(Cl.uint(4)); + expect(move1["move-value"]).toEqual(Cl.uint(1)); + expect(move1["block-height"].type).toBe(Cl.uint(1).type); + expect(Number(move1["block-height"].value)).toBeGreaterThan(0); + }); + + it("increments move count correctly throughout game", () => { + createGame(100, 0, 1, alice); + + // After creation, move count should be 1 + let moveCount = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move-count", + [Cl.uint(0)], + alice + ); + expect(moveCount.result).toBeOk(Cl.uint(1)); + + joinGame(1, 2, bob); + + // After join, move count should be 2 + moveCount = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move-count", + [Cl.uint(0)], + alice + ); + expect(moveCount.result).toBeOk(Cl.uint(2)); + + play(2, 1, alice); + + // After play, move count should be 3 + moveCount = simnet.callReadOnlyFn( + "tic-tac-toe", + "get-move-count", + [Cl.uint(0)], + alice + ); + expect(moveCount.result).toBeOk(Cl.uint(3)); + }); +}); \ No newline at end of file