npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@playertwo/core

v0.2.0

Published

Multiplayer without networking. Engine-agnostic multiplayer SDK with host-authoritative state sync.

Readme

@playertwo/core

Engine-agnostic multiplayer SDK with host-authoritative state synchronization.

Simple, clean, works with any game engine.


Features

  • Declarative API - Define state and actions, not networking code
  • Host-authoritative - Host runs the game, clients mirror state
  • Automatic sync - Efficient diff/patch algorithm for bandwidth optimization
  • Engine-agnostic - Works with Phaser, Unity, Godot, Three.js, etc.
  • Transport-agnostic - P2P, WebSocket, UDP - your choice
  • TypeScript - Full type safety

Installation

pnpm add @playertwo/core

Quick Start

1. Define Your Game

import { defineGame } from '@playertwo/core';

const game = defineGame({
  setup: ({ playerIds }) => ({
    players: Object.fromEntries(
      playerIds.map(id => [id, { x: 100, y: 100, score: 0 }])
    )
  }),

  actions: {
    move: {
      apply(state, playerId, input) {
        state.players[playerId].x = input.x;
        state.players[playerId].y = input.y;
      }
    }
  },

  onPlayerJoin(state, playerId) {
    state.players[playerId] = { x: 100, y: 100, score: 0 };
  },

  onPlayerLeave(state, playerId) {
    delete state.players[playerId];
  }
});

2. Create Runtime

import { GameRuntime } from '@playertwo/core';
import { TrysteroTransport } from '@playertwo/transport-trystero';

const transport = new TrysteroTransport({
  roomId: 'game-room-123',
  isHost: true
});

const runtime = new GameRuntime(game, transport, {
  isHost: true,
  playerIds: ['p1']
});

3. Use in Your Game

// Submit actions
runtime.submitAction('move', { x: 150, y: 200 });

// Listen for state changes
runtime.onChange((state) => {
  console.log('Players:', state.players);
});

// Broadcast custom events
runtime.broadcastEvent('explosion', { x: 100, y: 200 });

// Listen for events
runtime.onEvent('explosion', (senderId, eventName, payload) => {
  console.log(`Explosion at ${payload.x}, ${payload.y}`);
});

How It Works

Host-Authoritative Architecture

┌─────────────────────────────────────┐
│           HOST                      │
│  • Runs game logic                  │
│  • Applies actions                  │
│  • Syncs state to clients (20 FPS)  │
└─────────────────┬───────────────────┘
                  │
         state patches (diff)
                  │
         ┌────────┴────────┐
         ↓                 ↓
┌─────────────────┐ ┌─────────────────┐
│    CLIENT 1     │ │    CLIENT 2     │
│  • Sends actions│ │  • Sends actions│
│  • Mirrors state│ │  • Mirrors state│
└─────────────────┘ └─────────────────┘

Key Points:

  • Host is authoritative (runs real physics/logic)
  • Clients send inputs, receive state updates
  • Efficient diff/patch algorithm minimizes bandwidth
  • Default 20 FPS state sync (configurable)

Core Concepts

State

Plain JavaScript objects describing your game:

{
  players: {
    p1: { x: 100, y: 100, health: 100 },
    p2: { x: 200, y: 200, health: 100 }
  },
  bullets: [],
  gameState: 'playing'
}

Rules:

  • Must be JSON-serializable
  • No functions or class instances
  • Mutated directly (no immutability required)

Actions

The only way to modify state:

actions: {
  shoot: {
    apply(state, playerId, input) {
      state.bullets.push({
        x: input.x,
        y: input.y,
        ownerId: playerId
      });
    }
  }
}

Flow:

  1. Player calls runtime.submitAction('shoot', { x: 100, y: 200 })
  2. Host applies action immediately
  3. Host broadcasts state patch to clients
  4. Clients receive and apply patch

Lifecycle Hooks

Handle player join/leave:

onPlayerJoin(state, playerId) {
  state.players[playerId] = { x: 100, y: 100 };
},

onPlayerLeave(state, playerId) {
  delete state.players[playerId];
}

