Foto Profilo Dario Rosina

Dario Rosina

Scacchi online multiplayer gratis

Indice ▾

Piccola applicazione di scacchi multiplayer che gira interamente nel browser.

L’interfaccia usa cm-chessboard per la scacchiera e chess.js per la logica delle mosse; la comunicazione tra giocatori avviene in P2P (WebRTC) tramite Trystero (torrent strategy).

Versione online: Gioca subito →

Anteprima dell'applicazione

Il modo più semplice per iniziare

La comunicazione P2P tra dispositivi funziona nella maggior parte dei casi, ma dipende da molti fattori di rete che non sempre sono sotto il tuo controllo: il tipo di router, le impostazioni del firewall, l'operatore telefonico, la presenza di una VPN.

Per evitare qualsiasi problema e avere la migliore esperienza possibile — latenza minima, connessione immediata, nessuna configurazione — il modo più semplice e affidabile è collegare tutti i dispositivi alla stessa rete Wi-Fi.

In questo scenario i dispositivi si trovano e si parlano direttamente sulla rete locale, senza dover attraversare NAT, firewall o relay esterni. È la condizione ideale.

Se invece stai giocando con qualcuno che si trova in un'altra abitazione o su una rete mobile, la connessione funzionerà comunque nella grande maggioranza dei casi — ma se qualcosa non va, continua a leggere per capire come diagnosticarlo.

Il P2P non è magia: è un processo in tre fasi

Quando due browser si connettono con Trystero (strategia torrent), devono superare tre “gradini” in sequenza. Se uno di questi si inceppa, la connessione non parte.

1

Signaling — “ci sei?”

Trystero usa tracker BitTorrent pubblici (via WebSocket) per mettere in contatto i due peer. È come un centralinista: si limita a presentare le due parti, non passa i dati del gioco.

2

Discovery — “ti trovo”

Dopo il signaling, i peer si scambiano le coordinate di rete (candidati ICE). Ognuno dice: “mi puoi raggiungere a questi indirizzi”. Se le reti sono incompatibili, ci si perde qui.

3

Data channel — “parliamo”

Il canale WebRTC è attivo. Da qui i dati viaggiano direttamente tra i due browser. La qualità dipende dalla latenza: sotto 80ms è ottima, sopra 300ms diventa problematica.

Fasi del processo P2P

Cosa può impedire la connessione?

Ogni fase ha i suoi nemici specifici. Ecco le cause più comuni di fallimento, raggruppate per layer.

Layer 1 — Tracker irraggiungibili

Icona firewall

Firewall aziendale

Blocca WebSocket outbound verso domini BitTorrent

Icona estensione browser

Estensioni browser

uBlock Origin, Privacy Badger bloccano i tracker torrent

Icona lucchetto WebSocket

HTTPS + ws://

