logo

Babylon.js Market

By Lawrence

5 minutes

Lesson 7 put a remote fold on the bus. Now a seat's intent reaches every screen. But press deal in two tabs and two different decks fall. Four browsers deal four different tables. And even if they shared one deck, there's still a problem. The seat-2 player would see seat 0's hole cards at the bottom of their own screen. There are two problems here, and one idea fixes both. The deck is never sent. Instead, every peer computes the same deck on its own. Then each peer relabels the same deal into its own point of view.

The deck dealer already sits in the running scene. It dealt every single-player hand. Two new fields join its block now. One is the seat this browser owns. The other is the table's occupied seats:

"Dealer": {
  "components": {
    "DeckDealer": {
      "playerCount": 4,
      "cardsPerPlayer": 2,
      "riverSize": 5,
      "localIndex": 0,
      "roomCode": "table-7",
      "handNumber": 0,
      "mySeat": 0,
      "occupiedSeats": [0, 1, 2, 3]
    },
    "PokerSeating": { "cardsPerPlayer": 2, "riverSize": 5, "npcCount": 3 }
  }
}

mySeat and occupiedSeats are the two new lines. Part 2 uses them to set each player's point of view. roomCode is the shared input that makes Part 1 work. Here it is "table-7", a placeholder. The next lesson swaps it for the server's secret seed. Feed two browsers the same code and they shuffle the identical deck.

How every browser builds the same deck

If both ends can rebuild the deck, none of it has to cross the wire. The dealer seeds its shuffle from createGameRng(roomCode, handNumber). Same code, same hand index. So Fisher-Yates walks the identical swaps on every peer. The only catch is the next hand. When the table reshuffles, every peer has to land on the same new deck. They must do it in lockstep, without comparing notes.

That's what the reset wire is for. The director owns the hand counter. So it tells the dealer which hand to seed. Here it is in PokerTableDirector.ts:

src/components/PokerTableDirector/PokerTableDirector.ts
    // Reseed the deck for this hand. The deck's hand index is 0-based — the first
    // hand is deck hand 0, matching a scene's `handNumber: 0` — while rt.handNumber
    // counts hands from 1 for HAND_STARTED, so pass rt.handNumber - 1. In
    // multiplayer every peer reshuffles to createGameRng(roomCode, handIndex): the
    // identical deck. Single-player (empty roomCode) ignores it (Math.random), so a
    // fresh deck per hand either way.
    this.eventBus.emit(DeckDealerInputEvents.RESET_REQUEST, { handNumber: rt.handNumber - 1 });
    this.eventBus.emit(DeckDealerInputEvents.DEAL_REQUEST, { all: true });

rt.handNumber counts hands from 1 for the HUD. But the deck's hand index starts at 0. So the director passes rt.handNumber - 1. On the dealer's side, handleReset in DeckDealer.ts takes that number before it reshuffles:

src/components/DeckDealer/DeckDealer.ts
  private handleReset(e: DeckResetRequest = {}): void {
    const comp = this.getPrimary();
    if (!comp?.deck || !comp.table) return;
    // Multiplayer: adopt the director's per-hand number so the reshuffle seeds to
    // createGameRng(roomCode, handNumber) — identical on every peer. Single-player
    // omits it, so the component's own handNumber (and Math.random fallback) stand.
    if (typeof e.handNumber === 'number') comp.handNumber = e.handNumber;
    resetDeck(comp.deck, this.rngFor(comp));
    resetTable(comp.table);
    this.eventBus.emit(DeckDealerEvents.RESET, {
      remaining: cardsRemaining(comp.deck),
    } satisfies DeckResetPayload);
  }

The carried handNumber overwrites the component's own. Then resetDeck reshuffles through rngFor. That's the one line that turns a seed into a stream of random numbers:

src/components/DeckDealer/DeckDealer.ts
  private rngFor(comp: DeckDealerComponent): (() => number) | undefined {
    return comp.roomCode ? createGameRng(comp.roomCode, comp.handNumber) : undefined;
  }

A non-empty roomCode opts into the seeded shuffle. An empty one falls back to Math.random. That's single-player, where no two browsers need to agree. So on the first hand every peer runs the same trace:

// hand 1: the director emits deck.resetRequest with handNumber 0
// handleReset sets comp.handNumber = 0, then calls rngFor(comp):
rngFor(comp); // → createGameRng('table-7', 0)
// every peer holding roomCode 'table-7' now shuffles to the identical deck

No card id ever crosses the wire. Both sides already have the recipe to build the deck.

Continue reading

Unlock the Full Course

Every lesson, the runnable examples, and the finished build — yours to keep.

$9one-time

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search