如何用 Google Apps Script 製作跨瀏覽器個別登入的「玩數學99遊戲」?

 

如何用 Google Apps Script 製作跨瀏覽器個別登入的「玩數學99遊戲」?

在本篇文章中,我們將示範如何運用 Google Apps Script,設計一個多人共用狀態的遊戲系統,讓玩家分別以個別身份登入,並能在不同瀏覽器中即時同步遊戲狀態。這個範例以經典的「玩數學99遊戲」為例,實作重點包含以下功能:

  • 個別身份登入:玩家可從不同瀏覽器中選擇自己要扮演的角色(玩家1或玩家2),避免重複登入同一角色。
  • 共用遊戲狀態管理:所有玩家共用同一遊戲狀態,利用 Google Apps Script 的 PropertiesService 進行儲存與更新。
  • 即時輪詢同步:透過前端定時輪詢(polling)的方式,讓不同瀏覽器的遊戲畫面保持最新狀態。
  • 回合出招檢查:只有輪到自己的玩家才可以進行數字出招,並檢查輸入合法性及是否獲勝。

本文將分為兩大部分說明:

  1. 伺服端程式(Code.gs)
  2. 前端頁面(Index.html)

一、伺服端程式設計 (Code.gs)

在伺服端部分,我們主要負責管理遊戲的共用狀態。利用 PropertiesService 儲存 JSON 格式的遊戲狀態,並提供以下函式:

  • loginAsRole(playerName, role)
    玩家依據自己選擇的角色進行登入,若該角色尚未被佔用,則將玩家姓名寫入遊戲狀態。

  • getGameState() 與 saveGameState(state)
    用來讀取與儲存目前的遊戲狀態,確保所有玩家都能存取同一份資料。

  • processMoveShared(playerRole, moveValue)
    檢查傳入的玩家身份是否符合目前回合,並驗證數字輸入後更新遊戲狀態(包含累加總和、切換回合或設定獲勝者)。

以下是完整程式碼範例:


// Code.gs function doGet(e) { } // 取得目前遊戲狀態(從 Script Properties 取得) function getGameState() { } // 儲存遊戲狀態 function saveGameState(state) { } // 玩家以個別身份登入(可初始化或更新對應角色) function loginAsRole(playerName, role) { var state = getGameState(); if (!state) { // 尚未有遊戲狀態,根據所選角色初始化(另一角色先留空) if (role === "player1") { state = { player1: playerName, player2: "", currentTotal: 0, turn: 0, // 預設玩家1先行 winner: null, lastMoveTime: new Date().getTime() }; } else if (role === "player2") { state = { player1: "", player2: playerName, currentTotal: 0, turn: 0, winner: null, lastMoveTime: new Date().getTime() }; } } else { // 遊戲狀態已存在,更新尚未設定的角色 if (role === "player1") { if (state.player1) return { success: false, message: "玩家1已登入" }; state.player1 = playerName; } else if (role === "player2") { if (state.player2) return { success: false, message: "玩家2已登入" }; state.player2 = playerName; } } saveGameState(state); return { success: true, state: state, myRole: role }; } // 更新遊戲狀態(包含總和、回合切換與獲勝者設定) function updateGameState(newTotal, win) { var state = getGameState(); if (!state) return { success: false, message: "遊戲狀態不存在" }; state.currentTotal = newTotal; if (win) { state.winner = (state.turn === 0) ? state.player1 : state.player2; } else { state.turn = (state.turn + 1) % 2; } state.lastMoveTime = new Date().getTime(); saveGameState(state); return state; } // 處理玩家出招(須傳入玩家身份以確認是否為正確回合) function processMoveShared(playerRole, moveValue) { var state = getGameState(); if (!state) return { success: false, message: "遊戲狀態不存在" }; // 確認是否為該玩家的回合 if ((state.turn === 0 && playerRole !== "player1") || (state.turn === 1 && playerRole !== "player2")) { return { success: false, message: "目前不是你的回合!" }; } var move = parseInt(moveValue, 10); if (isNaN(move) || move < 1 || move > 10) { return { success: false, message: "請輸入1到10之間的數字" }; } var newTotal = state.currentTotal + move; if (newTotal > 99) { return { success: false, message: "總和超過99,無效的出招!" }; } else if (newTotal === 99) { var updatedState = updateGameState(newTotal, true); return { success: true, state: updatedState, win: true, message: "恭喜,達到99,獲勝!" }; } var updatedState = updateGameState(newTotal, false); return { success: true, state: updatedState, win: false, message: "目前總和為:" + newTotal }; }

重點提示:
此範例以 PropertiesService 儲存遊戲狀態,適合用於小型多人遊戲。但若未來遊戲玩家數量增加或需支援更多房間,可考慮採用 Google Spreadsheet 或其他資料庫進行狀態管理。


二、前端頁面設計 (Index.html)

前端部分主要負責呈現使用者介面與即時更新的機制。主要功能包含:

  • 個別登入表單
    玩家選擇身份(玩家1 或 玩家2)並輸入姓名,系統透過 loginAsRole 將資訊傳到伺服端。

  • 遊戲狀態顯示
    顯示目前累加總和、輪到哪位玩家以及已登入的玩家資訊。

  • 出招輸入與回合控制
    當輪到自己的回合時,啟用數字輸入與出招按鈕;若非己方回合則停用,並顯示等待訊息。

  • 定時輪詢同步
    使用 setInterval 每隔數秒呼叫伺服端取得最新狀態,讓所有玩家的畫面即時更新。

