DEV Community

UtkarshVarma
UtkarshVarma

Posted on

Building Your First Game on Midnight: A Complete Developer Tutorial

A comprehensive guide to building privacy-preserving blockchain games using Midnight Network and this starter template

Table of Contents

  1. Introduction to Midnight
  2. Why Build Games on Midnight?
  3. Understanding the Architecture
  4. Prerequisites & Setup
  5. Project Structure Deep Dive
  6. Writing Smart Contracts in Compact
  7. Building a Simple Game: Rock-Paper-Scissors
  8. Frontend Integration with React
  9. Testing Your Contracts
  10. Deployment Guide
  11. Advanced Patterns
  12. Troubleshooting & Best Practices
  13. Next Steps & Resources

Introduction to Midnight

What is Midnight?

Midnight is a privacy-first blockchain platform that enables developers to build decentralized applications (dApps) with built-in data protection. Unlike traditional blockchains where all transactions are publicly visible, Midnight uses zero-knowledge proofs (ZK proofs) to allow users to prove the validity of their actions without revealing sensitive information.

Key Features

  • Privacy by Default: All contract state can be kept private using ZK proofs
  • Shielded & Unshielded Assets: Support for both private and public funds
  • Compact Language: A purpose-built smart contract language optimized for privacy
  • Full-Stack dApp Support: Complete SDK for frontend integration
  • Testnet Ready: Preview network available for development and testing

The Privacy Advantage for Games

Imagine building a poker game where:

  • Players can prove they have valid cards without revealing them
  • Bet amounts can be hidden until the reveal phase
  • Game state remains private until the appropriate time

This is the power of Midnight for game development.


Why Build Games on Midnight?

Traditional Blockchain Gaming Problems

  1. Transparent State: On Ethereum, all contract state is visible. In a card game, this means everyone can see your hand.
  2. Front-Running: Miners/validators can see pending transactions and exploit them.
  3. Privacy Concerns: Players' strategies and holdings are public knowledge.

Midnight's Solutions

Problem Midnight Solution
Visible game state Private state with ZK proofs
Front-running attacks Encrypted transactions
Strategy exposure Shielded computations
Unfair advantages Verifiable random numbers

Perfect Use Cases

  • Card Games: Poker, Blackjack, Trading Card Games
  • Strategy Games: Hidden unit movements, fog of war
  • Betting/Prediction Markets: Private bets with public outcomes
  • Turn-Based Games: Hidden moves revealed after both players commit
  • NFT Games: Private ownership verification

Understanding the Architecture

The Midnight Stack

+--------------------------------------------------+
|                   Frontend (React)               |
|    - Wallet Integration (Lace)                   |
|    - Contract Interaction                        |
|    - State Subscriptions                         |
+--------------------------------------------------+
                        |
                        v
+--------------------------------------------------+
|              Midnight JS SDK                      |
|    - Contract Deployment                         |
|    - Transaction Building                        |
|    - Proof Generation                            |
+--------------------------------------------------+
                        |
                        v
+--------------------------------------------------+
|              Midnight Network                     |
|    +----------------+  +---------------------+   |
|    | Public Ledger  |  | Private State Store |   |
|    | (Visible)      |  | (Encrypted)         |   |
|    +----------------+  +---------------------+   |
+--------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Key Components Explained

1. Smart Contracts (Compact Language)

Written in Compact, a domain-specific language for privacy-preserving smart contracts. Compact compiles to WebAssembly and generates ZK circuits.

2. Public Ledger

The blockchain state visible to all participants. Used for:

  • Contract addresses
  • Public game outcomes
  • Scores and rankings

3. Private State

Encrypted state only accessible to its owner. Used for:

  • Player hands
  • Hidden moves
  • Secret strategies

4. Proof Server

Generates zero-knowledge proofs that verify transactions without revealing private data.

5. Indexer

Queries blockchain state efficiently for frontend applications.


Prerequisites & Setup

Required Tools

