
A Deep Dive into Building Your Own Rollup: RollupKit Architecture Explained
In recent years, rollups have become a popular approach to scaling blockchains. But building a rollup from scratch is no small feat—especially for those new to decentralized development. This is where RollupKit comes in. RollupKit is a simplified version of a Sovereign Rollup that offers developers a sandbox environment to understand the architecture and key components of rollups. In this post, we’ll go over the different parts of the RollupKit architecture, including data availability, state...
Rollups Made Blockchain Simpler
Blockchain technology, like Ethereum, is amazing. As a developer, I always think that how blockchain works and how the transaction is executed in the backend. When I started learning about the blockchain in 2022, one of the main concepts that came into my learning is layer 2 and rollups. What is L2 ? What is a Rollup ? Is every L2 a Rollup ?To answer all these questions first let us understand what is L2 ?What is L2 (layer 2) ?Layer 2 is another layer below the main blockchain (L1) which is c...

Dare to Deploy: Building Your L3 Rollup Chain with Arbitrum and Avail Explained🚀
Hey there👋, intrepid blockchain builder! If you've ever dreamt of setting up your own Layer 3 (L3) rollup chain, it's time to roll up those sleeves and make it happen. We’re diving into DYOR-inggg (Deploying your own rollup ) an Arbitrum Orbit L3 chain using Avail as the data availability (DA) layer on top of the Arbitrum Sepolia testnet. Don’t worry—I'll keep this guide straightforward and peppered with some idioms to keep things lively. Let’s jump right in! 🌟Prerequisites: ...
<100 subscribers

A Deep Dive into Building Your Own Rollup: RollupKit Architecture Explained
In recent years, rollups have become a popular approach to scaling blockchains. But building a rollup from scratch is no small feat—especially for those new to decentralized development. This is where RollupKit comes in. RollupKit is a simplified version of a Sovereign Rollup that offers developers a sandbox environment to understand the architecture and key components of rollups. In this post, we’ll go over the different parts of the RollupKit architecture, including data availability, state...
Rollups Made Blockchain Simpler
Blockchain technology, like Ethereum, is amazing. As a developer, I always think that how blockchain works and how the transaction is executed in the backend. When I started learning about the blockchain in 2022, one of the main concepts that came into my learning is layer 2 and rollups. What is L2 ? What is a Rollup ? Is every L2 a Rollup ?To answer all these questions first let us understand what is L2 ?What is L2 (layer 2) ?Layer 2 is another layer below the main blockchain (L1) which is c...

Dare to Deploy: Building Your L3 Rollup Chain with Arbitrum and Avail Explained🚀
Hey there👋, intrepid blockchain builder! If you've ever dreamt of setting up your own Layer 3 (L3) rollup chain, it's time to roll up those sleeves and make it happen. We’re diving into DYOR-inggg (Deploying your own rollup ) an Arbitrum Orbit L3 chain using Avail as the data availability (DA) layer on top of the Arbitrum Sepolia testnet. Don’t worry—I'll keep this guide straightforward and peppered with some idioms to keep things lively. Let’s jump right in! 🌟Prerequisites: ...
Share Dialog
Share Dialog


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!
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!
Before we dive in, make sure you have the following tools installed:
Node.js: Download and install from Node.js
Git: Download and install from Git
Bun: Installation guide can be found here
Stackr CLI: Install globally using:
bun install -g @stackr/cli
mkdir tic-tac-toe
cd tic-tac-toe
bunx @stackr/cli@latest init
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
In this section, we’ll define the game state using Stackr’s state management.
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)]
);
}
}
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.
Next, we’ll add the logic to handle player moves and check for wins.
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
StateMachine: Initializes the state machine with an ID, state class, initial state, and available transitions.
src/stackr/transitions.ts
Define the possible actions/transitions in the game.
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.
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.
Set up the Express server, define API endpoints, and integrate with Stackr.
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.
Create a public directory inside your project root to store frontend files.
mkdir src/public
touch src/public/tic-tac-toe.html
Create the HTML and JavaScript frontend for the Tic Tac Toe game.
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.
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!

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 or LinkedIn.
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!
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!
Before we dive in, make sure you have the following tools installed:
Node.js: Download and install from Node.js
Git: Download and install from Git
Bun: Installation guide can be found here
Stackr CLI: Install globally using:
bun install -g @stackr/cli
mkdir tic-tac-toe
cd tic-tac-toe
bunx @stackr/cli@latest init
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
In this section, we’ll define the game state using Stackr’s state management.
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)]
);
}
}
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.
Next, we’ll add the logic to handle player moves and check for wins.
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
StateMachine: Initializes the state machine with an ID, state class, initial state, and available transitions.
src/stackr/transitions.ts
Define the possible actions/transitions in the game.
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.
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.
Set up the Express server, define API endpoints, and integrate with Stackr.
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.
Create a public directory inside your project root to store frontend files.
mkdir src/public
touch src/public/tic-tac-toe.html
Create the HTML and JavaScript frontend for the Tic Tac Toe game.
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.
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!

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 or LinkedIn.
// 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,
};
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);
});
<!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>
// 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,
};
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);
});
<!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>
No comments yet