Pagina HTTPS non può connettersi a tracker non sicuri (ws://)

Layer 2 — Peer non si trovano

Icona NAT simmetrico

NAT simmetrico

Comune su reti aziendali e connessioni 4G/5G. STUN non basta.

Icona isolamento Wi-Fi

AP isolation

Reti Wi-Fi pubbliche (hotel, aeroporti) isolano i client tra loro

Icona doppio NAT

Doppio NAT

Router dietro router: i candidati ICE non si incrociano

Layer 3 — Canale lento o bloccato

Icona firewall UDP

Firewall UDP

WebRTC usa UDP di default. Senza UDP la connessione non parte

Icona VPN con blocco WebRTC

VPN con blocco WebRTC

Molte VPN bloccano WebRTC per prevenire IP leak

Icona Firefox hardened

Firefox hardened

media.peerconnection.enabled = false disabilita WebRTC

Il problema del NAT: una mappa

Non tutti i router sono uguali. Il tipo di NAT determina se la connessione WebRTC riesce in autonomia o ha bisogno di aiuto.

Tipo di NAT WebRTC diretto? Soluzione da implementare
Full cone Nessuna
Restricted cone STUN sufficiente
Simmetrico No Serve server TURN
Carrier-grade NAT (4G/5G) Spesso no TURN raccomandato
IPv6 disponibile Quasi sempre Nessuna

Nota importante: Trystero con strategia torrent non include un server TURN. Se entrambi i peer sono dietro NAT simmetrico la connessione fallisce silenziosamente, senza alcun errore visibile.

Il test diagnostico: come capire cosa non va

Il modulo p2p-diagnostics.js, che viene eseguito dall'applicazione di scacchi multiplayer quando si seleziona la stanza "Scacchi - diagnostica", esegue tre test in sequenza, uno per ogni layer. Ecco cosa controlla e cosa significa ogni risultato.

Raggiungibilità dei tracker BitTorrent

Il modulo tenta di aprire una connessione WebSocket verso tre tracker pubblici e misura il tempo di risposta.

OKtracker.openwebtorrent.com — connesso in 120ms
OKtracker.webtorrent.dev — connesso in 180ms
Erroretracker.btorrent.xyz — timeout (firewall?)

Se tutti i tracker falliscono: la rete blocca il signaling. Controlla firewall, VPN, estensioni browser. Senza signaling, nessun peer si trova mai.

Trovare un altro peer nella room

Il modulo entra in una room Trystero temporanea e aspetta fino a 15 secondi che un secondo peer si unisca. Questo test richiede due dispositivi o due tab aperti contemporaneamente.

OKPeer trovato in 3.2s — id: a4f92b1e…
TimeoutNessun peer in 15s — stesso room ID? AP isolation?

Se il signaling funziona ma nessun peer si trova: probabile AP isolation (reti Wi-Fi pubbliche) o mismatch di appId tra i client.

Latenza del canale dati (ping/pong)

Una volta connessi, vengono inviati 5 ping e misurato il tempo di risposta (RTT). La media determina la qualità della connessione.

Ping #1
45ms
Ping #2
52ms
Ping #3
38ms
Ping #4
55ms
Ping #5
43ms
Media
47ms

< 80ms

Ottimo. Connessione diretta, nessun relay

80–300ms

Accettabile. Possibile relay TURN attivo

> 300ms

Critico. Rete congestionata o relay lontano

Interpretare il report finale

Il modulo restituisce un oggetto JSON con il risultato di ogni layer. Ecco come leggere i casi più comuni.

Tutto OKTutti e 3 i layer funzionano — la rete è compatibile P2P
Layer 1 erroreTracker irraggiungibili → firewall o estensioni bloccano
Layer 2 errorePeer non trovati → AP isolation, NAT simmetrico, room ID sbagliato
Layer 3 erroreNessun pong → canale UDP bloccato, serve TURN
Layer 3 warningLatenza > 80ms → connessione via relay, non diretta

Soluzione universale: aggiungere un server TURN alla configurazione WebRTC risolve quasi tutti i casi di Layer 2 e Layer 3. Senza TURN, circa il 15–20% delle reti reali non riesce a stabilire la connessione diretta.

In sintesi

Trystero P2P richiede che tre cose funzionino tutte insieme. Se una sola si inceppa, i peer non si parlano:

Icona lucchetto WebSocket

Tracker raggiungibili

WebSocket verso tracker pubblici non bloccato

Icona NAT simmetrico

NAT attraversabile

STUN sufficiente, o TURN configurato come fallback

Icona firewall UDP

UDP aperto

WebRTC usa UDP; firewall troppo restrittivi lo bloccano

Il modulo p2p-diagnostics.js testa questi tre layer in sequenza e restituisce un report strutturato con causa e suggerimento per ogni problema trovato.

Il modulo p2p-diagnostics.js

/**
 * p2p-diagnostics.js
 * Diagnostica P2P per applicazioni Trystero (torrent strategy)
 *
 *
 * Uso:
 *   import { runDiagnostics } from './p2p-diagnostics.js';
 *   const result = await runDiagnostics({ onProgress: console.log });
 */

import { joinRoom } from 'https://cdn.jsdelivr.net/npm/trystero@0.21.0/torrent.js';

// ─── Configurazione ────────────────────────────────────────────────────────────

const CONFIG = {
  DIAGNOSTIC_ROOM:   '__p2p_diag_' + Math.random().toString(36).slice(2, 8),
  TIMEOUT_TRACKER:   10_000,
  TIMEOUT_DISCOVERY: 15_000,
  TIMEOUT_PING:      10_000,
  TRACKERS: [
    'wss://tracker.openwebtorrent.com',
    'wss://tracker.webtorrent.dev',
    'wss://tracker.btorrent.xyz',
  ],
  PING_COUNT: 5,
  APP_ID: 'p2p-diag-v1',
};

export const DiagResult = { OK: 'ok', WARNING: 'warning', ERROR: 'error' };

function now() {
  return typeof performance !== 'undefined' ? performance.now() : Date.now();
}

// ─── LAYER 1: Raggiungibilità tracker ─────────────────────────────────────────

function testSingleTracker(url) {
  return new Promise(resolve => {
    const t0 = now();
    let ws;
    const done = (reachable, error) => {
      try { ws?.close(); } catch (_) {}
      resolve({ url, reachable, latencyMs: Math.round(now() - t0), error });
    };
    try {
      ws = new WebSocket(url);
      const tid = setTimeout(() => done(false, 'timeout'), CONFIG.TIMEOUT_TRACKER);
      ws.onopen  = () => { clearTimeout(tid); done(true); };
      ws.onerror = e  => { clearTimeout(tid); done(false, 'connection error'); };
    } catch (e) {
      done(false, e.message);
    }
  });
}

async function testTrackers(onProgress) {
  onProgress?.({ phase: 'tracker', status: 'running',
    message: 'Test connessione tracker BitTorrent...' });

  const results  = await Promise.all(CONFIG.TRACKERS.map(testSingleTracker));
  const reachable = results.filter(r => r.reachable);
  const allFailed = reachable.length === 0;

  onProgress?.({ phase: 'tracker', status: allFailed ? 'error' : 'done',
    message: allFailed
      ? '❌ Nessun tracker raggiungibile (firewall? rete offline?)'
      : `✅ ${reachable.length}/${results.length} tracker raggiungibili`,
    details: results });

  return {
    layer: 1, name: 'Signaling (tracker)',
    result: allFailed
      ? DiagResult.ERROR
      : reachable.length < results.length ? DiagResult.WARNING : DiagResult.OK,
    trackers: results,
    reachableCount: reachable.length,
  };
}

// ─── LAYER 2 + 3: Discovery e Data Channel ────────────────────────────────────

async function testPeerConnection(onProgress) {
  onProgress?.({ phase: 'discovery', status: 'running',
    message: 'Apertura room Trystero diagnostica...' });

  return new Promise(resolve => {
    let room, discoveryResolved = false;
    let layer2Result = null, layer3Result = null;
    const peers = {};
    let pingInterval = null, pingSeq = 0;
    const pendingPings = {};

    const finish = () => {
      clearInterval(pingInterval);
      try { room?.leave?.(); } catch (_) {}
      resolve({ layer2: layer2Result, layer3: layer3Result });
    };

    // Timeout discovery
    const discoveryTimeout = setTimeout(() => {
      if (discoveryResolved) return;
      discoveryResolved = true;
      layer2Result = {
        layer: 2, name: 'Peer discovery', result: DiagResult.ERROR,
        message: '❌ Nessun peer trovato — stesso room ID? Signaling ok?',
        peersFound: 0,
      };
      onProgress?.({ phase: 'discovery', status: 'error', message: layer2Result.message });
      layer3Result = { layer: 3, name: 'Data channel', result: DiagResult.ERROR,
        message: '⏭️ Saltato (nessun peer disponibile)' };
      finish();
    }, CONFIG.TIMEOUT_DISCOVERY);

    try {
      room = joinRoom({ appId: CONFIG.APP_ID }, CONFIG.DIAGNOSTIC_ROOM);
    } catch (e) {
      clearTimeout(discoveryTimeout);
      layer2Result = { layer: 2, name: 'Peer discovery', result: DiagResult.ERROR,
        message: `❌ Impossibile creare la room: ${e.message}` };
      layer3Result = { layer: 3, name: 'Data channel', result: DiagResult.ERROR,
        message: '⏭️ Saltato' };
      return resolve({ layer2: layer2Result, layer3: layer3Result });
    }

    const [sendPing, getPing] = room.makeAction('ping');
    const [sendPong, getPong] = room.makeAction('pong');

    room.onPeerJoin(peerId => {
      peers[peerId] = { connectedAt: now(), latencies: [] };
      if (discoveryResolved) return;
      discoveryResolved = true;
      clearTimeout(discoveryTimeout);

      layer2Result = {
        layer: 2, name: 'Peer discovery', result: DiagResult.OK,
        message: `✅ Peer trovato (id: ${peerId.slice(0, 8)}…)`, peersFound: 1,
      };
      onProgress?.({ phase: 'discovery', status: 'done', message: layer2Result.message });
      onProgress?.({ phase: 'ping', status: 'running',
        message: `Misuro latenza RTT (${CONFIG.PING_COUNT} ping)...` });

      // Invia ping periodici
      let sent = 0;
      pingInterval = setInterval(() => {
        if (sent >= CONFIG.PING_COUNT) { clearInterval(pingInterval); return; }
        const seq = ++pingSeq;
        pendingPings[seq] = { peerId, sentAt: now() };
        sendPing({ seq }, peerId);
        sent++;
      }, 500);

      // Timeout conclusione ping
      setTimeout(() => {
        const allLat = Object.values(peers).flatMap(p => p.latencies);
        if (allLat.length === 0) {
          layer3Result = {
            layer: 3, name: 'Data channel (latenza)', result: DiagResult.ERROR,
            message: '❌ Nessun pong ricevuto — data channel bloccato (NAT simmetrico? firewall UDP?)',
          };
        } else {
          const avg = Math.round(allLat.reduce((a, b) => a + b, 0) / allLat.length);
          const max = Math.round(Math.max(...allLat));
          const q   = avg < 80 ? DiagResult.OK : avg < 300 ? DiagResult.WARNING : DiagResult.ERROR;
          layer3Result = {
            layer: 3, name: 'Data channel (latenza)', result: q,
            message: q === DiagResult.OK
              ? `✅ Latenza ottima: media ${avg}ms, max ${max}ms`
              : q === DiagResult.WARNING
              ? `⚠️ Latenza elevata: media ${avg}ms (possibile relay TURN)`
              : `❌ Latenza critica: media ${avg}ms — connessione degradata`,
            avgLatencyMs: avg, maxLatencyMs: max, samplesCount: allLat.length,
          };
        }
        onProgress?.({ phase: 'ping', status: layer3Result.result, message: layer3Result.message });
        finish();
      }, CONFIG.TIMEOUT_PING);
    });

    // Rispondo ai ping in arrivo (ruolo server)
    getPing(({ seq }, peerId) => sendPong({ seq }, peerId));

    // Ricevo pong e calcolo RTT
    getPong(({ seq }) => {
      const entry = pendingPings[seq];
      if (!entry) return;
      const rtt = now() - entry.sentAt;
      peers[entry.peerId]?.latencies.push(rtt);
      delete pendingPings[seq];
      onProgress?.({ phase: 'ping', status: 'sample',
        message: `  ping #${seq}: ${Math.round(rtt)}ms RTT`, seq, rttMs: Math.round(rtt) });
    });

    room.onPeerLeave(peerId => { delete peers[peerId]; });
  });
}

// ─── API pubblica ──────────────────────────────────────────────────────────────

export async function runDiagnostics({ onProgress } = {}) {
  const startedAt = new Date().toISOString();
  onProgress?.({ phase: 'start', message: '🔍 Avvio diagnostica P2P...' });

  const layer1 = await testTrackers(onProgress);
  if (layer1.result === DiagResult.ERROR) {
    const report = buildReport(startedAt, layer1, null, null);
    onProgress?.({ phase: 'done', report });
    return report;
  }

  const { layer2, layer3 } = await testPeerConnection(onProgress);
  const report = buildReport(startedAt, layer1, layer2, layer3);
  onProgress?.({ phase: 'done', report });
  return report;
}

function buildReport(startedAt, layer1, layer2, layer3) {
  const layers = [layer1, layer2, layer3].filter(Boolean);
  const worst  = layers.some(l => l.result === DiagResult.ERROR)   ? DiagResult.ERROR
               : layers.some(l => l.result === DiagResult.WARNING) ? DiagResult.WARNING
               : DiagResult.OK;
  return {
    startedAt,
    summary: worst === DiagResult.OK      ? '✅ Connessione P2P funzionante'
            : worst === DiagResult.WARNING ? '⚠️ P2P parzialmente degradato'
            :                               '❌ Connessione P2P non funzionante',
    overallResult: worst,
    layers,
  };
}

if (typeof window !== 'undefined') window.p2pDiag = { runDiagnostics };
Condividi: