ERC7824
Trivia Royale GuidePatterns & Examples

Complete Game Walkthrough

Full Trivia Royale implementation showing all patterns working together

Complete Game: Trivia Royale

This is a complete multiplayer trivia game demonstrating every major pattern in Yellow SDK. Three players compete in a quiz game with instant messaging, balance verification, and prize distribution.

Game Overview

Players: 3 players + 1 server (facilitator) Entry Fee: 0.01 USDC per player (0.03 total) Rounds: 3 trivia questions Scoring: First correct answer wins the round Prizes: 1st: 50%, 2nd: 30%, 3rd: 20%

Architecture

┌────────────┐  ┌────────────┐  ┌────────────┐
│  Player 1  │  │  Player 2  │  │  Player 3  │
│   Client   │  │   Client   │  │   Client   │
└─────┬──────┘  └─────┬──────┘  └─────┬──────┘
      │                │                │
      └────────────────┼────────────────┘

                  WebSocket

               ┌───────▼────────┐
               │   ClearNode    │
               │   (Broker)     │
               └───────┬────────┘

                ┌──────▼───────┐
                │    Server    │
                │ (Game Logic) │
                └──────────────┘

Complete Code

Message Schema

interface TriviaGameSchema extends MessageSchema {
  game_start: {
    data: { totalRounds: number; entryFee: string };
  };
  question: {
    data: { text: string; round: number };
  };
  answer: {
    data: {
      answer: string;
      round: number;
      from: Address;
      timestamp: number;
    };
  };
  round_result: {
    data: {
      winner: Address;
      correctAnswer: string;
      round: number;
    };
  };
  game_over: {
    data: {
      finalWinner: Address;
      scores: Record<string, number>;
    };
  };
}

Server Implementation

