Skip to content

Shell 端橋接

運營商這一側沒有客戶端 SDK——請直接針對下方文件化的訊息契約自行實作監聽器。

訊息契約

每則訊息都包在一個小 envelope 裡,這樣橋接流量才能和頁面上其他不相關的 postMessage 活動區分開:

ts
type Envelope<T> = { source: 'moose-platform-game-bridge', payload: T }

入站(遊戲 → shell),payload 是以下其中一種:

類型欄位
GAME_LOADEDbalanceMinor
BET_STARTamountMinorbalanceMinor
BET_ENDoutcome: 'win' | 'loss' | 'declined' | 'rolled_back'amountMinorbalanceMinorwinAmountMinor?(僅當 outcome === 'win' 時存在)
BALANCE_EXHAUSTED
EXIT_GAME
SESSION_REVOKEDreason?

目前沒有出站(shell → 遊戲)方向——這個橋接只承載遊戲上報給你 shell 的訊息。balanceMinor 在上面每個帶有此欄位的事件中都是必填的,不只是 BET_ENDGAME_LOADED 讓你能在遊戲一開啟時就顯示玩家的餘額,而不是要等到他第一次下注結算後才顯示。

BET_START/BET_END兩階段的結果揭曉,而不是兩個獨立的網路時刻——遊戲通常會在一次伺服端呼叫裡結算整局(包含任何派彩),所以並不存在一個可回報的「局中」檢查點。BET_START 會在這局結果已經在伺服端確定、遊戲即將開始揭曉它(例如播放輪帶/連消動畫)的那一刻觸發——它的 balanceMinor 是這筆下注「扣款之後」、但「派彩套用之前」的餘額,讓你能立刻顯示扣款,而不會因為提早揭露派彩而破壞動畫的效果。BET_END 則在揭曉動畫結束後觸發,帶著最終、已完全結算的餘額——兩者搭配,你就能為每一局核對 BET_START 時的餘額 ± 派彩 = BET_END 時的餘額被拒絕的下注會完全跳過 BET_START(因為根本沒有下注成功),直接以 outcome: 'declined' 觸發 BET_END。遊戲客戶端是從它自己對平台的錢包呼叫裡得知這個餘額的,而那個呼叫的結果又來自你的錢包回調 API的回應;除了這些事件之外,沒有另一條會把餘額推送給你 shell 的管道。

SESSION_REVOKEDEXIT_GAME 不同:玩家並非主動選擇離開——要嘛是你在遊戲仍開啟時透過 POST /v1/operator/players/kick 結束了他的 session,要嘛是平台自己的風控在伺服端這麼做了,而遊戲是透過自己的方式得知這件事的(下一次錢包呼叫收到 401,或者——若廠商有自建的伺服端推送通道——透過該通道)。reason 是遊戲客戶端自行選擇上報的內容,並不是你在踢出呼叫時提供的值——如果有值就顯示它,但不要依賴某個特定的值。

範例

自包含——不需要引用任何套件:

ts
const BRIDGE_SOURCE = 'moose-platform-game-bridge'

function isBridgeEnvelope(data: unknown): data is { source: string; payload: unknown } {
  return (
    typeof data === 'object' &&
    data !== null &&
    (data as { source?: unknown }).source === BRIDGE_SOURCE
  )
}

const gameOrigin = 'https://games.example' // 廠商精確的 origin —— 絕不能是 "*"
const iframe = document.querySelector('iframe')!

window.addEventListener('message', (event) => {
  if (event.origin !== gameOrigin) return
  if (event.source !== iframe.contentWindow) return
  if (!isBridgeEnvelope(event.data)) return

  const gameEvent = event.data.payload as {
    type: 'GAME_LOADED' | 'BET_START' | 'BET_END' | 'BALANCE_EXHAUSTED' | 'EXIT_GAME' | 'SESSION_REVOKED'
    amountMinor?: number
    balanceMinor?: number
    winAmountMinor?: number
    outcome?: 'win' | 'loss' | 'declined' | 'rolled_back'
    reason?: string
  }
  switch (gameEvent.type) {
    case 'GAME_LOADED':
      // gameEvent.balanceMinor —— 立即顯示它,不用等到第一次 BET_END
      // 隱藏你自己的載入動畫
      break
    case 'BET_START':
      // gameEvent.amountMinor, gameEvent.balanceMinor(這筆下注扣款後、派彩套用前的餘額)
      // 此時結果已在伺服端確定——這只是標記「揭曉動畫即將開始」,供你自己的 UI 使用
      break
    case 'BET_END':
      // gameEvent.outcome, gameEvent.amountMinor, gameEvent.balanceMinor, gameEvent.winAmountMinor
      // 更新任何你在 iframe 之外渲染的餘額顯示
      break
    case 'BALANCE_EXHAUSTED':
      // 顯示充值提示
      break
    case 'EXIT_GAME':
      // 移除/隱藏 iframe,展示你的大廳
      // 只有在啟動時沒有傳 lobbyUrl 才會觸發這個事件——見 launching-games.md
      // 的「返回大廳」。傳了 lobbyUrl 的話,遊戲會直接導向最上層視窗到那裡,
      // 而不會送出這則訊息。
      break
    case 'SESSION_REVOKED':
      // 移除/隱藏 iframe,顯示「你已被登出」(gameEvent.reason)
      break
  }
})

每則入站訊息在被信任之前都會核對三件事:精確的 gameOriginevent.source 是否確實是你繫結的那個 iframe,以及橋接專屬的 envelope 標記——這樣頁面上其他不相關的 postMessage 流量(瀏覽器擴充套件、其他嵌入內容)才會被忽略,而不是被誤處理。