A comprehensive guide to building privacy-preserving blockchain games using Midnight Network and this starter template
Table of Contents
- Introduction to Midnight
- Why Build Games on Midnight?
- Understanding the Architecture
- Prerequisites & Setup
- Project Structure Deep Dive
- Writing Smart Contracts in Compact
- Building a Simple Game: Rock-Paper-Scissors
- Frontend Integration with React
- Testing Your Contracts
- Deployment Guide
- Advanced Patterns
- Troubleshooting & Best Practices
- 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
- Transparent State: On Ethereum, all contract state is visible. In a card game, this means everyone can see your hand.
- Front-Running: Miners/validators can see pending transactions and exploit them.
- 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) | |
| +----------------+ +---------------------+ |
+--------------------------------------------------+
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
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
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
Step 3: Install Lace Wallet
The Lace wallet browser extension is required for interacting with Midnight.
- Install from Chrome Web Store
- Create a new wallet or import existing
- Switch to Midnight Preview Network
- 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
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
Edit the files with your configuration:
counter-cli/.env
MY_PREVIEW_MNEMONIC="your twelve word mnemonic phrase here"
MY_UNDEPLOYED_UNSHIELDED_ADDRESS=''
frontend-vite-react/.env
VITE_CONTRACT_ADDRESS=""
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
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);
}
Key Concepts
1. Ledger (Public State)
export ledger round: Counter;
-
ledgerdeclares public blockchain state -
Counteris a built-in type for counting - Everyone can see this value
2. Circuits
export circuit increment(): [] {
round.increment(1);
}
-
circuitdefines 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 = {};
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) |
+------------------------------------------------+
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;
}
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 = {};
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>
);
};
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>
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>
);
};
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>
);
};
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();
}
}
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,
}))
);
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);
});
});
Running Tests
# Run contract tests
cd counter-contract
npm run test
# Run all tests
npm run test
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);
});
});
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
Preview Network (Testnet)
For testing on the public testnet:
-
Get Test Tokens
- Visit Midnight Faucet
- Enter your wallet address
- Receive tSTAR tokens
Configure Environment
# counter-cli/.env
MY_PREVIEW_MNEMONIC="your wallet mnemonic phrase"
- Start Frontend
npm run dev:frontend
Production Build
# Build everything for production
npm run build-production
# Output:
# - counter-contract/dist/ - Compiled contract
# - frontend-vite-react/dist/ - Built frontend
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);
}
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);
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
}
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;
}
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);
}
Troubleshooting & Best Practices
Common Issues
1. Compact Compilation Errors
Error: Unknown type 'MyType'
Solution: Ensure all types are imported from CompactStandardLibrary or defined.
2. Proof Generation Timeout
Error: Proof server timeout
Solution:
- Check proof server is running
- Increase timeout in configuration
- Simplify circuit logic
3. Wallet Connection Issues
Error: No Midnight provider found
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]);
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...
}
2. Use Appropriate State Visibility
// Public: Scores, rankings, game outcomes
export ledger publicScore: Counter;
// Private: Player hands, strategies, hidden moves
// (handled in TypeScript witnesses)
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);
}
};
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");
});
Next Steps & Resources
Learning Path
- Beginner: Complete this tutorial, modify the counter contract
- Intermediate: Build the Rock-Paper-Scissors game
- Advanced: Create a multi-player card game
Official Resources
- Midnight Documentation: docs.midnight.network
- Compact Language Guide: docs.midnight.network/compact
- Lace Wallet: lace.io
- Faucet (Test Tokens): faucet.preview.midnight.network
Community
- Discord: Join the Midnight developer community
- GitHub: Contribute to open-source tools
- Forum: Ask questions and share projects
Starter Template Resources
- Live Demo: counter.nebula.builders
- Template Repo: MeshJS/midnight-starter-template
- Mesh SDK: meshjs.dev
- Webisoft Development Labs: webisoft.com
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");
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
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
Top comments (0)