# Building a Decentralized Tic Tac Toe Game with Stackr Labs

By [Kaushik](https://paragraph.com/@kaushik-2) · 2024-10-13

---

Hello, fellow developers! Today, I’m thrilled to guide you through creating a decentralized Tic Tac Toe game using **Stackr Labs** and **Micro Rollups (MRUs)**. This guide will take you through each step, from setting up your project to deploying your game on the blockchain. Let’s get started!

What is Stackr Labs?
--------------------

Stackr Labs gives your dapp a turbo boost! With Micro-Rollups (MRUs), helps developers build fast, scalable apps. Think of it like preparing food at home for a trip—most of the work happens off-chain, ensuring a smoother experience on the blockchain!

Table of Contents
-----------------

1.  [Prerequisites](https://mirror.xyz/0x5BDf4cE6749d7e93c05897fa23871C280BF59b5b/vE8e9dgoMC5saS5Jq-LS5nif4KVTdAVqu-uvoyLDlmg#prerequisites)
    
2.  [Setting Up Your Project](https://mirror.xyz/0x5BDf4cE6749d7e93c05897fa23871C280BF59b5b/vE8e9dgoMC5saS5Jq-LS5nif4KVTdAVqu-uvoyLDlmg)
    
3.  [Creating the State Machine](https://mirror.xyz/0x5BDf4cE6749d7e93c05897fa23871C280BF59b5b/vE8e9dgoMC5saS5Jq-LS5nif4KVTdAVqu-uvoyLDlmg)
    
4.  [Implementing the Game Logic](https://mirror.xyz/0x5BDf4cE6749d7e93c05897fa23871C280BF59b5b/vE8e9dgoMC5saS5Jq-LS5nif4KVTdAVqu-uvoyLDlmg)
    
5.  [Frontend Integration](https://mirror.xyz/0x5BDf4cE6749d7e93c05897fa23871C280BF59b5b/vE8e9dgoMC5saS5Jq-LS5nif4KVTdAVqu-uvoyLDlmg)
    
6.  [Testing Your Game](https://mirror.xyz/0x5BDf4cE6749d7e93c05897fa23871C280BF59b5b/vE8e9dgoMC5saS5Jq-LS5nif4KVTdAVqu-uvoyLDlmg)
    
7.  [Conclusion](https://mirror.xyz/0x5BDf4cE6749d7e93c05897fa23871C280BF59b5b/vE8e9dgoMC5saS5Jq-LS5nif4KVTdAVqu-uvoyLDlmg)
    

Prerequisites
-------------

Before we dive in, make sure you have the following tools installed:

*   **Node.js**: Download and install from [Node.js](https://nodejs.org/)
    
*   **Git**: Download and install from [Git](https://git-scm.com/)
    
*   **Bun**: Installation guide can be found [here](https://bun.sh/)
    
*   **Stackr CLI**: Install globally using:
    
        bun install -g @stackr/cli
        
    

**Create a New Directory**:
---------------------------

    mkdir tic-tac-toe
    cd tic-tac-toe
    

**Initialize a New Stackr Project**:
------------------------------------

    bunx @stackr/cli@latest init
    

Setting Up Your Project
-----------------------

You can use the following .env values:

    # PRIVATE_KEY is the hex-encoded private key of the Ethereum address that will act as the rollup operator.
    PRIVATE_KEY= --YOUR DEVELOPMENT PRIVATE KEY--
    
    # L1_RPC is the RPC endpoint of the L1 chain. Can be found in the network config docs above.
    L1_RPC=https://rpc2.sepolia.org/
    
    # VULCAN_RPC is the RPC endpoint of the Vulcan network. Can be found in the network config docs above.
    VULCAN_RPC=https://sepolia.vulcan.stf.xyz/
    
    # REGISTRY_CONTRACT is the address of the Stackr registry contract on the L1 chain. Can be found in the network config docs above.
    REGISTRY_CONTRACT=0x985bAfb3cAf7F4BD13a21290d1736b93D07A1760
    
    # DATABASE_URI is the connection URI of the SQL database.
    DATABASE_URI=./db.sqlite
    
    # NODE_ENV is the current environment (production or development). Relevant for express.js server.
    NODE_ENV=development
    

Creating the state machine
--------------------------

In this section, we’ll define the game state using Stackr’s state management.

### `src/stackr/state.ts`

    import { BytesLike, solidityPackedKeccak256 } from "ethers";
    import { State } from "@stackr/sdk/machine";
    
    // Define the structure of a Tic Tac Toe game
    interface Game {
      gameId: string;
      owner: string; // Player X's address
      playerO: string; // Player O's address
      board: string[]; // 9 cells represented as a string array
      currentPlayer: "X" | "O";
      winner: "X" | "O" | "Draw" | null;
      startedAt: number;
      endedAt: number | null;
    }
    
    // Define the overall application state
    export interface AppState {
      games: Record<string, Game>;
    }
    
    // Initial state of the application
    export const initialState: AppState = {
      games: {},
    };
    
    // TicTacToeState class extends the State class from @stackr/sdk
    export class TicTacToeState extends State<AppState> {
      constructor(state: AppState) {
        super(state);
      }
    
      // Generate a root hash of the current state
      // This is used for state verification and integrity checks
      getRootHash(): BytesLike {
        return solidityPackedKeccak256(
          ["string"],
          [JSON.stringify(this.state.games)]
        );
      }
    }
    

### Explanation:

*   We define an interface `Game` to represent the state of the Tic Tac Toe game.
    
*   The `initialState` sets up the initial conditions, including an empty board and the starting player.
    
*   A `State` instance is created to manage the game state.
    

Implementing the Game Logic
---------------------------

Next, we’ll add the logic to handle player moves and check for wins.

### `src/stackr/machine.ts`

    import { StateMachine } from "@stackr/sdk/machine";
    import { TicTacToeState, initialState } from "./state";
    import { transitions } from "./transitions";
    
    // Define the State Machine for Tic Tac Toe
    const machine = new StateMachine({
      id: "tic-tac-toe", // Unique identifier for this state machine
      stateClass: TicTacToeState, // The state class used by this machine
      initialState, // Initial state
      on: transitions, // Transitions that can be applied to the state
    });
    
    export { machine 
    

### Explanation:

*   `StateMachine`: Initializes the state machine with an ID, state class, initial state, and available transitions.
    

Implementing Transitions
------------------------

`src/stackr/transitions.ts`

Define the possible actions/transitions in the game.

    // src/stackr/transitions.ts
    
    import { Transitions, SolidityType } from "@stackr/sdk/machine";
    import { TicTacToeState } from "./state";
    import { hashMessage } from "ethers";
    
    const winningCombinations: number[][] = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6],
    ];
    
    // Define the createGame transition
    const createGame = TicTacToeState.STF({
      schema: {
        owner: SolidityType.ADDRESS,
        playerO: SolidityType.ADDRESS,
        startedAt: SolidityType.UINT,
      },
      handler: ({ state, inputs, msgSender, block, emit }) => {
        // Ensure that msgSender matches inputs.owner
        if (msgSender.toLowerCase() !== inputs.owner.toLowerCase()) {
          emit({
            name: "InvalidSigner",
            value: `${msgSender} is not authorized to create a game.`,
          });
          return state;
        }
    
        // Generate a unique game ID
        const gameId = hashMessage(
          `${msgSender}::${block.timestamp}::${Object.keys(state.games).length}`
        );
    
        // Initialize the new game
        state.games[gameId] = {
          gameId,
          owner: inputs.owner,
          playerO: inputs.playerO,
          board: Array(9).fill(""),
          currentPlayer: "X",
          winner: null,
          startedAt: inputs.startedAt,
          endedAt: null,
        };
    
        // Emit an event for game creation
        emit({
          name: "GameCreated",
          value: gameId,
        });
    
        return state;
      },
    });
    
    // Define the makeMove transition
    const makeMove = TicTacToeState.STF({
      schema: {
        gameId: SolidityType.STRING,
        index: SolidityType.UINT,
        player: SolidityType.STRING, // "X" or "O"
      },
      handler: ({ state, inputs, msgSender, block, emit }) => {
        const { gameId, index, player } = inputs;
        const game = state.games[gameId];
    
        if (!game) {
          emit({
            name: "GameNotFound",
            value: gameId,
          });
          return state;
        }
    
        // Validate player
        if (player !== game.currentPlayer) {
          emit({
            name: "InvalidPlayer",
            value: `${player} attempted to move.`,
          });
          return state;
        }
    
        // Validate move
        if (game.board[index] !== "") {
          emit({
            name: "InvalidMove",
            value: `${index} is already occupied.`,
          });
          return state;
        }
    
        // Make the move
        game.board[index] = player;
    
        // Check for a winner
        let winner: "X" | "O" | "Draw" | null = null;
        for (const combination of winningCombinations) {
          const [a, b, c] = combination;
          if (
            game.board[a] &&
            game.board[a] === game.board[b] &&
            game.board[a] === game.board[c]
          ) {
            winner = game.board[a] as "X" | "O";
            break;
          }
        }
    
        // Check for draw
        if (!winner && game.board.every((cell) => cell !== "")) {
          winner = "Draw";
        }
    
        if (winner) {
          game.winner = winner;
          game.endedAt = block.timestamp;
          emit({
            name: "GameEnded",
            value: `${gameId}::${winner}`,
          });
        } else {
          // Switch player
          game.currentPlayer = player === "X" ? "O" : "X";
          emit({
            name: "MoveMade",
            value: `${gameId}::${player}::${index}`,
          });
        }
    
        return state;
      },
    });
    
    // Define the resetGame transition
    const resetGame = TicTacToeState.STF({
      schema: {
        gameId: SolidityType.STRING,
      },
      handler: ({ state, inputs, msgSender, block, emit }) => {
        const { gameId } = inputs;
        const game = state.games[gameId];
    
        if (!game) {
          emit({
            name: "GameNotFound",
            value: gameId,
          });
          return state;
        }
    
        // Only the game owner can reset the game
        if (msgSender.toLowerCase() !== game.owner.toLowerCase()) {
          emit({
            name: "UnauthorizedReset",
            value: `${msgSender} is not authorized to reset the game.`,
          });
          return state;
        }
    
        // Reset the game
        state.games[gameId] = {
          ...game,
          board: Array(9).fill(""),
          currentPlayer: "X",
          winner: null,
          endedAt: null,
        };
    
        emit({
          name: "GameReset",
          value: gameId,
        });
    
        return state;
      },
    });
    
    // Export the transitions
    export const transitions: Transitions<TicTacToeState> = {
      createGame,
      makeMove,
      resetGame,
    };
    

**Explanation:**

*   `createGame` **Transition**: Initializes a new Tic Tac Toe game with two players.
    
*   `makeMove` **Transition**: Processes a player's move, updates the board, checks for a winner or draw, and switches the current player.
    
*   `resetGame` **Transition**: Resets an existing game to its initial state.
    

**Stackr Labs Integration**
---------------------------

### `src/stackr/mru.ts`

Initialize and configure the Stackr Micro-Rollup (MRU) instance.

    import { MicroRollup } from "@stackr/sdk";
    import { stackrConfig } from "../../stackr.config";
    import { machine } from "./machine";
    
    // Create a new MRU instance
    const mru = await MicroRollup({
      config: stackrConfig, // Configuration for the MRU
      stateMachines: [machine], // State machines used by the MRU
    });
    
    export { mru };
    

**Explanation:**

*   `MicroRollup`: Represents the Stackr MRU instance.
    
*   `stateMachines`: Array of state machines the MRU will manage.
    

* * *

**Main Application Logic**
--------------------------

### `src/index.ts`

Set up the Express server, define API endpoints, and integrate with Stackr.

    import { ActionConfirmationStatus, MicroRollup } from "@stackr/sdk";
    import { machine } from "./stackr/machine";
    import { Playground } from "@stackr/sdk/plugins";
    import express, { Request, Response } from "express";
    import { mru } from "./stackr/mru";
    import path from "path";
    import dotenv from "dotenv";
    import { verifyTypedData } from "ethers";
    
    dotenv.config();
    
    /**
     * Main function to set up and run the Stackr micro rollup server
     */
    const main = async () => {
      // Initialize the MRU instance
      await mru.init();
    
      // Initialize the Playground plugin for debugging and testing
      Playground.init(mru);
    
      // Set up Express server
      const app = express();
      const PORT = process.env.PORT || 3012;
    
      app.use(express.json());
    
      // Serve static files from the "public" directory
      app.use(express.static(path.join(__dirname, "public")));
    
      // Enable CORS for all routes
      app.use((_req, res, next) => {
        res.header("Access-Control-Allow-Origin", "*");
        res.header(
          "Access-Control-Allow-Headers",
          "Origin, X-Requested-With, Content-Type, Accept"
        );
        next();
      });
    
      // Serve the HTML file for the Tic Tac Toe game
      app.get("/", (req: Request, res: Response) => {
        res.sendFile(path.join(__dirname, "public", "tic_tac_toe.html"));
      });
    
      /**
       * GET /info - Retrieve information about the MicroRollup instance
       * Returns domain information and schema map for action reducers
       */
      app.get("/info", (req: Request, res: Response) => {
        const schemas = mru.getStfSchemaMap();
        const { name, version, chainId, verifyingContract, salt } =
          mru.config.domain;
        res.send({
          signingInstructions: "signTypedData(domain, schema.types, inputs)",
          domain: {
            name,
            version,
            chainId,
            verifyingContract,
            salt,
          },
          schemas,
        });
      });
    
      /**
       * POST /createGame - Submit a createGame action
       */
      app.post("/createGame", async (req: Request, res: Response) => {
        const reducerName = "createGame";
        const actionReducer = mru.getStfSchemaMap()[reducerName];
    
        if (!actionReducer) {
          res.status(400).send({ message: "NO_REDUCER_FOR_ACTION" });
          return;
        }
    
        try {
          const { msgSender, signature, inputs } = req.body;
    
          // Log received data for debugging
          console.log(`Received createGame action from: ${msgSender}`);
          console.log(`Inputs:`, inputs);
          console.log(`Signature: ${signature}`);
    
          const schemaTypes = {
            createGame: [
              { name: "owner", type: "address" },
              { name: "playerO", type: "address" },
              { name: "startedAt", type: "uint256" },
            ],
            Action: [
              { name: "name", type: "string" },
              { name: "inputs", type: "createGame" },
            ],
          };
    
          const createGameAction = {
            name: "createGame",
            inputs: inputs,
          };
    
    
          const actionParams = {
            name: reducerName,
            inputs,
            signature,
            msgSender,
          };
    
          // Submit the action to the MicroRollup instance
          const ack = await mru.submitAction(actionParams);
    
          // Wait for the action to be confirmed (C1 status)
          const { errors, logs } = await ack.waitFor(ActionConfirmationStatus.C1);
    
          if (errors?.length) {
            console.error("Action errors:", errors);
            throw new Error(errors[0].message);
          }
    
          res.status(201).send({ logs });
        } catch (e: any) {
          console.error("Error processing action:", e.message);
          res.status(400).send({ error: e.message });
        }
    
        return;
      });
    
      /**
       * POST /makeMove - Submit a makeMove action
       */
      app.post("/makeMove", async (req: Request, res: Response) => {
        const reducerName = "makeMove";
        const actionReducer = mru.getStfSchemaMap()[reducerName];
    
        if (!actionReducer) {
          res.status(400).send({ message: "NO_REDUCER_FOR_ACTION" });
          return;
        }
    
        try {
          const { msgSender, signature, inputs } = req.body;
    
          // Log received data for debugging
          console.log(`Received makeMove action from: ${msgSender}`);
          console.log(`Inputs:`, inputs);
          console.log(`Signature: ${signature}`);
    
          const actionParams = {
            name: reducerName,
            inputs,
            signature,
            msgSender,
          };
    
          // Submit the action to the MicroRollup instance
          const ack = await mru.submitAction(actionParams);
    
          // Wait for the action to be confirmed (C1 status)
          const { errors, logs } = await ack.waitFor(ActionConfirmationStatus.C1);
    
          if (errors?.length) {
            console.error("Action errors:", errors);
            throw new Error(errors[0].message);
          }
    
          res.status(201).send({ logs });
        } catch (e: any) {
          console.error("Error processing action:", e.message);
          res.status(400).send({ error: e.message });
        }
    
        return;
      });
    
      /**
       * POST /resetGame - Submit a resetGame action
       */
      app.post("/resetGame", async (req: Request, res: Response) => {
        const reducerName = "resetGame";
        const actionReducer = mru.getStfSchemaMap()[reducerName];
    
        if (!actionReducer) {
          res.status(400).send({ message: "NO_REDUCER_FOR_ACTION" });
          return;
        }
    
        try {
          const { msgSender, signature, inputs } = req.body;
    
          // Log received data for debugging
          console.log(`Received resetGame action from: ${msgSender}`);
          console.log(`Inputs:`, inputs);
          console.log(`Signature: ${signature}`);
    
          const actionParams = {
            name: reducerName,
            inputs,
            signature,
            msgSender,
          };
    
          // Submit the action to the MicroRollup instance
          const ack = await mru.submitAction(actionParams);
    
          // Wait for the action to be confirmed (C1 status)
          const { errors, logs } = await ack.waitFor(ActionConfirmationStatus.C1);
    
          if (errors?.length) {
            console.error("Action errors:", errors);
            throw new Error(errors[0].message);
          }
    
          res.status(201).send({ logs });
        } catch (e: any) {
          console.error("Error processing action:", e.message);
          res.status(400).send({ error: e.message });
        }
    
        return;
      });
    
      /**
       * GET /games - Retrieve all games from the state machine
       */
      app.get("/games", async (req: Request, res: Response) => {
        const { games } = machine.state;
        res.json(games);
      });
    
      /**
       * GET /games/:gameId - Retrieve a specific game by ID
       */
      app.get("/games/:gameId", async (req: Request, res: Response) => {
        const { gameId } = req.params;
        const { games } = machine.state;
    
        const game = games[gameId];
    
        if (!game) {
          res.status(404).send({ message: "GAME_NOT_FOUND" });
          return;
        }
    
        res.json(game);
      });
    
      // Start the server
      app.listen(PORT, () => {
        console.log(`Server running on http://localhost:${PORT}`);
      });
    };
    
    // Run the main function
    main().catch((error) => {
      console.error("Error starting the server:", error);
    });
    

**Explanation:**

*   `/info` Endpoint: Provides information about the MRU's domain and schemas.
    
*   `/:reducerName` Endpoint: Accepts actions to perform state transitions (e.g., `createGame`, `makeMove`, `resetGame`).
    
*   `/games` & `/games/:gameId` Endpoints: Retrieve all games or a specific game respectively.
    
*   **CORS**: Enabled for all routes to allow cross-origin requests.
    
*   **Static Files**: Serves the frontend from the `public` directory.
    

**Frontend Implementation**
---------------------------

### **A. Create the Frontend Directory**

Create a `public` directory inside your project root to store frontend files.

    mkdir src/public
    touch src/public/tic-tac-toe.html
    

### **B.** `src/public/tic-tac-toe.html`

Create the HTML and JavaScript frontend for the Tic Tac Toe game.

    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Tic Tac Toe - Stackr Labs</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                text-align: center;
                margin-top: 50px;
            }
            #board {
                display: grid;
                grid-template-columns: repeat(3, 100px);
                grid-gap: 5px;
                justify-content: center;
                margin: 20px auto;
            }
            .cell {
                width: 100px;
                height: 100px;
                border: 2px solid #333;
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 48px;
                cursor: pointer;
            }
            #message {
                margin-top: 20px;
                font-size: 24px;
            }
            #controls {
                margin-top: 30px;
            }
            button {
                padding: 10px 20px;
                font-size: 16px;
                margin: 0 10px;
                cursor: pointer;
            }
        </style>
    </head>
    <body>
    
    <h1>Tic Tac Toe</h1>
    
    <div id="gameControls">
        <button id="createGame">Create New Game</button>
    </div>
    
    <div id="board"></div>
    
    <div id="message"></div>
    
    <div id="controls">
        <button id="resetGame">Reset Game</button>
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/ethers@5.7.0/dist/ethers.umd.min.js"></script>
    <script>
        const provider = new ethers.providers.Web3Provider(window.ethereum);
        let signer;
        let gameId = null;
        let gameState = null;
    
        const messageElement = document.getElementById("message"); // Properly defined
    
        // Initialize the app
        async function init() {
            try {
                // Request account access if needed
                await provider.send("eth_requestAccounts", []);
                signer = provider.getSigner();
                console.log("Wallet connected:", await signer.getAddress());
    
                // Event listeners
                document.getElementById("createGame").addEventListener("click", createGame);
                document.getElementById("resetGame").addEventListener("click", resetGame);
            } catch (error) {
                console.error("Error connecting wallet:", error);
                messageElement.textContent = "Please connect your wallet.";
            }
        }
    
        // Function to create a new game
        async function createGame() {
            try {
                const msgSender = await signer.getAddress(); // Use the connected wallet's address
    
                // Define domain and schema types for signing data
                const domain = {
                        name: "Stackr MVP v0",
                        version: "1",
                        chainId: 11155111, // Sepolia Testnet
                        verifyingContract: "0x0000000000000000000000000000000000000000",
                        salt: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
                    };
    
                const schemaTypes = {
                    CreateGame: [
                        { name: "owner", type: "address" },
                        { name: "playerO", type: "address" },
                        { name: "startedAt", type: "uint256" },
                    ],
                    Action: [
                        { name: "name", type: "string" },
                        { name: "inputs", type: "CreateGame" },
                    ],
                };
    
                const playerO = prompt("Enter Player O's Ethereum Address:");
                if (!playerO || !ethers.utils.isAddress(playerO)) {
                    messageElement.textContent = "Invalid Player O address.";
                    return;
                }
    
                const createGameInputs = {
                    owner: msgSender,
                    playerO: playerO,
                    startedAt: Math.floor(Date.now() / 1000),
                };
    
                const createGameAction = {
                    name: "createGame",
                    inputs: createGameInputs,
                };
    
                // Sign the data using the connected wallet's signer
                const signature = await signer._signTypedData(
                    domain,
                    schemaTypes,
                    createGameAction
                );
    
                // Submit the createGame action to the backend
                const response = await fetch("/createGame", { // Ensure backend has '/createGame' route
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({
                        msgSender,
                        inputs: createGameInputs,
                        signature,
                    }),
                });
    
                if (!response.ok) {
                    const errorData = await response.json();
                    console.error("Backend Error:", errorData);
                    messageElement.textContent = `Error: ${errorData.error || "Unknown error."}`;
                    return;
                }
    
                const result = await response.json();
                console.log("Game created with logs:", result.logs);
    
                // Extract gameId from logs
                const gameCreatedLog = result.logs.find(log => log.name === "GameCreated");
                if (gameCreatedLog) {
                    gameId = gameCreatedLog.value;
                    messageElement.textContent = `Game created! Game ID: ${gameId}`;
                    fetchGameState();
                } else {
                    messageElement.textContent = "Failed to create game.";
                }
            } catch (error) {
                console.error("Error creating game:", error);
                messageElement.textContent = "Error creating game.";
            }
        }
    
        // Function to make a move
        async function makeMove(index) {
            if (!gameId) {
                messageElement.textContent = "Please create a game first.";
                return;
            }
    
            try {
                const msgSender = await signer.getAddress();
    
                // Define domain and schema types for signing data
                const domain = {
                        name: "Stackr MVP v0",
                        version: "1",
                        chainId: 11155111, // Sepolia Testnet
                        verifyingContract: "0x0000000000000000000000000000000000000000",
                        salt: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
                    };
    
                const schemaTypes = {
                    MakeMove: [
                        { name: "gameId", type: "string" },
                        { name: "index", type: "uint256" },
                        { name: "player", type: "string" }, // "X" or "O"
                    ],
                    Action: [
                        { name: "name", type: "string" },
                        { name: "inputs", type: "MakeMove" },
                    ],
                };
    
                const player = gameState.currentPlayer;
    
                const makeMoveInputs = {
                    gameId,
                    index,
                    player,
                };
    
                const makeMoveAction = {
                    name: "makeMove",
                    inputs: makeMoveInputs,
                };
    
                // Sign the data using the connected wallet's signer
                const signature = await signer._signTypedData(
                    domain,
                    schemaTypes,
                    makeMoveAction
                );
    
                // Submit the makeMove action to the backend
                const response = await fetch("/makeMove", { // Ensure backend has '/makeMove' route
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({
                        msgSender,
                        inputs: makeMoveInputs,
                        signature,
                    }),
                });
    
                if (!response.ok) {
                    const errorData = await response.json();
                    console.error("Backend Error:", errorData);
                    messageElement.textContent = `Error: ${errorData.error || "Unknown error."}`;
                    return;
                }
    
                const result = await response.json();
                console.log("Move made with logs:", result.logs);
    
                // Handle logs to update the UI
                const moveMadeLog = result.logs.find(log => log.name === "MoveMade");
                const gameEndedLog = result.logs.find(log => log.name === "GameEnded");
    
                if (moveMadeLog) {
                    messageElement.textContent = `Move made by ${makeMoveInputs.player}.`;
                    fetchGameState();
                } else if (gameEndedLog) {
                    const [endedGameId, winner] = gameEndedLog.value.split("::");
                    messageElement.textContent = winner === "Draw" ? "Game ended in a draw!" : `Player ${winner} wins!`;
                    fetchGameState();
                } else {
                    messageElement.textContent = "Failed to make move.";
                }
            } catch (error) {
                console.error("Error making move:", error);
                messageElement.textContent = "Error making move.";
            }
        }
    
        // Function to reset the game
        async function resetGame() {
            if (!gameId) {
                messageElement.textContent = "No game to reset.";
                return;
            }
    
            try {
                const msgSender = await signer.getAddress();
    
                // Define domain and schema types for signing data
                const domain = {
                        name: "Stackr MVP v0",
                        version: "1",
                        chainId: 11155111, // Sepolia Testnet
                        verifyingContract: "0x0000000000000000000000000000000000000000",
                        salt: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
                    };
                const schemaTypes = {
                    ResetGame: [
                        { name: "gameId", type: "string" },
                    ],
                    Action: [
                        { name: "name", type: "string" },
                        { name: "inputs", type: "ResetGame" },
                    ],
                };
    
                const resetGameInputs = {
                    gameId,
                };
    
                const resetGameAction = {
                    name: "resetGame",
                    inputs: resetGameInputs,
                };
    
                // Sign the data using the connected wallet's signer
                const signature = await signer._signTypedData(
                    domain,
                    schemaTypes,
                    resetGameAction
                );
    
                // Submit the resetGame action to the backend
                const response = await fetch("/resetGame", { // Ensure backend has '/resetGame' route
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({
                        msgSender,
                        inputs: resetGameInputs,
                        signature,
                    }),
                });
    
                if (!response.ok) {
                    const errorData = await response.json();
                    console.error("Backend Error:", errorData);
                    messageElement.textContent = `Error: ${errorData.error || "Unknown error."}`;
                    return;
                }
    
                const result = await response.json();
                console.log("Game reset with logs:", result.logs);
    
                const gameResetLog = result.logs.find(log => log.name === "GameReset");
    
                if (gameResetLog) {
                    messageElement.textContent = "Game has been reset!";
                    fetchGameState();
                } else {
                    messageElement.textContent = "Failed to reset game.";
                }
            } catch (error) {
                console.error("Error resetting game:", error);
                messageElement.textContent = "Error resetting game.";
            }
        }
    
        // Function to fetch the current game state
        async function fetchGameState() {
            try {
                const response = await fetch(`/games/${gameId}`);
                if (!response.ok) {
                    const errorData = await response.json();
                    console.error("Backend Error:", errorData);
                    messageElement.textContent = `Error: ${errorData.message || "Failed to fetch game state."}`;
                    return;
                }
                const data = await response.json();
                gameState = data;
                updateBoard();
                if (gameState.winner) {
                    messageElement.textContent = gameState.winner === "Draw" ? "Game ended in a draw!" : `Player ${gameState.winner} wins!`;
                } else {
                    messageElement.textContent = `Player ${gameState.currentPlayer}'s turn.`;
                }
            } catch (error) {
                console.error("Error fetching game state:", error);
                messageElement.textContent = "Error fetching game state.";
            }
        }
    
        // Function to update the board UI
        function updateBoard() {
            const boardElement = document.getElementById("board");
            boardElement.innerHTML = "";
            gameState.board.forEach((cell, index) => {
                const cellDiv = document.createElement("div");
                cellDiv.classList.add("cell");
                cellDiv.textContent = cell;
                if (cell === "") {
                    cellDiv.addEventListener("click", () => makeMove(index));
                }
                boardElement.appendChild(cellDiv);
            });
        }
    
        // Initialize the app on page load
        window.onload = init;
    </script>
    
    </body>
    </html>
    

### **Explanation:**

*   **Connect to MetaMask**: Requests the user to connect their wallet.
    
*   **Deterministic Wallet**: Generates a deterministic wallet based on the user's signature to avoid signing every transaction.
    
*   **Create Game**: Allows the user to create a new game by specifying Player O's Ethereum address.
    
*   **Make Move**: Enables players to make moves on the board by clicking on empty cells.
    
*   **Reset Game**: Resets the game to its initial state.
    
*   **Fetch Game State**: Retrieves the current state of the game to update the UI accordingly.
    

Testing Your Game
-----------------

After setting up the backend and frontend, you can start your development server:

    bun start
    

Open your browser and navigate to `http://localhost:3012` to see your Tic Tac Toe game in action!

![](https://storage.googleapis.com/papyrus_images/a5386c2754ebd4a5507fcbb524299e56b46c02a1867da9d2db8cdedf5525783d.png)

Conclusion
----------

Congratulations! 🎉 You’ve successfully built a decentralized Tic Tac Toe game using **Stackr Labs** and Micro Rollups. This project showcases how to manage game state on the blockchain while providing a simple and engaging user interface.

Feel free to extend the game further, add features like multiplayer support, or improve the UI. Happy coding! If you have any questions or need further assistance, please feel free to contact me. You can connect with me on [X / Twitter](https://x.com/kaushikk1704) or [LinkedIn](https://www.linkedin.com/in/kaushik-k-36b871219/).

---

*Originally published on [Kaushik](https://paragraph.com/@kaushik-2/building-a-decentralized-tic-tac-toe-game-with-stackr-labs)*
