A peer-to-peer TicTacToe game built with Pear Runtime — demonstrating real-time P2P connectivity with zero infrastructure.
┌───┬───┬───┐
│ X │ O │ X │
├───┼───┼───┤
│ │ X │ │
├───┼───┼───┤
│ O │ │ O │
└───┴───┴───┘
Left: Hosting a game with shareable room code · Right: Game finished with winner
- Screenshots
- Overview
- Prerequisites
- Quick Start
- How It Works
- Code Walkthrough
- Testing Locally
- Key Concepts
- Troubleshooting
- Resources
TicTacPear demonstrates how to build real-time multiplayer games using Pear's P2P infrastructure. Unlike traditional client-server architectures, this game connects players directly through a Distributed Hash Table (DHT), eliminating the need for:
- Central game servers
- User accounts or authentication
- Backend infrastructure
- Ongoing hosting costs
Two players can connect from anywhere in the world using only a 64-character room code.
Key Technologies:
- Pear Runtime — P2P application runtime
- Hyperswarm — P2P networking and NAT traversal
- pear-electron — Desktop GUI framework
-
Node.js (v18 or higher)
node --version
-
Pear Runtime (installed globally)
npm install -g pear
-
Verify installation
pear --version
# Clone the repository
git clone https://github.com/yourusername/tictacpear.git
cd tictacpear
# Install dependencies
npm install
# Run in development mode
pear run --dev .The app will open in a new window. Click "Create Room" to host a game, then share the room code with your opponent.
┌─────────────────────────────────────────────────────────────────┐
│ TicTacPear │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Player 1 │◄────── Hyperswarm ──────►│ Player 2 │ │
│ │ (Host) │ DHT │ (Guest) │ │
│ │ "X" │ │ "O" │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ │ Direct P2P Connection │ │
│ └────────────────────────────────────────┘ │
│ │
│ No servers. No accounts. Just peers. │
│ │
└─────────────────────────────────────────────────────────────────┘
Player 1 (Host) Player 2 (Guest)
─────────────── ─────────────────
│ │
│ 1. Generate random 32-byte topic │
│─────────────────────────────────► │
│ (displayed as 64-char hex) │
│ │
│ 2. Join DHT as SERVER │
│ swarm.join(topic, {server: true}) │
│ │
│ │ 3. Paste room code
│ │ Join DHT as CLIENT
│ │ swarm.join(topic, {client: true})
│ │
│◄─────────────────────────────────────────│
│ 4. DHT connects peers directly │
│ (NAT traversal handled automatically)│
│ │
│ 5. Bidirectional stream established │
│◄────────────────────────────────────────►│
│ │
│ 6. Exchange game moves as JSON │
│ { type: "move", index: 4 } │
│◄────────────────────────────────────────►│
Hyperswarm uses a Distributed Hash Table for peer discovery. Here's what happens under the hood:
- Topic Creation: The host generates a random 32-byte buffer as the "topic"
- Announcement: The host announces their presence on the DHT under this topic
- Discovery: When a guest joins with the same topic, the DHT facilitates discovery
- Connection: Hyperswarm handles NAT traversal using techniques like UDP hole-punching
- Stream: Once connected, peers communicate over an encrypted duplex stream
This all happens without any central coordination server.
tictacpear/
├── package.json # Pear configuration and dependencies
├── index.js # Pear runtime entry point
├── index.html # Game UI structure
├── app.js # Game logic + P2P networking
└── style.css # Visual styling
The entry point initializes the Pear runtime and sets up the Electron bridge:
/* global Pear */
import Runtime from 'pear-electron'
import Bridge from 'pear-bridge'
// Initialize the Pear-Electron runtime
const runtime = new Runtime()
// Bridge enables HTTP-like patterns over P2P
const bridge = new Bridge()
await bridge.ready()
// Start the runtime with the bridge
const pipe = runtime.start({ bridge })
// Cleanup handler when the app closes
Pear.teardown(async () => {
if (pipe && typeof pipe.end === 'function') {
pipe.end()
} else if (pipe && typeof pipe.destroy === 'function') {
pipe.destroy()
}
})Key Points:
pear-electronprovides the desktop window frameworkpear-bridgeenables P2P networking in the renderer processPear.teardown()ensures clean shutdown
The game maintains a simple state object:
const state = {
board: Array(9).fill(null), // 3x3 board as flat array
mySymbol: null, // 'X' or 'O'
currentTurn: 'X', // X always starts
isMyTurn: false, // Controls UI interaction
gameOver: false, // Prevents moves after game ends
isHost: false, // Host is X, guest is O
connected: false, // P2P connection status
peer: null // The peer connection stream
}Win detection checks all possible winning combinations:
const WINNING_COMBOS = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
[0, 4, 8], [2, 4, 6] // Diagonals
]
function checkWinner() {
for (const combo of WINNING_COMBOS) {
const [a, b, c] = combo
if (state.board[a] &&
state.board[a] === state.board[b] &&
state.board[a] === state.board[c]) {
return { winner: state.board[a], combo }
}
}
// Check for draw
if (state.board.every(cell => cell !== null)) {
return { winner: null, combo: null }
}
return null // Game continues
}import Hyperswarm from 'hyperswarm'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
async function hostGame() {
// Create a new Hyperswarm instance
swarm = new Hyperswarm()
// Generate a random 32-byte topic
topic = crypto.randomBytes(32)
// Listen for incoming connections
swarm.on('connection', (conn, info) => {
handleConnection(conn)
})
// Join the DHT as a server (accepting connections)
const discovery = swarm.join(topic, {
server: true, // Accept incoming connections
client: false // Don't seek other servers
})
// Wait for the topic to be fully announced
await discovery.flushed()
// Display the room code (topic as hex string)
const roomCode = b4a.toString(topic, 'hex')
console.log('Share this code:', roomCode)
}async function joinGame(roomCodeHex) {
swarm = new Hyperswarm()
// Convert the hex string back to a 32-byte buffer
topic = b4a.from(roomCodeHex, 'hex')
swarm.on('connection', (conn, info) => {
handleConnection(conn)
})
// Join the DHT as a client (seeking servers)
swarm.join(topic, {
server: false, // Don't accept connections
client: true // Seek servers announcing this topic
})
// Wait for connection attempts to complete
await swarm.flush()
}function handleConnection(conn) {
// Prevent multiple connections
if (state.peer) {
conn.destroy()
return
}
state.peer = conn
state.connected = true
// Handle incoming data
conn.on('data', (data) => {
const message = JSON.parse(data.toString())
handleMessage(message)
})
// Handle disconnection
conn.on('close', () => {
state.peer = null
state.connected = false
})
}The game uses a simple JSON protocol:
// Sending a move
function sendMove(cellIndex) {
const message = { type: 'move', index: cellIndex }
state.peer.write(JSON.stringify(message))
}
// Requesting a new game
function requestNewGame() {
const message = { type: 'new-game' }
state.peer.write(JSON.stringify(message))
}
// Handling incoming messages
function handleMessage(message) {
switch (message.type) {
case 'move':
// Opponent made a move
applyMove(message.index)
break
case 'new-game':
// Opponent wants a rematch
resetGame()
break
}
}To test the P2P functionality on a single machine, run two instances with separate storage:
Terminal 1 (Host):
pear run --dev .Terminal 2 (Guest):
pear run --dev --tmp-store .- In Terminal 1, click "Create Room"
- Copy the 64-character room code
- In Terminal 2, paste the code and click "Join Room"
- Play!
| Traditional Architecture | P2P Architecture |
|---|---|
| Requires game servers | No servers needed |
| Ongoing hosting costs | Zero infrastructure cost |
| Single point of failure | Resilient by design |
| Server location affects latency | Direct peer connection |
| Requires user accounts | Anonymous by default |
- DHT-based discovery: Find peers without central servers
- NAT traversal: Connect through firewalls and routers
- Encrypted streams: Secure communication by default
- Topic-based networking: Simple room code system
- Desktop apps with web tech: HTML, CSS, JavaScript
- Built-in P2P primitives: Hyperswarm, Hypercore, etc.
- No backend required: Everything runs on the client
- Cross-platform: macOS, Windows, Linux
Your Pear installation may need updating. Run:
pear run pear://runtime- Check firewall settings: Hyperswarm needs UDP access
- Try a different network: Some corporate networks block P2P
- Verify room code: Ensure all 64 characters are copied
Ensure package.json includes:
{
"type": "module"
}The teardown handler should clean up connections. Ensure index.js includes proper cleanup:
Pear.teardown(async () => {
// Cleanup logic
})- Pear Documentation
- Hyperswarm GitHub
- pear-electron GitHub
- Holepunch (Tether Data)
- KEET - Flagship P2P App
ISC
Built with 🍐 Pear Runtime