async function runTriviaGame() {
  const player1 = wallets.test34;
  const player2 = wallets.test35;
  const player3 = wallets.test36;
  const server = wallets.server;

  const ENTRY_FEE = '0.01';
  const QUESTIONS = [
    { question: 'What is 2+2?', answer: '4' },
    { question: 'What is the capital of France?', answer: 'Paris' },
    { question: 'Who created Bitcoin?', answer: 'Satoshi Nakamoto' },
  ];

  // Track game state
  const scores: Record<Address, number> = {
    [player1.address]: 0,
    [player2.address]: 0,
    [player3.address]: 0,
  };
  const answerSubmissions: Array<{
    round: number;
    from: Address;
    answer: string;
    timestamp: number;
  }> = [];

  // Create server client
  const serverClient = createBetterNitroliteClient<TriviaGameSchema>({
    wallet: server,
    onAppMessage: async (type, sessionId, data) => {
      // Server collects answers
      if (type === 'answer') {
        answerSubmissions.push(data);
      }
    }
  });

  // Create player clients with auto-response logic
  const client1 = createPlayerClient(player1, '4', 'London', 'Satoshi Nakamoto');
  const client2 = createPlayerClient(player2, '5', 'Paris', 'Satoshi Nakamoto');
  const client3 = createPlayerClient(player3, '4', 'Paris', 'Hal Finney');

  // Connect all clients
  await Promise.all([
    client1.connect(),
    client2.connect(),
    client3.connect(),
    serverClient.connect(),
  ]);

  // Record initial balances
  const p1Before = await client1.getBalances();
  const p2Before = await client2.getBalances();
  const p3Before = await client3.getBalances();

  console.log('💰 Initial Ledger Balances:');
  console.log(`  Player 1: ${formatUSDC(p1Before.ledger)}`);
  console.log(`  Player 2: ${formatUSDC(p2Before.ledger)}`);
  console.log(`  Player 3: ${formatUSDC(p3Before.ledger)}`);

  // Distributed session creation
  console.log('\n🔐 Creating session with distributed signatures...');

  const sessionRequest = serverClient.prepareSession({
    participants: [player1.address, player2.address, player3.address, server.address],
    allocations: [
      { participant: player1.address, asset: 'USDC', amount: ENTRY_FEE },
      { participant: player2.address, asset: 'USDC', amount: ENTRY_FEE },
      { participant: player3.address, asset: 'USDC', amount: ENTRY_FEE },
      { participant: server.address, asset: 'USDC', amount: '0' },
    ],
  });

  const [sig1, sig2, sig3, sigServer] = await Promise.all([
    client1.signSessionRequest(sessionRequest),
    client2.signSessionRequest(sessionRequest),
    client3.signSessionRequest(sessionRequest),
    serverClient.signSessionRequest(sessionRequest),
  ]);

  const sessionId = await serverClient.createSession(sessionRequest, [
    sigServer as `0x${string}`,
    sig1 as `0x${string}`,
    sig2 as `0x${string}`,
    sig3 as `0x${string}`
  ]);

  console.log(`  ✅ Session created: ${sessionId}\n`);

  // Start game
  await serverClient.sendMessage(sessionId, 'game_start', {
    totalRounds: 3,
    entryFee: ENTRY_FEE,
  });

  console.log('🎮 Game started!\n');

  // Play rounds
  for (let round = 1; round <= 3; round++) {
    const q = QUESTIONS[round - 1]!;

    console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
    console.log(`📝 ROUND ${round}: ${q.question}`);
    console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);

    answerSubmissions.length = 0;

    // Broadcast question
    await serverClient.sendMessage(sessionId, 'question', {
      text: q.question,
      round,
    });

    // Wait for answers
    await new Promise(resolve => setTimeout(resolve, 500));

    // Determine winner (fastest correct answer)
    const correctAnswers = answerSubmissions
      .filter(a => a.round === round && a.answer === q.answer)
      .sort((a, b) => a.timestamp - b.timestamp);

    const winner = correctAnswers[0]?.from;

    if (winner) {
      // Update score
      scores[winner]++;

      // Announce result
      await serverClient.sendMessage(sessionId, 'round_result', {
        winner,
        correctAnswer: q.answer,
        round,
      });

      await new Promise(resolve => setTimeout(resolve, 100));

      const winnerName = getPlayerName(winner, [player1, player2, player3]);
      console.log(`\n🏆 Winner: ${winnerName}\n`);
    }
  }

  // Final results
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
  console.log('📊 Final Results:\n');

  const sortedScores = Object.entries(scores).sort(([, a], [, b]) => b - a);

  sortedScores.forEach(([addr, score], idx) => {
    const name = getPlayerName(addr as Address, [player1, player2, player3]);
    const medal = idx === 0 ? '🥇' : idx === 1 ? '🥈' : '🥉';
    console.log(`  ${medal} ${name}: ${score} wins`);
  });

  const finalWinner = sortedScores[0]![0] as Address;

  await serverClient.sendMessage(sessionId, 'game_over', {
    finalWinner,
    scores: Object.fromEntries(sortedScores),
  });

  // Prize distribution (50%, 30%, 20%)
  const totalPot = parseUSDC(ENTRY_FEE) * 3n;
  const prizes = {
    first: (totalPot * 50n) / 100n,
    second: (totalPot * 30n) / 100n,
    third: (totalPot * 20n) / 100n,
  };

  const finalAllocations = [
    { participant: sortedScores[0]![0] as Address, asset: 'USDC', amount: formatUSDC(prizes.first) },
    { participant: sortedScores[1]![0] as Address, asset: 'USDC', amount: formatUSDC(prizes.second) },
    { participant: sortedScores[2]![0] as Address, asset: 'USDC', amount: formatUSDC(prizes.third) },
    { participant: server.address, asset: 'USDC', amount: '0' },
  ];

  console.log('\n🏆 Prize Distribution:');
  console.log(`  🥇 1st: ${formatUSDC(prizes.first)} USDC`);
  console.log(`  🥈 2nd: ${formatUSDC(prizes.second)} USDC`);
  console.log(`  🥉 3rd: ${formatUSDC(prizes.third)} USDC\n`);

  // Close session
  await serverClient.closeSession(sessionId, finalAllocations);
  console.log('✅ Session closed\n');

  // Verify fund conservation
  await new Promise(resolve => setTimeout(resolve, 500));

  const p1After = await client1.getBalances();
  const p2After = await client2.getBalances();
  const p3After = await client3.getBalances();

  const changes = {
    [player1.address]: p1After.ledger - p1Before.ledger,
    [player2.address]: p2After.ledger - p2Before.ledger,
    [player3.address]: p3After.ledger - p3Before.ledger,
  };

  console.log('💸 Ledger Balance Changes:\n');
  Object.entries(changes).forEach(([addr, change]) => {
    const name = getPlayerName(addr as Address, [player1, player2, player3]);
    const sign = change! >= 0n ? '+' : '';
    console.log(`  ${name}: ${sign}${formatUSDC(change!)} USDC`);
  });

  const totalChange = changes[player1.address]! + changes[player2.address]! + changes[player3.address]!;
  console.log(`\n  ✅ Fund conservation: ${totalChange === 0n ? 'PASS' : 'FAIL'}\n`);

  // Disconnect
  await Promise.all([
    client1.disconnect(),
    client2.disconnect(),
    client3.disconnect(),
    serverClient.disconnect(),
  ]);

  console.log('🎉 Game complete!\n');
}

