nostr-arena
v0.1.6
Published
Nostr-based real-time multiplayer game arena. No server required.
Maintainers
Readme
nostr-arena
Nostr-based real-time battle room for multiplayer games. No server required.
Features
- P2P Matchmaking: Create and join rooms via shareable URLs
- Real-time State Sync: Send game state to opponents with automatic throttling
- Connection Health: Heartbeat and disconnect detection
- Reconnection: Automatic reconnection from localStorage
- Rematch: Built-in rematch flow
- Framework Agnostic: Core classes work anywhere, React hooks included
Installation
npm install nostr-arena nostr-toolsQuick Start
React
import { useArena } from 'nostr-arena/react';
interface MyGameState {
score: number;
position: { x: number; y: number };
}
function Game() {
const { roomState, opponent, createRoom, joinRoom, sendState, leaveRoom } =
useArena<MyGameState>({
gameId: 'my-game',
});
const handleCreate = async () => {
const url = await createRoom();
// Share this URL with opponent
navigator.clipboard.writeText(url);
};
const handleMove = (x: number, y: number) => {
sendState({ score: 100, position: { x, y } });
};
return (
<div>
<p>Status: {roomState.status}</p>
{roomState.status === 'idle' && <button onClick={handleCreate}>Create Room</button>}
{opponent && <p>Opponent score: {opponent.gameState?.score ?? 0}</p>}
</div>
);
}Vanilla JavaScript
import { Arena } from 'nostr-arena';
interface MyGameState {
score: number;
}
const room = new Arena<MyGameState>({
gameId: 'my-game',
relays: ['wss://relay.damus.io'],
});
// Register event callbacks (chainable)
room
.onOpponentJoin((pubkey) => {
console.log('Opponent joined:', pubkey);
})
.onOpponentState((state) => {
console.log('Opponent score:', state.score);
})
.onOpponentDisconnect(() => {
console.log('Opponent disconnected');
});
// Create a room
room.connect();
const url = await room.create();
console.log('Share this URL:', url);
// Send state updates
room.sendState({ score: 100 });
// Game over
room.sendGameOver('win', 500);
// Cleanup
room.disconnect();API
ArenaConfig
interface ArenaConfig {
gameId: string; // Required: unique game identifier
relays?: string[]; // Nostr relay URLs (default: public relays)
roomExpiry?: number; // Room expiration in ms (default: 600000 = 10 min)
heartbeatInterval?: number; // Heartbeat interval in ms (default: 3000)
disconnectThreshold?: number; // Disconnect threshold in ms (default: 10000)
stateThrottle?: number; // State update throttle in ms (default: 100)
}Room States
| Status | Description | | -------- | ------------------------------------- | | idle | No room active | | creating | Creating a new room | | waiting | Waiting for opponent to join | | joining | Joining an existing room | | ready | Both players connected, ready to play | | playing | Game in progress | | finished | Game ended |
Events (Callbacks)
| Event | Parameters | Description | | ------------------ | -------------------------------- | -------------------------- | | opponentJoin | (publicKey: string) | Opponent joined the room | | opponentState | (state: TGameState) | Opponent sent state update | | opponentDisconnect | () | Opponent disconnected | | opponentGameOver | (reason: string, score?: number) | Opponent game over | | rematchRequested | () | Opponent requested rematch | | rematchStart | (newSeed: number) | Rematch starting | | error | (error: Error) | Error occurred |
Node.js / Proxy Support
For Node.js environments or when you need proxy support, call configureProxy() before creating any rooms:
import { configureProxy, Arena } from 'nostr-arena';
// Call once at startup
configureProxy();
// Now create rooms as usual
const room = new Arena({ gameId: 'my-game' });This function:
- Configures the
wspackage for Node.js WebSocket support - Reads proxy URL from environment variables:
HTTPS_PROXY,HTTP_PROXY, orALL_PROXY - No-op in browser environments (browsers handle proxies at OS level)
Required packages for Node.js:
npm install ws # Required for Node.js
npm install https-proxy-agent # Required for proxy supportTesting
import { MockArena } from 'nostr-arena/testing';
const mock = new MockArena<MyGameState>({ gameId: 'test' });
// Simulate opponent actions
mock.simulateOpponentJoin('pubkey123');
mock.simulateOpponentState({ score: 100 });
mock.simulateOpponentDisconnect();How It Works
- Room Creation: Host publishes a replaceable event (kind 30078) with room info
- Joining: Guest fetches room event, sends join notification (kind 25000)
- State Sync: Players send ephemeral events (kind 25000) with game state
- Heartbeat: Periodic heartbeat events detect disconnections
- Cleanup: Ephemeral events are not stored by relays (no garbage)
License
MIT