# Check if you have the prerequisites
node -v   # Required: v23+
npm -v    # Required: v11+
docker -v # Required: Latest
Enter fullscreen mode Exit fullscreen mode

Step 1: Install Git LFS

Git LFS handles large binary files in the repository.

# macOS
brew install git-lfs

# Ubuntu/Debian
sudo apt-get install git-lfs

# Fedora/RHEL
sudo dnf install git-lfs

# Initialize Git LFS
git lfs install
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Compact Compiler

Compact is Midnight's smart contract language.

# Install Compact tools
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh

# Install compiler version 0.27.0
compact update +0.27.0

# Verify installation
compact check
Enter fullscreen mode Exit fullscreen mode

Step 3: Install Lace Wallet

The Lace wallet browser extension is required for interacting with Midnight.

  1. Install from Chrome Web Store
  2. Create a new wallet or import existing
  3. Switch to Midnight Preview Network
  4. Get test tokens from Faucet

Step 4: Clone and Setup the Starter Template

# Clone the repository
git clone https://github.com/MeshJS/midnight-starter-template.git
cd midnight-starter-template

# Install dependencies
npm install

# Build the project (compiles contracts)
npm run build
Enter fullscreen mode Exit fullscreen mode

Step 5: Configure Environment Variables

# Create environment files from templates
cp counter-cli/.env_template counter-cli/.env
cp frontend-vite-react/.env_template frontend-vite-react/.env
Enter fullscreen mode Exit fullscreen mode

Edit the files with your configuration:

counter-cli/.env

MY_PREVIEW_MNEMONIC="your twelve word mnemonic phrase here"
MY_UNDEPLOYED_UNSHIELDED_ADDRESS=''
Enter fullscreen mode Exit fullscreen mode

frontend-vite-react/.env

VITE_CONTRACT_ADDRESS=""
Enter fullscreen mode Exit fullscreen mode

Project Structure Deep Dive

Directory Layout

midnight-starter-template/
├── counter-contract/           # Smart contract code
│   ├── src/
│   │   ├── counter.compact     # Main contract logic
│   │   ├── witnesses.ts        # Private state types
│   │   ├── managed/            # Compiled artifacts (generated)
│   │   └── test/               # Contract tests
│   └── package.json
│
├── counter-cli/                # CLI deployment tools
│   ├── src/
│   │   ├── config.ts           # Network configurations
│   │   └── test/               # Integration tests
│   └── package.json
│
├── frontend-vite-react/        # React frontend
│   ├── src/
│   │   ├── pages/              # Page components
│   │   ├── components/         # UI components
│   │   └── modules/midnight/   # Midnight SDK integration
│   │       ├── wallet-widget/  # Wallet connection
│   │       └── counter-sdk/    # Contract interaction
│   └── package.json
│
├── package.json                # Root workspace config
└── turbo.json                  # Build orchestration
Enter fullscreen mode Exit fullscreen mode

Understanding Each Component

counter-contract/

Contains the smart contract written in Compact. This is where your game logic lives.

counter-cli/

Command-line tools for deploying and testing contracts without a frontend.

frontend-vite-react/

A complete React application with:

  • Wallet integration
  • Contract deployment UI
  • Real-time state updates

Writing Smart Contracts in Compact

The Compact Language

Compact is designed specifically for privacy-preserving smart contracts. Let's understand its key concepts:

Basic Contract Structure

Here's the counter contract from the starter template:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// Public state - visible to everyone
export ledger round: Counter;