完整的前端程式碼如下:


<!DOCTYPE html> <html> <head> <base target="_top"> <style> .hidden { display: none; } .disabled { background-color: #eee; } </style> </head> <body> <!-- 登入區:玩家選擇身份及輸入姓名 --> <div id="loginSection"> <h2>個別登入</h2> <label>選擇身份:</label> <select id="roleSelect"> <option value="player1">玩家1</option> <option value="player2">玩家2</option> </select> <br><br> <label for="playerName">姓名:</label> <input type="text" id="playerName" placeholder="請輸入姓名"> <br><br> <button onclick="login()">登入</button> <p id="loginMsg" style="color:red;"></p> </div> <!-- 遊戲區 --> <div id="gameSection" class="hidden"> <h2>玩數學99遊戲</h2> <p id="playersDisplay"></p> <p>目前總和:<span id="currentTotal">0</span></p> <p id="turnDisplay"></p> <label for="move">請輸入1到10之間的數字:</label> <input type="number" id="move" min="1" max="10"> <button id="moveBtn" onclick="makeMove()">出招</button> <p id="message"></p> </div> <script> var myRole = ""; // 紀錄該瀏覽器的玩家身份 var currentState = {}; // 目前遊戲狀態 // 登入函式:傳入姓名與選擇的角色 function login() { } google.script.run.withSuccessHandler(function(response) { if (!response.success) { document.getElementById("loginMsg").innerText = response.message; } else { currentState = response.state; // 顯示玩家資訊 document.getElementById("playersDisplay").innerText = "玩家1:" + (currentState.player1 || "未登入") + " , 玩家2:" + (currentState.player2 || "未登入"); updateGameDisplay(); // 隱藏登入區,顯示遊戲區 document.getElementById("loginSection").classList.add("hidden"); document.getElementById("gameSection").classList.remove("hidden"); // 開始輪詢更新遊戲狀態 setInterval(pollGameState, 3000); } }).loginAsRole(playerName, myRole); } // 定時輪詢最新遊戲狀態 function pollGameState() { google.script.run.withSuccessHandler(function(state) { if (state) { currentState = state; updateGameDisplay(); } }).getGameState(); } // 根據最新狀態更新畫面,並依據是否輪到自己啟用/停用輸入欄位 function updateGameDisplay() { document.getElementById("currentTotal").innerText = currentState.currentTotal; var currentPlayer = (currentState.turn === 0) ? currentState.player1 : currentState.player2; document.getElementById("turnDisplay").innerText = "目前輪到:" + currentPlayer; document.getElementById("playersDisplay").innerText = "玩家1:" + (currentState.player1 || "未登入") + " , 玩家2:" + (currentState.player2 || "未登入"); // 若遊戲已分出勝負,顯示訊息並停用出招 if (currentState.winner) { document.getElementById("message").innerText = "遊戲結束,獲勝者:" + currentState.winner; document.getElementById("move").disabled = true; document.getElementById("moveBtn").disabled = true; } else { // 如果目前回合與自己身份相符,啟用輸入;否則停用 if ((currentState.turn === 0 && myRole === "player1") || (currentState.turn === 1 && myRole === "player2")) { document.getElementById("move").disabled = false; document.getElementById("moveBtn").disabled = false; document.getElementById("message").innerText = "輪到你出招"; } else { document.getElementById("move").disabled = true; document.getElementById("moveBtn").disabled = true; document.getElementById("message").innerText = "等待對方出招..."; } } } // 玩家出招:傳入自己的身份與輸入值 function makeMove() { var moveValue = document.getElementById("move").value; if (moveValue === "") { alert("請輸入數字"); return; } google.script.run.withSuccessHandler(function(result) { if (!result.success) { document.getElementById("message").innerText = result.message; } else { currentState = result.state; updateGameDisplay(); if (result.win) { alert("遊戲結束," + currentState.winner + " 獲勝!"); } } document.getElementById("move").value = ""; }).processMoveShared(myRole, moveValue); } </script> </body> </html>

小提醒:
前端透過 setInterval 每三秒呼叫一次 getGameState(),以確保各瀏覽器畫面能即時反映最新狀態。根據需求,您也可以調整輪詢頻率或考慮其他即時同步技術。


三、功能測試與後續發展

完成程式碼後,您可以在 Google Apps Script 編輯器中:

  1. 建立兩個檔案:Code.gsIndex.html,並貼上上述程式碼。
  2. 透過「發佈」>「部屬為網路應用程式」,設定權限並取得公開 URL。
  3. 利用不同瀏覽器或隱私視窗,同時以不同身份(玩家1 與 玩家2)登入,進行遊戲操作。

後續擴充方向:

  • 增加更多房間功能,讓多組玩家可同時進行遊戲。
  • 採用更即時的同步技術(例如 WebSocket 或 Firebase)改善輪詢方式帶來的延遲。
  • 加入更完善的驗證機制,確保玩家身份安全與遊戲記錄保存。

結論

透過這篇教學,我們學會如何運用 Google Apps Script 進行多人共用狀態的管理與即時同步,實作出可跨瀏覽器個別登入的「玩數學99遊戲」。這個範例不僅展現了基本的多人遊戲架構,也提供了後續擴充的方向。希望本篇文章能幫助您更進一步了解 Google Apps Script 的應用,也鼓勵您發揮創意設計更多有趣的互動系統!