// Helper: Create player client with auto-response
function createPlayerClient(
  wallet: Wallet,
  answer1: string,
  answer2: string,
  answer3: string
) {
  const answers = [answer1, answer2, answer3];

  return createBetterNitroliteClient<TriviaGameSchema>({
    wallet,
    sessionAllowance: '0.01',
    onAppMessage: async (type, sessionId, data) => {
      if (type === 'question') {
        const answer = answers[data.round - 1]!;

        setTimeout(async () => {
          await client.sendMessage(sessionId, 'answer', {
            answer,
            round: data.round,
            from: wallet.address,
            timestamp: Date.now(),
          });
        }, Math.random() * 200); // Random delay 0-200ms
      }
    },
  });
}

// Helper: Get player name for display
function getPlayerName(address: Address, players: Wallet[]): string {
  const index = players.findIndex(p => p.address === address);
  return index >= 0 ? `Player ${index + 1}` : 'Unknown';
}

// Helper: Format USDC with decimals
function formatUSDC(amount: bigint): string {
  const decimals = 6n;
  const whole = amount / (10n ** decimals);
  const fraction = amount % (10n ** decimals);
  return `${whole}.${fraction.toString().padStart(Number(decimals), '0')}`;
}

// Helper: Parse USDC string to bigint
function parseUSDC(amount: string): bigint {
  const [whole = '0', fraction = '0'] = amount.split('.');
  const paddedFraction = fraction.padEnd(6, '0').slice(0, 6);
  return BigInt(whole) * 1000000n + BigInt(paddedFraction);
}

📁 Full Implementation: View the complete working code:

Key files:

Key Patterns Demonstrated

1. Distributed Session Creation

// Prepare → Sign → Collect → Create
const request = server.prepareSession({ ... });
const signatures = await collectAllSignatures(request);
const sessionId = await server.createSession(request, signatures);

2. Typed Message Broadcasting

// Server broadcasts question
await server.sendMessage(sessionId, 'question', { text, round });

// All players receive and auto-respond
onAppMessage: (type, sessionId, data) => {
  if (type === 'question') {
    sendMessage(sessionId, 'answer', { ... });
  }
}

3. Balance Verification

// Record before
const before = await client.getBalances();

// ... play game ...

// Verify after
const after = await client.getBalances();
const change = after.ledger - before.ledger;

// Fund conservation check
const totalChange = change1 + change2 + change3;
assert(totalChange === 0n); // Must sum to zero!

4. Prize Distribution

// Close session with final allocations
await server.closeSession(sessionId, [
  { participant: winner, amount: '0.015' },   // 50%
  { participant: second, amount: '0.009' },   // 30%
  { participant: third, amount: '0.006' },    // 20%
]);

// Funds automatically return to ledger balances

Next Steps