// Circuit function - modifies state with ZK proof
export circuit increment(): [] {
  round.increment(1);
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts

1. Ledger (Public State)

export ledger round: Counter;
Enter fullscreen mode Exit fullscreen mode
  • ledger declares public blockchain state
  • Counter is a built-in type for counting
  • Everyone can see this value

2. Circuits

export circuit increment(): [] {
  round.increment(1);
}
Enter fullscreen mode Exit fullscreen mode
  • circuit defines a function that modifies state
  • Generates a ZK proof when called
  • [] indicates no return value

3. Private State (Witnesses)

Private state is defined in TypeScript alongside the contract:

// witnesses.ts
export type CounterPrivateState = {
  privateCounter: number;
};

export const createPrivateState = (value: number): CounterPrivateState => {
  return {
    privateCounter: value,
  };
};

export const witnesses = {};
Enter fullscreen mode Exit fullscreen mode

Data Types in Compact

Type Description Example
Counter Incrementable integer ledger score: Counter;
Uint<N> Unsigned integer (N bits) Uint<64>
Boolean True/False Boolean
Bytes<N> Fixed-size bytes Bytes<32>
Vector<T, N> Fixed-size array Vector<Uint<8>, 52>
Map<K, V> Key-value mapping Map<Address, Counter>

Building a Simple Game: Rock-Paper-Scissors

Now let's build a real game! We'll create a Rock-Paper-Scissors game that demonstrates Midnight's privacy features.

Game Design

+-------------------+     +-------------------+
|     Player 1      |     |     Player 2      |
+-------------------+     +-------------------+
| Private: move     |     | Private: move     |
| (rock/paper/      |     | (rock/paper/      |
|  scissors)        |     |  scissors)        |
+-------------------+     +-------------------+
          |                        |
          v                        v
+------------------------------------------------+
|              Public Ledger                      |
| - Game state (waiting/committed/revealed)       |
| - Player addresses                              |
| - Winner (after reveal)                         |
+------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Step 1: Create the Contract

Create a new file rps-contract/src/rps.compact:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// Game states
// 0 = Waiting for players
// 1 = Player 1 committed
// 2 = Both committed, waiting for reveal
// 3 = Game complete

// Public ledger state
export ledger gameState: Uint<8>;
export ledger player1Commitment: Bytes<32>;
export ledger player2Commitment: Bytes<32>;
export ledger winner: Uint<8>; // 0 = none, 1 = player1, 2 = player2, 3 = draw

// Move encoding: 1 = Rock, 2 = Paper, 3 = Scissors

// Player 1 commits their move (hashed)
export circuit commitMove1(commitment: Bytes<32>): [] {
  assert(gameState == 0, "Game already started");
  player1Commitment = commitment;
  gameState = 1;
}

// Player 2 commits their move (hashed)
export circuit commitMove2(commitment: Bytes<32>): [] {
  assert(gameState == 1, "Waiting for player 1");
  player2Commitment = commitment;
  gameState = 2;
}

// Reveal phase - players reveal moves
export circuit reveal(
  move1: Uint<8>,
  salt1: Bytes<32>,
  move2: Uint<8>,
  salt2: Bytes<32>
): [] {
  assert(gameState == 2, "Not in reveal phase");

  // Verify commitments match
  // In production, use proper hash verification

  // Determine winner
  // Rock (1) beats Scissors (3)
  // Paper (2) beats Rock (1)
  // Scissors (3) beats Paper (2)

  if (move1 == move2) {
    winner = 3; // Draw
  } else if (
    (move1 == 1 && move2 == 3) ||
    (move1 == 2 && move2 == 1) ||
    (move1 == 3 && move2 == 2)
  ) {
    winner = 1; // Player 1 wins
  } else {
    winner = 2; // Player 2 wins
  }

  gameState = 3;
}

// Reset for new game
export circuit resetGame(): [] {
  gameState = 0;
  winner = 0;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Define Private State

Create rps-contract/src/witnesses.ts:

export type RPSPrivateState = {
  myMove: number;      // 1=Rock, 2=Paper, 3=Scissors
  mySalt: string;      // Random salt for commitment
  hasCommitted: boolean;
};

export const createPrivateState = (): RPSPrivateState => {
  return {
    myMove: 0,
    mySalt: '',
    hasCommitted: false,
  };
};

// Helper to generate commitment hash
export const generateCommitment = (move: number, salt: string): string => {
  // In production, use proper cryptographic hash
  return `${move}-${salt}`;
};

export const witnesses = {};
Enter fullscreen mode Exit fullscreen mode

Step 3: Frontend Integration

Create a React component for the game:

// pages/rps/index.tsx
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

// Move icons
const moves = {
  1: { name: 'Rock', emoji: '🪨' },
  2: { name: 'Paper', emoji: '📄' },
  3: { name: 'Scissors', emoji: '✂️' },
};

export const RockPaperScissors = () => {
  const [gameState, setGameState] = useState(0);
  const [selectedMove, setSelectedMove] = useState<number | null>(null);
  const [winner, setWinner] = useState(0);

  // Subscribe to contract state
  useEffect(() => {
    // Contract state subscription would go here
    // Similar to the counter example
  }, []);

  const commitMove = async (move: number) => {
    setSelectedMove(move);
    // Generate salt
    const salt = crypto.randomUUID();
    // Generate commitment
    const commitment = await generateCommitment(move, salt);
    // Call contract
    // await contract.commitMove1(commitment);
  };

  const renderGameState = () => {
    switch (gameState) {
      case 0:
        return (
          <div className="text-center">
            <h2 className="text-xl mb-4">Choose Your Move</h2>
            <div className="flex gap-4 justify-center">
              {Object.entries(moves).map(([key, { name, emoji }]) => (
                <Button
                  key={key}
                  onClick={() => commitMove(Number(key))}
                  className="text-4xl p-8"
                >
                  {emoji}
                  <span className="ml-2 text-sm">{name}</span>
                </Button>
              ))}
            </div>
          </div>
        );
      case 1:
        return (
          <div className="text-center">
            <h2 className="text-xl">Waiting for opponent...</h2>
            <p>Your move is committed and hidden!</p>
          </div>
        );
      case 2:
        return (
          <div className="text-center">
            <h2 className="text-xl">Both players committed!</h2>
            <Button onClick={() => revealMoves()}>
              Reveal Moves
            </Button>
          </div>
        );
      case 3:
        return (
          <div className="text-center">
            <h2 className="text-2xl mb-4">
              {winner === 1 && 'Player 1 Wins!'}
              {winner === 2 && 'Player 2 Wins!'}
              {winner === 3 && "It's a Draw!"}
            </h2>
            <Button onClick={() => resetGame()}>
              Play Again
            </Button>
          </div>
        );
    }
  };

  return (
    <div className="min-h-screen bg-background py-12 px-4">
      <div className="max-w-2xl mx-auto">
        <Card>
          <CardHeader>
            <CardTitle className="text-center text-3xl">
              Rock Paper Scissors
            </CardTitle>
            <p className="text-center text-muted-foreground">
              Privacy-preserving game on Midnight
            </p>
          </CardHeader>
          <CardContent>
            {renderGameState()}
          </CardContent>
        </Card>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Frontend Integration with React

The Provider Pattern

The starter template uses a powerful provider pattern for managing blockchain state. Let's understand how it works.

Architecture Overview

<ThemeProvider>
  <MidnightMeshProvider>        {/* Wallet Connection */}
    <LocalStorageProvider>       {/* Persistence */}
      <ProvidersProvider>        {/* Blockchain Providers */}
        <DeployedProvider>       {/* Contract Instance */}
          <App />
        </DeployedProvider>
      </ProvidersProvider>
    </LocalStorageProvider>
  </MidnightMeshProvider>
</ThemeProvider>
Enter fullscreen mode Exit fullscreen mode

Key Hooks

1. useWallet() - Wallet Connection

import { useWallet } from '@/modules/midnight/wallet-widget/hooks/useWallet';

const MyComponent = () => {
  const {
    status,           // Connection status
    connectWallet,    // Connect function
    disconnectWallet, // Disconnect function
    addresses,        // Wallet addresses
    balances          // Token balances
  } = useWallet();

  return (
    <div>
      {status?.status === 'connected' ? (
        <p>Connected: {addresses?.shieldedAddress}</p>
      ) : (
        <Button onClick={connectWallet}>Connect Wallet</Button>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

2. useContractSubscription() - Real-time State

import { useContractSubscription } from '@/modules/midnight/counter-sdk/hooks/use-contract-subscription';

const GameComponent = () => {
  const {
    deployedContractAPI,  // Contract instance
    derivedState,         // Current state
    onDeploy,             // Deploy new contract
    providers             // Blockchain providers
  } = useContractSubscription();

  // State updates automatically via RxJS observables
  useEffect(() => {
    console.log('Counter value:', derivedState?.round);
  }, [derivedState]);

  const handleIncrement = async () => {
    await deployedContractAPI?.increment();
  };

  return (
    <div>
      <p>Current Count: {derivedState?.round || 0}</p>
      <Button onClick={handleIncrement}>Increment</Button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Contract Controller Pattern

The ContractController class manages all contract interactions:

// api/contractController.ts
export class ContractController implements ContractControllerInterface {
  readonly deployedContractAddress: ContractAddress;
  readonly state$: Rx.Observable<DerivedState>;

  // Deploy a new contract
  static async deploy(
    providers: CounterProviders,
    logger: Logger
  ): Promise<ContractController> {
    const deployedContract = await deployContract(providers, {
      contract: counterContractInstance,
      initialPrivateState: createPrivateState(0),
    });
    return new ContractController(deployedContract, providers, logger);
  }

  // Join an existing contract
  static async join(
    contractAddress: ContractAddress,
    providers: CounterProviders,
    logger: Logger
  ): Promise<ContractController> {
    const deployedContract = await findDeployedContract(providers, {
      contractAddress,
      contract: counterContractInstance,
    });
    return new ContractController(deployedContract, providers, logger);
  }

  // Call contract function
  async increment(): Promise<void> {
    await this.deployedContract.callTx.increment();
  }
}
Enter fullscreen mode Exit fullscreen mode

Observable State Management

The template uses RxJS for reactive state updates:

// State combines multiple sources
this.state$ = Rx.combineLatest([
  // Public ledger state from indexer
  providers.publicDataProvider
    .contractStateObservable(this.deployedContractAddress, { type: 'all' })
    .pipe(Rx.map((state) => Counter.ledger(state.data))),

  // Private state from local storage
  Rx.from(providers.privateStateProvider.get(privateStateId)),

  // User action status
  this.turns$,
]).pipe(
  Rx.map(([ledgerState, privateState, userActions]) => ({
    round: ledgerState.round,
    privateState,
    turns: userActions,
  }))
);
Enter fullscreen mode Exit fullscreen mode

Testing Your Contracts

Unit Testing with Vitest

The template includes a testing framework for contracts:

// counter.test.ts
import { CounterSimulator } from "./simulators/simulator";
import { describe, it, expect } from "vitest";

describe("Counter smart contract", () => {
  it("displays initial values", () => {
    const simulator = CounterSimulator.deployContract(0);
    const ledgerState = simulator.as("player1").getLedger();
    const privateState = simulator.as("player1").getPrivateState();

    expect(ledgerState.round).toEqual(0n);
    expect(privateState).toEqual({ privateCounter: 0 });
  });

  it("increments the counter correctly", () => {
    const simulator = CounterSimulator.deployContract(0);

    // Call increment
    const newState = simulator.as("player1").increment();

    expect(newState.round).toEqual(1n);
  });

  it("maintains private state separately", () => {
    const simulator = CounterSimulator.deployContract(0);
    simulator.createPrivateState("player2", 100);

    // Each player has their own private state
    const p1State = simulator.as("player1").getPrivateState();
    const p2State = simulator.as("player2").getPrivateState();

    expect(p1State.privateCounter).toEqual(0);
    expect(p2State.privateCounter).toEqual(100);
  });
});
Enter fullscreen mode Exit fullscreen mode

Running Tests

# Run contract tests
cd counter-contract
npm run test

# Run all tests
npm run test
Enter fullscreen mode Exit fullscreen mode

Integration Testing

For full integration tests with the blockchain:

// integration.test.ts
import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';

describe("Integration tests", () => {
  it("deploys and interacts with contract on testnet", async () => {
    // Setup providers
    const providers = await setupProviders();

    // Deploy contract
    const deployed = await deployContract(providers, {
      contract: counterContract,
      initialPrivateState: { privateCounter: 0 },
    });

    // Interact
    await deployed.callTx.increment();

    // Verify state
    const state = await deployed.getState();
    expect(state.round).toEqual(1n);
  });
});
Enter fullscreen mode Exit fullscreen mode

Deployment Guide

Local Development (Undeployed Network)

For rapid development without connecting to the testnet:

# Terminal 1: Start local Midnight node
npm run setup-standalone

# This starts:
# - Local Midnight node (ws://127.0.0.1:9944)
# - Indexer (http://127.0.0.1:8088)
# - Proof server (http://127.0.0.1:6300)

# Terminal 2: Start frontend
npm run dev:frontend
Enter fullscreen mode Exit fullscreen mode

Preview Network (Testnet)

For testing on the public testnet:

  1. Get Test Tokens

  2. Configure Environment

   # counter-cli/.env
   MY_PREVIEW_MNEMONIC="your wallet mnemonic phrase"
Enter fullscreen mode Exit fullscreen mode
  1. Start Frontend
   npm run dev:frontend
Enter fullscreen mode Exit fullscreen mode

Production Build

# Build everything for production
npm run build-production

# Output:
# - counter-contract/dist/  - Compiled contract
# - frontend-vite-react/dist/ - Built frontend
Enter fullscreen mode Exit fullscreen mode

Deployment Checklist

  • [ ] Contract compiled successfully
  • [ ] Tests passing
  • [ ] Environment variables configured
  • [ ] Wallet funded with tSTAR
  • [ ] Contract deployed and address saved
  • [ ] Frontend configured with contract address

Advanced Patterns

Pattern 1: Multi-Player Game State

// Define player structure
export ledger players: Map<Address, PlayerState>;
export ledger currentTurn: Address;
export ledger maxPlayers: Uint<8>;
export ledger playerCount: Counter;

// Join game
export circuit joinGame(): [] {
  assert(playerCount < maxPlayers, "Game full");
  // Add player logic
  playerCount.increment(1);
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Commit-Reveal Scheme

For hidden moves that are revealed later:

// Client-side commitment
const createCommitment = async (move: number, salt: string) => {
  const encoder = new TextEncoder();
  const data = encoder.encode(`${move}:${salt}`);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  return new Uint8Array(hashBuffer);
};

// Store commitment
await contract.commitMove(commitment);

// Later, reveal
await contract.revealMove(move, salt);
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Time-Locked Actions

export ledger deadline: Uint<64>;
export ledger roundStartTime: Uint<64>;

export circuit startRound(): [] {
  // Set deadline for this round
  deadline = currentTime() + 300; // 5 minutes
}

export circuit submitMove(move: Uint<8>): [] {
  assert(currentTime() < deadline, "Round ended");
  // Process move
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Random Number Generation

// Use block hash as randomness source
export circuit generateRandom(): Uint<64> {
  // Combine multiple entropy sources
  let seed = blockHash ^ transactionHash ^ timestamp;
  return seed;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Token Staking for Games

export ledger stakes: Map<Address, Uint<64>>;
export ledger prizePool: Counter;

export circuit stake(amount: Uint<64>): [] {
  // Transfer tokens to contract
  transferToContract(amount);
  stakes[caller] = amount;
  prizePool.increment(amount);
}

export circuit claimPrize(): [] {
  assert(isWinner(caller), "Not winner");
  let prize = prizePool;
  prizePool = 0;
  transferFromContract(caller, prize);
}
Enter fullscreen mode Exit fullscreen mode

Troubleshooting & Best Practices

Common Issues

1. Compact Compilation Errors

Error: Unknown type 'MyType'
Enter fullscreen mode Exit fullscreen mode

Solution: Ensure all types are imported from CompactStandardLibrary or defined.

2. Proof Generation Timeout

Error: Proof server timeout
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Check proof server is running
  • Increase timeout in configuration
  • Simplify circuit logic

3. Wallet Connection Issues

Error: No Midnight provider found
Enter fullscreen mode Exit fullscreen mode

Solution:

  • Install Lace wallet extension
  • Ensure wallet is on correct network
  • Refresh page after installing

4. State Not Updating

Solution: Check observable subscription:

useEffect(() => {
  const sub = contract.state$.subscribe(setState);
  return () => sub.unsubscribe();
}, [contract]);
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Minimize Circuit Complexity

// Good: Simple, focused circuits
export circuit incrementScore(): [] {
  score.increment(1);
}

// Avoid: Complex circuits increase proof time
export circuit complexOperation(): [] {
  // Many operations...
}
Enter fullscreen mode Exit fullscreen mode

2. Use Appropriate State Visibility

// Public: Scores, rankings, game outcomes
export ledger publicScore: Counter;

// Private: Player hands, strategies, hidden moves
// (handled in TypeScript witnesses)
Enter fullscreen mode Exit fullscreen mode

3. Handle Errors Gracefully

const handleAction = async () => {
  try {
    setLoading(true);
    await contract.someAction();
  } catch (error) {
    console.error('Transaction failed:', error);
    setError('Transaction failed. Please try again.');
  } finally {
    setLoading(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

4. Test Edge Cases

it("handles maximum players", () => {
  const sim = createSimulator();
  for (let i = 0; i < MAX_PLAYERS; i++) {
    sim.joinGame();
  }
  expect(() => sim.joinGame()).toThrow("Game full");
});
Enter fullscreen mode Exit fullscreen mode

Next Steps & Resources

Learning Path

  1. Beginner: Complete this tutorial, modify the counter contract
  2. Intermediate: Build the Rock-Paper-Scissors game
  3. Advanced: Create a multi-player card game

Official Resources

Community

  • Discord: Join the Midnight developer community
  • GitHub: Contribute to open-source tools
  • Forum: Ask questions and share projects

Starter Template Resources


Quick Reference Card

Compact Cheat Sheet

// Pragma (required)
pragma language_version >= 0.19;

// Imports
import CompactStandardLibrary;

// Public state (ledger)
export ledger myCounter: Counter;
export ledger myValue: Uint<64>;
export ledger myMap: Map<Address, Uint<64>>;

// Circuits (state-changing functions)
export circuit myFunction(param: Uint<64>): [] {
  myCounter.increment(param);
}

// Pure functions (no state change)
export pure function add(a: Uint<64>, b: Uint<64>): Uint<64> {
  return a + b;
}

// Assertions
assert(condition, "Error message");
Enter fullscreen mode Exit fullscreen mode

npm Scripts Reference

npm install          # Install dependencies
npm run build        # Compile contracts & build all
npm run dev:frontend # Start development server
npm run test         # Run tests
npm run setup-standalone  # Start local network
npm run build-production  # Production build
Enter fullscreen mode Exit fullscreen mode

File Locations

File Purpose
counter-contract/src/*.compact Smart contract logic
counter-contract/src/witnesses.ts Private state types
frontend-vite-react/src/modules/midnight/ SDK integration
counter-cli/.env CLI configuration
frontend-vite-react/.env Frontend configuration

Conclusion

You now have everything you need to start building privacy-preserving games on Midnight! The combination of:

  • Compact for smart contract logic
  • Zero-knowledge proofs for privacy
  • React + TypeScript for the frontend
  • RxJS for reactive state management

...gives you a powerful toolkit for creating games that were previously impossible on blockchain.

Start with the counter example, experiment with the concepts, and gradually build more complex games. The privacy features of Midnight open up entirely new categories of blockchain games where fairness and secrecy can coexist.

Happy building!


Built with the Midnight Starter Template

Webisoft Development Labs

Top comments (0)