Integration with Game Engines

Phaser

Use @playertwo/phaser for automatic sprite syncing:

import { PhaserAdapter } from '@playertwo/phaser';

class GameScene extends Phaser.Scene {
  create() {
    const adapter = new PhaserAdapter(runtime, this);

    const player = this.physics.add.sprite(100, 100, 'player');
    adapter.trackSprite(player, `player-${adapter.myId}`);

    // That's it! Sprite automatically syncs across network
  }
}

See Phaser Adapter docs for details.

Other Engines

For Unity, Godot, Three.js, etc.:

runtime.onChange((state) => {
  // Update your game objects based on state
  for (const [id, player] of Object.entries(state.players)) {
    updateGameObject(id, player.x, player.y);
  }
});

Transports

@playertwo/core is transport-agnostic. Choose your backend:

P2P (Serverless)

import { TrysteroTransport } from '@playertwo/transport-trystero';

const transport = new TrysteroTransport({
  roomId: 'game-123',
  isHost: true // URL-based host selection
});

Pros: Zero server costs, simple setup Cons: NAT traversal issues (5-10% of users)

WebSocket (Coming Soon)

import { WebSocketTransport } from '@playertwo/transport-ws';

const transport = new WebSocketTransport({
  url: 'wss://your-server.com'
});

Pros: Reliable, works for everyone Cons: Requires server hosting

Custom Transport

Implement the Transport interface:

interface Transport {
  send(message: WireMessage, targetId?: string): void;
  onMessage(handler: (msg: WireMessage, senderId: string) => void): () => void;
  onPeerJoin(handler: (peerId: string) => void): () => void;
  onPeerLeave(handler: (peerId: string) => void): () => void;
  getPlayerId(): string;
  getPeerIds(): string[];
  isHost(): boolean;
}

API Reference

Full documentation: API Reference


Examples

Examples

See interactive demos:

Run them locally:

cd @playertwo/demos
pnpm dev

Testing

# Run tests
pnpm test

# Watch mode
pnpm test:watch

# Coverage
pnpm test:coverage

Current coverage: 96%+ on core algorithms


Development

# Build
pnpm build

# Watch mode
pnpm dev

# Clean
pnpm clean

Architecture

@playertwo/core (this package)
  ↓
  ├─ defineGame()    - Declarative game definition
  ├─ GameRuntime     - State management, action execution
  ├─ sync.ts         - Diff/patch algorithm
  └─ transport.ts    - Transport interface

Used by:
  ├─ @playertwo/phaser          - Phaser 3 adapter
  ├─ @playertwo/transport-*     - Transport implementations
  └─ Your game                - Direct usage

Design Philosophy

Host-Authoritative

Host runs the real game, clients mirror state. Simple, works with any physics engine.

Why not deterministic?

  • Most games don't need it
  • Works with existing Phaser/Unity code
  • AI can generate code easily
  • Faster development

Declarative

Define state and actions once, not networking code.

// ❌ Imperative networking
socket.on('player-moved', (data) => {
  players[data.id].x = data.x;
});

// ✅ Declarative actions
actions: {
  move: {
    apply(state, playerId, input) {
      state.players[playerId].x = input.x;
    }
  }
}

Transport-Agnostic

Swap networking backends without changing game code:

// Development: P2P
const transport = new TrysteroTransport({ roomId: 'dev-123' });

// Production: WebSocket
const transport = new WebSocketTransport({ url: 'wss://game.com' });

Roadmap

  • [x] Host-authoritative mode
  • [x] P2P transport (Trystero)
  • [x] Phaser adapter
  • [x] Comprehensive tests (96%+ coverage)
  • [ ] WebSocket transport
  • [ ] Unity C# bindings
  • [ ] Godot GDScript bindings
  • [ ] Client prediction (optional advanced mode)

License

MIT - See LICENSE


Contributing

See CONTRIBUTING.md

Areas needing help:

  • WebSocket transport implementation
  • Unity/Godot adapters
  • Example games
  • Documentation improvements

Support