真實局面留在伺服器端

迷霧國際象棋給國際象棋增加了一條隱藏資訊規則:每一方只能看到己方棋子可及的格子。實作問題在於這條規則在哪裡執行。在 Mistboard 上,它執行在伺服器端,所以瀏覽器收到的是一個 PlayerView,而不是蓋著迷霧圖層的完整棋盤。

這組三聯棋盤就是整個架構的縮影。中央棋盤只存在於伺服器上。白方與黑方各自收到不同的投影檢視,任何一份投影都不是用視覺圖層遮住完整真實局面。

規則很簡單:只計算一次真實狀態,再為每個席位投影其獲准看到的檢視,並在對局結束前將完整事件紀錄保持私密。

這一條邊界同時支援即時玩家對戰、引擎對局、校準、賽事和複盤。本文聚焦面向玩家的即時房間:每個瀏覽器會收到什麼、誰可以接收,以及對局紀錄何時公開。

檢視如何計算

對玩家而言,這條邊界就是 PlayerView:該席位可見的格子、可見棋子、合法著法、狀態和時鐘。可見範圍之外的對方棋子不是被藏在欄位裡,而是根本不存在於資料中。

// packages/game/src/variants.ts (condensed)

// 1. Which squares can this player see?
export function fogVisibleSquares(state, player) {
  // every square one of your own pieces stands on...
  const visible = new Set(ownPieceSquares(state.board, player));
  // ...plus every square one of your pieces could move to or capture on
  for (const move of getVisibilityMoves(state, player)) visible.add(move.to);
  return [...visible].sort();
}

// 2. Keep only the pieces standing on those squares.
export function boardVisibleTo(board, visibleSquares) {
  const visible = new Set(visibleSquares);
  const playerBoard = {};
  for (const [square, piece] of Object.entries(board))
    if (piece && visible.has(square)) playerBoard[square] = piece;
  return playerBoard;
}

// 3. Assemble the view that gets sent.
getPlayerView(state, player) {
  const visibleSquares = fogVisibleSquares(state, player);
  const board = boardVisibleTo(state.board, visibleSquares);
  return {
    board,            // only the pieces kept by step 2
    visibleSquares,   // step 1: which squares render clear vs. fogged
    legalMoves: yourTurn(state, player) ? getFogMovesForPlayer(state, player) : [],
    status, perspective: player, moveNumber, clock,
    lastMove,         // your own last move; the opponent's is stripped
  };
}

關鍵在於相依方向。用戶端因為收到可見性遮罩而能夠渲染迷霧,卻無法透過移除迷霧來恢復從未傳送給它的棋子。

範例資料負載

即時走子串流使用 event-appended,每步傳送一個訊框。下面是上方局面中傳給白方的負載,已縮減為關鍵欄位:

{
  "type": "event-appended",
  "roomId": "mb-demo-room-001",
  "seat": "white",
  "seq": 6,
  "state": {
    "board": {
      "a1": { "color": "white", "role": "rook" },
      "e4": { "color": "white", "role": "pawn" },
      "e5": { "color": "black", "role": "pawn" },
      "f7": { "color": "black", "role": "pawn" }
    },
    "visibleSquares": ["a1", "a2", "a3", "..."],
    "legalMoves": [{ "from": "b1", "to": "a3" }, "..."],
    "status": { "type": "playing", "turn": "white" },
    "perspective": "white",
    "clock": { "...": "current clock state" }
  }
}

核心欄位:seat 標識接收者,seq 為資料流排序,state.board 是去識別化後的棋盤,state.visibleSquares 是清晰區域與迷霧區域的遮罩,state.status 攜帶規範的輪次與結果狀態。

如果新增事件對該席位可見,這個訊框會包含一個經過過濾的 event。如果走子被隱藏,event 會被省略,但投影後的 state 仍會推進。玩家知道一回合已經發生,卻不知道迷霧中發生了什麼。

首次連線、明確復原和最終重新同步仍會使用快照。快照攜帶用戶端初始化所需的過濾事件歷程,因此比逐步訊框更大。

玩家走子

走子請求只有座標:

// client -> server, sent on player's move
{ type: 'move', from: 'e2', to: 'e4' }

伺服器根據規範狀態驗證請求,執行走子,附加事件,再投影下一份檢視。用戶端永遠不負責判斷是否存在隱藏資訊、是否發生了不可見走子,或對局是否結束。

按席位控制的即時房間

即時對局期間,伺服器只向兩個對局席位傳送遊戲資料。每步之後,它分別為白方和黑方投影一份檢視,再將每份檢視只傳送給已經證明自己控制該席位的通訊端。

// live room gate (condensed)
export const seat = verifySeatClaim(socket, room);

if (!seat) {
  closeSocket(1008, 'private room');
  return;
}

send(projectPlayerView(room.gameState, seat));

席位證明

通訊端只有在證明自己控制白方或黑方席位後,才能取得即時房間資料。匿名席位使用隨機持有人權杖;伺服器保存 SHA-256 權杖雜湊,並以固定時間比較提交的權杖。

帳號席位

已登入席位會在席位聲明之上增加帳號工作階段檢查。權杖證明該瀏覽器可以取回席位;工作階段則證明帳號仍與席位分配相符。

沒有即時觀戰檢視

非對局玩家不會取得即時觀戰投影。沒有有效席位的通訊端會在房間資料傳送前被拒絕,而即時回放端點會一直回傳 403,直到對局進入終局狀態。

賽後複盤

對局進入終局狀態後,隱私規則隨之改變。結果產生後,房間不再拒絕非對局玩家,遊戲頁面則成為持久的公開複盤介面。

GET /room/abc123          active game, seat token required
GET /api/games/abc123/events  active game, 403
GET /game/abc123          finished game, public review
GET /room/abc123          finished game, opens without a seat

觀眾在對局進行時打開房間,看不到棋盤。結果產生後,同一個人可以打開已結束的遊戲頁面並檢查事件紀錄。這就是產品規則:決策仍在進行時保持私密,紀錄確定後可以複盤。

這種區分對計分對局很重要。計分結果可以指向一盤公開的已完成對局,同時不讓非對局玩家接觸即時隱藏資訊。

它也讓重新連線與複盤建立在同一基礎上。即時重新連線從事件紀錄重建過濾後的玩家檢視;隱藏資訊限制失效後,賽後複盤使用同一份紀錄。

範圍與驗證

這並不是一項完整的反作弊聲明,而是該架構能夠證明的、更具體的完整性保證:即時對局期間,隱藏真實狀態不會傳送到未經授權的瀏覽器路徑;對局結束後,紀錄可供複盤。

匿名休閒席位依靠持有人權杖,並不具備帳號級身分保證;隱藏資訊遊戲也沒有即時觀戰模式。

Mistboard 用 WebSocket 與負載迴歸測試覆蓋這條邊界:測試會執行真實走子,並斷言每個席位實際收到的位元組。

這就是 Mistboard 守住的界線:對局期間,瀏覽器端沒有可以揭開的真實局面;對局結束後,則有公開紀錄可供檢查。

接下來去哪

來玩 Misty 的迷霧國際象棋,或閱讀面向玩家的規則文章,了解同一套視野模型。

對戰 Misty閱讀迷霧國際象棋規則全部文章